# Copyright The IETF Trust 2009-2024, All Rights Reserved # -*- coding: utf-8 -*- import datetime import io import json import os import random import re import shutil import pytz import requests.exceptions import requests_mock from unittest import skipIf from mock import call, patch, PropertyMock from pyquery import PyQuery from lxml.etree import tostring from io import StringIO, BytesIO from bs4 import BeautifulSoup from urllib.parse import urlparse, urlsplit from PIL import Image from pathlib import Path from tempfile import NamedTemporaryFile from zoneinfo import ZoneInfo from django.urls import reverse as urlreverse from django.conf import settings from django.contrib.auth.models import User from django.core.serializers.json import DjangoJSONEncoder from django.test import Client, override_settings from django.db.models import F, Max from django.http import QueryDict, FileResponse from django.template import Context, Template from django.utils import timezone from django.utils.text import slugify import debug # pyflakes:ignore from ietf.doc.models import Document, NewRevisionDocEvent from ietf.group.models import Group, Role, GroupFeatures from ietf.group.utils import can_manage_group from ietf.person.models import Person from ietf.meeting.helpers import can_approve_interim_request, can_request_interim_meeting, can_view_interim_request, preprocess_assignments_for_agenda from ietf.meeting.helpers import send_interim_approval_request, AgendaKeywordTagger from ietf.meeting.helpers import send_interim_meeting_cancellation_notice, send_interim_session_cancellation_notice from ietf.meeting.helpers import send_interim_minutes_reminder, populate_important_dates, update_important_dates from ietf.meeting.models import Session, TimeSlot, Meeting, SchedTimeSessAssignment, Schedule, SessionPresentation, SlideSubmission, SchedulingEvent, Room, Constraint, ConstraintName from ietf.meeting.test_data import make_meeting_test_data, make_interim_meeting, make_interim_test_data from ietf.meeting.utils import condition_slide_order from ietf.meeting.utils import add_event_info_to_session_qs, participants_for_meeting from ietf.meeting.utils import create_recording, delete_recording, get_next_sequence, bluesheet_data from ietf.meeting.views import session_draft_list, parse_agenda_filter_params, sessions_post_save, agenda_extract_schedule from ietf.meeting.views import get_summary_by_area, get_summary_by_type, get_summary_by_purpose, generate_agenda_data from ietf.name.models import SessionStatusName, ImportantDateName, RoleName, ProceedingsMaterialTypeName from ietf.utils.decorators import skip_coverage from ietf.utils.mail import outbox, empty_outbox, get_payload_text from ietf.utils.test_utils import TestCase, login_testing_unauthorized, unicontent from ietf.utils.timezone import date_today, time_now from ietf.person.factories import PersonFactory, PersonalApiKeyFactory from ietf.group.factories import GroupFactory, GroupEventFactory, RoleFactory from ietf.meeting.factories import (SessionFactory, ScheduleFactory, SessionPresentationFactory, MeetingFactory, FloorPlanFactory, TimeSlotFactory, SlideSubmissionFactory, RoomFactory, ConstraintFactory, MeetingHostFactory, ProceedingsMaterialFactory, AttendedFactory) from ietf.stats.factories import MeetingRegistrationFactory from ietf.doc.factories import DocumentFactory, WgDraftFactory from ietf.submit.tests import submission_file from ietf.utils.test_utils import assert_ical_response_is_valid if os.path.exists(settings.GHOSTSCRIPT_COMMAND): skip_pdf_tests = False skip_message = "" else: skip_pdf_tests = True skip_message = ("Skipping pdf test: The binary for ghostscript wasn't found in the\n " "location indicated in settings.py.") print(" "+skip_message) class BaseMeetingTestCase(TestCase): """Base class for meeting-related tests that need to set up temporary directories This creates temporary directories for meeting-related uploads, then updates settings to point to them. It also patches the Storage class to use the temporary directories. When done, removes its files, resets the settings, and shuts off the patched Storage. If subclasses have their own setUp/tearDown routines, they must remember to call the superclass methods. """ def setUp(self): super().setUp() self.materials_dir = self.tempdir('materials') self.storage_dir = self.tempdir('storage') # archive_dir = Path(settings.INTERNET_DRAFT_ARCHIVE_DIR) (archive_dir / "unknown_ids").mkdir() (archive_dir / "deleted_tombstones").mkdir() (archive_dir / "expired_without_tombstone").mkdir() # self.saved_agenda_path = settings.AGENDA_PATH self.saved_meetinghost_logo_path = settings.MEETINGHOST_LOGO_PATH # settings.AGENDA_PATH = self.materials_dir settings.MEETINGHOST_LOGO_PATH = self.storage_dir # The FileSystemStorage has already set its location before # the settings were changed. Mock the method it uses to get the # location and fill in our temporary location. Without this, test # files will upload to the locations specified in settings.py. # Note that this will affect any use of the storage class in # meeting.models - i.e., FloorPlan.image and MeetingHost.logo self.patcher = patch('ietf.meeting.models.NoLocationMigrationFileSystemStorage.base_location', new_callable=PropertyMock) mocked = self.patcher.start() mocked.return_value = self.storage_dir def tearDown(self): self.patcher.stop() # shutil.rmtree(self.storage_dir) shutil.rmtree(self.materials_dir) # settings.AGENDA_PATH = self.saved_agenda_path settings.MEETINGHOST_LOGO_PATH = self.saved_meetinghost_logo_path super().tearDown() def write_materials_file(self, meeting, doc, content, charset="utf-8", with_ext=None): if with_ext is None: filename = doc.uploaded_filename else: filename = Path(doc.uploaded_filename).with_suffix(with_ext) path = os.path.join(self.materials_dir, "%s/%s/%s" % (meeting.number, doc.type_id, filename)) dirname = os.path.dirname(path) if not os.path.exists(dirname): os.makedirs(dirname) if isinstance(content, str): content = content.encode(charset) with io.open(path, "wb") as f: f.write(content) def write_materials_files(self, meeting, session): draft = Document.objects.filter(type="draft", group=session.group).first() self.write_materials_file(meeting, session.materials.get(type="agenda"), "1. WG status (15 minutes)\n\n2. Status of %s\n\n" % draft.name) self.write_materials_file(meeting, session.materials.get(type="minutes"), "1. More work items underway\n\n2. The draft will be finished before next meeting\n\n") self.write_materials_file(meeting, session.materials.filter(type="slides").exclude(states__type__slug='slides',states__slug='deleted').first(), "This is a slideshow") class AgendaApiTests(TestCase): def test_agenda_extract_schedule_location(self): meeting = MeetingFactory(type_id='ietf') room = RoomFactory(meeting=meeting, floorplan=FloorPlanFactory(meeting=meeting)) hidden_ts = TimeSlotFactory(meeting=meeting, location=room, show_location=False) shown_ts = TimeSlotFactory(meeting=meeting, location=room, show_location=True) hidden_sess = SessionFactory(meeting=meeting, add_to_schedule=False) shown_sess = SessionFactory(meeting=meeting, add_to_schedule=False) meeting.schedule.assignments.create(timeslot=hidden_ts, session=hidden_sess) meeting.schedule.assignments.create(timeslot=shown_ts, session=shown_sess) processed = preprocess_assignments_for_agenda( SchedTimeSessAssignment.objects.filter(session__in=[hidden_sess, shown_sess]), meeting ) AgendaKeywordTagger(assignments=processed).apply() extracted = {item.session.pk: agenda_extract_schedule(item) for item in processed} hidden = extracted[hidden_sess.pk] self.assertIsNone(hidden['room']) self.assertEqual(hidden['location'], {}) shown = extracted[shown_sess.pk] self.assertEqual(shown['room'], room.name) self.assertEqual(shown['location'], {'name': room.floorplan.name, 'short': room.floorplan.short}) def test_agenda_extract_schedule_names(self): meeting = MeetingFactory(type_id='ietf') named_timeslots = TimeSlotFactory.create_batch(2, meeting=meeting, name='Timeslot Name') unnamed_timeslots = TimeSlotFactory.create_batch(2, meeting=meeting, name='') named_sessions = SessionFactory.create_batch(2, meeting=meeting, name='Session Name') unnamed_sessions = SessionFactory.create_batch(2, meeting=meeting, name='') pk_with = { 'both named': named_sessions[0].timeslotassignments.create( schedule=meeting.schedule, timeslot=named_timeslots[0], ).pk, 'session named': named_sessions[1].timeslotassignments.create( schedule=meeting.schedule, timeslot=unnamed_timeslots[0], ).pk, 'timeslot named': unnamed_sessions[0].timeslotassignments.create( schedule=meeting.schedule, timeslot=named_timeslots[1], ).pk, 'neither named': unnamed_sessions[1].timeslotassignments.create( schedule=meeting.schedule, timeslot=unnamed_timeslots[1], ).pk, } processed = preprocess_assignments_for_agenda(meeting.schedule.assignments.all(), meeting) AgendaKeywordTagger(assignments=processed).apply() extracted = {item.pk: agenda_extract_schedule(item) for item in processed} self.assertEqual(extracted[pk_with['both named']]['name'], 'Session Name') self.assertEqual(extracted[pk_with['both named']]['slotName'], 'Timeslot Name') self.assertEqual(extracted[pk_with['session named']]['name'], 'Session Name') self.assertEqual(extracted[pk_with['session named']]['slotName'], '') self.assertEqual(extracted[pk_with['timeslot named']]['name'], '') self.assertEqual(extracted[pk_with['timeslot named']]['slotName'], 'Timeslot Name') self.assertEqual(extracted[pk_with['neither named']]['name'], '') self.assertEqual(extracted[pk_with['neither named']]['slotName'], '') class MeetingTests(BaseMeetingTestCase): @override_settings( MEETECHO_ONSITE_TOOL_URL="https://onsite.example.com", MEETECHO_VIDEO_STREAM_URL="https://meetecho.example.com", ) def test_meeting_agenda(self): meeting = make_meeting_test_data() session = Session.objects.filter(meeting=meeting, group__acronym="mars").first() session.remote_instructions='https://remote.example.com' session.save() slot = TimeSlot.objects.get(sessionassignments__session=session,sessionassignments__schedule=meeting.schedule) meeting.timeslot_set.filter(type_id="break").update(show_location=False) # self.write_materials_files(meeting, session) # future_year = date_today().year+1 future_num = (future_year-1984)*3 # valid for the mid-year meeting future_meeting = Meeting.objects.create(date=datetime.date(future_year, 7, 22), number=future_num, type_id='ietf', city="Panama City", country="PA", time_zone='America/Panama') registration_text = "Registration" # Extremely rudementary test of agenda-neue - to be replaced with back-end tests as the front-end tests are developed. r = self.client.get(urlreverse("agenda", kwargs=dict(num=meeting.number,utc='-utc'))) self.assertEqual(r.status_code, 200) # Agenda API tests # -> Meeting data # First, check that the generation function does the right thing generated_data = generate_agenda_data(meeting.number) self.assertEqual( generated_data, { "meeting": { "number": meeting.number, "city": meeting.city, "startDate": meeting.date.isoformat(), "endDate": meeting.end_date().isoformat(), "updated": generated_data.get("meeting").get("updated"), # Just expect the value to exist "timezone": meeting.time_zone, "infoNote": meeting.agenda_info_note, "warningNote": meeting.agenda_warning_note }, "categories": generated_data.get("categories"), # Just expect the value to exist "isCurrentMeeting": True, "usesNotes": False, # make_meeting_test_data sets number=72 "schedule": generated_data.get("schedule"), # Just expect the value to exist "floors": [] } ) with patch("ietf.meeting.views.generate_agenda_data", return_value=generated_data): r = self.client.get(urlreverse("ietf.meeting.views.api_get_agenda_data", kwargs=dict(num=meeting.number))) self.assertEqual(r.status_code, 200) # json.dumps using the DjangoJSONEncoder to handle timestamps consistently self.assertJSONEqual(r.content.decode("utf8"), json.dumps(generated_data, cls=DjangoJSONEncoder)) # -> Session MaterialM r = self.client.get(urlreverse("ietf.meeting.views.api_get_session_materials", kwargs=dict(session_id=session.id))) self.assertEqual(r.status_code, 200) rjson = json.loads(r.content.decode("utf8")) minutes = session.minutes() self.assertJSONEqual( r.content.decode("utf8"), { "url": session.agenda().get_href(), "slides": rjson.get("slides"), # Just expect the value to exist "minutes": { "id": minutes.id, "title": minutes.title, "url": minutes.get_href(), "ext": minutes.file_extension() } if minutes is not None else None } ) # text r = self.client.get(urlreverse("ietf.meeting.views.agenda_plain", kwargs=dict(num=meeting.number, ext=".txt"))) self.assertContains(r, session.group.acronym) self.assertContains(r, session.group.name) self.assertContains(r, session.group.parent.acronym.upper()) self.assertContains(r, slot.location.name) self.assertContains(r, "{}-{}".format( slot.time.astimezone(meeting.tz()).strftime("%H%M"), (slot.time + slot.duration).astimezone(meeting.tz()).strftime("%H%M"), )) self.assertContains(r, f"shown in the {meeting.tz()} time zone") updated = meeting.updated().astimezone(meeting.tz()).strftime("%Y-%m-%d %H:%M:%S %Z") self.assertContains(r, f"Updated {updated}") # text, UTC r = self.client.get(urlreverse( "ietf.meeting.views.agenda_plain", kwargs=dict(num=meeting.number, ext=".txt", utc="-utc"), )) self.assertContains(r, session.group.acronym) self.assertContains(r, session.group.name) self.assertContains(r, session.group.parent.acronym.upper()) self.assertContains(r, slot.location.name) self.assertContains(r, "{}-{}".format( slot.time.astimezone(datetime.timezone.utc).strftime("%H%M"), (slot.time + slot.duration).astimezone(datetime.timezone.utc).strftime("%H%M"), )) self.assertContains(r, "shown in UTC") updated = meeting.updated().astimezone(datetime.timezone.utc).strftime("%Y-%m-%d %H:%M:%S %Z") self.assertContains(r, f"Updated {updated}") # text, invalid updated (none) with patch("ietf.meeting.models.Meeting.updated", return_value=None): r = self.client.get(urlreverse( "ietf.meeting.views.agenda_plain", kwargs=dict(num=meeting.number, ext=".txt", utc="-utc"), )) self.assertNotContains(r, "Updated ") # future meeting, no agenda r = self.client.get(urlreverse("ietf.meeting.views.agenda_plain", kwargs=dict(num=future_meeting.number, ext=".txt"))) self.assertContains(r, "There is no agenda available yet.") self.assertTemplateUsed(r, 'meeting/no-agenda.txt') # CSV r = self.client.get(urlreverse("ietf.meeting.views.agenda_plain", kwargs=dict(num=meeting.number, ext=".csv"))) self.assertContains(r, session.group.acronym) self.assertContains(r, session.group.name) self.assertContains(r, session.group.parent.acronym.upper()) self.assertContains(r, slot.location.name) self.assertContains(r, registration_text) start_time = slot.time.astimezone(meeting.tz()) end_time = slot.end_time().astimezone(meeting.tz()) self.assertContains(r, '"{}","{}","{}"'.format( start_time.strftime("%Y-%m-%d"), start_time.strftime("%H%M"), end_time.strftime("%H%M"), )) self.assertContains(r, session.materials.get(type='agenda').uploaded_filename) self.assertContains(r, session.materials.filter(type='slides').exclude(states__type__slug='slides',states__slug='deleted').first().uploaded_filename) self.assertNotContains(r, session.materials.filter(type='slides',states__type__slug='slides',states__slug='deleted').first().uploaded_filename) # CSV, utc r = self.client.get(urlreverse( "ietf.meeting.views.agenda_plain", kwargs=dict(num=meeting.number, ext=".csv", utc="-utc"), )) self.assertContains(r, session.group.acronym) self.assertContains(r, session.group.name) self.assertContains(r, session.group.parent.acronym.upper()) self.assertContains(r, slot.location.name) self.assertContains(r, registration_text) start_time = slot.time.astimezone(datetime.timezone.utc) end_time = slot.end_time().astimezone(datetime.timezone.utc) self.assertContains(r, '"{}","{}","{}"'.format( start_time.strftime("%Y-%m-%d"), start_time.strftime("%H%M"), end_time.strftime("%H%M"), )) self.assertContains(r, session.materials.get(type='agenda').uploaded_filename) self.assertContains(r, session.materials.filter(type='slides').exclude(states__type__slug='slides',states__slug='deleted').first().uploaded_filename) self.assertNotContains(r, session.materials.filter(type='slides',states__type__slug='slides',states__slug='deleted').first().uploaded_filename) # iCal, no session filtering ical_url = urlreverse("ietf.meeting.views.agenda_ical", kwargs=dict(num=meeting.number)) r = self.client.get(ical_url) assert_ical_response_is_valid(self, r) self.assertContains(r, "BEGIN:VTIMEZONE") self.assertContains(r, "END:VTIMEZONE") # iCal, single group r = self.client.get(ical_url + "?show=" + session.group.parent.acronym.upper()) assert_ical_response_is_valid(self, r) self.assertContains(r, session.group.acronym) self.assertContains(r, session.group.name) self.assertContains(r, session.remote_instructions) self.assertContains(r, slot.location.name) self.assertContains(r, 'https://onsite.example.com') self.assertContains(r, 'https://meetecho.example.com') self.assertContains(r, "BEGIN:VTIMEZONE") self.assertContains(r, "END:VTIMEZONE") self.assertContains(r, session.agenda().get_href()) self.assertContains( r, urlreverse( 'ietf.meeting.views.session_details', kwargs=dict(num=meeting.number, acronym=session.group.acronym)), msg_prefix='ical should contain link to meeting materials page for session') # Floor Plan r = self.client.get(urlreverse('floor-plan', kwargs=dict(num=meeting.number))) self.assertEqual(r.status_code, 200) def test_session_recordings_via_factories(self): session = SessionFactory(meeting__type_id="ietf", meeting__date=date_today()-datetime.timedelta(days=180)) self.assertEqual(session.meetecho_recording_name, "") self.assertEqual(len(session.recordings()), 0) url = urlreverse("ietf.meeting.views.session_details", kwargs=dict(num=session.meeting.number, acronym=session.group.acronym)) r = self.client.get(url) q = PyQuery(r.content) # debug.show("q(f'#notes_and_recordings_{session.pk}')") self.assertEqual(len(q(f"#notes_and_recordings_{session.pk} tr")), 1) link = q(f"#notes_and_recordings_{session.pk} tr a") self.assertEqual(len(link), 1) self.assertEqual(link[0].attrib['href'], str(session.session_recording_url())) session.meetecho_recording_name = 'my_test_session_name' session.save() r = self.client.get(url) q = PyQuery(r.content) self.assertEqual(len(q(f"#notes_and_recordings_{session.pk} tr")), 1) links = q(f"#notes_and_recordings_{session.pk} tr a") self.assertEqual(len(links), 1) self.assertEqual(links[0].attrib['href'], session.session_recording_url()) new_recording_url = "https://www.youtube.com/watch?v=jNQXAC9IVRw" new_recording_title = "Me at the zoo" create_recording(session, new_recording_url, new_recording_title) r = self.client.get(url) q = PyQuery(r.content) self.assertEqual(len(q(f"#notes_and_recordings_{session.pk} tr")), 2) links = q(f"#notes_and_recordings_{session.pk} tr a") self.assertEqual(len(links), 2) self.assertEqual(links[0].attrib['href'], new_recording_url) self.assertIn(new_recording_title, links[0].text_content()) #debug.show("q(f'#notes_and_recordings_{session_pk}')") def test_delete_recordings(self): # No user specified, active recording state sp = SessionPresentationFactory( document__type_id="recording", document__external_url="https://example.com/some-recording", document__states=[("recording", "active")], ) doc = sp.document doc.docevent_set.all().delete() # clear this out delete_recording(sp) self.assertFalse(SessionPresentation.objects.filter(pk=sp.pk).exists()) self.assertEqual(doc.get_state("recording").slug, "deleted", "recording state updated") self.assertEqual(doc.docevent_set.count(), 1, "one event added") event = doc.docevent_set.first() self.assertEqual(event.type, "changed_state", "event is a changed_state event") self.assertEqual(event.by.name, "(System)", "system user is responsible") # Specified user, no recording state sp = SessionPresentationFactory( document__type_id="recording", document__external_url="https://example.com/some-recording", document__states=[], ) doc = sp.document doc.docevent_set.all().delete() # clear this out user = PersonFactory() # naming matches the methods - user is a Person, not a User delete_recording(sp, user=user) self.assertFalse(SessionPresentation.objects.filter(pk=sp.pk).exists()) self.assertEqual(doc.get_state("recording").slug, "deleted", "recording state updated") self.assertEqual(doc.docevent_set.count(), 1, "one event added") event = doc.docevent_set.first() self.assertEqual(event.type, "changed_state", "event is a changed_state event") self.assertEqual(event.by, user, "user is responsible") # Document is not a recording sp = SessionPresentationFactory( document__type_id="draft", document__external_url="https://example.com/some-recording", ) with self.assertRaises(ValueError): delete_recording(sp) def test_agenda_ical_next_meeting_type(self): # start with no upcoming IETF meetings, just an interim MeetingFactory( type_id="interim", date=date_today() + datetime.timedelta(days=15) ) r = self.client.get(urlreverse("ietf.meeting.views.agenda_ical", kwargs={})) self.assertEqual( r.status_code, 404, "Should not return an interim meeting as next meeting" ) # create an IETF meeting after the interim - it should be found as "next" ietf_meeting = MeetingFactory( type_id="ietf", date=date_today() + datetime.timedelta(days=30) ) SessionFactory(meeting=ietf_meeting, name="Session at IETF meeting") r = self.client.get(urlreverse("ietf.meeting.views.agenda_ical", kwargs={})) self.assertContains(r, "Session at IETF meeting", status_code=200) def test_agenda_json_next_meeting_type(self): # start with no upcoming IETF meetings, just an interim MeetingFactory( type_id="interim", date=date_today() + datetime.timedelta(days=15) ) r = self.client.get(urlreverse("ietf.meeting.views.agenda_json", kwargs={})) self.assertEqual( r.status_code, 404, "Should not return an interim meeting as next meeting" ) # create an IETF meeting after the interim - it should be found as "next" ietf_meeting = MeetingFactory( type_id="ietf", date=date_today() + datetime.timedelta(days=30) ) SessionFactory(meeting=ietf_meeting, name="Session at IETF meeting") r = self.client.get(urlreverse("ietf.meeting.views.agenda_json", kwargs={})) self.assertContains(r, "Session at IETF meeting", status_code=200) @override_settings(PROCEEDINGS_V1_BASE_URL='https://example.com/{meeting.number}') def test_agenda_redirects_for_old_meetings(self): """Meetings before 64 should be forwarded to their proceedings""" # meeting with record but no schedule MeetingFactory(type_id='ietf', number='35', populate_schedule=False) r = self.client.get( urlreverse( 'agenda', kwargs={'num': '35', 'ext': '.html'}, )) self.assertRedirects(r, 'https://example.com/35', fetch_redirect_response=False) # meeting with record and schedule but no assignments meeting_with_schedule = MeetingFactory(type_id='ietf', number='36', populate_schedule=True) r = self.client.get( urlreverse( 'agenda', kwargs={'num': '36', 'ext': '.html'}, )) self.assertRedirects(r, 'https://example.com/36', fetch_redirect_response=False) # meeting with an assignment SessionFactory(meeting=meeting_with_schedule) r = self.client.get( urlreverse( 'agenda', kwargs={'num': '36', 'ext': '.html'}, )) self.assertRedirects(r, 'https://example.com/36', fetch_redirect_response=False) def test_agenda_for_nonexistent_meeting(self): """Return a 404 for a bad IETF meeting number""" # Meetings pre-64 are redirected, but should be a 404 if there is no Meeting instance r = self.client.get( urlreverse( 'agenda', kwargs={'num': '32', 'ext': '.html'}, )) self.assertEqual(r.status_code, 404) @override_settings(MEETING_MATERIALS_SERVE_LOCALLY=False, MEETING_DOC_HREFS = settings.MEETING_DOC_CDN_HREFS) def test_materials_through_cdn(self): meeting = make_meeting_test_data(create_interims=True) session107 = SessionFactory(meeting__number='172',group__acronym='mars') doc = DocumentFactory.create(name='agenda-172-mars', type_id='agenda', title="Agenda", uploaded_filename="agenda-172-mars.txt", group=session107.group, rev='00', states=[('agenda','active')]) pres = SessionPresentation.objects.create(session=session107,document=doc,rev=doc.rev) session107.presentations.add(pres) # doc = DocumentFactory.create(name='minutes-172-mars', type_id='minutes', title="Minutes", uploaded_filename="minutes-172-mars.md", group=session107.group, rev='00', states=[('minutes','active')]) pres = SessionPresentation.objects.create(session=session107,document=doc,rev=doc.rev) session107.presentations.add(pres) doc = DocumentFactory.create(name='slides-172-mars-1-active', type_id='slides', title="Slideshow", uploaded_filename="slides-172-mars.txt", group=session107.group, rev='00', states=[('slides','active'), ('reuse_policy', 'single')]) pres = SessionPresentation.objects.create(session=session107,document=doc,rev=doc.rev) session107.presentations.add(pres) for session in ( Session.objects.filter(meeting=meeting, group__acronym="mars").first(), session107, Session.objects.filter(meeting__type_id='interim', group__acronym='mars', schedulingevent__status='sched').first(), ): self.write_materials_files(session.meeting, session) for document in (session.agenda(),session.minutes(),session.slides()[0]): url = urlreverse("ietf.meeting.views.materials_document", kwargs=dict(num=session.meeting.number, document=document)) r = self.client.get(url) if session.meeting.number.isdigit() and int(session.meeting.number)<=96: self.assertEqual(r.status_code,200) else: self.assertEqual(r.status_code,302) self.assertEqual(r['Location'],document.get_href()) self.assertNotEqual(urlsplit(r['Location'])[2],url) def test_materials(self): meeting = make_meeting_test_data() session = Session.objects.filter(meeting=meeting, group__acronym="mars").first() self.do_test_materials(meeting, session) def test_interim_materials(self): make_meeting_test_data() group = Group.objects.get(acronym='mars') date = timezone.now() - datetime.timedelta(days=10) meeting = make_interim_meeting(group=group, date=date, status='sched') session = meeting.session_set.first() self.do_test_materials(meeting, session) def test_named_session(self): """Session with a name should appear separately in the materials""" meeting = MeetingFactory(type_id='ietf', number='100') meeting.importantdate_set.create(name_id='revsub',date=date_today() + datetime.timedelta(days=20)) group = GroupFactory() plain_session = SessionFactory(meeting=meeting, group=group) named_session = SessionFactory(meeting=meeting, group=group, name='I Got a Name') for doc_type_id in ('agenda', 'minutes', 'slides', 'draft'): # Set up sessions materials that will have distinct URLs for each session. # This depends on settings.MEETING_DOC_HREFS and may need updating if that changes. SessionPresentationFactory( session=plain_session, document__type_id=doc_type_id, document__uploaded_filename=f'upload-{doc_type_id}-plain', document__external_url=f'external_url-{doc_type_id}-plain', ) SessionPresentationFactory( session=named_session, document__type_id=doc_type_id, document__uploaded_filename=f'upload-{doc_type_id}-named', document__external_url=f'external_url-{doc_type_id}-named', ) url = urlreverse('ietf.meeting.views.materials', kwargs={'num': meeting.number}) r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) plain_label = q(f'div#{group.acronym}') self.assertEqual(plain_label.text(), group.acronym) plain_row = plain_label.closest('tr') self.assertTrue(plain_row) named_label = q(f'div#{slugify(named_session.name)}') self.assertEqual(named_label.text(), named_session.name) named_row = named_label.closest('tr') self.assertTrue(named_row) for material in (sp.document for sp in plain_session.presentations.all()): if material.type_id == 'draft': expected_url = urlreverse( 'ietf.doc.views_doc.document_main', kwargs={'name': material.name}, ) else: expected_url = material.get_href(meeting) self.assertTrue(plain_row.find(f'a[href="{expected_url}"]')) self.assertFalse(named_row.find(f'a[href="{expected_url}"]')) for material in (sp.document for sp in named_session.presentations.all()): if material.type_id == 'draft': expected_url = urlreverse( 'ietf.doc.views_doc.document_main', kwargs={'name': material.name}, ) else: expected_url = material.get_href(meeting) self.assertFalse(plain_row.find(f'a[href="{expected_url}"]')) self.assertTrue(named_row.find(f'a[href="{expected_url}"]')) @override_settings(MEETING_MATERIALS_SERVE_LOCALLY=True) def test_meeting_materials_non_utf8(self): meeting = make_meeting_test_data() session = Session.objects.filter(meeting=meeting, group__acronym="mars").first() doc = session.materials.get(type="minutes") self.write_materials_file(meeting, doc, "1. More work items underway\n\n2. The draft will be finished before next meeting\n\n - É", charset="iso-8859-1") url = urlreverse("ietf.meeting.views.materials_document", kwargs=dict(num=meeting.number, document=session.minutes())) for accept, cont_type, content in [ ('text/html,text/plain,text/markdown', 'text/html', '
  • \n

    More work items underway

    \n
  • '), ('text/markdown,text/html,text/plain', 'text/markdown', '1. More work items underway'), ('text/plain,text/markdown, text/html', 'text/plain', '1. More work items underway'), ('text/html', 'text/html', '
  • \n

    More work items underway

    \n
  • '), ('text/markdown', 'text/markdown', '1. More work items underway'), ('text/plain', 'text/plain', '1. More work items underway'), ]: client = Client(HTTP_ACCEPT=accept) r = client.get(url) rtype = r['Content-Type'].split(';')[0] self.assertEqual(cont_type, rtype) self.assertContains(r, content) @override_settings(MEETING_MATERIALS_SERVE_LOCALLY=True) def do_test_materials(self, meeting, session): self.write_materials_files(meeting, session) # session agenda document = session.agenda() url = urlreverse("ietf.meeting.views.materials_document", kwargs=dict(num=meeting.number, document=document)) r = self.client.get(url) if r.status_code != 200: q = PyQuery(r.content) debug.show('q(".alert").text()') self.assertContains(r, "1. WG status") # session minutes url = urlreverse("ietf.meeting.views.materials_document", kwargs=dict(num=meeting.number, document=session.minutes())) r = self.client.get(url) self.assertContains(r, "1. More work items underway") cont_disp = r.headers.get('content-disposition', ('Content-Disposition', ''))[1] cont_disp = re.split('; ?', cont_disp) cont_disp_settings = dict( e.split('=', 1) for e in cont_disp if '=' in e ) filename = cont_disp_settings.get('filename', '').strip('"') if filename.endswith('.md'): for accept, cont_type, content in [ ('text/html,text/plain,text/markdown', 'text/html', '
  • \n

    More work items underway

    \n
  • '), ('text/markdown,text/html,text/plain', 'text/markdown', '1. More work items underway'), ('text/plain,text/markdown, text/html', 'text/plain', '1. More work items underway'), ('text/html', 'text/html', '
  • \n

    More work items underway

    \n
  • '), ('text/markdown', 'text/markdown', '1. More work items underway'), ('text/plain', 'text/plain', '1. More work items underway'), ]: client = Client(HTTP_ACCEPT=accept) r = client.get(url) rtype = r['Content-Type'].split(';')[0] self.assertEqual(cont_type, rtype) self.assertContains(r, content) # test with explicit meeting number in url if meeting.number.isdigit(): url = urlreverse("ietf.meeting.views.materials", kwargs=dict(num=meeting.number)) r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) row = q('#content #%s' % str(session.group.acronym)).closest("tr") self.assertTrue(row.find('a:contains("Agenda")')) self.assertTrue(row.find('a:contains("Minutes")')) self.assertTrue(row.find('a:contains("Slideshow")')) self.assertFalse(row.find("a:contains(\"Bad Slideshow\")")) # test with no meeting number in url # Add various group sessions groups = [] parent_groups = [ GroupFactory.create(type_id="area", acronym="gen"), GroupFactory.create(acronym="iab"), GroupFactory.create(acronym="irtf"), ] for parent in parent_groups: groups.append(GroupFactory.create(parent=parent)) for acronym in ["rsab", "edu"]: groups.append(GroupFactory.create(acronym=acronym)) for group in groups: SessionFactory(meeting=meeting, group=group) self.write_materials_files(meeting, session) url = urlreverse("ietf.meeting.views.materials", kwargs=dict()) r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) row = q('#content #%s' % str(session.group.acronym)).closest("tr") self.assertTrue(row.find('a:contains("Agenda")')) self.assertTrue(row.find('a:contains("Minutes")')) self.assertTrue(row.find('a:contains("Slideshow")')) self.assertFalse(row.find("a:contains(\"Bad Slideshow\")")) # test for different sections sections = ["plenaries", "gen", "iab", "editorial", "irtf", "training"] for section in sections: self.assertEqual(len(q(f"#{section}")), 1, f"{section} section should exists in proceedings") # test with a loggged-in wg chair self.client.login(username="marschairman", password="marschairman+password") url = urlreverse("ietf.meeting.views.materials", kwargs=dict(num=meeting.number)) r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) row = q('#content #%s' % str(session.group.acronym)).closest("tr") self.assertTrue(row.find('a:contains("Agenda")')) self.assertTrue(row.find('a:contains("Minutes")')) self.assertTrue(row.find('a:contains("Slideshow")')) self.assertFalse(row.find("a:contains(\"Bad Slideshow\")")) self.assertTrue(row.find('a:contains("Edit materials")')) # FIXME: missing tests of .pdf/.tar generation (some code can # probably be lifted from similar tests in iesg/tests.py) # document-specific urls for doc in session.materials.exclude(states__slug='deleted'): url = urlreverse('ietf.meeting.views.materials_document', kwargs=dict(num=meeting.number, document=doc.name)) r = self.client.get(url) self.assertEqual(unicontent(r), doc.text()) def test_materials_has_edit_links(self): meeting = make_meeting_test_data() url = urlreverse("ietf.meeting.views.materials", kwargs=dict(num=meeting.number)) r = self.client.get(url) self.assertNotContains(r, 'Edit materials', status_code=200) # mars chairman can edit materials for mars group self.client.login(username='marschairman', password='marschairman+password') r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content.decode()) edit_url = urlreverse( 'ietf.meeting.views.session_details', kwargs={'num': meeting.number, 'acronym': 'mars'}, ) self.assertEqual(len(q(f'a[href^="{edit_url}"]')), 1, 'Link to mars session_details for mars chairman') for acro in ['ietf', 'ames']: # other groups with materials edit_url = urlreverse( 'ietf.meeting.views.session_details', kwargs={'num': meeting.number, 'acronym': acro}, ) self.assertEqual(len(q(f'a[href^="{edit_url}"]')), 0, f'No link to {acro} session_details for mars chairman') # secretary can edit all groups self.client.login(username='secretary', password='secretary+password') r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content.decode()) for acro in ['mars', 'ames']: # wgs edit_url = urlreverse( 'ietf.meeting.views.session_details', kwargs={'num': meeting.number, 'acronym': acro}, ) self.assertEqual(len(q(f'a[href^="{edit_url}"]')), 1, f'Link to session_details page for {acro}') # The IETF Plenary has a "#sessionX" tacked on to the edit url to differentiate from other sessions, # so test it separately. Not bothering to check the exact session pk in detail. edit_url = urlreverse( 'ietf.meeting.views.session_details', kwargs={'num': meeting.number, 'acronym': 'ietf'}, ) self.assertEqual(len(q(f'a[href^="{edit_url}#session"]')), 1, f'Link to session_details page for {acro}') def test_materials_document_extension_choice(self): def _url(**kwargs): return urlreverse("ietf.meeting.views.materials_document", kwargs=kwargs) presentation = SessionPresentationFactory( document__rev="00", document__name="slides-whatever", document__uploaded_filename="slides-whatever-00.txt", document__type_id="slides", document__states=(("reuse_policy", "single"),) ) session = presentation.session meeting = session.meeting # This is not a realistic set of files to exist, but is useful for testing. Normally, # we'd have _either_ txt, pdf, or pptx + pdf. self.write_materials_file(meeting, presentation.document, "Hi I'm a txt", with_ext=".txt") self.write_materials_file(meeting, presentation.document, "Hi I'm a pptx", with_ext=".pptx") # with no rev, prefers the uploaded_filename r = self.client.get(_url(document="slides-whatever", num=meeting.number)) # no rev self.assertEqual(r.status_code, 200) self.assertEqual(r.content.decode(), "Hi I'm a txt") # with a rev, prefers pptx because it comes first alphabetically r = self.client.get(_url(document="slides-whatever-00", num=meeting.number)) self.assertEqual(r.status_code, 200) self.assertEqual(r.content.decode(), "Hi I'm a pptx") # now create a pdf self.write_materials_file(meeting, presentation.document, "Hi I'm a pdf", with_ext=".pdf") # with no rev, still prefers uploaded_filename r = self.client.get(_url(document="slides-whatever", num=meeting.number)) # no rev self.assertEqual(r.status_code, 200) self.assertEqual(r.content.decode(), "Hi I'm a txt") # pdf should be preferred with a rev r = self.client.get(_url(document="slides-whatever-00", num=meeting.number)) self.assertEqual(r.status_code, 200) self.assertEqual(r.content.decode(), "Hi I'm a pdf") # and explicit extensions should, of course, be respected for ext in ["pdf", "pptx", "txt"]: r = self.client.get(_url(document="slides-whatever-00", num=meeting.number, ext=f".{ext}")) self.assertEqual(r.status_code, 200) self.assertEqual(r.content.decode(), f"Hi I'm a {ext}") # and 404 should come up if the ext is not found r = self.client.get(_url(document="slides-whatever-00", num=meeting.number, ext=".docx")) self.assertEqual(r.status_code, 404) def test_materials_editable_groups(self): meeting = make_meeting_test_data() self.client.login(username="marschairman", password="marschairman+password") r = self.client.get(urlreverse("ietf.meeting.views.materials_editable_groups", kwargs={'num':meeting.number})) self.assertContains(r, meeting.number) self.assertContains(r, "mars") self.assertNotContains(r, "No session requested") self.client.login(username="ad", password="ad+password") r = self.client.get(urlreverse("ietf.meeting.views.materials_editable_groups", kwargs={'num':meeting.number})) self.assertContains(r, meeting.number) self.assertContains(r, "frfarea") self.assertContains(r, "No session requested") self.client.login(username="plain",password="plain+password") r = self.client.get(urlreverse("ietf.meeting.views.materials_editable_groups", kwargs={'num':meeting.number})) self.assertContains(r, meeting.number) self.assertContains(r, "You cannot manage the meeting materials for any groups") @override_settings(MEETING_MATERIALS_SERVE_LOCALLY=True) def test_materials_name_endswith_hyphen_number_number(self): # be sure a shadowed filename without the hyphen does not interfere shadow = SessionPresentationFactory( document__name="slides-115-junk", document__type_id="slides", document__states=[("reuse_policy", "single")], ) shadow.document.uploaded_filename = ( f"{shadow.document.name}-{shadow.document.rev}.pdf" ) shadow.document.save() # create the material we want to find for the test sp = SessionPresentationFactory( document__name="slides-115-junk-15", document__type_id="slides", document__states=[("reuse_policy", "single")], ) sp.document.uploaded_filename = f"{sp.document.name}-{sp.document.rev}.pdf" sp.document.save() self.write_materials_file( sp.session.meeting, sp.document, "Fake slide contents rev 00" ) # create rev 01 sp.document.rev = "01" sp.document.uploaded_filename = f"{sp.document.name}-{sp.document.rev}.pdf" sp.document.save_with_history( [ NewRevisionDocEvent.objects.create( type="new_revision", doc=sp.document, rev=sp.document.rev, by=Person.objects.get(name="(System)"), desc=f"New version available: {sp.document.name}-{sp.document.rev}.txt", ) ] ) self.write_materials_file( sp.session.meeting, sp.document, "Fake slide contents rev 01" ) url = urlreverse( "ietf.meeting.views.materials_document", kwargs=dict(document=sp.document.name, num=sp.session.meeting.number), ) r = self.client.get(url) self.assertContains( r, "Fake slide contents rev 01", status_code=200, msg_prefix="Should return latest rev by default", ) url = urlreverse( "ietf.meeting.views.materials_document", kwargs=dict(document=sp.document.name + "-00", num=sp.session.meeting.number), ) r = self.client.get(url) self.assertContains( r, "Fake slide contents rev 00", status_code=200, msg_prefix="Should return existing version on request", ) url = urlreverse( "ietf.meeting.views.materials_document", kwargs=dict(document=sp.document.name + "-02", num=sp.session.meeting.number), ) r = self.client.get(url) self.assertEqual(r.status_code, 404, "Should not find nonexistent version") def test_important_dates(self): meeting=MeetingFactory(type_id='ietf') meeting.show_important_dates = True meeting.save() populate_important_dates(meeting) url = urlreverse('ietf.meeting.views.important_dates',kwargs={'num':meeting.number}) r = self.client.get(url) self.assertContains(r, str(meeting.importantdate_set.first().date)) idn = ImportantDateName.objects.filter(used=True).first() pre_date = meeting.importantdate_set.get(name=idn).date idn.default_offset_days -= 1 idn.save() update_important_dates(meeting) post_date = meeting.importantdate_set.get(name=idn).date self.assertEqual(pre_date, post_date+datetime.timedelta(days=1)) def test_important_dates_ical(self): meeting = MeetingFactory(type_id='ietf') meeting.show_important_dates = True meeting.save() populate_important_dates(meeting) url = urlreverse('ietf.meeting.views.important_dates', kwargs={'num': meeting.number, 'output_format': 'ics'}) r = self.client.get(url) for d in meeting.importantdate_set.all(): self.assertContains(r, d.date.isoformat()) updated = meeting.updated() self.assertIsNotNone(updated) expected_updated = updated.astimezone(datetime.timezone.utc).strftime("%Y%m%dT%H%M%SZ") self.assertContains(r, f"DTSTAMP:{expected_updated}") dtstamps_count = r.content.decode("utf-8").count(f"DTSTAMP:{expected_updated}") self.assertEqual(dtstamps_count, meeting.importantdate_set.count()) # With default cached_updated, 1970-01-01 with patch("ietf.meeting.models.Meeting.updated", return_value=None): r = self.client.get(url) for d in meeting.importantdate_set.all(): self.assertContains(r, d.date.isoformat()) expected_updated = "19700101T000000Z" self.assertContains(r, f"DTSTAMP:{expected_updated}") dtstamps_count = r.content.decode("utf-8").count(f"DTSTAMP:{expected_updated}") self.assertEqual(dtstamps_count, meeting.importantdate_set.count()) def test_group_ical(self): meeting = make_meeting_test_data() s1 = Session.objects.filter(meeting=meeting, group__acronym="mars").first() a1 = s1.official_timeslotassignment() t1 = a1.timeslot # Create an extra session t2 = TimeSlotFactory.create( meeting=meeting, time=meeting.tz().localize( datetime.datetime.combine(meeting.date, datetime.time(11, 30)) ) ) s2 = SessionFactory.create(meeting=meeting, group=s1.group, add_to_schedule=False) SchedTimeSessAssignment.objects.create(timeslot=t2, session=s2, schedule=meeting.schedule) # url = urlreverse('ietf.meeting.views.agenda_ical', kwargs={'num':meeting.number, 'acronym':s1.group.acronym, }) r = self.client.get(url) assert_ical_response_is_valid(self, r, expected_event_summaries=['mars - Martian Special Interest Group'], expected_event_count=2) self.assertContains(r, t1.local_start_time().strftime('%Y%m%dT%H%M%S')) self.assertContains(r, t2.local_start_time().strftime('%Y%m%dT%H%M%S')) # url = urlreverse('ietf.meeting.views.agenda_ical', kwargs={'num':meeting.number, 'session_id':s1.id, }) r = self.client.get(url) assert_ical_response_is_valid(self, r, expected_event_summaries=['mars - Martian Special Interest Group'], expected_event_count=1) self.assertContains(r, t1.local_start_time().strftime('%Y%m%dT%H%M%S')) self.assertNotContains(r, t2.local_start_time().strftime('%Y%m%dT%H%M%S')) def test_parse_agenda_filter_params(self): def _r(show=(), hide=(), showtypes=(), hidetypes=()): """Helper to create expected result dict""" return dict(show=set(show), hide=set(hide), showtypes=set(showtypes), hidetypes=set(hidetypes)) self.assertIsNone(parse_agenda_filter_params(QueryDict(''))) # test valid combos (not exhaustive) for qstr, expected in ( ('show=', _r()), ('hide=', _r()), ('showtypes=', _r()), ('hidetypes=', _r()), ('show=x', _r(show=['x'])), ('hide=x', _r(hide=['x'])), ('showtypes=x', _r(showtypes=['x'])), ('hidetypes=x', _r(hidetypes=['x'])), ('show=x,y,z', _r(show=['x','y','z'])), ('hide=x,y,z', _r(hide=['x','y','z'])), ('showtypes=x,y,z', _r(showtypes=['x','y','z'])), ('hidetypes=x,y,z', _r(hidetypes=['x','y','z'])), ('show=a&hide=a', _r(show=['a'], hide=['a'])), ('show=a&hide=b', _r(show=['a'], hide=['b'])), ('show=a&hide=b&showtypes=c&hidetypes=d', _r(show=['a'], hide=['b'], showtypes=['c'], hidetypes=['d'])), ): self.assertEqual( parse_agenda_filter_params(QueryDict(qstr)), expected, 'Parsed "%s" incorrectly' % qstr, ) def do_ical_filter_test(self, meeting, querystring, expected_session_summaries): url = urlreverse('ietf.meeting.views.agenda_ical', kwargs={'num':meeting.number}) r = self.client.get(url + querystring) self.assertEqual(r.status_code, 200) assert_ical_response_is_valid(self, r, expected_event_summaries=expected_session_summaries, expected_event_count=len(expected_session_summaries)) def test_ical_filter(self): # Just a quick check of functionality - permutations tested via tests_js.AgendaTests meeting = make_meeting_test_data() self.do_ical_filter_test( meeting, querystring='', expected_session_summaries=[ 'Morning Break', 'Registration', 'IETF Plenary', 'ames - Asteroid Mining Equipment Standardization Group', 'mars - Martian Special Interest Group', ] ) self.do_ical_filter_test( meeting, querystring='?show=plenary,secretariat,ames&hide=admin', expected_session_summaries=[ 'Morning Break', 'IETF Plenary', 'ames - Asteroid Mining Equipment Standardization Group', ] ) def build_session_setup(self): # This setup is intentionally unusual - the session has one draft attached as a session presentation, # but lists a different on in its agenda. The expectation is that the pdf and tgz views will return both. session = SessionFactory(group__type_id='wg',meeting__type_id='ietf') draft1 = WgDraftFactory(group=session.group) session.presentations.create(document=draft1) draft2 = WgDraftFactory(group=session.group) agenda = DocumentFactory(type_id='agenda',group=session.group, uploaded_filename='agenda-%s-%s' % (session.meeting.number,session.group.acronym), states=[('agenda','active')]) session.presentations.create(document=agenda) self.write_materials_file(session.meeting, session.materials.get(type="agenda"), "1. WG status (15 minutes)\n\n2. Status of %s\n\n" % draft2.name) filenames = [] for d in (draft1, draft2): file,_ = submission_file(name_in_doc=f'{d.name}-00',name_in_post=f'{d.name}-00.txt',templatename='test_submission.txt',group=session.group) filename = os.path.join(d.get_file_path(),file.name) with io.open(filename,'w') as draftbits: draftbits.write(file.getvalue()) filenames.append(filename) self.assertEqual( len(session_draft_list(session.meeting.number,session.group.acronym)), 2) return (session, filenames) def test_session_draft_tarfile(self): session, filenames = self.build_session_setup() try: url = urlreverse('ietf.meeting.views.session_draft_tarfile', kwargs={'num':session.meeting.number,'acronym':session.group.acronym}) response = self.client.get(url) self.assertEqual(response.status_code, 200) self.assertEqual(response.get('Content-Type'), 'application/octet-stream') finally: for filename in filenames: os.unlink(filename) @skipIf(skip_pdf_tests, skip_message) @skip_coverage def test_session_draft_pdf(self): session, filenames = self.build_session_setup() try: url = urlreverse('ietf.meeting.views.session_draft_pdf', kwargs={'num':session.meeting.number,'acronym':session.group.acronym}) response = self.client.get(url) self.assertEqual(response.status_code, 200) self.assertEqual(response.get('Content-Type'), 'application/pdf') finally: for filename in filenames: os.unlink(filename) def test_current_materials(self): url = urlreverse('ietf.meeting.views.current_materials') response = self.client.get(url) self.assertEqual(response.status_code, 404) MeetingFactory(type_id='ietf', date=date_today()) response = self.client.get(url) self.assertEqual(response.status_code, 302) def test_edit_schedule_properties(self): self.client.login(username='secretary',password='secretary+password') url = urlreverse('ietf.meeting.views.edit_schedule_properties',kwargs={'owner':'does@notexist.example','name':'doesnotexist','num':00}) response = self.client.get(url) self.assertEqual(response.status_code,404) self.client.logout() schedule = ScheduleFactory(meeting__type_id='ietf',visible=False,public=False) url = urlreverse('ietf.meeting.views.edit_schedule_properties',kwargs={'owner':schedule.owner.email(),'name':schedule.name,'num':schedule.meeting.number}) response = self.client.get(url) self.assertEqual(response.status_code,302) self.client.login(username='secretary',password='secretary+password') response = self.client.get(url) self.assertEqual(response.status_code,200) new_base = Schedule.objects.create(name="newbase", owner=schedule.owner, meeting=schedule.meeting) response = self.client.post(url, { 'name': 'some-other-name', 'visible':True, 'public':True, 'notes': "New Notes", 'base': new_base.pk, } ) self.assertNoFormPostErrors(response) self.assertRedirects( response, urlreverse( 'ietf.meeting.views.edit_meeting_schedule', kwargs={'num': schedule.meeting.number, 'owner': schedule.owner.email(), 'name': 'some-other-name'} ), ) schedule.refresh_from_db() self.assertTrue(schedule.visible) self.assertTrue(schedule.public) self.assertEqual(schedule.notes, "New Notes") self.assertEqual(schedule.base_id, new_base.pk) self.assertEqual(schedule.name, 'some-other-name') def test_agenda_by_type_ics(self): session=SessionFactory(meeting__type_id='ietf',type_id='lead') url = urlreverse('ietf.meeting.views.agenda_by_type_ics',kwargs={'num':session.meeting.number,'type':'lead'}) login_testing_unauthorized(self,"secretary",url) response = self.client.get(url) self.assertEqual(response.status_code,200) self.assertEqual(response.get('Content-Type'), 'text/calendar') def test_cancelled_ics(self): session=SessionFactory(meeting__type_id='ietf',status_id='canceled') url = urlreverse('ietf.meeting.views.agenda_ical', kwargs=dict(num=session.meeting.number)) r = self.client.get(url) self.assertEqual(r.status_code,200) self.assertIn('STATUS:CANCELLED',unicontent(r)) self.assertNotIn('STATUS:CONFIRMED',unicontent(r)) def test_session_materials(self): meeting = make_meeting_test_data() session = Session.objects.filter(meeting=meeting, group__acronym="mars").first() url = urlreverse('ietf.meeting.views.session_materials', kwargs=dict(session_id=session.pk)) r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) agenda_div = q('div.agenda-frame') self.assertIsNotNone(agenda_div) self.assertEqual(agenda_div.attr('data-src'), session.agenda().get_href()) minutes_div = q('div.minutes-frame') self.assertIsNotNone(minutes_div) self.assertEqual(minutes_div.attr('data-src'), session.minutes().get_href()) # Make sure undeleted slides are present and deleted slides are not not_deleted_slides = session.materials.filter( type='slides' ).exclude( states__type__slug='slides',states__slug='deleted' ) self.assertGreater(not_deleted_slides.count(), 0) # make sure this isn't a pointless test deleted_slides = session.materials.filter( type='slides', states__type__slug='slides', states__slug='deleted' ) self.assertGreater(deleted_slides.count(), 0) # make sure this isn't a pointless test # live slides should be found for slide in not_deleted_slides: self.assertTrue(q('ul li a:contains("%s")' % slide.title)) # deleted slides should not be found for slide in deleted_slides: self.assertFalse(q('ul li a:contains("%s")' % slide.title)) def test_meetinghost_logo(self): host = MeetingHostFactory() url = urlreverse('ietf.meeting.views_proceedings.meetinghost_logo',kwargs=dict(host_id=host.pk,num=host.meeting.number)) r = self.client.get(url) self.assertIs(type(r),FileResponse) @override_settings(MEETING_SESSION_LOCK_TIME=datetime.timedelta(minutes=10)) class EditMeetingScheduleTests(TestCase): """Tests of the meeting editor view This has tests in tests_js.py as well. """ def test_room_grouping(self): """Blocks of rooms in the editor should have identical timeslots""" # set up a meeting, but we'll construct our own timeslots/rooms meeting = MeetingFactory(type_id='ietf', populate_schedule=False) sched = ScheduleFactory(meeting=meeting) # Make groups of rooms with timeslots identical within a group, distinct between groups times = [ [datetime.time(11,0), datetime.time(12,0), datetime.time(13,0)], [datetime.time(11,0), datetime.time(12,0), datetime.time(13,0)], # same times, but durations will differ [datetime.time(11,30), datetime.time(12, 0), datetime.time(13,0)], # different time [datetime.time(12,0)], # different number of timeslots ] durations = [ [30, 60, 90], [60, 60, 90], [30, 60, 90], [60], ] # check that times and durations are same-sized arrays self.assertEqual(len(times), len(durations)) for time_row, duration_row in zip(times, durations): self.assertEqual(len(time_row), len(duration_row)) # Create an array of room groups, each with rooms_per_group Rooms in it. # Assign TimeSlots according to the times/durations above to each Room. room_groups = [] rooms_in_group = 1 # will be incremented with each group for time_row, duration_row in zip(times, durations): room_groups.append(RoomFactory.create_batch(rooms_in_group, meeting=meeting)) rooms_in_group += 1 # put a different number of rooms in each group to help identify errors in grouping for time, duration in zip(time_row, duration_row): for room in room_groups[-1]: TimeSlotFactory( meeting=meeting, location=room, time=meeting.tz().localize( datetime.datetime.combine(meeting.date, time) ), duration=datetime.timedelta(minutes=duration), ) # Now retrieve the edit meeting schedule page url = urlreverse('ietf.meeting.views.edit_meeting_schedule', kwargs=dict(num=meeting.number, owner=sched.owner.email(), name=sched.name)) r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) day_divs = q('div.day') # There's only one day with TimeSlots. This means there will be two divs with class 'day': # the first is the room label column, the second is the TimeSlot grid. # Using eq() instead of [] gives us PyQuery objects instead of Elements label_divs = day_divs.eq(0).find('div.room-group') self.assertEqual(len(label_divs), len(room_groups)) room_group_divs = day_divs.eq(1).find('div.room-group') self.assertEqual(len(room_group_divs), len(room_groups)) for rg, l_div, rg_div in zip( room_groups, label_divs.items(), # items() gives us PyQuery objects room_group_divs.items(), # items() gives us PyQuery objects ): # Check that room labels are correctly grouped self.assertCountEqual( [div.text() for div in l_div.find('div.room-name').items()], [room.name for room in rg], ) # And that the time labels are correct. Just check that the individual timeslot labels agree with # the time-header above each room group. time_header_labels = rg_div.find('div.time-header div.time-label').text() timeslot_rows = rg_div.find('div.timeslots') for row in timeslot_rows.items(): time_labels = row.find('div.time-label div:not(.past-flag)').text() self.assertEqual(time_labels, time_header_labels) def test_bof_session_tag(self): """Sessions for BOF groups should be marked as such""" meeting = MeetingFactory(type_id='ietf') non_bof_session = SessionFactory(meeting=meeting) bof_session = SessionFactory(meeting=meeting, group__state_id='bof') url = urlreverse('ietf.meeting.views.edit_meeting_schedule', kwargs=dict(num=meeting.number)) self.client.login(username='secretary', password='secretary+password') r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertEqual(len(q('#session{} .bof-tag'.format(non_bof_session.pk))), 0, 'Non-BOF session should not be tagged as a BOF session') bof_tags = q('#session{} .bof-tag'.format(bof_session.pk)) self.assertEqual(len(bof_tags), 1, 'BOF session should have one BOF session tag') self.assertIn('BOF', bof_tags.eq(0).text(), 'BOF tag should contain text "BOF"') def _setup_for_swap_timeslots(self): """Create a meeting, rooms, and schedule for swap_timeslots testing Creates two groups of rooms with disjoint timeslot sets, modeling the room grouping in the edit_meeting_schedule view. """ # Meeting must be in the future so it can be edited meeting = MeetingFactory( type_id='ietf', date=date_today() + datetime.timedelta(days=7), populate_schedule=False, ) meeting.schedule = ScheduleFactory(meeting=meeting) meeting.save() # Create room groups room_groups = [ RoomFactory.create_batch(2, meeting=meeting), RoomFactory.create_batch(2, meeting=meeting), ] # Set up different sets of timeslots # Work with t0 in UTC for arithmetic. This does not change the results but is cleaner if someone looks # at intermediate results which may be misleading until passed through tz.normalize(). t0 = meeting.tz().localize( datetime.datetime.combine(meeting.date, datetime.time(11, 0)) ).astimezone(pytz.utc) dur = datetime.timedelta(hours=2) for room in room_groups[0]: TimeSlotFactory(meeting=meeting, location=room, duration=dur, time=t0) TimeSlotFactory(meeting=meeting, location=room, duration=dur, time=t0 + datetime.timedelta(days=1, hours=2)) TimeSlotFactory(meeting=meeting, location=room, duration=dur, time=t0 + datetime.timedelta(days=2, hours=4)) for room in room_groups[1]: TimeSlotFactory(meeting=meeting, location=room, duration=dur, time=t0 + datetime.timedelta(hours=1)) TimeSlotFactory(meeting=meeting, location=room, duration=dur, time=t0 + datetime.timedelta(days=1, hours=3)) TimeSlotFactory(meeting=meeting, location=room, duration=dur, time=t0 + datetime.timedelta(days=2, hours=5)) # And now put sessions in the timeslots for ts in meeting.timeslot_set.all(): SessionFactory( meeting=meeting, name=str(ts.pk), # label to identify where it started add_to_schedule=False, ).timeslotassignments.create( timeslot=ts, schedule=meeting.schedule, ) return meeting, room_groups def test_swap_timeslots(self): """Schedule timeslot groups should swap properly This tests the case currently exercised by the UI - where the rooms are grouped according to entirely equivalent sets of timeslots. Thus, there is always a matching timeslot for every (or no) room as long as the rooms parameter to the ajax call includes only one group. """ meeting, room_groups = self._setup_for_swap_timeslots() url = urlreverse('ietf.meeting.views.edit_meeting_schedule', kwargs=dict(num=meeting.number)) username = meeting.schedule.owner.user.username self.client.login(username=username, password=username + '+password') # Swap group 0's first and last sessions r = self.client.post( url, dict( action='swaptimeslots', origin_timeslot=str(room_groups[0][0].timeslot_set.first().pk), target_timeslot=str(room_groups[0][0].timeslot_set.last().pk), rooms=','.join([str(room.pk) for room in room_groups[0]]), ) ) self.assertEqual(r.status_code, 302) # Validate results for index, room in enumerate(room_groups[0]): timeslots = list(room.timeslot_set.all()) self.assertEqual(timeslots[0].session.name, str(timeslots[-1].pk), 'Session from last timeslot in room (0, {}) should now be in first'.format(index)) self.assertEqual(timeslots[-1].session.name, str(timeslots[0].pk), 'Session from first timeslot in room (0, {}) should now be in last'.format(index)) self.assertEqual( [ts.session.name for ts in timeslots[1:-1]], [str(ts.pk) for ts in timeslots[1:-1]], 'Sessions in middle timeslots should be unchanged' ) for index, room in enumerate(room_groups[1]): timeslots = list(room.timeslot_set.all()) self.assertFalse( any(ts.session is None for ts in timeslots), "Sessions in other room group's timeslots should still be assigned" ) self.assertEqual( [ts.session.name for ts in timeslots], [str(ts.pk) for ts in timeslots], "Sessions in other room group's timeslots should be unchanged" ) def test_swap_timeslots_denies_past(self): """Swapping past timeslots is not allowed for an official schedule""" meeting, room_groups = self._setup_for_swap_timeslots() # clone official schedule as an unofficial schedule Schedule.objects.create( name='unofficial', owner=meeting.schedule.owner, meeting=meeting, base=meeting.schedule.base, origin=meeting.schedule, ) official_url = urlreverse('ietf.meeting.views.edit_meeting_schedule', kwargs=dict(num=meeting.number)) unofficial_url = urlreverse('ietf.meeting.views.edit_meeting_schedule', kwargs=dict(num=meeting.number, owner=str(meeting.schedule.owner.email()), name='unofficial')) username = meeting.schedule.owner.user.username self.client.login(username=username, password=username + '+password') # Swap group 0's first and last sessions, first in the past right_now = self._right_now_in(meeting.tz()) for room in room_groups[0]: ts = room.timeslot_set.last() ts.time = right_now - datetime.timedelta(minutes=5) ts.save() # timeslot_set is ordered by -time, so check that we know which is past/future self.assertTrue(room_groups[0][0].timeslot_set.last().time < right_now) self.assertTrue(room_groups[0][0].timeslot_set.first().time > right_now) post_data = dict( action='swaptimeslots', origin_timeslot=str(room_groups[0][0].timeslot_set.first().pk), target_timeslot=str(room_groups[0][0].timeslot_set.last().pk), rooms=','.join([str(room.pk) for room in room_groups[0]]), ) r = self.client.post(official_url, post_data) self.assertContains(r, "Can't swap these timeslots.", status_code=400) # same request should succeed for an unofficial schedule r = self.client.post(unofficial_url, post_data) self.assertEqual(r.status_code, 302) # now with origin/target reversed post_data = dict( action='swaptimeslots', origin_timeslot=str(room_groups[0][0].timeslot_set.last().pk), target_timeslot=str(room_groups[0][0].timeslot_set.first().pk), rooms=','.join([str(room.pk) for room in room_groups[0]]), ) r = self.client.post(official_url, post_data) self.assertContains(r, "Can't swap these timeslots.", status_code=400) # same request should succeed for an unofficial schedule r = self.client.post(unofficial_url, post_data) self.assertEqual(r.status_code, 302) # now with the "past" timeslot less than MEETING_SESSION_LOCK_TIME in the future for room in room_groups[0]: ts = room.timeslot_set.last() ts.time = right_now + datetime.timedelta(minutes=9) # must be < MEETING_SESSION_LOCK_TIME ts.save() self.assertTrue(room_groups[0][0].timeslot_set.last().time < right_now + settings.MEETING_SESSION_LOCK_TIME) self.assertTrue(room_groups[0][0].timeslot_set.first().time > right_now + settings.MEETING_SESSION_LOCK_TIME) post_data = dict( action='swaptimeslots', origin_timeslot=str(room_groups[0][0].timeslot_set.first().pk), target_timeslot=str(room_groups[0][0].timeslot_set.last().pk), rooms=','.join([str(room.pk) for room in room_groups[0]]), ) r = self.client.post(official_url, post_data) self.assertContains(r, "Can't swap these timeslots.", status_code=400) # now with both in the past for room in room_groups[0]: ts = room.timeslot_set.last() ts.time = right_now - datetime.timedelta(minutes=5) ts.save() ts = room.timeslot_set.first() ts.time = right_now - datetime.timedelta(hours=1) ts.save() past_slots = room_groups[0][0].timeslot_set.filter(time__lt=right_now) self.assertEqual(len(past_slots), 2, 'Need two timeslots in the past!') post_data = dict( action='swaptimeslots', origin_timeslot=str(past_slots[0].pk), target_timeslot=str(past_slots[1].pk), rooms=','.join([str(room.pk) for room in room_groups[0]]), ) r = self.client.post(official_url, post_data) self.assertContains(r, "Can't swap these timeslots.", status_code=400) # same request should succeed for an unofficial schedule r = self.client.post(unofficial_url, post_data) self.assertEqual(r.status_code, 302) def test_swap_timeslots_handles_unmatched(self): """Sessions in unmatched timeslots should be unassigned when swapped This more generally tests the back end by exercising the situation where a timeslot in the affected rooms does not have an equivalent timeslot target. This is not used by the UI as of now (2021-06-22), but should function correctly. """ meeting, room_groups = self._setup_for_swap_timeslots() # Remove a timeslot and session from only one room in group 0 ts_to_remove = room_groups[0][1].timeslot_set.last() ts_to_remove.session.delete() ts_to_remove.delete() # our object still exists but has no db object # Add a matching timeslot to group 1 so we can be sure it's being ignored. # If not, this session will be unassigned when we swap timeslots on group 0. new_ts = TimeSlotFactory( meeting=meeting, location=room_groups[1][0], duration=ts_to_remove.duration, time=ts_to_remove.time, ) SessionFactory( meeting=meeting, name=str(new_ts.pk), add_to_schedule=False, ).timeslotassignments.create( timeslot=new_ts, schedule=meeting.schedule, ) url = urlreverse('ietf.meeting.views.edit_meeting_schedule', kwargs=dict(num=meeting.number)) username = meeting.schedule.owner.user.username self.client.login(username=username, password=username + '+password') # Now swap between first and last timeslots in group 0 r = self.client.post( url, dict( action='swaptimeslots', origin_timeslot=str(room_groups[0][0].timeslot_set.first().pk), target_timeslot=str(room_groups[0][0].timeslot_set.last().pk), rooms=','.join([str(room.pk) for room in room_groups[0]]), ) ) self.assertEqual(r.status_code, 302) # Validate results for index, room in enumerate(room_groups[0]): timeslots = list(room.timeslot_set.all()) if index == 1: # special case - this has no matching timeslot because we deleted it above self.assertIsNone(timeslots[0].session, 'Unmatched timeslot should be empty after swap') session_that_should_be_unassigned = Session.objects.get(name=str(timeslots[0].pk)) self.assertEqual(session_that_should_be_unassigned.timeslotassignments.count(), 0, 'Session that was in an unmatched timeslot should now be unassigned') # check from 2nd timeslot to the last since we deleted the original last timeslot self.assertEqual( [ts.session.name for ts in timeslots[1:]], [str(ts.pk) for ts in timeslots[1:]], 'Sessions in middle timeslots should be unchanged' ) else: self.assertEqual(timeslots[0].session.name, str(timeslots[-1].pk), 'Session from last timeslot in room (0, {}) should now be in first'.format(index)) self.assertEqual(timeslots[-1].session.name, str(timeslots[0].pk), 'Session from first timeslot in room (0, {}) should now be in last'.format(index)) self.assertEqual( [ts.session.name for ts in timeslots[1:-1]], [str(ts.pk) for ts in timeslots[1:-1]], 'Sessions in middle timeslots should be unchanged' ) # Still should have no effect on other rooms, even if they matched a timeslot for index, room in enumerate(room_groups[1]): timeslots = list(room.timeslot_set.all()) self.assertFalse( any(ts.session is None for ts in timeslots), "Sessions in other room group's timeslots should still be assigned" ) self.assertEqual( [ts.session.name for ts in timeslots], [str(ts.pk) for ts in timeslots], "Sessions in other room group's timeslots should be unchanged" ) def test_swap_days_denies_past(self): """Swapping past days is not allowed for an official schedule""" meeting, room_groups = self._setup_for_swap_timeslots() # clone official schedule as an unofficial schedule Schedule.objects.create( name='unofficial', owner=meeting.schedule.owner, meeting=meeting, base=meeting.schedule.base, origin=meeting.schedule, ) official_url = urlreverse('ietf.meeting.views.edit_meeting_schedule', kwargs=dict(num=meeting.number)) unofficial_url = urlreverse('ietf.meeting.views.edit_meeting_schedule', kwargs=dict(num=meeting.number, owner=str(meeting.schedule.owner.email()), name='unofficial')) username = meeting.schedule.owner.user.username self.client.login(username=username, password=username + '+password') # Swap group 0's first and last sessions, first in the past right_now = self._right_now_in(meeting.tz()) yesterday = right_now.date() - datetime.timedelta(days=1) day_before = right_now.date() - datetime.timedelta(days=2) for room in room_groups[0]: ts = room.timeslot_set.last() # Calculation keeps local clock time, shifted to a different day. ts.time = meeting.tz().localize( datetime.datetime.combine( yesterday, ts.time.astimezone(meeting.tz()).time() ), ) ts.save() # timeslot_set is ordered by -time, so check that we know which is past/future self.assertTrue(room_groups[0][0].timeslot_set.last().time < right_now) self.assertTrue(room_groups[0][0].timeslot_set.first().time > right_now) post_data = dict( action='swapdays', source_day=yesterday.isoformat(), target_day=room_groups[0][0].timeslot_set.first().time.date().isoformat(), ) r = self.client.post(official_url, post_data) self.assertContains(r, "Can't swap these days.", status_code=400) # same request should succeed for an unofficial schedule r = self.client.post(unofficial_url, post_data) self.assertEqual(r.status_code, 302) # now with origin/target reversed post_data = dict( action='swapdays', source_day=room_groups[0][0].timeslot_set.first().time.date().isoformat(), target_day=yesterday.isoformat(), rooms=','.join([str(room.pk) for room in room_groups[0]]), ) r = self.client.post(official_url, post_data) self.assertContains(r, "Can't swap these days.", status_code=400) # same request should succeed for an unofficial schedule r = self.client.post(unofficial_url, post_data) self.assertEqual(r.status_code, 302) # now with both in the past for room in room_groups[0]: ts = room.timeslot_set.first() ts.time = meeting.tz().localize( datetime.datetime.combine( day_before, ts.time.astimezone(meeting.tz()).time(), ) ) ts.save() past_slots = room_groups[0][0].timeslot_set.filter(time__lt=right_now) self.assertEqual(len(past_slots), 2, 'Need two timeslots in the past!') post_data = dict( action='swapdays', source_day=yesterday.isoformat(), target_day=day_before.isoformat(), ) r = self.client.post(official_url, post_data) self.assertContains(r, "Can't swap these days.", status_code=400) # same request should succeed for an unofficial schedule r = self.client.post(unofficial_url, post_data) self.assertEqual(r.status_code, 302) def _decode_json_response(self, r): try: return json.loads(r.content.decode()) except json.JSONDecodeError as err: self.fail('Response was not valid JSON: {}'.format(err)) @staticmethod def _right_now_in(tzinfo): right_now = timezone.now().astimezone(tzinfo) return right_now def test_assign_session(self): """Allow assignment to future timeslots only for official schedule""" meeting = MeetingFactory( type_id='ietf', date=(timezone.now() - datetime.timedelta(days=1)).date(), days=3, ) right_now = self._right_now_in(meeting.tz()) schedules = dict( official=meeting.schedule, unofficial=ScheduleFactory(meeting=meeting, owner=meeting.schedule.owner), ) timeslots = dict( past=TimeSlotFactory(meeting=meeting, time=right_now - datetime.timedelta(hours=1)), future=TimeSlotFactory(meeting=meeting, time=right_now + datetime.timedelta(hours=1)), ) url_for = lambda sched: urlreverse( 'ietf.meeting.views.edit_meeting_schedule', kwargs=dict( num=meeting.number, owner=str(sched.owner.email()), name=sched.name, ) ) post_data = lambda ts: dict( action='assign', session=str(SessionFactory(meeting=meeting, add_to_schedule=False).pk), timeslot=str(ts.pk), ) username = meeting.schedule.owner.user.username self.assertTrue(self.client.login(username=username, password=username + '+password')) # past timeslot, official schedule: reject r = self.client.post(url_for(schedules['official']), post_data(timeslots['past'])) self.assertEqual(r.status_code, 400) self.assertEqual( self._decode_json_response(r), dict(success=False, error="Can't assign to this timeslot."), ) # past timeslot, unofficial schedule: allow r = self.client.post(url_for(schedules['unofficial']), post_data(timeslots['past'])) self.assertEqual(r.status_code, 200) self.assertTrue(self._decode_json_response(r)['success']) # future timeslot, official schedule: allow r = self.client.post(url_for(schedules['official']), post_data(timeslots['future'])) self.assertEqual(r.status_code, 200) self.assertTrue(self._decode_json_response(r)['success']) # future timeslot, unofficial schedule: allow r = self.client.post(url_for(schedules['unofficial']), post_data(timeslots['future'])) self.assertEqual(r.status_code, 200) self.assertTrue(self._decode_json_response(r)['success']) def test_reassign_session(self): """Do not allow assignment of past sessions for official schedule""" meeting = MeetingFactory( type_id='ietf', date=(timezone.now() - datetime.timedelta(days=1)).date(), days=3, ) right_now = self._right_now_in(meeting.tz()) schedules = dict( official=meeting.schedule, unofficial=ScheduleFactory(meeting=meeting, owner=meeting.schedule.owner), ) timeslots = dict( past=TimeSlotFactory(meeting=meeting, time=right_now - datetime.timedelta(hours=1)), other_past=TimeSlotFactory(meeting=meeting, time=right_now - datetime.timedelta(hours=2)), barely_future=TimeSlotFactory(meeting=meeting, time=right_now + datetime.timedelta(minutes=9)), future=TimeSlotFactory(meeting=meeting, time=right_now + datetime.timedelta(hours=1)), other_future=TimeSlotFactory(meeting=meeting, time=right_now + datetime.timedelta(hours=2)), ) self.assertLess( timeslots['barely_future'].time - right_now, settings.MEETING_SESSION_LOCK_TIME, '"barely_future" timeslot is too far in the future. Check MEETING_SESSION_LOCK_TIME settings', ) url_for = lambda sched: urlreverse( 'ietf.meeting.views.edit_meeting_schedule', kwargs=dict( num=meeting.number, owner=str(sched.owner.email()), name=sched.name, ) ) def _new_session_in(timeslot, schedule): return SchedTimeSessAssignment.objects.create( schedule=schedule, session=SessionFactory(meeting=meeting, add_to_schedule=False), timeslot=timeslot, ).session post_data = lambda session, new_ts: dict( action='assign', session=str(session.pk), timeslot=str(new_ts.pk), ) username = meeting.schedule.owner.user.username self.assertTrue(self.client.login(username=username, password=username + '+password')) # past session to past timeslot, official: not allowed session = _new_session_in(timeslots['past'], schedules['official']) r = self.client.post(url_for(schedules['official']), post_data(session, timeslots['other_past'])) self.assertEqual(r.status_code, 400) self.assertEqual( self._decode_json_response(r), dict(success=False, error="Can't assign to this timeslot."), ) session.delete() # takes the SchedTimeSessAssignment with it # past session to future timeslot, official: not allowed session = _new_session_in(timeslots['past'], schedules['official']) r = self.client.post(url_for(schedules['official']), post_data(session, timeslots['future'])) self.assertEqual(r.status_code, 400) self.assertEqual( self._decode_json_response(r), dict(success=False, error="Can't reassign this session."), ) session.delete() # takes the SchedTimeSessAssignment with it # future session to past, timeslot, official: not allowed session = _new_session_in(timeslots['future'], schedules['official']) r = self.client.post(url_for(schedules['official']), post_data(session, timeslots['past'])) self.assertEqual(r.status_code, 400) self.assertEqual( self._decode_json_response(r), dict(success=False, error="Can't assign to this timeslot."), ) session.delete() # takes the SchedTimeSessAssignment with it # future session to future timeslot, unofficial: allowed session = _new_session_in(timeslots['future'], schedules['unofficial']) r = self.client.post(url_for(schedules['unofficial']), post_data(session, timeslots['other_future'])) self.assertEqual(r.status_code, 200) self.assertTrue(self._decode_json_response(r)['success']) session.delete() # takes the SchedTimeSessAssignment with it # future session to barely future timeslot, official: not allowed session = _new_session_in(timeslots['future'], schedules['official']) r = self.client.post(url_for(schedules['official']), post_data(session, timeslots['barely_future'])) self.assertEqual(r.status_code, 400) self.assertEqual( self._decode_json_response(r), dict(success=False, error="Can't assign to this timeslot."), ) session.delete() # takes the SchedTimeSessAssignment with it # future session to future timeslot, unofficial: allowed session = _new_session_in(timeslots['future'], schedules['unofficial']) r = self.client.post(url_for(schedules['unofficial']), post_data(session, timeslots['barely_future'])) self.assertEqual(r.status_code, 200) self.assertTrue(self._decode_json_response(r)['success']) session.delete() # takes the SchedTimeSessAssignment with it # past session to past timeslot, unofficial: allowed session = _new_session_in(timeslots['past'], schedules['unofficial']) r = self.client.post(url_for(schedules['unofficial']), post_data(session, timeslots['other_past'])) self.assertEqual(r.status_code, 200) self.assertTrue(self._decode_json_response(r)['success']) session.delete() # takes the SchedTimeSessAssignment with it # past session to future timeslot, unofficial: allowed session = _new_session_in(timeslots['past'], schedules['unofficial']) r = self.client.post(url_for(schedules['unofficial']), post_data(session, timeslots['future'])) self.assertEqual(r.status_code, 200) self.assertTrue(self._decode_json_response(r)['success']) session.delete() # takes the SchedTimeSessAssignment with it # future session to past timeslot, unofficial: allowed session = _new_session_in(timeslots['future'], schedules['unofficial']) r = self.client.post(url_for(schedules['unofficial']), post_data(session, timeslots['past'])) self.assertEqual(r.status_code, 200) self.assertTrue(self._decode_json_response(r)['success']) session.delete() # takes the SchedTimeSessAssignment with it # future session to future timeslot, unofficial: allowed session = _new_session_in(timeslots['future'], schedules['unofficial']) r = self.client.post(url_for(schedules['unofficial']), post_data(session, timeslots['other_future'])) self.assertEqual(r.status_code, 200) self.assertTrue(self._decode_json_response(r)['success']) session.delete() # takes the SchedTimeSessAssignment with it def test_unassign_session(self): """Allow unassignment only of future timeslots for official schedule""" meeting = MeetingFactory( type_id='ietf', date=(timezone.now() - datetime.timedelta(days=1)).date(), days=3, ) right_now = self._right_now_in(meeting.tz()) schedules = dict( official=meeting.schedule, unofficial=ScheduleFactory(meeting=meeting, owner=meeting.schedule.owner), ) timeslots = dict( past=TimeSlotFactory(meeting=meeting, time=right_now - datetime.timedelta(hours=1)), future=TimeSlotFactory(meeting=meeting, time=right_now + datetime.timedelta(hours=1)), barely_future=TimeSlotFactory(meeting=meeting, time=right_now + datetime.timedelta(minutes=9)), ) self.assertLess( timeslots['barely_future'].time - right_now, settings.MEETING_SESSION_LOCK_TIME, '"barely_future" timeslot is too far in the future. Check MEETING_SESSION_LOCK_TIME settings', ) url_for = lambda sched: urlreverse( 'ietf.meeting.views.edit_meeting_schedule', kwargs=dict( num=meeting.number, owner=str(sched.owner.email()), name=sched.name, ) ) post_data = lambda ts, sched: dict( action='unassign', session=str( SchedTimeSessAssignment.objects.create( schedule=sched, timeslot=ts, session=SessionFactory(meeting=meeting, add_to_schedule=False), ).session.pk ), ) username = meeting.schedule.owner.user.username self.assertTrue(self.client.login(username=username, password=username + '+password')) # past session, official schedule: reject r = self.client.post(url_for(schedules['official']), post_data(timeslots['past'], schedules['official'])) self.assertEqual(r.status_code, 400) self.assertEqual( self._decode_json_response(r), dict(success=False, error="Can't unassign this session."), ) # past timeslot, unofficial schedule: allow r = self.client.post(url_for(schedules['unofficial']), post_data(timeslots['past'], schedules['unofficial'])) self.assertEqual(r.status_code, 200) self.assertTrue(self._decode_json_response(r)['success']) # barely future session, official schedule: reject r = self.client.post(url_for(schedules['official']), post_data(timeslots['barely_future'], schedules['official'])) self.assertEqual(r.status_code, 400) self.assertEqual( self._decode_json_response(r), dict(success=False, error="Can't unassign this session."), ) # barely future timeslot, unofficial schedule: allow r = self.client.post(url_for(schedules['unofficial']), post_data(timeslots['barely_future'], schedules['unofficial'])) self.assertEqual(r.status_code, 200) self.assertTrue(self._decode_json_response(r)['success']) # future timeslot, official schedule: allow r = self.client.post(url_for(schedules['official']), post_data(timeslots['future'], schedules['official'])) self.assertEqual(r.status_code, 200) self.assertTrue(self._decode_json_response(r)['success']) # future timeslot, unofficial schedule: allow r = self.client.post(url_for(schedules['unofficial']), post_data(timeslots['future'], schedules['unofficial'])) self.assertEqual(r.status_code, 200) self.assertTrue(self._decode_json_response(r)['success']) def test_editor_with_no_timeslots(self): """Schedule editor should not crash when there are no timeslots""" meeting = MeetingFactory( type_id='ietf', date=date_today() + datetime.timedelta(days=7), populate_schedule=False, ) meeting.schedule = ScheduleFactory(meeting=meeting) meeting.save() SessionFactory(meeting=meeting, add_to_schedule=False) self.assertEqual(meeting.timeslot_set.count(), 0, 'Test problem - meeting should not have any timeslots') url = urlreverse('ietf.meeting.views.edit_meeting_schedule', kwargs={'num': meeting.number}) self.assertTrue(self.client.login(username='secretary', password='secretary+password')) r = self.client.get(url) self.assertEqual(r.status_code, 200) self.assertContains(r, 'No timeslots exist') self.assertContains(r, urlreverse('ietf.meeting.views.edit_timeslots', kwargs={'num': meeting.number})) def test_editor_time_zone(self): """Agenda editor should show meeting time zone""" time_zone = 'Etc/GMT+8' meeting_tz = ZoneInfo(time_zone) meeting = MeetingFactory( type_id='ietf', date=date_today(meeting_tz) + datetime.timedelta(days=7), populate_schedule=False, time_zone=time_zone, ) meeting.schedule = ScheduleFactory(meeting=meeting) meeting.save() timeslot = TimeSlotFactory(meeting=meeting) ts_start = timeslot.time.astimezone(meeting_tz) ts_end = timeslot.end_time().astimezone(meeting_tz) url = urlreverse('ietf.meeting.views.edit_meeting_schedule', kwargs={'num': meeting.number}) self.assertTrue(self.client.login(username='secretary', password='secretary+password')) r = self.client.get(url) self.assertEqual(r.status_code, 200) pq = PyQuery(r.content) day_header = pq('.day-flow .day-label') self.assertIn(ts_start.strftime('%A'), day_header.text()) day_swap = day_header.find('.swap-days') self.assertEqual(day_swap.attr('data-dayid'), ts_start.date().isoformat()) self.assertEqual(day_swap.attr('data-start'), ts_start.date().isoformat()) time_label = pq('.day-flow .time-header .time-label') self.assertEqual(len(time_label), 1) # strftime() does not seem to support hours without leading 0, so do this manually time_label_string = f'{ts_start.hour:d}:{ts_start.minute:02d} - {ts_end.hour:d}:{ts_end.minute:02d}' self.assertIn(time_label_string, time_label.text()) self.assertEqual(time_label.attr('data-start'), ts_start.astimezone(datetime.timezone.utc).isoformat()) self.assertEqual(time_label.attr('data-end'), ts_end.astimezone(datetime.timezone.utc).isoformat()) ts_swap = time_label.find('.swap-timeslot-col') origin_label = ts_swap.attr('data-origin-label') # testing the exact date in origin_label is hard because Django's date filter uses # different month formats than Python's strftime, so just check a couple parts. self.assertIn(ts_start.strftime('%A'), origin_label) self.assertIn(f'{ts_start.hour:d}:{ts_start.minute:02d}-{ts_end.hour:d}:{ts_end.minute:02d}', origin_label) timeslot_elt = pq(f'#timeslot{timeslot.pk}') self.assertEqual(len(timeslot_elt), 1) self.assertEqual(timeslot_elt.attr('data-start'), ts_start.astimezone(datetime.timezone.utc).isoformat()) self.assertEqual(timeslot_elt.attr('data-end'), ts_end.astimezone(datetime.timezone.utc).isoformat()) timeslot_label = pq(f'#timeslot{timeslot.pk} .time-label') self.assertEqual(len(timeslot_label), 1) self.assertIn(time_label_string, timeslot_label.text()) class EditTimeslotsTests(TestCase): def login(self, username='secretary'): """Log in with permission to edit timeslots""" self.client.login(username=username, password='{}+password'.format(username)) @staticmethod def edit_timeslots_url(meeting): return urlreverse('ietf.meeting.views.edit_timeslots', kwargs={'num': meeting.number}) @staticmethod def edit_timeslot_url(ts: TimeSlot): return urlreverse('ietf.meeting.views.edit_timeslot', kwargs={'num': ts.meeting.number, 'slot_id': ts.pk}) @staticmethod def create_timeslots_url(meeting): return urlreverse('ietf.meeting.views.create_timeslot', kwargs={'num': meeting.number}) @staticmethod def create_bare_meeting(number=120) -> Meeting: """Create a basic IETF meeting""" return MeetingFactory( type_id='ietf', number=number, date=date_today() + datetime.timedelta(days=10), populate_schedule=False, ) @staticmethod def create_initial_schedule(meeting): """Create initial / base schedule in the same manner as through the UI""" owner = User.objects.get(username='secretary').person base_schedule = Schedule.objects.create( meeting=meeting, name='base', owner=owner, visible=True, public=True, ) schedule = Schedule.objects.create(meeting = meeting, name = "%s-1" % slugify(owner.plain_name()), owner = owner, visible = True, public = True, base = base_schedule, ) meeting.schedule = schedule meeting.save() def create_meeting(self, number=120): """Create a meeting ready for adding timeslots in the usual workflow""" meeting = self.create_bare_meeting(number=number) RoomFactory.create_batch(8, meeting=meeting) self.create_initial_schedule(meeting) # retrieve meeting from DB so it goes through Django's processing return Meeting.objects.get(pk=meeting.pk) def test_view_permissions(self): """Only the secretary should be able to edit timeslots""" # test prep and helper method usernames_to_reject = [ 'plain', RoleFactory(name_id='chair').person.user.username, RoleFactory(name_id='ad', group__type_id='area').person.user.username, ] meeting = self.create_bare_meeting() url = self.edit_timeslots_url(meeting) def _assert_permissions(comment): self.client.logout() logged_in_username = '' try: # loop through all the usernames that should be rejected for username in usernames_to_reject: login_testing_unauthorized(self, username, url) logged_in_username = username # test the last username to reject and log in as secretary login_testing_unauthorized(self, 'secretary', url) except AssertionError: # give a better failure message self.fail( '{} should not be able to access the edit timeslots page {}'.format( logged_in_username, comment, ) ) r = self.client.get(url) # confirm secretary can retrieve the page self.assertEqual(r.status_code, 200, 'secretary should be able to access the edit timeslots page {}'.format(comment)) # Actual tests here _assert_permissions('without schedule') # first test without a meeting schedule self.create_initial_schedule(meeting) _assert_permissions('with schedule') # then test with a meeting schedule def test_linked_from_agenda_list(self): """The edit timeslots view should be linked from the agenda list view""" ad = RoleFactory(name_id='ad', group__type_id='area').person meeting = self.create_bare_meeting() self.create_initial_schedule(meeting) url = urlreverse('ietf.meeting.views.list_schedules', kwargs={'num': meeting.number}) # Should have no link when logged in as area director self.login(ad.user.username) r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertEqual( len(q('a[href="{}"]'.format(self.edit_timeslots_url(meeting)))), 0, 'User who cannot edit timeslots should not see a link to the edit timeslots page' ) # Should have a link when logged in as secretary self.login() r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertGreaterEqual( len(q('a[href="{}"]'.format(self.edit_timeslots_url(meeting)))), 1, 'Must be at least one link from the agenda list page to the edit timeslots page' ) def assert_helpful_url(self, response, helpful_url, message): q = PyQuery(response.content) self.assertGreaterEqual( len(q('.timeslot-edit a[href="{}"]'.format(helpful_url))), 1, message, ) def test_with_no_rooms(self): """Editor should be helpful when there are no rooms yet""" meeting = self.create_bare_meeting() self.login() # with no schedule, should get a link to the meeting page in the secr app until we can # handle this situation in the meeting app r = self.client.get(self.edit_timeslots_url(meeting)) self.assertEqual(r.status_code, 200) self.assert_helpful_url( r, urlreverse('ietf.secr.meetings.views.view', kwargs={'meeting_id': meeting.number}), 'Must be a link to a helpful URL when there are no rooms and no schedule' ) # with a schedule, should get a link to the create rooms page in the secr app self.create_initial_schedule(meeting) r = self.client.get(self.edit_timeslots_url(meeting)) self.assertEqual(r.status_code, 200) self.assert_helpful_url( r, urlreverse('ietf.secr.meetings.views.rooms', kwargs={'meeting_id': meeting.number, 'schedule_name': meeting.schedule.name}), 'Must be a link to a helpful URL when there are no rooms' ) def test_with_no_timeslots(self): """Editor should be helpful when there are rooms but no timeslots yet""" meeting = self.create_bare_meeting() RoomFactory(meeting=meeting) self.login() helpful_url = self.create_timeslots_url(meeting) # with no schedule, should get a link to the meeting page in the secr app until we can # handle this situation in the meeting app r = self.client.get(self.edit_timeslots_url(meeting)) self.assertEqual(r.status_code, 200) self.assert_helpful_url(r, helpful_url, 'Must be a link to a helpful URL when there are no timeslots and no schedule') # with a schedule, should get a link to the create rooms page in the secr app self.create_initial_schedule(meeting) r = self.client.get(self.edit_timeslots_url(meeting)) self.assertEqual(r.status_code, 200) self.assert_helpful_url(r, helpful_url, 'Must be a link to a helpful URL when there are no timeslots') def assert_required_links_present(self, response, meeting): """Assert that required links on the editor page are present""" q = PyQuery(response.content) self.assertGreaterEqual( len(q('a[href="{}"]'.format(self.create_timeslots_url(meeting)))), 1, 'Timeslot edit page should have a link to create timeslots' ) self.assertGreaterEqual( len(q('a[href="{}"]'.format(urlreverse('ietf.secr.meetings.views.rooms', kwargs={'meeting_id': meeting.number, 'schedule_name': meeting.schedule.name})) )), 1, 'Timeslot edit page should have a link to edit rooms' ) def test_required_links_present(self): """Editor should have links to create timeslots and edit rooms""" meeting = self.create_meeting() self.create_initial_schedule(meeting) RoomFactory.create_batch(8, meeting=meeting) self.login() r = self.client.get(self.edit_timeslots_url(meeting)) self.assertEqual(r.status_code, 200) self.assert_required_links_present(r, meeting) def test_shows_timeslots(self): """Timeslots should be displayed properly""" def _col_index(elt): """Find the column index of an element in its table row First column is 1 """ selector = 'td, th' # accept both td and th elements col_elt = elt.closest(selector) tr = col_elt.parent('tr') return 1 + tr.children(selector).index(col_elt[0]) # [0] gets bare element meeting = self.create_meeting() # add some timeslots times = [datetime.time(hour=h) for h in (11, 14)] days = [meeting.get_meeting_date(ii) for ii in range(meeting.days)] timeslots = [] duration = datetime.timedelta(minutes=90) for room in meeting.room_set.all(): for day in days: timeslots.extend( TimeSlotFactory( meeting=meeting, location=room, time=meeting.tz().localize(datetime.datetime.combine(day, t)), duration=duration, ) for t in times ) # get the page under test self.login() r = self.client.get(self.edit_timeslots_url(meeting)) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) table = q('#timeslot-table') self.assertEqual(len(table), 1, 'Exactly one timeslot-table required') table = table.eq(0) # check the day super-column headings day_headings = table.find('.day-label') self.assertEqual(len(day_headings), len(days)) day_columns = dict() # map datetime to iterable with table col indices for that day next_col = _col_index(day_headings.eq(0)) # find column of the first day for day, heading in zip(days, day_headings.items()): self.assertIn(day.strftime('%a'), heading.text(), 'Weekday abbrev for {} not found in heading'.format(day)) self.assertIn(day.strftime('%Y-%m-%d'), heading.text(), 'Numeric date for {} not found in heading'.format(day)) cols = int(heading.attr('colspan')) # columns spanned by day header day_columns[day] = range(next_col, next_col + cols) next_col += cols # check the timeslot time headings time_headings = table.find('.time-label') self.assertEqual(len(time_headings), len(times) * len(days)) expected_columns = dict() # [date][time] element is expected column for a timeslot for day, columns in day_columns.items(): headings = time_headings.filter( # selector for children in any of the day's columns ','.join( ':nth-child({})'.format(col) for col in columns ) ) expected_columns[day] = dict() for time, heading in zip(times, headings.items()): self.assertIn(time.strftime('%H:%M'), heading.text(), 'Timeslot start {} not found for day {}'.format(time, day)) expected_columns[day][time] = _col_index(heading) # check that the expected timeslots are shown with expected info / ui features timeslot_elts = table.find('.timeslot') self.assertEqual(len(timeslot_elts), len(timeslots), 'Unexpected or missing timeslot elements') for ts in timeslots: pk_elts = timeslot_elts.filter('#timeslot{}'.format(ts.pk)) self.assertEqual(len(pk_elts), 1, 'Expect exactly one element for each timeslot') elt = pk_elts.eq(0) self.assertIn(ts.name, elt.text(), 'Timeslot name should appear in the element for {}'.format(ts)) self.assertIn(str(ts.type), elt.text(), 'Timeslot type should appear in the element for {}'.format(ts)) self.assertEqual(_col_index(elt), expected_columns[ts.time.date()][ts.time.time()], 'Timeslot {} is in the wrong column'.format(ts)) delete_btn = elt.find('.delete-button[data-delete-scope="timeslot"]') self.assertEqual(len(delete_btn), 1, 'Timeslot {} should have one delete button'.format(ts)) edit_btn = elt.find('a[href="{}"]'.format( urlreverse('ietf.meeting.views.edit_timeslot', kwargs=dict(num=meeting.number, slot_id=ts.pk)) )) self.assertEqual(len(edit_btn), 1, 'Timeslot {} should have one edit button'.format(ts)) # find the room heading for the row tr = elt.closest('tr') self.assertIn(ts.location.name, tr.children('th').eq(0).text(), 'Timeslot {} is not shown in the correct row'.format(ts)) def test_bulk_delete_buttons_exist(self): """Delete buttons for days and columns should be shown""" meeting = self.create_meeting() for day in range(meeting.days): TimeSlotFactory( meeting=meeting, location=meeting.room_set.first(), time=meeting.tz().localize( datetime.datetime.combine( meeting.get_meeting_date(day), datetime.time(hour=11), ) ), ) TimeSlotFactory( meeting=meeting, location=meeting.room_set.first(), time=meeting.tz().localize( datetime.datetime.combine( meeting.get_meeting_date(day), datetime.time(hour=14), ) ), ) self.login() r = self.client.get(self.edit_timeslots_url(meeting)) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) table = q('#timeslot-table') days = table.find('.day-label') self.assertEqual(len(days), meeting.days, 'Wrong number of day labels') for day_label in days.items(): self.assertEqual(len(day_label.find('.delete-button[data-delete-scope="day"]')), 1, 'No delete button for day {}'.format(day_label.text())) slots = table.find('.time-label') self.assertEqual(len(slots), 2 * meeting.days, 'Wrong number of slot labels') for slot_label in slots.items(): self.assertEqual(len(slot_label.find('.delete-button[data-delete-scope="column"]')), 1, 'No delete button for slot {}'.format(slot_label.text())) def test_timeslot_collision_flag(self): """Overlapping timeslots in a room should be flagged Only checks exact overlap because that is all we currently handle. The display puts overlapping but not exactly matching timeslots in separate columns which must be manually checked. """ meeting = self.create_bare_meeting() t1 = TimeSlotFactory(meeting=meeting) TimeSlotFactory(meeting=meeting, time=t1.time, duration=t1.duration, location=t1.location) TimeSlotFactory(meeting=meeting, time=t1.time, duration=t1.duration) # other location TimeSlotFactory(meeting=meeting, time=t1.time.replace(hour=t1.time.hour + 1), location=t1.location) # other time self.login() r = self.client.get(self.edit_timeslots_url(meeting)) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) slots = q('#timeslot-table .tscell') self.assertEqual(len(slots), 4) # one per location per distinct time collision = slots.filter('.timeslot-collision') no_collision = slots.filter(':not(.timeslot-collision)') self.assertEqual(len(collision), 1, 'Wrong number of timeslot collisions flagged') self.assertEqual(len(no_collision), 3, 'Wrong number of non-colliding timeslots') # check that the cell containing t1 is the one flagged as a conflict self.assertEqual(len(collision.find('#timeslot{}'.format(t1.pk))), 1, 'Wrong timeslot cell flagged as having a collision') def test_timeslot_in_use_flag(self): """Timeslots that are in use should be flagged""" meeting = self.create_meeting() # assign sessions to some timeslots empty, has_official, has_other = TimeSlotFactory.create_batch(3, meeting=meeting, location=meeting.room_set.first()) SchedTimeSessAssignment.objects.create( timeslot=has_official, session=SessionFactory(meeting=meeting, add_to_schedule=False), schedule=meeting.schedule, # official schedule ) SchedTimeSessAssignment.objects.create( timeslot=has_other, session=SessionFactory(meeting=meeting, add_to_schedule=False), schedule=ScheduleFactory(meeting=meeting), # not the official schedule ) # get the page self.login() r = self.client.get(self.edit_timeslots_url(meeting)) self.assertEqual(r.status_code, 200) # now check that all timeslots appear, flagged appropriately q = PyQuery(r.content) empty_elt = q('#timeslot{}'.format(empty.pk)) has_official_elt = q('#timeslot{}'.format(has_official.pk)) has_other_elt = q('#timeslot{}'.format(has_other.pk)) self.assertEqual(empty_elt.attr('data-unofficial-use'), 'false', 'Unused timeslot should not be in use') self.assertEqual(empty_elt.attr('data-official-use'), 'false', 'Unused timeslot should not be in use') self.assertEqual(has_other_elt.attr('data-unofficial-use'), 'true', 'Unofficially used timeslot should be flagged') self.assertEqual(has_other_elt.attr('data-official-use'), 'false', 'Unofficially used timeslot is not in official use') self.assertEqual(has_official_elt.attr('data-unofficial-use'), 'false', 'Officially used timeslot not in unofficial use') self.assertEqual(has_official_elt.attr('data-official-use'), 'true', 'Officially used timeslot should be flagged') def test_edit_timeslot(self): """Edit page should work as expected""" meeting = self.create_meeting() name_before = 'Name Classic (tm)' type_before = 'regular' time_utc = pytz.utc.localize(datetime.datetime.combine(meeting.date, datetime.time(hour=10))) time_before = time_utc.astimezone(meeting.tz()) duration_before = datetime.timedelta(minutes=60) show_location_before = True location_before = meeting.room_set.first() ts = TimeSlotFactory( meeting=meeting, name=name_before, type_id=type_before, time=time_before, duration=duration_before, show_location=show_location_before, location=location_before, ) self.login() url = self.edit_timeslot_url(ts) # check that sched parameter is preserved r = self.client.get(url) self.assertNotContains(r, '?sched=', status_code=200) r = self.client.get(url + '?sched=1234') self.assertContains(r, '?sched=1234', status_code=200) # could check in more detail name_after = 'New Name (tm)' type_after = 'plenary' time_after = (time_utc + datetime.timedelta(days=1, hours=2)).astimezone(meeting.tz()) duration_after = duration_before * 2 show_location_after = False location_after = meeting.room_set.last() post_data = dict( name=name_after, type=type_after, time_0=time_after.strftime('%Y-%m-%d'), # date for SplitDateTimeField time_1=time_after.strftime('%H:%M'), # time for SplitDateTimeField duration=str(duration_after), # show_location=show_location_after, # False values are omitted from form location=location_after.pk, ) r = self.client.post(url, data=post_data) self.assertEqual(r.status_code, 302) # expect redirect to timeslot edit url self.assertEqual(r['Location'], self.edit_timeslots_url(meeting), 'Expected to be redirected to meeting timeslots edit page') # check that we changed things self.assertNotEqual(name_before, name_after) self.assertNotEqual(type_before, type_after) self.assertNotEqual(time_before, time_after) self.assertNotEqual(duration_before, duration_after) self.assertNotEqual(location_before, location_after) # and that we have the new values ts = TimeSlot.objects.get(pk=ts.pk) self.assertEqual(ts.name, name_after) self.assertEqual(ts.type_id, type_after) self.assertEqual(ts.time, time_after) self.assertEqual(ts.duration, duration_after) self.assertEqual(ts.show_location, show_location_after) self.assertEqual(ts.location, location_after) # and check with sched param set r = self.client.post(url + '?sched=1234', data=post_data) self.assertEqual(r.status_code, 302) # expect redirect to timeslot edit url self.assertEqual(r['Location'], self.edit_timeslots_url(meeting) + '?sched=1234', 'Expected to be redirected to meeting timeslots edit page with sched param set') def test_invalid_edit_timeslot(self): meeting = self.create_bare_meeting() ts: TimeSlot = TimeSlotFactory(meeting=meeting, name='slot') # type: ignore[annotation-unchecked] self.login() r = self.client.post( self.edit_timeslot_url(ts), data=dict( name='', type=ts.type.pk, time_0=ts.time.strftime('%Y-%m-%d'), time_1=ts.time.strftime('%H:%M'), duration=str(ts.duration), show_location=ts.show_location, location=str(ts.location.pk), ) ) self.assertContains(r, 'This field is required', status_code=400, msg_prefix='Missing name not properly rejected') r = self.client.post( self.edit_timeslot_url(ts), data=dict( name='different name', type='this is not a type id', time_0=ts.time.strftime('%Y-%m-%d'), time_1=ts.time.strftime('%H:%M'), duration=str(ts.duration), show_location=ts.show_location, location=str(ts.location.pk), ) ) self.assertContains(r, 'Select a valid choice', status_code=400, msg_prefix='Invalid type not properly rejected') r = self.client.post( self.edit_timeslot_url(ts), data=dict( name='different name', type=ts.type.pk, time_0='this is not a date', time_1=ts.time.strftime('%H:%M'), duration=str(ts.duration), show_location=ts.show_location, location=str(ts.location.pk), ) ) self.assertContains(r, 'Enter a valid date', status_code=400, msg_prefix='Invalid date not properly rejected') r = self.client.post( self.edit_timeslot_url(ts), data=dict( name='different name', type=ts.type.pk, time_0=ts.time.strftime('%Y-%m-%d'), time_1='this is not a time', duration=str(ts.duration), show_location=ts.show_location, location=str(ts.location.pk), ) ) self.assertContains(r, 'Enter a valid time', status_code=400, msg_prefix='Invalid time not properly rejected') r = self.client.post( self.edit_timeslot_url(ts), data=dict( name='different name', type=ts.type.pk, time_0=ts.time.strftime('%Y-%m-%d'), time_1=ts.time.strftime('%H:%M'), duration='this is not a duration', show_location=ts.show_location, location=str(ts.location.pk), ) ) self.assertContains(r, 'Enter a valid duration', status_code=400, msg_prefix='Invalid duration not properly rejected') r = self.client.post( self.edit_timeslot_url(ts), data=dict( name='different name', type=ts.type.pk, time_0=ts.time.strftime('%Y-%m-%d'), time_1=ts.time.strftime('%H:%M'), duration='26:00', # longer than 12 hours, show_location=ts.show_location, location=str(ts.location.pk), ) ) self.assertContains(r, 'Ensure this value is less than or equal to', status_code=400, msg_prefix='Overlong duration not properly rejected') r = self.client.post( self.edit_timeslot_url(ts), data=dict( name='different name', type=str(ts.type.pk), time_0=ts.time.strftime('%Y-%m-%d'), time_1=ts.time.strftime('%H:%M'), duration=str(ts.duration), show_location=ts.show_location, location='this is not a location', ) ) self.assertContains(r, 'Select a valid choice', status_code=400, msg_prefix='Invalid location not properly rejected') ts_after = meeting.timeslot_set.get(pk=ts.pk) self.assertEqual(ts.name, ts_after.name) self.assertEqual(ts.type, ts_after.type) self.assertEqual(ts.time, ts_after.time) self.assertEqual(ts.duration, ts_after.duration) self.assertEqual(ts.show_location, ts_after.show_location) self.assertEqual(ts.location, ts_after.location) def test_create_single_timeslot(self): """Creating a single timeslot should work""" meeting = self.create_meeting() timeslots_before = set(ts.pk for ts in meeting.timeslot_set.all()) url = self.create_timeslots_url(meeting) post_data = dict( name='some name', type='regular', days=str(meeting.date.toordinal()), time='14:37', duration='1:13', # does not include seconds show_location=True, locations=str(meeting.room_set.first().pk), ) self.login() # check that sched parameter is preserved r = self.client.get(url) self.assertNotContains(r, '?sched=', status_code=200) r = self.client.get(url + '?sched=1234') self.assertContains(r, '?sched=1234', status_code=200) # could check in more detail r = self.client.post(url, data=post_data) self.assertEqual(r.status_code, 302) self.assertEqual(r['Location'], self.edit_timeslots_url(meeting), 'Expected to be redirected to meeting timeslots edit page') self.assertEqual(meeting.timeslot_set.count(), len(timeslots_before) + 1) ts = meeting.timeslot_set.exclude(pk__in=timeslots_before).first() # only 1 self.assertEqual(ts.name, post_data['name']) self.assertEqual(ts.type_id, post_data['type']) self.assertEqual(str(ts.local_start_time().date().toordinal()), post_data['days']) self.assertEqual(ts.local_start_time().strftime('%H:%M'), post_data['time']) self.assertEqual(str(ts.duration), '{}:00'.format(post_data['duration'])) # add seconds self.assertEqual(ts.show_location, post_data['show_location']) self.assertEqual(str(ts.location.pk), post_data['locations']) # check again with sched parameter r = self.client.post(url + '?sched=1234', data=post_data) self.assertEqual(r.status_code, 302) self.assertEqual(r['Location'], self.edit_timeslots_url(meeting) + '?sched=1234', 'Expected to be redirected to meeting timeslots edit page with sched parameter set') def test_create_single_timeslot_outside_meeting_days(self): """Creating a single timeslot outside the official meeting days should work""" meeting = self.create_meeting() timeslots_before = set(ts.pk for ts in meeting.timeslot_set.all()) other_date = meeting.get_meeting_date(-7) post_data = dict( name='some name', type='regular', other_date=other_date.strftime('%Y-%m-%d'), time='14:37', duration='1:13', # does not include seconds show_location=True, locations=str(meeting.room_set.first().pk), ) self.login() r = self.client.post( self.create_timeslots_url(meeting), data=post_data, ) self.assertEqual(r.status_code, 302) self.assertEqual(r['Location'], self.edit_timeslots_url(meeting), 'Expected to be redirected to meeting timeslots edit page') self.assertEqual(meeting.timeslot_set.count(), len(timeslots_before) + 1) ts = meeting.timeslot_set.exclude(pk__in=timeslots_before).first() # only 1 self.assertEqual(ts.name, post_data['name']) self.assertEqual(ts.type_id, post_data['type']) self.assertEqual(ts.local_start_time().date(), other_date) self.assertEqual(ts.local_start_time().strftime('%H:%M'), post_data['time']) self.assertEqual(str(ts.duration), '{}:00'.format(post_data['duration'])) # add seconds self.assertEqual(ts.show_location, post_data['show_location']) self.assertEqual(str(ts.location.pk), post_data['locations']) def test_invalid_create_timeslot(self): meeting = self.create_bare_meeting() room_pk = str(RoomFactory(meeting=meeting).pk) timeslot_count = TimeSlot.objects.count() self.login() r = self.client.post( self.create_timeslots_url(meeting), data=dict( name='', type='regular', days=str(meeting.date.toordinal()), time='14:37', duration='1:13', # does not include seconds show_location=True, locations=room_pk, ) ) self.assertContains(r, 'This field is required', status_code=400, msg_prefix='Empty name not rejected properly') r = self.client.post( self.create_timeslots_url(meeting), data=dict( name='this is a name', type='this is not a type', days=str(meeting.date.toordinal()), time='14:37', duration='1:13', # does not include seconds show_location=True, locations=room_pk, ) ) self.assertContains(r, 'Select a valid choice', status_code=400, msg_prefix='Invalid type not rejected properly') r = self.client.post( self.create_timeslots_url(meeting), data=dict( name='this is a name', type='regular', # days='', time='14:37', duration='1:13', # does not include seconds show_location=True, locations=room_pk, ) ) self.assertContains(r, 'Please select a day or specify a date', status_code=400, msg_prefix='Missing date not rejected properly') r = self.client.post( self.create_timeslots_url(meeting), data=dict( name='this is a name', type='regular', days='this is not an ordinal date', time='14:37', duration='1:13', # does not include seconds show_location=True, locations=room_pk, ) ) self.assertContains(r, 'Select a valid choice', status_code=400, msg_prefix='Invalid day not rejected properly') r = self.client.post( self.create_timeslots_url(meeting), data=dict( name='this is a name', type='regular', days=[str(meeting.date.toordinal()), 'this is not an ordinal date'], time='14:37', duration='1:13', # does not include seconds show_location=True, locations=room_pk, ) ) self.assertContains(r, 'Select a valid choice', status_code=400, msg_prefix='Invalid day with valid day not rejected properly') r = self.client.post( self.create_timeslots_url(meeting), data=dict( name='this is a name', type='regular', days=str(meeting.date.toordinal()), other_date='this is not a date', time='14:37', duration='1:13', # does not include seconds show_location=True, locations=room_pk, ) ) self.assertContains(r, 'Enter a valid date', status_code=400, msg_prefix='Invalid other_date with valid days not rejected properly') r = self.client.post( self.create_timeslots_url(meeting), data=dict( name='this is a name', type='regular', days='this is not an ordinal date', other_date='2021-07-13', time='14:37', duration='1:13', # does not include seconds show_location=True, locations=room_pk, ) ) self.assertContains(r, 'Select a valid choice', status_code=400, msg_prefix='Invalid day with valid other_date not rejected properly') r = self.client.post( self.create_timeslots_url(meeting), data=dict( name='this is a name', type='regular', other_date='this is not a date', time='14:37', duration='1:13', # does not include seconds show_location=True, locations=room_pk, ) ) self.assertContains(r, 'Enter a valid date', status_code=400, msg_prefix='Invalid other_date not rejected properly') r = self.client.post( self.create_timeslots_url(meeting), data=dict( name='this is a name', type='regular', days=str(meeting.date.toordinal()), time='14:37', duration="ceci n'est pas une duree", show_location=True, locations=room_pk, ) ) self.assertContains(r, 'Enter a valid duration', status_code=400, msg_prefix='Invalid duration not rejected properly') r = self.client.post( self.create_timeslots_url(meeting), data=dict( name='this is a name', type='regular', days=str(meeting.date.toordinal()), time='14:37', duration="26:00", show_location=True, locations=room_pk, ) ) self.assertContains(r, 'Ensure this value is less than or equal to', status_code=400, msg_prefix='Overlong duration not rejected properly') r = self.client.post( self.create_timeslots_url(meeting), data=dict( name='this is a name', type='regular', days=str(meeting.date.toordinal()), time='14:37', duration="1:13", show_location=True, locations='this is not a room', ) ) self.assertContains(r, 'is not a valid value', status_code=400, msg_prefix='Invalid location not rejected properly') r = self.client.post( self.create_timeslots_url(meeting), data=dict( name='this is a name', type='regular', days=str(meeting.date.toordinal()), time='14:37', duration="1:13", show_location=True, locations=[room_pk, 'this is not a room'], ) ) self.assertContains(r, 'is not a valid value', status_code=400, msg_prefix='Invalid location with valid location not rejected properly') r = self.client.post( self.create_timeslots_url(meeting), data=dict( name='this is a name', type='regular', days=str(meeting.date.toordinal()), time='14:37', duration="1:13", show_location=True, ) ) self.assertContains(r, 'This field is required', status_code=400, msg_prefix='Missing location not rejected properly') self.assertEqual(TimeSlot.objects.count(), timeslot_count, 'TimeSlot unexpectedly created') def test_create_bulk_timeslots(self): """Creating multiple timeslots should work""" meeting = self.create_meeting() timeslots_before = set(ts.pk for ts in meeting.timeslot_set.all()) days = [meeting.get_meeting_date(n) for n in range(meeting.days)] other_date = meeting.get_meeting_date(-1) # date before start of meeting self.assertNotIn(other_date, days) locations = meeting.room_set.all() post_data = dict( name='some name', type='regular', days=[str(d.toordinal()) for d in days], other_date=other_date.strftime('%Y-%m-%d'), time='14:37', duration='1:13', # does not include seconds show_location=True, locations=[str(loc.pk) for loc in locations], ) self.login() r = self.client.post( self.create_timeslots_url(meeting), data=post_data, ) self.assertEqual(r.status_code, 302) self.assertEqual(r['Location'], self.edit_timeslots_url(meeting), 'Expected to be redirected to meeting timeslots edit page') days.append(other_date) new_slot_count = len(days) * len(locations) self.assertEqual(meeting.timeslot_set.count(), len(timeslots_before) + new_slot_count) day_locs = set((day, loc) for day in days for loc in locations) # cartesian product for ts in meeting.timeslot_set.exclude(pk__in=timeslots_before): self.assertEqual(ts.name, post_data['name']) self.assertEqual(ts.type_id, post_data['type']) self.assertEqual(ts.local_start_time().strftime('%H:%M'), post_data['time']) self.assertEqual(str(ts.duration), '{}:00'.format(post_data['duration'])) # add seconds self.assertEqual(ts.show_location, post_data['show_location']) self.assertIn(ts.local_start_time().date(), days) self.assertIn(ts.location, locations) self.assertIn((ts.time.date(), ts.location), day_locs, 'Duplicated day / location found') day_locs.discard((ts.time.date(), ts.location)) self.assertEqual(day_locs, set(), 'Not all day/location combinations created') def test_sched_param_preserved(self): meeting = MeetingFactory(type_id='ietf') url = urlreverse('ietf.meeting.views.edit_timeslots', kwargs={'num': meeting.number}) self.client.login(username='secretary', password='secretary+password') r = self.client.get(url) self.assertNotContains(r, '?sched=', status_code=200) self.assertNotContains(r, "Back to agenda") r = self.client.get(url + '?sched=1234') self.assertContains(r, '?sched=1234', status_code=200) # could check in more detail self.assertContains(r, "Back to agenda") def test_ajax_delete_timeslot(self): """AJAX call to delete timeslot should work""" meeting = self.create_bare_meeting() ts_to_del, ts_to_keep = TimeSlotFactory.create_batch(2, meeting=meeting) self.login() r = self.client.post( self.edit_timeslots_url(meeting), data=dict( action='delete', slot_id=str(ts_to_del.pk), ) ) self.assertEqual(r.status_code, 200) self.assertContains(r, 'Deleted TimeSlot {}'.format(ts_to_del.pk)) self.assertNotContains(r, 'Deleted TimeSlot {}'.format(ts_to_keep.pk)) self.assertEqual(meeting.timeslot_set.filter(pk=ts_to_del.pk).count(), 0, 'Timeslot not deleted') self.assertEqual(meeting.timeslot_set.filter(pk=ts_to_keep.pk).count(), 1, 'Extra timeslot deleted') def test_ajax_delete_timeslots(self): """AJAX call to delete several timeslots should work""" meeting = self.create_bare_meeting() ts_to_del = TimeSlotFactory.create_batch(5, meeting=meeting) ts_to_keep = TimeSlotFactory(meeting=meeting) self.login() r = self.client.post( self.edit_timeslots_url(meeting), data=dict( action='delete', slot_id=','.join(str(ts.pk) for ts in ts_to_del), ) ) self.assertEqual(r.status_code, 200) for ts in ts_to_del: self.assertContains(r, 'Deleted TimeSlot {}'.format(ts.pk)) self.assertNotContains(r, 'Deleted TimeSlot {}'.format(ts_to_keep.pk)) self.assertEqual( meeting.timeslot_set.filter(pk__in=(ts.pk for ts in ts_to_del)).count(), 0, 'Timeslots not deleted', ) self.assertEqual(meeting.timeslot_set.filter(pk=ts_to_keep.pk).count(), 1, 'Extra timeslot deleted') def test_ajax_delete_timeslots_invalid(self): meeting = self.create_bare_meeting() ts = TimeSlotFactory(meeting=meeting) self.login() r = self.client.post( self.edit_timeslots_url(meeting), ) self.assertEqual(r.status_code, 400, 'Missing POST data not handled') r = self.client.post( self.edit_timeslots_url(meeting), data=dict() ) self.assertEqual(r.status_code, 400, 'Empty POST data not handled') r = self.client.post( self.edit_timeslots_url(meeting), data=dict( slot_id=str(ts.pk), ) ) self.assertEqual(r.status_code, 400, 'Missing action not handled') r = self.client.post( self.edit_timeslots_url(meeting), data=dict( action='deletify', slot_id=str(ts.pk), ) ) self.assertEqual(r.status_code, 400, 'Invalid action not handled') r = self.client.post( self.edit_timeslots_url(meeting), data=dict( action='delete', ) ) self.assertEqual(r.status_code, 400, 'Missing slot_id not handled') r = self.client.post( self.edit_timeslots_url(meeting), data=dict( action='delete', slot_id='not an id', ) ) self.assertEqual(r.status_code, 400, 'Invalid slot_id not handled') r = self.client.post( self.edit_timeslots_url(meeting), data=dict( action='delete', slot_id='{}, not an id'.format(ts.pk), ) ) self.assertEqual(r.status_code, 400, 'Invalid slot_id not handled in bulk') nonexistent_id = TimeSlot.objects.all().aggregate(Max('id'))['id__max'] + 1 r = self.client.post( self.edit_timeslots_url(meeting), data=dict( action='delete', slot_id=str(nonexistent_id), ) ) self.assertEqual(r.status_code, 404, 'Nonexistent slot_id not handled in bulk') r = self.client.post( self.edit_timeslots_url(meeting), data=dict( action='delete', slot_id='{},{}'.format(nonexistent_id, ts.pk), ) ) self.assertEqual(r.status_code, 404, 'Nonexistent slot_id not handled in bulk') self.assertEqual(meeting.timeslot_set.filter(pk=ts.pk).count(), 1, 'TimeSlot unexpectedly deleted') class ReorderSlidesTests(TestCase): @override_settings(MEETECHO_API_CONFIG="fake settings") # enough to trigger API calls @patch("ietf.meeting.views.SlidesManager") def test_add_slides_to_session(self, mock_slides_manager_cls): for type_id in ('ietf','interim'): chair_role = RoleFactory(name_id='chair') session = SessionFactory(group=chair_role.group, meeting__date=date_today() - datetime.timedelta(days=90), meeting__type_id=type_id) slides = DocumentFactory(type_id='slides') url = urlreverse('ietf.meeting.views.ajax_add_slides_to_session', kwargs={'session_id':session.pk, 'num':session.meeting.number}) # Not a valid user r = self.client.post(url, {'order':1, 'name':slides.name }) self.assertEqual(r.status_code, 403) self.assertIn('have permission', unicontent(r)) self.assertFalse(mock_slides_manager_cls.called) self.client.login(username=chair_role.person.user.username, password=chair_role.person.user.username+"+password") # Past submission cutoff r = self.client.post(url, {'order':0, 'name':slides.name }) self.assertEqual(r.status_code, 403) self.assertIn('materials cutoff', unicontent(r)) self.assertFalse(mock_slides_manager_cls.called) session.meeting.date = date_today() session.meeting.save() # Invalid order r = self.client.post(url, {}) self.assertEqual(r.status_code, 200) self.assertEqual(r.json()['success'],False) self.assertIn('No data',r.json()['error']) self.assertFalse(mock_slides_manager_cls.called) r = self.client.post(url, {'garbage':'garbage'}) self.assertEqual(r.status_code, 200) self.assertEqual(r.json()['success'],False) self.assertIn('order is not valid',r.json()['error']) self.assertFalse(mock_slides_manager_cls.called) r = self.client.post(url, {'order':0, 'name':slides.name }) self.assertEqual(r.status_code, 200) self.assertEqual(r.json()['success'],False) self.assertIn('order is not valid',r.json()['error']) self.assertFalse(mock_slides_manager_cls.called) r = self.client.post(url, {'order':2, 'name':slides.name }) self.assertEqual(r.status_code, 200) self.assertEqual(r.json()['success'],False) self.assertIn('order is not valid',r.json()['error']) self.assertFalse(mock_slides_manager_cls.called) r = self.client.post(url, {'order':'garbage', 'name':slides.name }) self.assertEqual(r.status_code, 200) self.assertEqual(r.json()['success'],False) self.assertIn('order is not valid',r.json()['error']) self.assertFalse(mock_slides_manager_cls.called) # Invalid name r = self.client.post(url, {'order':1 }) self.assertEqual(r.status_code, 200) self.assertEqual(r.json()['success'],False) self.assertIn('name is not valid',r.json()['error']) self.assertFalse(mock_slides_manager_cls.called) r = self.client.post(url, {'order':1, 'name':'garbage' }) self.assertEqual(r.status_code, 200) self.assertEqual(r.json()['success'],False) self.assertIn('name is not valid',r.json()['error']) self.assertFalse(mock_slides_manager_cls.called) # Valid post r = self.client.post(url, {'order':1, 'name':slides.name }) self.assertEqual(r.status_code, 200) self.assertEqual(r.json()['success'],True) self.assertEqual(session.presentations.count(),1) self.assertTrue(mock_slides_manager_cls.called) self.assertEqual(mock_slides_manager_cls.call_args, call(api_config="fake settings")) self.assertTrue(mock_slides_manager_cls.return_value.add.called) self.assertEqual(mock_slides_manager_cls.return_value.add.call_args, call(session=session, slides=slides, order=1)) mock_slides_manager_cls.reset_mock() # Ignore a request to add slides that are already in a session r = self.client.post(url, {'order':1, 'name':slides.name }) self.assertEqual(r.status_code, 200) self.assertEqual(r.json()['success'],True) self.assertEqual(session.presentations.count(),1) self.assertFalse(mock_slides_manager_cls.called) session2 = SessionFactory(group=session.group, meeting=session.meeting) SessionPresentationFactory.create_batch(3, document__type_id='slides', session=session2) for num, sp in enumerate(session2.presentations.filter(document__type_id='slides'),start=1): sp.order = num sp.save() url = urlreverse('ietf.meeting.views.ajax_add_slides_to_session', kwargs={'session_id':session2.pk, 'num':session2.meeting.number}) more_slides = DocumentFactory.create_batch(3, type_id='slides') # Insert at beginning r = self.client.post(url, {'order':1, 'name':more_slides[0].name}) self.assertEqual(r.status_code, 200) self.assertEqual(r.json()['success'],True) self.assertEqual(session2.presentations.get(document=more_slides[0]).order,1) self.assertEqual(list(session2.presentations.order_by('order').values_list('order',flat=True)), list(range(1,5))) self.assertTrue(mock_slides_manager_cls.called) self.assertEqual(mock_slides_manager_cls.call_args, call(api_config="fake settings")) self.assertTrue(mock_slides_manager_cls.return_value.add.called) self.assertEqual(mock_slides_manager_cls.return_value.add.call_args, call(session=session2, slides=more_slides[0], order=1)) mock_slides_manager_cls.reset_mock() # Insert at end r = self.client.post(url, {'order':5, 'name':more_slides[1].name}) self.assertEqual(r.status_code, 200) self.assertEqual(r.json()['success'],True) self.assertEqual(session2.presentations.get(document=more_slides[1]).order,5) self.assertEqual(list(session2.presentations.order_by('order').values_list('order',flat=True)), list(range(1,6))) self.assertTrue(mock_slides_manager_cls.called) self.assertEqual(mock_slides_manager_cls.call_args, call(api_config="fake settings")) self.assertTrue(mock_slides_manager_cls.return_value.add.called) self.assertEqual(mock_slides_manager_cls.return_value.add.call_args, call(session=session2, slides=more_slides[1], order=5)) mock_slides_manager_cls.reset_mock() # Insert in middle r = self.client.post(url, {'order':3, 'name':more_slides[2].name}) self.assertEqual(r.status_code, 200) self.assertEqual(r.json()['success'],True) self.assertEqual(session2.presentations.get(document=more_slides[2]).order,3) self.assertEqual(list(session2.presentations.order_by('order').values_list('order',flat=True)), list(range(1,7))) self.assertTrue(mock_slides_manager_cls.called) self.assertEqual(mock_slides_manager_cls.call_args, call(api_config="fake settings")) self.assertTrue(mock_slides_manager_cls.return_value.add.called) self.assertEqual(mock_slides_manager_cls.return_value.add.call_args, call(session=session2, slides=more_slides[2], order=3)) mock_slides_manager_cls.reset_mock() @override_settings(MEETECHO_API_CONFIG="fake settings") # enough to trigger API calls @patch("ietf.meeting.views.SlidesManager") def test_remove_slides_from_session(self, mock_slides_manager_cls): for type_id in ['ietf','interim']: chair_role = RoleFactory(name_id='chair') session = SessionFactory(group=chair_role.group, meeting__date=date_today()-datetime.timedelta(days=90), meeting__type_id=type_id) slides = DocumentFactory(type_id='slides') url = urlreverse('ietf.meeting.views.ajax_remove_slides_from_session', kwargs={'session_id':session.pk, 'num':session.meeting.number}) # Not a valid user r = self.client.post(url, {'oldIndex':1, 'name':slides.name }) self.assertEqual(r.status_code, 403) self.assertIn('have permission', unicontent(r)) self.assertFalse(mock_slides_manager_cls.called) self.client.login(username=chair_role.person.user.username, password=chair_role.person.user.username+"+password") # Past submission cutoff r = self.client.post(url, {'oldIndex':0, 'name':slides.name }) self.assertEqual(r.status_code, 403) self.assertIn('materials cutoff', unicontent(r)) self.assertFalse(mock_slides_manager_cls.called) session.meeting.date = date_today() session.meeting.save() # Invalid order r = self.client.post(url, {}) self.assertEqual(r.status_code, 200) self.assertEqual(r.json()['success'],False) self.assertIn('No data',r.json()['error']) self.assertFalse(mock_slides_manager_cls.called) r = self.client.post(url, {'garbage':'garbage'}) self.assertEqual(r.status_code, 200) self.assertEqual(r.json()['success'],False) self.assertIn('index is not valid',r.json()['error']) self.assertFalse(mock_slides_manager_cls.called) r = self.client.post(url, {'oldIndex':0, 'name':slides.name }) self.assertEqual(r.status_code, 200) self.assertEqual(r.json()['success'],False) self.assertIn('index is not valid',r.json()['error']) self.assertFalse(mock_slides_manager_cls.called) r = self.client.post(url, {'oldIndex':'garbage', 'name':slides.name }) self.assertEqual(r.status_code, 200) self.assertEqual(r.json()['success'],False) self.assertIn('index is not valid',r.json()['error']) self.assertFalse(mock_slides_manager_cls.called) # No matching thing to delete r = self.client.post(url, {'oldIndex':1, 'name':slides.name }) self.assertEqual(r.status_code, 200) self.assertEqual(r.json()['success'],False) self.assertIn('index is not valid',r.json()['error']) self.assertFalse(mock_slides_manager_cls.called) session.presentations.create(document=slides, rev=slides.rev, order=1) # Bad names r = self.client.post(url, {'oldIndex':1}) self.assertEqual(r.status_code, 200) self.assertEqual(r.json()['success'],False) self.assertIn('name is not valid',r.json()['error']) self.assertFalse(mock_slides_manager_cls.called) r = self.client.post(url, {'oldIndex':1, 'name':'garbage' }) self.assertEqual(r.status_code, 200) self.assertEqual(r.json()['success'],False) self.assertIn('name is not valid',r.json()['error']) self.assertFalse(mock_slides_manager_cls.called) slides2 = DocumentFactory(type_id='slides') # index/name mismatch r = self.client.post(url, {'oldIndex':1, 'name':slides2.name }) self.assertEqual(r.status_code, 200) self.assertEqual(r.json()['success'],False) self.assertIn('SessionPresentation not found',r.json()['error']) self.assertFalse(mock_slides_manager_cls.called) session.presentations.create(document=slides2, rev=slides2.rev, order=2) r = self.client.post(url, {'oldIndex':1, 'name':slides2.name }) self.assertEqual(r.status_code, 200) self.assertEqual(r.json()['success'],False) self.assertIn('Name does not match index',r.json()['error']) self.assertFalse(mock_slides_manager_cls.called) # valid removal r = self.client.post(url, {'oldIndex':1, 'name':slides.name }) self.assertEqual(r.status_code, 200) self.assertEqual(r.json()['success'],True) self.assertEqual(session.presentations.count(),1) self.assertTrue(mock_slides_manager_cls.called) self.assertEqual(mock_slides_manager_cls.call_args, call(api_config="fake settings")) self.assertTrue(mock_slides_manager_cls.return_value.delete.called) self.assertEqual(mock_slides_manager_cls.return_value.delete.call_args, call(session=session, slides=slides)) mock_slides_manager_cls.reset_mock() session2 = SessionFactory(group=session.group, meeting=session.meeting) sp_list = SessionPresentationFactory.create_batch(5, document__type_id='slides', session=session2) for num, sp in enumerate(session2.presentations.filter(document__type_id='slides'),start=1): sp.order = num sp.save() url = urlreverse('ietf.meeting.views.ajax_remove_slides_from_session', kwargs={'session_id':session2.pk, 'num':session2.meeting.number}) # delete at first of list r = self.client.post(url, {'oldIndex':1, 'name':sp_list[0].document.name }) self.assertEqual(r.status_code, 200) self.assertEqual(r.json()['success'],True) self.assertFalse(session2.presentations.filter(pk=sp_list[0].pk).exists()) self.assertEqual(list(session2.presentations.order_by('order').values_list('order',flat=True)), list(range(1,5))) self.assertTrue(mock_slides_manager_cls.called) self.assertEqual(mock_slides_manager_cls.call_args, call(api_config="fake settings")) self.assertTrue(mock_slides_manager_cls.return_value.delete.called) self.assertEqual(mock_slides_manager_cls.return_value.delete.call_args, call(session=session2, slides=sp_list[0].document)) mock_slides_manager_cls.reset_mock() # delete in middle of list r = self.client.post(url, {'oldIndex':4, 'name':sp_list[4].document.name }) self.assertEqual(r.status_code, 200) self.assertEqual(r.json()['success'],True) self.assertFalse(session2.presentations.filter(pk=sp_list[4].pk).exists()) self.assertEqual(list(session2.presentations.order_by('order').values_list('order',flat=True)), list(range(1,4))) self.assertTrue(mock_slides_manager_cls.called) self.assertEqual(mock_slides_manager_cls.call_args, call(api_config="fake settings")) self.assertTrue(mock_slides_manager_cls.return_value.delete.called) self.assertEqual(mock_slides_manager_cls.return_value.delete.call_args, call(session=session2, slides=sp_list[4].document)) mock_slides_manager_cls.reset_mock() # delete at end of list r = self.client.post(url, {'oldIndex':2, 'name':sp_list[2].document.name }) self.assertEqual(r.status_code, 200) self.assertEqual(r.json()['success'],True) self.assertFalse(session2.presentations.filter(pk=sp_list[2].pk).exists()) self.assertEqual(list(session2.presentations.order_by('order').values_list('order',flat=True)), list(range(1,3))) self.assertTrue(mock_slides_manager_cls.called) self.assertEqual(mock_slides_manager_cls.call_args, call(api_config="fake settings")) self.assertTrue(mock_slides_manager_cls.return_value.delete.called) self.assertEqual(mock_slides_manager_cls.return_value.delete.call_args, call(session=session2, slides=sp_list[2].document)) mock_slides_manager_cls.reset_mock() @override_settings(MEETECHO_API_CONFIG="fake settings") # enough to trigger API calls @patch("ietf.meeting.views.SlidesManager") def test_reorder_slides_in_session(self, mock_slides_manager_cls): def _sppk_at(sppk, positions): return [sppk[p-1] for p in positions] chair_role = RoleFactory(name_id='chair') session = SessionFactory(group=chair_role.group, meeting__date=date_today() - datetime.timedelta(days=90)) sp_list = SessionPresentationFactory.create_batch(5, document__type_id='slides', session=session) sppk = [o.pk for o in sp_list] for num, sp in enumerate(sp_list, start=1): sp.order = num sp.save() url = urlreverse('ietf.meeting.views.ajax_reorder_slides_in_session', kwargs={'session_id':session.pk, 'num':session.meeting.number}) for type_id in ['ietf','interim']: session.meeting.type_id = type_id session.meeting.date = date_today()-datetime.timedelta(days=90) session.meeting.save() # Not a valid user r = self.client.post(url, {'oldIndex':1, 'newIndex':2 }) self.assertEqual(r.status_code, 403) self.assertIn('have permission', unicontent(r)) self.assertFalse(mock_slides_manager_cls.called) self.client.login(username=chair_role.person.user.username, password=chair_role.person.user.username+"+password") # Past submission cutoff r = self.client.post(url, {'oldIndex':1, 'newIndex':2 }) self.assertEqual(r.status_code, 403) self.assertIn('materials cutoff', unicontent(r)) self.assertFalse(mock_slides_manager_cls.called) session.meeting.date = date_today() session.meeting.save() # Bad index values r = self.client.post(url, {'oldIndex':0, 'newIndex':2 }) self.assertEqual(r.status_code, 200) self.assertEqual(r.json()['success'],False) self.assertIn('index is not valid',r.json()['error']) self.assertFalse(mock_slides_manager_cls.called) r = self.client.post(url, {'oldIndex':2, 'newIndex':6 }) self.assertEqual(r.status_code, 200) self.assertEqual(r.json()['success'],False) self.assertIn('index is not valid',r.json()['error']) self.assertFalse(mock_slides_manager_cls.called) r = self.client.post(url, {'oldIndex':2, 'newIndex':2 }) self.assertEqual(r.status_code, 200) self.assertEqual(r.json()['success'],False) self.assertIn('index is not valid',r.json()['error']) self.assertFalse(mock_slides_manager_cls.called) # Move from beginning r = self.client.post(url, {'oldIndex':1, 'newIndex':3}) self.assertEqual(r.status_code, 200) self.assertEqual(r.json()['success'],True) self.assertEqual(list(session.presentations.order_by('order').values_list('pk',flat=True)),_sppk_at(sppk,[2,3,1,4,5])) self.assertTrue(mock_slides_manager_cls.called) self.assertEqual(mock_slides_manager_cls.call_args, call(api_config="fake settings")) self.assertTrue(mock_slides_manager_cls.return_value.send_update.called) self.assertEqual(mock_slides_manager_cls.return_value.send_update.call_args, call(session)) mock_slides_manager_cls.reset_mock() # Move to beginning r = self.client.post(url, {'oldIndex':3, 'newIndex':1}) self.assertEqual(r.status_code, 200) self.assertEqual(r.json()['success'],True) self.assertEqual(list(session.presentations.order_by('order').values_list('pk',flat=True)),_sppk_at(sppk,[1,2,3,4,5])) self.assertTrue(mock_slides_manager_cls.called) self.assertEqual(mock_slides_manager_cls.call_args, call(api_config="fake settings")) self.assertTrue(mock_slides_manager_cls.return_value.send_update.called) self.assertEqual(mock_slides_manager_cls.return_value.send_update.call_args, call(session)) mock_slides_manager_cls.reset_mock() # Move from end r = self.client.post(url, {'oldIndex':5, 'newIndex':3}) self.assertEqual(r.status_code, 200) self.assertEqual(r.json()['success'],True) self.assertEqual(list(session.presentations.order_by('order').values_list('pk',flat=True)),_sppk_at(sppk,[1,2,5,3,4])) self.assertTrue(mock_slides_manager_cls.called) self.assertEqual(mock_slides_manager_cls.call_args, call(api_config="fake settings")) self.assertTrue(mock_slides_manager_cls.return_value.send_update.called) self.assertEqual(mock_slides_manager_cls.return_value.send_update.call_args, call(session)) mock_slides_manager_cls.reset_mock() # Move to end r = self.client.post(url, {'oldIndex':3, 'newIndex':5}) self.assertEqual(r.status_code, 200) self.assertEqual(r.json()['success'],True) self.assertEqual(list(session.presentations.order_by('order').values_list('pk',flat=True)),_sppk_at(sppk,[1,2,3,4,5])) self.assertTrue(mock_slides_manager_cls.called) self.assertEqual(mock_slides_manager_cls.call_args, call(api_config="fake settings")) self.assertTrue(mock_slides_manager_cls.return_value.send_update.called) self.assertEqual(mock_slides_manager_cls.return_value.send_update.call_args, call(session)) mock_slides_manager_cls.reset_mock() # Move beginning to end r = self.client.post(url, {'oldIndex':1, 'newIndex':5}) self.assertEqual(r.status_code, 200) self.assertEqual(r.json()['success'],True) self.assertEqual(list(session.presentations.order_by('order').values_list('pk',flat=True)),_sppk_at(sppk,[2,3,4,5,1])) self.assertTrue(mock_slides_manager_cls.called) self.assertEqual(mock_slides_manager_cls.call_args, call(api_config="fake settings")) self.assertTrue(mock_slides_manager_cls.return_value.send_update.called) self.assertEqual(mock_slides_manager_cls.return_value.send_update.call_args, call(session)) mock_slides_manager_cls.reset_mock() # Move middle to middle r = self.client.post(url, {'oldIndex':3, 'newIndex':4}) self.assertEqual(r.status_code, 200) self.assertEqual(r.json()['success'],True) self.assertEqual(list(session.presentations.order_by('order').values_list('pk',flat=True)),_sppk_at(sppk,[2,3,5,4,1])) self.assertTrue(mock_slides_manager_cls.called) self.assertEqual(mock_slides_manager_cls.call_args, call(api_config="fake settings")) self.assertTrue(mock_slides_manager_cls.return_value.send_update.called) self.assertEqual(mock_slides_manager_cls.return_value.send_update.call_args, call(session)) mock_slides_manager_cls.reset_mock() r = self.client.post(url, {'oldIndex':3, 'newIndex':2}) self.assertEqual(r.status_code, 200) self.assertEqual(r.json()['success'],True) self.assertEqual(list(session.presentations.order_by('order').values_list('pk',flat=True)),_sppk_at(sppk,[2,5,3,4,1])) self.assertTrue(mock_slides_manager_cls.called) self.assertEqual(mock_slides_manager_cls.call_args, call(api_config="fake settings")) self.assertTrue(mock_slides_manager_cls.return_value.send_update.called) self.assertEqual(mock_slides_manager_cls.return_value.send_update.call_args, call(session)) mock_slides_manager_cls.reset_mock() # Reset for next iteration in the loop session.presentations.update(order=F('pk')) self.client.logout() def test_slide_order_reconditioning(self): chair_role = RoleFactory(name_id='chair') session = SessionFactory(group=chair_role.group, meeting__date=date_today() - datetime.timedelta(days=90)) sp_list = SessionPresentationFactory.create_batch(5, document__type_id='slides', session=session) for num, sp in enumerate(sp_list, start=1): sp.order = 2*num sp.save() try: condition_slide_order(session) except AssertionError: pass self.assertEqual(list(session.presentations.order_by('order').values_list('order',flat=True)),list(range(1,6))) class EditTests(TestCase): """Test schedule edit operations""" def test_official_record_schedule_is_read_only(self): def _set_date_offset_and_retrieve_page(meeting, days_offset, client): meeting.date = date_today() + datetime.timedelta(days=days_offset) meeting.save() client.login(username="secretary", password="secretary+password") url = urlreverse("ietf.meeting.views.edit_meeting_schedule", kwargs=dict(num=meeting.number)) r = client.get(url) q = PyQuery(r.content) return(r, q) # Setup #################################################################################### # Basic test data meeting = make_meeting_test_data() # Set the secretary as the owner of the schedule schedule = meeting.schedule schedule.owner = Person.objects.get(user__username="secretary") schedule.save() # Tests #################################################################################### # 1) Check that we get told the page is not editable ####################################################### r, q = _set_date_offset_and_retrieve_page(meeting, 0 - 2 - meeting.days, # Meeting ended 2 days ago self.client) self.assertTrue(q(""".alert:contains("You can't edit this schedule")""")) self.assertTrue(q(""".alert:contains("This is the official schedule for a meeting in the past")""")) # 2) An ongoing meeting ####################################################### r, q = _set_date_offset_and_retrieve_page(meeting, 0, # Meeting starts today self.client) self.assertFalse(q(""".alert:contains("You can't edit this schedule")""")) self.assertFalse(q(""".alert:contains("This is the official schedule for a meeting in the past")""")) # 3) A meeting in the future ####################################################### r, q = _set_date_offset_and_retrieve_page(meeting, 7, # Meeting starts next week self.client) self.assertFalse(q(""".alert:contains("You can't edit this schedule")""")) self.assertFalse(q(""".alert:contains("This is the official schedule for a meeting in the past")""")) def test_edit_meeting_schedule(self): meeting = make_meeting_test_data() self.client.login(username="secretary", password="secretary+password") s1 = Session.objects.filter(meeting=meeting, type='regular').first() s2 = Session.objects.filter(meeting=meeting, type='regular').exclude(group=s1.group).first() s1.comments = "Hello world!" s1.attendees = 1234 s1.save() Constraint.objects.create( meeting=meeting, source=s1.group, target=s2.group, name=ConstraintName.objects.get(slug="conflict"), ) p = Person.objects.order_by('pk')[1] Constraint.objects.create( meeting=meeting, source=s1.group, person=p, name=ConstraintName.objects.get(slug="bethere"), ) Constraint.objects.create( meeting=meeting, source=s2.group, person=p, name=ConstraintName.objects.get(slug="bethere"), ) room = Room.objects.get(meeting=meeting, session_types='regular') base_timeslot = TimeSlot.objects.create(meeting=meeting, type_id='regular', location=room, duration=datetime.timedelta(minutes=50), time=meeting.tz().localize( datetime.datetime.combine(meeting.date + datetime.timedelta(days=2), datetime.time(9, 30)) )) timeslots = list(TimeSlot.objects.filter(meeting=meeting, type='regular').order_by('time')) base_session = SessionFactory(meeting=meeting, group=Group.objects.get(acronym="irg"), attendees=20, requested_duration=datetime.timedelta(minutes=30), add_to_schedule=False) SchedulingEvent.objects.create(session=base_session, status_id='schedw', by=Person.objects.get(user__username='secretary')) SchedTimeSessAssignment.objects.create(timeslot=base_timeslot, session=base_session, schedule=meeting.schedule.base) # check we have the grid and everything set up as a baseline - # the Javascript tests check that the Javascript can work with # it url = urlreverse("ietf.meeting.views.edit_meeting_schedule", kwargs=dict(num=meeting.number)) r = self.client.get(url) q = PyQuery(r.content) self.assertTrue(q(".room-name:contains(\"{}\")".format(room.name))) self.assertTrue(q(".room-name:contains(\"{}\")".format(room.capacity))) self.assertTrue(q("#timeslot{}".format(timeslots[0].pk))) for s in [s1, s2]: e = q("#session{}".format(s.pk)) # should be link to edit/cancel session edit_session_url = urlreverse( 'ietf.meeting.views.edit_session', kwargs={'session_id': s.pk} ) + f'?sched={meeting.schedule.pk}' self.assertTrue( e.find(f'a[href="{edit_session_url}"]') ) self.assertTrue( e.find('a[href="{}?sched={}"]'.format( urlreverse('ietf.meeting.views.cancel_session', kwargs={'session_id': s.pk}), meeting.schedule.pk, )) ) # info in the item representing the session that can be moved around self.assertIn(s.group.acronym, e.find(".session-label").text()) if s.comments: self.assertTrue(e.find(".comments")) if s.attendees is not None: self.assertIn(str(s.attendees), e.find(".attendees").text()) self.assertTrue(e.hasClass("parent-{}".format(s.group.parent.acronym))) constraints = e.find(".constraints > span") s_other = s2 if s == s1 else s1 self.assertEqual(len(constraints), 3) self.assertEqual(constraints.eq(0).attr("data-sessions"), str(s_other.pk)) self.assertEqual(constraints.eq(0).find(".bi-people-fill").parent().text(), "1") # 1 person in the constraint self.assertEqual(constraints.eq(1).attr("data-sessions"), str(s_other.pk)) self.assertEqual(constraints.eq(1).find(".encircled").text(), "1" if s_other == s2 else "-1") self.assertEqual(constraints.eq(2).attr("data-sessions"), str(s_other.pk)) self.assertEqual(constraints.eq(2).find(".encircled").text(), "AD") # session info for the panel self.assertIn(str(round(s.requested_duration.total_seconds() / 60.0 / 60, 1)), e.find(".session-info .title").text()) event = SchedulingEvent.objects.filter(session=s).order_by("id").first() if event: self.assertTrue(e.find("div:contains(\"{}\")".format(event.by.name))) if s.comments: self.assertIn(s.comments, e.find(".comments").text()) formatted_constraints1 = q("#session{} .session-info .formatted-constraints > *".format(s1.pk)) self.assertIn(s2.group.acronym, formatted_constraints1.eq(0).html()) self.assertIn(p.name, formatted_constraints1.eq(1).html()) formatted_constraints2 = q("#session{} .session-info .formatted-constraints > *".format(s2.pk)) self.assertIn(p.name, formatted_constraints2.eq(0).html()) self.assertEqual(len(q("#session{}.readonly".format(base_session.pk))), 1) self.assertTrue(q(".alert:contains(\"You can't edit this schedule\")")) # can't change anything r = self.client.post(url, { 'action': 'assign', 'timeslot': timeslots[0].pk, 'session': s1.pk, }) self.assertEqual(r.status_code, 403) # turn us into owner schedule = meeting.schedule schedule.owner = Person.objects.get(user__username="secretary") schedule.save() meeting.schedule = None meeting.save() url = urlreverse("ietf.meeting.views.edit_meeting_schedule", kwargs=dict(num=meeting.number, owner=schedule.owner_email(), name=schedule.name)) r = self.client.get(url) q = PyQuery(r.content) self.assertTrue(not q("em:contains(\"You can't edit this schedule\")")) SchedTimeSessAssignment.objects.filter(session=s1).delete() # assign r = self.client.post(url, { 'action': 'assign', 'timeslot': timeslots[0].pk, 'session': s1.pk, }) self.assertEqual(json.loads(r.content)['success'], True) self.assertEqual(SchedTimeSessAssignment.objects.get(schedule=schedule, session=s1).timeslot, timeslots[0]) # move assignment on unofficial schedule r = self.client.post(url, { 'action': 'assign', 'timeslot': timeslots[1].pk, 'session': s1.pk, }) self.assertEqual(json.loads(r.content)['success'], True) self.assertEqual(SchedTimeSessAssignment.objects.get(schedule=schedule, session=s1).timeslot, timeslots[1]) # move assignment on official schedule, leaving tombstone meeting.schedule = schedule meeting.save() SchedulingEvent.objects.create( session=s1, status=SessionStatusName.objects.get(slug='sched'), by=Person.objects.get(name='(System)') ) r = self.client.post(url, { 'action': 'assign', 'timeslot': timeslots[0].pk, 'session': s1.pk, }) json_content = json.loads(r.content) self.assertEqual(json_content['success'], True) self.assertEqual(SchedTimeSessAssignment.objects.get(schedule=schedule, session=s1).timeslot, timeslots[0]) sessions_for_group = Session.objects.filter(group=s1.group, meeting=meeting) self.assertEqual(len(sessions_for_group), 2) s_tombstone = [s for s in sessions_for_group if s != s1][0] self.assertEqual(s_tombstone.tombstone_for, s1) tombstone_event = SchedulingEvent.objects.get(session=s_tombstone) self.assertEqual(tombstone_event.status_id, 'resched') self.assertEqual(SchedTimeSessAssignment.objects.get(schedule=schedule, session=s_tombstone).timeslot, timeslots[1]) self.assertTrue(PyQuery(json_content['tombstone'])("#session{}.readonly".format(s_tombstone.pk)).html()) # unassign r = self.client.post(url, { 'action': 'unassign', 'session': s1.pk, }) self.assertEqual(json.loads(r.content)['success'], True) self.assertEqual(list(SchedTimeSessAssignment.objects.filter(schedule=schedule, session=s1)), []) # try swapping days SchedTimeSessAssignment.objects.create(schedule=schedule, session=s1, timeslot=timeslots[0]) self.assertEqual(len(SchedTimeSessAssignment.objects.filter(schedule=schedule, session=s1, timeslot=timeslots[0])), 1) self.assertEqual(len(SchedTimeSessAssignment.objects.filter(schedule=schedule, session=s2, timeslot=timeslots[1])), 1) self.assertEqual(list(SchedTimeSessAssignment.objects.filter(schedule=schedule, timeslot=timeslots[2])), []) r = self.client.post(url, { 'action': 'swapdays', 'source_day': timeslots[0].time.date().isoformat(), 'target_day': timeslots[2].time.date().isoformat(), }) self.assertEqual(r.status_code, 302) self.assertEqual(list(SchedTimeSessAssignment.objects.filter(schedule=schedule, timeslot=timeslots[0])), []) self.assertEqual(list(SchedTimeSessAssignment.objects.filter(schedule=schedule, timeslot=timeslots[1])), []) self.assertEqual(len(SchedTimeSessAssignment.objects.filter(schedule=schedule, session=s1, timeslot=timeslots[2])), 1) self.assertEqual(list(SchedTimeSessAssignment.objects.filter(schedule=schedule, session=s2)), []) # swap back r = self.client.post(url, { 'action': 'swapdays', 'source_day': timeslots[2].time.date().isoformat(), 'target_day': timeslots[0].time.date().isoformat(), }) self.assertEqual(r.status_code, 302) self.assertEqual(len(SchedTimeSessAssignment.objects.filter(schedule=schedule, session=s1, timeslot=timeslots[0])), 1) self.assertEqual(list(SchedTimeSessAssignment.objects.filter(schedule=schedule, timeslot=timeslots[1])), []) self.assertEqual(list(SchedTimeSessAssignment.objects.filter(schedule=schedule, timeslot=timeslots[2])), []) def test_edit_meeting_timeslots_and_misc_sessions(self): meeting = make_meeting_test_data() self.client.login(username="secretary", password="secretary+password") # check we have the grid and everything set up as a baseline - # the Javascript tests check that the Javascript can work with # it url = urlreverse("ietf.meeting.views.edit_meeting_timeslots_and_misc_sessions", kwargs=dict(num=meeting.number, owner=meeting.schedule.base.owner_email(), name=meeting.schedule.base.name)) r = self.client.get(url) q = PyQuery(r.content) breakfast_room = Room.objects.get(meeting=meeting, name="Breakfast Room") break_room = Room.objects.get(meeting=meeting, name="Break Area") reg_room = Room.objects.get(meeting=meeting, name="Registration Area") for i in range(meeting.days): self.assertTrue(q("[data-day=\"{}\"]".format((meeting.date + datetime.timedelta(days=i)).isoformat()))) self.assertTrue(q(".room-label:contains(\"{}\")".format(breakfast_room.name))) self.assertTrue(q(".room-label:contains(\"{}\")".format(break_room.name))) self.assertTrue(q(".room-label:contains(\"{}\")".format(reg_room.name))) break_slot = TimeSlot.objects.get(location=break_room, type='break') room_row = q(".room-row[data-day=\"{}\"][data-room=\"{}\"]".format(break_slot.time.date().isoformat(), break_slot.location_id)) self.assertTrue(room_row) self.assertTrue(room_row.find("#timeslot{}".format(break_slot.pk))) self.assertTrue(q(".timeslot-form")) # add timeslot ietf_group = Group.objects.get(acronym='ietf') r = self.client.post(url, { 'day': meeting.date, 'time': '08:30', 'duration': '1:30', 'location': break_room.pk, 'show_location': 'on', 'type': 'other', 'group': ietf_group.pk, 'name': "IETF Testing", 'short': "ietf-testing", 'scroll': 1234, 'action': 'add-timeslot', }) self.assertNoFormPostErrors(r) self.assertIn("#scroll=1234", r['Location']) test_timeslot = TimeSlot.objects.get(meeting=meeting, name="IETF Testing") self.assertEqual( test_timeslot.time, meeting.tz().localize( datetime.datetime.combine(meeting.date, datetime.time(8, 30)) ), ) self.assertEqual(test_timeslot.duration, datetime.timedelta(hours=1, minutes=30)) self.assertEqual(test_timeslot.location_id, break_room.pk) self.assertEqual(test_timeslot.show_location, True) self.assertEqual(test_timeslot.type_id, 'other') test_session = Session.objects.get(meeting=meeting, timeslotassignments__timeslot=test_timeslot) self.assertEqual(test_session.short, 'ietf-testing') self.assertEqual(test_session.group, ietf_group) self.assertTrue(SchedulingEvent.objects.filter(session=test_session, status='sched')) # edit timeslot r = self.client.get(url, { 'timeslot': test_timeslot.pk, 'action': 'edit-timeslot', }) self.assertEqual(r.status_code, 200) edit_form_html = json.loads(r.content)['form'] q = PyQuery(edit_form_html) self.assertEqual(q("[name=name]").val(), test_timeslot.name) self.assertEqual(q("[name=location]").val(), str(test_timeslot.location_id)) self.assertEqual(q("[name=timeslot]").val(), str(test_timeslot.pk)) self.assertEqual(q("[name=type]").val(), str(test_timeslot.type_id)) self.assertEqual(q("[name=group]").val(), str(ietf_group.pk)) iab_group = Group.objects.get(acronym='iab') r = self.client.post(url, { 'timeslot': test_timeslot.pk, 'day': meeting.date, 'time': '09:30', 'duration': '1:00', 'location': breakfast_room.pk, 'type': 'other', 'group': iab_group.pk, 'name': "IETF Testing 2", 'short': "ietf-testing2", 'action': 'edit-timeslot', }) self.assertNoFormPostErrors(r) test_timeslot.refresh_from_db() self.assertEqual( test_timeslot.time, meeting.tz().localize( datetime.datetime.combine(meeting.date, datetime.time(9, 30)) ), ) self.assertEqual(test_timeslot.duration, datetime.timedelta(hours=1)) self.assertEqual(test_timeslot.location_id, breakfast_room.pk) self.assertEqual(test_timeslot.show_location, False) self.assertEqual(test_timeslot.type_id, 'other') test_session.refresh_from_db() self.assertEqual(test_session.short, 'ietf-testing2') self.assertEqual(test_session.group, iab_group) # cancel timeslot r = self.client.post(url, { 'timeslot': test_timeslot.pk, 'action': 'cancel-timeslot', }) self.assertNoFormPostErrors(r) event = SchedulingEvent.objects.filter( session__timeslotassignments__timeslot=test_timeslot ).order_by('-id').first() self.assertEqual(event.status_id, 'canceled') # delete timeslot test_presentation = Document.objects.create(name='slides-test', type_id='slides') SessionPresentation.objects.create( document=test_presentation, rev='1', session=test_session ) r = self.client.post(url, { 'timeslot': test_timeslot.pk, 'action': 'delete-timeslot', }) self.assertNoFormPostErrors(r) self.assertEqual(list(TimeSlot.objects.filter(pk=test_timeslot.pk)), []) self.assertEqual(list(Session.objects.filter(pk=test_session.pk)), []) self.assertEqual(test_presentation.get_state_slug(), 'deleted') # set agenda note assignment = SchedTimeSessAssignment.objects.filter(session__group__acronym='mars', schedule=meeting.schedule).first() url = urlreverse("ietf.meeting.views.edit_meeting_timeslots_and_misc_sessions", kwargs=dict(num=meeting.number, owner=meeting.schedule.owner_email(), name=meeting.schedule.name)) r = self.client.post(url, { 'timeslot': assignment.timeslot_id, 'day': assignment.timeslot.time.date().isoformat(), 'time': assignment.timeslot.time.time().isoformat(), 'duration': assignment.timeslot.duration, 'location': assignment.timeslot.location_id, 'type': assignment.slot_type().slug, 'name': assignment.timeslot.name, 'agenda_note': "New Test Note", 'action': 'edit-timeslot', }) self.assertNoFormPostErrors(r) assignment.session.refresh_from_db() self.assertEqual(assignment.session.agenda_note, "New Test Note") def test_edit_meeting_schedule_conflict_types(self): """The meeting schedule editor should show the constraint types enabled for the meeting""" meeting = MeetingFactory( type_id='ietf', group_conflicts=[], # show none to start with ) s1 = SessionFactory( meeting=meeting, type_id='regular', attendees=12, comments='chair conflict', ) s2 = SessionFactory( meeting=meeting, type_id='regular', attendees=34, comments='old-fashioned conflict', ) Constraint.objects.create( meeting=meeting, source=s1.group, target=s2.group, name=ConstraintName.objects.get(slug="chair_conflict"), ) Constraint.objects.create( meeting=meeting, source=s2.group, target=s1.group, name=ConstraintName.objects.get(slug="conflict"), ) # log in as secretary so we have access self.client.login(username="secretary", password="secretary+password") url = urlreverse("ietf.meeting.views.edit_meeting_schedule", kwargs=dict(num=meeting.number)) # Should have no conflict constraints listed because the meeting has all disabled r = self.client.get(url) q = PyQuery(r.content) self.assertEqual(len(q('#session{} span.constraints > span'.format(s1.pk))), 0) self.assertEqual(len(q('#session{} span.constraints > span'.format(s2.pk))), 0) # Now enable the 'chair_conflict' constraint only chair_conflict = ConstraintName.objects.get(slug='chair_conflict') chair_conf_label = b'' # result of etree.tostring(etree.fromstring(editor_label)) meeting.group_conflict_types.add(chair_conflict) r = self.client.get(url) q = PyQuery(r.content) # verify that there is a constraint pointing from 1 to 2 # # The constraint is represented in the HTML as #
    # [...] # # [constraint label] # #
    # # Where the constraint label is the editor_label for the ConstraintName. # If this pk is the constraint target, the editor_label includes a # '-' prefix, which may be before the editor_label or inserted inside # it. # # For simplicity, this test is tied to the current values of editor_label. # It also assumes the order of constraints will be constant. # If those change, the test will need to be updated. s1_constraints = q('#session{} span.constraints > span'.format(s1.pk)) s2_constraints = q('#session{} span.constraints > span'.format(s2.pk)) # Check the forward constraint self.assertEqual(len(s1_constraints), 1) self.assertEqual(s1_constraints[0].attrib['data-sessions'], str(s2.pk)) self.assertEqual(s1_constraints[0].text, None) # no '-' prefix on the source self.assertEqual(tostring(s1_constraints[0][0]), chair_conf_label) # [0][0] is the innermost # And the reverse constraint self.assertEqual(len(s2_constraints), 1) self.assertEqual(s2_constraints[0].attrib['data-sessions'], str(s1.pk)) self.assertEqual(s2_constraints[0].text, '-') # '-' prefix on the target self.assertEqual(tostring(s2_constraints[0][0]), chair_conf_label) # [0][0] is the innermost # Now also enable the 'conflict' constraint conflict = ConstraintName.objects.get(slug='conflict') conf_label = b'1' conf_label_reversed = b'-1' # the '-' is inside the span! meeting.group_conflict_types.add(conflict) r = self.client.get(url) q = PyQuery(r.content) s1_constraints = q('#session{} span.constraints > span'.format(s1.pk)) s2_constraints = q('#session{} span.constraints > span'.format(s2.pk)) # Check the forward constraint self.assertEqual(len(s1_constraints), 2) self.assertEqual(s1_constraints[0].attrib['data-sessions'], str(s2.pk)) self.assertEqual(s1_constraints[0].text, None) # no '-' prefix on the source self.assertEqual(tostring(s1_constraints[0][0]), chair_conf_label) # [0][0] is the innermost self.assertEqual(s1_constraints[1].attrib['data-sessions'], str(s2.pk)) self.assertEqual(tostring(s1_constraints[1][0]), conf_label_reversed) # [0][0] is the innermost # And the reverse constraint self.assertEqual(len(s2_constraints), 2) self.assertEqual(s2_constraints[0].attrib['data-sessions'], str(s1.pk)) self.assertEqual(s2_constraints[0].text, '-') # '-' prefix on the target self.assertEqual(tostring(s2_constraints[0][0]), chair_conf_label) # [0][0] is the innermost self.assertEqual(s2_constraints[1].attrib['data-sessions'], str(s1.pk)) self.assertEqual(tostring(s2_constraints[1][0]), conf_label) # [0][0] is the innermost def test_new_meeting_schedule(self): """Can create a meeting schedule from scratch""" meeting = make_meeting_test_data() self.client.login(username="secretary", password="secretary+password") # new from scratch url = urlreverse("ietf.meeting.views.new_meeting_schedule", kwargs=dict(num=meeting.number)) r = self.client.get(url) self.assertEqual(r.status_code, 200) r = self.client.post(url, { 'name': "scratch", 'public': "on", 'visible': "on", 'notes': "New scratch", 'base': meeting.schedule.base_id, }) self.assertNoFormPostErrors(r) new_schedule = Schedule.objects.get(meeting=meeting, owner__user__username='secretary', name='scratch') self.assertEqual(new_schedule.public, True) self.assertEqual(new_schedule.visible, True) self.assertEqual(new_schedule.notes, "New scratch") self.assertEqual(new_schedule.origin, None) self.assertEqual(new_schedule.base_id, meeting.schedule.base_id) def test_copy_meeting_schedule(self): """Can create a copy of an existing meeting schedule""" meeting = make_meeting_test_data() self.client.login(username="secretary", password="secretary+password") url = urlreverse("ietf.meeting.views.new_meeting_schedule", kwargs=dict(num=meeting.number, owner=meeting.schedule.owner_email(), name=meeting.schedule.name)) r = self.client.get(url) self.assertEqual(r.status_code, 200) r = self.client.post(url, { 'name': "copy", 'public': "on", 'notes': "New copy", 'base': meeting.schedule.base_id, }) self.assertNoFormPostErrors(r) new_schedule = Schedule.objects.get(meeting=meeting, owner__user__username='secretary', name='copy') self.assertEqual(new_schedule.public, True) self.assertEqual(new_schedule.visible, False) self.assertEqual(new_schedule.notes, "New copy") self.assertEqual(new_schedule.origin, meeting.schedule) self.assertEqual(new_schedule.base_id, meeting.schedule.base_id) old_assignments = {(a.session_id, a.timeslot_id) for a in SchedTimeSessAssignment.objects.filter(schedule=meeting.schedule)} for a in SchedTimeSessAssignment.objects.filter(schedule=new_schedule): self.assertIn((a.session_id, a.timeslot_id), old_assignments) def test_schedule_read_permissions(self): meeting = make_meeting_test_data() schedule = meeting.schedule # try to get non-existing agenda url = urlreverse("ietf.meeting.views.edit_meeting_schedule", kwargs=dict(num=meeting.number, owner=schedule.owner_email(), name="foo")) r = self.client.get(url) self.assertEqual(r.status_code, 404) url = urlreverse("ietf.meeting.views.edit_meeting_schedule", kwargs=dict(num=meeting.number, owner=schedule.owner_email(), name=schedule.name)) self.client.login(username='ad', password='ad+password') r = self.client.get(url) self.assertEqual(r.status_code, 200) schedule.visible = True schedule.public = False schedule.save() # get as anonymous doesn't work self.client.logout() r = self.client.get(url) self.assertEqual(r.status_code, 403) # public, now anonymous works schedule.public = True schedule.save() r = self.client.get(url) self.assertEqual(r.status_code, 200) # Secretariat can always see it schedule.visible = False schedule.public = False schedule.save() self.client.login(username="secretary", password="secretary+password") r = self.client.get(url) self.assertEqual(r.status_code, 200) def test_new_meeting_schedule_rejects_invalid_names(self): meeting = make_meeting_test_data() orig_schedule_count = meeting.schedule_set.count() self.client.login(username="ad", password="ad+password") url = urlreverse("ietf.meeting.views.new_meeting_schedule", kwargs=dict(num=meeting.number)) r = self.client.post(url, { 'name': "/no/this/should/not/work/it/is/too/long", 'public': "on", 'notes': "Name too long", 'base': meeting.schedule.base_id, }) self.assertEqual(r.status_code, 200) self.assertFormError(r.context["form"], 'name', 'Enter a valid value.') self.assertEqual(meeting.schedule_set.count(), orig_schedule_count, 'Schedule should not be created') r = self.client.post(url, { 'name': "/invalid/chars/", 'public': "on", 'notes': "Name too long", 'base': meeting.schedule.base_id, }) self.assertEqual(r.status_code, 200) self.assertFormError(r.context["form"], 'name', 'Enter a valid value.') self.assertEqual(meeting.schedule_set.count(), orig_schedule_count, 'Schedule should not be created') # Non-ASCII alphanumeric characters r = self.client.post(url, { 'name': "f\u00E9ling", 'public': "on", 'notes': "Name too long", 'base': meeting.schedule.base_id, }) self.assertEqual(r.status_code, 200) self.assertFormError(r.context["form"], 'name', 'Enter a valid value.') self.assertEqual(meeting.schedule_set.count(), orig_schedule_count, 'Schedule should not be created') def test_edit_session(self): session = SessionFactory(meeting__type_id='ietf', group__type_id='team') # type determines allowed session purposes edit_meeting_url = urlreverse('ietf.meeting.views.edit_meeting_schedule', kwargs={'num': session.meeting.number}) self.client.login(username='secretary', password='secretary+password') url = urlreverse('ietf.meeting.views.edit_session', kwargs={'session_id': session.pk}) r = self.client.get(url) self.assertContains(r, 'Edit session', status_code=200) pq = PyQuery(r.content) back_button = pq(f'a[href="{edit_meeting_url}"]') self.assertEqual(len(back_button), 1) post_data = { 'name': 'this is a name', 'short': 'tian', 'purpose': 'coding', 'type': 'other', 'requested_duration': '3600', 'on_agenda': True, 'remote_instructions': 'Do this do that', 'attendees': '103', 'comments': 'So much to say', 'chat_room': 'xyzzy', } r = self.client.post(url, post_data) self.assertNoFormPostErrors(r) self.assertRedirects(r, edit_meeting_url) session = Session.objects.get(pk=session.pk) # refresh objects from DB self.assertEqual(session.name, 'this is a name') self.assertEqual(session.short, 'tian') self.assertEqual(session.purpose_id, 'coding') self.assertEqual(session.type_id, 'other') self.assertEqual(session.requested_duration, datetime.timedelta(hours=1)) self.assertEqual(session.on_agenda, True) self.assertEqual(session.remote_instructions, 'Do this do that') self.assertEqual(session.attendees, 103) self.assertEqual(session.comments, 'So much to say') self.assertEqual(session.chat_room, 'xyzzy') # Verify return to correct schedule when sched query parameter is present other_schedule = ScheduleFactory(meeting=session.meeting) r = self.client.get(url + f'?sched={other_schedule.pk}') edit_meeting_url = urlreverse( 'ietf.meeting.views.edit_meeting_schedule', kwargs={ 'num': session.meeting.number, 'owner': other_schedule.owner.email(), 'name': other_schedule.name, }, ) pq = PyQuery(r.content) back_button = pq(f'a[href="{edit_meeting_url}"]') self.assertEqual(len(back_button), 1) r = self.client.post(url + f'?sched={other_schedule.pk}', post_data) self.assertRedirects(r, edit_meeting_url) def test_cancel_session(self): # session for testing with official schedule session = SessionFactory(meeting__type_id='ietf') url = urlreverse('ietf.meeting.views.cancel_session', kwargs={'session_id': session.pk}) return_url = urlreverse('ietf.meeting.views.edit_meeting_schedule', kwargs={'num': session.meeting.number}) # session for testing with unofficial schedule other_session = SessionFactory(meeting=session.meeting) unofficial_schedule = ScheduleFactory(meeting=other_session.meeting) url_unofficial = urlreverse( 'ietf.meeting.views.cancel_session', kwargs={'session_id': other_session.pk}, ) + f'?sched={unofficial_schedule.pk}' return_url_unofficial = urlreverse( 'ietf.meeting.views.edit_meeting_schedule', kwargs={ 'num': other_session.meeting.number, 'name': unofficial_schedule.name, 'owner': unofficial_schedule.owner_email(), }, ) login_testing_unauthorized(self, 'secretary', url) r = self.client.get(url) self.assertContains(r, 'Cancel session', status_code=200) self.assertIn(return_url, r.content.decode()) r = self.client.get(url_unofficial) self.assertContains(r, 'Cancel session', status_code=200) self.assertIn(return_url_unofficial, r.content.decode()) r = self.client.post(url, {}) self.assertFormError(r.context["form"], 'confirmed', 'This field is required.') r = self.client.post(url_unofficial, {}) self.assertFormError(r.context["form"], 'confirmed', 'This field is required.') r = self.client.post(url, {'confirmed': 'on'}) self.assertRedirects(r, return_url) session = Session.objects.with_current_status().get(pk=session.pk) self.assertEqual(session.current_status, 'canceled') r = self.client.get(url) self.assertRedirects(r, return_url) # should redirect immediately when session is already canceled r = self.client.post(url_unofficial, {'confirmed': 'on'}) self.assertRedirects(r, return_url_unofficial) other_session = Session.objects.with_current_status().get(pk=other_session.pk) self.assertEqual(other_session.current_status, 'canceled') r = self.client.get(url_unofficial) self.assertRedirects(r, return_url_unofficial) # should redirect immediately when session is already canceled def test_edit_timeslots(self): meeting = make_meeting_test_data() self.client.login(username="secretary", password="secretary+password") r = self.client.get(urlreverse("ietf.meeting.views.edit_timeslots", kwargs=dict(num=meeting.number))) self.assertContains(r, meeting.room_set.all().first().name) def test_edit_timeslot_type(self): timeslot = TimeSlotFactory(meeting__type_id='ietf') url = urlreverse('ietf.meeting.views.edit_timeslot_type', kwargs=dict(num=timeslot.meeting.number,slot_id=timeslot.id)) login_testing_unauthorized(self,"secretary",url) r = self.client.get(url) self.assertEqual(r.status_code, 200) r = self.client.post(url,{'type':'other',}) self.assertEqual(r.status_code, 302) timeslot = TimeSlot.objects.get(id=timeslot.id) self.assertEqual(timeslot.type.slug,'other') def test_slot_to_the_right(self): meeting = make_meeting_test_data() session = Session.objects.filter(meeting=meeting, group__acronym="mars").first() mars_scheduled = session.timeslotassignments.get(schedule__name='test-schedule') mars_slot = TimeSlot.objects.get(sessionassignments__session=session,sessionassignments__schedule__name='test-schedule') mars_ends = mars_slot.time + mars_slot.duration session = Session.objects.filter(meeting=meeting, group__acronym="ames").first() ames_slot_qs = TimeSlot.objects.filter(sessionassignments__session=session,sessionassignments__schedule__name='test-schedule') ames_slot_qs.update(time=mars_ends + datetime.timedelta(seconds=11 * 60)) self.assertTrue(not mars_slot.slot_to_the_right) self.assertTrue(not mars_scheduled.slot_to_the_right) ames_slot_qs.update(time=mars_ends + datetime.timedelta(seconds=10 * 60)) self.assertTrue(mars_slot.slot_to_the_right) self.assertTrue(mars_scheduled.slot_to_the_right) def test_updateview(self): """The updateview action should set visible timeslot types in the session""" meeting = MeetingFactory(type_id='ietf') url = urlreverse('ietf.meeting.views.edit_meeting_schedule', kwargs={'num': meeting.number}) types_to_enable = ['regular', 'reg', 'other'] r = self.client.post( url, { 'action': 'updateview', 'enabled_timeslot_types[]': types_to_enable, }, ) self.assertEqual(r.status_code, 200) session_data = self.client.session self.assertIn('edit_meeting_schedule', session_data) self.assertCountEqual( session_data['edit_meeting_schedule']['enabled_timeslot_types'], types_to_enable, 'Should set types requested', ) r = self.client.post( url, { 'action': 'updateview', 'enabled_timeslot_types[]': types_to_enable + ['faketype'], }, ) self.assertEqual(r.status_code, 200) session_data = self.client.session self.assertIn('edit_meeting_schedule', session_data) self.assertCountEqual( session_data['edit_meeting_schedule']['enabled_timeslot_types'], types_to_enable, 'Should ignore unknown types', ) def test_persistent_enabled_timeslot_types(self): meeting = MeetingFactory(type_id='ietf') TimeSlotFactory(meeting=meeting, type_id='other') TimeSlotFactory(meeting=meeting, type_id='reg') # test default behavior (only 'regular' enabled) r = self.client.get(urlreverse('ietf.meeting.views.edit_meeting_schedule', kwargs={'num': meeting.number})) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertEqual(len(q('#timeslot-type-toggles-modal input[value="regular"][checked]')), 1) self.assertEqual(len(q('#timeslot-type-toggles-modal input[value="other"]:not([checked])')), 1) self.assertEqual(len(q('#timeslot-type-toggles-modal input[value="reg"]:not([checked])')), 1) # test with 'regular' and 'other' enabled via session store client_session = self.client.session # must store as var, new session is created on access client_session['edit_meeting_schedule'] = { 'enabled_timeslot_types': ['regular', 'other'] } client_session.save() r = self.client.get(urlreverse('ietf.meeting.views.edit_meeting_schedule', kwargs={'num': meeting.number})) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertEqual(len(q('#timeslot-type-toggles-modal input[value="regular"][checked]')), 1) self.assertEqual(len(q('#timeslot-type-toggles-modal input[value="other"][checked]')), 1) self.assertEqual(len(q('#timeslot-type-toggles-modal input[value="reg"]:not([checked])')), 1) class SessionDetailsTests(TestCase): def test_session_details(self): group = GroupFactory.create(type_id='wg',state_id='active') session = SessionFactory.create(meeting__type_id='ietf',group=group, meeting__date=date_today() + datetime.timedelta(days=90)) SessionPresentationFactory.create(session=session,document__type_id='draft',rev=None) SessionPresentationFactory.create(session=session,document__type_id='minutes') SessionPresentationFactory.create(session=session,document__type_id='slides') SessionPresentationFactory.create(session=session,document__type_id='agenda') url = urlreverse('ietf.meeting.views.session_details', kwargs=dict(num=session.meeting.number, acronym=group.acronym)) r = self.client.get(url) self.assertTrue(all([x in unicontent(r) for x in ('slides','agenda','minutes','draft')])) self.assertNotContains(r, 'deleted') def test_session_details_has_import_minutes_buttons(self): group = GroupFactory.create( type_id='wg', state_id='active', ) session = SessionFactory.create( meeting__type_id='ietf', group=group, meeting__date=date_today() + datetime.timedelta(days=90), ) session_details_url = urlreverse( 'ietf.meeting.views.session_details', kwargs={'num': session.meeting.number, 'acronym': group.acronym}, ) import_minutes_url = urlreverse( 'ietf.meeting.views.import_session_minutes', kwargs={'num': session.meeting.number, 'session_id': session.pk}, ) # test without existing minutes with patch('ietf.meeting.views.can_manage_session_materials', return_value=False): r = self.client.get(session_details_url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertEqual( len(q(f'a[href="{import_minutes_url}"]')), 0, 'Do not show import new minutes buttons to non-materials manager', ) with patch('ietf.meeting.views.can_manage_session_materials', return_value=True): r = self.client.get(session_details_url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertGreater( len(q(f'a[href="{import_minutes_url}"]')), 0, 'Show import new minutes buttons to materials manager', ) # now create minutes and test that we can still have the import button SessionPresentationFactory.create(session=session,document__type_id='minutes') with patch('ietf.meeting.views.can_manage_session_materials', return_value=False): r = self.client.get(session_details_url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertEqual( len(q(f'a[href="{import_minutes_url}"]')), 0, 'Do not show import revised minutes buttons to non-materials manager', ) with patch('ietf.meeting.views.can_manage_session_materials', return_value=True): r = self.client.get(session_details_url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertGreater( len(q(f'a[href="{import_minutes_url}"]')), 0, 'Show import revised minutes buttons to materials manager', ) def test_session_details_past_interim(self): group = GroupFactory.create(type_id='wg',state_id='active') chair = RoleFactory(name_id='chair',group=group) session = SessionFactory.create(meeting__type_id='interim',group=group, meeting__date=date_today() - datetime.timedelta(days=90)) SessionPresentationFactory.create(session=session,document__type_id='draft',rev=None) SessionPresentationFactory.create(session=session,document__type_id='minutes') SessionPresentationFactory.create(session=session,document__type_id='slides') SessionPresentationFactory.create(session=session,document__type_id='agenda') url = urlreverse('ietf.meeting.views.session_details', kwargs=dict(num=session.meeting.number, acronym=group.acronym)) r = self.client.get(url) self.assertEqual(r.status_code,200) self.assertNotIn('The materials upload cutoff date for this session has passed', unicontent(r)) r = self.client.get(url) self.assertEqual(r.status_code,200) self.client.login(username=chair.person.user.username,password=chair.person.user.username+'+password') self.assertTrue(all([x in unicontent(r) for x in ('slides','agenda','minutes','draft')])) def test_add_session_drafts(self): group = GroupFactory.create(type_id='wg',state_id='active') group_chair = PersonFactory.create() group.role_set.create(name_id='chair',person = group_chair, email = group_chair.email()) session = SessionFactory.create(meeting__type_id='ietf',group=group, meeting__date=date_today() + datetime.timedelta(days=90)) SessionPresentationFactory.create(session=session,document__type_id='draft',rev=None) old_draft = session.presentations.filter(document__type='draft').first().document new_draft = DocumentFactory(type_id='draft') url = urlreverse('ietf.meeting.views.add_session_drafts', kwargs=dict(num=session.meeting.number, session_id=session.pk)) r = self.client.get(url) self.assertEqual(r.status_code, 404) self.client.login(username="plain",password="plain+password") r = self.client.get(url) self.assertEqual(r.status_code, 404) self.client.login(username=group_chair.user.username, password='%s+password'%group_chair.user.username) r = self.client.get(url) self.assertContains(r, old_draft.name) r = self.client.post(url,dict(drafts=[new_draft.pk, old_draft.pk])) self.assertTrue(r.status_code, 200) q = PyQuery(r.content) self.assertIn("Already linked:", q('form .text-danger').text()) self.assertEqual(1,session.presentations.count()) r = self.client.post(url,dict(drafts=[new_draft.pk,])) self.assertTrue(r.status_code, 302) self.assertEqual(2,session.presentations.count()) session.meeting.date -= datetime.timedelta(days=180) session.meeting.save() r = self.client.get(url) self.assertEqual(r.status_code,404) self.client.login(username='secretary',password='secretary+password') r = self.client.get(url) self.assertEqual(r.status_code,200) q = PyQuery(r.content) self.assertEqual(1,len(q(".alert-warning:contains('may affect published proceedings')"))) class EditScheduleListTests(TestCase): def setUp(self): super().setUp() self.mtg = MeetingFactory(type_id='ietf') ScheduleFactory(meeting=self.mtg, name='secretary1') def test_list_schedules(self): url = urlreverse('ietf.meeting.views.list_schedules',kwargs={'num':self.mtg.number}) login_testing_unauthorized(self,"secretary",url) r = self.client.get(url) self.assertTrue(r.status_code, 200) def test_diff_schedules(self): meeting = make_meeting_test_data() url = urlreverse('ietf.meeting.views.diff_schedules',kwargs={'num':meeting.number}) login_testing_unauthorized(self,"secretary", url) r = self.client.get(url) self.assertTrue(r.status_code, 200) from_schedule = Schedule.objects.get(meeting=meeting, name="test-unofficial-schedule") session1 = Session.objects.filter(meeting=meeting, group__acronym='mars').first() session2 = Session.objects.filter(meeting=meeting, group__acronym='ames').first() session3 = SessionFactory(meeting=meeting, group=Group.objects.get(acronym='mars'), attendees=10, requested_duration=datetime.timedelta(minutes=70), add_to_schedule=False) SchedulingEvent.objects.create(session=session3, status_id='schedw', by=Person.objects.first()) slot2 = TimeSlot.objects.filter(meeting=meeting, type='regular').order_by('-time').first() slot3 = TimeSlot.objects.create( meeting=meeting, type_id='regular', location=slot2.location, duration=datetime.timedelta(minutes=60), time=slot2.time + datetime.timedelta(minutes=60), ) # copy new_url = urlreverse("ietf.meeting.views.new_meeting_schedule", kwargs=dict(num=meeting.number, owner=from_schedule.owner_email(), name=from_schedule.name)) r = self.client.post(new_url, { 'name': "newtest", 'public': "on", }) self.assertNoFormPostErrors(r) to_schedule = Schedule.objects.get(meeting=meeting, name='newtest') # make some changes edit_url = urlreverse("ietf.meeting.views.edit_meeting_schedule", kwargs=dict(num=meeting.number, owner=to_schedule.owner_email(), name=to_schedule.name)) # schedule session r = self.client.post(edit_url, { 'action': 'assign', 'timeslot': slot3.pk, 'session': session3.pk, }) self.assertEqual(json.loads(r.content)['success'], True) # unschedule session r = self.client.post(edit_url, { 'action': 'unassign', 'session': session1.pk, }) self.assertEqual(json.loads(r.content)['success'], True) # move session r = self.client.post(edit_url, { 'action': 'assign', 'timeslot': slot2.pk, 'session': session2.pk, }) self.assertEqual(json.loads(r.content)['success'], True) # now get differences r = self.client.get(url, { 'from_schedule': from_schedule.name, 'to_schedule': to_schedule.name, }) self.assertTrue(r.status_code, 200) q = PyQuery(r.content) self.assertEqual(len(q(".schedule-diffs tr")), 3+1) def test_delete_schedule(self): url = urlreverse('ietf.meeting.views.delete_schedule', kwargs={'num':self.mtg.number, 'owner':self.mtg.schedule.owner.email_address(), 'name':self.mtg.schedule.name, }) login_testing_unauthorized(self,"secretary",url) r = self.client.get(url) self.assertTrue(r.status_code, 403) r = self.client.post(url,{'save':1}) self.assertTrue(r.status_code, 403) self.assertEqual(self.mtg.schedule_set.count(),2) self.mtg.schedule=None self.mtg.save() r = self.client.get(url) self.assertTrue(r.status_code, 200) r = self.client.post(url,{'save':1}) self.assertTrue(r.status_code, 302) self.assertEqual(self.mtg.schedule_set.count(),1) def test_make_schedule_official(self): schedule = self.mtg.schedule_set.exclude(id=self.mtg.schedule.id).first() url = urlreverse('ietf.meeting.views.make_schedule_official', kwargs={'num':self.mtg.number, 'owner':schedule.owner.email_address(), 'name':schedule.name, }) login_testing_unauthorized(self,"secretary",url) r = self.client.get(url) self.assertTrue(r.status_code, 200) r = self.client.post(url,{'save':1}) self.assertTrue(r.status_code, 302) mtg = Meeting.objects.get(number=self.mtg.number) self.assertEqual(mtg.schedule,schedule) # ------------------------------------------------- # Interim Meeting Tests # ------------------------------------------------- class InterimTests(TestCase): settings_temp_path_overrides = TestCase.settings_temp_path_overrides + ['AGENDA_PATH'] # test_interim_announce subsumed by test_appears_on_announce def do_interim_skip_announcement_test(self, base_session=False, extra_session=False, canceled_session=False): make_meeting_test_data() group = Group.objects.get(acronym='irg') date = date_today() + datetime.timedelta(days=30) meeting = make_interim_meeting(group=group, date=date, status='scheda') session = meeting.session_set.first() if base_session: base_session = SessionFactory(meeting=meeting, status_id='apprw', add_to_schedule=False) meeting.schedule.base = Schedule.objects.create( meeting=meeting, owner=PersonFactory(), name="base", visible=True, public=True ) SchedTimeSessAssignment.objects.create( timeslot=TimeSlotFactory.create(meeting=meeting), session=base_session, schedule=meeting.schedule.base, ) meeting.schedule.save() if extra_session: extra_session = SessionFactory(meeting=meeting, status_id='scheda') if canceled_session: canceled_session = SessionFactory(meeting=meeting, status_id='canceledpa') url = urlreverse("ietf.meeting.views.interim_skip_announcement", kwargs={'number': meeting.number}) login_testing_unauthorized(self, "secretary", url) r = self.client.get(url) self.assertEqual(r.status_code, 200) # check post len_before = len(outbox) r = self.client.post(url) self.assertRedirects(r, urlreverse('ietf.meeting.views.interim_announce')) meeting_sessions = meeting.session_set.with_current_status() self.assertEqual(meeting_sessions.get(pk=session.pk).current_status, 'sched') if base_session: self.assertEqual(meeting_sessions.get(pk=base_session.pk).current_status, 'sched') if extra_session: self.assertEqual(meeting_sessions.get(pk=extra_session.pk).current_status, 'sched') if canceled_session: self.assertEqual(meeting_sessions.get(pk=canceled_session.pk).current_status, 'canceledpa') self.assertEqual(len(outbox), len_before) def test_interim_skip_announcement(self): """skip_announcement should move single session to sched state""" self.do_interim_skip_announcement_test() def test_interim_skip_announcement_with_base_sched(self): """skip_announcement should move single session to sched state""" self.do_interim_skip_announcement_test(base_session=True) def test_interim_skip_announcement_with_extra_session(self): """skip_announcement should move multiple sessions to sched state""" self.do_interim_skip_announcement_test(extra_session=True) def test_interim_skip_announcement_with_extra_session_and_base_sched(self): """skip_announcement should move multiple sessions to sched state""" self.do_interim_skip_announcement_test(extra_session=True, base_session=True) def test_interim_skip_announcement_with_canceled_session(self): """skip_announcement should not schedule a canceled session""" self.do_interim_skip_announcement_test(canceled_session=True) def test_interim_skip_announcement_with_canceled_session_and_base_sched(self): """skip_announcement should not schedule a canceled session""" self.do_interim_skip_announcement_test(canceled_session=True, base_session=True) def test_interim_skip_announcement_with_extra_and_canceled_sessions(self): """skip_announcement should schedule multiple sessions and leave canceled session alone""" self.do_interim_skip_announcement_test(extra_session=True, canceled_session=True) def test_interim_skip_announcement_with_extra_and_canceled_sessions_and_base_sched(self): """skip_announcement should schedule multiple sessions and leave canceled session alone""" self.do_interim_skip_announcement_test(extra_session=True, canceled_session=True, base_session=True) def do_interim_send_announcement_test(self, base_session=False, extra_session=False, canceled_session=False): make_interim_test_data(meeting_tz='America/Los_Angeles') session = Session.objects.with_current_status().filter( meeting__type='interim', group__acronym='mars', current_status='apprw').first() meeting = session.meeting if base_session: base_session = SessionFactory(meeting=meeting, status_id='apprw', add_to_schedule=False) meeting.schedule.base = Schedule.objects.create( meeting=meeting, owner=PersonFactory(), name="base", visible=True, public=True ) SchedTimeSessAssignment.objects.create( timeslot=TimeSlotFactory.create(meeting=meeting), session=base_session, schedule=meeting.schedule.base, ) meeting.schedule.save() if extra_session: extra_session = SessionFactory(meeting=meeting, status_id='apprw') if canceled_session: canceled_session = SessionFactory(meeting=meeting, status_id='canceledpa') url = urlreverse("ietf.meeting.views.interim_send_announcement", kwargs={'number': meeting.number}) login_testing_unauthorized(self, "secretary", url) r = self.client.get(url) self.assertEqual(r.status_code, 200) initial = r.context['form'].initial # send announcement len_before = len(outbox) r = self.client.post(url, initial) self.assertRedirects(r, urlreverse('ietf.meeting.views.interim_announce')) self.assertEqual(len(outbox), len_before + 1) announcement_msg = outbox[-1] announcement_text = get_payload_text(announcement_msg) self.assertIn('WG Virtual Meeting', announcement_msg['Subject']) self.assertIn('09:00 to 09:20 America/Los_Angeles', announcement_text) for sess in [session, base_session, extra_session]: if sess: timeslot = sess.official_timeslotassignment().timeslot self.assertIn(timeslot.time.strftime('%Y-%m-%d'), announcement_text) self.assertRegex( announcement_text, r'(%s\s+to\s+%s\s+UTC)' % ( timeslot.utc_start_time().strftime('%H:%M'),timeslot.utc_end_time().strftime('%H:%M') )) # Count number of sessions listed if base_session and extra_session: expected_session_matches = 3 elif base_session or extra_session: expected_session_matches = 2 else: expected_session_matches = 0 # no session list when only one session session_matches = re.findall(r'Session \d+:', announcement_text) self.assertEqual(len(session_matches), expected_session_matches) meeting_sessions = meeting.session_set.with_current_status() self.assertEqual(meeting_sessions.get(pk=session.pk).current_status, 'sched') if base_session: self.assertEqual(meeting_sessions.get(pk=base_session.pk).current_status, 'sched') if extra_session: self.assertEqual(meeting_sessions.get(pk=extra_session.pk).current_status, 'sched') if canceled_session: self.assertEqual(meeting_sessions.get(pk=canceled_session.pk).current_status, 'canceledpa') def test_interim_send_announcement(self): self.do_interim_send_announcement_test() def test_interim_send_announcement_with_base_sched(self): self.do_interim_send_announcement_test(base_session=True) def test_interim_send_announcement_with_extra_session(self): self.do_interim_send_announcement_test(extra_session=True) def test_interim_send_announcement_with_extra_session_and_base_sched(self): self.do_interim_send_announcement_test(extra_session=True, base_session=True) def test_interim_send_announcement_with_canceled_session(self): self.do_interim_send_announcement_test(canceled_session=True) def test_interim_send_announcement_with_canceled_session_and_base_sched(self): self.do_interim_send_announcement_test(canceled_session=True, base_session=True) def test_interim_send_announcement_with_extra_and_canceled_sessions(self): self.do_interim_send_announcement_test(extra_session=True, canceled_session=True) def test_interim_send_announcement_with_extra_and_canceled_sessions_and_base_sched(self): self.do_interim_send_announcement_test(extra_session=True, canceled_session=True, base_session=True) def do_interim_approve_by_ad_test(self, base_session=False, extra_session=False, canceled_session=False): make_interim_test_data() session = Session.objects.with_current_status().filter( meeting__type='interim', group__acronym='mars', current_status='apprw').first() meeting = session.meeting if base_session: base_session = SessionFactory(meeting=meeting, status_id='apprw', add_to_schedule=False) meeting.schedule.base = Schedule.objects.create( meeting=meeting, owner=PersonFactory(), name="base", visible=True, public=True ) SchedTimeSessAssignment.objects.create( timeslot=TimeSlotFactory.create(meeting=meeting), session=base_session, schedule=meeting.schedule.base, ) meeting.schedule.save() if extra_session: extra_session = SessionFactory(meeting=meeting, status_id='apprw') if canceled_session: canceled_session = SessionFactory(meeting=meeting, status_id='canceledpa') url = urlreverse('ietf.meeting.views.interim_request_details', kwargs={'number': meeting.number}) length_before = len(outbox) login_testing_unauthorized(self, "ad", url) r = self.client.post(url, {'approve': 'approve'}) self.assertRedirects(r, urlreverse('ietf.meeting.views.interim_pending')) for sess in [session, base_session, extra_session]: if sess: self.assertEqual(Session.objects.with_current_status().get(pk=sess.pk).current_status, 'scheda') if canceled_session: self.assertEqual(Session.objects.with_current_status().get(pk=canceled_session.pk).current_status, 'canceledpa') self.assertEqual(len(outbox), length_before + 1) self.assertIn('ready for announcement', outbox[-1]['Subject']) def test_interim_approve_by_ad(self): self.do_interim_approve_by_ad_test() def test_interim_approve_by_ad_with_base_sched(self): self.do_interim_approve_by_ad_test(base_session=True) def test_interim_approve_by_ad_with_extra_session(self): self.do_interim_approve_by_ad_test(extra_session=True) def test_interim_approve_by_ad_with_extra_session_and_base_sched(self): self.do_interim_approve_by_ad_test(extra_session=True, base_session=True) def test_interim_approve_by_ad_with_canceled_session(self): self.do_interim_approve_by_ad_test(canceled_session=True) def test_interim_approve_by_ad_with_canceled_session_and_base_sched(self): self.do_interim_approve_by_ad_test(canceled_session=True, base_session=True) def test_interim_approve_by_ad_with_extra_and_canceled_sessions(self): self.do_interim_approve_by_ad_test(extra_session=True, canceled_session=True) def test_interim_approve_by_ad_with_extra_and_canceled_sessions_and_base_sched(self): self.do_interim_approve_by_ad_test(extra_session=True, canceled_session=True, base_session=True) def do_interim_approve_by_secretariat_test(self, base_session=False, extra_session=False, canceled_session=False): make_interim_test_data() session = Session.objects.with_current_status().filter( meeting__type='interim', group__acronym='mars', current_status='apprw').first() meeting = session.meeting if base_session: base_session = SessionFactory(meeting=meeting, status_id='apprw', add_to_schedule=False) meeting.schedule.base = Schedule.objects.create( meeting=meeting, owner=PersonFactory(), name="base", visible=True, public=True ) SchedTimeSessAssignment.objects.create( timeslot=TimeSlotFactory.create(meeting=meeting), session=base_session, schedule=meeting.schedule.base, ) meeting.schedule.save() if extra_session: extra_session = SessionFactory(meeting=meeting, status_id='apprw') if canceled_session: canceled_session = SessionFactory(meeting=meeting, status_id='canceledpa') url = urlreverse('ietf.meeting.views.interim_request_details', kwargs={'number': meeting.number}) length_before = len(outbox) login_testing_unauthorized(self, "secretary", url) r = self.client.post(url, {'approve': 'approve'}) self.assertRedirects(r, urlreverse('ietf.meeting.views.interim_send_announcement', kwargs={'number': meeting.number})) for sess in [session, base_session, extra_session]: if sess: self.assertEqual(Session.objects.with_current_status().get(pk=sess.pk).current_status, 'scheda') if canceled_session: self.assertEqual(Session.objects.with_current_status().get(pk=canceled_session.pk).current_status, 'canceledpa') self.assertEqual(len(outbox), length_before) def test_interim_approve_by_secretariat(self): self.do_interim_approve_by_secretariat_test() def test_interim_approve_by_secretariat_with_base_sched(self): self.do_interim_approve_by_secretariat_test(base_session=True) def test_interim_approve_by_secretariat_with_extra_session(self): self.do_interim_approve_by_secretariat_test(extra_session=True) def test_interim_approve_by_secretariat_with_extra_session_and_base_sched(self): self.do_interim_approve_by_secretariat_test(extra_session=True, base_session=True) def test_interim_approve_by_secretariat_with_canceled_session(self): self.do_interim_approve_by_secretariat_test(canceled_session=True) def test_interim_approve_by_secretariat_with_canceled_session_and_base_sched(self): self.do_interim_approve_by_secretariat_test(canceled_session=True, base_session=True) def test_interim_approve_by_secretariat_with_extra_and_canceled_sessions(self): self.do_interim_approve_by_secretariat_test(extra_session=True, canceled_session=True) def test_interim_approve_by_secretariat_with_extra_and_canceled_sessions_and_base_sched(self): self.do_interim_approve_by_secretariat_test(extra_session=True, canceled_session=True, base_session=True) def test_past(self): today = date_today() last_week = today - datetime.timedelta(days=7) ietf = SessionFactory(meeting__type_id='ietf',meeting__date=last_week,group__state_id='active',group__parent=GroupFactory(state_id='active')) SessionFactory(meeting__type_id='interim',meeting__date=last_week,status_id='canceled',group__state_id='active',group__parent=GroupFactory(state_id='active')) url = urlreverse('ietf.meeting.views.past') r = self.client.get(url) self.assertContains(r, 'IETF-%02d'%int(ietf.meeting.number)) q = PyQuery(r.content) #id="-%s" % interim.group.acronym #self.assertIn('Cancelled', q('[id*="'+id+'"]').text()) self.assertIn('Cancelled', q('tr>td>a+span').text()) def do_upcoming_test(self, querystring=None, create_meeting=True): if create_meeting: make_meeting_test_data(create_interims=True) url = urlreverse("ietf.meeting.views.upcoming") if querystring is not None: url += '?' + querystring today = date_today() interims = dict( mars=add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', meeting__date__gt=today, group__acronym='mars')).filter(current_status='sched').first().meeting, ames=add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', meeting__date__gt=today, group__acronym='ames')).filter(current_status='canceled').first().meeting, ) return self.client.get(url), interims def test_upcoming(self): r, interims = self.do_upcoming_test() self.assertContains(r, interims['mars'].number) self.assertContains(r, interims['ames'].number) self.assertContains(r, 'IETF 72') # cancelled session q = PyQuery(r.content) self.assertIn('Cancelled', q('tr>td.text-end>span').text()) # test_upcoming_filters_ignored removed - we _don't_ want to ignore filters now, and the test passed because it wasn't testing the filtering anyhow (which requires testing the js). def test_upcoming_ical(self): meeting = make_meeting_test_data(create_interims=True) populate_important_dates(meeting) url = urlreverse("ietf.meeting.views.upcoming_ical") # Expect events 3 sessions - one for each WG and one for the IETF meeting expected_event_summaries = [ 'ames - Asteroid Mining Equipment Standardization Group', 'mars - Martian Special Interest Group', 'IETF 72', ] Session.objects.filter( meeting__type_id='interim', group__acronym="mars", ).update( remote_instructions='https://someurl.example.com', ) r = self.client.get(url) self.assertEqual(r.status_code, 200) assert_ical_response_is_valid(self, r, expected_event_summaries=expected_event_summaries, expected_event_count=len(expected_event_summaries)) self.assertContains(r, 'Remote instructions: https://someurl.example.com') Session.objects.filter(meeting__type_id='interim').update(remote_instructions='') r = self.client.get(url) self.assertEqual(r.status_code, 200) assert_ical_response_is_valid(self, r, expected_event_summaries=expected_event_summaries, expected_event_count=len(expected_event_summaries)) self.assertNotContains(r, 'Remote instructions:') updated = meeting.updated() self.assertIsNotNone(updated) expected_updated = updated.astimezone(datetime.timezone.utc).strftime("%Y%m%dT%H%M%SZ") self.assertContains(r, f"DTSTAMP:{expected_updated}") # With default cached_updated, 1970-01-01 with patch("ietf.meeting.models.Meeting.updated", return_value=None): r = self.client.get(url) self.assertEqual(r.status_code, 200) self.assertEqual(meeting.type_id, "ietf") expected_updated = "19700101T000000Z" self.assertEqual(1, r.content.decode("utf-8").count(f"DTSTAMP:{expected_updated}")) @patch("ietf.meeting.utils.preprocess_meeting_important_dates") def test_upcoming_ical_filter(self, mock_preprocess_meeting_important_dates): # Just a quick check of functionality - details tested by test_js.InterimTests make_meeting_test_data(create_interims=True) url = urlreverse("ietf.meeting.views.upcoming_ical") r = self.client.get(url + '?show=mars') self.assertEqual(r.status_code, 200) assert_ical_response_is_valid(self, r, expected_event_summaries=[ 'mars - Martian Special Interest Group', ], expected_event_count=1) r = self.client.get(url + '?show=mars,ietf-meetings') self.assertEqual(r.status_code, 200) assert_ical_response_is_valid(self, r, expected_event_summaries=[ 'mars - Martian Special Interest Group', 'IETF 72', ], expected_event_count=2) # Verify preprocess_meeting_important_dates isn't being called mock_preprocess_meeting_important_dates.assert_not_called() def test_upcoming_json(self): make_meeting_test_data(create_interims=True) url = urlreverse("ietf.meeting.views.upcoming_json") r = self.client.get(url) self.assertEqual(r.status_code, 200) self.assertEqual(r.get('Content-Type'), "application/json;charset=utf-8") data = r.json() self.assertEqual(len(data), 3) def test_interim_request_permissions(self): '''Ensure only authorized users see link to request interim meeting''' make_meeting_test_data() # test unauthorized not logged in upcoming_url = urlreverse("ietf.meeting.views.upcoming") request_url = urlreverse("ietf.meeting.views.interim_request") r = self.client.get(upcoming_url) self.assertNotContains(r,'Request new interim meeting') # test unauthorized user login_testing_unauthorized(self,"plain",request_url) r = self.client.get(upcoming_url) self.assertNotContains(r,'Request new interim meeting') r = self.client.get(request_url) self.assertEqual(r.status_code, 403) self.client.logout() # test authorized for username in ('secretary','ad','marschairman','irtf-chair','irgchairman'): self.client.login(username=username, password= username + "+password") r = self.client.get(upcoming_url) self.assertContains(r,'Request new interim meeting') r = self.client.get(request_url) self.assertEqual(r.status_code, 200) self.client.logout() def test_interim_request_options(self): make_meeting_test_data() # secretariat can request for any group self.client.login(username="secretary", password="secretary+password") r = self.client.get("/meeting/interim/request/") self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertEqual(Group.objects.with_meetings().filter(state__in=('active', 'proposed', 'bof')).count(), len(q("#id_group option")) - 1) # -1 for options placeholder self.client.logout() # wg chair self.client.login(username="marschairman", password="marschairman+password") r = self.client.get("/meeting/interim/request/") self.assertEqual(r.status_code, 200) q = PyQuery(r.content) user = User.objects.get(username='marschairman') person = user.person count = person.role_set.filter(name='chair',group__type__in=('wg', 'rg'), group__state__in=('active', 'proposed')).count() self.assertEqual(count, len(q("#id_group option")) - 1) # -1 for options placeholder # wg AND rg chair group = Group.objects.get(acronym='irg') Role.objects.create(name_id='chair',group=group,person=person,email=person.email()) r = self.client.get("/meeting/interim/request/") self.assertEqual(r.status_code, 200) q = PyQuery(r.content) count = person.role_set.filter(name='chair',group__type__in=('wg', 'rg'), group__state__in=('active', 'proposed')).count() self.assertEqual(count, len(q("#id_group option")) - 1) # -1 for options placeholder def do_interim_request_single_virtual(self, emails_expected): make_meeting_test_data() group = Group.objects.get(acronym='mars') date = date_today() + datetime.timedelta(days=30) time = time_now().replace(microsecond=0,second=0) dt = pytz.utc.localize(datetime.datetime.combine(date, time)) duration = datetime.timedelta(hours=3) remote_instructions = 'Use webex' agenda = 'Intro. Slides. Discuss.' agenda_note = 'On second level' length_before = len(outbox) meeting_count = Meeting.objects.filter(number__contains='-%s-'%group.acronym, date__year=date.year).count() next_num = "%02d" % (meeting_count+1) self.client.login(username="marschairman", password="marschairman+password") data = {'group':group.pk, 'meeting_type':'single', 'city':'', 'country':'', 'time_zone':'UTC', 'session_set-0-date':date.strftime("%Y-%m-%d"), 'session_set-0-time':time.strftime('%H:%M'), 'session_set-0-requested_duration':'03:00:00', 'session_set-0-remote_instructions':remote_instructions, 'session_set-0-agenda':agenda, 'session_set-0-agenda_note':agenda_note, 'session_set-TOTAL_FORMS':1, 'session_set-INITIAL_FORMS':0, 'session_set-MIN_NUM_FORMS':0, 'session_set-MAX_NUM_FORMS':1000} with patch('ietf.meeting.views.sessions_post_save', wraps=sessions_post_save) as mock: r = self.client.post(urlreverse("ietf.meeting.views.interim_request"),data) self.assertTrue(mock.called) self.assertRedirects(r,urlreverse('ietf.meeting.views.upcoming')) meeting = Meeting.objects.order_by('id').last() self.assertEqual(meeting.type_id,'interim') self.assertEqual(meeting.date,date) self.assertEqual(meeting.number,'interim-%s-%s-%s' % (date.year, group.acronym, next_num)) self.assertEqual(meeting.city,'') self.assertEqual(meeting.country,'') self.assertEqual(meeting.time_zone,'UTC') session = meeting.session_set.first() self.assertEqual(session.remote_instructions,remote_instructions) self.assertEqual(session.agenda_note,agenda_note) timeslot = session.official_timeslotassignment().timeslot self.assertEqual(timeslot.time,dt) self.assertEqual(timeslot.duration,duration) # ensure agenda document was created self.assertEqual(session.materials.count(),1) doc = session.materials.first() path = os.path.join(doc.get_file_path(),doc.filename_with_rev()) self.assertTrue(os.path.exists(path)) # check notices to secretariat and chairs self.assertEqual(len(outbox), length_before + emails_expected) return meeting @override_settings(VIRTUAL_INTERIMS_REQUIRE_APPROVAL = True) def test_interim_request_single_virtual_settings_approval_required(self): meeting = self.do_interim_request_single_virtual(emails_expected=1) self.assertEqual(meeting.session_set.last().schedulingevent_set.last().status_id,'apprw') self.assertIn('New Interim Meeting Request', outbox[-1]['Subject']) self.assertIn('session-request@ietf.org', outbox[-1]['To']) self.assertIn('aread@example.org', outbox[-1]['Cc']) @override_settings(VIRTUAL_INTERIMS_REQUIRE_APPROVAL = False) def test_interim_request_single_virtual_settings_approval_not_required(self): meeting = self.do_interim_request_single_virtual(emails_expected=2) self.assertEqual(meeting.session_set.last().schedulingevent_set.last().status_id,'scheda') self.assertIn('iesg-secretary@ietf.org', outbox[-1]['To']) self.assertIn('interim meeting ready for announcement', outbox[-1]['Subject']) def test_interim_request_single_in_person(self): make_meeting_test_data() group = Group.objects.get(acronym='mars') date = date_today() + datetime.timedelta(days=30) time = time_now().replace(microsecond=0,second=0) time_zone = 'America/Los_Angeles' tz = pytz.timezone(time_zone) dt = tz.localize(datetime.datetime.combine(date, time)) duration = datetime.timedelta(hours=3) city = 'San Francisco' country = 'US' remote_instructions = 'Use webex' agenda = 'Intro. Slides. Discuss.' agenda_note = 'On second level' meeting_count = Meeting.objects.filter(number__contains='-%s-'%group.acronym, date__year=date.year).count() next_num = "%02d" % (meeting_count+1) self.client.login(username="secretary", password="secretary+password") data = {'group':group.pk, 'meeting_type':'single', 'city':city, 'country':country, 'time_zone':time_zone, 'session_set-0-date':date.strftime("%Y-%m-%d"), 'session_set-0-time':time.strftime('%H:%M'), 'session_set-0-requested_duration':'03:00:00', 'session_set-0-remote_instructions':remote_instructions, 'session_set-0-agenda':agenda, 'session_set-0-agenda_note':agenda_note, 'session_set-TOTAL_FORMS':1, 'session_set-INITIAL_FORMS':0} with patch('ietf.meeting.views.sessions_post_save', wraps=sessions_post_save) as mock: r = self.client.post(urlreverse("ietf.meeting.views.interim_request"),data) self.assertTrue(mock.called) self.assertRedirects(r,urlreverse('ietf.meeting.views.upcoming')) meeting = Meeting.objects.order_by('id').last() self.assertEqual(meeting.type_id,'interim') self.assertEqual(meeting.date,date) self.assertEqual(meeting.number,'interim-%s-%s-%s' % (date.year, group.acronym, next_num)) self.assertEqual(meeting.city,city) self.assertEqual(meeting.country,country) self.assertEqual(meeting.time_zone,time_zone) session = meeting.session_set.first() self.assertEqual(session.remote_instructions,remote_instructions) self.assertEqual(session.agenda_note,agenda_note) timeslot = session.official_timeslotassignment().timeslot self.assertEqual(timeslot.time,dt) self.assertEqual(timeslot.duration,duration) def test_interim_request_multi_day(self): make_meeting_test_data() date = date_today() + datetime.timedelta(days=30) date2 = date + datetime.timedelta(days=1) time = time_now().replace(microsecond=0,second=0) time_zone = 'America/Los_Angeles' tz = pytz.timezone(time_zone) dt = tz.localize(datetime.datetime.combine(date, time)) dt2 = tz.localize(datetime.datetime.combine(date2, time)) duration = datetime.timedelta(hours=3) group = Group.objects.get(acronym='mars') city = 'San Francisco' country = 'US' remote_instructions = 'Use webex' agenda = 'Intro. Slides. Discuss.' agenda_note = 'On second level' meeting_count = Meeting.objects.filter(number__contains='-%s-'%group.acronym, date__year=date.year).count() next_num = "%02d" % (meeting_count+1) self.client.login(username="secretary", password="secretary+password") data = {'group':group.pk, 'meeting_type':'multi-day', 'city':city, 'country':country, 'time_zone':time_zone, 'session_set-0-date':date.strftime("%Y-%m-%d"), 'session_set-0-time':time.strftime('%H:%M'), 'session_set-0-requested_duration':'03:00:00', 'session_set-0-remote_instructions':remote_instructions, 'session_set-0-agenda':agenda, 'session_set-0-agenda_note':agenda_note, 'session_set-1-date':date2.strftime("%Y-%m-%d"), 'session_set-1-time':time.strftime('%H:%M'), 'session_set-1-requested_duration':'03:00:00', 'session_set-1-remote_instructions':remote_instructions, 'session_set-1-agenda':agenda, 'session_set-1-agenda_note':agenda_note, 'session_set-TOTAL_FORMS':2, 'session_set-INITIAL_FORMS':0} with patch('ietf.meeting.views.sessions_post_save', wraps=sessions_post_save) as mock: r = self.client.post(urlreverse("ietf.meeting.views.interim_request"),data) self.assertTrue(mock.called) self.assertRedirects(r,urlreverse('ietf.meeting.views.upcoming')) meeting = Meeting.objects.order_by('id').last() self.assertEqual(meeting.type_id,'interim') self.assertEqual(meeting.date,date) self.assertEqual(meeting.number,'interim-%s-%s-%s' % (date.year, group.acronym, next_num)) self.assertEqual(meeting.city,city) self.assertEqual(meeting.country,country) self.assertEqual(meeting.time_zone,time_zone) self.assertEqual(meeting.session_set.count(),2) # first sesstion session = meeting.session_set.all()[0] self.assertEqual(session.remote_instructions,remote_instructions) timeslot = session.official_timeslotassignment().timeslot self.assertEqual(timeslot.time,dt) self.assertEqual(timeslot.duration,duration) self.assertEqual(session.agenda_note,agenda_note) # second sesstion session = meeting.session_set.all()[1] self.assertEqual(session.remote_instructions,remote_instructions) timeslot = session.official_timeslotassignment().timeslot self.assertEqual(timeslot.time,dt2) self.assertEqual(timeslot.duration,duration) self.assertEqual(session.agenda_note,agenda_note) def test_interim_request_multi_day_non_consecutive(self): make_meeting_test_data() date = date_today() + datetime.timedelta(days=30) date2 = date + datetime.timedelta(days=2) time = timezone.now().time().replace(microsecond=0,second=0) group = Group.objects.get(acronym='mars') city = 'San Francisco' country = 'US' time_zone = 'America/Los_Angeles' remote_instructions = 'Use webex' agenda = 'Intro. Slides. Discuss.' agenda_note = 'On second level' self.client.login(username="secretary", password="secretary+password") data = {'group':group.pk, 'meeting_type':'multi-day', 'city':city, 'country':country, 'time_zone':time_zone, 'session_set-0-date':date.strftime("%Y-%m-%d"), 'session_set-0-time':time.strftime('%H:%M'), 'session_set-0-requested_duration':'03:00:00', 'session_set-0-remote_instructions':remote_instructions, 'session_set-0-agenda':agenda, 'session_set-0-agenda_note':agenda_note, 'session_set-1-date':date2.strftime("%Y-%m-%d"), 'session_set-1-time':time.strftime('%H:%M'), 'session_set-1-requested_duration':'03:00:00', 'session_set-1-remote_instructions':remote_instructions, 'session_set-1-agenda':agenda, 'session_set-1-agenda_note':agenda_note, 'session_set-TOTAL_FORMS':2, 'session_set-INITIAL_FORMS':0} r = self.client.post(urlreverse("ietf.meeting.views.interim_request"),data) self.assertContains(r, 'days must be consecutive') def test_interim_request_multi_day_cancel(self): """All sessions of a multi-day interim request should be canceled""" length_before = len(outbox) date = date_today() + datetime.timedelta(days=15) # Set up an interim request with several sessions num_sessions = 3 meeting = MeetingFactory(type_id='interim', date=date) for _ in range(num_sessions): SessionFactory(meeting=meeting) # Cancel the interim request url = urlreverse('ietf.meeting.views.interim_request_cancel', kwargs={'number': meeting.number}) self.client.login(username="secretary", password="secretary+password") r = self.client.post(url) # Verify results self.assertRedirects(r, urlreverse('ietf.meeting.views.upcoming')) for session in add_event_info_to_session_qs(meeting.session_set.all()): self.assertEqual(session.current_status, 'canceled') self.assertEqual(len(outbox), length_before + 1) self.assertIn('Interim Meeting Cancelled', outbox[-1]['Subject']) def test_interim_request_series(self): make_meeting_test_data() meeting_count_before = Meeting.objects.filter(type='interim').count() date = date_today() + datetime.timedelta(days=30) if (date.month, date.day) == (12, 31): # Avoid date and date2 in separate years # (otherwise the test will fail if run on December 1st) date += datetime.timedelta(days=1) date2 = date + datetime.timedelta(days=1) # ensure dates are in the same year if date.year != date2.year: date += datetime.timedelta(days=1) date2 += datetime.timedelta(days=1) time = time_now().replace(microsecond=0,second=0) time_zone = 'America/Los_Angeles' tz = pytz.timezone(time_zone) dt = tz.localize(datetime.datetime.combine(date, time)) dt2 = tz.localize(datetime.datetime.combine(date2, time)) duration = datetime.timedelta(hours=3) group = Group.objects.get(acronym='mars') city = '' country = '' remote_instructions = 'Use webex' agenda = 'Intro. Slides. Discuss.' agenda_note = 'On second level' meeting_count = Meeting.objects.filter(number__contains='-%s-'%group.acronym, date__year=date.year).count() next_num = "%02d" % (meeting_count+1) next_num2 = "%02d" % (meeting_count+2) self.client.login(username="secretary", password="secretary+password") r = self.client.get(urlreverse("ietf.meeting.views.interim_request")) self.assertEqual(r.status_code, 200) data = {'group':group.pk, 'meeting_type':'series', 'city':city, 'country':country, 'time_zone':time_zone, 'session_set-0-date':date.strftime("%Y-%m-%d"), 'session_set-0-time':time.strftime('%H:%M'), 'session_set-0-requested_duration':'03:00:00', 'session_set-0-remote_instructions':remote_instructions, 'session_set-0-agenda':agenda, 'session_set-0-agenda_note':agenda_note, 'session_set-1-date':date2.strftime("%Y-%m-%d"), 'session_set-1-time':time.strftime('%H:%M'), 'session_set-1-requested_duration':'03:00:00', 'session_set-1-remote_instructions':remote_instructions, 'session_set-1-agenda':agenda, 'session_set-1-agenda_note':agenda_note, 'session_set-TOTAL_FORMS':2, 'session_set-INITIAL_FORMS':0} with patch('ietf.meeting.views.sessions_post_save', wraps=sessions_post_save) as mock: r = self.client.post(urlreverse("ietf.meeting.views.interim_request"),data) self.assertTrue(mock.called) self.assertRedirects(r,urlreverse('ietf.meeting.views.upcoming')) meeting_count_after = Meeting.objects.filter(type='interim').count() self.assertEqual(meeting_count_after,meeting_count_before + 2) meetings = Meeting.objects.order_by('-id')[:2] # first meeting meeting = meetings[1] self.assertEqual(meeting.type_id,'interim') self.assertEqual(meeting.date,date) self.assertEqual(meeting.number,'interim-%s-%s-%s' % (date.year, group.acronym, next_num)) self.assertEqual(meeting.city,city) self.assertEqual(meeting.country,country) self.assertEqual(meeting.time_zone,time_zone) self.assertEqual(meeting.session_set.count(),1) session = meeting.session_set.first() self.assertEqual(session.remote_instructions,remote_instructions) timeslot = session.official_timeslotassignment().timeslot self.assertEqual(timeslot.time,dt) self.assertEqual(timeslot.duration,duration) self.assertEqual(session.agenda_note,agenda_note) # second meeting meeting = meetings[0] self.assertEqual(meeting.type_id,'interim') self.assertEqual(meeting.date,date2) self.assertEqual(meeting.number,'interim-%s-%s-%s' % (date2.year, group.acronym, next_num2)) self.assertEqual(meeting.city,city) self.assertEqual(meeting.country,country) self.assertEqual(meeting.time_zone,time_zone) self.assertEqual(meeting.session_set.count(),1) session = meeting.session_set.first() self.assertEqual(session.remote_instructions,remote_instructions) timeslot = session.official_timeslotassignment().timeslot self.assertEqual(timeslot.time,dt2) self.assertEqual(timeslot.duration,duration) self.assertEqual(session.agenda_note,agenda_note) # test_interim_pending subsumed by test_appears_on_pending def test_can_approve_interim_request(self): make_interim_test_data() # unprivileged user user = User.objects.get(username='plain') group = Group.objects.get(acronym='mars') meeting = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', group=group)).filter(current_status='apprw').first().meeting self.assertFalse(can_approve_interim_request(meeting=meeting,user=user)) # Secretariat user = User.objects.get(username='secretary') self.assertTrue(can_approve_interim_request(meeting=meeting,user=user)) # related AD user = User.objects.get(username='ad') self.assertTrue(can_approve_interim_request(meeting=meeting,user=user)) # AD from other area user = User.objects.get(username='ops-ad') self.assertFalse(can_approve_interim_request(meeting=meeting,user=user)) # AD from other area assigned as the WG AD anyhow (cross-area AD) user = RoleFactory(name_id='ad',group=group).person.user self.assertTrue(can_approve_interim_request(meeting=meeting,user=user)) # WG Chair user = User.objects.get(username='marschairman') self.assertFalse(can_approve_interim_request(meeting=meeting,user=user)) def test_can_view_interim_request(self): make_interim_test_data() # unprivileged user user = User.objects.get(username='plain') group = Group.objects.get(acronym='mars') meeting = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', group=group)).filter(current_status='apprw').first().meeting self.assertFalse(can_view_interim_request(meeting=meeting,user=user)) # Secretariat user = User.objects.get(username='secretary') self.assertTrue(can_view_interim_request(meeting=meeting,user=user)) # related AD user = User.objects.get(username='ad') self.assertTrue(can_view_interim_request(meeting=meeting,user=user)) # other AD user = User.objects.get(username='ops-ad') self.assertTrue(can_view_interim_request(meeting=meeting,user=user)) # WG Chair user = User.objects.get(username='marschairman') self.assertTrue(can_view_interim_request(meeting=meeting,user=user)) # Other WG Chair user = User.objects.get(username='ameschairman') self.assertFalse(can_view_interim_request(meeting=meeting,user=user)) def test_can_manage_group(self): make_meeting_test_data() # unprivileged user user = User.objects.get(username='plain') group = Group.objects.get(acronym='mars') self.assertFalse(can_manage_group(user=user,group=group)) # Secretariat user = User.objects.get(username='secretary') self.assertTrue(can_manage_group(user=user,group=group)) # related AD user = User.objects.get(username='ad') self.assertTrue(can_manage_group(user=user,group=group)) # other AD user = User.objects.get(username='ops-ad') self.assertTrue(can_manage_group(user=user,group=group)) # WG Chair user = User.objects.get(username='marschairman') self.assertTrue(can_manage_group(user=user,group=group)) # Other WG Chair user = User.objects.get(username='ameschairman') self.assertFalse(can_manage_group(user=user,group=group)) def test_interim_request_details(self): make_interim_test_data(meeting_tz='America/Chicago') meeting = Session.objects.with_current_status().filter( meeting__type='interim', group__acronym='mars', current_status='apprw').first().meeting url = urlreverse('ietf.meeting.views.interim_request_details',kwargs={'number':meeting.number}) login_testing_unauthorized(self,"secretary",url) r = self.client.get(url) self.assertEqual(r.status_code, 200) start_time = meeting.session_set.first().official_timeslotassignment().timeslot.local_start_time().strftime('%H:%M') utc_start_time = meeting.session_set.first().official_timeslotassignment().timeslot.utc_start_time().strftime('%H:%M') self.assertIn(start_time, unicontent(r)) self.assertIn(utc_start_time, unicontent(r)) def test_interim_request_details_announcement(self): '''Test access to Announce / Skip Announce features''' make_meeting_test_data() date = date_today() + datetime.timedelta(days=30) group = Group.objects.get(acronym='mars') meeting = make_interim_meeting(group=group, date=date, status='scheda') url = urlreverse('ietf.meeting.views.interim_request_details',kwargs={'number':meeting.number}) # Chair, no access self.client.login(username="marschairman", password="marschairman+password") r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertEqual(len(q("a.btn:contains('Announce')")),0) # Secretariat has access self.client.login(username="secretary", password="secretary+password") r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertEqual(len(q("a.btn:contains('nnounce')")),2) def test_interim_request_details_cancel(self): """Test access to cancel meeting / session features""" make_interim_test_data() mars_sessions = Session.objects.with_current_status( ).filter( meeting__type='interim', group__acronym='mars', ) meeting_apprw = mars_sessions.filter(current_status='apprw').first().meeting meeting_sched = mars_sessions.filter(current_status='sched').first().meeting # All these roles should have access to cancel the request usernames_and_passwords = ( ('marschairman', 'marschairman+password'), ('secretary', 'secretary+password') ) # Start with one session - there should not be any cancel session buttons for meeting in (meeting_apprw, meeting_sched): url = urlreverse('ietf.meeting.views.interim_request_details', kwargs={'number': meeting.number}) for username, password in usernames_and_passwords: self.client.login(username=username, password=password) r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) cancel_meeting_btns = q("a.btn:contains('Cancel meeting')") self.assertEqual(len(cancel_meeting_btns), 1, 'Should be exactly one cancel meeting button for user %s' % username) self.assertEqual(cancel_meeting_btns.eq(0).attr('href'), urlreverse('ietf.meeting.views.interim_request_cancel', kwargs={'number': meeting.number}), 'Cancel meeting points to wrong URL') self.assertEqual(len(q("a.btn:contains('Cancel Session')")), 0, 'Should be no cancel session buttons for user %s' % username) # Add a second session SessionFactory(meeting=meeting_apprw, status_id='apprw') SessionFactory(meeting=meeting_sched, status_id='sched') for meeting in (meeting_apprw, meeting_sched): url = urlreverse('ietf.meeting.views.interim_request_details', kwargs={'number': meeting.number}) for username, password in usernames_and_passwords: self.client.login(username=username, password=password) r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) cancel_meeting_btns = q("a.btn:contains('Cancel meeting')") self.assertEqual(len(cancel_meeting_btns), 1, 'Should be exactly one cancel meeting button for user %s' % username) self.assertEqual(cancel_meeting_btns.eq(0).attr('href'), urlreverse('ietf.meeting.views.interim_request_cancel', kwargs={'number': meeting.number}), 'Cancel meeting button points to wrong URL') cancel_session_btns = q("a.btn:contains('Cancel session')") self.assertEqual(len(cancel_session_btns), 2, 'Should be two cancel session buttons for user %s' % username) hrefs = [btn.attr('href') for btn in cancel_session_btns.items()] for index, session in enumerate(meeting.session_set.all()): self.assertIn(urlreverse('ietf.meeting.views.interim_request_session_cancel', kwargs={'sessionid': session.pk}), hrefs, 'Session missing a link to its cancel URL') def test_interim_request_details_status(self): """Test statuses on the interim request details page""" make_interim_test_data() some_person = PersonFactory() self.client.login(username='marschairman', password='marschairman+password') # These are the first sessions for each meeting - hang on to them sessions = list( Session.objects.with_current_status().filter(meeting__type='interim', group__acronym='mars') ) # Hack: change the name for the 'canceled' session status so we can tell it apart # from the 'canceledpa' session status more easily canceled_status = SessionStatusName.objects.get(slug='canceled') canceled_status.name = 'This is cancelled' canceled_status.save() canceledpa_status = SessionStatusName.objects.get(slug='canceledpa') notmeet_status = SessionStatusName.objects.get(slug='notmeet') # Simplest case - single session for each meeting for session in [Session.objects.with_current_status().get(pk=s.pk) for s in sessions]: url = urlreverse('ietf.meeting.views.interim_request_details', kwargs={'number': session.meeting.number}) r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) status = SessionStatusName.objects.get(slug=session.current_status) self.assertEqual( len(q("dd:contains('%s')" % status.name)), 1 # once - for the meeting status, no session status shown when only one session ) # Now add a second session with a different status - it should not change meeting status for session in [Session.objects.with_current_status().get(pk=s.pk) for s in sessions]: SessionFactory(meeting=session.meeting, status_id=notmeet_status.pk) url = urlreverse('ietf.meeting.views.interim_request_details', kwargs={'number': session.meeting.number}) r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) status = SessionStatusName.objects.get(slug=session.current_status) self.assertEqual( len(q("dd:contains('%s')" % status.name)), 2 # twice - once as the meeting status, once as the session status ) self.assertEqual( len(q("dd:contains('%s')" % notmeet_status.name)), 1 # only for the session status ) # Now cancel the first session - second meeting status should be shown for meeting for session in [Session.objects.with_current_status().get(pk=s.pk) for s in sessions]: # Use 'canceledpa' here and 'canceled' later SchedulingEvent.objects.create(session=session, status=canceledpa_status, by=some_person) url = urlreverse('ietf.meeting.views.interim_request_details', kwargs={'number': session.meeting.number}) r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertEqual( len(q("dd:contains('%s')" % canceledpa_status.name)), 1 # only for the session status ) self.assertEqual( len(q("dd:contains('%s')" % notmeet_status.name)), 2 # twice - once as the meeting status, once as the session status ) # Now cancel the second session - first meeting status should be shown for meeting again for session in [Session.objects.with_current_status().get(pk=s.pk) for s in sessions]: second_session = session.meeting.session_set.exclude(pk=session.pk).first() # use canceled so we can differentiate between the first and second session statuses SchedulingEvent.objects.create(session=second_session, status=canceled_status, by=some_person) url = urlreverse('ietf.meeting.views.interim_request_details', kwargs={'number': session.meeting.number}) r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertEqual( len(q("dd:contains('%s')" % canceledpa_status.name)), 2 # twice - once as the meeting status, once as the session status ) self.assertEqual( len(q("dd:contains('%s')" % canceled_status.name)), 1 # only as the session status ) def do_interim_request_disapprove_test(self, extra_session=False, canceled_session=False): make_interim_test_data() session = Session.objects.with_current_status().filter( meeting__type='interim', group__acronym='mars', current_status='apprw').first() meeting = session.meeting if extra_session: extra_session = SessionFactory(meeting=meeting, status_id='apprw') if canceled_session: canceled_session = SessionFactory(meeting=meeting, status_id='canceledpa') url = urlreverse('ietf.meeting.views.interim_request_details',kwargs={'number':meeting.number}) login_testing_unauthorized(self,"secretary",url) r = self.client.post(url,{'disapprove':'Disapprove'}) self.assertRedirects(r, urlreverse('ietf.meeting.views.interim_pending')) for sess in [session, extra_session]: if sess: self.assertEqual(Session.objects.with_current_status().get(pk=sess.pk).current_status, 'disappr') if canceled_session: self.assertEqual(Session.objects.with_current_status().get(pk=canceled_session.pk).current_status, 'canceledpa') def test_interim_request_disapprove(self): self.do_interim_request_disapprove_test() def test_interim_request_disapprove_with_extra_session(self): self.do_interim_request_disapprove_test(extra_session=True) def test_interim_request_disapprove_with_canceled_session(self): self.do_interim_request_disapprove_test(canceled_session=True) def test_interim_request_disapprove_with_extra_and_canceled_sessions(self): self.do_interim_request_disapprove_test(extra_session=True, canceled_session=True) @patch('ietf.meeting.views.sessions_post_cancel') def test_interim_request_cancel(self, mock): """Test that interim request cancel function works Does not test that UI buttons are present, that is handled elsewhere. """ make_interim_test_data() meeting = Session.objects.with_current_status( ).filter( meeting__type='interim', group__acronym='mars', current_status='apprw', ).first().meeting # ensure fail unauthorized url = urlreverse('ietf.meeting.views.interim_request_cancel', kwargs={'number': meeting.number}) comments = 'Bob cannot make it' self.client.login(username="ameschairman", password="ameschairman+password") r = self.client.post(url, {'comments': comments}) self.assertEqual(r.status_code, 403) self.assertFalse(mock.called, 'Should not cancel sessions if request rejected') # test with overly-long comments comments += '0123456789abcdef'*32 self.client.login(username="marschairman", password="marschairman+password") r = self.client.post(url, {'comments': comments}) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertTrue(q('form .is-invalid')) # truncate to max_length comments = comments[:512] # test cancelling before announcement length_before = len(outbox) r = self.client.post(url, {'comments': comments}) self.assertRedirects(r, urlreverse('ietf.meeting.views.upcoming')) for session in meeting.session_set.with_current_status(): self.assertEqual(session.current_status,'canceledpa') self.assertEqual(session.agenda_note, comments) self.assertEqual(len(outbox), length_before) # no email notice self.assertTrue(mock.called, 'Should cancel sessions if request handled') self.assertCountEqual(mock.call_args[0][1], meeting.session_set.all()) # test cancelling after announcement mock.reset_mock() meeting = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', group__acronym='mars')).filter(current_status='sched').first().meeting url = urlreverse('ietf.meeting.views.interim_request_cancel', kwargs={'number': meeting.number}) r = self.client.post(url, {'comments': comments}) self.assertRedirects(r, urlreverse('ietf.meeting.views.upcoming')) for session in meeting.session_set.with_current_status(): self.assertEqual(session.current_status,'canceled') self.assertEqual(session.agenda_note, comments) self.assertEqual(len(outbox), length_before + 1) self.assertIn('Interim Meeting Cancelled', outbox[-1]['Subject']) self.assertIn(comments, get_payload_text(outbox[-1])) self.assertTrue(mock.called, 'Should cancel sessions if request handled') self.assertCountEqual(mock.call_args[0][1], meeting.session_set.all()) @patch('ietf.meeting.views.sessions_post_cancel') def test_interim_request_session_cancel(self, mock): """Test that interim meeting session cancellation functions Does not test that UI buttons are present, that is handled elsewhere. """ make_interim_test_data() session = Session.objects.with_current_status().filter( meeting__type='interim', group__acronym='mars', current_status='apprw',).first() meeting = session.meeting comments = 'Bob cannot make it' # Should not be able to cancel when there is only one session self.client.login(username="marschairman", password="marschairman+password") url = urlreverse('ietf.meeting.views.interim_request_session_cancel', kwargs={'sessionid': session.pk}) r = self.client.post(url, {'comments': comments}) self.assertEqual(r.status_code, 409) self.assertFalse(mock.called, 'Should not cancel sessions if request rejected') # Add a second session SessionFactory(meeting=meeting, status_id='apprw') # ensure fail unauthorized url = urlreverse('ietf.meeting.views.interim_request_session_cancel', kwargs={'sessionid': session.pk}) self.client.login(username="ameschairman", password="ameschairman+password") r = self.client.post(url, {'comments': comments}) self.assertEqual(r.status_code, 403) self.assertFalse(mock.called, 'Should not cancel sessions if request rejected') # test cancelling before announcement self.client.login(username="marschairman", password="marschairman+password") length_before = len(outbox) canceled_count_before = meeting.session_set.with_current_status().filter( current_status__in=['canceled', 'canceledpa']).count() r = self.client.post(url, {'comments': comments}) self.assertRedirects(r, urlreverse('ietf.meeting.views.interim_request_details', kwargs={'number': meeting.number})) self.assertTrue(mock.called, 'Should cancel sessions if request handled') self.assertCountEqual(mock.call_args[0][1], [session]) # This session should be canceled... sessions = meeting.session_set.with_current_status() session = sessions.filter(id=session.pk).first() # reload our session info self.assertEqual(session.current_status, 'canceledpa') self.assertEqual(session.agenda_note, comments) # But others should not - count should have changed by only 1 self.assertEqual( sessions.filter(current_status__in=['canceled', 'canceledpa']).count(), canceled_count_before + 1 ) self.assertEqual(len(outbox), length_before) # no email notice # test cancelling after announcement mock.reset_mock() session = Session.objects.with_current_status().filter( meeting__type='interim', group__acronym='mars', current_status='sched').first() meeting = session.meeting # Try to cancel when there's only one session in the meeting url = urlreverse('ietf.meeting.views.interim_request_session_cancel', kwargs={'sessionid': session.pk}) r = self.client.post(url, {'comments': comments}) self.assertEqual(r.status_code, 409) self.assertFalse(mock.called, 'Should not cancel sessions if request rejected') # Add another session SessionFactory(meeting=meeting, status_id='sched') # two sessions so canceling a session makes sense canceled_count_before = meeting.session_set.with_current_status().filter( current_status__in=['canceled', 'canceledpa']).count() r = self.client.post(url, {'comments': comments}) self.assertRedirects(r, urlreverse('ietf.meeting.views.interim_request_details', kwargs={'number': meeting.number})) self.assertTrue(mock.called, 'Should cancel sessions if request handled') self.assertCountEqual(mock.call_args[0][1], [session]) # This session should be canceled... sessions = meeting.session_set.with_current_status() session = sessions.filter(id=session.pk).first() # reload our session info self.assertEqual(session.current_status, 'canceled') self.assertEqual(session.agenda_note, comments) # But others should not - count should have changed by only 1 self.assertEqual( sessions.filter(current_status__in=['canceled', 'canceledpa']).count(), canceled_count_before + 1 ) self.assertEqual(len(outbox), length_before + 1) # email notice sent self.assertIn('session cancelled', outbox[-1]['Subject']) def test_interim_request_edit_no_notice(self): '''Edit a request. No notice should go out if it hasn't been announced yet''' make_interim_test_data() meeting = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', group__acronym='mars')).filter(current_status='apprw').first().meeting group = meeting.session_set.first().group url = urlreverse('ietf.meeting.views.interim_request_edit', kwargs={'number': meeting.number}) # test unauthorized access self.client.login(username="ameschairman", password="ameschairman+password") r = self.client.get(url) self.assertEqual(r.status_code, 403) # test authorized use login_testing_unauthorized(self, "secretary", url) r = self.client.get(url) self.assertEqual(r.status_code, 200) # post changes length_before = len(outbox) form_initial = r.context['form'].initial formset_initial = r.context['formset'].forms[0].initial data = {'group':group.pk, 'meeting_type':'single', 'session_set-0-id':meeting.session_set.first().id, 'session_set-0-date':formset_initial['date'].strftime('%Y-%m-%d'), 'session_set-0-time':'12:34', 'session_set-0-requested_duration': '00:30', 'session_set-0-remote_instructions':formset_initial['remote_instructions'], #'session_set-0-agenda':formset_initial['agenda'], 'session_set-0-agenda_note':formset_initial['agenda_note'], 'session_set-TOTAL_FORMS':1, 'session_set-INITIAL_FORMS':1} data.update(form_initial) r = self.client.post(url, data) self.assertRedirects(r, urlreverse('ietf.meeting.views.interim_request_details', kwargs={'number': meeting.number})) self.assertEqual(len(outbox),length_before) session = meeting.session_set.first() timeslot = session.official_timeslotassignment().timeslot self.assertEqual( timeslot.time, meeting.tz().localize(datetime.datetime.combine(formset_initial['date'], datetime.time(12, 34))), ) def test_interim_request_edit(self): '''Edit request. Send notice of change''' make_interim_test_data() meeting = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', group__acronym='mars')).filter(current_status='sched').first().meeting group = meeting.session_set.first().group url = urlreverse('ietf.meeting.views.interim_request_edit', kwargs={'number': meeting.number}) # test unauthorized access self.client.login(username="ameschairman", password="ameschairman+password") r = self.client.get(url) self.assertEqual(r.status_code, 403) # test authorized use login_testing_unauthorized(self, "secretary", url) r = self.client.get(url) self.assertEqual(r.status_code, 200) # post changes length_before = len(outbox) form_initial = r.context['form'].initial formset_initial = r.context['formset'].forms[0].initial new_duration = formset_initial['requested_duration'] + datetime.timedelta(hours=1) data = {'group':group.pk, 'meeting_type':'single', 'session_set-0-id':meeting.session_set.first().id, 'session_set-0-date':formset_initial['date'].strftime('%Y-%m-%d'), 'session_set-0-time': '12:34', 'session_set-0-requested_duration':self.strfdelta(new_duration, '{hours}:{minutes}'), 'session_set-0-remote_instructions':formset_initial['remote_instructions'], #'session_set-0-agenda':formset_initial['agenda'], 'session_set-0-agenda_note':formset_initial['agenda_note'], 'session_set-TOTAL_FORMS':1, 'session_set-INITIAL_FORMS':1} data.update(form_initial) r = self.client.post(url, data) self.assertRedirects(r, urlreverse('ietf.meeting.views.interim_request_details', kwargs={'number': meeting.number})) self.assertEqual(len(outbox),length_before+1) self.assertIn('CHANGED', outbox[-1]['Subject']) session = meeting.session_set.first() timeslot = session.official_timeslotassignment().timeslot self.assertEqual( timeslot.time, meeting.tz().localize(datetime.datetime.combine(formset_initial['date'], datetime.time(12, 34))), ) self.assertEqual(timeslot.duration,new_duration) def strfdelta(self, tdelta, fmt): d = {"days": tdelta.days} d["hours"], rem = divmod(tdelta.seconds, 3600) d["minutes"], d["seconds"] = divmod(rem, 60) return fmt.format(**d) def test_interim_request_edit_agenda_updates_doc(self): """Updating the agenda through the request edit form should update the doc correctly""" make_interim_test_data() meeting = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', group__acronym='mars')).filter(current_status='sched').first().meeting group = meeting.session_set.first().group url = urlreverse('ietf.meeting.views.interim_request_edit', kwargs={'number': meeting.number}) session = meeting.session_set.first() agenda_doc = session.agenda() rev_before = agenda_doc.rev uploaded_filename_before = agenda_doc.uploaded_filename self.client.login(username='secretary', password='secretary+password') r = self.client.get(url) form_initial = r.context['form'].initial formset_initial = r.context['formset'].forms[0].initial data = { 'group': group.pk, 'meeting_type': 'single', 'session_set-0-id': session.id, 'session_set-0-date': formset_initial['date'].strftime('%Y-%m-%d'), 'session_set-0-time': formset_initial['time'].strftime('%H:%M'), 'session_set-0-requested_duration': '00:30', 'session_set-0-remote_instructions': formset_initial['remote_instructions'], 'session_set-0-agenda': 'modified agenda contents', 'session_set-0-agenda_note': formset_initial['agenda_note'], 'session_set-TOTAL_FORMS': 1, 'session_set-INITIAL_FORMS': 1, } data.update(form_initial) r = self.client.post(url, data) self.assertRedirects(r, urlreverse('ietf.meeting.views.interim_request_details', kwargs={'number': meeting.number})) session = Session.objects.get(pk=session.pk) # refresh agenda_doc = session.agenda() self.assertEqual(agenda_doc.rev, f'{int(rev_before) + 1:02}', 'Revision of agenda should increase') self.assertNotEqual(agenda_doc.uploaded_filename, uploaded_filename_before, 'Uploaded filename should be updated') with (Path(agenda_doc.get_file_path()) / agenda_doc.uploaded_filename).open() as f: self.assertEqual(f.read(), 'modified agenda contents', 'New agenda contents should be saved') def test_interim_request_details_permissions(self): make_interim_test_data() meeting = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', group__acronym='mars')).filter(current_status='apprw').first().meeting url = urlreverse('ietf.meeting.views.interim_request_details',kwargs={'number':meeting.number}) # unprivileged user login_testing_unauthorized(self,"plain",url) r = self.client.get(url) self.assertEqual(r.status_code, 403) def test_send_interim_approval_request(self): make_interim_test_data() meeting = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', group__acronym='mars')).filter(current_status='apprw').first().meeting length_before = len(outbox) send_interim_approval_request(meetings=[meeting]) self.assertEqual(len(outbox),length_before+1) self.assertIn('New Interim Meeting Request', outbox[-1]['Subject']) def test_send_interim_meeting_cancellation_notice(self): make_interim_test_data() meeting = Session.objects.with_current_status( ).filter( meeting__type='interim', group__acronym='mars', current_status='sched', ).first().meeting length_before = len(outbox) send_interim_meeting_cancellation_notice(meeting) self.assertEqual(len(outbox),length_before + 1) self.assertIn('Interim Meeting Cancelled', outbox[-1]['Subject']) def test_send_interim_session_cancellation_notice(self): make_interim_test_data() session = Session.objects.with_current_status( ).filter( meeting__type='interim', group__acronym='mars', current_status='sched', ).first() length_before = len(outbox) send_interim_session_cancellation_notice(session) self.assertEqual(len(outbox), length_before + 1) self.assertIn('session cancelled', outbox[-1]['Subject']) def test_send_interim_minutes_reminder(self): make_meeting_test_data() group = Group.objects.get(acronym='mars') date = timezone.now() - datetime.timedelta(days=10) meeting = make_interim_meeting(group=group, date=date, status='sched') length_before = len(outbox) send_interim_minutes_reminder(meeting=meeting) self.assertEqual(len(outbox),length_before+1) self.assertIn('Action Required: Minutes', outbox[-1]['Subject']) def test_group_ical(self): make_interim_test_data() meeting = Meeting.objects.filter(type='interim', session__group__acronym='mars').first() s1 = Session.objects.filter(meeting=meeting, group__acronym="mars").first() self.assertGreater(len(s1.remote_instructions), 0, 'Expected remote_instructions to be set') a1 = s1.official_timeslotassignment() t1 = a1.timeslot # Create an extra session t2 = TimeSlotFactory.create( meeting=meeting, time=meeting.tz().localize( datetime.datetime.combine(meeting.date, datetime.time(11, 30)) )) s2 = SessionFactory.create(meeting=meeting, group=s1.group, add_to_schedule=False) SchedTimeSessAssignment.objects.create(timeslot=t2, session=s2, schedule=meeting.schedule) # url = urlreverse('ietf.meeting.views.agenda_ical', kwargs={'num':meeting.number, 'acronym':s1.group.acronym, }) r = self.client.get(url) self.assertEqual(r.get('Content-Type'), "text/calendar") self.assertContains(r, 'BEGIN:VEVENT') self.assertEqual(r.content.count(b'UID'), 2) self.assertContains(r, 'SUMMARY:mars - Martian Special Interest Group') self.assertContains(r, t1.local_start_time().strftime('%Y%m%dT%H%M%S')) self.assertContains(r, s1.remote_instructions) self.assertContains(r, t2.local_start_time().strftime('%Y%m%dT%H%M%S')) self.assertContains(r, 'END:VEVENT') # url = urlreverse('ietf.meeting.views.agenda_ical', kwargs={'num':meeting.number, 'session_id':s1.id, }) r = self.client.get(url) self.assertEqual(r.get('Content-Type'), "text/calendar") self.assertContains(r, 'BEGIN:VEVENT') self.assertEqual(r.content.count(b'UID'), 1) self.assertContains(r, 'SUMMARY:mars - Martian Special Interest Group') self.assertContains(r, t1.time.strftime('%Y%m%dT%H%M%S')) self.assertContains(r, s1.remote_instructions) self.assertNotContains(r, t2.time.strftime('%Y%m%dT%H%M%S')) self.assertContains(r, 'END:VEVENT') class IphoneAppJsonTests(TestCase): def test_iphone_app_json_interim(self): make_interim_test_data() meeting = Meeting.objects.filter(type_id='interim').order_by('id').last() url = urlreverse('ietf.meeting.views.agenda_json',kwargs={'num':meeting.number}) r = self.client.get(url) self.assertEqual(r.status_code,200) data = r.json() self.assertIn(meeting.number, data.keys()) jsessions = [ s for s in data[meeting.number] if s['objtype'] == 'session' ] msessions = meeting.session_set.exclude(type__in=['lead','offagenda','break','reg']) self.assertEqual(len(jsessions), msessions.count()) for s in jsessions: self.assertTrue(msessions.filter(group__acronym=s['group']['acronym']).exists()) def test_iphone_app_json(self): make_meeting_test_data() meeting = Meeting.objects.filter(type_id='ietf').order_by('id').last() floorplan = FloorPlanFactory.create(meeting=meeting) for room in meeting.room_set.all(): room.floorplan = floorplan room.x1 = random.randint(0,100) room.y1 = random.randint(0,100) room.x2 = random.randint(0,100) room.y2 = random.randint(0,100) room.save() url = urlreverse('ietf.meeting.views.agenda_json',kwargs={'num':meeting.number}) r = self.client.get(url) self.assertEqual(r.status_code,200) data = r.json() self.assertIn(meeting.number, data.keys()) jsessions = [ s for s in data[meeting.number] if s['objtype'] == 'session' ] msessions = meeting.session_set.exclude(type__in=['lead','offagenda','break','reg']) self.assertEqual(len(jsessions), msessions.count()) for s in jsessions: self.assertTrue(msessions.filter(group__acronym=s['group']['acronym']).exists()) class FinalizeProceedingsTests(TestCase): def test_finalize_proceedings(self): make_meeting_test_data() meeting = Meeting.objects.filter(type_id='ietf').order_by('id').last() meeting.session_set.filter(group__acronym='mars').first().presentations.create(document=Document.objects.filter(type='draft').first(),rev=None) url = urlreverse('ietf.meeting.views.finalize_proceedings',kwargs={'num':meeting.number}) login_testing_unauthorized(self,"secretary",url) r = self.client.get(url) self.assertEqual(r.status_code, 200) self.assertEqual(meeting.proceedings_final,False) self.assertEqual(meeting.session_set.filter(group__acronym="mars").first().presentations.filter(document__type="draft").first().rev,None) r = self.client.post(url,{'finalize':1}) self.assertEqual(r.status_code, 302) meeting = Meeting.objects.get(pk=meeting.pk) self.assertEqual(meeting.proceedings_final,True) self.assertEqual(meeting.session_set.filter(group__acronym="mars").first().presentations.filter(document__type="draft").first().rev,'00') @patch("ietf.meeting.utils.generate_bluesheet") def test_bluesheet_generation(self, mock): meeting = MeetingFactory(type_id="ietf", number="107") # number where generate_bluesheets should not be called SessionFactory.create_batch(5, meeting=meeting) url = urlreverse("ietf.meeting.views.finalize_proceedings", kwargs={"num": meeting.number}) self.client.login(username="secretary", password="secretary+password") r = self.client.get(url) self.assertEqual(r.status_code, 200) self.assertFalse(mock.called) r = self.client.post(url,{'finalize': 1}) self.assertEqual(r.status_code, 302) self.assertFalse(mock.called) meeting = MeetingFactory(type_id="ietf", number="108") # number where generate_bluesheets should be called SessionFactory.create_batch(5, meeting=meeting) url = urlreverse("ietf.meeting.views.finalize_proceedings", kwargs={"num": meeting.number}) r = self.client.get(url) self.assertEqual(r.status_code, 200) self.assertFalse(mock.called) r = self.client.post(url,{'finalize': 1}) self.assertEqual(r.status_code, 302) self.assertTrue(mock.called) self.assertCountEqual( [call_args[0][1] for call_args in mock.call_args_list], [sess for sess in meeting.session_set.all()], ) class MaterialsTests(TestCase): settings_temp_path_overrides = TestCase.settings_temp_path_overrides + [ 'AGENDA_PATH', 'SLIDE_STAGING_PATH' ] def setUp(self): super().setUp() self.materials_dir = self.tempdir('materials') if not os.path.exists(self.materials_dir): os.mkdir(self.materials_dir) def tearDown(self): shutil.rmtree(self.materials_dir) super().tearDown() def crawl_materials(self, url, top): seen = set() def follow(url): seen.add(url) r = self.client.get(url) self.assertEqual(r.status_code, 200) if not ('.' in url and url.rsplit('.', 1)[1] in ['tgz', 'pdf', ]): if r.content: page = unicontent(r) soup = BeautifulSoup(page, 'html.parser') for a in soup('a'): href = a.get('href') path = urlparse(href).path if (path and path not in seen and path.startswith(top)): follow(path) follow(url) def test_upload_bluesheets(self): session = SessionFactory(meeting__type_id='ietf') url = urlreverse('ietf.meeting.views.upload_session_bluesheets',kwargs={'num':session.meeting.number,'session_id':session.id}) login_testing_unauthorized(self,"secretary",url) r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertIn('Upload', str(q("title"))) self.assertFalse(session.presentations.exists()) test_file = StringIO('%PDF-1.4\n%âãÏÓ\nthis is some text for a test') test_file.name = "not_really.pdf" r = self.client.post(url,dict(file=test_file)) self.assertEqual(r.status_code, 302) bs_doc = session.presentations.filter(document__type_id='bluesheets').first().document self.assertEqual(bs_doc.rev,'00') r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertIn('Revise', str(q("title"))) test_file = StringIO('%PDF-1.4\n%âãÏÓ\nthis is some different text for a test') test_file.name = "also_not_really.pdf" r = self.client.post(url,dict(file=test_file)) self.assertEqual(r.status_code, 302) bs_doc = Document.objects.get(pk=bs_doc.pk) self.assertEqual(bs_doc.rev,'01') def test_upload_bluesheets_chair_access(self): make_meeting_test_data() mars = Group.objects.get(acronym='mars') session=SessionFactory(meeting__type_id='ietf',group=mars) url = urlreverse('ietf.meeting.views.upload_session_bluesheets',kwargs={'num':session.meeting.number,'session_id':session.id}) self.client.login(username="marschairman", password="marschairman+password") r = self.client.get(url) self.assertEqual(r.status_code, 403) def test_upload_bluesheets_interim(self): session=SessionFactory(meeting__type_id='interim') url = urlreverse('ietf.meeting.views.upload_session_bluesheets',kwargs={'num':session.meeting.number,'session_id':session.id}) login_testing_unauthorized(self,"secretary",url) r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertIn('Upload', str(q("title"))) self.assertFalse(session.presentations.exists()) test_file = StringIO('%PDF-1.4\n%âãÏÓ\nthis is some text for a test') test_file.name = "not_really.pdf" r = self.client.post(url,dict(file=test_file)) self.assertEqual(r.status_code, 302) bs_doc = session.presentations.filter(document__type_id='bluesheets').first().document self.assertEqual(bs_doc.rev,'00') def test_upload_bluesheets_interim_chair_access(self): make_meeting_test_data() mars = Group.objects.get(acronym='mars') session=SessionFactory(meeting__type_id='interim',group=mars, meeting__date = date_today()) url = urlreverse('ietf.meeting.views.upload_session_bluesheets',kwargs={'num':session.meeting.number,'session_id':session.id}) self.client.login(username="marschairman", password="marschairman+password") r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertIn('Upload', str(q("title"))) def test_upload_minutes_agenda(self): for doctype in ('minutes','agenda'): session = SessionFactory(meeting__type_id='ietf') if doctype == 'minutes': url = urlreverse('ietf.meeting.views.upload_session_minutes',kwargs={'num':session.meeting.number,'session_id':session.id}) else: url = urlreverse('ietf.meeting.views.upload_session_agenda',kwargs={'num':session.meeting.number,'session_id':session.id}) self.client.logout() login_testing_unauthorized(self,"secretary",url) r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertIn('Upload', str(q("Title"))) self.assertFalse(session.presentations.exists()) self.assertFalse(q('form input[type="checkbox"]')) session2 = SessionFactory(meeting=session.meeting,group=session.group) r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertTrue(q('form input[type="checkbox"]')) # test not submitting a file r = self.client.post(url, dict(submission_method="upload")) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertTrue(q("form .is-invalid")) test_file = BytesIO(b'this is some text for a test') test_file.name = "not_really.json" r = self.client.post(url,dict(submission_method="upload",file=test_file)) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertTrue(q('form .is-invalid')) test_file = BytesIO(b'this is some text for a test'*1510000) test_file.name = "not_really.pdf" r = self.client.post(url,dict(submission_method="upload",file=test_file)) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertTrue(q('form .is-invalid')) test_file = BytesIO(b'') test_file.name = "not_really.html" r = self.client.post(url,dict(submission_method="upload",file=test_file)) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertTrue(q('form .is-invalid')) # Test html sanitization test_file = BytesIO(b'Title

    Title

    Some text
    ') test_file.name = "some.html" r = self.client.post(url,dict(submission_method="upload",file=test_file)) self.assertEqual(r.status_code, 302) doc = session.presentations.filter(document__type_id=doctype).first().document self.assertEqual(doc.rev,'00') text = doc.text() self.assertIn('Some text', text) self.assertNotIn('
    ', text) # txt upload test_file = BytesIO(b'This is some text for a test, with the word\nvirtual at the beginning of a line.') test_file.name = "some.txt" r = self.client.post(url,dict(submission_method="upload",file=test_file,apply_to_all=False)) self.assertEqual(r.status_code, 302) doc = session.presentations.filter(document__type_id=doctype).first().document self.assertEqual(doc.rev,'01') self.assertFalse(session2.presentations.filter(document__type_id=doctype)) r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertIn('Revise', str(q("Title"))) test_file = BytesIO(b'this is some different text for a test') test_file.name = "also_some.txt" r = self.client.post(url,dict(submission_method="upload",file=test_file,apply_to_all=True)) self.assertEqual(r.status_code, 302) doc = Document.objects.get(pk=doc.pk) self.assertEqual(doc.rev,'02') self.assertTrue(session2.presentations.filter(document__type_id=doctype)) # Test bad encoding test_file = BytesIO('

    Title

    Some\x93text
    '.encode('latin1')) test_file.name = "some.html" r = self.client.post(url,dict(submission_method="upload",file=test_file)) self.assertContains(r, 'Could not identify the file encoding') doc = Document.objects.get(pk=doc.pk) self.assertEqual(doc.rev,'02') # Verify that we don't have dead links url = urlreverse('ietf.meeting.views.session_details', kwargs={'num':session.meeting.number, 'acronym': session.group.acronym}) top = '/meeting/%s/' % session.meeting.number self.requests_mock.get(f'{session.notes_url()}/download', text='markdown notes') self.requests_mock.get(f'{session.notes_url()}/info', text=json.dumps({'title': 'title', 'updatetime': '2021-12-01T17:11:00z'})) self.crawl_materials(url=url, top=top) def test_upload_minutes_agenda_unscheduled(self): for doctype in ('minutes','agenda'): session = SessionFactory(meeting__type_id='ietf', add_to_schedule=False) if doctype == 'minutes': url = urlreverse('ietf.meeting.views.upload_session_minutes',kwargs={'num':session.meeting.number,'session_id':session.id}) else: url = urlreverse('ietf.meeting.views.upload_session_agenda',kwargs={'num':session.meeting.number,'session_id':session.id}) self.client.logout() login_testing_unauthorized(self,"secretary",url) r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertIn('Upload', str(q("Title"))) self.assertFalse(session.presentations.exists()) self.assertFalse(q('form input[type="checkbox"]')) test_file = BytesIO(b'this is some text for a test') test_file.name = "not_really.txt" r = self.client.post(url,dict(submission_method="upload",file=test_file,apply_to_all=False)) self.assertEqual(r.status_code, 410) @override_settings(MEETING_MATERIALS_SERVE_LOCALLY=True) def test_upload_minutes_agenda_interim(self): session=SessionFactory(meeting__type_id='interim') for doctype in ('minutes','agenda'): if doctype=='minutes': url = urlreverse('ietf.meeting.views.upload_session_minutes',kwargs={'num':session.meeting.number,'session_id':session.id}) else: url = urlreverse('ietf.meeting.views.upload_session_agenda',kwargs={'num':session.meeting.number,'session_id':session.id}) self.client.logout() login_testing_unauthorized(self,"secretary",url) r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertIn('Upload', str(q("title"))) self.assertFalse(session.presentations.filter(document__type_id=doctype)) test_file = BytesIO(b'this is some text for a test') test_file.name = "not_really.txt" r = self.client.post(url,dict(submission_method="upload",file=test_file)) self.assertEqual(r.status_code, 302) doc = session.presentations.filter(document__type_id=doctype).first().document self.assertEqual(doc.rev,'00') # Verify that we don't have dead links url = urlreverse('ietf.meeting.views.session_details', kwargs={'num':session.meeting.number, 'acronym': session.group.acronym}) top = '/meeting/%s/' % session.meeting.number self.requests_mock.get(f'{session.notes_url()}/download', text='markdown notes') self.requests_mock.get(f'{session.notes_url()}/info', text=json.dumps({'title': 'title', 'updatetime': '2021-12-01T17:11:00z'})) self.crawl_materials(url=url, top=top) @override_settings(MEETING_MATERIALS_SERVE_LOCALLY=True) def test_upload_narrativeminutes(self): for type_id in ["interim","ietf"]: session=SessionFactory(meeting__type_id=type_id,group__acronym='iesg') doctype='narrativeminutes' url = urlreverse('ietf.meeting.views.upload_session_narrativeminutes',kwargs={'num':session.meeting.number,'session_id':session.id}) self.client.logout() login_testing_unauthorized(self,"secretary",url) r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertIn('Upload', str(q("title"))) self.assertFalse(session.presentations.filter(document__type_id=doctype)) test_file = BytesIO(b'this is some text for a test') test_file.name = "not_really.txt" r = self.client.post(url,dict(submission_method="upload",file=test_file)) self.assertEqual(r.status_code, 302) doc = session.presentations.filter(document__type_id=doctype).first().document self.assertEqual(doc.rev,'00') # Verify that we don't have dead links url = urlreverse('ietf.meeting.views.session_details', kwargs={'num':session.meeting.number, 'acronym': session.group.acronym}) top = '/meeting/%s/' % session.meeting.number self.requests_mock.get(f'{session.notes_url()}/download', text='markdown notes') self.requests_mock.get(f'{session.notes_url()}/info', text=json.dumps({'title': 'title', 'updatetime': '2021-12-01T17:11:00z'})) self.crawl_materials(url=url, top=top) def test_enter_agenda(self): session = SessionFactory(meeting__type_id='ietf') url = urlreverse('ietf.meeting.views.upload_session_agenda',kwargs={'num':session.meeting.number,'session_id':session.id}) redirect_url = urlreverse('ietf.meeting.views.session_details', kwargs={'num':session.meeting.number,'acronym':session.group.acronym}) login_testing_unauthorized(self,"secretary",url) r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertIn('Upload', str(q("Title"))) self.assertFalse(session.presentations.exists()) test_text = 'Enter agenda from scratch' r = self.client.post(url,dict(submission_method="enter",content=test_text)) self.assertRedirects(r, redirect_url) doc = session.presentations.filter(document__type_id='agenda').first().document self.assertEqual(doc.rev,'00') r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertIn('Revise', str(q("Title"))) test_file = BytesIO(b'Upload after enter') test_file.name = "some.txt" r = self.client.post(url,dict(submission_method="upload",file=test_file)) self.assertRedirects(r, redirect_url) doc = Document.objects.get(pk=doc.pk) self.assertEqual(doc.rev,'01') r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertIn('Revise', str(q("Title"))) test_text = 'Enter after upload' r = self.client.post(url,dict(submission_method="enter",content=test_text)) self.assertRedirects(r, redirect_url) doc = Document.objects.get(pk=doc.pk) self.assertEqual(doc.rev,'02') @override_settings(MEETECHO_API_CONFIG="fake settings") # enough to trigger API calls @patch("ietf.meeting.views.SlidesManager") def test_upload_slides(self, mock_slides_manager_cls): session1 = SessionFactory(meeting__type_id='ietf') session2 = SessionFactory(meeting=session1.meeting,group=session1.group) url = urlreverse('ietf.meeting.views.upload_session_slides',kwargs={'num':session1.meeting.number,'session_id':session1.id}) login_testing_unauthorized(self,"secretary",url) r = self.client.get(url) self.assertEqual(r.status_code, 200) self.assertFalse(mock_slides_manager_cls.called) q = PyQuery(r.content) self.assertIn('Upload', str(q("title"))) self.assertFalse(session1.presentations.filter(document__type_id='slides')) test_file = BytesIO(b'this is not really a slide') test_file.name = 'not_really.txt' r = self.client.post(url,dict(file=test_file,title='a test slide file',apply_to_all=True,approved=True)) self.assertEqual(r.status_code, 302) self.assertEqual(session1.presentations.count(),1) self.assertEqual(session2.presentations.count(),1) sp = session2.presentations.first() self.assertEqual(sp.document.name, 'slides-%s-%s-a-test-slide-file' % (session1.meeting.number,session1.group.acronym ) ) self.assertEqual(sp.order,1) self.assertEqual(mock_slides_manager_cls.call_count, 1) self.assertEqual(mock_slides_manager_cls.call_args, call(api_config="fake settings")) self.assertEqual(mock_slides_manager_cls.return_value.add.call_count, 2) # don't care which order they were called in, just that both sessions were updated self.assertCountEqual( mock_slides_manager_cls.return_value.add.call_args_list, [ call(session=session1, slides=sp.document, order=1), call(session=session2, slides=sp.document, order=1), ], ) mock_slides_manager_cls.reset_mock() url = urlreverse('ietf.meeting.views.upload_session_slides',kwargs={'num':session2.meeting.number,'session_id':session2.id}) test_file = BytesIO(b'some other thing still not slidelike') test_file.name = 'also_not_really.txt' r = self.client.post(url,dict(file=test_file,title='a different slide file',apply_to_all=False,approved=True)) self.assertEqual(r.status_code, 302) self.assertEqual(session1.presentations.count(),1) self.assertEqual(session2.presentations.count(),2) sp = session2.presentations.get(document__name__endswith='-a-different-slide-file') self.assertEqual(sp.order,2) self.assertEqual(sp.rev,'00') self.assertEqual(sp.document.rev,'00') self.assertEqual(mock_slides_manager_cls.call_count, 1) self.assertEqual(mock_slides_manager_cls.call_args, call(api_config="fake settings")) self.assertEqual(mock_slides_manager_cls.return_value.add.call_count, 1) self.assertEqual( mock_slides_manager_cls.return_value.add.call_args, call(session=session2, slides=sp.document, order=2), ) mock_slides_manager_cls.reset_mock() url = urlreverse('ietf.meeting.views.upload_session_slides',kwargs={'num':session2.meeting.number,'session_id':session2.id,'name':session2.presentations.get(order=2).document.name}) r = self.client.get(url) self.assertTrue(r.status_code, 200) q = PyQuery(r.content) self.assertIn('Revise', str(q("title"))) test_file = BytesIO(b'new content for the second slide deck') test_file.name = 'doesnotmatter.txt' r = self.client.post(url,dict(file=test_file,title='rename the presentation',apply_to_all=False, approved=True)) self.assertEqual(r.status_code, 302) self.assertEqual(session1.presentations.count(),1) self.assertEqual(session2.presentations.count(),2) replacement_sp = session2.presentations.get(order=2) self.assertEqual(replacement_sp.rev,'01') self.assertEqual(replacement_sp.document.rev,'01') self.assertEqual(mock_slides_manager_cls.call_count, 1) self.assertEqual(mock_slides_manager_cls.call_args, call(api_config="fake settings")) self.assertEqual(mock_slides_manager_cls.return_value.revise.call_count, 1) self.assertEqual( mock_slides_manager_cls.return_value.revise.call_args, call(session=session2, slides=sp.document), ) def test_upload_slide_title_bad_unicode(self): session1 = SessionFactory(meeting__type_id='ietf') url = urlreverse('ietf.meeting.views.upload_session_slides',kwargs={'num':session1.meeting.number,'session_id':session1.id}) login_testing_unauthorized(self,"secretary",url) r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertIn('Upload', str(q("title"))) self.assertFalse(session1.presentations.filter(document__type_id='slides')) test_file = BytesIO(b'this is not really a slide') test_file.name = 'not_really.txt' r = self.client.post(url,dict(file=test_file,title='title with bad character \U0001fabc ')) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertTrue(q('form .is-invalid')) self.assertIn("Unicode BMP", q('form .is-invalid div').text()) @override_settings(MEETECHO_API_CONFIG="fake settings") # enough to trigger API calls @patch("ietf.meeting.views.SlidesManager") def test_remove_sessionpresentation(self, mock_slides_manager_cls): session = SessionFactory(meeting__type_id='ietf') agenda = DocumentFactory(type_id='agenda') doc = DocumentFactory(type_id='slides') session.presentations.create(document=agenda) session.presentations.create(document=doc) url = urlreverse('ietf.meeting.views.remove_sessionpresentation',kwargs={'num':session.meeting.number,'session_id':session.id,'name':'no-such-doc'}) response = self.client.get(url) self.assertEqual(response.status_code, 404) self.assertFalse(mock_slides_manager_cls.called) url = urlreverse('ietf.meeting.views.remove_sessionpresentation',kwargs={'num':session.meeting.number,'session_id':0,'name':doc.name}) response = self.client.get(url) self.assertEqual(response.status_code, 404) self.assertFalse(mock_slides_manager_cls.called) url = urlreverse('ietf.meeting.views.remove_sessionpresentation',kwargs={'num':session.meeting.number,'session_id':session.id,'name':doc.name}) login_testing_unauthorized(self,"secretary",url) response = self.client.get(url) self.assertEqual(response.status_code, 200) self.assertFalse(mock_slides_manager_cls.called) # Removing slides should remove the materials and call MeetechoAPI self.assertEqual(2, session.presentations.count()) response = self.client.post(url,{'remove_session':''}) self.assertEqual(response.status_code, 302) self.assertEqual(1, session.presentations.count()) self.assertEqual(2, doc.docevent_set.count()) self.assertEqual(mock_slides_manager_cls.call_count, 1) self.assertEqual(mock_slides_manager_cls.call_args, call(api_config="fake settings")) self.assertEqual(mock_slides_manager_cls.return_value.delete.call_count, 1) self.assertEqual( mock_slides_manager_cls.return_value.delete.call_args, call(session=session, slides=doc), ) mock_slides_manager_cls.reset_mock() # Removing non-slides should only remove the materials url = urlreverse( "ietf.meeting.views.remove_sessionpresentation", kwargs={ "num": session.meeting.number, "session_id": session.id, "name": agenda.name, }, ) response = self.client.post(url, {"remove_session" : ""}) self.assertEqual(response.status_code, 302) self.assertEqual(0, session.presentations.count()) self.assertEqual(2, agenda.docevent_set.count()) self.assertFalse(mock_slides_manager_cls.called) def test_propose_session_slides(self): for type_id in ['ietf','interim']: session = SessionFactory(meeting__type_id=type_id) chair = RoleFactory(group=session.group,name_id='chair').person session.meeting.importantdate_set.create(name_id='revsub',date=date_today() + datetime.timedelta(days=20)) newperson = PersonFactory() session_overview_url = urlreverse('ietf.meeting.views.session_details',kwargs={'num':session.meeting.number,'acronym':session.group.acronym}) upload_url = urlreverse('ietf.meeting.views.upload_session_slides', kwargs={'session_id':session.pk, 'num': session.meeting.number}) r = self.client.get(session_overview_url) self.assertEqual(r.status_code,200) q = PyQuery(r.content) self.assertFalse(q('.uploadslides')) self.assertFalse(q('.proposeslides')) self.client.login(username=newperson.user.username,password=newperson.user.username+"+password") r = self.client.get(session_overview_url) self.assertEqual(r.status_code,200) q = PyQuery(r.content) self.assertTrue(q('.proposeslides')) self.client.logout() login_testing_unauthorized(self,newperson.user.username,upload_url) r = self.client.get(upload_url) self.assertEqual(r.status_code,200) test_file = BytesIO(b'this is not really a slide') test_file.name = 'not_really.txt' empty_outbox() r = self.client.post(upload_url,dict(file=test_file,title='a test slide file',apply_to_all=True,approved=False)) self.assertEqual(r.status_code, 302) session = Session.objects.get(pk=session.pk) self.assertEqual(session.slidesubmission_set.count(),1) self.assertEqual(len(outbox),1) r = self.client.get(session_overview_url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertEqual(len(q('.proposedslidelist p')), 1) SlideSubmissionFactory(session = session) self.client.logout() self.client.login(username=chair.user.username, password=chair.user.username+"+password") r = self.client.get(session_overview_url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertEqual(len(q('.proposedslidelist p')), 2) self.client.logout() login_testing_unauthorized(self,chair.user.username,upload_url) r = self.client.get(upload_url) self.assertEqual(r.status_code,200) test_file = BytesIO(b'this is not really a slide either') test_file.name = 'again_not_really.txt' empty_outbox() r = self.client.post(upload_url,dict(file=test_file,title='a selfapproved test slide file',apply_to_all=True,approved=True)) self.assertEqual(r.status_code, 302) self.assertEqual(len(outbox),0) self.assertEqual(session.slidesubmission_set.count(),2) self.client.logout() self.client.login(username=chair.user.username, password=chair.user.username+"+password") r = self.client.get(session_overview_url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertEqual(len(q('.uploadslidelist p')), 0) self.client.logout() def test_disapprove_proposed_slides(self): submission = SlideSubmissionFactory() submission.session.meeting.importantdate_set.create(name_id='revsub',date=date_today() + datetime.timedelta(days=20)) self.assertEqual(SlideSubmission.objects.filter(status__slug = 'pending').count(), 1) chair = RoleFactory(group=submission.session.group,name_id='chair').person url = urlreverse('ietf.meeting.views.approve_proposed_slides', kwargs={'slidesubmission_id':submission.pk,'num':submission.session.meeting.number}) login_testing_unauthorized(self, chair.user.username, url) r = self.client.get(url) self.assertEqual(r.status_code,200) r = self.client.post(url,dict(title='some title',disapprove="disapprove")) self.assertEqual(r.status_code,302) self.assertEqual(SlideSubmission.objects.filter(status__slug = 'rejected').count(), 1) self.assertEqual(SlideSubmission.objects.filter(status__slug = 'pending').count(), 0) r = self.client.get(url) self.assertEqual(r.status_code, 200) self.assertRegex(r.content.decode(), r"These\s+slides\s+have\s+already\s+been\s+rejected") @override_settings(MEETECHO_API_CONFIG="fake settings") # enough to trigger API calls @patch("ietf.meeting.views.SlidesManager") def test_approve_proposed_slides(self, mock_slides_manager_cls): submission = SlideSubmissionFactory() session = submission.session session.meeting.importantdate_set.create(name_id='revsub',date=date_today() + datetime.timedelta(days=20)) chair = RoleFactory(group=submission.session.group,name_id='chair').person url = urlreverse('ietf.meeting.views.approve_proposed_slides', kwargs={'slidesubmission_id':submission.pk,'num':submission.session.meeting.number}) login_testing_unauthorized(self, chair.user.username, url) self.assertEqual(submission.status_id, 'pending') self.assertIsNone(submission.doc) r = self.client.get(url) self.assertEqual(r.status_code,200) empty_outbox() r = self.client.post(url,dict(title='different title',approve='approve')) self.assertEqual(r.status_code,302) self.assertEqual(SlideSubmission.objects.filter(status__slug = 'pending').count(), 0) self.assertEqual(SlideSubmission.objects.filter(status__slug = 'approved').count(), 1) submission.refresh_from_db() self.assertEqual(submission.status_id, 'approved') self.assertIsNotNone(submission.doc) self.assertEqual(session.presentations.count(),1) self.assertEqual(session.presentations.first().document.title,'different title') self.assertEqual(mock_slides_manager_cls.call_count, 1) self.assertEqual(mock_slides_manager_cls.call_args, call(api_config="fake settings")) self.assertEqual(mock_slides_manager_cls.return_value.add.call_count, 1) self.assertEqual( mock_slides_manager_cls.return_value.add.call_args, call(session=session, slides=submission.doc, order=1), ) mock_slides_manager_cls.reset_mock() r = self.client.get(url) self.assertEqual(r.status_code, 200) self.assertRegex(r.content.decode(), r"These\s+slides\s+have\s+already\s+been\s+approved") self.assertFalse(mock_slides_manager_cls.called) self.assertEqual(len(outbox), 1) self.assertIn(submission.submitter.email_address(), outbox[0]['To']) self.assertIn('Slides approved', outbox[0]['Subject']) @override_settings(MEETECHO_API_CONFIG="fake settings") # enough to trigger API calls @patch("ietf.meeting.views.SlidesManager") def test_approve_proposed_slides_multisession_apply_one(self, mock_slides_manager_cls): submission = SlideSubmissionFactory(session__meeting__type_id='ietf') session1 = submission.session session2 = SessionFactory(group=submission.session.group, meeting=submission.session.meeting) submission.session.meeting.importantdate_set.create(name_id='revsub',date=date_today() + datetime.timedelta(days=20)) chair = RoleFactory(group=submission.session.group,name_id='chair').person url = urlreverse('ietf.meeting.views.approve_proposed_slides', kwargs={'slidesubmission_id':submission.pk,'num':submission.session.meeting.number}) login_testing_unauthorized(self, chair.user.username, url) r = self.client.get(url) self.assertEqual(r.status_code,200) q = PyQuery(r.content) self.assertTrue(q('#id_apply_to_all')) r = self.client.post(url,dict(title='yet another title',approve='approve')) submission.refresh_from_db() self.assertIsNotNone(submission.doc) self.assertEqual(r.status_code,302) self.assertEqual(session1.presentations.count(),1) self.assertEqual(session2.presentations.count(),0) self.assertEqual(mock_slides_manager_cls.call_count, 1) self.assertEqual(mock_slides_manager_cls.call_args, call(api_config="fake settings")) self.assertEqual(mock_slides_manager_cls.return_value.add.call_count, 1) self.assertEqual( mock_slides_manager_cls.return_value.add.call_args, call(session=session1, slides=submission.doc, order=1), ) @override_settings(MEETECHO_API_CONFIG="fake settings") # enough to trigger API calls @patch("ietf.meeting.views.SlidesManager") def test_approve_proposed_slides_multisession_apply_all(self, mock_slides_manager_cls): submission = SlideSubmissionFactory(session__meeting__type_id='ietf') session1 = submission.session session2 = SessionFactory(group=submission.session.group, meeting=submission.session.meeting) submission.session.meeting.importantdate_set.create(name_id='revsub',date=date_today() + datetime.timedelta(days=20)) chair = RoleFactory(group=submission.session.group,name_id='chair').person url = urlreverse('ietf.meeting.views.approve_proposed_slides', kwargs={'slidesubmission_id':submission.pk,'num':submission.session.meeting.number}) login_testing_unauthorized(self, chair.user.username, url) r = self.client.get(url) self.assertEqual(r.status_code,200) r = self.client.post(url,dict(title='yet another title',apply_to_all=1,approve='approve')) submission.refresh_from_db() self.assertEqual(r.status_code,302) self.assertEqual(session1.presentations.count(),1) self.assertEqual(session2.presentations.count(),1) self.assertEqual(mock_slides_manager_cls.call_count, 1) self.assertEqual(mock_slides_manager_cls.call_args, call(api_config="fake settings")) self.assertEqual(mock_slides_manager_cls.return_value.add.call_count, 2) self.assertCountEqual( mock_slides_manager_cls.return_value.add.call_args_list, [ call(session=session1, slides=submission.doc, order=1), call(session=session2, slides=submission.doc, order=1), ] ) @override_settings(MEETECHO_API_CONFIG="fake settings") # enough to trigger API calls @patch("ietf.meeting.views.SlidesManager") def test_submit_and_approve_multiple_versions(self, mock_slides_manager_cls): session = SessionFactory(meeting__type_id='ietf') chair = RoleFactory(group=session.group,name_id='chair').person session.meeting.importantdate_set.create(name_id='revsub',date=date_today()+datetime.timedelta(days=20)) newperson = PersonFactory() upload_url = urlreverse('ietf.meeting.views.upload_session_slides', kwargs={'session_id':session.pk, 'num': session.meeting.number}) login_testing_unauthorized(self,newperson.user.username,upload_url) test_file = BytesIO(b'this is not really a slide') test_file.name = 'not_really.txt' r = self.client.post(upload_url,dict(file=test_file,title='a test slide file',apply_to_all=True,approved=False)) self.assertEqual(r.status_code, 302) self.client.logout() submission = SlideSubmission.objects.get(session=session) approve_url = urlreverse('ietf.meeting.views.approve_proposed_slides', kwargs={'slidesubmission_id':submission.pk,'num':submission.session.meeting.number}) login_testing_unauthorized(self, chair.user.username, approve_url) r = self.client.post(approve_url,dict(title=submission.title,approve='approve')) submission.refresh_from_db() self.assertEqual(r.status_code,302) self.client.logout() self.assertEqual(mock_slides_manager_cls.call_count, 1) self.assertEqual(mock_slides_manager_cls.call_args, call(api_config="fake settings")) self.assertEqual(mock_slides_manager_cls.return_value.add.call_count, 1) self.assertEqual( mock_slides_manager_cls.return_value.add.call_args, call(session=session, slides=submission.doc, order=1), ) mock_slides_manager_cls.reset_mock() self.assertEqual(session.presentations.first().document.rev,'00') login_testing_unauthorized(self,newperson.user.username,upload_url) test_file = BytesIO(b'this is not really a slide, but it is another version of it') test_file.name = 'not_really.txt' r = self.client.post(upload_url,dict(file=test_file,title='a test slide file',apply_to_all=True)) self.assertEqual(r.status_code, 302) test_file = BytesIO(b'this is not really a slide, but it is third version of it') test_file.name = 'not_really.txt' r = self.client.post(upload_url,dict(file=test_file,title='a test slide file',apply_to_all=True)) self.assertEqual(r.status_code, 302) self.client.logout() (first_submission, second_submission) = SlideSubmission.objects.filter(session=session, status__slug = 'pending').order_by('id') approve_url = urlreverse('ietf.meeting.views.approve_proposed_slides', kwargs={'slidesubmission_id':second_submission.pk,'num':second_submission.session.meeting.number}) login_testing_unauthorized(self, chair.user.username, approve_url) r = self.client.post(approve_url,dict(title=submission.title,approve='approve')) first_submission.refresh_from_db() second_submission.refresh_from_db() self.assertEqual(r.status_code,302) self.assertEqual(mock_slides_manager_cls.call_count, 1) self.assertEqual(mock_slides_manager_cls.call_args, call(api_config="fake settings")) self.assertEqual(mock_slides_manager_cls.return_value.add.call_count, 0) self.assertEqual(mock_slides_manager_cls.return_value.revise.call_count, 1) self.assertEqual( mock_slides_manager_cls.return_value.revise.call_args, call(session=session, slides=second_submission.doc), ) mock_slides_manager_cls.reset_mock() disapprove_url = urlreverse('ietf.meeting.views.approve_proposed_slides', kwargs={'slidesubmission_id':first_submission.pk,'num':first_submission.session.meeting.number}) r = self.client.post(disapprove_url,dict(title='some title',disapprove="disapprove")) self.assertEqual(r.status_code,302) self.client.logout() self.assertFalse(mock_slides_manager_cls.called) self.assertEqual(SlideSubmission.objects.filter(status__slug = 'pending').count(),0) self.assertEqual(SlideSubmission.objects.filter(status__slug = 'rejected').count(),1) self.assertEqual(session.presentations.first().document.rev,'01') path = os.path.join(submission.session.meeting.get_materials_path(),'slides') filename = os.path.join(path,session.presentations.first().document.name+'-01.txt') self.assertTrue(os.path.exists(filename)) fd = io.open(filename, 'r') contents = fd.read() fd.close() self.assertIn('third version', contents) @override_settings(IETF_NOTES_URL='https://notes.ietf.org/') class ImportNotesTests(TestCase): settings_temp_path_overrides = TestCase.settings_temp_path_overrides + ['AGENDA_PATH'] def setUp(self): super().setUp() self.session = SessionFactory(meeting__type_id='ietf') self.meeting = self.session.meeting def test_retrieves_note(self): """Can import and preview a note from notes.ietf.org""" url = urlreverse('ietf.meeting.views.import_session_minutes', kwargs={'num': self.meeting.number, 'session_id': self.session.pk}) self.client.login(username='secretary', password='secretary+password') with requests_mock.Mocker() as mock: mock.get(f'https://notes.ietf.org/{self.session.notes_id()}/download', text='markdown text') mock.get(f'https://notes.ietf.org/{self.session.notes_id()}/info', text=json.dumps({"title": "title", "updatetime": "2021-12-02T11:22:33z"})) r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) iframe = q('iframe#preview') self.assertEqual('

    markdown text

    ', iframe.attr('srcdoc')) markdown_text_input = q('form #id_markdown_text') self.assertEqual(markdown_text_input.val(), 'markdown text') def test_retrieves_with_broken_metadata(self): """Can import and preview a note even if it has a metadata problem""" url = urlreverse('ietf.meeting.views.import_session_minutes', kwargs={'num': self.meeting.number, 'session_id': self.session.pk}) self.client.login(username='secretary', password='secretary+password') with requests_mock.Mocker() as mock: mock.get(f'https://notes.ietf.org/{self.session.notes_id()}/download', text='markdown text') mock.get(f'https://notes.ietf.org/{self.session.notes_id()}/info', text='this is not valid json {]') r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) iframe = q('iframe#preview') self.assertEqual('

    markdown text

    ', iframe.attr('srcdoc')) markdown_text_input = q('form #id_markdown_text') self.assertEqual(markdown_text_input.val(), 'markdown text') def test_redirects_on_success(self): """Redirects to session details page after import""" url = urlreverse('ietf.meeting.views.import_session_minutes', kwargs={'num': self.meeting.number, 'session_id': self.session.pk}) self.client.login(username='secretary', password='secretary+password') r = self.client.post(url, {'markdown_text': 'markdown text'}) self.assertRedirects( r, urlreverse( 'ietf.meeting.views.session_details', kwargs={ 'num': self.meeting.number, 'acronym': self.session.group.acronym, }, ), ) def test_imports_previewed_text(self): """Import text that was shown as preview even if notes site is updated""" url = urlreverse('ietf.meeting.views.import_session_minutes', kwargs={'num': self.meeting.number, 'session_id': self.session.pk}) self.client.login(username='secretary', password='secretary+password') with requests_mock.Mocker() as mock: mock.get(f'https://notes.ietf.org/{self.session.notes_id()}/download', text='updated markdown text') mock.get(f'https://notes.ietf.org/{self.session.notes_id()}/info', text=json.dumps({"title": "title", "updatetime": "2021-12-02T11:22:33z"})) r = self.client.post(url, {'markdown_text': 'original markdown text'}) self.assertEqual(r.status_code, 302) minutes_path = Path(self.meeting.get_materials_path()) / 'minutes' with (minutes_path / self.session.minutes().uploaded_filename).open() as f: self.assertEqual(f.read(), 'original markdown text') def test_refuses_identical_import(self): """Should not be able to import text identical to the current revision""" url = urlreverse('ietf.meeting.views.import_session_minutes', kwargs={'num': self.meeting.number, 'session_id': self.session.pk}) self.client.login(username='secretary', password='secretary+password') with requests_mock.Mocker() as mock: mock.get(f'https://notes.ietf.org/{self.session.notes_id()}/download', text='original markdown text') mock.get(f'https://notes.ietf.org/{self.session.notes_id()}/info', text=json.dumps({"title": "title", "updatetime": "2021-12-02T11:22:33z"})) # Create a revision. Run the original text through the preprocessing done when importing # from the notes site. r = self.client.get(url) # let GET do its preprocessing q = PyQuery(r.content) r = self.client.post(url, {'markdown_text': q('input[name="markdown_text"]').attr['value']}) self.assertEqual(r.status_code, 302) r = self.client.get(url) # try to import the same text self.assertContains(r, "This document is identical", status_code=200) q = PyQuery(r.content) self.assertEqual(len(q('#content button:disabled[type="submit"]')), 1) self.assertEqual(len(q('#content button:enabled[type="submit"]')), 0) def test_allows_import_on_existing_bad_unicode(self): """Should not be able to import text identical to the current revision""" url = urlreverse('ietf.meeting.views.import_session_minutes', kwargs={'num': self.meeting.number, 'session_id': self.session.pk}) self.client.login(username='secretary', password='secretary+password') r = self.client.post(url, {'markdown_text': 'replaced below'}) # create a rev with open( self.session.presentations.filter(document__type="minutes").first().document.get_file_name(), 'wb' ) as f: # Replace existing content with an invalid Unicode byte string. The particular invalid # values here are accented characters in the MacRoman charset (see ticket #3756). f.write(b'invalid \x8e unicode \x99\n') self.assertEqual(r.status_code, 302) with requests_mock.Mocker() as mock: mock.get(f'https://notes.ietf.org/{self.session.notes_id()}/download', text='original markdown text') mock.get(f'https://notes.ietf.org/{self.session.notes_id()}/info', text=json.dumps({"title": "title", "updatetime": "2021-12-02T11:22:33z"})) r = self.client.get(url) # try to import the same text self.assertNotContains(r, "This document is identical", status_code=200) q = PyQuery(r.content) self.assertEqual(len(q('#content button:enabled[type="submit"]')), 1) self.assertEqual(len(q('#content button:disabled[type="submit"]')), 0) def test_handles_missing_previous_revision_file(self): """Should still allow import if the file for the previous revision is missing""" url = urlreverse('ietf.meeting.views.import_session_minutes', kwargs={'num': self.meeting.number, 'session_id': self.session.pk}) self.client.login(username='secretary', password='secretary+password') r = self.client.post(url, {'markdown_text': 'original markdown text'}) # create a rev # remove the file uploaded for the first rev minutes_docs = self.session.presentations.filter(document__type='minutes') self.assertEqual(minutes_docs.count(), 1) Path(minutes_docs.first().document.get_file_name()).unlink() self.assertEqual(r.status_code, 302) with requests_mock.Mocker() as mock: mock.get(f'https://notes.ietf.org/{self.session.notes_id()}/download', text='original markdown text') mock.get(f'https://notes.ietf.org/{self.session.notes_id()}/info', text=json.dumps({"title": "title", "updatetime": "2021-12-02T11:22:33z"})) r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) iframe = q('iframe#preview') self.assertEqual('

    original markdown text

    ', iframe.attr('srcdoc')) markdown_text_input = q('form #id_markdown_text') self.assertEqual(markdown_text_input.val(), 'original markdown text') def test_handles_note_does_not_exist(self): """Should not try to import a note that does not exist""" url = urlreverse('ietf.meeting.views.import_session_minutes', kwargs={'num': self.meeting.number, 'session_id': self.session.pk}) self.client.login(username='secretary', password='secretary+password') with requests_mock.Mocker() as mock: mock.get(requests_mock.ANY, status_code=404) r = self.client.get(url, follow=True) self.assertContains(r, 'Could not import', status_code=200) def test_handles_notes_server_failure(self): """Problems communicating with the notes server should be handled gracefully""" url = urlreverse('ietf.meeting.views.import_session_minutes', kwargs={'num': self.meeting.number, 'session_id': self.session.pk}) self.client.login(username='secretary', password='secretary+password') with requests_mock.Mocker() as mock: mock.get(re.compile(r'.+/download'), exc=requests.exceptions.ConnectTimeout) mock.get(re.compile(r'.+//info'), text='{}') r = self.client.get(url, follow=True) self.assertContains(r, 'Could not reach the notes server', status_code=200) class SessionTests(TestCase): def test_get_summary_by_area(self): meeting = make_meeting_test_data(meeting=MeetingFactory(type_id='ietf', number='100')) sessions = Session.objects.filter(meeting=meeting).with_current_status() data = get_summary_by_area(sessions) self.assertEqual(data[0][0], 'Duration') self.assertGreater(len(data), 2) self.assertEqual(data[-1][0], 'Total Hours') def test_get_summary_by_type(self): meeting = make_meeting_test_data(meeting=MeetingFactory(type_id='ietf', number='100')) sessions = Session.objects.filter(meeting=meeting).with_current_status() data = get_summary_by_type(sessions) self.assertEqual(data[0][0], 'Group Type') self.assertGreater(len(data), 2) def test_get_summary_by_purpose(self): meeting = make_meeting_test_data(meeting=MeetingFactory(type_id='ietf', number='100')) sessions = Session.objects.filter(meeting=meeting).with_current_status() data = get_summary_by_purpose(sessions) self.assertEqual(data[0][0], 'Purpose') self.assertGreater(len(data), 2) def test_meeting_requests(self): meeting = MeetingFactory(type_id='ietf') # a couple non-wg group types, confirm that their has_meetings features are as expected group_type_with_meetings = 'adhoc' self.assertTrue(GroupFeatures.objects.get(pk=group_type_with_meetings).has_meetings) group_type_without_meetings = 'sdo' self.assertFalse(GroupFeatures.objects.get(pk=group_type_without_meetings).has_meetings) area = GroupFactory(type_id='area', acronym='area') requested_session = SessionFactory(meeting=meeting,group__parent=area,status_id='schedw',add_to_schedule=False) conflicting_session = SessionFactory(meeting=meeting,group__parent=area,status_id='schedw',add_to_schedule=False) ConstraintFactory(name_id='key_participant',meeting=meeting,source=requested_session.group,target=conflicting_session.group) not_meeting = SessionFactory(meeting=meeting,group__parent=area,status_id='notmeet',add_to_schedule=False) has_meetings = SessionFactory( meeting=meeting, group__type_id=group_type_with_meetings, status_id='schedw', add_to_schedule=False, ) has_meetings_not_meeting = SessionFactory( meeting=meeting, group__type_id=group_type_with_meetings, status_id='notmeet', add_to_schedule=False, ) # admin and social sessions are not to be shown on the requests page has_meetings_admin_session = SessionFactory( meeting=meeting, group__type_id=group_type_with_meetings, status_id='schedw', purpose_id='admin', type_id='other', add_to_schedule=False, ) has_meetings_social_session = SessionFactory( meeting=meeting, group__type_id=group_type_with_meetings, status_id='schedw', purpose_id='social', type_id='break', add_to_schedule=False, ) not_has_meetings = SessionFactory( meeting=meeting, group__type_id=group_type_without_meetings, status_id='schedw', add_to_schedule=False, ) # bof sessions should be shown bof_session = SessionFactory( meeting=meeting, group__parent=area, group__state_id='bof', status_id='schedw', add_to_schedule=False, ) # proposed WG sessions should be shown proposed_wg_session = SessionFactory( meeting=meeting, group__parent=area, group__state_id='proposed', status_id='schedw', add_to_schedule=False, ) # rg sessions should be shown under 'irtf' heading rg_session = SessionFactory( meeting=meeting, group__type_id='rg', status_id='schedw', add_to_schedule=False, ) session_with_none_purpose = SessionFactory( meeting=meeting, group__parent=area, purpose_id="none", status_id="schedw", add_to_schedule=False, ) tutorial_session = SessionFactory( meeting=meeting, group__parent=area, purpose_id="tutorial", status_id="schedw", add_to_schedule=False, ) def _sreq_edit_link(sess): return urlreverse( 'ietf.secr.sreq.views.edit', kwargs={ 'num': meeting.number, 'acronym': sess.group.acronym, }, ) url = urlreverse('ietf.meeting.views.meeting_requests',kwargs={'num':meeting.number}) r = self.client.get(url) # requested_session group should be listed with a link to the request self.assertContains(r, requested_session.group.acronym) self.assertContains(r, _sreq_edit_link(requested_session)) # link to the session request self.assertContains(r, not_meeting.group.acronym) # The admin/social session groups should be listed under "no timeslot request received"; it's easier # to check that the group is listed but that there is no link to the session request than to try to # parse the HTML. If the view is changed to link to the "no timeslot request received" session requests, # then need to revisit. self.assertContains(r, has_meetings_admin_session.group.acronym) self.assertNotContains(r, _sreq_edit_link(has_meetings_admin_session)) # no link to the session request self.assertContains(r, has_meetings_social_session.group.acronym) self.assertNotContains(r, _sreq_edit_link(has_meetings_social_session)) # no link to the session request self.assertContains(r, requested_session.constraints().first().name) self.assertContains(r, conflicting_session.group.acronym) self.assertContains(r, _sreq_edit_link(conflicting_session)) # link to the session request self.assertContains(r, has_meetings.group.acronym) self.assertContains(r, _sreq_edit_link(has_meetings)) # link to the session request self.assertContains(r, has_meetings_not_meeting.group.acronym) self.assertContains(r, _sreq_edit_link(has_meetings_not_meeting)) # link to the session request self.assertNotContains(r, not_has_meetings.group.acronym) self.assertNotContains(r, _sreq_edit_link(not_has_meetings)) # no link to the session request self.assertContains(r, bof_session.group.acronym) self.assertContains(r, _sreq_edit_link(bof_session)) # link to the session request self.assertContains(r, proposed_wg_session.group.acronym) self.assertContains(r, _sreq_edit_link(proposed_wg_session)) # link to the session request self.assertContains(r, rg_session.group.acronym) self.assertContains(r, _sreq_edit_link(rg_session)) # link to the session request self.assertContains(r, session_with_none_purpose.group.acronym) self.assertContains(r, tutorial_session.group.acronym) # check headings - note that the special types (has_meetings, etc) do not have a group parent # so they show up in 'other' q = PyQuery(r.content) self.assertEqual(len(q('h2#area')), 1) self.assertEqual(len(q('h2#other-groups')), 1) self.assertEqual(len(q('h2#irtf')), 1) # rg group has irtf group as parent # check rounded pills self.assertNotContains( # no rounded pill for sessions with regular purpose r, 'Regular', html=True, ) self.assertNotContains( # no rounded pill for session with no purpose specified r, 'None', html=True, ) self.assertContains( # rounded pill for session with non-regular purpose r, 'Tutorial', html=True, ) def test_request_minutes(self): meeting = MeetingFactory(type_id='ietf') area = GroupFactory(type_id='area') has_minutes = SessionFactory(meeting=meeting,group__parent=area) has_no_minutes = SessionFactory(meeting=meeting,group__parent=area) SessionPresentation.objects.create(session=has_minutes,document=DocumentFactory(type_id='minutes')) empty_outbox() url = urlreverse('ietf.meeting.views.request_minutes',kwargs={'num':meeting.number}) login_testing_unauthorized(self,"secretary",url) r = self.client.get(url) self.assertNotContains(r, has_minutes.group.acronym.upper()) self.assertContains(r, has_no_minutes.group.acronym.upper()) r = self.client.post(url,{'to':'wgchairs@ietf.org', 'cc': 'irsg@irtf.org', 'subject': 'I changed the subject', 'body': 'corpus', }) self.assertEqual(r.status_code,302) self.assertEqual(len(outbox),1) @override_settings(YOUTUBE_DOMAINS=["youtube.com"]) def test_add_session_recordings(self): session = SessionFactory(meeting__type_id="ietf") url = urlreverse( "ietf.meeting.views.add_session_recordings", kwargs={"session_id": session.pk, "num": session.meeting.number}, ) # does not fully validate authorization for non-secretariat users :-( login_testing_unauthorized(self, "secretary", url) r = self.client.get(url) pq = PyQuery(r.content) title_input = pq("input#id_title") self.assertIsNotNone(title_input) self.assertEqual( title_input.attr.value, "Video recording of {acro} for {timestamp}".format( acro=session.group.acronym, timestamp=session.official_timeslotassignment().timeslot.utc_start_time().strftime( "%Y-%m-%d %H:%M" ), ), ) with patch("ietf.meeting.views.create_recording") as mock_create: r = self.client.post( url, data={ "title": "This is my video title", "url": "", } ) self.assertFalse(mock_create.called) with patch("ietf.meeting.views.create_recording") as mock_create: r = self.client.post( url, data={ "title": "This is my video title", "url": "https://yubtub.com/this-is-not-a-youtube-video", } ) self.assertFalse(mock_create.called) with patch("ietf.meeting.views.create_recording") as mock_create: r = self.client.post( url, data={ "title": "This is my video title", "url": "https://youtube.com/finally-a-video", } ) self.assertTrue(mock_create.called) self.assertEqual( mock_create.call_args, call( session, "https://youtube.com/finally-a-video", title="This is my video title", user=Person.objects.get(user__username="secretary"), ), ) # CAN delete session presentation for this session sp = SessionPresentationFactory( session=session, document__type_id="recording", document__external_url="https://example.com/some-video", ) with patch("ietf.meeting.views.delete_recording") as mock_delete: r = self.client.post( url, data={ "delete": str(sp.pk), } ) self.assertEqual(r.status_code, 200) self.assertTrue(mock_delete.called) self.assertEqual(mock_delete.call_args, call(sp)) # ValueError message from delete_recording does not reach the user sp = SessionPresentationFactory( session=session, document__type_id="recording", document__external_url="https://example.com/some-video", ) with patch("ietf.meeting.views.delete_recording", side_effect=ValueError("oh joy!")) as mock_delete: r = self.client.post( url, data={ "delete": str(sp.pk), } ) self.assertTrue(mock_delete.called) self.assertNotContains(r, "oh joy!", status_code=200) # CANNOT delete session presentation for a different session sp_for_other_session = SessionPresentationFactory( document__type_id="recording", document__external_url="https://example.com/some-other-video", ) with patch("ietf.meeting.views.delete_recording") as mock_delete: r = self.client.post( url, data={ "delete": str(sp_for_other_session.pk), } ) self.assertEqual(r.status_code, 404) self.assertFalse(mock_delete.called) class HasMeetingsTests(TestCase): settings_temp_path_overrides = TestCase.settings_temp_path_overrides + ['AGENDA_PATH'] def do_request_interim(self, url, group, user, meeting_count): login_testing_unauthorized(self,user.username, url) r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertTrue(q('#id_group option[value="%d"]'%group.pk)) date = date_today() + datetime.timedelta(days=30+meeting_count) time = time_now().replace(microsecond=0,second=0) remote_instructions = 'Use webex' agenda = 'Intro. Slides. Discuss.' agenda_note = 'On second level' meeting_count = Meeting.objects.filter(number__contains='-%s-'%group.acronym, date__year=date.year).count() next_num = "%02d" % (meeting_count+1) data = {'group':group.pk, 'meeting_type':'single', 'city':'', 'country':'', 'time_zone':'UTC', 'session_set-0-date':date.strftime("%Y-%m-%d"), 'session_set-0-time':time.strftime('%H:%M'), 'session_set-0-requested_duration':'03:00:00', 'session_set-0-remote_instructions':remote_instructions, 'session_set-0-agenda':agenda, 'session_set-0-agenda_note':agenda_note, 'session_set-TOTAL_FORMS':1, 'session_set-INITIAL_FORMS':0, 'session_set-MIN_NUM_FORMS':0, 'session_set-MAX_NUM_FORMS':1000} empty_outbox() r = self.client.post(urlreverse("ietf.meeting.views.interim_request"),data) self.assertRedirects(r,urlreverse('ietf.meeting.views.upcoming')) meeting = Meeting.objects.order_by('id').last() self.assertEqual(meeting.type_id,'interim') self.assertEqual(meeting.date,date) self.assertEqual(meeting.number,'interim-%s-%s-%s' % (date.year, group.acronym, next_num)) self.assertTrue(len(outbox)>0) self.assertIn('interim approved',outbox[0]["Subject"]) self.assertIn(user.person.email().address,outbox[0]["To"]) self.client.logout() def create_role_for_authrole(self, authrole): role = None if authrole == 'Secretariat': role = RoleFactory.create(group__acronym='secretariat',name_id='secr') elif authrole == 'Area Director': role = RoleFactory.create(name_id='ad', group__type_id='area') elif authrole == 'IAB': role = RoleFactory.create(name_id='member', group__acronym='iab') elif authrole == 'IRTF Chair': role = RoleFactory.create(name_id='chair', group__acronym='irtf') if role is None: self.assertIsNone("Can't test authrole:"+authrole) self.assertNotEqual(role, None) return role def test_can_request_interim(self): url = urlreverse('ietf.meeting.views.interim_request') for gf in GroupFeatures.objects.filter(has_meetings=True): meeting_count = 0 for role in gf.groupman_roles: role = RoleFactory(group__type_id=gf.type_id, name_id=role) self.do_request_interim(url, role.group, role.person.user, meeting_count) for authrole in gf.groupman_authroles: group = GroupFactory(type_id=gf.type_id) role = self.create_role_for_authrole(authrole) self.do_request_interim(url, group, role.person.user, 0) def test_cannot_request_interim(self): url = urlreverse('ietf.meeting.views.interim_request') self.client.login(username='secretary', password='secretary+password') nomeetings = [] for gf in GroupFeatures.objects.exclude(has_meetings=True): nomeetings.append(GroupFactory(type_id=gf.type_id)) r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) for group in nomeetings: self.assertFalse(q('#id_group option[value="%d"]'%group.pk)) self.client.logout() all_role_names = set(RoleName.objects.values_list('slug',flat=True)) for gf in GroupFeatures.objects.filter(has_meetings=True): for role_name in all_role_names - set(gf.groupman_roles): role = RoleFactory(group__type_id=gf.type_id,name_id=role_name) self.assertFalse(can_request_interim_meeting(role.person.user)) def test_appears_on_upcoming(self): url = urlreverse('ietf.meeting.views.upcoming') sessions=[] for gf in GroupFeatures.objects.filter(has_meetings=True): session = SessionFactory( group__type_id = gf.type_id, meeting__type_id='interim', meeting__date = timezone.now()+datetime.timedelta(days=30), status_id='sched', ) sessions.append(session) r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) for session in sessions: self.assertIn(session.meeting.number, q('.interim-meeting-link').text()) def test_appears_on_pending(self): url = urlreverse('ietf.meeting.views.interim_pending') sessions=[] for gf in GroupFeatures.objects.filter(has_meetings=True): group = GroupFactory(type_id=gf.type_id) meeting_date = timezone.now() + datetime.timedelta(days=30) session = SessionFactory( group=group, meeting__type_id='interim', meeting__date = meeting_date, meeting__number = 'interim-%d-%s-00'%(meeting_date.year,group.acronym), status_id='apprw', ) sessions.append(session) r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) for session in sessions: self.assertIn(session.meeting.number, q('.interim-meeting-link').text()) def test_appears_on_announce(self): url = urlreverse('ietf.meeting.views.interim_announce') sessions=[] for gf in GroupFeatures.objects.filter(has_meetings=True): group = GroupFactory(type_id=gf.type_id) meeting_date = timezone.now() + datetime.timedelta(days=30) session = SessionFactory( group=group, meeting__type_id='interim', meeting__date = meeting_date, meeting__number = 'interim-%d-%s-00'%(meeting_date.year,group.acronym), status_id='scheda', ) sessions.append(session) r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) for session in sessions: self.assertIn(session.meeting.number, q('.interim-meeting-link').text()) class AgendaFilterTests(TestCase): """Tests for the AgendaFilter template""" def test_agenda_filter_template(self): """Test rendering of input data by the agenda filter template""" def _assert_button_ok(btn, expected_label=None, expected_filter_item=None, expected_filter_keywords=None): """Test button properties""" if expected_label: self.assertIn(btn.text(), expected_label) self.assertEqual(btn.attr('data-filter-item'), expected_filter_item) self.assertEqual(btn.attr('data-filter-keywords'), expected_filter_keywords) template = Template('{% include "meeting/agenda_filter.html" %}') # Test with/without custom button text context = Context({'customize_button_text': None, 'filter_categories': []}) q = PyQuery(template.render(context)) self.assertIn('Customize...', q('h2.accordion-header').text()) self.assertEqual(q('table'), []) # no filter_categories, so no button table context['customize_button_text'] = 'My custom text...' q = PyQuery(template.render(context)) self.assertIn(context['customize_button_text'], q('h2.accordion-header').text()) self.assertEqual(q('table'), []) # no filter_categories, so no button table # Now add a non-trivial set of filters context['filter_categories'] = [ [ # first category dict( label='area0', keyword='keyword0', children=[ dict( label='child00', keyword='keyword00', toggled_by=['keyword0'], is_bof=False, ), dict( label='child01', keyword='keyword01', toggled_by=['keyword0', 'bof'], is_bof=True, ), ]), dict( label='area1', keyword='keyword1', children=[ dict( label='child10', keyword='keyword10', toggled_by=['keyword1'], is_bof=False, ), dict( label='child11', keyword='keyword11', toggled_by=['keyword1', 'bof'], is_bof=True, ), ]), ], [ # second category dict( label='area2', keyword='keyword2', children=[ dict( label='child20', keyword='keyword20', toggled_by=['keyword2', 'bof'], is_bof=True, ), dict( label='child21', keyword='keyword21', toggled_by=['keyword2'], is_bof=False, ), ]), ], [ # third category dict( label=None, keyword=None, children=[ dict( label='child30', keyword='keyword30', toggled_by=[], is_bof=False, ), dict( label='child31', keyword='keyword31', toggled_by=['bof'], is_bof=True, ), ]), ], ] q = PyQuery(template.render(context)) self.assertIn(context['customize_button_text'], q('h2.accordion-header').text()) self.assertNotEqual(q('button.pickview'), []) # should now have group buttons # Check that buttons are present for the expected things header_row = q('.col-1 .row:first') self.assertEqual(len(header_row), 4) button_row = q('.row.view') self.assertEqual(len(button_row), 4) # verify correct headers header_cells = header_row('.row') self.assertEqual(len(header_cells), 4) header_buttons = header_cells('button.pickview') self.assertEqual(len(header_buttons), 3) # last column has disabled header, so only 3 # verify buttons button_cells = button_row('.btn-group-vertical') # area0 _assert_button_ok(header_cells.eq(0)('button.keyword0'), expected_label='area0', expected_filter_item='keyword0') buttons = button_cells.eq(0)('button.pickview') self.assertEqual(len(buttons), 2) # two children _assert_button_ok(buttons('.keyword00'), expected_label='child00', expected_filter_item='keyword00', expected_filter_keywords='keyword0') _assert_button_ok(buttons('.keyword01'), expected_label='child01', expected_filter_item='keyword01', expected_filter_keywords='keyword0,bof') # area1 _assert_button_ok(header_cells.eq(1)('button.keyword1'), expected_label='area1', expected_filter_item='keyword1') buttons = button_cells.eq(1)('button.pickview') self.assertEqual(len(buttons), 2) # two children _assert_button_ok(buttons('.keyword10'), expected_label='child10', expected_filter_item='keyword10', expected_filter_keywords='keyword1') _assert_button_ok(buttons('.keyword11'), expected_label='child11', expected_filter_item='keyword11', expected_filter_keywords='keyword1,bof') # area2 _assert_button_ok(header_cells.eq(2)('button.keyword2'), expected_label='area2', expected_filter_item='keyword2') buttons = button_cells.eq(2)('button.pickview') self.assertEqual(len(buttons), 2) # two children _assert_button_ok(buttons('.keyword20'), expected_label='child20', expected_filter_item='keyword20', expected_filter_keywords='keyword2,bof') _assert_button_ok(buttons('.keyword21'), expected_label='child21', expected_filter_item='keyword21', expected_filter_keywords='keyword2') # area3 _assert_button_ok(header_cells.eq(3)('button.keyword2'), expected_label=None, expected_filter_item=None) buttons = button_cells.eq(3)('button.pickview') self.assertEqual(len(buttons), 2) # two children _assert_button_ok(buttons('.keyword30'), expected_label='child30', expected_filter_item='keyword30', expected_filter_keywords=None) _assert_button_ok(buttons('.keyword31'), expected_label='child31', expected_filter_item='keyword31', expected_filter_keywords='bof') def logo_file(width=128, height=128, format='PNG', ext=None): img = Image.new('RGB', (width, height)) # just a black image data = BytesIO() img.save(data, format=format) data.seek(0) data.name = f'logo.{ext if ext is not None else format.lower()}' return data class MeetingHostTests(BaseMeetingTestCase): def _assertHostFieldCountGreaterEqual(self, r, min_count): q = PyQuery(r.content) self.assertGreaterEqual( len(q('input[type="text"][name^="meetinghosts-"][name$="-name"]')), min_count, f'Must have at least {min_count} host name field(s)', ) self.assertGreaterEqual( len(q('input[type="file"][name^="meetinghosts-"][name$="-logo"]')), min_count, f'Must have at least {min_count} host logo field(s)', ) def _create_first_host(self, meeting, logo, url): """Helper to create a first host via POST""" return self.client.post( url, { 'meetinghosts-TOTAL_FORMS': '2', 'meetinghosts-INITIAL_FORMS': '0', 'meetinghosts-MIN_NUM_FORMS': '0', 'meetinghosts-MAX_NUM_FORMS': '1000', 'meetinghosts-0-id': '', 'meetinghosts-0-meeting': str(meeting.pk), 'meetinghosts-0-name': 'Some Sponsor, Inc.', 'meetinghosts-0-logo': logo, 'meetinghosts-1-id': '', 'meetinghosts-1-meeting': str(meeting.pk), 'meetinghosts-1-name': '', }, ) def test_permissions(self): meeting = MeetingFactory(type_id='ietf') url = urlreverse('ietf.meeting.views_proceedings.edit_meetinghosts', kwargs=dict(num=meeting.number)) self.client.logout() login_testing_unauthorized(self, 'ad', url) login_testing_unauthorized(self, 'secretary', url) r = self.client.get(url) self.assertEqual(r.status_code, 200) self.client.logout() login_testing_unauthorized(self, 'ad', url, method='post') login_testing_unauthorized(self, 'secretary', url, method='post') # don't bother checking a real post - it'll be tested in other methods def _assertMatch(self, value, pattern): self.assertIsNotNone(re.match(pattern, value)) def test_add(self): """Can add a new meeting host""" meeting = MeetingFactory(type_id='ietf') url = urlreverse('ietf.meeting.views_proceedings.edit_meetinghosts', kwargs=dict(num=meeting.number)) # get the edit page to check that it has the necessary fields self.client.login(username='secretary', password='secretary+password') r = self.client.get(url) self.assertEqual(r.status_code, 200) self._assertHostFieldCountGreaterEqual(r, 1) # post our response logos = [logo_file() for _ in range(2)] r = self._create_first_host(meeting, logos[0], url) self.assertRedirects(r, urlreverse('ietf.meeting.views.materials', kwargs=dict(num=meeting.number))) self.assertEqual(meeting.meetinghosts.count(), 1) host = meeting.meetinghosts.first() self.assertEqual(host.name, 'Some Sponsor, Inc.') logo_filename = Path(host.logo.path) self._assertMatch(logo_filename.name, r'logo-[a-z]+.png') self.assertCountEqual( logo_filename.parent.iterdir(), [logo_filename], 'Unexpected or missing files in the output directory', ) # retrieve the page again to ensure we have more fields r = self.client.get(url) self.assertEqual(r.status_code, 200) self._assertHostFieldCountGreaterEqual(r, 2) # must have at least one extra # post our response to add an additional host r = self.client.post( url, { 'meetinghosts-TOTAL_FORMS': '3', 'meetinghosts-INITIAL_FORMS': '1', 'meetinghosts-MIN_NUM_FORMS': '0', 'meetinghosts-MAX_NUM_FORMS': '1000', 'meetinghosts-0-id': str(host.pk), 'meetinghosts-0-meeting': str(meeting.pk), 'meetinghosts-0-name': 'Some Sponsor, Inc.', 'meetinghosts-1-id':'', 'meetinghosts-1-meeting': str(meeting.pk), 'meetinghosts-1-name': 'Another Sponsor, Ltd.', 'meetinghosts-1-logo': logos[1], 'meetinghosts-2-id':'', 'meetinghosts-2-meeting': str(meeting.pk), 'meetinghosts-2-name': '', }, ) self.assertRedirects(r, urlreverse('ietf.meeting.views.materials', kwargs=dict(num=meeting.number))) self.assertEqual(meeting.meetinghosts.count(), 2) host = meeting.meetinghosts.first() self.assertEqual(host.name, 'Some Sponsor, Inc.') logo_filename = Path(host.logo.path) self._assertMatch(logo_filename.name, r'logo-[a-z]+.png') host = meeting.meetinghosts.last() self.assertEqual(host.name, 'Another Sponsor, Ltd.') logo2_filename = Path(host.logo.path) self._assertMatch(logo2_filename.name, r'logo-[a-z]+.png') self.assertCountEqual( logo_filename.parent.iterdir(), [logo_filename, logo2_filename], 'Unexpected or missing files in the output directory', ) # retrieve the page again to ensure we have yet more fields r = self.client.get(url) self.assertEqual(r.status_code, 200) self._assertHostFieldCountGreaterEqual(r, 3) # must have at least one extra def test_edit_name(self): """Can change name of meeting host The main complication is checking that the file has been renamed to match the new host name. """ meeting = MeetingFactory(type_id='ietf') url = urlreverse('ietf.meeting.views_proceedings.edit_meetinghosts', kwargs=dict(num=meeting.number)) # create via UI so we don't have to deal with creating storage paths self.client.login(username='secretary', password='secretary+password') logo = logo_file() r = self._create_first_host(meeting, logo, url) self.assertRedirects(r, urlreverse('ietf.meeting.views.materials', kwargs=dict(num=meeting.number))) self.assertEqual(meeting.meetinghosts.count(), 1) host = meeting.meetinghosts.first() self.assertEqual(host.name, 'Some Sponsor, Inc.') orig_logopath = Path(host.logo.path) self._assertMatch(orig_logopath.name, r'logo-[a-z]+.png') self.assertTrue(orig_logopath.exists()) # post our response to modify the name r = self.client.post( url, { 'meetinghosts-TOTAL_FORMS': '3', 'meetinghosts-INITIAL_FORMS': '1', 'meetinghosts-MIN_NUM_FORMS': '0', 'meetinghosts-MAX_NUM_FORMS': '1000', 'meetinghosts-0-id': str(host.pk), 'meetinghosts-0-meeting': str(meeting.pk), 'meetinghosts-0-name': 'Modified Sponsor, Inc.', 'meetinghosts-1-id':'', 'meetinghosts-1-meeting': str(meeting.pk), 'meetinghosts-1-name': '', 'meetinghosts-2-id':'', 'meetinghosts-2-meeting': str(meeting.pk), 'meetinghosts-2-name': '', }, ) self.assertRedirects(r, urlreverse('ietf.meeting.views.materials', kwargs=dict(num=meeting.number))) self.assertEqual(meeting.meetinghosts.count(), 1) host = meeting.meetinghosts.first() self.assertEqual(host.name, 'Modified Sponsor, Inc.') second_logopath = Path(host.logo.path) self.assertEqual(second_logopath, orig_logopath) self.assertTrue(second_logopath.exists()) with second_logopath.open('rb') as f: self.assertEqual(f.read(), logo.getvalue()) def test_meeting_host_replace_logo(self): """Can replace logo of a meeting host""" meeting = MeetingFactory(type_id='ietf') url = urlreverse('ietf.meeting.views_proceedings.edit_meetinghosts', kwargs=dict(num=meeting.number)) # create via UI so we don't have to deal with creating storage paths self.client.login(username='secretary', password='secretary+password') logo = logo_file() r = self._create_first_host(meeting, logo, url) self.assertRedirects(r, urlreverse('ietf.meeting.views.materials', kwargs=dict(num=meeting.number))) self.assertEqual(meeting.meetinghosts.count(), 1) host = meeting.meetinghosts.first() self.assertEqual(host.name, 'Some Sponsor, Inc.') orig_logopath = Path(host.logo.path) self._assertMatch(orig_logopath.name, r'logo-[a-z]+.png') self.assertTrue(orig_logopath.exists()) # post our response to replace the logo new_logo = logo_file(200, 200) # different size to distinguish images r = self.client.post( url, { 'meetinghosts-TOTAL_FORMS': '3', 'meetinghosts-INITIAL_FORMS': '1', 'meetinghosts-MIN_NUM_FORMS': '0', 'meetinghosts-MAX_NUM_FORMS': '1000', 'meetinghosts-0-id': str(host.pk), 'meetinghosts-0-meeting': str(meeting.pk), 'meetinghosts-0-name': 'Some Sponsor, Inc.', 'meetinghosts-0-logo': new_logo, 'meetinghosts-1-id':'', 'meetinghosts-1-meeting': str(meeting.pk), 'meetinghosts-1-name': '', 'meetinghosts-2-id':'', 'meetinghosts-2-meeting': str(meeting.pk), 'meetinghosts-2-name': '', }, ) self.assertRedirects(r, urlreverse('ietf.meeting.views.materials', kwargs=dict(num=meeting.number))) self.assertEqual(meeting.meetinghosts.count(), 1) host = meeting.meetinghosts.first() self.assertEqual(host.name, 'Some Sponsor, Inc.') second_logopath = Path(host.logo.path) self._assertMatch(second_logopath.name, r'logo-[a-z]+.png') self.assertTrue(second_logopath.exists()) with second_logopath.open('rb') as f: self.assertEqual(f.read(), new_logo.getvalue()) def test_change_name_and_replace_logo(self): """Can simultaneously change name and replace logo""" meeting = MeetingFactory(type_id='ietf') url = urlreverse('ietf.meeting.views_proceedings.edit_meetinghosts', kwargs=dict(num=meeting.number)) # create via UI so we don't have to deal with creating storage paths self.client.login(username='secretary', password='secretary+password') logo = logo_file() r = self._create_first_host(meeting, logo, url) self.assertRedirects(r, urlreverse('ietf.meeting.views.materials', kwargs=dict(num=meeting.number))) self.assertEqual(meeting.meetinghosts.count(), 1) host = meeting.meetinghosts.first() self.assertEqual(host.name, 'Some Sponsor, Inc.') orig_logopath = Path(host.logo.path) self._assertMatch(orig_logopath.name, r'logo-[a-z]+.png') self.assertTrue(orig_logopath.exists()) # post our response to replace the logo new_logo = logo_file(200, 200) # different size to distinguish images r = self.client.post( url, { 'meetinghosts-TOTAL_FORMS': '3', 'meetinghosts-INITIAL_FORMS': '1', 'meetinghosts-MIN_NUM_FORMS': '0', 'meetinghosts-MAX_NUM_FORMS': '1000', 'meetinghosts-0-id': str(host.pk), 'meetinghosts-0-meeting': str(meeting.pk), 'meetinghosts-0-name': 'Modified Sponsor, Ltd.', 'meetinghosts-0-logo': new_logo, 'meetinghosts-1-id':'', 'meetinghosts-1-meeting': str(meeting.pk), 'meetinghosts-1-name': '', 'meetinghosts-2-id':'', 'meetinghosts-2-meeting': str(meeting.pk), 'meetinghosts-2-name': '', }, ) self.assertRedirects(r, urlreverse('ietf.meeting.views.materials', kwargs=dict(num=meeting.number))) self.assertEqual(meeting.meetinghosts.count(), 1) host = meeting.meetinghosts.first() self.assertEqual(host.name, 'Modified Sponsor, Ltd.') second_logopath = Path(host.logo.path) self._assertMatch(second_logopath.name, r'logo-[a-z]+.png') self.assertTrue(second_logopath.exists()) with second_logopath.open('rb') as f: self.assertEqual(f.read(), new_logo.getvalue()) self.assertFalse(orig_logopath.exists()) def test_remove(self): """Can delete a meeting host and its logo""" meeting = MeetingFactory(type_id='ietf') url = urlreverse('ietf.meeting.views_proceedings.edit_meetinghosts', kwargs=dict(num=meeting.number)) # create via UI so we don't have to deal with creating storage paths self.client.login(username='secretary', password='secretary+password') logo = logo_file() r = self._create_first_host(meeting, logo, url) self.assertRedirects(r, urlreverse('ietf.meeting.views.materials', kwargs=dict(num=meeting.number))) self.assertEqual(meeting.meetinghosts.count(), 1) host = meeting.meetinghosts.first() self.assertEqual(host.name, 'Some Sponsor, Inc.') logopath = Path(host.logo.path) self._assertMatch(logopath.name, r'logo-[a-z]+.png') self.assertTrue(logopath.exists()) # now delete r = self.client.post( url, { 'meetinghosts-TOTAL_FORMS': '3', 'meetinghosts-INITIAL_FORMS': '1', 'meetinghosts-MIN_NUM_FORMS': '0', 'meetinghosts-MAX_NUM_FORMS': '1000', 'meetinghosts-0-id': str(host.pk), 'meetinghosts-0-meeting': str(meeting.pk), 'meetinghosts-0-name': 'Modified Sponsor, Ltd.', 'meetinghosts-0-DELETE': 'on', 'meetinghosts-1-id':'', 'meetinghosts-1-meeting': str(meeting.pk), 'meetinghosts-1-name': '', 'meetinghosts-2-id':'', 'meetinghosts-2-meeting': str(meeting.pk), 'meetinghosts-2-name': '', }, ) self.assertRedirects(r, urlreverse('ietf.meeting.views.materials', kwargs=dict(num=meeting.number))) self.assertEqual(meeting.meetinghosts.count(), 0) self.assertFalse(logopath.exists()) def test_remove_with_selected_logo(self): """Can delete a meeting host after selecting a replacement file""" meeting = MeetingFactory(type_id='ietf') url = urlreverse('ietf.meeting.views_proceedings.edit_meetinghosts', kwargs=dict(num=meeting.number)) # create via UI so we don't have to deal with creating storage paths self.client.login(username='secretary', password='secretary+password') logo = logo_file() r = self._create_first_host(meeting, logo, url) self.assertRedirects(r, urlreverse('ietf.meeting.views.materials', kwargs=dict(num=meeting.number))) self.assertEqual(meeting.meetinghosts.count(), 1) host = meeting.meetinghosts.first() self.assertEqual(host.name, 'Some Sponsor, Inc.') logopath = Path(host.logo.path) self._assertMatch(logopath.name, r'logo-[a-z]+.png') self.assertTrue(logopath.exists()) # now delete r = self.client.post( url, { 'meetinghosts-TOTAL_FORMS': '3', 'meetinghosts-INITIAL_FORMS': '1', 'meetinghosts-MIN_NUM_FORMS': '0', 'meetinghosts-MAX_NUM_FORMS': '1000', 'meetinghosts-0-id': str(host.pk), 'meetinghosts-0-meeting': str(meeting.pk), 'meetinghosts-0-name': 'Modified Sponsor, Ltd.', 'meetinghosts-0-DELETE': 'on', 'meetinghosts-0-logo': logo_file(format='JPEG'), 'meetinghosts-1-id':'', 'meetinghosts-1-meeting': str(meeting.pk), 'meetinghosts-1-name': '', 'meetinghosts-2-id':'', 'meetinghosts-2-meeting': str(meeting.pk), 'meetinghosts-2-name': '', }, ) self.assertRedirects(r, urlreverse('ietf.meeting.views.materials', kwargs=dict(num=meeting.number))) self.assertEqual(meeting.meetinghosts.count(), 0) self.assertFalse(logopath.exists()) def test_logo_types_checked(self): """Only allowed image types should be accepted""" allowed_formats = [('JPEG', 'jpg'), ('JPEG', 'jpeg'), ('PNG', 'png')] meeting = MeetingFactory(type_id='ietf') url = urlreverse('ietf.meeting.views_proceedings.edit_meetinghosts', kwargs=dict(num=meeting.number)) self.client.login(username='secretary', password='secretary+password') junk = BytesIO() junk.write(b'this is not an image') junk.seek(0) r = self._create_first_host(meeting, junk, url) self.assertContains(r, 'Upload a valid image', status_code=200) self.assertEqual(meeting.meetinghosts.count(), 0) for fmt, ext in allowed_formats: r = self._create_first_host( meeting, logo_file(format=fmt, ext=ext), url ) self.assertRedirects(r, urlreverse('ietf.meeting.views.materials', kwargs=dict(num=meeting.number))) self.assertEqual(meeting.meetinghosts.count(), 1) meeting.meetinghosts.all().delete() # Keep these settings consistent with the assumptions in these tests @override_settings(PROCEEDINGS_VERSION_CHANGES=[0, 97, 111]) class ProceedingsTests(BaseMeetingTestCase): """Tests related to meeting proceedings display Fills in all material types """ def _create_proceedings_materials(self, meeting): """Create various types of proceedings materials for meeting""" MeetingHostFactory.create_batch(2, meeting=meeting) # create a couple of meeting hosts/logos ProceedingsMaterialFactory( # default title, not removed meeting=meeting, type=ProceedingsMaterialTypeName.objects.get(slug='supporters') ) ProceedingsMaterialFactory( # custom title, not removed meeting=meeting, type=ProceedingsMaterialTypeName.objects.get(slug='host_speaker_series'), document__title='Speakers' ) ProceedingsMaterialFactory( # default title, removed meeting=meeting, type=ProceedingsMaterialTypeName.objects.get(slug='social_event'), document__states=[('procmaterials', 'removed')] ) ProceedingsMaterialFactory( # custom title, removed meeting=meeting, type=ProceedingsMaterialTypeName.objects.get(slug='additional_information'), document__title='Party', document__states=[('procmaterials', 'removed')] ) ProceedingsMaterialFactory( # url meeting=meeting, type=ProceedingsMaterialTypeName.objects.get(slug='wiki'), document__external_url='https://example.com/wiki' ) @staticmethod def _proceedings_file(): """Get a file containing content suitable for a proceedings document Currently returns the same file every time. """ path = Path(settings.BASE_DIR) / 'meeting/test_procmat.pdf' return path.open('rb') def _assertMeetingHostsDisplayed(self, response, meeting): pq = PyQuery(response.content) host_divs = pq('div.host-logo') self.assertEqual(len(host_divs), meeting.meetinghosts.count(), 'Should have a logo for every meeting host') self.assertEqual( [(img.attr('title'), img.attr('src')) for img in host_divs.items('img')], [ (host.name, urlreverse( 'ietf.meeting.views_proceedings.meetinghost_logo', kwargs=dict(num=meeting.number, host_id=host.pk), )) for host in meeting.meetinghosts.all() ], 'Correct image and name for each host should appear in the correct order' ) def _assertProceedingsMaterialsDisplayed(self, response, meeting): """Checks that all (and only) active materials are linked with correct href and title""" expected_materials = [ m for m in meeting.proceedings_materials.order_by('type__order') if m.active() ] pq = PyQuery(response.content) links = pq('div.proceedings-material a') self.assertEqual(len(links), len(expected_materials), 'Should have an entry for each active ProceedingsMaterial') self.assertEqual( [(link.eq(0).text(), link.eq(0).attr('href')) for link in links.items()], [(str(pm), pm.get_href()) for pm in expected_materials], 'Correct title and link for each ProceedingsMaterial should appear in the correct order' ) def _assertGroupSessions(self, response, meeting): """Checks that group/sessions are present""" pq = PyQuery(response.content) sections = ["plenaries", "gen", "iab", "editorial", "irtf", "training"] for section in sections: self.assertEqual(len(pq(f"#{section}")), 1, f"{section} section should exists in proceedings") def test_proceedings(self): """Proceedings should be displayed correctly Currently only tests that the view responds with a 200 response code and checks the ProceedingsMaterials at the top of the proceedings. Ought to actually test the display of the individual group/session materials as well. """ meeting = make_meeting_test_data(meeting=MeetingFactory(type_id='ietf', number='100')) session = Session.objects.filter(meeting=meeting, group__acronym="mars").first() GroupEventFactory(group=session.group,type='status_update') SessionPresentationFactory(document__type_id='recording',session=session) SessionPresentationFactory(document__type_id='recording',session=session,document__title="Audio recording for tests") # Add various group sessions groups = [] parent_groups = [ GroupFactory.create(type_id="area", acronym="gen"), GroupFactory.create(acronym="iab"), GroupFactory.create(acronym="irtf"), ] for parent in parent_groups: groups.append(GroupFactory.create(parent=parent)) for acronym in ["rsab", "edu"]: groups.append(GroupFactory.create(acronym=acronym)) for group in groups: SessionFactory(meeting=meeting, group=group) self.write_materials_files(meeting, session) self._create_proceedings_materials(meeting) url = urlreverse("ietf.meeting.views.proceedings", kwargs=dict(num=meeting.number)) r = self.client.get(url) self.assertEqual(r.status_code, 200) if len(meeting.city) > 0: self.assertContains(r, meeting.city) if len(meeting.venue_name) > 0: self.assertContains(r, meeting.venue_name) # standard items on every proceedings pq = PyQuery(r.content) self.assertNotEqual( pq('a[href="{}"]'.format( urlreverse('ietf.meeting.views.proceedings_overview', kwargs=dict(num=meeting.number))) ), [], 'Should have a link to IETF overview', ) self.assertNotEqual( pq('a[href="{}"]'.format( urlreverse('ietf.meeting.views.proceedings_attendees', kwargs=dict(num=meeting.number))) ), [], 'Should have a link to attendees', ) self.assertNotEqual( pq('a[href="{}"]'.format( urlreverse('ietf.meeting.views.proceedings_activity_report', kwargs=dict(num=meeting.number))) ), [], 'Should have a link to activity report', ) self.assertNotEqual( pq('a[href="{}"]'.format( urlreverse('ietf.meeting.views.important_dates', kwargs=dict(num=meeting.number))) ), [], 'Should have a link to important dates', ) # configurable contents self._assertMeetingHostsDisplayed(r, meeting) self._assertProceedingsMaterialsDisplayed(r, meeting) self._assertGroupSessions(r, meeting) def test_named_session(self): """Session with a name should appear separately in the proceedings""" meeting = MeetingFactory(type_id='ietf', number='100', proceedings_final=True) group = GroupFactory() plain_session = SessionFactory(meeting=meeting, group=group) named_session = SessionFactory(meeting=meeting, group=group, name='I Got a Name') for doc_type_id in ('agenda', 'minutes', 'bluesheets', 'recording', 'slides', 'draft'): # Set up sessions materials that will have distinct URLs for each session. # This depends on settings.MEETING_DOC_HREFS and may need updating if that changes. SessionPresentationFactory( session=plain_session, document__type_id=doc_type_id, document__uploaded_filename=f'upload-{doc_type_id}-plain', document__external_url=f'external_url-{doc_type_id}-plain', ) SessionPresentationFactory( session=named_session, document__type_id=doc_type_id, document__uploaded_filename=f'upload-{doc_type_id}-named', document__external_url=f'external_url-{doc_type_id}-named', ) url = urlreverse('ietf.meeting.views.proceedings', kwargs={'num': meeting.number}) r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) plain_label = q(f'div#{group.acronym}') self.assertEqual(plain_label.text(), group.acronym) plain_row = plain_label.closest('tr') self.assertTrue(plain_row) named_label = q(f'div#{slugify(named_session.name)}') self.assertEqual(named_label.text(), named_session.name) named_row = named_label.closest('tr') self.assertTrue(named_row) for material in (sp.document for sp in plain_session.presentations.all()): if material.type_id == 'draft': expected_url = urlreverse( 'ietf.doc.views_doc.document_main', kwargs={'name': material.name}, ) else: expected_url = material.get_href(meeting) self.assertTrue(plain_row.find(f'a[href="{expected_url}"]')) self.assertFalse(named_row.find(f'a[href="{expected_url}"]')) for material in (sp.document for sp in named_session.presentations.all()): if material.type_id == 'draft': expected_url = urlreverse( 'ietf.doc.views_doc.document_main', kwargs={'name': material.name}, ) else: expected_url = material.get_href(meeting) self.assertFalse(plain_row.find(f'a[href="{expected_url}"]')) self.assertTrue(named_row.find(f'a[href="{expected_url}"]')) def test_proceedings_no_agenda(self): # Meeting number must be larger than the last special-cased proceedings (currently 96) meeting = MeetingFactory(type_id='ietf',populate_schedule=False,date=date_today(), number='100') url = urlreverse('ietf.meeting.views.proceedings') r = self.client.get(url) self.assertRedirects(r, urlreverse('ietf.meeting.views.materials')) url = urlreverse('ietf.meeting.views.proceedings', kwargs=dict(num=meeting.number)) r = self.client.get(url) self.assertRedirects(r, urlreverse('ietf.meeting.views.materials', kwargs=dict(num=meeting.number))) def test_proceedings_acknowledgements(self): make_meeting_test_data() meeting = MeetingFactory(type_id='ietf', date=datetime.date(2016,7,14), number="97") meeting.acknowledgements = 'test acknowledgements' meeting.save() url = urlreverse('ietf.meeting.views.proceedings_acknowledgements',kwargs={'num':meeting.number}) response = self.client.get(url) self.assertContains(response, 'test acknowledgements') def test_proceedings_acknowledgements_link(self): """Link to proceedings_acknowledgements view should not appear for 'new' meetings With the PROCEEDINGS_VERSION_CHANGES settings value used here, expect the proceedings_acknowledgements view to be linked for meetings 95-110. """ meeting_with_acks = MeetingFactory(type_id='ietf', date=datetime.date(2020,7,25), number='108') SessionFactory(meeting=meeting_with_acks) # make sure meeting has a scheduled session meeting_with_acks.acknowledgements = 'these acknowledgements should appear' meeting_with_acks.save() url = urlreverse('ietf.meeting.views.proceedings',kwargs={'num':meeting_with_acks.number}) response = self.client.get(url) self.assertEqual(response.status_code, 200) q = PyQuery(response.content) self.assertEqual( len(q('a[href="{}"]'.format( urlreverse('ietf.meeting.views.proceedings_acknowledgements', kwargs={'num':meeting_with_acks.number}) ))), 1, ) meeting_without_acks = MeetingFactory(type_id='ietf', date=datetime.date(2022,7,25), number='113') SessionFactory(meeting=meeting_without_acks) # make sure meeting has a scheduled session meeting_without_acks.acknowledgements = 'these acknowledgements should not appear' meeting_without_acks.save() url = urlreverse('ietf.meeting.views.proceedings',kwargs={'num':meeting_without_acks.number}) response = self.client.get(url) self.assertEqual(response.status_code, 200) q = PyQuery(response.content) self.assertEqual( len(q('a[href="{}"]'.format( urlreverse('ietf.meeting.views.proceedings_acknowledgements', kwargs={'num':meeting_without_acks.number}) ))), 0, ) def test_proceedings_attendees(self): """Test proceedings attendee list. Check the following: - assert onsite checkedin=True appears, not onsite checkedin=False - assert remote attended appears, not remote not attended - prefer onsite checkedin=True to remote attended when same person has both """ meeting = MeetingFactory(type_id='ietf', date=datetime.date(2023, 11, 4), number="118") person_a = PersonFactory(name='Person A') person_b = PersonFactory(name='Person B') person_c = PersonFactory(name='Person C') person_d = PersonFactory(name='Person D') MeetingRegistrationFactory(meeting=meeting, person=person_a, reg_type='onsite', checkedin=True) MeetingRegistrationFactory(meeting=meeting, person=person_b, reg_type='onsite', checkedin=False) MeetingRegistrationFactory(meeting=meeting, person=person_a, reg_type='remote') AttendedFactory(session__meeting=meeting, session__type_id='plenary', person=person_a) MeetingRegistrationFactory(meeting=meeting, person=person_c, reg_type='remote') AttendedFactory(session__meeting=meeting, session__type_id='plenary', person=person_c) MeetingRegistrationFactory(meeting=meeting, person=person_d, reg_type='remote') url = urlreverse('ietf.meeting.views.proceedings_attendees',kwargs={'num': 118}) response = self.client.get(url) self.assertContains(response, 'Attendee list') q = PyQuery(response.content) self.assertEqual(2, len(q("#id_attendees tbody tr"))) text = q('#id_attendees tbody tr').text().replace('\n', ' ') self.assertEqual(text, "A Person onsite C Person remote") def test_proceedings_overview(self): '''Test proceedings IETF Overview page. Note: old meetings aren't supported so need to add a new meeting then test. ''' meeting = make_meeting_test_data(meeting=MeetingFactory(type_id='ietf', date=datetime.date(2016,7,14), number="97")) # finalize meeting url = urlreverse('ietf.meeting.views.finalize_proceedings',kwargs={'num':meeting.number}) login_testing_unauthorized(self,"secretary",url) r = self.client.post(url,{'finalize':1}) self.assertEqual(r.status_code, 302) url = urlreverse('ietf.meeting.views.proceedings_overview',kwargs={'num':97}) response = self.client.get(url) self.assertContains(response, 'The Internet Engineering Task Force') def test_proceedings_activity_report(self): make_meeting_test_data() MeetingFactory(type_id='ietf', date=datetime.date(2016,4,3), number="96") MeetingFactory(type_id='ietf', date=datetime.date(2016,7,14), number="97") url = urlreverse('ietf.meeting.views.proceedings_activity_report',kwargs={'num':97}) response = self.client.get(url) self.assertContains(response, 'Activity Report') def test_feed(self): meeting = make_meeting_test_data() session = Session.objects.filter(meeting=meeting, group__acronym="mars").first() r = self.client.get("/feed/wg-proceedings/") self.assertContains(r, "agenda") self.assertContains(r, session.group.acronym) def _procmat_test_meeting(self): """Generate a meeting for proceedings material test""" # meeting number 123 avoids various legacy cases that affect these tests # (as of Aug 2021, anything above 96 is probably ok) return MeetingFactory(type_id='ietf', number='123', date=date_today()) def _secretary_only_permission_test(self, url, include_post=True): self.client.logout() login_testing_unauthorized(self, 'ad', url) login_testing_unauthorized(self, 'secretary', url) r = self.client.get(url) self.assertEqual(r.status_code, 200) if include_post: self.client.logout() login_testing_unauthorized(self, 'ad', url, method='post') login_testing_unauthorized(self, 'secretary', url, method='post') # don't bother checking a real post - it'll be tested in other methods def test_material_management_permissions(self): """Only the secreatariat should be able to manage proceedings materials""" meeting = self._procmat_test_meeting() # test all materials types in case they wind up treated differently # (unlikely, but more likely than an unwieldy number of types are introduced) for mat_type in ProceedingsMaterialTypeName.objects.filter(used=True): self._secretary_only_permission_test( urlreverse( 'ietf.meeting.views_proceedings.material_details', kwargs=dict(num=meeting.number), )) self._secretary_only_permission_test( urlreverse( 'ietf.meeting.views_proceedings.upload_material', kwargs=dict(num=meeting.number, material_type=mat_type.slug), )) # remaining tests need material to exist, so create ProceedingsMaterialFactory(meeting=meeting, type=mat_type) self._secretary_only_permission_test( urlreverse( 'ietf.meeting.views_proceedings.edit_material', kwargs=dict(num=meeting.number, material_type=mat_type.slug), )) self._secretary_only_permission_test( urlreverse( 'ietf.meeting.views_proceedings.remove_material', kwargs=dict(num=meeting.number, material_type=mat_type.slug), )) # it's ok to use active materials for restore test - no restore is actually issued self._secretary_only_permission_test( urlreverse( 'ietf.meeting.views_proceedings.restore_material', kwargs=dict(num=meeting.number, material_type=mat_type.slug), )) def test_proceedings_material_details(self): """Material details page should correctly show materials""" meeting = self._procmat_test_meeting() url = urlreverse('ietf.meeting.views_proceedings.material_details', kwargs=dict(num=meeting.number)) self.client.login(username='secretary', password='secretary+password') procmat_types = ProceedingsMaterialTypeName.objects.filter(used=True) r = self.client.get(url) self.assertEqual(r.status_code, 200) pq = PyQuery(r.content) body_rows = pq('tbody > tr') self.assertEqual(len(body_rows), procmat_types.count()) for row, mat_type in zip(body_rows.items(), procmat_types.all()): cells = row.find('td') # no materials, so rows should be empty except for label and 'Add' button self.assertEqual(len(cells), 3) # label, blank, buttons self.assertEqual(cells.eq(0).text(), str(mat_type), 'First column should be material type name') self.assertEqual(cells.eq(1).text(), '', 'Second column should be empty') add_url = urlreverse('ietf.meeting.views_proceedings.upload_material', kwargs=dict(num=meeting.number, material_type=mat_type.slug)) self.assertEqual(len(cells.eq(2).find(f'a[href="{add_url}"]')), 1, 'Third column should have Add link') self._create_proceedings_materials(meeting) r = self.client.get(url) self.assertEqual(r.status_code, 200) pq = PyQuery(r.content) body_rows = pq('tbody > tr') self.assertEqual(len(body_rows), procmat_types.count()) # n.b., this loop is over materials, not the type names! for row, mat in zip(body_rows.items(), meeting.proceedings_materials.order_by('type__order')): add_url = urlreverse('ietf.meeting.views_proceedings.upload_material', kwargs=dict(num=meeting.number, material_type=mat.type.slug)) edit_url = urlreverse('ietf.meeting.views_proceedings.upload_material', kwargs=dict(num=meeting.number, material_type=mat.type.slug)) remove_url = urlreverse('ietf.meeting.views_proceedings.upload_material', kwargs=dict(num=meeting.number, material_type=mat.type.slug)) restore_url = urlreverse('ietf.meeting.views_proceedings.upload_material', kwargs=dict(num=meeting.number, material_type=mat.type.slug)) cells = row.find('td') # no materials, so rows should be empty except for label and 'Add' button self.assertEqual(cells.eq(0).text(), str(mat.type), 'First column should be material type name') if mat.active(): self.assertEqual(len(cells), 5) # label, title, doc, updated, buttons self.assertEqual(cells.eq(1).text(), str(mat), 'Second column should be active material title') self.assertEqual( cells.eq(2).text(), '{} ({})'.format( str(mat.document), 'external URL' if mat.document.external_url else 'uploaded file', )) mod_time = mat.document.time.astimezone(pytz.utc) c3text = cells.eq(3).text() self.assertIn(mod_time.strftime('%Y-%m-%d'), c3text, 'Updated date incorrect') self.assertIn(mod_time.strftime('%H:%M:%S'), c3text, 'Updated time incorrect') self.assertEqual(len(cells.eq(4).find(f'a[href="{add_url}"]')), 1, 'Fourth column should have a Replace link') self.assertEqual(len(cells.eq(4).find(f'a[href="{edit_url}"]')), 1, 'Fourth column should have an Edit link') self.assertEqual(len(cells.eq(4).find(f'a[href="{remove_url}"]')), 1, 'Fourth column should have a Remove link') else: self.assertEqual(len(cells), 3) # label, blank, buttons self.assertEqual(cells.eq(0).text(), str(mat.type), 'First column should be material type name') self.assertEqual(cells.eq(1).text(), '', 'Second column should be empty') add_url = urlreverse('ietf.meeting.views_proceedings.upload_material', kwargs=dict(num=meeting.number, material_type=mat.type.slug)) self.assertEqual(len(cells.eq(2).find(f'a[href="{add_url}"]')), 1, 'Third column should have Add link') self.assertEqual(len(cells.eq(2).find(f'a[href="{restore_url}"]')), 1, 'Third column should have Restore link') def upload_proceedings_material_test(self, meeting, mat_type, post_data): """Test the upload_proceedings view using provided POST data""" url = urlreverse( 'ietf.meeting.views_proceedings.upload_material', kwargs=dict(num=meeting.number, material_type=mat_type.slug), ) self.client.login(username='secretary', password='secretary+password') mats_before = [m.pk for m in meeting.proceedings_materials.all()] r = self.client.post(url, post_data) self.assertRedirects( r, urlreverse('ietf.meeting.views_proceedings.material_details', kwargs=dict(num=meeting.number)), ) self.assertEqual(meeting.proceedings_materials.count(), len(mats_before) + 1) mat = meeting.proceedings_materials.exclude(pk__in=mats_before).first() self.assertEqual(mat.type, mat_type) self.assertEqual(str(mat), mat_type.name) self.assertEqual(mat.document.rev, '00') return mat # use a simple and predictable href format for this test @override_settings(MEETING_DOC_HREFS={'procmaterials': '{doc.name}:{doc.rev}'}) def test_add_proceedings_material_doc(self): """Upload proceedings materials document""" meeting = self._procmat_test_meeting() for mat_type in ProceedingsMaterialTypeName.objects.filter(used=True): with self._proceedings_file() as fd: mat = self.upload_proceedings_material_test( meeting, mat_type, {'file': fd, 'external_url': ''}, ) self.assertEqual(mat.get_href(), f'{mat.document.name}:00') def test_add_proceedings_material_doc_invalid_ext(self): """Upload proceedings materials document with disallowed extension""" meeting = self._procmat_test_meeting() self.client.login(username='secretary', password='secretary+password') with NamedTemporaryFile('w+', suffix='.png') as invalid_file: invalid_file.write('this is not a PDF file!!') for mat_type in ProceedingsMaterialTypeName.objects.filter(used=True): url = urlreverse( 'ietf.meeting.views_proceedings.upload_material', kwargs=dict(num=meeting.number, material_type=mat_type.slug), ) invalid_file.seek(0) # read the file contents again r = self.client.post(url, {'file': invalid_file, 'external_url': ''}) self.assertEqual(r.status_code, 200) self.assertFormError(r.context["form"], 'file', 'Found an unexpected extension: .png. Expected one of .pdf') def test_add_proceedings_material_doc_empty(self): """Upload proceedings materials document without specifying a file""" meeting = self._procmat_test_meeting() self.client.login(username='secretary', password='secretary+password') for mat_type in ProceedingsMaterialTypeName.objects.filter(used=True): url = urlreverse( 'ietf.meeting.views_proceedings.upload_material', kwargs=dict(num=meeting.number, material_type=mat_type.slug), ) r = self.client.post(url, {'external_url': ''}) self.assertEqual(r.status_code, 200) self.assertFormError(r.context["form"], 'file', 'This field is required') def test_add_proceedings_material_url(self): """Add a URL as proceedings material""" meeting = self._procmat_test_meeting() for mat_type in ProceedingsMaterialTypeName.objects.filter(used=True): mat = self.upload_proceedings_material_test( meeting, mat_type, {'use_url': 'on', 'external_url': 'https://example.com'}, ) self.assertEqual(mat.get_href(), 'https://example.com') def test_add_proceedings_material_url_invalid(self): """Add proceedings materials URL with a non-URL value""" meeting = self._procmat_test_meeting() self.client.login(username='secretary', password='secretary+password') for mat_type in ProceedingsMaterialTypeName.objects.filter(used=True): url = urlreverse( 'ietf.meeting.views_proceedings.upload_material', kwargs=dict(num=meeting.number, material_type=mat_type.slug), ) r = self.client.post(url, {'use_url': 'on', 'external_url': "Ceci n'est pas une URL"}) self.assertEqual(r.status_code, 200) self.assertFormError(r.context["form"], 'external_url', 'Enter a valid URL.') def test_add_proceedings_material_url_empty(self): """Add proceedings materials URL without specifying the URL""" meeting = self._procmat_test_meeting() self.client.login(username='secretary', password='secretary+password') for mat_type in ProceedingsMaterialTypeName.objects.filter(used=True): url = urlreverse( 'ietf.meeting.views_proceedings.upload_material', kwargs=dict(num=meeting.number, material_type=mat_type.slug), ) r = self.client.post(url, {'use_url': 'on', 'external_url': ''}) self.assertEqual(r.status_code, 200) self.assertFormError(r.context["form"], 'external_url', 'This field is required') @override_settings(MEETING_DOC_HREFS={'procmaterials': '{doc.name}:{doc.rev}'}) def test_replace_proceedings_material(self): """Replace uploaded document with new uploaded document""" # Set up a meeting with a proceedings material in place meeting = self._procmat_test_meeting() pm_doc = ProceedingsMaterialFactory(meeting=meeting) with self._proceedings_file() as f: self.write_materials_file(meeting, pm_doc.document, f.read()) pm_url = ProceedingsMaterialFactory(meeting=meeting, document__external_url='https://example.com/first') success_url = urlreverse('ietf.meeting.views_proceedings.material_details', kwargs=dict(num=meeting.number)) self.assertNotEqual(pm_doc.type, pm_url.type) self.assertEqual(meeting.proceedings_materials.count(), 2) # Replace the uploaded document with another uploaded document pm_doc_url = urlreverse( 'ietf.meeting.views_proceedings.upload_material', kwargs=dict(num=meeting.number, material_type=pm_doc.type.slug), ) self.client.login(username='secretary', password='secretary+password') with self._proceedings_file() as fd: r = self.client.post(pm_doc_url, {'file': fd, 'external_url': ''}) self.assertRedirects(r, success_url) self.assertEqual(meeting.proceedings_materials.count(), 2) pm_doc = meeting.proceedings_materials.get(pk=pm_doc.pk) # refresh from DB self.assertEqual(pm_doc.document.rev, '01') self.assertEqual(pm_doc.get_href(), f'{pm_doc.document.name}:01') # Replace the uploaded document with a URL r = self.client.post(pm_doc_url, {'use_url': 'on', 'external_url': 'https://example.com/second'}) self.assertRedirects(r, success_url) self.assertEqual(meeting.proceedings_materials.count(), 2) pm_doc = meeting.proceedings_materials.get(pk=pm_doc.pk) # refresh from DB self.assertEqual(pm_doc.document.rev, '02') self.assertEqual(pm_doc.get_href(), 'https://example.com/second') # Now replace the URL doc with another URL pm_url_url = urlreverse( 'ietf.meeting.views_proceedings.upload_material', kwargs=dict(num=meeting.number, material_type=pm_url.type.slug), ) r = self.client.post(pm_url_url, {'use_url': 'on', 'external_url': 'https://example.com/third'}) self.assertRedirects(r, success_url) self.assertEqual(meeting.proceedings_materials.count(), 2) pm_url = meeting.proceedings_materials.get(pk=pm_url.pk) # refresh from DB self.assertEqual(pm_url.document.rev, '01') self.assertEqual(pm_url.get_href(), 'https://example.com/third') # Now replace the URL doc with an uploaded file with self._proceedings_file() as fd: r = self.client.post(pm_url_url, {'file': fd, 'external_url': ''}) self.assertRedirects(r, success_url) self.assertEqual(meeting.proceedings_materials.count(), 2) pm_url = meeting.proceedings_materials.get(pk=pm_url.pk) # refresh from DB self.assertEqual(pm_url.document.rev, '02') self.assertEqual(pm_url.get_href(), f'{pm_url.document.name}:02') def test_remove_proceedings_material(self): """Proceedings material can be removed""" meeting = self._procmat_test_meeting() pm = ProceedingsMaterialFactory(meeting=meeting) self.assertEqual(pm.active(), True) url = urlreverse( 'ietf.meeting.views_proceedings.remove_material', kwargs=dict(num=meeting.number, material_type=pm.type.slug), ) self.client.login(username='secretary', password='secretary+password') r = self.client.post(url) self.assertRedirects( r, urlreverse('ietf.meeting.views_proceedings.material_details', kwargs=dict(num=meeting.number)), ) pm = meeting.proceedings_materials.get(pk=pm.pk) self.assertEqual(pm.active(), False) def test_restore_proceedings_material(self): """Proceedings material can be removed""" meeting = self._procmat_test_meeting() pm = ProceedingsMaterialFactory(meeting=meeting, document__states=[('procmaterials', 'removed')]) self.assertEqual(pm.active(), False) url = urlreverse( 'ietf.meeting.views_proceedings.restore_material', kwargs=dict(num=meeting.number, material_type=pm.type.slug), ) self.client.login(username='secretary', password='secretary+password') r = self.client.post(url) self.assertRedirects( r, urlreverse('ietf.meeting.views_proceedings.material_details', kwargs=dict(num=meeting.number)), ) pm = meeting.proceedings_materials.get(pk=pm.pk) self.assertEqual(pm.active(), True) def test_rename_proceedings_material(self): """Proceedings material can be renamed""" meeting = self._procmat_test_meeting() pm = ProceedingsMaterialFactory(meeting=meeting) self.assertEqual(str(pm), pm.type.name) orig_rev = pm.document.rev url = urlreverse( 'ietf.meeting.views_proceedings.edit_material', kwargs=dict(num=meeting.number, material_type=pm.type.slug), ) self.client.login(username='secretary', password='secretary+password') r = self.client.post(url, {'title': 'This Is Not the Default Name'}) self.assertRedirects( r, urlreverse('ietf.meeting.views_proceedings.material_details', kwargs=dict(num=meeting.number)), ) pm = meeting.proceedings_materials.get(pk=pm.pk) self.assertEqual(str(pm), 'This Is Not the Default Name') self.assertEqual(pm.document.rev, orig_rev, 'Renaming should not change document revision') def test_create_recording(self): session = SessionFactory(meeting__type_id='ietf', meeting__number=72, group__acronym='mars') filename = 'ietf42-testroomt-20000101-0800.mp3' url = settings.IETF_AUDIO_URL + 'ietf{}/{}'.format(session.meeting.number, filename) doc = create_recording(session, url) self.assertEqual(doc.name,'recording-72-mars-1') self.assertEqual(doc.group,session.group) self.assertEqual(doc.external_url,url) self.assertTrue(doc in session.materials.all()) def test_get_next_sequence(self): session = SessionFactory(meeting__type_id='ietf', meeting__number=72, group__acronym='mars') meeting = session.meeting group = session.group sequence = get_next_sequence(group,meeting,'recording') self.assertEqual(sequence,1) def test_participants_for_meeting(self): person_a = PersonFactory() person_b = PersonFactory() person_c = PersonFactory() person_d = PersonFactory() m = MeetingFactory.create(type_id='ietf') MeetingRegistrationFactory(meeting=m, person=person_a, reg_type='onsite', checkedin=True) MeetingRegistrationFactory(meeting=m, person=person_b, reg_type='onsite', checkedin=False) MeetingRegistrationFactory(meeting=m, person=person_c, reg_type='remote') MeetingRegistrationFactory(meeting=m, person=person_d, reg_type='remote') AttendedFactory(session__meeting=m, session__type_id='plenary', person=person_c) checked_in, attended = participants_for_meeting(m) self.assertTrue(person_a.pk in checked_in) self.assertTrue(person_b.pk not in checked_in) self.assertTrue(person_c.pk in attended) self.assertTrue(person_d.pk not in attended) def test_session_attendance(self): meeting = MeetingFactory(type_id='ietf', date=datetime.date(2023, 11, 4), number='118') make_meeting_test_data(meeting=meeting) session = Session.objects.filter(meeting=meeting, group__acronym='mars').first() regs = MeetingRegistrationFactory.create_batch(3, meeting=meeting) persons = [reg.person for reg in regs] self.assertEqual(session.attended_set.count(), 0) # If there are no attendees, the link isn't offered, and getting # the page directly returns an empty list. session_url = urlreverse('ietf.meeting.views.session_details', kwargs={'num':meeting.number, 'acronym':session.group.acronym}) attendance_url = urlreverse('ietf.meeting.views.session_attendance', kwargs={'num':meeting.number, 'session_id':session.id}) r = self.client.get(session_url) self.assertNotContains(r, attendance_url) r = self.client.get(attendance_url) self.assertEqual(r.status_code, 200) self.assertContains(r, '0 attendees') # Add some attendees add_attendees_url = urlreverse('ietf.meeting.views.api_add_session_attendees') recmanrole = RoleFactory(group__type_id='ietf', name_id='recman', person__user__last_login=timezone.now()) recman = recmanrole.person apikey = PersonalApiKeyFactory(endpoint=add_attendees_url, person=recman) attendees = [person.user.pk for person in persons] self.client.login(username='recman', password='recman+password') r = self.client.post(add_attendees_url, {'apikey':apikey.hash(), 'attended':f'{{"session_id":{session.pk},"attendees":{attendees}}}'}) self.assertEqual(r.status_code, 200) self.assertEqual(session.attended_set.count(), 3) # Before a meeting is finalized, session_attendance renders a live # view of the Attended records for the session. r = self.client.get(session_url) self.assertContains(r, attendance_url) r = self.client.get(attendance_url) self.assertEqual(r.status_code, 200) self.assertContains(r, '3 attendees') for person in persons: self.assertContains(r, person.plain_name()) # Test for the "I was there" button. def _test_button(person, expected): username = person.user.username self.client.login(username=username, password=f'{username}+password') r = self.client.get(attendance_url) self.assertEqual(b"I was there" in r.content, expected) # recman isn't registered for the meeting _test_button(recman, False) # person0 is already on the bluesheet _test_button(persons[0], False) # person3 attests he was there persons.append(MeetingRegistrationFactory(meeting=meeting).person) # button isn't shown if we're outside the corrections windows meeting.importantdate_set.create(name_id='revsub',date=date_today() - datetime.timedelta(days=20)) _test_button(persons[3], False) # attempt to POST anyway is ignored r = self.client.post(attendance_url) self.assertEqual(r.status_code, 200) self.assertNotContains(r, persons[3].plain_name()) self.assertEqual(session.attended_set.count(), 3) # button is shown, and POST is accepted meeting.importantdate_set.update(name_id='revsub',date=date_today() + datetime.timedelta(days=20)) _test_button(persons[3], True) r = self.client.post(attendance_url) self.assertEqual(r.status_code, 200) self.assertContains(r, persons[3].plain_name()) self.assertEqual(session.attended_set.count(), 4) # When the meeting is finalized, a bluesheet file is generated, # and session_attendance redirects to the file. self.client.login(username='secretary',password='secretary+password') finalize_url = urlreverse('ietf.meeting.views.finalize_proceedings', kwargs={'num':meeting.number}) r = self.client.post(finalize_url, {'finalize':1}) self.assertRedirects(r, urlreverse('ietf.meeting.views.proceedings', kwargs={'num':meeting.number})) doc = session.presentations.filter(document__type_id='bluesheets').first().document self.assertEqual(doc.rev,'00') text = doc.text() self.assertIn('4 attendees', text) for person in persons: self.assertIn(person.plain_name(), text) r = self.client.get(session_url) self.assertContains(r, doc.get_href()) self.assertNotContains(r, attendance_url) r = self.client.get(attendance_url) self.assertEqual(r.status_code,302) self.assertEqual(r['Location'],doc.get_href()) # An interim meeting is considered finalized immediately. meeting = make_interim_meeting(group=GroupFactory(acronym='mars'), date=date_today()) session = Session.objects.filter(meeting=meeting, group__acronym='mars').first() attendance_url = urlreverse('ietf.meeting.views.session_attendance', kwargs={'num':meeting.number, 'session_id':session.id}) self.assertEqual(session.attended_set.count(), 0) self.client.login(username='recman', password='recman+password') attendees = [person.user.pk for person in persons] r = self.client.post(add_attendees_url, {'apikey':apikey.hash(), 'attended':f'{{"session_id":{session.pk},"attendees":{attendees}}}'}) self.assertEqual(r.status_code, 200) self.assertEqual(session.attended_set.count(), 4) doc = session.presentations.filter(document__type_id='bluesheets').first().document self.assertEqual(doc.rev,'00') session_url = urlreverse('ietf.meeting.views.session_details', kwargs={'num':meeting.number, 'acronym':session.group.acronym}) r = self.client.get(session_url) self.assertContains(r, doc.get_href()) self.assertNotContains(r, attendance_url) r = self.client.get(attendance_url) self.assertEqual(r.status_code,302) self.assertEqual(r['Location'],doc.get_href()) def test_bluesheet_data(self): session = SessionFactory(meeting__type_id="ietf") attended_with_affil = MeetingRegistrationFactory(meeting=session.meeting, affiliation="Somewhere") AttendedFactory(session=session, person=attended_with_affil.person, time="2023-03-13T01:24:00Z") # joined 2nd attended_no_affil = MeetingRegistrationFactory(meeting=session.meeting) AttendedFactory(session=session, person=attended_no_affil.person, time="2023-03-13T01:23:00Z") # joined 1st MeetingRegistrationFactory(meeting=session.meeting) # did not attend data = bluesheet_data(session) self.assertEqual( data, [ {"name": attended_no_affil.person.plain_name(), "affiliation": ""}, {"name": attended_with_affil.person.plain_name(), "affiliation": "Somewhere"}, ] )