diff --git a/ietf/doc/tests.py b/ietf/doc/tests.py index c3ab7efa4..09abb5692 100644 --- a/ietf/doc/tests.py +++ b/ietf/doc/tests.py @@ -40,7 +40,7 @@ from ietf.doc.utils import create_ballot_if_not_open, uppercase_std_abbreviated_ from ietf.group.models import Group from ietf.group.factories import GroupFactory, RoleFactory from ietf.ipr.factories import HolderIprDisclosureFactory -from ietf.meeting.models import Meeting, Session, SessionPresentation, SchedulingEvent +from ietf.meeting.models import Meeting, SessionPresentation, SchedulingEvent from ietf.meeting.factories import ( MeetingFactory, SessionFactory, SessionPresentationFactory, ProceedingsMaterialFactory ) @@ -1473,12 +1473,12 @@ class DocTestCase(TestCase): ) doc.set_state(State.objects.get(type="slides", slug="active")) - session = Session.objects.create( + session = SessionFactory( name = "session-72-mars-1", meeting = Meeting.objects.get(number='72'), group = Group.objects.get(acronym='mars'), modified = datetime.datetime.now(), - type_id = 'regular', + add_to_schedule=False, ) SchedulingEvent.objects.create( session=session, diff --git a/ietf/doc/tests_material.py b/ietf/doc/tests_material.py index 8d4482139..3f7879e98 100644 --- a/ietf/doc/tests_material.py +++ b/ietf/doc/tests_material.py @@ -17,8 +17,8 @@ from django.urls import reverse as urlreverse from ietf.doc.models import Document, State, DocAlias, NewRevisionDocEvent from ietf.group.factories import RoleFactory from ietf.group.models import Group -from ietf.meeting.factories import MeetingFactory -from ietf.meeting.models import Meeting, Session, SessionPresentation, SchedulingEvent +from ietf.meeting.factories import MeetingFactory, SessionFactory +from ietf.meeting.models import Meeting, SessionPresentation, SchedulingEvent from ietf.name.models import SessionStatusName from ietf.person.models import Person from ietf.utils.test_utils import TestCase, login_testing_unauthorized @@ -152,12 +152,11 @@ class GroupMaterialTests(TestCase): def test_revise(self): doc = self.create_slides() - session = Session.objects.create( + session = SessionFactory( name = "session-42-mars-1", meeting = Meeting.objects.get(number='42'), group = Group.objects.get(acronym='mars'), modified = datetime.datetime.now(), - type_id='regular', ) SchedulingEvent.objects.create( session=session, diff --git a/ietf/group/migrations/0051_groupfeatures_session_purposes.py b/ietf/group/migrations/0051_groupfeatures_session_purposes.py index d6adfca56..e3f42fcc5 100644 --- a/ietf/group/migrations/0051_groupfeatures_session_purposes.py +++ b/ietf/group/migrations/0051_groupfeatures_session_purposes.py @@ -12,7 +12,7 @@ class Migration(migrations.Migration): dependencies = [ ('group', '0050_populate_groupfeatures_agenda_filter_type'), - ('name', '0035_sessionpurposename'), + ('name', '0034_sessionpurposename'), ] operations = [ diff --git a/ietf/group/migrations/0052_populate_groupfeatures_session_purposes.py b/ietf/group/migrations/0052_populate_groupfeatures_session_purposes.py index 86bee4a15..4c65071aa 100644 --- a/ietf/group/migrations/0052_populate_groupfeatures_session_purposes.py +++ b/ietf/group/migrations/0052_populate_groupfeatures_session_purposes.py @@ -7,23 +7,23 @@ from django.db import migrations default_purposes = dict( adhoc=['presentation'], - adm=['closed_meeting', 'office_hours'], + adm=['closed_meeting', 'officehours'], ag=['regular'], area=['regular'], dir=['open_meeting', 'presentation', 'regular', 'social', 'tutorial'], iab=['closed_meeting', 'regular'], - iabasg=['closed_meeting', 'open_meeting'], - iana=['office_hours'], + iabasg=['closed_meeting', 'officehours', 'open_meeting'], + iana=['officehours'], iesg=['closed_meeting', 'open_meeting'], ietf=['admin', 'plenary', 'presentation', 'social'], irtf=[], - ise=['office_hours'], - isoc=['office_hours', 'open_meeting', 'presentation'], - nomcom=['closed_meeting', 'office_hours'], + ise=['officehours'], + isoc=['officehours', 'open_meeting', 'presentation'], + nomcom=['closed_meeting', 'officehours'], program=['regular', 'tutorial'], rag=['regular'], review=['open_meeting', 'social'], - rfcedtyp=['office_hours'], + rfcedtyp=['officehours'], rg=['regular'], team=['coding', 'presentation', 'social', 'tutorial'], wg=['regular'], @@ -55,7 +55,7 @@ class Migration(migrations.Migration): dependencies = [ ('group', '0051_groupfeatures_session_purposes'), - ('name', '0036_populate_sessionpurposename'), + ('name', '0035_populate_sessionpurposename'), ] diff --git a/ietf/meeting/ajax.py b/ietf/meeting/ajax.py index d58d7a528..b8a3c01a5 100644 --- a/ietf/meeting/ajax.py +++ b/ietf/meeting/ajax.py @@ -10,7 +10,7 @@ from django.views.decorators.http import require_POST from ietf.ietfauth.utils import role_required, has_role from ietf.meeting.helpers import get_meeting, get_schedule, schedule_permissions, get_person_by_email, get_schedule_by_name from ietf.meeting.models import TimeSlot, Session, Schedule, Room, Constraint, SchedTimeSessAssignment, ResourceAssociation -from ietf.meeting.views import edit_timeslots, edit_schedule +from ietf.meeting.views import edit_timeslots, edit_meeting_schedule import debug # pyflakes:ignore @@ -286,7 +286,7 @@ def schedule_add(request, meeting): if "HTTP_ACCEPT" in request.META and "application/json" in request.META['HTTP_ACCEPT']: return redirect(schedule_infourl, meeting.number, newschedule.owner_email(), newschedule.name) else: - return redirect(edit_schedule, meeting.number, newschedule.owner_email(), newschedule.name) + return redirect(edit_meeting_schedule, meeting.number, newschedule.owner_email(), newschedule.name) @require_POST def schedule_update(request, meeting, schedule): @@ -325,7 +325,7 @@ def schedule_update(request, meeting, schedule): return HttpResponse(json.dumps(schedule.json_dict(request.build_absolute_uri('/'))), content_type="application/json") else: - return redirect(edit_schedule, meeting.number, schedule.owner_email(), schedule.name) + return redirect(edit_meeting_schedule, meeting.number, schedule.owner_email(), schedule.name) @role_required('Secretariat') def schedule_del(request, meeting, schedule): diff --git a/ietf/meeting/factories.py b/ietf/meeting/factories.py index 729525745..a227b405a 100644 --- a/ietf/meeting/factories.py +++ b/ietf/meeting/factories.py @@ -12,7 +12,8 @@ from django.db.models import Q from ietf.meeting.models import (Meeting, Session, SchedulingEvent, Schedule, TimeSlot, SessionPresentation, FloorPlan, Room, SlideSubmission, Constraint, MeetingHost, ProceedingsMaterial) -from ietf.name.models import ConstraintName, SessionStatusName, ProceedingsMaterialTypeName, TimerangeName +from ietf.name.models import (ConstraintName, SessionStatusName, ProceedingsMaterialTypeName, + TimerangeName, SessionPurposeName) from ietf.doc.factories import ProceedingsMaterialDocFactory from ietf.group.factories import GroupFactory from ietf.person.factories import PersonFactory @@ -104,9 +105,11 @@ class SessionFactory(factory.django.DjangoModelFactory): model = Session meeting = factory.SubFactory(MeetingFactory) - type_id='regular' + purpose_id = 'regular' + type_id = 'regular' group = factory.SubFactory(GroupFactory) requested_duration = datetime.timedelta(hours=1) + on_agenda = factory.lazy_attribute(lambda obj: SessionPurposeName.objects.get(pk=obj.purpose_id).on_agenda) @factory.post_generation def status_id(obj, create, extracted, **kwargs): @@ -128,7 +131,7 @@ class SessionFactory(factory.django.DjangoModelFactory): status=SessionStatusName.objects.get(slug=extracted), by=PersonFactory(), ) - + @factory.post_generation def add_to_schedule(obj, create, extracted, **kwargs): # pylint: disable=no-self-argument ''' diff --git a/ietf/meeting/fields.py b/ietf/meeting/fields.py index 06533f41c..8f126d55e 100644 --- a/ietf/meeting/fields.py +++ b/ietf/meeting/fields.py @@ -5,7 +5,7 @@ from django import forms from ietf.name.models import SessionPurposeName, TimeSlotTypeName -import debug +import debug # pyflakes: ignore class SessionPurposeAndTypeWidget(forms.MultiWidget): css_class = 'session_purpose_widget' # class to apply to all widgets diff --git a/ietf/meeting/helpers.py b/ietf/meeting/helpers.py index e750b3392..0ca6e0d12 100644 --- a/ietf/meeting/helpers.py +++ b/ietf/meeting/helpers.py @@ -310,7 +310,7 @@ class AgendaKeywordTool: @property def filterable_purposes(self): - return SessionPurposeName.objects.exclude(slug='regular').order_by('name') + return SessionPurposeName.objects.exclude(slug='none').order_by('name') class AgendaFilterOrganizer(AgendaKeywordTool): @@ -336,7 +336,7 @@ class AgendaFilterOrganizer(AgendaKeywordTool): # group acronyms in this list will never be used as filter buttons exclude_acronyms = ('iesg', 'ietf', 'secretariat') # extra keywords to include in the no-heading column if they apply to any sessions - extra_labels = ('BoF', 'Plenary') + extra_labels = ('BoF',) # group types whose acronyms should be word-capitalized capitalized_group_types = ('team',) # group types whose acronyms should be all-caps @@ -352,6 +352,8 @@ class AgendaFilterOrganizer(AgendaKeywordTool): # filled in when _organize_filters() is called self.filter_categories = None self.special_filters = None + if self._use_legacy_keywords(): + self.extra_labels += ('Plenary',) # need this when not using session purpose def get_non_area_keywords(self): """Get list of any 'non-area' (aka 'special') keywords @@ -465,11 +467,14 @@ class AgendaFilterOrganizer(AgendaKeywordTool): # Call legacy version for older meetings if self._use_legacy_keywords(): - return self._legacy_non_group_filters() + return self._legacy_non_group_filters(sessions) # Not using legacy version filter_cols = [] for purpose in self.filterable_purposes: + if purpose.slug == 'regular': + continue + # Map label to its keyword, discarding duplicate labels. # This does what we want as long as sessions with the same # name and purpose belong to the same group. @@ -497,19 +502,16 @@ class AgendaFilterOrganizer(AgendaKeywordTool): return filter_cols - def _legacy_non_group_filters(self): + def _legacy_non_group_filters(self, sessions): """Get list of non-group filters for older meetings Returns a list of filter columns """ - if self.assignments is None: - return [] # can only use timeslot type when we have assignments - office_hours_items = set() suffix = ' office hours' - for a in self.assignments: - if a.session.name.lower().endswith(suffix): - office_hours_items.add((a.session.name[:-len(suffix)].strip(), a.session.group)) + for s in sessions: + if s.name.lower().endswith(suffix): + office_hours_items.add((s.name[:-len(suffix)].strip(), s.group)) headings = [] # currently we only do office hours @@ -640,8 +642,7 @@ class AgendaKeywordTagger(AgendaKeywordTool): Keywords are all lower case. """ for a in self.assignments: - a.filter_keywords = {a.slot_type().slug.lower()} - a.filter_keywords.update(self._filter_keywords_for_assignment(a)) + a.filter_keywords = self._filter_keywords_for_assignment(a) a.filter_keywords = sorted(list(a.filter_keywords)) def _tag_sessions_with_filter_keywords(self): @@ -652,14 +653,17 @@ class AgendaKeywordTagger(AgendaKeywordTool): @staticmethod def _legacy_extra_session_keywords(session): """Get extra keywords for a session at a legacy meeting""" + extra = [] + if session.type_id == 'plenary': + extra.append('plenary') office_hours_match = re.match(r'^ *\w+(?: +\w+)* +office hours *$', session.name, re.IGNORECASE) if office_hours_match is not None: suffix = 'officehours' - return [ + extra.extend([ 'officehours', session.name.lower().replace(' ', '')[:-len(suffix)] + '-officehours', - ] - return [] + ]) + return extra def _filter_keywords_for_session(self, session): keywords = set() diff --git a/ietf/meeting/migrations/0050_populate_session_on_agenda.py b/ietf/meeting/migrations/0050_populate_session_on_agenda.py index 286035fa1..85e7a71c7 100644 --- a/ietf/meeting/migrations/0050_populate_session_on_agenda.py +++ b/ietf/meeting/migrations/0050_populate_session_on_agenda.py @@ -15,7 +15,9 @@ def forward(apps, schema_editor): ), timeslot__type__private=True, ) - Session.objects.filter(timeslotassignments__in=private_assignments).update(on_agenda=False) + for pa in private_assignments: + pa.session.on_agenda = False + pa.session.save() # Also update any sessions to match their purpose's default setting (this intentionally # overrides the timeslot settings above, but that is unlikely to matter because the # purposes will roll out at the same time as the on_agenda field) diff --git a/ietf/meeting/models.py b/ietf/meeting/models.py index 20db83b69..ea394569e 100644 --- a/ietf/meeting/models.py +++ b/ietf/meeting/models.py @@ -1162,7 +1162,7 @@ class Session(models.Model): meeting = ForeignKey(Meeting) name = models.CharField(blank=True, max_length=255, help_text="Name of session, in case the session has a purpose rather than just being a group meeting.") short = models.CharField(blank=True, max_length=32, help_text="Short version of 'name' above, for use in filenames.") - purpose = ForeignKey(SessionPurposeName, null=True, help_text='Purpose of the session') + purpose = ForeignKey(SessionPurposeName, null=False, help_text='Purpose of the session') type = ForeignKey(TimeSlotTypeName) group = ForeignKey(Group) # The group type historically determined the session type. BOFs also need to be added as a group. Note that not all meeting requests have a natural group to associate with. joint_with_groups = models.ManyToManyField(Group, related_name='sessions_joint_in',blank=True) diff --git a/ietf/meeting/templatetags/agenda_custom_tags.py b/ietf/meeting/templatetags/agenda_custom_tags.py index 1b64f9adf..0bdd9d222 100644 --- a/ietf/meeting/templatetags/agenda_custom_tags.py +++ b/ietf/meeting/templatetags/agenda_custom_tags.py @@ -5,8 +5,6 @@ from django import template from django.urls import reverse -from ietf.utils.text import xslugify - register = template.Library() diff --git a/ietf/meeting/test_data.py b/ietf/meeting/test_data.py index d7d21bd27..e5fdd71c5 100644 --- a/ietf/meeting/test_data.py +++ b/ietf/meeting/test_data.py @@ -11,8 +11,9 @@ import debug # pyflakes:ignore from ietf.doc.factories import DocumentFactory from ietf.group.factories import GroupFactory, RoleFactory -from ietf.group.models import Group -from ietf.meeting.models import (Meeting, Room, TimeSlot, Session, Schedule, SchedTimeSessAssignment, +from ietf.group.models import Group +from ietf.meeting.factories import SessionFactory +from ietf.meeting.models import (Meeting, Room, TimeSlot, Schedule, SchedTimeSessAssignment, ResourceAssociation, SessionPresentation, UrlResource, SchedulingEvent) from ietf.meeting.helpers import create_interim_meeting from ietf.name.models import RoomResourceName @@ -24,11 +25,11 @@ def make_interim_meeting(group,date,status='sched'): system_person = Person.objects.get(name="(System)") time = datetime.datetime.combine(date, datetime.time(9)) meeting = create_interim_meeting(group=group,date=date) - session = Session.objects.create(meeting=meeting, group=group, - attendees=10, - requested_duration=datetime.timedelta(minutes=20), - remote_instructions='http://webex.com', - type_id='regular') + session = SessionFactory(meeting=meeting, group=group, + attendees=10, + requested_duration=datetime.timedelta(minutes=20), + remote_instructions='http://webex.com', + add_to_schedule=False) SchedulingEvent.objects.create(session=session, status_id=status, by=system_person) slot = TimeSlot.objects.create( meeting=meeting, @@ -121,52 +122,52 @@ def make_meeting_test_data(meeting=None, create_interims=False): time=datetime.datetime.combine(session_date, datetime.time(11,0))) # mars WG mars = Group.objects.get(acronym='mars') - mars_session = Session.objects.create(meeting=meeting, group=mars, - attendees=10, requested_duration=datetime.timedelta(minutes=50), - type_id='regular') + mars_session = SessionFactory(meeting=meeting, group=mars, + attendees=10, requested_duration=datetime.timedelta(minutes=50), + add_to_schedule=False) SchedulingEvent.objects.create(session=mars_session, status_id='schedw', by=system_person) SchedTimeSessAssignment.objects.create(timeslot=slot1, session=mars_session, schedule=schedule) SchedTimeSessAssignment.objects.create(timeslot=slot2, session=mars_session, schedule=unofficial_schedule) # ames WG - ames_session = Session.objects.create(meeting=meeting, group=Group.objects.get(acronym="ames"), - attendees=10, - requested_duration=datetime.timedelta(minutes=60), - type_id='regular') + ames_session = SessionFactory(meeting=meeting, group=Group.objects.get(acronym="ames"), + attendees=10, + requested_duration=datetime.timedelta(minutes=60), + add_to_schedule=False) SchedulingEvent.objects.create(session=ames_session, status_id='schedw', by=system_person) SchedTimeSessAssignment.objects.create(timeslot=slot2, session=ames_session, schedule=schedule) SchedTimeSessAssignment.objects.create(timeslot=slot1, session=ames_session, schedule=unofficial_schedule) # IESG breakfast - iesg_session = Session.objects.create(meeting=meeting, group=Group.objects.get(acronym="iesg"), - name="IESG Breakfast", attendees=25, - requested_duration=datetime.timedelta(minutes=60), - type_id="lead") + iesg_session = SessionFactory(meeting=meeting, group=Group.objects.get(acronym="iesg"), + name="IESG Breakfast", attendees=25, + requested_duration=datetime.timedelta(minutes=60), + type_id="lead", purpose_id='closed_meeting', add_to_schedule=False) SchedulingEvent.objects.create(session=iesg_session, status_id='schedw', by=system_person) SchedTimeSessAssignment.objects.create(timeslot=breakfast_slot, session=iesg_session, schedule=schedule) # No breakfast on unofficial schedule # Registration - reg_session = Session.objects.create(meeting=meeting, group=Group.objects.get(acronym="secretariat"), - name="Registration", attendees=250, - requested_duration=datetime.timedelta(minutes=480), - type_id="reg") + reg_session = SessionFactory(meeting=meeting, group=Group.objects.get(acronym="secretariat"), + name="Registration", attendees=250, + requested_duration=datetime.timedelta(minutes=480), + type_id="reg", purpose_id='admin', add_to_schedule=False) SchedulingEvent.objects.create(session=reg_session, status_id='schedw', by=system_person) SchedTimeSessAssignment.objects.create(timeslot=reg_slot, session=reg_session, schedule=base_schedule) # Break - break_session = Session.objects.create(meeting=meeting, group=Group.objects.get(acronym="secretariat"), - name="Morning Break", attendees=250, - requested_duration=datetime.timedelta(minutes=30), - type_id="break") + break_session = SessionFactory(meeting=meeting, group=Group.objects.get(acronym="secretariat"), + name="Morning Break", attendees=250, + requested_duration=datetime.timedelta(minutes=30), + type_id="break", purpose_id='social', add_to_schedule=False) SchedulingEvent.objects.create(session=break_session, status_id='schedw', by=system_person) SchedTimeSessAssignment.objects.create(timeslot=break_slot, session=break_session, schedule=base_schedule) # IETF Plenary - plenary_session = Session.objects.create(meeting=meeting, group=Group.objects.get(acronym="ietf"), - name="IETF Plenary", attendees=250, - requested_duration=datetime.timedelta(minutes=60), - type_id="plenary") + plenary_session = SessionFactory(meeting=meeting, group=Group.objects.get(acronym="ietf"), + name="IETF Plenary", attendees=250, + requested_duration=datetime.timedelta(minutes=60), + type_id="plenary", purpose_id='plenary', add_to_schedule=False) SchedulingEvent.objects.create(session=plenary_session, status_id='schedw', by=system_person) SchedTimeSessAssignment.objects.create(timeslot=plenary_slot, session=plenary_session, schedule=schedule) diff --git a/ietf/meeting/tests_helpers.py b/ietf/meeting/tests_helpers.py index b37118a86..269d785fb 100644 --- a/ietf/meeting/tests_helpers.py +++ b/ietf/meeting/tests_helpers.py @@ -1,13 +1,14 @@ # Copyright The IETF Trust 2020, All Rights Reserved # -*- coding: utf-8 -*- -import copy +from django.conf import settings from django.test import override_settings from ietf.group.factories import GroupFactory from ietf.group.models import Group from ietf.meeting.factories import SessionFactory, MeetingFactory, TimeSlotFactory from ietf.meeting.helpers import AgendaFilterOrganizer, AgendaKeywordTagger +from ietf.meeting.models import SchedTimeSessAssignment from ietf.meeting.test_data import make_meeting_test_data from ietf.utils.test_utils import TestCase @@ -21,7 +22,6 @@ class AgendaKeywordTaggerTests(TestCase): The historic param can be None, group, or parent, to specify whether to test with no historic_group, a historic_group but no historic_parent, or both. """ - session_types = ['regular', 'plenary'] # decide whether meeting should use legacy keywords (for office hours) legacy_keywords = meeting_num <= 111 @@ -44,71 +44,92 @@ class AgendaKeywordTaggerTests(TestCase): expected_group = group expected_area = group.parent - # create the ordinary sessions - for session_type in session_types: - sess = SessionFactory(group=group, meeting=meeting, type_id=session_type, add_to_schedule=False) - sess.timeslotassignments.create( - timeslot=TimeSlotFactory(meeting=meeting, type_id=session_type), + # create sessions, etc + session_data = [ + { + 'description': 'regular wg session', + 'session': SessionFactory( + group=group, meeting=meeting, add_to_schedule=False, + purpose_id='none' if legacy_keywords else 'regular', + type_id='regular', + ), + 'expected_keywords': { + expected_group.acronym, + expected_area.acronym, + # if legacy_keywords, next line repeats a previous entry to avoid adding anything to the set + expected_group.acronym if legacy_keywords else 'regular', + f'{expected_group.acronym}-sessa', + }, + }, + { + 'description': 'plenary session', + 'session': SessionFactory( + group=group, meeting=meeting, add_to_schedule=False, + name=f'{group.acronym} plenary', + purpose_id='none' if legacy_keywords else 'plenary', + type_id='plenary', + ), + 'expected_keywords': { + expected_group.acronym, + expected_area.acronym, + f'{expected_group.acronym}-sessb', + 'plenary', + f'{group.acronym}-plenary', + }, + }, + { + 'description': 'office hours session', + 'session': SessionFactory( + group=group, meeting=meeting, add_to_schedule=False, + name=f'{group.acronym} office hours', + purpose_id='none' if legacy_keywords else 'officehours', + type_id='other', + ), + 'expected_keywords': { + expected_group.acronym, + expected_area.acronym, + f'{expected_group.acronym}-sessc', + 'officehours', + f'{group.acronym}-officehours' if legacy_keywords else 'officehours', + # officehours in prev line is a repeated value - since this is a set, it will be ignored + f'{group.acronym}-office-hours', + }, + } + ] + for sd in session_data: + sd['session'].timeslotassignments.create( + timeslot=TimeSlotFactory(meeting=meeting, type=sd['session'].type), schedule=meeting.schedule, ) - # Create an office hours session in the group's area (i.e., parent). Handle this separately - # from other session creation to test legacy office hours naming. - office_hours = SessionFactory( - name='some office hours', - group=Group.objects.get(acronym='iesg') if legacy_keywords else expected_area, - meeting=meeting, - type_id='other' if legacy_keywords else 'officehours', - add_to_schedule=False, - ) - office_hours.timeslotassignments.create( - timeslot=TimeSlotFactory(meeting=meeting, type_id=office_hours.type_id), - schedule=meeting.schedule, - ) - assignments = meeting.schedule.assignments.all() - orig_num_assignments = len(assignments) - # Set up historic groups if needed. We've already set the office hours group properly - # so skip that session. The expected_group will already have its historic_parent set - # if historic == 'parent' + # Set up historic groups if needed. if historic: for a in assignments: - if a.session != office_hours: - a.session.historic_group = expected_group + a.session.historic_group = expected_group # Execute the method under test AgendaKeywordTagger(assignments=assignments).apply() # Assert expected results - self.assertEqual(len(assignments), orig_num_assignments, 'Should not change number of assignments') - for assignment in assignments: - expected_filter_keywords = {assignment.slot_type().slug, assignment.session.type.slug} - - if assignment.session == office_hours: - expected_filter_keywords.update([ - office_hours.group.acronym, - 'officehours', - 'some-officehours' if legacy_keywords else '{}-officehours'.format(expected_area.acronym), - ]) - else: - expected_filter_keywords.update([ - expected_group.acronym, - expected_area.acronym - ]) - if bof: - expected_filter_keywords.add('bof') - token = assignment.session.docname_token_only_for_multiple() - if token is not None: - expected_filter_keywords.update([expected_group.acronym + "-" + token]) + # check the assignment count - paranoid, but the method mutates its input so let's be careful + self.assertEqual(len(assignments), len(session_data), 'Should not change number of assignments') + assignment_by_session_pk = {a.session.pk: a for a in assignments} + for sd in session_data: + assignment = assignment_by_session_pk[sd['session'].pk] + expected_filter_keywords = sd['expected_keywords'] + if bof: + expected_filter_keywords.add('bof') self.assertCountEqual( assignment.filter_keywords, expected_filter_keywords, - 'Assignment has incorrect filter keywords' + f'Assignment for "{sd["description"]}" has incorrect filter keywords' ) + @override_settings(MEETING_LEGACY_OFFICE_HOURS_END=111) def test_tag_assignments_with_filter_keywords(self): # use distinct meeting numbers > 111 for non-legacy keyword tests self.do_test_tag_assignments_with_filter_keywords(112) @@ -119,6 +140,7 @@ class AgendaKeywordTaggerTests(TestCase): self.do_test_tag_assignments_with_filter_keywords(117, bof=True, historic='parent') + @override_settings(MEETING_LEGACY_OFFICE_HOURS_END=111) def test_tag_assignments_with_filter_keywords_legacy(self): # use distinct meeting numbers <= 111 for legacy keyword tests self.do_test_tag_assignments_with_filter_keywords(101) @@ -131,8 +153,19 @@ class AgendaKeywordTaggerTests(TestCase): class AgendaFilterOrganizerTests(TestCase): def test_get_filter_categories(self): + self.do_get_filter_categories_test(False) + + def test_get_legacy_filter_categories(self): + self.do_get_filter_categories_test(True) + + def do_get_filter_categories_test(self, legacy): # set up meeting = make_meeting_test_data() + if legacy: + meeting.session_set.all().update(purpose_id='none') # legacy meetings did not have purposes + else: + meeting.number = str(settings.MEETING_LEGACY_OFFICE_HOURS_END + 1) + meeting.save() # create extra groups for testing iab = Group.objects.get(acronym='iab') @@ -147,55 +180,97 @@ class AgendaFilterOrganizerTests(TestCase): # office hours session SessionFactory( group=Group.objects.get(acronym='farfut'), + purpose_id='officehours' if not legacy else 'none', + type_id='other', name='FARFUT office hours', meeting=meeting ) - expected = [ - [ - # area category - {'label': 'FARFUT', 'keyword': 'farfut', 'is_bof': False, - 'children': [ - {'label': 'ames', 'keyword': 'ames', 'is_bof': False}, - {'label': 'mars', 'keyword': 'mars', 'is_bof': False}, - ]}, - ], - [ - # non-area category - {'label': 'IAB', 'keyword': 'iab', 'is_bof': False, - 'children': [ - {'label': iab_child.acronym, 'keyword': iab_child.acronym, 'is_bof': False}, - ]}, - {'label': 'IRTF', 'keyword': 'irtf', 'is_bof': False, - 'children': [ - {'label': irtf_child.acronym, 'keyword': irtf_child.acronym, 'is_bof': True}, - ]}, - ], - [ - # non-group category - {'label': 'Office Hours', 'keyword': 'officehours', 'is_bof': False, - 'children': [ - {'label': 'FARFUT', 'keyword': 'farfut-officehours', 'is_bof': False} - ]}, - {'label': None, 'keyword': None,'is_bof': False, - 'children': [ - {'label': 'BoF', 'keyword': 'bof', 'is_bof': False}, - {'label': 'Plenary', 'keyword': 'plenary', 'is_bof': False}, - ]}, - ], - ] - - # when using sessions instead of assignments, won't get timeslot-type based filters - expected_with_sessions = copy.deepcopy(expected) - expected_with_sessions[-1].pop(0) # pops 'office hours' column - + if legacy: + expected = [ + [ + # area category + {'label': 'FARFUT', 'keyword': 'farfut', 'is_bof': False, 'toggled_by': [], + 'children': [ + {'label': 'ames', 'keyword': 'ames', 'is_bof': False, 'toggled_by': ['farfut']}, + {'label': 'mars', 'keyword': 'mars', 'is_bof': False, 'toggled_by': ['farfut']}, + ]}, + ], + [ + # non-area category + {'label': 'IAB', 'keyword': 'iab', 'is_bof': False, 'toggled_by': [], + 'children': [ + {'label': iab_child.acronym, 'keyword': iab_child.acronym, 'is_bof': False, 'toggled_by': ['iab']}, + ]}, + {'label': 'IRTF', 'keyword': 'irtf', 'is_bof': False, 'toggled_by': [], + 'children': [ + {'label': irtf_child.acronym, 'keyword': irtf_child.acronym, 'is_bof': True, 'toggled_by': ['bof', 'irtf']}, + ]}, + ], + [ + # non-group category + {'label': 'Office Hours', 'keyword': 'officehours', 'is_bof': False, 'toggled_by': [], + 'children': [ + {'label': 'FARFUT', 'keyword': 'farfut-officehours', 'is_bof': False, 'toggled_by': ['officehours', 'farfut']} + ]}, + {'label': None, 'keyword': None,'is_bof': False, 'toggled_by': [], + 'children': [ + {'label': 'BoF', 'keyword': 'bof', 'is_bof': False, 'toggled_by': []}, + {'label': 'Plenary', 'keyword': 'plenary', 'is_bof': False, 'toggled_by': []}, + ]}, + ], + ] + else: + expected = [ + [ + # area category + {'label': 'FARFUT', 'keyword': 'farfut', 'is_bof': False, 'toggled_by': [], + 'children': [ + {'label': 'ames', 'keyword': 'ames', 'is_bof': False, 'toggled_by': ['farfut']}, + {'label': 'mars', 'keyword': 'mars', 'is_bof': False, 'toggled_by': ['farfut']}, + ]}, + ], + [ + # non-area category + {'label': 'IAB', 'keyword': 'iab', 'is_bof': False, 'toggled_by': [], + 'children': [ + {'label': iab_child.acronym, 'keyword': iab_child.acronym, 'is_bof': False, 'toggled_by': ['iab']}, + ]}, + {'label': 'IRTF', 'keyword': 'irtf', 'is_bof': False, 'toggled_by': [], + 'children': [ + {'label': irtf_child.acronym, 'keyword': irtf_child.acronym, 'is_bof': True, 'toggled_by': ['bof', 'irtf']}, + ]}, + ], + [ + # non-group category + {'label': 'Administrative', 'keyword': 'admin', 'is_bof': False, 'toggled_by': [], + 'children': [ + {'label': 'Registration', 'keyword': 'registration', 'is_bof': False, 'toggled_by': ['admin', 'secretariat']}, + ]}, + {'label': 'Closed meeting', 'keyword': 'closed_meeting', 'is_bof': False, 'toggled_by': [], + 'children': [ + {'label': 'IESG Breakfast', 'keyword': 'iesg-breakfast', 'is_bof': False, 'toggled_by': ['closed_meeting', 'iesg']}, + ]}, + {'label': 'Office hours', 'keyword': 'officehours', 'is_bof': False, 'toggled_by': [], + 'children': [ + {'label': 'FARFUT office hours', 'keyword': 'farfut-office-hours', 'is_bof': False, 'toggled_by': ['officehours', 'farfut']} + ]}, + {'label': 'Plenary', 'keyword': 'plenary', 'is_bof': False, 'toggled_by': [], + 'children': [ + {'label': 'IETF Plenary', 'keyword': 'ietf-plenary', 'is_bof': False, 'toggled_by': ['plenary', 'ietf']}, + ]}, + {'label': 'Social', 'keyword': 'social', 'is_bof': False, 'toggled_by': [], + 'children': [ + {'label': 'Morning Break', 'keyword': 'morning-break', 'is_bof': False, 'toggled_by': ['social', 'secretariat']}, + ]}, + {'label': None, 'keyword': None,'is_bof': False, 'toggled_by': [], + 'children': [ + {'label': 'BoF', 'keyword': 'bof', 'is_bof': False, 'toggled_by': []}, + ]}, + ], + ] # put all the above together for single-column tests - expected_single_category = [ - sorted(sum(expected, []), key=lambda col: col['label'] or 'zzzzz') - ] - expected_single_category_with_sessions = [ - sorted(sum(expected_with_sessions, []), key=lambda col: col['label'] or 'zzzzz') - ] + expected_single_category = [sum(expected, [])] ### # test using sessions @@ -204,15 +279,17 @@ class AgendaFilterOrganizerTests(TestCase): # default filter_organizer = AgendaFilterOrganizer(sessions=sessions) - self.assertEqual(filter_organizer.get_filter_categories(), expected_with_sessions) + self.assertEqual(filter_organizer.get_filter_categories(), expected) # single-column filter_organizer = AgendaFilterOrganizer(sessions=sessions, single_category=True) - self.assertEqual(filter_organizer.get_filter_categories(), expected_single_category_with_sessions) + self.assertEqual(filter_organizer.get_filter_categories(), expected_single_category) ### # test again using assignments - assignments = meeting.schedule.assignments.all() + assignments = SchedTimeSessAssignment.objects.filter( + schedule__in=(meeting.schedule, meeting.schedule.base) + ) AgendaKeywordTagger(assignments=assignments).apply() # default diff --git a/ietf/meeting/tests_js.py b/ietf/meeting/tests_js.py index 2341221ca..14dfaf5f0 100644 --- a/ietf/meeting/tests_js.py +++ b/ietf/meeting/tests_js.py @@ -89,7 +89,13 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase): s2.save() SchedTimeSessAssignment.objects.filter(session=s1).delete() - s2b = Session.objects.create(meeting=meeting, group=s2.group, attendees=10, requested_duration=datetime.timedelta(minutes=60), type_id='regular') + s2b = SessionFactory( + meeting=meeting, + group=s2.group, + attendees=10, + requested_duration=datetime.timedelta(minutes=60), + add_to_schedule=False, + ) SchedulingEvent.objects.create( session=s2b, @@ -110,34 +116,34 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase): WebDriverWait(self.driver, 2).until(expected_conditions.presence_of_element_located((By.CSS_SELECTOR, '.edit-meeting-schedule'))) - self.assertEqual(len(self.driver.find_elements_by_css_selector('.session')), 3) + self.assertEqual(len(self.driver.find_elements(By.CSS_SELECTOR, '.session.purpose-regular')), 3) # select - show session info - s2_element = self.driver.find_element_by_css_selector('#session{}'.format(s2.pk)) - s2b_element = self.driver.find_element_by_css_selector('#session{}'.format(s2b.pk)) + s2_element = self.driver.find_element(By.CSS_SELECTOR, '#session{}'.format(s2.pk)) + s2b_element = self.driver.find_element(By.CSS_SELECTOR, '#session{}'.format(s2b.pk)) self.assertNotIn('other-session-selected', s2b_element.get_attribute('class')) s2_element.click() # other session for group should be flagged for highlighting - s2b_element = self.driver.find_element_by_css_selector('#session{}'.format(s2b.pk)) + s2b_element = self.driver.find_element(By.CSS_SELECTOR, '#session{}'.format(s2b.pk)) self.assertIn('other-session-selected', s2b_element.get_attribute('class')) # other session for group should appear in the info panel - session_info_container = self.driver.find_element_by_css_selector('.session-info-container') - self.assertIn(s2.group.acronym, session_info_container.find_element_by_css_selector(".title").text) - self.assertEqual(session_info_container.find_element_by_css_selector(".other-session .time").text, "not yet scheduled") + session_info_container = self.driver.find_element(By.CSS_SELECTOR, '.session-info-container') + self.assertIn(s2.group.acronym, session_info_container.find_element(By.CSS_SELECTOR, ".title").text) + self.assertEqual(session_info_container.find_element(By.CSS_SELECTOR, ".other-session .time").text, "not yet scheduled") # deselect - self.driver.find_element_by_css_selector('.scheduling-panel').click() + self.driver.find_element(By.CSS_SELECTOR, '.scheduling-panel').click() - self.assertEqual(session_info_container.find_elements_by_css_selector(".title"), []) + self.assertEqual(session_info_container.find_elements(By.CSS_SELECTOR, ".title"), []) self.assertNotIn('other-session-selected', s2b_element.get_attribute('class')) # unschedule # we would like to do # - # unassigned_sessions_element = self.driver.find_element_by_css_selector('.unassigned-sessions') + # unassigned_sessions_element = self.driver.find_element(By.CSS_SELECTOR, '.unassigned-sessions') # ActionChains(self.driver).drag_and_drop(s2_element, unassigned_sessions_element).perform() # # but unfortunately, Selenium does not simulate drag and drop events, see @@ -158,20 +164,20 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase): # sorting unassigned sorted_pks = [s.pk for s in sorted([s1, s2, s2b], key=lambda s: (s.group.acronym, s.requested_duration, s.pk))] - self.driver.find_element_by_css_selector('[name=sort_unassigned] option[value=name]').click() - self.assertTrue(self.driver.find_element_by_css_selector('.unassigned-sessions .drop-target #session{} + #session{} + #session{}'.format(*sorted_pks))) + self.driver.find_element(By.CSS_SELECTOR, '[name=sort_unassigned] option[value=name]').click() + self.assertTrue(self.driver.find_element(By.CSS_SELECTOR, '.unassigned-sessions .drop-target #session{} + #session{} + #session{}'.format(*sorted_pks))) sorted_pks = [s.pk for s in sorted([s1, s2, s2b], key=lambda s: (s.group.parent.acronym, s.group.acronym, s.requested_duration, s.pk))] - self.driver.find_element_by_css_selector('[name=sort_unassigned] option[value=parent]').click() - self.assertTrue(self.driver.find_element_by_css_selector('.unassigned-sessions .drop-target #session{} + #session{}'.format(*sorted_pks))) + self.driver.find_element(By.CSS_SELECTOR, '[name=sort_unassigned] option[value=parent]').click() + self.assertTrue(self.driver.find_element(By.CSS_SELECTOR, '.unassigned-sessions .drop-target #session{} + #session{}'.format(*sorted_pks))) sorted_pks = [s.pk for s in sorted([s1, s2, s2b], key=lambda s: (s.requested_duration, s.group.parent.acronym, s.group.acronym, s.pk))] - self.driver.find_element_by_css_selector('[name=sort_unassigned] option[value=duration]').click() - self.assertTrue(self.driver.find_element_by_css_selector('.unassigned-sessions .drop-target #session{} + #session{}'.format(*sorted_pks))) + self.driver.find_element(By.CSS_SELECTOR, '[name=sort_unassigned] option[value=duration]').click() + self.assertTrue(self.driver.find_element(By.CSS_SELECTOR, '.unassigned-sessions .drop-target #session{} ~ #session{}'.format(*sorted_pks))) sorted_pks = [s.pk for s in sorted([s1, s2, s2b], key=lambda s: (int(bool(s.comments)), s.group.parent.acronym, s.group.acronym, s.requested_duration, s.pk))] - self.driver.find_element_by_css_selector('[name=sort_unassigned] option[value=comments]').click() - self.assertTrue(self.driver.find_element_by_css_selector('.unassigned-sessions .drop-target #session{} + #session{}'.format(*sorted_pks))) + self.driver.find_element(By.CSS_SELECTOR, '[name=sort_unassigned] option[value=comments]').click() + self.assertTrue(self.driver.find_element(By.CSS_SELECTOR, '.unassigned-sessions .drop-target #session{} + #session{}'.format(*sorted_pks))) # schedule self.driver.execute_script("jQuery('#session{}').simulateDragDrop({{dropTarget: '#timeslot{} .drop-target'}});".format(s2.pk, slot1.pk)) @@ -182,30 +188,30 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase): self.assertEqual(assignment.timeslot, slot1) # timeslot constraint hints when selected - s1_element = self.driver.find_element_by_css_selector('#session{}'.format(s1.pk)) + s1_element = self.driver.find_element(By.CSS_SELECTOR, '#session{}'.format(s1.pk)) s1_element.click() # violated due to constraints - both the timeslot and its timeslot label - self.assertTrue(self.driver.find_elements_by_css_selector('#timeslot{}.would-violate-hint'.format(slot1.pk))) + self.assertTrue(self.driver.find_elements(By.CSS_SELECTOR, '#timeslot{}.would-violate-hint'.format(slot1.pk))) # Find the timeslot label for slot1 - it's the first timeslot in the first room group - slot1_roomgroup_elt = self.driver.find_element_by_css_selector( + slot1_roomgroup_elt = self.driver.find_element(By.CSS_SELECTOR, '.day-flow .day:first-child .room-group:nth-child(2)' # count from 2 - first-child is the day label ) self.assertTrue( - slot1_roomgroup_elt.find_elements_by_css_selector( + slot1_roomgroup_elt.find_elements(By.CSS_SELECTOR, '.time-header > .time-label.would-violate-hint:first-child' ), 'Timeslot header label should show a would-violate hint for a constraint violation' ) # violated due to missing capacity - self.assertTrue(self.driver.find_elements_by_css_selector('#timeslot{}.would-violate-hint'.format(slot3.pk))) + self.assertTrue(self.driver.find_elements(By.CSS_SELECTOR, '#timeslot{}.would-violate-hint'.format(slot3.pk))) # Find the timeslot label for slot3 - it's the second timeslot in the second room group - slot3_roomgroup_elt = self.driver.find_element_by_css_selector( + slot3_roomgroup_elt = self.driver.find_element(By.CSS_SELECTOR, '.day-flow .day:first-child .room-group:nth-child(3)' # count from 2 - first-child is the day label ) self.assertFalse( - slot3_roomgroup_elt.find_elements_by_css_selector( + slot3_roomgroup_elt.find_elements(By.CSS_SELECTOR, '.time-header > .time-label.would-violate-hint:nth-child(2)' ), 'Timeslot header label should not show a would-violate hint for room capacity violation' @@ -220,15 +226,15 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase): self.assertEqual(assignment.timeslot, slot2) # too many attendees warning - self.assertTrue(self.driver.find_elements_by_css_selector('#session{}.too-many-attendees'.format(s2.pk))) + self.assertTrue(self.driver.find_elements(By.CSS_SELECTOR, '#session{}.too-many-attendees'.format(s2.pk))) # overfull timeslot - self.assertTrue(self.driver.find_elements_by_css_selector('#timeslot{}.overfull'.format(slot2.pk))) + self.assertTrue(self.driver.find_elements(By.CSS_SELECTOR, '#timeslot{}.overfull'.format(slot2.pk))) # constraint hints s1_element.click() self.assertIn('would-violate-hint', s2_element.get_attribute('class')) - constraint_element = s2_element.find_element_by_css_selector(".constraints span[data-sessions=\"{}\"].would-violate-hint".format(s1.pk)) + constraint_element = s2_element.find_element(By.CSS_SELECTOR, ".constraints span[data-sessions=\"{}\"].would-violate-hint".format(s1.pk)) self.assertTrue(constraint_element.is_displayed()) # current constraint violations @@ -236,12 +242,12 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase): WebDriverWait(self.driver, 2).until(expected_conditions.presence_of_element_located((By.CSS_SELECTOR, '#timeslot{} #session{}'.format(slot1.pk, s1.pk)))) - constraint_element = s2_element.find_element_by_css_selector(".constraints span[data-sessions=\"{}\"].violated-hint".format(s1.pk)) + constraint_element = s2_element.find_element(By.CSS_SELECTOR, ".constraints span[data-sessions=\"{}\"].violated-hint".format(s1.pk)) self.assertTrue(constraint_element.is_displayed()) # hide sessions in area self.assertTrue(s1_element.is_displayed()) - self.driver.find_element_by_css_selector(".session-parent-toggles [value=\"{}\"]".format(s1.group.parent.acronym)).click() + self.driver.find_element(By.CSS_SELECTOR, ".session-parent-toggles [value=\"{}\"]".format(s1.group.parent.acronym)).click() self.assertTrue(s1_element.is_displayed()) # should still be displayed self.assertIn('hidden-parent', s1_element.get_attribute('class'), 'Session should be hidden when parent disabled') @@ -249,7 +255,7 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase): self.assertNotIn('selected', s1_element.get_attribute('class'), 'Session should not be selectable when parent disabled') - self.driver.find_element_by_css_selector(".session-parent-toggles [value=\"{}\"]".format(s1.group.parent.acronym)).click() + self.driver.find_element(By.CSS_SELECTOR, ".session-parent-toggles [value=\"{}\"]".format(s1.group.parent.acronym)).click() self.assertTrue(s1_element.is_displayed()) self.assertNotIn('hidden-parent', s1_element.get_attribute('class'), 'Session should not be hidden when parent enabled') @@ -258,32 +264,32 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase): 'Session should be selectable when parent enabled') # hide timeslots - self.driver.find_element_by_css_selector(".timeslot-group-toggles button").click() - self.assertTrue(self.driver.find_element_by_css_selector("#timeslot-group-toggles-modal").is_displayed()) - self.driver.find_element_by_css_selector("#timeslot-group-toggles-modal [value=\"{}\"]".format("ts-group-{}-{}".format(slot2.time.strftime("%Y%m%d-%H%M"), int(slot2.duration.total_seconds() / 60)))).click() - self.driver.find_element_by_css_selector("#timeslot-group-toggles-modal [data-dismiss=\"modal\"]").click() - self.assertTrue(not self.driver.find_element_by_css_selector("#timeslot-group-toggles-modal").is_displayed()) + self.driver.find_element(By.CSS_SELECTOR, "#timeslot-toggle-modal-open").click() + self.assertTrue(self.driver.find_element(By.CSS_SELECTOR, "#timeslot-group-toggles-modal").is_displayed()) + self.driver.find_element(By.CSS_SELECTOR, "#timeslot-group-toggles-modal [value=\"{}\"]".format("ts-group-{}-{}".format(slot2.time.strftime("%Y%m%d-%H%M"), int(slot2.duration.total_seconds() / 60)))).click() + self.driver.find_element(By.CSS_SELECTOR, "#timeslot-group-toggles-modal [data-dismiss=\"modal\"]").click() + self.assertTrue(not self.driver.find_element(By.CSS_SELECTOR, "#timeslot-group-toggles-modal").is_displayed()) # swap days - self.driver.find_element_by_css_selector(".day .swap-days[data-dayid=\"{}\"]".format(slot4.time.date().isoformat())).click() - self.assertTrue(self.driver.find_element_by_css_selector("#swap-days-modal").is_displayed()) - self.driver.find_element_by_css_selector("#swap-days-modal input[name=\"target_day\"][value=\"{}\"]".format(slot1.time.date().isoformat())).click() - self.driver.find_element_by_css_selector("#swap-days-modal button[type=\"submit\"]").click() + self.driver.find_element(By.CSS_SELECTOR, ".day .swap-days[data-dayid=\"{}\"]".format(slot4.time.date().isoformat())).click() + self.assertTrue(self.driver.find_element(By.CSS_SELECTOR, "#swap-days-modal").is_displayed()) + self.driver.find_element(By.CSS_SELECTOR, "#swap-days-modal input[name=\"target_day\"][value=\"{}\"]".format(slot1.time.date().isoformat())).click() + self.driver.find_element(By.CSS_SELECTOR, "#swap-days-modal button[type=\"submit\"]").click() - self.assertTrue(self.driver.find_elements_by_css_selector('#timeslot{} #session{}'.format(slot4.pk, s1.pk)), + self.assertTrue(self.driver.find_elements(By.CSS_SELECTOR, '#timeslot{} #session{}'.format(slot4.pk, s1.pk)), 'Session s1 should have moved to second meeting day') # swap timeslot column - put session in a differently-timed timeslot - self.driver.find_element_by_css_selector( + self.driver.find_element(By.CSS_SELECTOR, '.day .swap-timeslot-col[data-timeslot-pk="{}"]'.format(slot1b.pk) ).click() # open modal on the second timeslot for room1 - self.assertTrue(self.driver.find_element_by_css_selector("#swap-timeslot-col-modal").is_displayed()) - self.driver.find_element_by_css_selector( + self.assertTrue(self.driver.find_element(By.CSS_SELECTOR, "#swap-timeslot-col-modal").is_displayed()) + self.driver.find_element(By.CSS_SELECTOR, '#swap-timeslot-col-modal input[name="target_timeslot"][value="{}"]'.format(slot4.pk) ).click() # select room1 timeslot that has a session in it - self.driver.find_element_by_css_selector('#swap-timeslot-col-modal button[type="submit"]').click() + self.driver.find_element(By.CSS_SELECTOR, '#swap-timeslot-col-modal button[type="submit"]').click() - self.assertTrue(self.driver.find_elements_by_css_selector('#timeslot{} #session{}'.format(slot1b.pk, s1.pk)), + self.assertTrue(self.driver.find_elements(By.CSS_SELECTOR, '#timeslot{} #session{}'.format(slot1b.pk, s1.pk)), 'Session s1 should have moved to second timeslot on first meeting day') def test_past_flags(self): @@ -351,19 +357,19 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase): self.login(username=meeting.schedule.owner.user.username) self.driver.get(url) - past_flags = self.driver.find_elements_by_css_selector( + past_flags = self.driver.find_elements(By.CSS_SELECTOR, ','.join('#timeslot{} .past-flag'.format(ts.pk) for ts in past_timeslots) ) self.assertGreaterEqual(len(past_flags), len(past_timeslots) + len(past_sessions), 'Expected at least one flag for each past timeslot and session') - now_flags = self.driver.find_elements_by_css_selector( + now_flags = self.driver.find_elements(By.CSS_SELECTOR, ','.join('#timeslot{} .past-flag'.format(ts.pk) for ts in now_timeslots) ) self.assertGreaterEqual(len(now_flags), len(now_timeslots) + len(now_sessions), 'Expected at least one flag for each "now" timeslot and session') - future_flags = self.driver.find_elements_by_css_selector( + future_flags = self.driver.find_elements(By.CSS_SELECTOR, ','.join('#timeslot{} .past-flag'.format(ts.pk) for ts in future_timeslots) ) self.assertGreaterEqual(len(future_flags), len(future_timeslots) + len(future_sessions), @@ -417,21 +423,21 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase): self.login(username=meeting.schedule.owner.user.username) self.driver.get(url) - past_swap_days_buttons = self.driver.find_elements_by_css_selector( + past_swap_days_buttons = self.driver.find_elements(By.CSS_SELECTOR, ','.join( '.swap-days[data-start="{}"]'.format(ts.time.date().isoformat()) for ts in past_timeslots ) ) self.assertEqual(len(past_swap_days_buttons), len(past_timeslots), 'Missing past swap days buttons') - future_swap_days_buttons = self.driver.find_elements_by_css_selector( + future_swap_days_buttons = self.driver.find_elements(By.CSS_SELECTOR, ','.join( '.swap-days[data-start="{}"]'.format(ts.time.date().isoformat()) for ts in future_timeslots ) ) self.assertEqual(len(future_swap_days_buttons), len(future_timeslots), 'Missing future swap days buttons') - now_swap_days_buttons = self.driver.find_elements_by_css_selector( + now_swap_days_buttons = self.driver.find_elements(By.CSS_SELECTOR, ','.join( '.swap-days[data-start="{}"]'.format(ts.time.date().isoformat()) for ts in now_timeslots ) @@ -475,7 +481,7 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase): self.fail('Modal never appeared') self.assertFalse( any(radio.is_enabled() - for radio in modal.find_elements_by_css_selector(','.join( + for radio in modal.find_elements(By.CSS_SELECTOR, ','.join( 'input[name="target_day"][value="{}"]'.format(ts.time.date().isoformat()) for ts in past_timeslots) )), 'Past day is enabled in swap-days modal for official schedule', @@ -484,14 +490,14 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase): enabled_timeslots = (ts for ts in future_timeslots if ts != future_timeslots[clicked_index]) self.assertTrue( all(radio.is_enabled() - for radio in modal.find_elements_by_css_selector(','.join( + for radio in modal.find_elements(By.CSS_SELECTOR, ','.join( 'input[name="target_day"][value="{}"]'.format(ts.time.date().isoformat()) for ts in enabled_timeslots) )), 'Future day is not enabled in swap-days modal for official schedule', ) self.assertFalse( any(radio.is_enabled() - for radio in modal.find_elements_by_css_selector(','.join( + for radio in modal.find_elements(By.CSS_SELECTOR, ','.join( 'input[name="target_day"][value="{}"]'.format(ts.time.date().isoformat()) for ts in now_timeslots) )), '"Now" day is enabled in swap-days modal for official schedule', @@ -533,21 +539,21 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase): self.login(username=meeting.schedule.owner.user.username) self.driver.get(url) - past_swap_ts_buttons = self.driver.find_elements_by_css_selector( + past_swap_ts_buttons = self.driver.find_elements(By.CSS_SELECTOR, ','.join( '.swap-timeslot-col[data-start="{}"]'.format(ts.utc_start_time().isoformat()) for ts in past_timeslots ) ) self.assertEqual(len(past_swap_ts_buttons), len(past_timeslots), 'Missing past swap timeslot col buttons') - future_swap_ts_buttons = self.driver.find_elements_by_css_selector( + future_swap_ts_buttons = self.driver.find_elements(By.CSS_SELECTOR, ','.join( '.swap-timeslot-col[data-start="{}"]'.format(ts.utc_start_time().isoformat()) for ts in future_timeslots ) ) self.assertEqual(len(future_swap_ts_buttons), len(future_timeslots), 'Missing future swap timeslot col buttons') - now_swap_ts_buttons = self.driver.find_elements_by_css_selector( + now_swap_ts_buttons = self.driver.find_elements(By.CSS_SELECTOR, ','.join( '.swap-timeslot-col[data-start="{}"]'.format(ts.utc_start_time().isoformat()) for ts in now_timeslots ) @@ -590,7 +596,7 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase): self.fail('Modal never appeared') self.assertFalse( any(radio.is_enabled() - for radio in modal.find_elements_by_css_selector(','.join( + for radio in modal.find_elements(By.CSS_SELECTOR, ','.join( 'input[name="target_timeslot"][value="{}"]'.format(ts.pk) for ts in past_timeslots) )), 'Past timeslot is enabled in swap-timeslot-col modal for official schedule', @@ -599,14 +605,14 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase): enabled_timeslots = (ts for ts in future_timeslots if ts != future_timeslots[clicked_index]) self.assertTrue( all(radio.is_enabled() - for radio in modal.find_elements_by_css_selector(','.join( + for radio in modal.find_elements(By.CSS_SELECTOR, ','.join( 'input[name="target_timeslot"][value="{}"]'.format(ts.pk) for ts in enabled_timeslots) )), 'Future timeslot is not enabled in swap-timeslot-col modal for official schedule', ) self.assertFalse( any(radio.is_enabled() - for radio in modal.find_elements_by_css_selector(','.join( + for radio in modal.find_elements(By.CSS_SELECTOR, ','.join( 'input[name="target_timeslot"][value="{}"]'.format(ts.pk) for ts in now_timeslots) )), '"Now" timeslot is enabled in swap-timeslot-col modal for official schedule', @@ -625,7 +631,7 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase): def sort_by_position(driver, sessions): """Helper to sort sessions by the position of their session element in the unscheduled box""" def _sort_key(sess): - elt = driver.find_element_by_id('session{}'.format(sess.pk)) + elt = driver.find_element(By.ID, 'session{}'.format(sess.pk)) return (elt.location['y'], elt.location['x']) return sorted(sessions, key=_sort_key) @@ -687,10 +693,10 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase): self.login('secretary') self.driver.get(url) - select = self.driver.find_element_by_name('sort_unassigned') + select = self.driver.find_element(By.NAME, 'sort_unassigned') options = { opt.get_attribute('value'): opt - for opt in select.find_elements_by_tag_name('option') + for opt in select.find_elements(By.TAG_NAME, 'option') } # check sorting by name @@ -760,18 +766,8 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase): all of the events needed by the editor. """ # Set up a meeting and a schedule a plain user can edit - meeting = make_meeting_test_data() - schedule = Schedule.objects.filter(meeting=meeting, owner__user__username="plain").first() - sessions = meeting.session_set.filter(type_id='regular') - timeslots = meeting.timeslot_set.filter(type_id='regular') - self.assertGreaterEqual(timeslots.count(), sessions.count(), - 'Need a timeslot for each session') - for index, session in enumerate(sessions): - SchedTimeSessAssignment.objects.create( - schedule=schedule, - timeslot=timeslots[index], - session=session, - ) + schedule = ScheduleFactory(owner__user__username="plain") + meeting = schedule.meeting # Open the editor self.login() @@ -780,12 +776,11 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase): kwargs=dict(num=meeting.number, name=schedule.name, owner=schedule.owner_email()) ) self.driver.get(url) - # Check that the drop target for unassigned sessions is actually empty - drop_target = self.driver.find_element_by_css_selector( + drop_target = self.driver.find_element(By.CSS_SELECTOR, '.unassigned-sessions .drop-target' ) - self.assertEqual(len(drop_target.find_elements_by_class_name('session')), 0, + self.assertEqual(len(drop_target.find_elements(By.CLASS_NAME, 'session')), 0, 'Unassigned sessions box is not empty, test is broken') # Check that the drop target has non-zero size @@ -829,7 +824,7 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase): kwargs=dict(num=meeting.number, owner=schedule.owner.email(), name=schedule.name)) self.login(schedule.owner.user.username) self.driver.get(url) - session_elements = [self.driver.find_element_by_css_selector(f'#session{sess.pk}') for sess in sessions] + session_elements = [self.driver.find_element(By.CSS_SELECTOR, f'#session{sess.pk}') for sess in sessions] session_elements[0].click() # All conflicting sessions should be flagged with the would-violate-hint class. @@ -864,20 +859,20 @@ class ScheduleEditTests(IetfSeleniumTestCase): # driver.get() will wait for scripts to finish, but not ajax # requests. Wait for completion of the permissions check: - read_only_note = self.driver.find_element_by_id('read_only') + read_only_note = self.driver.find_element(By.ID, 'read_only') WebDriverWait(self.driver, 10).until(expected_conditions.invisibility_of_element(read_only_note), "Read-only schedule") s1 = Session.objects.filter(group__acronym='mars', meeting=meeting).first() selector = "#session_{}".format(s1.pk) WebDriverWait(self.driver, 30).until(expected_conditions.presence_of_element_located((By.CSS_SELECTOR, selector)), "Did not find %s"%selector) - self.assertEqual(self.driver.find_elements_by_css_selector("#sortable-list #session_{}".format(s1.pk)), []) + self.assertEqual(self.driver.find_elements(By.CSS_SELECTOR, "#sortable-list #session_{}".format(s1.pk)), []) - element = self.driver.find_element_by_id('session_{}'.format(s1.pk)) - target = self.driver.find_element_by_id('sortable-list') + element = self.driver.find_element(By.ID, 'session_{}'.format(s1.pk)) + target = self.driver.find_element(By.ID, 'sortable-list') ActionChains(self.driver).drag_and_drop(element,target).perform() - self.assertTrue(self.driver.find_elements_by_css_selector("#sortable-list #session_{}".format(s1.pk))) + self.assertTrue(self.driver.find_elements(By.CSS_SELECTOR, "#sortable-list #session_{}".format(s1.pk))) time.sleep(0.1) # The API that modifies the database runs async @@ -905,8 +900,8 @@ class SlideReorderTests(IetfSeleniumTestCase): self.secr_login() self.driver.get(url) #debug.show('unicode(self.driver.page_source)') - second = self.driver.find_element_by_css_selector('#slides tr:nth-child(2)') - third = self.driver.find_element_by_css_selector('#slides tr:nth-child(3)') + second = self.driver.find_element(By.CSS_SELECTOR, '#slides tr:nth-child(2)') + third = self.driver.find_element(By.CSS_SELECTOR, '#slides tr:nth-child(3)') ActionChains(self.driver).drag_and_drop(second,third).perform() time.sleep(0.1) # The API that modifies the database runs async @@ -1003,7 +998,7 @@ class AgendaTests(IetfSeleniumTestCase): self.driver.get(self.absreverse('ietf.meeting.views.agenda') + querystring) self.assert_agenda_item_visibility(visible_groups) self.assert_agenda_view_filter_matches_ics_filter(querystring) - weekview_iframe = self.driver.find_element_by_id('weekview') + weekview_iframe = self.driver.find_element(By.ID, 'weekview') if len(querystring) == 0: self.assertFalse(weekview_iframe.is_displayed(), 'Weekview should be hidden when filters off') else: @@ -1184,7 +1179,7 @@ class AgendaTests(IetfSeleniumTestCase): for item in self.get_expected_items(): row_id = self.row_id_for_item(item) try: - item_row = self.driver.find_element_by_id(row_id) + item_row = self.driver.find_element(By.ID, row_id) except NoSuchElementException: item_row = None self.assertIsNotNone(item_row, 'No row for schedule item "%s"' % row_id) @@ -1205,7 +1200,7 @@ class AgendaTests(IetfSeleniumTestCase): label = 'Free Slot' try: - item_div = self.driver.find_element_by_xpath('//div/span[contains(text(),"%s")]/..' % label) + item_div = self.driver.find_element(By.XPATH, '//div/span[contains(text(),"%s")]/..' % label) except NoSuchElementException: item_div = None @@ -1480,7 +1475,7 @@ class AgendaTests(IetfSeleniumTestCase): ics_url = self.absreverse('ietf.meeting.views.agenda_ical') # parse out the events - agenda_rows = self.driver.find_elements_by_css_selector('[id^="row-"]:not(.info)') + agenda_rows = self.driver.find_elements(By.CSS_SELECTOR, '[id^="row-"]:not(.info)') visible_rows = [r for r in agenda_rows if r.is_displayed()] sessions = [self.session_from_agenda_row_id(row.get_attribute("id")) for row in visible_rows] @@ -1510,7 +1505,7 @@ class AgendaTests(IetfSeleniumTestCase): self.driver.get(url) # modal should start hidden - modal_div = self.driver.find_element_by_css_selector('div#modal-%s' % slug) + modal_div = self.driver.find_element(By.CSS_SELECTOR, 'div#modal-%s' % slug) self.assertFalse(modal_div.is_displayed()) # Click the 'materials' button @@ -1535,7 +1530,7 @@ class AgendaTests(IetfSeleniumTestCase): ) self.assertGreater(not_deleted_slides.count(), 0) # make sure this isn't a pointless test for slide in not_deleted_slides: - anchor = self.driver.find_element_by_xpath('//a[text()="%s"]' % slide.title) + anchor = self.driver.find_element(By.XPATH, '//a[text()="%s"]' % slide.title) self.assertIsNotNone(anchor) deleted_slides = session.materials.filter( @@ -1544,7 +1539,7 @@ class AgendaTests(IetfSeleniumTestCase): self.assertGreater(deleted_slides.count(), 0) # make sure this isn't a pointless test for slide in deleted_slides: with self.assertRaises(NoSuchElementException): - self.driver.find_element_by_xpath('//a[text()="%s"]' % slide.title) + self.driver.find_element(By.XPATH, '//a[text()="%s"]' % slide.title) # Now close the modal close_modal_button = WebDriverWait(self.driver, 2).until( @@ -1589,7 +1584,7 @@ class AgendaTests(IetfSeleniumTestCase): self.assertNotIn(newly_deleted_slide, not_deleted_slides) self.assertIn(newly_undeleted_slide, not_deleted_slides) for slide in not_deleted_slides: - anchor = self.driver.find_element_by_xpath('//a[text()="%s"]' % slide.title) + anchor = self.driver.find_element(By.XPATH, '//a[text()="%s"]' % slide.title) self.assertIsNotNone(anchor) deleted_slides = session.materials.filter( @@ -1599,7 +1594,7 @@ class AgendaTests(IetfSeleniumTestCase): self.assertNotIn(newly_undeleted_slide, deleted_slides) for slide in deleted_slides: with self.assertRaises(NoSuchElementException): - self.driver.find_element_by_xpath('//a[text()="%s"]' % slide.title) + self.driver.find_element(By.XPATH, '//a[text()="%s"]' % slide.title) def test_agenda_time_zone_selection(self): self.assertNotEqual(self.meeting.time_zone, 'UTC', 'Meeting time zone must not be UTC') @@ -1616,14 +1611,14 @@ class AgendaTests(IetfSeleniumTestCase): ) ) - tz_select_input = self.driver.find_element_by_id('timezone-select') - meeting_tz_link = self.driver.find_element_by_id('meeting-timezone') - local_tz_link = self.driver.find_element_by_id('local-timezone') - utc_tz_link = self.driver.find_element_by_id('utc-timezone') - tz_displays = self.driver.find_elements_by_css_selector('.current-tz') + tz_select_input = self.driver.find_element(By.ID, 'timezone-select') + meeting_tz_link = self.driver.find_element(By.ID, 'meeting-timezone') + local_tz_link = self.driver.find_element(By.ID, 'local-timezone') + utc_tz_link = self.driver.find_element(By.ID, 'utc-timezone') + tz_displays = self.driver.find_elements(By.CSS_SELECTOR, '.current-tz') self.assertGreaterEqual(len(tz_displays), 1) # we'll check that all current-tz elements are updated, but first check that at least one is in the nav sidebar - self.assertIsNotNone(self.driver.find_element_by_css_selector('.nav .current-tz')) + self.assertIsNotNone(self.driver.find_element(By.CSS_SELECTOR, '.nav .current-tz')) # Moment.js guesses local time zone based on the behavior of Selenium's web client. This seems # to inherit Django's settings.TIME_ZONE but I don't know whether that's guaranteed to be consistent. @@ -1632,9 +1627,9 @@ class AgendaTests(IetfSeleniumTestCase): self.assertNotEqual(self.meeting.time_zone, local_tz, 'Meeting time zone must not be local time zone') self.assertNotEqual(local_tz, 'UTC', 'Local time zone must not be UTC') - meeting_tz_opt = tz_select_input.find_element_by_css_selector('option[value="%s"]' % self.meeting.time_zone) - local_tz_opt = tz_select_input.find_element_by_css_selector('option[value="%s"]' % local_tz) - utc_tz_opt = tz_select_input.find_element_by_css_selector('option[value="UTC"]') + meeting_tz_opt = tz_select_input.find_element(By.CSS_SELECTOR, 'option[value="%s"]' % self.meeting.time_zone) + local_tz_opt = tz_select_input.find_element(By.CSS_SELECTOR, 'option[value="%s"]' % local_tz) + utc_tz_opt = tz_select_input.find_element(By.CSS_SELECTOR, 'option[value="UTC"]') # Should start off in meeting time zone self.assertTrue(meeting_tz_opt.is_selected()) @@ -1753,21 +1748,21 @@ class AgendaTests(IetfSeleniumTestCase): # Verify that elements are all updated when the filters change. That the correct elements # have the appropriate classes is a separate test. - elements_to_check = self.driver.find_elements_by_css_selector('.agenda-link.filterable') + elements_to_check = self.driver.find_elements(By.CSS_SELECTOR, '.agenda-link.filterable') self.assertGreater(len(elements_to_check), 0, 'No elements with agenda links to update were found') self.assertFalse( any(checkbox.is_selected() - for checkbox in self.driver.find_elements_by_css_selector( + for checkbox in self.driver.find_elements(By.CSS_SELECTOR, 'input.checkbox[name="selected-sessions"]')), 'Sessions were selected before being clicked', ) - mars_sessa_checkbox = self.driver.find_element_by_css_selector('input[type="checkbox"][name="selected-sessions"][data-filter-item="mars-sessa"]') - mars_sessb_checkbox = self.driver.find_element_by_css_selector('input[type="checkbox"][name="selected-sessions"][data-filter-item="mars-sessb"]') - farfut_button = self.driver.find_element_by_css_selector('button[data-filter-item="farfut"]') - break_checkbox = self.driver.find_element_by_css_selector('input[type="checkbox"][name="selected-sessions"][data-filter-item="secretariat-sessb"]') - registration_checkbox = self.driver.find_element_by_css_selector('input[type="checkbox"][name="selected-sessions"][data-filter-item="secretariat-sessa"]') + mars_sessa_checkbox = self.driver.find_element(By.CSS_SELECTOR, 'input[type="checkbox"][name="selected-sessions"][data-filter-item="mars-sessa"]') + mars_sessb_checkbox = self.driver.find_element(By.CSS_SELECTOR, 'input[type="checkbox"][name="selected-sessions"][data-filter-item="mars-sessb"]') + farfut_button = self.driver.find_element(By.CSS_SELECTOR, 'button[data-filter-item="farfut"]') + break_checkbox = self.driver.find_element(By.CSS_SELECTOR, 'input[type="checkbox"][name="selected-sessions"][data-filter-item="secretariat-sessb"]') + registration_checkbox = self.driver.find_element(By.CSS_SELECTOR, 'input[type="checkbox"][name="selected-sessions"][data-filter-item="secretariat-sessa"]') mars_sessa_checkbox.click() # select mars session try: @@ -1884,9 +1879,9 @@ class WeekviewTests(IetfSeleniumTestCase): def _assert_wrapped(displayed, expected_time_string): self.assertEqual(len(displayed), 2) first = displayed[0] - first_parent = first.find_element_by_xpath('..') + first_parent = first.find_element(By.XPATH, '..') second = displayed[1] - second_parent = second.find_element_by_xpath('..') + second_parent = second.find_element(By.XPATH, '..') self.assertNotIn('continued', first.text) self.assertIn(expected_time_string, first_parent.text) self.assertIn('continued', second.text) @@ -1895,7 +1890,7 @@ class WeekviewTests(IetfSeleniumTestCase): def _assert_not_wrapped(displayed, expected_time_string): self.assertEqual(len(displayed), 1) first = displayed[0] - first_parent = first.find_element_by_xpath('..') + first_parent = first.find_element(By.XPATH, '..') self.assertNotIn('continued', first.text) self.assertIn(expected_time_string, first_parent.text) @@ -2021,6 +2016,7 @@ class InterimTests(IetfSeleniumTestCase): sg_interim = make_interim_meeting(somegroup, datetime.date.today() + datetime.timedelta(days=20)) sg_sess = sg_interim.session_set.first() sg_slot = sg_sess.timeslotassignments.first().timeslot + sg_sess.purpose_id = 'plenary' sg_sess.type_id = 'plenary' sg_slot.type_id = 'plenary' sg_sess.save() @@ -2069,7 +2065,7 @@ class InterimTests(IetfSeleniumTestCase): return meetings def find_upcoming_meeting_entries(self): - return self.driver.find_elements_by_css_selector( + return self.driver.find_elements(By.CSS_SELECTOR, 'table#upcoming-meeting-table a.ietf-meeting-link, table#upcoming-meeting-table a.interim-meeting-link' ) @@ -2119,7 +2115,7 @@ class InterimTests(IetfSeleniumTestCase): # 12 in order to check the starting month of the following year, which # will usually contain the day 1 year from the start date. for _ in range(13): - entries = self.driver.find_elements_by_css_selector( + entries = self.driver.find_elements(By.CSS_SELECTOR, 'div#calendar div.fc-content' ) for entry in entries: @@ -2150,9 +2146,9 @@ class InterimTests(IetfSeleniumTestCase): if simplified_querystring in ['?show=', '?hide=', '?show=&hide=']: simplified_querystring = '' # these empty querystrings will be dropped (not an exhaustive list) - ics_link = self.driver.find_element_by_link_text('Download as .ics') + ics_link = self.driver.find_element(By.LINK_TEXT, 'Download as .ics') self.assertIn(simplified_querystring, ics_link.get_attribute('href')) - webcal_link = self.driver.find_element_by_link_text('Subscribe with webcal') + webcal_link = self.driver.find_element(By.LINK_TEXT, 'Subscribe with webcal') self.assertIn(simplified_querystring, webcal_link.get_attribute('href')) def assert_upcoming_view_filter_matches_ics_filter(self, filter_string): @@ -2314,8 +2310,8 @@ class InterimTests(IetfSeleniumTestCase): ts = session.official_timeslotassignment().timeslot start = ts.utc_start_time().astimezone(zone).strftime('%Y-%m-%d %H:%M') end = ts.utc_end_time().astimezone(zone).strftime('%H:%M') - meeting_link = self.driver.find_element_by_link_text(session.meeting.number) - time_td = meeting_link.find_element_by_xpath('../../td[@class="session-time"]') + meeting_link = self.driver.find_element(By.LINK_TEXT, session.meeting.number) + time_td = meeting_link.find_element(By.XPATH, '../../td[@class="session-time"]') self.assertIn('%s - %s' % (start, end), time_td.text) def _assert_ietf_tz_correct(meetings, tz): @@ -2333,8 +2329,8 @@ class InterimTests(IetfSeleniumTestCase): start = start_dt.astimezone(zone).strftime('%Y-%m-%d') end = end_dt.astimezone(zone).strftime('%Y-%m-%d') - meeting_link = self.driver.find_element_by_link_text("IETF " + meeting.number) - time_td = meeting_link.find_element_by_xpath('../../td[@class="meeting-time"]') + meeting_link = self.driver.find_element(By.LINK_TEXT, "IETF " + meeting.number) + time_td = meeting_link.find_element(By.XPATH, '../../td[@class="meeting-time"]') self.assertIn('%s - %s' % (start, end), time_td.text) sessions = [m.session_set.first() for m in self.displayed_interims()] @@ -2343,12 +2339,12 @@ class InterimTests(IetfSeleniumTestCase): self.assertGreater(len(ietf_meetings), 0) self.driver.get(self.absreverse('ietf.meeting.views.upcoming')) - tz_select_input = self.driver.find_element_by_id('timezone-select') - tz_select_bottom_input = self.driver.find_element_by_id('timezone-select-bottom') - local_tz_link = self.driver.find_element_by_id('local-timezone') - utc_tz_link = self.driver.find_element_by_id('utc-timezone') - local_tz_bottom_link = self.driver.find_element_by_id('local-timezone-bottom') - utc_tz_bottom_link = self.driver.find_element_by_id('utc-timezone-bottom') + tz_select_input = self.driver.find_element(By.ID, 'timezone-select') + tz_select_bottom_input = self.driver.find_element(By.ID, 'timezone-select-bottom') + local_tz_link = self.driver.find_element(By.ID, 'local-timezone') + utc_tz_link = self.driver.find_element(By.ID, 'utc-timezone') + local_tz_bottom_link = self.driver.find_element(By.ID, 'local-timezone-bottom') + utc_tz_bottom_link = self.driver.find_element(By.ID, 'utc-timezone-bottom') # wait for the select box to be updated - look for an arbitrary time zone to be in # its options list to detect this @@ -2358,18 +2354,18 @@ class InterimTests(IetfSeleniumTestCase): (By.CSS_SELECTOR, '#timezone-select > option[value="%s"]' % arbitrary_tz) ) ) - arbitrary_tz_bottom_opt = tz_select_bottom_input.find_element_by_css_selector( + arbitrary_tz_bottom_opt = tz_select_bottom_input.find_element(By.CSS_SELECTOR, 'option[value="%s"]' % arbitrary_tz) - utc_tz_opt = tz_select_input.find_element_by_css_selector('option[value="UTC"]') - utc_tz_bottom_opt= tz_select_bottom_input.find_element_by_css_selector('option[value="UTC"]') + utc_tz_opt = tz_select_input.find_element(By.CSS_SELECTOR, 'option[value="UTC"]') + utc_tz_bottom_opt= tz_select_bottom_input.find_element(By.CSS_SELECTOR, 'option[value="UTC"]') # Moment.js guesses local time zone based on the behavior of Selenium's web client. This seems # to inherit Django's settings.TIME_ZONE but I don't know whether that's guaranteed to be consistent. # To avoid test fragility, ask Moment what it considers local and expect that. local_tz = self.driver.execute_script('return moment.tz.guess();') - local_tz_opt = tz_select_input.find_element_by_css_selector('option[value=%s]' % local_tz) - local_tz_bottom_opt = tz_select_bottom_input.find_element_by_css_selector('option[value="%s"]' % local_tz) + local_tz_opt = tz_select_input.find_element(By.CSS_SELECTOR, 'option[value=%s]' % local_tz) + local_tz_bottom_opt = tz_select_bottom_input.find_element(By.CSS_SELECTOR, 'option[value="%s"]' % local_tz) # Should start off in local time zone self.assertTrue(local_tz_opt.is_selected()) @@ -2466,7 +2462,7 @@ class InterimTests(IetfSeleniumTestCase): slug = assignment.slug() # modal should start hidden - modal_div = self.driver.find_element_by_css_selector('div#modal-%s' % slug) + modal_div = self.driver.find_element(By.CSS_SELECTOR, 'div#modal-%s' % slug) self.assertFalse(modal_div.is_displayed()) # Click the 'materials' button diff --git a/ietf/meeting/tests_views.py b/ietf/meeting/tests_views.py index fda407ada..cae8d2a39 100644 --- a/ietf/meeting/tests_views.py +++ b/ietf/meeting/tests_views.py @@ -322,12 +322,12 @@ class MeetingTests(BaseMeetingTestCase): parent=iab, list_email="venus@ietf.org", ) - venus_session = Session.objects.create( + venus_session = SessionFactory( meeting=meeting, group=venus, attendees=10, requested_duration=datetime.timedelta(minutes=60), - type_id='regular', + add_to_schedule=False, ) system_person = Person.objects.get(name="(System)") SchedulingEvent.objects.create(session=venus_session, status_id='schedw', by=system_person) @@ -784,7 +784,7 @@ class MeetingTests(BaseMeetingTestCase): ) self.do_ical_filter_test( meeting, - querystring='?show=plenary,secretariat,ames&hide=reg', + querystring='?show=plenary,secretariat,ames&hide=admin', expected_session_summaries=[ 'Morning Break', 'IETF Plenary', @@ -3062,10 +3062,14 @@ class EditTests(TestCase): def test_edit_schedule(self): meeting = make_meeting_test_data() - + self.client.login(username="secretary", password="secretary+password") - r = self.client.get(urlreverse("ietf.meeting.views.edit_schedule", kwargs=dict(num=meeting.number))) - self.assertContains(r, "load_assignments") + r = self.client.get(urlreverse("ietf.meeting.views.edit_schedule", kwargs={'num': meeting.number})) + self.assertRedirects( + r, + urlreverse("ietf.meeting.views.edit_meeting_schedule", kwargs={'num': meeting.number}), + status_code=301, + ) def test_official_record_schedule_is_read_only(self): def _set_date_offset_and_retrieve_page(meeting, days_offset, client): @@ -3156,9 +3160,9 @@ class EditTests(TestCase): timeslots = list(TimeSlot.objects.filter(meeting=meeting, type='regular').order_by('time')) - base_session = Session.objects.create(meeting=meeting, group=Group.objects.get(acronym="irg"), - attendees=20, requested_duration=datetime.timedelta(minutes=30), - type_id='regular') + 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) @@ -3872,9 +3876,9 @@ class EditScheduleListTests(TestCase): session1 = Session.objects.filter(meeting=meeting, group__acronym='mars').first() session2 = Session.objects.filter(meeting=meeting, group__acronym='ames').first() - session3 = Session.objects.create(meeting=meeting, group=Group.objects.get(acronym='mars'), - attendees=10, requested_duration=datetime.timedelta(minutes=70), - type_id='regular') + 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() @@ -6200,11 +6204,13 @@ class AgendaFilterTests(TestCase): dict( label='child00', keyword='keyword00', + toggled_by=['keyword0'], is_bof=False, ), dict( label='child01', keyword='keyword01', + toggled_by=['keyword0', 'bof'], is_bof=True, ), ]), @@ -6215,11 +6221,13 @@ class AgendaFilterTests(TestCase): dict( label='child10', keyword='keyword10', + toggled_by=['keyword1'], is_bof=False, ), dict( label='child11', keyword='keyword11', + toggled_by=['keyword1', 'bof'], is_bof=True, ), ]), @@ -6232,11 +6240,13 @@ class AgendaFilterTests(TestCase): dict( label='child20', keyword='keyword20', + toggled_by=['keyword2', 'bof'], is_bof=True, ), dict( label='child21', keyword='keyword21', + toggled_by=['keyword2'], is_bof=False, ), ]), @@ -6249,11 +6259,13 @@ class AgendaFilterTests(TestCase): dict( label='child30', keyword='keyword30', + toggled_by=[], is_bof=False, ), dict( label='child31', keyword='keyword31', + toggled_by=['bof'], is_bof=True, ), ]), @@ -6283,7 +6295,6 @@ class AgendaFilterTests(TestCase): _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'), diff --git a/ietf/meeting/urls.py b/ietf/meeting/urls.py index f87870d3d..7e42e1940 100644 --- a/ietf/meeting/urls.py +++ b/ietf/meeting/urls.py @@ -88,7 +88,7 @@ type_ietf_only_patterns_id_optional = [ url(r'^agenda(?P.csv)$', views.agenda), url(r'^agenda/edit$', RedirectView.as_view(pattern_name='ietf.meeting.views.edit_meeting_schedule', permanent=True), - name='ietf.meetingviews.edit_schedule'), + name='ietf.meeting.views.edit_schedule'), url(r'^agenda/edit/$', views.edit_meeting_schedule), url(r'^requests$', views.meeting_requests), url(r'^agenda/agenda\.ics$', views.agenda_ical), diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py index 65315ba46..fcadf6bfb 100644 --- a/ietf/meeting/views.py +++ b/ietf/meeting/views.py @@ -88,7 +88,7 @@ from ietf.meeting.utils import diff_meeting_schedules, prefetch_schedule_diff_ob from ietf.meeting.utils import swap_meeting_schedule_timeslot_assignments, bulk_create_timeslots from ietf.meeting.utils import preprocess_meeting_important_dates from ietf.message.utils import infer_message -from ietf.name.models import SlideSubmissionStatusName, ProceedingsMaterialTypeName +from ietf.name.models import SlideSubmissionStatusName, ProceedingsMaterialTypeName, SessionPurposeName from ietf.secr.proceedings.utils import handle_upload_file from ietf.secr.proceedings.proc_utils import (get_progress_stats, post_process, import_audio_files, create_recording) @@ -621,7 +621,7 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None): s.scheduling_label = "???" s.purpose_label = None - if (s.purpose is None or s.purpose.slug == 'regular') and s.group: + if (s.purpose.slug in ('none', 'regular')) and s.group: s.scheduling_label = s.group.acronym s.purpose_label = 'BoF' if s.group.is_bof() else s.group.type.name else: @@ -1058,6 +1058,7 @@ class TimeSlotForm(forms.Form): location = RoomNameModelChoiceField(queryset=Room.objects.all(), required=False, empty_label="(No location)") show_location = forms.BooleanField(initial=True, required=False) type = forms.ModelChoiceField(queryset=TimeSlotTypeName.objects.filter(used=True), empty_label=None, required=False) + purpose = forms.ModelChoiceField(queryset=SessionPurposeName.objects.filter(used=True), required=False, widget=forms.HiddenInput) name = forms.CharField(help_text='Name that appears on the agenda', required=False) short = forms.CharField(max_length=32,label='Short name', help_text='Abbreviated session name used for material file names', required=False) group = forms.ModelChoiceField(queryset=Group.objects.filter(type__in=['ietf', 'team'], state='active'), @@ -1083,6 +1084,12 @@ class TimeSlotForm(forms.Form): self.active_assignment = None + # only allow timeslots with at least one purpose + timeslot_types_with_purpose = set() + for spn in SessionPurposeName.objects.filter(used=True): + timeslot_types_with_purpose.update(spn.timeslot_types) + self.fields['type'].queryset = self.fields['type'].queryset.filter(pk__in=timeslot_types_with_purpose) + if timeslot: self.initial = { 'day': timeslot.time.date(), @@ -1115,7 +1122,10 @@ class TimeSlotForm(forms.Form): ts_type = self.cleaned_data.get('type') short = self.cleaned_data.get('short') - if ts_type: + if not ts_type: + # assign a generic purpose if no type has been set + self.cleaned_data['purpose'] = SessionPurposeName.objects.get(slug='open_meeting') + else: if ts_type.slug in ['break', 'reg', 'reserved', 'unavail', 'regular']: if ts_type.slug != 'regular': self.cleaned_data['group'] = self.fields['group'].queryset.get(acronym='secretariat') @@ -1128,6 +1138,15 @@ class TimeSlotForm(forms.Form): if self.timeslot and self.timeslot.type.slug == 'regular' and self.active_assignment and ts_type.slug != self.timeslot.type.slug: self.add_error('type', "Can't change type on time slots for regular sessions when a session has been assigned") + # find an allowed session purpose (guaranteed by TimeSlotForm) + for purpose in SessionPurposeName.objects.filter(used=True): + if ts_type.pk in purpose.timeslot_types: + self.cleaned_data['purpose'] = purpose + break + if self.cleaned_data['purpose'] is None: + self.add_error('type', f'{ts_type} has no allowed purposes') + + if (self.active_assignment and self.active_assignment.session.group != self.cleaned_data.get('group') and self.active_assignment.session.materials.exists() @@ -1211,6 +1230,7 @@ def edit_meeting_timeslots_and_misc_sessions(request, num=None, owner=None, name short=c['short'], group=c['group'], type=c['type'], + purpose=c['purpose'], agenda_note=c.get('agenda_note') or "", ) diff --git a/ietf/name/fixtures/names.json b/ietf/name/fixtures/names.json index 213a3efab..dd87e5a74 100644 --- a/ietf/name/fixtures/names.json +++ b/ietf/name/fixtures/names.json @@ -2620,6 +2620,7 @@ ], "req_subm_approval": true, "role_order": "[\n \"chair\",\n \"lead\",\n \"delegate\",\n \"matman\"\n]", + "session_purposes": "[\n \"presentation\"\n]", "show_on_agenda": true }, "model": "group.groupfeatures", @@ -2656,6 +2657,7 @@ "parent_types": [], "req_subm_approval": false, "role_order": "[\n \"chair\"\n]", + "session_purposes": "[\n \"closed_meeting\",\n \"officehours\"\n]", "show_on_agenda": false }, "model": "group.groupfeatures", @@ -2695,6 +2697,7 @@ ], "req_subm_approval": true, "role_order": "[\n \"chair\",\n \"secr\"\n]", + "session_purposes": "[\n \"regular\"\n]", "show_on_agenda": true }, "model": "group.groupfeatures", @@ -2733,6 +2736,7 @@ ], "req_subm_approval": true, "role_order": "[\n \"chair\",\n \"secr\"\n]", + "session_purposes": "[\n \"regular\"\n]", "show_on_agenda": false }, "model": "group.groupfeatures", @@ -2771,6 +2775,7 @@ ], "req_subm_approval": true, "role_order": "[\n \"chair\",\n \"secr\"\n]", + "session_purposes": "[\n \"open_meeting\",\n \"presentation\",\n \"regular\",\n \"social\",\n \"tutorial\"\n]", "show_on_agenda": false }, "model": "group.groupfeatures", @@ -2809,6 +2814,7 @@ ], "req_subm_approval": true, "role_order": "[\n \"chair\",\n \"secr\"\n]", + "session_purposes": "[\n \"closed_meeting\",\n \"regular\"\n]", "show_on_agenda": true }, "model": "group.groupfeatures", @@ -2819,6 +2825,7 @@ "about_page": "ietf.group.views.group_about", "acts_like_wg": false, "admin_roles": "[\n \"lead\"\n]", + "agenda_filter_type": "none", "agenda_type": "ad", "create_wiki": false, "custom_group_roles": true, @@ -2846,6 +2853,7 @@ ], "req_subm_approval": false, "role_order": "[\n \"lead\",\n \"chair\",\n \"secr\"\n]", + "session_purposes": "[\n \"closed_meeting\",\n \"officehours\",\n \"open_meeting\"\n]", "show_on_agenda": false }, "model": "group.groupfeatures", @@ -2882,6 +2890,7 @@ "parent_types": [], "req_subm_approval": false, "role_order": "[\n \"chair\"\n]", + "session_purposes": "[\n \"officehours\"\n]", "show_on_agenda": false }, "model": "group.groupfeatures", @@ -2918,6 +2927,7 @@ "parent_types": [], "req_subm_approval": true, "role_order": "[\n \"chair\",\n \"delegate\",\n \"member\"\n]", + "session_purposes": "[\n \"closed_meeting\",\n \"open_meeting\"\n]", "show_on_agenda": false }, "model": "group.groupfeatures", @@ -2956,6 +2966,7 @@ ], "req_subm_approval": true, "role_order": "[\n \"chair\",\n \"secr\"\n]", + "session_purposes": "[\n \"admin\",\n \"plenary\",\n \"presentation\",\n \"social\"\n]", "show_on_agenda": false }, "model": "group.groupfeatures", @@ -2994,6 +3005,7 @@ ], "req_subm_approval": false, "role_order": "[\n \"chair\",\n \"secr\"\n]", + "session_purposes": "[]", "show_on_agenda": false }, "model": "group.groupfeatures", @@ -3032,6 +3044,7 @@ ], "req_subm_approval": true, "role_order": "[\n \"chair\",\n \"secr\"\n]", + "session_purposes": "[]", "show_on_agenda": false }, "model": "group.groupfeatures", @@ -3068,6 +3081,7 @@ "parent_types": [], "req_subm_approval": true, "role_order": "[\n \"chair\",\n \"delegate\"\n]", + "session_purposes": "[\n \"officehours\"\n]", "show_on_agenda": false }, "model": "group.groupfeatures", @@ -3106,6 +3120,7 @@ ], "req_subm_approval": true, "role_order": "[\n \"chair\",\n \"secr\"\n]", + "session_purposes": "[\n \"officehours\",\n \"open_meeting\",\n \"presentation\"\n]", "show_on_agenda": false }, "model": "group.groupfeatures", @@ -3144,6 +3159,7 @@ ], "req_subm_approval": true, "role_order": "[\n \"chair\",\n \"member\",\n \"advisor\"\n]", + "session_purposes": "[\n \"closed_meeting\",\n \"officehours\"\n]", "show_on_agenda": false }, "model": "group.groupfeatures", @@ -3182,6 +3198,7 @@ ], "req_subm_approval": false, "role_order": "[\n \"lead\",\n \"chair\",\n \"secr\"\n]", + "session_purposes": "[\n \"regular\",\n \"tutorial\"\n]", "show_on_agenda": false }, "model": "group.groupfeatures", @@ -3220,6 +3237,7 @@ ], "req_subm_approval": true, "role_order": "[\n \"chair\",\n \"secr\"\n]", + "session_purposes": "[\n \"regular\"\n]", "show_on_agenda": true }, "model": "group.groupfeatures", @@ -3258,6 +3276,7 @@ ], "req_subm_approval": true, "role_order": "[\n \"chair\",\n \"secr\"\n]", + "session_purposes": "[\n \"open_meeting\",\n \"social\"\n]", "show_on_agenda": false }, "model": "group.groupfeatures", @@ -3294,6 +3313,7 @@ "parent_types": [], "req_subm_approval": true, "role_order": "[\n \"chair\",\n \"secr\"\n]", + "session_purposes": "[\n \"officehours\"\n]", "show_on_agenda": false }, "model": "group.groupfeatures", @@ -3332,6 +3352,7 @@ ], "req_subm_approval": true, "role_order": "[\n \"chair\",\n \"delegate\",\n \"secr\"\n]", + "session_purposes": "[\n \"regular\"\n]", "show_on_agenda": true }, "model": "group.groupfeatures", @@ -3371,6 +3392,7 @@ ], "req_subm_approval": true, "role_order": "[\n \"liaiman\"\n]", + "session_purposes": "[]", "show_on_agenda": false }, "model": "group.groupfeatures", @@ -3409,6 +3431,7 @@ ], "req_subm_approval": false, "role_order": "[\n \"chair\",\n \"member\",\n \"matman\"\n]", + "session_purposes": "[\n \"coding\",\n \"presentation\",\n \"social\",\n \"tutorial\"\n]", "show_on_agenda": false }, "model": "group.groupfeatures", @@ -3447,6 +3470,7 @@ ], "req_subm_approval": true, "role_order": "[\n \"chair\",\n \"secr\",\n \"delegate\"\n]", + "session_purposes": "[\n \"regular\"\n]", "show_on_agenda": true }, "model": "group.groupfeatures", @@ -12738,6 +12762,138 @@ "model": "name.roomresourcename", "pk": "webex" }, + { + "fields": { + "desc": "Meeting administration", + "name": "Administrative", + "on_agenda": true, + "order": 5, + "timeslot_types": "[\n \"other\",\n \"reg\"\n]", + "used": true + }, + "model": "name.sessionpurposename", + "pk": "admin" + }, + { + "fields": { + "desc": "Closed meeting", + "name": "Closed meeting", + "on_agenda": false, + "order": 10, + "timeslot_types": "[\n \"other\",\n \"regular\"\n]", + "used": true + }, + "model": "name.sessionpurposename", + "pk": "closed_meeting" + }, + { + "fields": { + "desc": "Coding session", + "name": "Coding", + "on_agenda": true, + "order": 4, + "timeslot_types": "[\n \"other\"\n]", + "used": true + }, + "model": "name.sessionpurposename", + "pk": "coding" + }, + { + "fields": { + "desc": "Value not set (do not use for new sessions)", + "name": "None", + "on_agenda": true, + "order": 0, + "timeslot_types": "[]", + "used": false + }, + "model": "name.sessionpurposename", + "pk": "none" + }, + { + "fields": { + "desc": "Office hours session", + "name": "Office hours", + "on_agenda": true, + "order": 3, + "timeslot_types": "[\n \"other\"\n]", + "used": true + }, + "model": "name.sessionpurposename", + "pk": "officehours" + }, + { + "fields": { + "desc": "Open meeting", + "name": "Open meeting", + "on_agenda": true, + "order": 9, + "timeslot_types": "[\n \"other\"\n]", + "used": true + }, + "model": "name.sessionpurposename", + "pk": "open_meeting" + }, + { + "fields": { + "desc": "Plenary session", + "name": "Plenary", + "on_agenda": true, + "order": 7, + "timeslot_types": "[\n \"plenary\"\n]", + "used": true + }, + "model": "name.sessionpurposename", + "pk": "plenary" + }, + { + "fields": { + "desc": "Presentation session", + "name": "Presentation", + "on_agenda": true, + "order": 8, + "timeslot_types": "[\n \"other\",\n \"regular\"\n]", + "used": true + }, + "model": "name.sessionpurposename", + "pk": "presentation" + }, + { + "fields": { + "desc": "Regular group session", + "name": "Regular", + "on_agenda": true, + "order": 1, + "timeslot_types": "[\n \"regular\"\n]", + "used": true + }, + "model": "name.sessionpurposename", + "pk": "regular" + }, + { + "fields": { + "desc": "Social event or activity", + "name": "Social", + "on_agenda": true, + "order": 6, + "timeslot_types": "[\n \"break\",\n \"other\"\n]", + "used": true + }, + "model": "name.sessionpurposename", + "pk": "social" + }, + { + "fields": { + "desc": "Tutorial or training session", + "name": "Tutorial", + "on_agenda": true, + "order": 2, + "timeslot_types": "[\n \"other\"\n]", + "used": true + }, + "model": "name.sessionpurposename", + "pk": "tutorial" + }, { "fields": { "desc": "", @@ -13163,7 +13319,6 @@ "desc": "", "name": "Break", "order": 0, - "private": false, "used": true }, "model": "name.timeslottypename", @@ -13174,7 +13329,6 @@ "desc": "Leadership Meetings", "name": "Leadership", "order": 0, - "private": true, "used": true }, "model": "name.timeslottypename", @@ -13185,29 +13339,16 @@ "desc": "Other Meetings Not Published on Agenda", "name": "Off Agenda", "order": 0, - "private": true, - "used": true + "used": false }, "model": "name.timeslottypename", "pk": "offagenda" }, - { - "fields": { - "desc": "Office hours timeslot", - "name": "Office Hours", - "order": 0, - "private": false, - "used": true - }, - "model": "name.timeslottypename", - "pk": "officehours" - }, { "fields": { "desc": "", "name": "Other", "order": 0, - "private": false, "used": true }, "model": "name.timeslottypename", @@ -13218,7 +13359,6 @@ "desc": "", "name": "Plenary", "order": 0, - "private": false, "used": true }, "model": "name.timeslottypename", @@ -13229,7 +13369,6 @@ "desc": "", "name": "Registration", "order": 0, - "private": false, "used": true }, "model": "name.timeslottypename", @@ -13240,7 +13379,6 @@ "desc": "", "name": "Regular", "order": 0, - "private": false, "used": true }, "model": "name.timeslottypename", @@ -13251,8 +13389,7 @@ "desc": "A room has been reserved for use by another body the timeslot indicated", "name": "Room Reserved", "order": 0, - "private": false, - "used": true + "used": false }, "model": "name.timeslottypename", "pk": "reserved" @@ -13262,7 +13399,6 @@ "desc": "A room was not booked for the timeslot indicated", "name": "Room Unavailable", "order": 0, - "private": false, "used": true }, "model": "name.timeslottypename", diff --git a/ietf/name/migrations/0034_add_officehours_timeslottypename.py b/ietf/name/migrations/0034_add_officehours_timeslottypename.py deleted file mode 100644 index db24c4cdd..000000000 --- a/ietf/name/migrations/0034_add_officehours_timeslottypename.py +++ /dev/null @@ -1,29 +0,0 @@ -# Generated by Django 2.2.19 on 2021-03-29 08:28 - -from django.db import migrations - - -def forward(apps, schema_editor): - TimeSlotTypeName = apps.get_model('name', 'TimeSlotTypeName') - - TimeSlotTypeName.objects.get_or_create( - slug='officehours', - defaults=dict( - name='Office Hours', - desc='Office hours timeslot', - used=True, - ) - ) - -def reverse(apps, schema_editor): - pass # don't remove the name when migrating - -class Migration(migrations.Migration): - - dependencies = [ - ('name', '0033_populate_agendafiltertypename'), - ] - - operations = [ - migrations.RunPython(forward, reverse), - ] diff --git a/ietf/name/migrations/0035_sessionpurposename.py b/ietf/name/migrations/0034_sessionpurposename.py similarity index 95% rename from ietf/name/migrations/0035_sessionpurposename.py rename to ietf/name/migrations/0034_sessionpurposename.py index ebb970d60..ca22e1a15 100644 --- a/ietf/name/migrations/0035_sessionpurposename.py +++ b/ietf/name/migrations/0034_sessionpurposename.py @@ -10,7 +10,7 @@ import jsonfield class Migration(migrations.Migration): dependencies = [ - ('name', '0034_add_officehours_timeslottypename'), + ('name', '0033_populate_agendafiltertypename'), ] operations = [ diff --git a/ietf/name/migrations/0036_populate_sessionpurposename.py b/ietf/name/migrations/0035_populate_sessionpurposename.py similarity index 73% rename from ietf/name/migrations/0036_populate_sessionpurposename.py rename to ietf/name/migrations/0035_populate_sessionpurposename.py index 75034ee17..8af7d60d1 100644 --- a/ietf/name/migrations/0036_populate_sessionpurposename.py +++ b/ietf/name/migrations/0035_populate_sessionpurposename.py @@ -9,17 +9,18 @@ def forward(apps, schema_editor): SessionPurposeName = apps.get_model('name', 'SessionPurposeName') TimeSlotTypeName = apps.get_model('name', 'TimeSlotTypeName') - for order, (slug, name, desc, tstypes, on_agenda) in enumerate(( - ('regular', 'Regular', 'Regular group session', ['regular'], True), - ('tutorial', 'Tutorial', 'Tutorial or training session', ['other'], True), - ('office_hours', 'Office hours', 'Office hours session', ['other'], True), - ('coding', 'Coding', 'Coding session', ['other'], True), - ('admin', 'Administrative', 'Meeting administration', ['other', 'reg'], True), - ('social', 'Social', 'Social event or activity', ['break', 'other'], True), - ('plenary', 'Plenary', 'Plenary session', ['plenary'], True), - ('presentation', 'Presentation', 'Presentation session', ['other', 'regular'], True), - ('open_meeting', 'Open meeting', 'Open meeting', ['other'], True), - ('closed_meeting', 'Closed meeting', 'Closed meeting', ['other', 'regular'], False), + for order, (slug, name, desc, tstypes, on_agenda, used) in enumerate(( + ('none', 'None', 'Value not set (do not use for new sessions)', [], True, False), + ('regular', 'Regular', 'Regular group session', ['regular'], True, True), + ('tutorial', 'Tutorial', 'Tutorial or training session', ['other'], True, True), + ('officehours', 'Office hours', 'Office hours session', ['other'], True, True), + ('coding', 'Coding', 'Coding session', ['other'], True, True), + ('admin', 'Administrative', 'Meeting administration', ['other', 'reg'], True, True), + ('social', 'Social', 'Social event or activity', ['break', 'other'], True, True), + ('plenary', 'Plenary', 'Plenary session', ['plenary'], True, True), + ('presentation', 'Presentation', 'Presentation session', ['other', 'regular'], True, True), + ('open_meeting', 'Open meeting', 'Open meeting', ['other'], True, True), + ('closed_meeting', 'Closed meeting', 'Closed meeting', ['other', 'regular'], False, True), )): # verify that we're not about to use an invalid type for ts_type in tstypes: @@ -29,7 +30,7 @@ def forward(apps, schema_editor): slug=slug, name=name, desc=desc, - used=True, + used=used, order=order, timeslot_types = tstypes, on_agenda=on_agenda, @@ -44,7 +45,7 @@ def reverse(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('name', '0035_sessionpurposename'), + ('name', '0034_sessionpurposename'), ] operations = [ diff --git a/ietf/name/migrations/0037_depopulate_timeslottypename_private.py b/ietf/name/migrations/0036_depopulate_timeslottypename_private.py similarity index 94% rename from ietf/name/migrations/0037_depopulate_timeslottypename_private.py rename to ietf/name/migrations/0036_depopulate_timeslottypename_private.py index 09407b2f4..6f1db8e76 100644 --- a/ietf/name/migrations/0037_depopulate_timeslottypename_private.py +++ b/ietf/name/migrations/0036_depopulate_timeslottypename_private.py @@ -22,7 +22,7 @@ def reverse(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('name', '0036_populate_sessionpurposename'), + ('name', '0035_populate_sessionpurposename'), ('meeting', '0050_populate_session_on_agenda'), ] diff --git a/ietf/name/migrations/0038_remove_timeslottypename_private.py b/ietf/name/migrations/0037_remove_timeslottypename_private.py similarity index 82% rename from ietf/name/migrations/0038_remove_timeslottypename_private.py rename to ietf/name/migrations/0037_remove_timeslottypename_private.py index 0c2a9578d..2a8678056 100644 --- a/ietf/name/migrations/0038_remove_timeslottypename_private.py +++ b/ietf/name/migrations/0037_remove_timeslottypename_private.py @@ -6,7 +6,7 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('name', '0037_depopulate_timeslottypename_private'), + ('name', '0036_depopulate_timeslottypename_private'), ] operations = [ diff --git a/ietf/name/migrations/0038_disuse_offagenda_and_reserved.py b/ietf/name/migrations/0038_disuse_offagenda_and_reserved.py new file mode 100644 index 000000000..be0b507bd --- /dev/null +++ b/ietf/name/migrations/0038_disuse_offagenda_and_reserved.py @@ -0,0 +1,24 @@ +# Generated by Django 2.2.24 on 2021-10-29 06:44 + +from django.db import migrations + + +def forward(apps, schema_editor): + TimeSlotTypeName = apps.get_model('name', 'TimeSlotTypeName') + TimeSlotTypeName.objects.filter(slug__in=('offagenda', 'reserved')).update(used=False) + + +def reverse(apps, schema_editor): + TimeSlotTypeName = apps.get_model('name', 'TimeSlotTypeName') + TimeSlotTypeName.objects.filter(slug__in=('offagenda', 'reserved')).update(used=True) + + +class Migration(migrations.Migration): + + dependencies = [ + ('name', '0037_remove_timeslottypename_private'), + ] + + operations = [ + migrations.RunPython(forward, reverse), + ] diff --git a/ietf/name/resources.py b/ietf/name/resources.py index 2af01bb98..764c5e108 100644 --- a/ietf/name/resources.py +++ b/ietf/name/resources.py @@ -18,7 +18,7 @@ from ietf.name.models import ( AgendaFilterTypeName, AgendaTypeName, BallotPosit ReviewAssignmentStateName, ReviewRequestStateName, ReviewResultName, ReviewTypeName, RoleName, RoomResourceName, SessionStatusName, StdLevelName, StreamName, TimeSlotTypeName, TopicAudienceName, ReviewerQueuePolicyName, TimerangeName, ExtResourceTypeName, ExtResourceName, - SlideSubmissionStatusName, ProceedingsMaterialTypeName) + SlideSubmissionStatusName, ProceedingsMaterialTypeName, SessionPurposeName ) class TimeSlotTypeNameResource(ModelResource): class Meta: @@ -701,3 +701,22 @@ class AgendaFilterTypeNameResource(ModelResource): "order": ALL, } api.name.register(AgendaFilterTypeNameResource()) + + +class SessionPurposeNameResource(ModelResource): + class Meta: + queryset = SessionPurposeName.objects.all() + serializer = api.Serializer() + cache = SimpleCache() + #resource_name = 'sessionpurposename' + ordering = ['slug', ] + filtering = { + "slug": ALL, + "name": ALL, + "desc": ALL, + "used": ALL, + "order": ALL, + "timeslot_types": ALL, + "on_agenda": ALL, + } +api.name.register(SessionPurposeNameResource()) diff --git a/ietf/secr/meetings/tests.py b/ietf/secr/meetings/tests.py index 1bc74aa0b..e0519ee23 100644 --- a/ietf/secr/meetings/tests.py +++ b/ietf/secr/meetings/tests.py @@ -336,7 +336,8 @@ class SecrMeetingTestCase(TestCase): 'duration':'02:00', 'name':'Testing', 'short':'test', - 'type':'reg', + 'purpose_0': 'admin', # purpose + 'purpose_1':'reg', # type 'group':group.pk, 'location': room.pk, 'remote_instructions': 'http://webex.com/foobar', @@ -382,7 +383,8 @@ class SecrMeetingTestCase(TestCase): 'time':new_time.strftime('%H:%M'), 'duration':'01:00', 'day':'2', - 'type':'other', + 'purpose_0': 'coding', # purpose + 'purpose_1': 'other', # type 'remote_instructions': 'http://webex.com/foobar', }) self.assertRedirects(response, redirect_url) diff --git a/ietf/secr/sreq/forms.py b/ietf/secr/sreq/forms.py index ff67e8b69..1b61ac392 100644 --- a/ietf/secr/sreq/forms.py +++ b/ietf/secr/sreq/forms.py @@ -260,7 +260,7 @@ class SessionForm(forms.Form): num_sessions_expected = int(data.get('num_session', '')) except ValueError: self.add_error('num_session', 'Invalid value for number of sessions') - if len(self.session_forms.errors) == 0 and num_sessions_with_data < num_sessions_expected: + if num_sessions_with_data < num_sessions_expected: self.add_error('num_session', 'Must provide data for all sessions') # if default (empty) option is selected, cleaned_data won't include num_session key diff --git a/ietf/secr/sreq/templatetags/ams_filters.py b/ietf/secr/sreq/templatetags/ams_filters.py index 751517320..47109a081 100644 --- a/ietf/secr/sreq/templatetags/ams_filters.py +++ b/ietf/secr/sreq/templatetags/ams_filters.py @@ -25,6 +25,8 @@ def display_duration(value): """ Maps a session requested duration from select index to label.""" + if value in (None, ''): + return 'unspecified' value = int(value) map = {0: 'None', 1800: '30 Minutes', diff --git a/ietf/secr/sreq/tests.py b/ietf/secr/sreq/tests.py index 9beb96ed7..2e5b92291 100644 --- a/ietf/secr/sreq/tests.py +++ b/ietf/secr/sreq/tests.py @@ -4,6 +4,7 @@ import datetime +from django.test import override_settings from django.urls import reverse import debug # pyflakes:ignore @@ -76,6 +77,7 @@ class SessionRequestTestCase(TestCase): self.assertRedirects(r,reverse('ietf.secr.sreq.views.main')) self.assertEqual(SchedulingEvent.objects.filter(session=session).order_by('-id')[0].status_id, 'deleted') + @override_settings(SECR_VIRTUAL_MEETINGS=tuple()) # ensure not unexpectedly testing a virtual meeting session def test_edit(self): meeting = MeetingFactory(type_id='ietf', date=datetime.date.today()) mars = RoleFactory(name_id='chair', person__user__username='marschairman', group__acronym='mars').group @@ -90,17 +92,43 @@ class SessionRequestTestCase(TestCase): self.client.login(username="marschairman", password="marschairman+password") r = self.client.get(url) self.assertEqual(r.status_code, 200) + attendees = 10 + comments = 'need lights' + mars_sessions = meeting.session_set.filter(group__acronym='mars') post_data = {'num_session':'2', - 'length_session1':'3600', - 'length_session2':'3600', - 'attendees':'10', + 'attendees': attendees, 'constraint_chair_conflict':iabprog.acronym, - 'comments':'need lights', 'session_time_relation': 'subsequent-days', 'adjacent_with_wg': group2.acronym, 'joint_with_groups': group3.acronym + ' ' + group4.acronym, 'joint_for_session': '2', 'timeranges': ['thursday-afternoon-early', 'thursday-afternoon-late'], + 'session_set-TOTAL_FORMS': '2', + 'session_set-INITIAL_FORMS': '1', + 'session_set-MIN_NUM_FORMS': '1', + 'session_set-MAX_NUM_FORMS': '3', + 'session_set-0-id':mars_sessions[0].pk, + 'session_set-0-name': mars_sessions[0].name, + 'session_set-0-short': mars_sessions[0].short, + 'session_set-0-purpose': mars_sessions[0].purpose_id, + 'session_set-0-type': mars_sessions[0].type_id, + 'session_set-0-requested_duration': '3600', + 'session_set-0-on_agenda': mars_sessions[0].on_agenda, + 'session_set-0-remote_instructions': mars_sessions[0].remote_instructions, + 'session_set-0-attendees': attendees, + 'session_set-0-comments': comments, + 'session_set-0-DELETE': '', + # no session_set-1-id because it's a new request + 'session_set-1-name': '', + 'session_set-1-short': '', + 'session_set-1-purpose': 'regular', + 'session_set-1-type': 'regular', + 'session_set-1-requested_duration': '3600', + 'session_set-1-on_agenda': True, + 'session_set-1-remote_instructions': mars_sessions[0].remote_instructions, + 'session_set-1-attendees': attendees, + 'session_set-1-comments': comments, + 'session_set-1-DELETE': '', 'submit': 'Continue'} r = self.client.post(url, post_data, HTTP_HOST='example.com') redirect_url = reverse('ietf.secr.sreq.views.view', kwargs={'acronym': 'mars'}) @@ -133,11 +161,37 @@ class SessionRequestTestCase(TestCase): post_data = {'num_session':'2', 'length_session1':'3600', 'length_session2':'3600', - 'attendees':'10', + 'attendees':attendees, 'constraint_chair_conflict':'', 'comments':'need lights', 'joint_with_groups': group2.acronym, 'joint_for_session': '1', + 'session_set-TOTAL_FORMS': '2', + 'session_set-INITIAL_FORMS': '2', + 'session_set-MIN_NUM_FORMS': '1', + 'session_set-MAX_NUM_FORMS': '3', + 'session_set-0-id':sessions[0].pk, + 'session_set-0-name': sessions[0].name, + 'session_set-0-short': sessions[0].short, + 'session_set-0-purpose': sessions[0].purpose_id, + 'session_set-0-type': sessions[0].type_id, + 'session_set-0-requested_duration': '3600', + 'session_set-0-on_agenda': sessions[0].on_agenda, + 'session_set-0-remote_instructions': sessions[0].remote_instructions, + 'session_set-0-attendees': sessions[0].attendees, + 'session_set-0-comments': sessions[1].comments, + 'session_set-0-DELETE': '', + 'session_set-1-id': sessions[1].pk, + 'session_set-1-name': sessions[1].name, + 'session_set-1-short': sessions[1].short, + 'session_set-1-purpose': sessions[1].purpose_id, + 'session_set-1-type': sessions[1].type_id, + 'session_set-1-requested_duration': '3600', + 'session_set-1-on_agenda': sessions[1].on_agenda, + 'session_set-1-remote_instructions': sessions[1].remote_instructions, + 'session_set-1-attendees': sessions[1].attendees, + 'session_set-1-comments': sessions[1].comments, + 'session_set-1-DELETE': '', 'submit': 'Continue'} r = self.client.post(url, post_data, HTTP_HOST='example.com') self.assertRedirects(r, redirect_url) @@ -160,7 +214,7 @@ class SessionRequestTestCase(TestCase): """Inactive conflicts should be displayed and removable""" meeting = MeetingFactory(type_id='ietf', date=datetime.date.today(), group_conflicts=['chair_conflict']) mars = RoleFactory(name_id='chair', person__user__username='marschairman', group__acronym='mars').group - SessionFactory(meeting=meeting, group=mars, status_id='sched') + session = SessionFactory(meeting=meeting, group=mars, status_id='sched') other_group = GroupFactory() Constraint.objects.create( meeting=meeting, @@ -184,16 +238,31 @@ class SessionRequestTestCase(TestCase): # check that the target is displayed correctly in the UI self.assertIn(other_group.acronym, delete_checkbox.find('../input[@type="text"]').value) + attendees = '10' post_data = { 'num_session': '1', - 'length_session1': '3600', - 'attendees': '10', + 'attendees': attendees, 'constraint_chair_conflict':'', 'comments':'', 'joint_with_groups': '', 'joint_for_session': '', - 'submit': 'Save', 'delete_conflict': 'on', + 'session_set-TOTAL_FORMS': '1', + 'session_set-INITIAL_FORMS': '1', + 'session_set-MIN_NUM_FORMS': '1', + 'session_set-MAX_NUM_FORMS': '3', + 'session_set-0-id':session.pk, + 'session_set-0-name': session.name, + 'session_set-0-short': session.short, + 'session_set-0-purpose': session.purpose_id, + 'session_set-0-type': session.type_id, + 'session_set-0-requested_duration': '3600', + 'session_set-0-on_agenda': session.on_agenda, + 'session_set-0-remote_instructions': session.remote_instructions, + 'session_set-0-attendees': attendees, + 'session_set-0-comments': '', + 'session_set-0-DELETE': '', + 'submit': 'Save', } r = self.client.post(url, post_data, HTTP_HOST='example.com') redirect_url = reverse('ietf.secr.sreq.views.view', kwargs={'acronym': 'mars'}) @@ -283,15 +352,31 @@ class SubmitRequestCase(TestCase): url = reverse('ietf.secr.sreq.views.new',kwargs={'acronym':group.acronym}) confirm_url = reverse('ietf.secr.sreq.views.confirm',kwargs={'acronym':group.acronym}) main_url = reverse('ietf.secr.sreq.views.main') + attendees = '10' + comments = 'need projector' post_data = {'num_session':'1', - 'length_session1':'3600', - 'attendees':'10', + 'attendees':attendees, 'constraint_chair_conflict':'', - 'comments':'need projector', + 'comments':comments, 'adjacent_with_wg': group2.acronym, 'timeranges': ['thursday-afternoon-early', 'thursday-afternoon-late'], 'joint_with_groups': group3.acronym + ' ' + group4.acronym, 'joint_for_session': '1', + 'session_set-TOTAL_FORMS': '1', + 'session_set-INITIAL_FORMS': '0', + 'session_set-MIN_NUM_FORMS': '1', + 'session_set-MAX_NUM_FORMS': '3', + # no 'session_set-0-id' to create a new session + 'session_set-0-name': '', + 'session_set-0-short': '', + 'session_set-0-purpose': 'regular', + 'session_set-0-type': 'regular', + 'session_set-0-requested_duration': '3600', + 'session_set-0-on_agenda': True, + 'session_set-0-remote_instructions': '', + 'session_set-0-attendees': attendees, + 'session_set-0-comments': comments, + 'session_set-0-DELETE': '', 'submit': 'Continue'} self.client.login(username="secretary", password="secretary+password") r = self.client.post(url,post_data) @@ -313,7 +398,7 @@ class SubmitRequestCase(TestCase): self.assertRedirects(r, main_url) session_count_after = Session.objects.filter(meeting=meeting, group=group, type='regular').count() self.assertEqual(session_count_after, session_count_before + 1) - + # Verify database content session = Session.objects.get(meeting=meeting, group=group) self.assertEqual(session.constraints().get(name='wg_adjacent').target.acronym, group2.acronym) @@ -329,17 +414,35 @@ class SubmitRequestCase(TestCase): area = RoleFactory(name_id='ad', person=ad, group__type_id='area').group group = GroupFactory(parent=area) url = reverse('ietf.secr.sreq.views.new',kwargs={'acronym':group.acronym}) - post_data = {'num_session':'2', - 'length_session1':'3600', - 'attendees':'10', - 'constraint_chair_conflict':'', - 'comments':'need projector'} + attendees = '10' + comments = 'need projector' + post_data = { + 'num_session':'2', + 'attendees':attendees, + 'constraint_chair_conflict':'', + 'comments':comments, + 'session_set-TOTAL_FORMS': '1', + 'session_set-INITIAL_FORMS': '1', + 'session_set-MIN_NUM_FORMS': '1', + 'session_set-MAX_NUM_FORMS': '3', + # no 'session_set-0-id' to create a new session + 'session_set-0-name': '', + 'session_set-0-short': '', + 'session_set-0-purpose': 'regular', + 'session_set-0-type': 'regular', + 'session_set-0-requested_duration': '3600', + 'session_set-0-on_agenda': True, + 'session_set-0-remote_instructions': '', + 'session_set-0-attendees': attendees, + 'session_set-0-comments': comments, + 'session_set-0-DELETE': '', + } self.client.login(username="secretary", password="secretary+password") r = self.client.post(url,post_data) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertEqual(len(q('#session-request-form')),1) - self.assertContains(r, 'You must enter a length for all sessions') + self.assertContains(r, 'Must provide data for all sessions') def test_submit_request_check_constraints(self): m1 = MeetingFactory(type_id='ietf', date=datetime.date.today() - datetime.timedelta(days=100)) @@ -363,7 +466,7 @@ class SubmitRequestCase(TestCase): target=inactive_group, name_id='chair_conflict', ) - SessionFactory(group=group, meeting=m1) + session = SessionFactory(group=group, meeting=m1) self.client.login(username="secretary", password="secretary+password") @@ -375,11 +478,27 @@ class SubmitRequestCase(TestCase): self.assertIn(still_active_group.acronym, conflict1) self.assertNotIn(inactive_group.acronym, conflict1) + attendees = '10' + comments = 'need projector' post_data = {'num_session':'1', - 'length_session1':'3600', - 'attendees':'10', + 'attendees':attendees, 'constraint_chair_conflict': group.acronym, - 'comments':'need projector', + 'comments':comments, + 'session_set-TOTAL_FORMS': '1', + 'session_set-INITIAL_FORMS': '1', + 'session_set-MIN_NUM_FORMS': '1', + 'session_set-MAX_NUM_FORMS': '3', + # no 'session_set-0-id' to create a new session + 'session_set-0-name': '', + 'session_set-0-short': '', + 'session_set-0-purpose': session.purpose_id, + 'session_set-0-type': session.type_id, + 'session_set-0-requested_duration': '3600', + 'session_set-0-on_agenda': session.on_agenda, + 'session_set-0-remote_instructions': session.remote_instructions, + 'session_set-0-attendees': attendees, + 'session_set-0-comments': comments, + 'session_set-0-DELETE': '', 'submit': 'Continue'} r = self.client.post(url,post_data) self.assertEqual(r.status_code, 200) @@ -405,10 +524,9 @@ class SubmitRequestCase(TestCase): url = reverse('ietf.secr.sreq.views.new',kwargs={'acronym':group.acronym}) confirm_url = reverse('ietf.secr.sreq.views.confirm',kwargs={'acronym':group.acronym}) len_before = len(outbox) + attendees = '10' post_data = {'num_session':'2', - 'length_session1':'3600', - 'length_session2':'3600', - 'attendees':'10', + 'attendees':attendees, 'bethere':str(ad.pk), 'constraint_chair_conflict':group4.acronym, 'comments':'', @@ -418,6 +536,32 @@ class SubmitRequestCase(TestCase): 'joint_with_groups': group3.acronym, 'joint_for_session': '2', 'timeranges': ['thursday-afternoon-early', 'thursday-afternoon-late'], + 'session_set-TOTAL_FORMS': '2', + 'session_set-INITIAL_FORMS': '0', + 'session_set-MIN_NUM_FORMS': '1', + 'session_set-MAX_NUM_FORMS': '3', + # no 'session_set-0-id' for new session + 'session_set-0-name': '', + 'session_set-0-short': '', + 'session_set-0-purpose': 'regular', + 'session_set-0-type': 'regular', + 'session_set-0-requested_duration': '3600', + 'session_set-0-on_agenda': True, + 'session_set-0-remote_instructions': '', + 'session_set-0-attendees': attendees, + 'session_set-0-comments': '', + 'session_set-0-DELETE': '', + # no 'session_set-1-id' for new session + 'session_set-1-name': '', + 'session_set-1-short': '', + 'session_set-1-purpose': 'regular', + 'session_set-1-type': 'regular', + 'session_set-1-requested_duration': '3600', + 'session_set-1-on_agenda': True, + 'session_set-1-remote_instructions': '', + 'session_set-1-attendees': attendees, + 'session_set-1-comments': '', + 'session_set-1-DELETE': '', 'submit': 'Continue'} self.client.login(username="ameschairman", password="ameschairman+password") # submit @@ -539,23 +683,59 @@ class SessionFormTest(TestCase): self.group5 = GroupFactory() self.group6 = GroupFactory() + attendees = '10' + comments = 'need lights' self.valid_form_data = { 'num_session': '2', 'third_session': 'true', - 'length_session1': '3600', - 'length_session2': '3600', - 'length_session3': '3600', - 'attendees': '10', + 'attendees': attendees, 'constraint_chair_conflict': self.group2.acronym, 'constraint_tech_overlap': self.group3.acronym, 'constraint_key_participant': self.group4.acronym, - 'comments': 'need lights', + 'comments': comments, 'session_time_relation': 'subsequent-days', 'adjacent_with_wg': self.group5.acronym, 'joint_with_groups': self.group6.acronym, 'joint_for_session': '3', 'timeranges': ['thursday-afternoon-early', 'thursday-afternoon-late'], - 'submit': 'Continue' + 'submit': 'Continue', + 'session_set-TOTAL_FORMS': '3', + 'session_set-INITIAL_FORMS': '0', + 'session_set-MIN_NUM_FORMS': '1', + 'session_set-MAX_NUM_FORMS': '3', + # no 'session_set-0-id' for new session + 'session_set-0-name': '', + 'session_set-0-short': '', + 'session_set-0-purpose': 'regular', + 'session_set-0-type': 'regular', + 'session_set-0-requested_duration': '3600', + 'session_set-0-on_agenda': True, + 'session_set-0-remote_instructions': '', + 'session_set-0-attendees': attendees, + 'session_set-0-comments': '', + 'session_set-0-DELETE': '', + # no 'session_set-1-id' for new session + 'session_set-1-name': '', + 'session_set-1-short': '', + 'session_set-1-purpose': 'regular', + 'session_set-1-type': 'regular', + 'session_set-1-requested_duration': '3600', + 'session_set-1-on_agenda': True, + 'session_set-1-remote_instructions': '', + 'session_set-1-attendees': attendees, + 'session_set-1-comments': '', + 'session_set-1-DELETE': '', + # no 'session_set-2-id' for new session + 'session_set-2-name': '', + 'session_set-2-short': '', + 'session_set-2-purpose': 'regular', + 'session_set-2-type': 'regular', + 'session_set-2-requested_duration': '3600', + 'session_set-2-on_agenda': True, + 'session_set-2-remote_instructions': '', + 'session_set-2-attendees': attendees, + 'session_set-2-comments': '', + 'session_set-2-DELETE': '', } def test_valid(self): @@ -637,58 +817,65 @@ class SessionFormTest(TestCase): def test_invalid_joint_for_session(self): form = self._invalid_test_helper({ 'third_session': '', + 'session_set-TOTAL_FORMS': '2', 'num_session': 2, 'joint_for_session': '3', }) self.assertEqual(form.errors, { - 'joint_for_session': ['The third session can not be the joint session, ' - 'because you have not requested a third session.'] + 'joint_for_session': [ + 'Session 3 can not be the joint session, the session has not been requested.'] }) form = self._invalid_test_helper({ 'third_session': '', - 'length_session2': '', + 'session_set-TOTAL_FORMS': '1', 'num_session': 1, 'joint_for_session': '2', 'session_time_relation': '', }) self.assertEqual(form.errors, { - 'joint_for_session': ['The second session can not be the joint session, ' - 'because you have not requested a second session.'] + 'joint_for_session': [ + 'Session 2 can not be the joint session, the session has not been requested.'] }) def test_invalid_missing_session_length(self): form = self._invalid_test_helper({ - 'length_session2': '', + 'session_set-TOTAL_FORMS': '2', + 'session_set-1-requested_duration': '', 'third_session': 'false', 'joint_for_session': None, }) - self.assertEqual(form.errors, - { - 'length_session2': ['You must enter a length for all sessions'], - }) + self.assertEqual(form.session_forms.errors, + [ + {}, + {'requested_duration': ['This field is required.']}, + ]) form = self._invalid_test_helper({ - 'length_session2': '', - 'length_session3': '', + 'session_set-1-requested_duration': '', + 'session_set-2-requested_duration': '', 'joint_for_session': None, }) - self.assertEqual(form.errors, - { - 'length_session2': ['You must enter a length for all sessions'], - 'length_session3': ['You must enter a length for all sessions'], - }) + self.assertEqual( + form.session_forms.errors, + [ + {}, + {'requested_duration': ['This field is required.']}, + {'requested_duration': ['This field is required.']}, + ]) form = self._invalid_test_helper({ - 'length_session3': '', + 'session_set-2-requested_duration': '', 'joint_for_session': None, }) - self.assertEqual(form.errors, - { - 'length_session3': ['You must enter a length for all sessions'], - }) + self.assertEqual(form.session_forms.errors, + [ + {}, + {}, + {'requested_duration': ['This field is required.']}, + ]) def _invalid_test_helper(self, new_form_data): form_data = dict(self.valid_form_data, **new_form_data) diff --git a/ietf/secr/sreq/views.py b/ietf/secr/sreq/views.py index 1b344a1ab..d5034eb25 100644 --- a/ietf/secr/sreq/views.py +++ b/ietf/secr/sreq/views.py @@ -336,7 +336,7 @@ def confirm(request, acronym): jfs = form.data.get('joint_for_session', '-1') if not jfs: # jfs might be '' jfs = '-1' - if int(jfs) == count: + if int(jfs) == count + 1: # count is zero-indexed groups_split = form.cleaned_data.get('joint_with_groups').replace(',',' ').split() joint = Group.objects.filter(acronym__in=groups_split) new_session.joint_with_groups.set(joint) @@ -645,7 +645,7 @@ def new(request, acronym): raise Http404(f'Cannot request sessions for group "{acronym}"') meeting = get_meeting(days=14) session_conflicts = dict(inbound=inbound_session_conflicts_as_string(group, meeting)) - is_virtual = meeting.number in settings.SECR_VIRTUAL_MEETINGS, + is_virtual = meeting.number in settings.SECR_VIRTUAL_MEETINGS FormClass = get_session_form_class() # check if app is locked @@ -723,6 +723,7 @@ def no_session(request, acronym): meeting=meeting, requested_duration=datetime.timedelta(0), type_id='regular', + purpose_id='regular', ) SchedulingEvent.objects.create( session=session, @@ -845,7 +846,8 @@ def view(request, acronym, num = None): return render(request, 'sreq/view.html', { 'is_locked': is_locked, 'is_virtual': meeting.number in settings.SECR_VIRTUAL_MEETINGS, - 'session': session, + 'session': session, # legacy processed data + 'sessions': sessions, # actual session instances 'activities': activities, 'meeting': meeting, 'group': group, diff --git a/ietf/secr/templates/includes/session_info.txt b/ietf/secr/templates/includes/session_info.txt index db22a520b..80910a593 100644 --- a/ietf/secr/templates/includes/session_info.txt +++ b/ietf/secr/templates/includes/session_info.txt @@ -6,7 +6,7 @@ Session Requester: {{ login }} {% if session.joint_with_groups %}{{ session.joint_for_session_display }} joint with: {{ session.joint_with_groups }}{% endif %} Number of Sessions: {{ session.num_session }} -Length of Session(s): {{ session.length_session1|display_duration }}{% if session.length_session2 %}, {{ session.length_session2|display_duration }}{% endif %}{% if session.length_session3 %}, {{ session.length_session3|display_duration }}{% endif %} +Length of Session(s): {{ session.length_session1|display_duration }}{% if session.length_session2 %}, {{ session.length_session2|display_duration }}{% endif %}{% if session.length_session3 %}, {{ session.length_session3|display_duration }}{% endif %} Number of Attendees: {{ session.attendees }} Conflicts to Avoid: {% for line in session.outbound_conflicts %} {{line}} diff --git a/ietf/secr/templates/includes/sessions_request_view.html b/ietf/secr/templates/includes/sessions_request_view.html index bb19f5e2a..b2350190f 100644 --- a/ietf/secr/templates/includes/sessions_request_view.html +++ b/ietf/secr/templates/includes/sessions_request_view.html @@ -3,25 +3,12 @@ Working Group Name:{{ group.name }} ({{ group.acronym }}) Area Name:{{ group.parent }} - Number of Sessions Requested:{% if session.length_session3 %}3{% else %}{{ session.num_session }}{% endif %} - {% for sess_form in form.session_forms %}{% if sess_form.cleaned_data and not sess_form.cleaned_data.DELETE %} - Session {{ forloop.counter }}: -
-
Length
{{ sess_form.cleaned_data.requested_duration.total_seconds|display_duration }}
- {% if sess_form.cleaned_data.name %}
Name
{{ sess_form.cleaned_data.name }}
{% endif %} - {% if sess_form.cleaned_data.purpose.slug != 'regular' %} -
Purpose
-
- {{ sess_form.cleaned_data.purpose }} - {% if sess_form.cleaned_data.purpose.timeslot_types|length > 1 %}({{ sess_form.cleaned_data.type }}){% endif %} -
- {% endif %} -
- - {% if group.features.acts_like_wg and forloop.counter == 2 and not is_virtual %} - Time between sessions:{% if session.session_time_relation_display %}{{ session.session_time_relation_display }}{% else %}No preference{% endif %} - {% endif %} - {% endif %}{% endfor %} + Number of Sessions Requested:{% if session.third_session %}3{% else %}{{ session.num_session }}{% endif %} + {% if form %} + {% include 'includes/sessions_request_view_formset.html' with formset=form.session_forms group=group session=session only %} + {% else %} + {% include 'includes/sessions_request_view_session_set.html' with session_set=sessions group=group session=session only %} + {% endif %} Number of Attendees:{{ session.attendees }} Conflicts to Avoid: diff --git a/ietf/secr/templates/includes/sessions_request_view_formset.html b/ietf/secr/templates/includes/sessions_request_view_formset.html new file mode 100644 index 000000000..ff502dea3 --- /dev/null +++ b/ietf/secr/templates/includes/sessions_request_view_formset.html @@ -0,0 +1,30 @@ +{% load ams_filters %}{# keep this in sync with sessions_request_view_session_set.html #} +{% for sess_form in formset %}{% if sess_form.cleaned_data and not sess_form.cleaned_data.DELETE %} + + Session {{ forloop.counter }}: + +
+
Length
+
{{ sess_form.cleaned_data.requested_duration.total_seconds|display_duration }}
+ {% if sess_form.cleaned_data.name %} +
Name
+
{{ sess_form.cleaned_data.name }}
{% endif %} + {% if sess_form.cleaned_data.purpose.slug != 'regular' %} +
Purpose
+
+ {{ sess_form.cleaned_data.purpose }} + {% if sess_form.cleaned_data.purpose.timeslot_types|length > 1 %}({{ sess_form.cleaned_data.type }} + ){% endif %} +
+ {% endif %} +
+ + + {% if group.features.acts_like_wg and forloop.counter == 2 and not is_virtual %} + + Time between sessions: + {% if session.session_time_relation_display %}{{ session.session_time_relation_display }}{% else %}No + preference{% endif %} + + {% endif %} +{% endif %}{% endfor %} \ No newline at end of file diff --git a/ietf/secr/templates/includes/sessions_request_view_session_set.html b/ietf/secr/templates/includes/sessions_request_view_session_set.html new file mode 100644 index 000000000..1f953ae3a --- /dev/null +++ b/ietf/secr/templates/includes/sessions_request_view_session_set.html @@ -0,0 +1,30 @@ +{% load ams_filters %}{# keep this in sync with sessions_request_view_formset.html #} +{% for sess in session_set %} + + Session {{ forloop.counter }}: + +
+
Length
+
{{ sess.requested_duration.total_seconds|display_duration }}
+ {% if sess.name %} +
Name
+
{{ sess.name }}
{% endif %} + {% if sess.purpose.slug != 'regular' %} +
Purpose
+
+ {{ sess.purpose }} + {% if sess.purpose.timeslot_types|length > 1 %}({{ sess.type }} + ){% endif %} +
+ {% endif %} +
+ + + {% if group.features.acts_like_wg and forloop.counter == 2 and not is_virtual %} + + Time between sessions: + {% if session.session_time_relation_display %}{{ session.session_time_relation_display }}{% else %}No + preference{% endif %} + + {% endif %} +{% endfor %} \ No newline at end of file diff --git a/ietf/settings.py b/ietf/settings.py index 75ec5bc89..f836ceb59 100644 --- a/ietf/settings.py +++ b/ietf/settings.py @@ -963,7 +963,7 @@ FLOORPLAN_LEGACY_BASE_URL = 'https://tools.ietf.org/agenda/{meeting.number}/venu FLOORPLAN_LAST_LEGACY_MEETING = 95 # last meeting to use FLOORPLAN_LEGACY_BASE_URL MEETING_USES_CODIMD_DATE = datetime.date(2020,7,6) -MEETING_LEGACY_OFFICE_HOURS_END = 111 # last meeting to use legacy office hours representation +MEETING_LEGACY_OFFICE_HOURS_END = 112 # last meeting to use legacy office hours representation # Maximum dimensions to accept at all MEETINGHOST_LOGO_MAX_UPLOAD_WIDTH = 400 diff --git a/ietf/static/ietf/js/edit-meeting-schedule.js b/ietf/static/ietf/js/edit-meeting-schedule.js index 191a435ba..60bce13ea 100644 --- a/ietf/static/ietf/js/edit-meeting-schedule.js +++ b/ietf/static/ietf/js/edit-meeting-schedule.js @@ -740,6 +740,18 @@ jQuery(document).ready(function () { if (timeSlotTypeInputs.length > 0) { timeSlotTypeInputs.on("change", updateTimeSlotTypeToggling); updateTimeSlotTypeToggling(); + content.find('#timeslot-group-toggles-modal .timeslot-type-toggles .select-all').get(0).addEventListener( + 'click', + function() { + timeSlotTypeInputs.prop('checked', true); + updateTimeSlotTypeToggling(); + }); + content.find('#timeslot-group-toggles-modal .timeslot-type-toggles .clear-all').get(0).addEventListener( + 'click', + function() { + timeSlotTypeInputs.prop('checked', false); + updateTimeSlotTypeToggling(); + }); } // Toggling session purposes @@ -771,10 +783,10 @@ jQuery(document).ready(function () { } // toggling visible timeslots - let timeslotGroupInputs = content.find("#timeslot-group-toggles-modal .modal-body .individual-timeslots input"); - function updateTimeslotGroupToggling() { + let timeSlotGroupInputs = content.find("#timeslot-group-toggles-modal .modal-body .individual-timeslots input"); + function updateTimeSlotGroupToggling() { let checked = []; - timeslotGroupInputs.filter(":checked").each(function () { + timeSlotGroupInputs.filter(":checked").each(function () { checked.push("." + this.value); }); @@ -786,8 +798,21 @@ jQuery(document).ready(function () { }); } - timeslotGroupInputs.on("click change", updateTimeslotGroupToggling); - updateTimeslotGroupToggling(); + timeSlotGroupInputs.on("click change", updateTimeSlotGroupToggling); + content.find('#timeslot-group-toggles-modal .timeslot-group-buttons .select-all').get(0).addEventListener( + 'click', + function() { + timeSlotGroupInputs.prop('checked', true); + updateTimeSlotGroupToggling(); + }); + content.find('#timeslot-group-toggles-modal .timeslot-group-buttons .clear-all').get(0).addEventListener( + 'click', + function() { + timeSlotGroupInputs.prop('checked', false); + updateTimeSlotGroupToggling(); + }); + + updateTimeSlotGroupToggling(); updatePastTimeslots(); setInterval(updatePastTimeslots, 10 * 1000 /* ms */); diff --git a/ietf/templates/meeting/edit_meeting_schedule.html b/ietf/templates/meeting/edit_meeting_schedule.html index a4b22b9df..e834215d0 100644 --- a/ietf/templates/meeting/edit_meeting_schedule.html +++ b/ietf/templates/meeting/edit_meeting_schedule.html @@ -195,9 +195,9 @@ {% if session_purposes|length > 1 %} - + {% endif %} - + @@ -216,7 +216,12 @@