# Copyright The IETF Trust 2009-2020, All Rights Reserved # -*- coding: utf-8 -*- import datetime import io import json import os import random import re import shutil import pytz from unittest import skipIf from mock import 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 django.urls import reverse as urlreverse from django.conf import settings from django.contrib.auth.models import User 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.text import slugify from django.utils.timezone import now import debug # pyflakes:ignore from ietf.doc.models import Document 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_view_interim_request from ietf.meeting.helpers import send_interim_approval_request 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 finalize, condition_slide_order from ietf.meeting.utils import add_event_info_to_session_qs from ietf.meeting.views import session_draft_list, parse_agenda_filter_params 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.text import xslugify from ietf.person.factories import PersonFactory from ietf.group.factories import GroupFactory, GroupEventFactory, RoleFactory from ietf.meeting.factories import ( SessionFactory, ScheduleFactory, SessionPresentationFactory, MeetingFactory, FloorPlanFactory, TimeSlotFactory, SlideSubmissionFactory, RoomFactory, ConstraintFactory, MeetingHostFactory, ProceedingsMaterialFactory ) 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): path = os.path.join(self.materials_dir, "%s/%s/%s" % (meeting.number, doc.type_id, doc.uploaded_filename)) dirname = os.path.dirname(path) if not os.path.exists(dirname): os.makedirs(dirname) if isinstance(content, str): content = content.encode() 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 MeetingTests(BaseMeetingTestCase): def test_meeting_agenda(self): meeting = make_meeting_test_data() session = Session.objects.filter(meeting=meeting, group__acronym="mars").first() slot = TimeSlot.objects.get(sessionassignments__session=session,sessionassignments__schedule=meeting.schedule) # self.write_materials_files(meeting, session) # future_year = datetime.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" # utc time_interval = "%s-%s" % (slot.utc_start_time().strftime("%H:%M").lstrip("0"), (slot.utc_start_time() + slot.duration).strftime("%H:%M").lstrip("0")) r = self.client.get(urlreverse("ietf.meeting.views.agenda", kwargs=dict(num=meeting.number,utc='-utc'))) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) agenda_content = q("#content").html() self.assertIn(session.group.acronym, agenda_content) self.assertIn(session.group.name, agenda_content) self.assertIn(session.group.parent.acronym.upper(), agenda_content) self.assertIn(slot.location.name, agenda_content) self.assertIn(time_interval, agenda_content) self.assertIsNotNone(q(':input[value="%s"]' % meeting.time_zone), 'Time zone selector should show meeting timezone') self.assertIsNotNone(q('.nav *:contains("%s")' % meeting.time_zone), 'Time zone indicator should be in nav sidebar') # plain time_interval = "%s-%s" % (slot.time.strftime("%H:%M").lstrip("0"), (slot.time + slot.duration).strftime("%H:%M").lstrip("0")) r = self.client.get(urlreverse("ietf.meeting.views.agenda", kwargs=dict(num=meeting.number))) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) agenda_content = q("#content").html() self.assertIn(session.group.acronym, agenda_content) self.assertIn(session.group.name, agenda_content) self.assertIn(session.group.parent.acronym.upper(), agenda_content) self.assertIn(slot.location.name, agenda_content) self.assertIn(time_interval, agenda_content) self.assertIn(registration_text, agenda_content) # Make sure there's a frame for the session agenda and it points to the right place assignment_url = urlreverse('ietf.meeting.views.session_materials', kwargs=dict(session_id=session.pk)) self.assertTrue( any( [assignment_url in x.attrib["data-src"] for x in q('tr div.modal-body div.session-materials')] ) ) # future meeting, no agenda r = self.client.get(urlreverse("ietf.meeting.views.agenda", kwargs=dict(num=future_meeting.number))) self.assertContains(r, "There is no agenda available yet.") self.assertTemplateUsed(r, 'meeting/no-agenda.html') # text # the rest of the results don't have as nicely formatted times time_interval = time_interval.replace(":", "") r = self.client.get(urlreverse("ietf.meeting.views.agenda", 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, time_interval) r = self.client.get(urlreverse("ietf.meeting.views.agenda", kwargs=dict(num=meeting.number,name=meeting.unofficial_schedule.name,owner=meeting.unofficial_schedule.owner.email()))) self.assertContains(r, 'not the official schedule') # future meeting, no agenda r = self.client.get(urlreverse("ietf.meeting.views.agenda", 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", 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) 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 r = self.client.get(urlreverse("ietf.meeting.views.agenda_ical", kwargs=dict(num=meeting.number)) + "?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, slot.location.name) 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') self.assertContains( r, urlreverse( 'ietf.meeting.views.agenda', kwargs=dict(num=meeting.number) ) + f'#row-{session.official_timeslotassignment().slug()}', msg_prefix='ical should contain link to agenda entry for session') # week view r = self.client.get(urlreverse("ietf.meeting.views.week_view", kwargs=dict(num=meeting.number))) self.assertNotContains(r, 'CANCELLED') self.assertContains(r, session.group.acronym) self.assertContains(r, slot.location.name) self.assertContains(r, registration_text) # week view with a cancelled session SchedulingEvent.objects.create( session=session, status=SessionStatusName.objects.get(slug='canceled'), by=Person.objects.get(name='(System)') ) r = self.client.get(urlreverse("ietf.meeting.views.week_view", kwargs=dict(num=meeting.number))) self.assertContains(r, 'CANCELLED') self.assertContains(r, session.group.acronym) self.assertContains(r, slot.location.name) def test_meeting_agenda_filters_ignored(self): """The agenda view should ignore filter querystrings (They are handled by javascript on the front end) """ meeting = make_meeting_test_data() expected_items = meeting.schedule.assignments.exclude(timeslot__type__in=['lead','offagenda']) expected_rows = ['row-%s' % item.slug() for item in expected_items] r = self.client.get(urlreverse('ietf.meeting.views.agenda')) for row_id in expected_rows: self.assertContains(r, row_id) r = self.client.get(urlreverse('ietf.meeting.views.agenda') + '?show=mars') for row_id in expected_rows: self.assertContains(r, row_id) r = self.client.get(urlreverse('ietf.meeting.views.agenda') + '?show=mars&hide=ames,mars,plenary,ietf,bof') for row_id in expected_rows: self.assertContains(r, row_id) def test_agenda_iab_session(self): date = datetime.date.today() meeting = MeetingFactory(type_id='ietf', date=date ) make_meeting_test_data(meeting=meeting) iab = Group.objects.get(acronym='iab') venus = Group.objects.create( name="Three letter acronym", acronym="venus", description="This group discusses exploration of Venus", state_id="active", type_id="program", parent=iab, list_email="venus@ietf.org", ) venus_session = SessionFactory( meeting=meeting, group=venus, attendees=10, requested_duration=datetime.timedelta(minutes=60), add_to_schedule=False, ) system_person = Person.objects.get(name="(System)") SchedulingEvent.objects.create(session=venus_session, status_id='schedw', by=system_person) room = Room.objects.create(meeting=meeting, name="Aphrodite", capacity=100, functional_name="Aphrodite Room") room.session_types.add('regular') session_date = meeting.date + datetime.timedelta(days=1) slot3 = TimeSlot.objects.create(meeting=meeting, type_id='regular', location=room, duration=datetime.timedelta(minutes=60), time=datetime.datetime.combine(session_date, datetime.time(13, 30))) SchedTimeSessAssignment.objects.create(timeslot=slot3, session=venus_session, schedule=meeting.schedule) url = urlreverse('ietf.meeting.views.agenda', kwargs=dict(num=meeting.number)) r = self.client.get(url) self.assertContains(r, 'venus') q = PyQuery(r.content) venus_row = q('[id*="-iab-"]').html() self.assertIn('venus', venus_row) def test_agenda_current_audio(self): date = datetime.date.today() meeting = MeetingFactory(type_id='ietf', date=date ) make_meeting_test_data(meeting=meeting) url = urlreverse("ietf.meeting.views.agenda", kwargs=dict(num=meeting.number)) r = self.client.get(url) self.assertContains(r, "Audio stream") def test_agenda_by_room(self): meeting = make_meeting_test_data() url = urlreverse("ietf.meeting.views.agenda_by_room",kwargs=dict(num=meeting.number)) login_testing_unauthorized(self,"secretary",url) r = self.client.get(url) self.assertTrue(all([x in unicontent(r) for x in ['mars','IESG Breakfast','Test Room','Breakfast Room']])) url = urlreverse("ietf.meeting.views.agenda_by_room",kwargs=dict(num=meeting.number,name=meeting.unofficial_schedule.name,owner=meeting.unofficial_schedule.owner.email())) r = self.client.get(url) self.assertTrue(all([x in unicontent(r) for x in ['mars','Test Room',]])) self.assertNotContains(r, 'IESG Breakfast') def test_agenda_by_type(self): meeting = make_meeting_test_data() url = urlreverse("ietf.meeting.views.agenda_by_type",kwargs=dict(num=meeting.number)) login_testing_unauthorized(self,"secretary",url) r = self.client.get(url) self.assertTrue(all([x in unicontent(r) for x in ['mars','IESG Breakfast','Test Room','Breakfast Room']])) url = urlreverse("ietf.meeting.views.agenda_by_type",kwargs=dict(num=meeting.number,name=meeting.unofficial_schedule.name,owner=meeting.unofficial_schedule.owner.email())) r = self.client.get(url) self.assertTrue(all([x in unicontent(r) for x in ['mars','Test Room',]])) self.assertNotContains(r, 'IESG Breakfast') url = urlreverse("ietf.meeting.views.agenda_by_type",kwargs=dict(num=meeting.number,type='regular')) r = self.client.get(url) self.assertTrue(all([x in unicontent(r) for x in ['mars','Test Room']])) self.assertFalse(any([x in unicontent(r) for x in ['IESG Breakfast','Breakfast Room']])) url = urlreverse("ietf.meeting.views.agenda_by_type",kwargs=dict(num=meeting.number,type='lead')) r = self.client.get(url) self.assertFalse(any([x in unicontent(r) for x in ['mars','Test Room']])) self.assertTrue(all([x in unicontent(r) for x in ['IESG Breakfast','Breakfast Room']])) url = urlreverse("ietf.meeting.views.agenda_by_type",kwargs=dict(num=meeting.number,type='lead',name=meeting.unofficial_schedule.name,owner=meeting.unofficial_schedule.owner.email())) r = self.client.get(url) self.assertFalse(any([x in unicontent(r) for x in ['IESG Breakfast','Breakfast Room']])) def test_agenda_room_view(self): meeting = make_meeting_test_data() url = urlreverse("ietf.meeting.views.room_view",kwargs=dict(num=meeting.number)) login_testing_unauthorized(self,"secretary",url) r = self.client.get(url) self.assertEqual(r.status_code,200) self.assertTrue(all([x in unicontent(r) for x in ['mars','IESG Breakfast','Test Room','Breakfast Room']])) url = urlreverse("ietf.meeting.views.room_view",kwargs=dict(num=meeting.number,name=meeting.unofficial_schedule.name,owner=meeting.unofficial_schedule.owner.email())) r = self.client.get(url) self.assertTrue(all([x in unicontent(r) for x in ['mars','Test Room','Breakfast Room']])) self.assertNotContains(r, 'IESG Breakfast') def test_agenda_week_view(self): meeting = make_meeting_test_data() url = urlreverse("ietf.meeting.views.week_view",kwargs=dict(num=meeting.number)) + "?show=farfut" r = self.client.get(url) self.assertEqual(r.status_code,200) self.assertTrue(all([x in unicontent(r) for x in ['redraw_weekview', 'draw_calendar', ]])) # Specifying a time zone should not change the output (time zones are handled by the JS) url = urlreverse("ietf.meeting.views.week_view",kwargs=dict(num=meeting.number)) + "?show=farfut&tz=Asia/Bangkok" r_with_tz = self.client.get(url) self.assertEqual(r_with_tz.status_code,200) self.assertEqual(r.content, r_with_tz.content) def test_agenda_personalize(self): """Session selection page should have a checkbox for each session with appropriate keywords""" meeting = make_meeting_test_data() url = urlreverse("ietf.meeting.views.agenda_personalize",kwargs=dict(num=meeting.number)) r = self.client.get(url) self.assertEqual(r.status_code,200) q = PyQuery(r.content) for assignment in SchedTimeSessAssignment.objects.filter( schedule__in=[meeting.schedule, meeting.schedule.base], session__on_agenda=True, ): row = q('#row-{}'.format(assignment.slug())) self.assertIsNotNone(row, 'No row for assignment {}'.format(assignment)) checkboxes = row('input[type="checkbox"][name="selected-sessions"]') self.assertEqual(len(checkboxes), 1, 'Row for assignment {} does not have a checkbox input'.format(assignment)) checkbox = checkboxes.eq(0) kw_token = assignment.session.docname_token_only_for_multiple() self.assertEqual( checkbox.attr('data-filter-item'), assignment.session.group.acronym.lower() + ( '' if kw_token is None else f'-{kw_token}' ) ) def test_agenda_personalize_updates_urls(self): """The correct URLs should be updated when filter settings change on the personalize agenda view Tests that the expected elements have the necessary classes. The actual update of these fields is tested in the JS tests """ meeting = make_meeting_test_data() url = urlreverse("ietf.meeting.views.agenda_personalize",kwargs=dict(num=meeting.number)) r = self.client.get(url) self.assertEqual(r.status_code,200) q = PyQuery(r.content) # Find all the elements expected to be updated expected_elements = [] nav_tab_anchors = q('ul.nav.nav-tabs > li > a') for anchor in nav_tab_anchors.items(): text = anchor.text().strip() if text in ['Agenda', 'UTC agenda', 'Personalize agenda']: expected_elements.append(anchor) for btn in q('.buttonlist a.btn').items(): text = btn.text().strip() if text in ['View personal agenda', 'Download .ics of personal agenda', 'Subscribe to personal agenda']: expected_elements.append(btn) # Check that all the expected elements have the correct classes for elt in expected_elements: self.assertTrue(elt.has_class('agenda-link')) self.assertTrue(elt.has_class('filterable')) # Finally, check that there are no unexpected elements marked to be updated. # If there are, they should be added to the test above. self.assertEqual(len(expected_elements), len(q('.agenda-link.filterable')), 'Unexpected elements updated') @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.sessionpresentation_set.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.sessionpresentation_set.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.sessionpresentation_set.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 = datetime.datetime.today() - 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) @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 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 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_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): sp = SessionPresentationFactory(document__name='slides-junk-15',document__type_id='slides',document__states=[('reuse_policy','single')]) sp.document.uploaded_filename = '%s-%s.pdf'%(sp.document.name,sp.document.rev) sp.document.save() self.write_materials_file(sp.session.meeting, sp.document, 'Fake slide contents') url = urlreverse("ietf.meeting.views.materials_document", kwargs=dict(document=sp.document.name,num=sp.session.meeting.number)) r = self.client.get(url) self.assertEqual(r.status_code, 200) 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()) 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=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.time.strftime('%Y%m%dT%H%M%S')) self.assertContains(r, t2.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.time.strftime('%Y%m%dT%H%M%S')) self.assertNotContains(r, t2.time.strftime('%Y%m%dT%H%M%S')) def test_meeting_agenda_has_static_ical_links(self): """Links to the agenda_ical view must appear on the agenda page Confirms that these have the correct querystrings. Does not test the JS-based 'Customized schedule' button. """ meeting = make_meeting_test_data() # get the agenda url = urlreverse('ietf.meeting.views.agenda', kwargs=dict(num=meeting.number)) r = self.client.get(url) # Check that it has the links we expect ical_url = urlreverse('ietf.meeting.views.agenda_ical', kwargs=dict(num=meeting.number)) q = PyQuery(r.content) content = q('#content').html() assignments = meeting.schedule.assignments.exclude(timeslot__type__in=['lead', 'offagenda']) # Assume the test meeting is not using historic groups groups = [a.session.group for a in assignments if a.session is not None] for g in groups: if g.parent_id is not None: self.assertIn('%s?show=%s' % (ical_url, g.parent.acronym.lower()), content) # The 'non-area events' are those whose keywords are in the last column of buttons na_col = q('#customize .col-1:last') # find the column non_area_labels = [e.attrib['data-filter-item'] for e in na_col.find('button.pickview')] assert len(non_area_labels) > 0 # test setup must produce at least one label for this test # Should be a 'non-area events' link showing appropriate types self.assertIn('%s?show=%s' % (ical_url, ','.join(non_area_labels).lower()), content) 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.sessionpresentation_set.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.sessionpresentation_set.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=d.name,format='txt',templatename='test_submission.txt',group=session.group,rev="00") 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() 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') 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() 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') 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=datetime.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':schedule.name, 'visible':True, 'public':True, 'notes': "New Notes", 'base': new_base.pk, } ) self.assertNoFormPostErrors(response) 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) 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=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=datetime.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 t0 = datetime.datetime.combine(meeting.date, datetime.time(11, 0)) 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.time_zone) 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.time_zone) yesterday = (right_now - datetime.timedelta(days=1)).date() day_before = (right_now - datetime.timedelta(days=2)).date() for room in room_groups[0]: ts = room.timeslot_set.last() ts.time = datetime.datetime.combine(yesterday, ts.time.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 = datetime.datetime.combine(day_before, ts.time.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(tzname): right_now = now().astimezone(pytz.timezone(tzname)) if not settings.USE_TZ: right_now = right_now.replace(tzinfo=None) return right_now def test_assign_session(self): """Allow assignment to future timeslots only for official schedule""" meeting = MeetingFactory( type_id='ietf', date=(datetime.datetime.today() - datetime.timedelta(days=1)).date(), days=3, ) right_now = self._right_now_in(meeting.time_zone) 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=(datetime.datetime.today() - datetime.timedelta(days=1)).date(), days=3, ) right_now = self._right_now_in(meeting.time_zone) 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=(datetime.datetime.today() - datetime.timedelta(days=1)).date(), days=3, ) right_now = self._right_now_in(meeting.time_zone) 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']) 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=datetime.datetime.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) return meeting 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).date() 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=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=datetime.datetime.combine( meeting.get_meeting_date(day).date(), datetime.time(hour=11) ), ) TimeSlotFactory( meeting=meeting, location=meeting.room_set.first(), time=datetime.datetime.combine( meeting.get_meeting_date(day).date(), 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_before = datetime.datetime.combine( meeting.date, datetime.time(hour=10), ) 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() name_after = 'New Name (tm)' type_after = 'plenary' time_after = time_before + datetime.timedelta(days=1, hours=2) duration_after = duration_before * 2 show_location_after = False location_after = meeting.room_set.last() r = self.client.post( self.edit_timeslot_url(ts), 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, ) ) 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) def test_invalid_edit_timeslot(self): meeting = self.create_bare_meeting() ts: TimeSlot = TimeSlotFactory(meeting=meeting, name='slot') # n.b., colon indicates type hinting 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()) 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() 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(str(ts.time.date().toordinal()), post_data['days']) self.assertEqual(ts.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_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).date() 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.time.date(), other_date) self.assertEqual(ts.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).date() for n in range(meeting.days)] other_date = meeting.get_meeting_date(-1).date() # 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.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.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_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): def test_add_slides_to_session(self): for type_id in ('ietf','interim'): chair_role = RoleFactory(name_id='chair') session = SessionFactory(group=chair_role.group, meeting__date=datetime.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.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)) session.meeting.date = datetime.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']) 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']) 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']) 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']) 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']) # 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']) 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']) # 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.sessionpresentation_set.count(),1) # Ingore 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.sessionpresentation_set.count(),1) session2 = SessionFactory(group=session.group, meeting=session.meeting) SessionPresentationFactory.create_batch(3, document__type_id='slides', session=session2) for num, sp in enumerate(session2.sessionpresentation_set.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.sessionpresentation_set.get(document=more_slides[0]).order,1) self.assertEqual(list(session2.sessionpresentation_set.order_by('order').values_list('order',flat=True)), list(range(1,5))) # 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.sessionpresentation_set.get(document=more_slides[1]).order,5) self.assertEqual(list(session2.sessionpresentation_set.order_by('order').values_list('order',flat=True)), list(range(1,6))) # 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.sessionpresentation_set.get(document=more_slides[2]).order,3) self.assertEqual(list(session2.sessionpresentation_set.order_by('order').values_list('order',flat=True)), list(range(1,7))) def test_remove_slides_from_session(self): for type_id in ['ietf','interim']: chair_role = RoleFactory(name_id='chair') session = SessionFactory(group=chair_role.group, meeting__date=datetime.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.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)) session.meeting.date = datetime.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']) 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']) 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']) 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']) # 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']) session.sessionpresentation_set.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']) 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']) 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']) session.sessionpresentation_set.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']) # 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.sessionpresentation_set.count(),1) 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.sessionpresentation_set.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.sessionpresentation_set.filter(pk=sp_list[0].pk).exists()) self.assertEqual(list(session2.sessionpresentation_set.order_by('order').values_list('order',flat=True)), list(range(1,5))) # 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.sessionpresentation_set.filter(pk=sp_list[4].pk).exists()) self.assertEqual(list(session2.sessionpresentation_set.order_by('order').values_list('order',flat=True)), list(range(1,4))) # 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.sessionpresentation_set.filter(pk=sp_list[2].pk).exists()) self.assertEqual(list(session2.sessionpresentation_set.order_by('order').values_list('order',flat=True)), list(range(1,3))) def test_reorder_slides_in_session(self): chair_role = RoleFactory(name_id='chair') session = SessionFactory(group=chair_role.group, meeting__date=datetime.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 = 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 = datetime.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.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)) session.meeting.date = datetime.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']) 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']) 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']) # 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.sessionpresentation_set.order_by('order').values_list('pk',flat=True)),[2,3,1,4,5]) # 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.sessionpresentation_set.order_by('order').values_list('pk',flat=True)),[1,2,3,4,5]) # 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.sessionpresentation_set.order_by('order').values_list('pk',flat=True)),[1,2,5,3,4]) # 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.sessionpresentation_set.order_by('order').values_list('pk',flat=True)),[1,2,3,4,5]) # 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.sessionpresentation_set.order_by('order').values_list('pk',flat=True)),[2,3,4,5,1]) # 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.sessionpresentation_set.order_by('order').values_list('pk',flat=True)),[2,3,5,4,1]) 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.sessionpresentation_set.order_by('order').values_list('pk',flat=True)),[2,5,3,4,1]) # Reset for next iteration in the loop session.sessionpresentation_set.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=datetime.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.sessionpresentation_set.order_by('order').values_list('order',flat=True)),list(range(1,6))) class EditTests(TestCase): """Test schedule edit operations""" def setUp(self): super().setUp() # make sure we have the colors of the area from ietf.group.colors import fg_group_colors, bg_group_colors area_upper = "FARFUT" fg_group_colors[area_upper] = "#333" bg_group_colors[area_upper] = "#aaa" def test_official_record_schedule_is_read_only(self): def _set_date_offset_and_retrieve_page(meeting, days_offset, client): meeting.date = datetime.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=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)) # 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-person").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, 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, 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, '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, '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, '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 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) r = self.client.post(url, { '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', }) self.assertNoFormPostErrors(r) self.assertRedirects(r, urlreverse('ietf.meeting.views.edit_meeting_schedule', kwargs={'num': session.meeting.number})) 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') 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) 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=datetime.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') q = PyQuery(r.content) self.assertTrue(q('div#session-buttons-%s' % session.id), 'Session detail page does not contain session tool buttons') self.assertFalse(q('div#session-buttons-%s span.bi-arrows-fullscreen' % session.id), 'The session detail page is incorrectly showing the "Show meeting materials" button') 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=datetime.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=datetime.date.today()+datetime.timedelta(days=90)) SessionPresentationFactory.create(session=session,document__type_id='draft',rev=None) old_draft = session.sessionpresentation_set.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.sessionpresentation_set.count()) r = self.client.post(url,dict(drafts=[new_draft.pk,])) self.assertTrue(r.status_code, 302) self.assertEqual(2,session.sessionpresentation_set.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) 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 = datetime.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() session = Session.objects.with_current_status().filter( meeting__type='interim', group__acronym='mars', current_status='apprw').first() meeting = session.meeting meeting.time_zone = 'America/Los_Angeles' meeting.save() 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.assertIn( '(%s to %s UTC)' % ( timeslot.utc_start_time().strftime('%H:%M'),timeslot.utc_end_time().strftime('%H:%M') ), announcement_text) # 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 = datetime.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 = datetime.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") r = self.client.get(url) self.assertEqual(r.status_code, 200) # Expect events 3 sessions - one for each WG and one for the IETF meeting assert_ical_response_is_valid(self, r, expected_event_summaries=[ 'ames - Asteroid Mining Equipment Standardization Group', 'mars - Martian Special Interest Group', 'IETF 72', ], expected_event_count=3) def test_upcoming_ical_filter(self): # 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', 'IETF 72', ], expected_event_count=2) 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) Group.objects.filter(type_id__in=GroupFeatures.objects.filter(has_meetings=True).values_list('type_id',flat=True), state__in=('active', 'proposed', 'bof')) self.assertEqual(Group.objects.filter(type_id__in=GroupFeatures.objects.filter(has_meetings=True).values_list('type_id',flat=True), 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 = datetime.date.today() + datetime.timedelta(days=30) time = datetime.datetime.now().time().replace(microsecond=0,second=0) dt = 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} 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.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 = datetime.date.today() + datetime.timedelta(days=30) time = datetime.datetime.now().time().replace(microsecond=0,second=0) dt = datetime.datetime.combine(date, time) duration = datetime.timedelta(hours=3) city = 'San Francisco' country = 'US' time_zone = 'America/Los_Angeles' 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} 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.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 = datetime.date.today() + datetime.timedelta(days=30) date2 = date + datetime.timedelta(days=1) time = datetime.datetime.now().time().replace(microsecond=0,second=0) dt = datetime.datetime.combine(date, time) dt2 = datetime.datetime.combine(date2, time) duration = datetime.timedelta(hours=3) 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' 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} 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.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 = datetime.date.today() + datetime.timedelta(days=30) date2 = date + datetime.timedelta(days=2) time = datetime.datetime.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 = datetime.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 = datetime.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 = datetime.datetime.now().time().replace(microsecond=0,second=0) dt = datetime.datetime.combine(date, time) dt2 = datetime.datetime.combine(date2, time) duration = datetime.timedelta(hours=3) group = Group.objects.get(acronym='mars') city = '' country = '' time_zone = 'America/Los_Angeles' 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} r = self.client.post(urlreverse("ietf.meeting.views.interim_request"),data) 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 = 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.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 = datetime.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) def test_interim_request_cancel(self): """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) # test cancelling before announcement self.client.login(username="marschairman", password="marschairman+password") 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 # test cancelling after announcement 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']) def test_interim_request_session_cancel(self): """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) # 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) # 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})) # 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 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) # 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})) # 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 new_time = formset_initial['time'] + 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':new_time.strftime('%H:%M'), '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,new_time) 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_time = formset_initial['time'] + datetime.timedelta(hours=1) 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':new_time.strftime('%H:%M'), '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,new_time) 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_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 = datetime.datetime.today() - 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() a1 = s1.official_timeslotassignment() t1 = a1.timeslot # Create an extra session t2 = TimeSlotFactory.create(meeting=meeting, time=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.time.strftime('%Y%m%dT%H%M%S')) self.assertContains(r, t2.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.assertNotContains(r, t2.time.strftime('%Y%m%dT%H%M%S')) self.assertContains(r, 'END:VEVENT') class AjaxTests(TestCase): def test_ajax_get_utc(self): # test bad queries url = urlreverse('ietf.meeting.views.ajax_get_utc') + "?date=2016-1-1&time=badtime&timezone=UTC" r = self.client.get(url) self.assertEqual(r.status_code, 200) data = r.json() self.assertEqual(data["error"], True) url = urlreverse('ietf.meeting.views.ajax_get_utc') + "?date=2016-1-1&time=25:99&timezone=UTC" r = self.client.get(url) self.assertEqual(r.status_code, 200) data = r.json() self.assertEqual(data["error"], True) url = urlreverse('ietf.meeting.views.ajax_get_utc') + "?date=2016-1-1&time=10:00am&timezone=UTC" r = self.client.get(url) self.assertEqual(r.status_code, 200) data = r.json() self.assertEqual(data["error"], True) # test good query url = urlreverse('ietf.meeting.views.ajax_get_utc') + "?date=2016-1-1&time=12:00&timezone=America/Los_Angeles" r = self.client.get(url) self.assertEqual(r.status_code, 200) data = r.json() self.assertIn('timezone', data) self.assertIn('time', data) self.assertIn('utc', data) self.assertNotIn('error', data) self.assertEqual(data['utc'], '20:00') class FloorPlanTests(TestCase): def test_floor_plan_page(self): make_meeting_test_data() meeting = Meeting.objects.filter(type_id='ietf').order_by('id').last() floorplan = FloorPlanFactory.create(meeting=meeting) url = urlreverse('ietf.meeting.views.floor_plan') r = self.client.get(url) self.assertEqual(r.status_code, 200) url = urlreverse('ietf.meeting.views.floor_plan', kwargs={'floor': xslugify(floorplan.name)} ) r = self.client.get(url) self.assertEqual(r.status_code, 200) 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): @patch('urllib.request.urlopen') def test_finalize_proceedings(self, mock_urlopen): mock_urlopen.return_value = BytesIO(b'[{"LastName":"Smith","FirstName":"John","Company":"ABC","Country":"US"}]') make_meeting_test_data() meeting = Meeting.objects.filter(type_id='ietf').order_by('id').last() meeting.session_set.filter(group__acronym='mars').first().sessionpresentation_set.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().sessionpresentation_set.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().sessionpresentation_set.filter(document__type="draft").first().rev,'00') 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.sessionpresentation_set.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.sessionpresentation_set.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.sessionpresentation_set.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.sessionpresentation_set.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 = datetime.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.sessionpresentation_set.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_file = BytesIO(b'this is some text for a test') test_file.name = "not_really.json" r = self.client.post(url,dict(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(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(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(file=test_file)) self.assertEqual(r.status_code, 302) doc = session.sessionpresentation_set.filter(document__type_id=doctype).first().document self.assertEqual(doc.rev,'00') text = doc.text() self.assertIn('Some text', text) self.assertNotIn('
    ', text) self.assertIn('charset="utf-8"', 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(file=test_file,apply_to_all=False)) self.assertEqual(r.status_code, 302) doc = session.sessionpresentation_set.filter(document__type_id=doctype).first().document self.assertEqual(doc.rev,'01') self.assertFalse(session2.sessionpresentation_set.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(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.sessionpresentation_set.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(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 = url=urlreverse('ietf.meeting.views.session_details', kwargs={'num':session.meeting.number, 'acronym': session.group.acronym}) top = '/meeting/%s/' % session.meeting.number 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.sessionpresentation_set.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(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.sessionpresentation_set.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(file=test_file)) self.assertEqual(r.status_code, 302) doc = session.sessionpresentation_set.filter(document__type_id=doctype).first().document self.assertEqual(doc.rev,'00') # Verify that we don't have dead links url = url=urlreverse('ietf.meeting.views.session_details', kwargs={'num':session.meeting.number, 'acronym': session.group.acronym}) top = '/meeting/%s/' % session.meeting.number self.crawl_materials(url=url, top=top) def test_upload_slides(self): 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) q = PyQuery(r.content) self.assertIn('Upload', str(q("title"))) self.assertFalse(session1.sessionpresentation_set.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)) self.assertEqual(r.status_code, 302) self.assertEqual(session1.sessionpresentation_set.count(),1) self.assertEqual(session2.sessionpresentation_set.count(),1) sp = session2.sessionpresentation_set.first() self.assertEqual(sp.document.name, 'slides-%s-%s-a-test-slide-file' % (session1.meeting.number,session1.group.acronym ) ) self.assertEqual(sp.order,1) 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)) self.assertEqual(r.status_code, 302) self.assertEqual(session1.sessionpresentation_set.count(),1) self.assertEqual(session2.sessionpresentation_set.count(),2) sp = session2.sessionpresentation_set.get(document__name__endswith='-a-different-slide-file') self.assertEqual(sp.order,2) self.assertEqual(sp.rev,'00') self.assertEqual(sp.document.rev,'00') url = urlreverse('ietf.meeting.views.upload_session_slides',kwargs={'num':session2.meeting.number,'session_id':session2.id,'name':session2.sessionpresentation_set.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)) self.assertEqual(r.status_code, 302) self.assertEqual(session1.sessionpresentation_set.count(),1) self.assertEqual(session2.sessionpresentation_set.count(),2) sp = session2.sessionpresentation_set.get(order=2) self.assertEqual(sp.rev,'01') self.assertEqual(sp.document.rev,'01') 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.sessionpresentation_set.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()) def test_remove_sessionpresentation(self): session = SessionFactory(meeting__type_id='ietf') doc = DocumentFactory(type_id='slides') session.sessionpresentation_set.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) 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) 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.assertEqual(1,session.sessionpresentation_set.count()) response = self.client.post(url,{'remove_session':''}) self.assertEqual(response.status_code, 302) self.assertEqual(0,session.sessionpresentation_set.count()) self.assertEqual(2,doc.docevent_set.count()) 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=datetime.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}) propose_url = urlreverse('ietf.meeting.views.propose_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,propose_url) r = self.client.get(propose_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(propose_url,dict(file=test_file,title='a test slide file',apply_to_all=True)) 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() def test_disapprove_proposed_slides(self): submission = SlideSubmissionFactory() submission.session.meeting.importantdate_set.create(name_id='revsub',date=datetime.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") def test_approve_proposed_slides(self): submission = SlideSubmissionFactory() session = submission.session session.meeting.importantdate_set.create(name_id='revsub',date=datetime.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) 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 = SlideSubmission.objects.get(id = submission.id) self.assertEqual(submission.status_id, 'approved') self.assertIsNotNone(submission.doc) self.assertEqual(session.sessionpresentation_set.count(),1) self.assertEqual(session.sessionpresentation_set.first().document.title,'different title') 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") def test_approve_proposed_slides_multisession_apply_one(self): 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=datetime.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')) self.assertEqual(r.status_code,302) self.assertEqual(session1.sessionpresentation_set.count(),1) self.assertEqual(session2.sessionpresentation_set.count(),0) def test_approve_proposed_slides_multisession_apply_all(self): 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=datetime.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')) self.assertEqual(r.status_code,302) self.assertEqual(session1.sessionpresentation_set.count(),1) self.assertEqual(session2.sessionpresentation_set.count(),1) def test_submit_and_approve_multiple_versions(self): session = SessionFactory(meeting__type_id='ietf') chair = RoleFactory(group=session.group,name_id='chair').person session.meeting.importantdate_set.create(name_id='revsub',date=datetime.date.today()+datetime.timedelta(days=20)) newperson = PersonFactory() propose_url = urlreverse('ietf.meeting.views.propose_session_slides', kwargs={'session_id':session.pk, 'num': session.meeting.number}) login_testing_unauthorized(self,newperson.user.username,propose_url) test_file = BytesIO(b'this is not really a slide') test_file.name = 'not_really.txt' r = self.client.post(propose_url,dict(file=test_file,title='a test slide file',apply_to_all=True)) 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')) self.assertEqual(r.status_code,302) self.client.logout() self.assertEqual(session.sessionpresentation_set.first().document.rev,'00') login_testing_unauthorized(self,newperson.user.username,propose_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(propose_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(propose_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')) self.assertEqual(r.status_code,302) 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.assertEqual(SlideSubmission.objects.filter(status__slug = 'pending').count(),0) self.assertEqual(SlideSubmission.objects.filter(status__slug = 'rejected').count(),1) self.assertEqual(session.sessionpresentation_set.first().document.rev,'01') path = os.path.join(submission.session.meeting.get_materials_path(),'slides') filename = os.path.join(path,session.sessionpresentation_set.first().document.name+'-01.txt') self.assertTrue(os.path.exists(filename)) contents = io.open(filename,'r').read() self.assertIn('third version', contents) class SessionTests(TestCase): def test_meeting_requests(self): meeting = MeetingFactory(type_id='ietf') area = GroupFactory(type_id='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) url = urlreverse('ietf.meeting.views.meeting_requests',kwargs={'num':meeting.number}) r = self.client.get(url) self.assertContains(r, requested_session.group.acronym) self.assertContains(r, not_meeting.group.acronym) self.assertContains(r, requested_session.constraints().first().name) self.assertContains(r, conflicting_session.group.acronym) 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) 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 = datetime.date.today() + datetime.timedelta(days=30+meeting_count) time = datetime.datetime.now().time().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.client.login(username=role.person.user.username, password=role.person.user.username+'+password') r = self.client.get(url) self.assertEqual(r.status_code, 403) self.client.logout() 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 = datetime.datetime.today()+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 = datetime.datetime.today() + 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 = datetime.datetime.today() + 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_width_scale_filter(self): """Test calculation of UI column width by agenda_width_scale filter""" template = Template('{% load agenda_filter_tags %}{{ categories|agenda_width_scale:spacing }}') # Should get '1' as min value when input is empty context = Context({'categories': [], 'spacing': 7}) self.assertEqual(template.render(context), '1') # 3 columns, no spacers context = Context({'categories': [range(3)], 'spacing': 7}) self.assertEqual(template.render(context), '21') # 6 columns, 1 spacer context = Context({'categories': [range(3), range(3)], 'spacing': 7}) self.assertEqual(template.render(context), '43') # 10 columns, 2 spacers context = Context({'categories': [range(3), range(3), range(4)], 'spacing': 7}) self.assertEqual(template.render(context), '72') # 10 columns, 2 spacers, different spacer scale context = Context({'categories': [range(3), range(3), range(4)], 'spacing': 5}) self.assertEqual(template.render(context), '52') 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 test_proceedings(self): """Proceedings should be displayed correctly""" 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") 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_progress_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) 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=datetime.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, ) @patch('ietf.meeting.utils.requests.get') def test_proceedings_attendees(self, mockobj): mockobj.return_value.text = b'[{"LastName":"Smith","FirstName":"John","Company":"ABC","Country":"US"}]' mockobj.return_value.json = lambda: json.loads(b'[{"LastName":"Smith","FirstName":"John","Company":"ABC","Country":"US"}]') make_meeting_test_data() meeting = MeetingFactory(type_id='ietf', date=datetime.date(2016,7,14), number="97") finalize(meeting) url = urlreverse('ietf.meeting.views.proceedings_attendees',kwargs={'num':97}) response = self.client.get(url) self.assertContains(response, 'Attendee list') q = PyQuery(response.content) self.assertEqual(1,len(q("#id_attendees tbody tr"))) @patch('urllib.request.urlopen') def test_proceedings_overview(self, mock_urlopen): '''Test proceedings IETF Overview page. Note: old meetings aren't supported so need to add a new meeting then test. ''' mock_urlopen.return_value = BytesIO(b'[{"LastName":"Smith","FirstName":"John","Company":"ABC","Country":"US"}]') make_meeting_test_data() meeting = MeetingFactory(type_id='ietf', date=datetime.date(2016,7,14), number="97") finalize(meeting) 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_progress_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_progress_report',kwargs={'num':97}) response = self.client.get(url) self.assertContains(response, 'Progress 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=datetime.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): mat = self.upload_proceedings_material_test( meeting, mat_type, {'file': self._proceedings_file(), 'external_url': ''}, ) self.assertEqual(mat.get_href(), f'{mat.document.name}:00') 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') @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') r = self.client.post(pm_doc_url, {'file': self._proceedings_file(), '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 r = self.client.post(pm_url_url, {'file': self._proceedings_file(), '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')