Eliminate the officehours timeslot type, update/renumber migrations, mark offagenda/reserved TimeSlotTypeNames as not used, add a 'none' SessionPurposeName and disallow null, update agenda filter keywords/filter helpers, fix broken tests and general debugging

- Legacy-Id: 19550
This commit is contained in:
Jennifer Richards 2021-11-04 17:06:06 +00:00
parent 3dfce7b850
commit 7b35c09c40
38 changed files with 1012 additions and 478 deletions

View file

@ -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.models import Group
from ietf.group.factories import GroupFactory, RoleFactory from ietf.group.factories import GroupFactory, RoleFactory
from ietf.ipr.factories import HolderIprDisclosureFactory 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, from ietf.meeting.factories import ( MeetingFactory, SessionFactory, SessionPresentationFactory,
ProceedingsMaterialFactory ) ProceedingsMaterialFactory )
@ -1473,12 +1473,12 @@ class DocTestCase(TestCase):
) )
doc.set_state(State.objects.get(type="slides", slug="active")) doc.set_state(State.objects.get(type="slides", slug="active"))
session = Session.objects.create( session = SessionFactory(
name = "session-72-mars-1", name = "session-72-mars-1",
meeting = Meeting.objects.get(number='72'), meeting = Meeting.objects.get(number='72'),
group = Group.objects.get(acronym='mars'), group = Group.objects.get(acronym='mars'),
modified = datetime.datetime.now(), modified = datetime.datetime.now(),
type_id = 'regular', add_to_schedule=False,
) )
SchedulingEvent.objects.create( SchedulingEvent.objects.create(
session=session, session=session,

View file

@ -17,8 +17,8 @@ from django.urls import reverse as urlreverse
from ietf.doc.models import Document, State, DocAlias, NewRevisionDocEvent from ietf.doc.models import Document, State, DocAlias, NewRevisionDocEvent
from ietf.group.factories import RoleFactory from ietf.group.factories import RoleFactory
from ietf.group.models import Group from ietf.group.models import Group
from ietf.meeting.factories import MeetingFactory from ietf.meeting.factories import MeetingFactory, SessionFactory
from ietf.meeting.models import Meeting, Session, SessionPresentation, SchedulingEvent from ietf.meeting.models import Meeting, SessionPresentation, SchedulingEvent
from ietf.name.models import SessionStatusName from ietf.name.models import SessionStatusName
from ietf.person.models import Person from ietf.person.models import Person
from ietf.utils.test_utils import TestCase, login_testing_unauthorized from ietf.utils.test_utils import TestCase, login_testing_unauthorized
@ -152,12 +152,11 @@ class GroupMaterialTests(TestCase):
def test_revise(self): def test_revise(self):
doc = self.create_slides() doc = self.create_slides()
session = Session.objects.create( session = SessionFactory(
name = "session-42-mars-1", name = "session-42-mars-1",
meeting = Meeting.objects.get(number='42'), meeting = Meeting.objects.get(number='42'),
group = Group.objects.get(acronym='mars'), group = Group.objects.get(acronym='mars'),
modified = datetime.datetime.now(), modified = datetime.datetime.now(),
type_id='regular',
) )
SchedulingEvent.objects.create( SchedulingEvent.objects.create(
session=session, session=session,

View file

@ -12,7 +12,7 @@ class Migration(migrations.Migration):
dependencies = [ dependencies = [
('group', '0050_populate_groupfeatures_agenda_filter_type'), ('group', '0050_populate_groupfeatures_agenda_filter_type'),
('name', '0035_sessionpurposename'), ('name', '0034_sessionpurposename'),
] ]
operations = [ operations = [

View file

@ -7,23 +7,23 @@ from django.db import migrations
default_purposes = dict( default_purposes = dict(
adhoc=['presentation'], adhoc=['presentation'],
adm=['closed_meeting', 'office_hours'], adm=['closed_meeting', 'officehours'],
ag=['regular'], ag=['regular'],
area=['regular'], area=['regular'],
dir=['open_meeting', 'presentation', 'regular', 'social', 'tutorial'], dir=['open_meeting', 'presentation', 'regular', 'social', 'tutorial'],
iab=['closed_meeting', 'regular'], iab=['closed_meeting', 'regular'],
iabasg=['closed_meeting', 'open_meeting'], iabasg=['closed_meeting', 'officehours', 'open_meeting'],
iana=['office_hours'], iana=['officehours'],
iesg=['closed_meeting', 'open_meeting'], iesg=['closed_meeting', 'open_meeting'],
ietf=['admin', 'plenary', 'presentation', 'social'], ietf=['admin', 'plenary', 'presentation', 'social'],
irtf=[], irtf=[],
ise=['office_hours'], ise=['officehours'],
isoc=['office_hours', 'open_meeting', 'presentation'], isoc=['officehours', 'open_meeting', 'presentation'],
nomcom=['closed_meeting', 'office_hours'], nomcom=['closed_meeting', 'officehours'],
program=['regular', 'tutorial'], program=['regular', 'tutorial'],
rag=['regular'], rag=['regular'],
review=['open_meeting', 'social'], review=['open_meeting', 'social'],
rfcedtyp=['office_hours'], rfcedtyp=['officehours'],
rg=['regular'], rg=['regular'],
team=['coding', 'presentation', 'social', 'tutorial'], team=['coding', 'presentation', 'social', 'tutorial'],
wg=['regular'], wg=['regular'],
@ -55,7 +55,7 @@ class Migration(migrations.Migration):
dependencies = [ dependencies = [
('group', '0051_groupfeatures_session_purposes'), ('group', '0051_groupfeatures_session_purposes'),
('name', '0036_populate_sessionpurposename'), ('name', '0035_populate_sessionpurposename'),
] ]

View file

@ -10,7 +10,7 @@ from django.views.decorators.http import require_POST
from ietf.ietfauth.utils import role_required, has_role 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.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.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 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']: 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) return redirect(schedule_infourl, meeting.number, newschedule.owner_email(), newschedule.name)
else: 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 @require_POST
def schedule_update(request, meeting, schedule): 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('/'))), return HttpResponse(json.dumps(schedule.json_dict(request.build_absolute_uri('/'))),
content_type="application/json") content_type="application/json")
else: 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') @role_required('Secretariat')
def schedule_del(request, meeting, schedule): def schedule_del(request, meeting, schedule):

View file

@ -12,7 +12,8 @@ from django.db.models import Q
from ietf.meeting.models import (Meeting, Session, SchedulingEvent, Schedule, from ietf.meeting.models import (Meeting, Session, SchedulingEvent, Schedule,
TimeSlot, SessionPresentation, FloorPlan, Room, SlideSubmission, Constraint, TimeSlot, SessionPresentation, FloorPlan, Room, SlideSubmission, Constraint,
MeetingHost, ProceedingsMaterial) 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.doc.factories import ProceedingsMaterialDocFactory
from ietf.group.factories import GroupFactory from ietf.group.factories import GroupFactory
from ietf.person.factories import PersonFactory from ietf.person.factories import PersonFactory
@ -104,9 +105,11 @@ class SessionFactory(factory.django.DjangoModelFactory):
model = Session model = Session
meeting = factory.SubFactory(MeetingFactory) meeting = factory.SubFactory(MeetingFactory)
type_id='regular' purpose_id = 'regular'
type_id = 'regular'
group = factory.SubFactory(GroupFactory) group = factory.SubFactory(GroupFactory)
requested_duration = datetime.timedelta(hours=1) 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 @factory.post_generation
def status_id(obj, create, extracted, **kwargs): def status_id(obj, create, extracted, **kwargs):
@ -128,7 +131,7 @@ class SessionFactory(factory.django.DjangoModelFactory):
status=SessionStatusName.objects.get(slug=extracted), status=SessionStatusName.objects.get(slug=extracted),
by=PersonFactory(), by=PersonFactory(),
) )
@factory.post_generation @factory.post_generation
def add_to_schedule(obj, create, extracted, **kwargs): # pylint: disable=no-self-argument def add_to_schedule(obj, create, extracted, **kwargs): # pylint: disable=no-self-argument
''' '''

View file

@ -5,7 +5,7 @@ from django import forms
from ietf.name.models import SessionPurposeName, TimeSlotTypeName from ietf.name.models import SessionPurposeName, TimeSlotTypeName
import debug import debug # pyflakes: ignore
class SessionPurposeAndTypeWidget(forms.MultiWidget): class SessionPurposeAndTypeWidget(forms.MultiWidget):
css_class = 'session_purpose_widget' # class to apply to all widgets css_class = 'session_purpose_widget' # class to apply to all widgets

View file

@ -310,7 +310,7 @@ class AgendaKeywordTool:
@property @property
def filterable_purposes(self): 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): class AgendaFilterOrganizer(AgendaKeywordTool):
@ -336,7 +336,7 @@ class AgendaFilterOrganizer(AgendaKeywordTool):
# group acronyms in this list will never be used as filter buttons # group acronyms in this list will never be used as filter buttons
exclude_acronyms = ('iesg', 'ietf', 'secretariat') exclude_acronyms = ('iesg', 'ietf', 'secretariat')
# extra keywords to include in the no-heading column if they apply to any sessions # 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 # group types whose acronyms should be word-capitalized
capitalized_group_types = ('team',) capitalized_group_types = ('team',)
# group types whose acronyms should be all-caps # group types whose acronyms should be all-caps
@ -352,6 +352,8 @@ class AgendaFilterOrganizer(AgendaKeywordTool):
# filled in when _organize_filters() is called # filled in when _organize_filters() is called
self.filter_categories = None self.filter_categories = None
self.special_filters = 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): def get_non_area_keywords(self):
"""Get list of any 'non-area' (aka 'special') keywords """Get list of any 'non-area' (aka 'special') keywords
@ -465,11 +467,14 @@ class AgendaFilterOrganizer(AgendaKeywordTool):
# Call legacy version for older meetings # Call legacy version for older meetings
if self._use_legacy_keywords(): if self._use_legacy_keywords():
return self._legacy_non_group_filters() return self._legacy_non_group_filters(sessions)
# Not using legacy version # Not using legacy version
filter_cols = [] filter_cols = []
for purpose in self.filterable_purposes: for purpose in self.filterable_purposes:
if purpose.slug == 'regular':
continue
# Map label to its keyword, discarding duplicate labels. # Map label to its keyword, discarding duplicate labels.
# This does what we want as long as sessions with the same # This does what we want as long as sessions with the same
# name and purpose belong to the same group. # name and purpose belong to the same group.
@ -497,19 +502,16 @@ class AgendaFilterOrganizer(AgendaKeywordTool):
return filter_cols 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 """Get list of non-group filters for older meetings
Returns a list of filter columns 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() office_hours_items = set()
suffix = ' office hours' suffix = ' office hours'
for a in self.assignments: for s in sessions:
if a.session.name.lower().endswith(suffix): if s.name.lower().endswith(suffix):
office_hours_items.add((a.session.name[:-len(suffix)].strip(), a.session.group)) office_hours_items.add((s.name[:-len(suffix)].strip(), s.group))
headings = [] headings = []
# currently we only do office hours # currently we only do office hours
@ -640,8 +642,7 @@ class AgendaKeywordTagger(AgendaKeywordTool):
Keywords are all lower case. Keywords are all lower case.
""" """
for a in self.assignments: for a in self.assignments:
a.filter_keywords = {a.slot_type().slug.lower()} a.filter_keywords = self._filter_keywords_for_assignment(a)
a.filter_keywords.update(self._filter_keywords_for_assignment(a))
a.filter_keywords = sorted(list(a.filter_keywords)) a.filter_keywords = sorted(list(a.filter_keywords))
def _tag_sessions_with_filter_keywords(self): def _tag_sessions_with_filter_keywords(self):
@ -652,14 +653,17 @@ class AgendaKeywordTagger(AgendaKeywordTool):
@staticmethod @staticmethod
def _legacy_extra_session_keywords(session): def _legacy_extra_session_keywords(session):
"""Get extra keywords for a session at a legacy meeting""" """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) office_hours_match = re.match(r'^ *\w+(?: +\w+)* +office hours *$', session.name, re.IGNORECASE)
if office_hours_match is not None: if office_hours_match is not None:
suffix = 'officehours' suffix = 'officehours'
return [ extra.extend([
'officehours', 'officehours',
session.name.lower().replace(' ', '')[:-len(suffix)] + '-officehours', session.name.lower().replace(' ', '')[:-len(suffix)] + '-officehours',
] ])
return [] return extra
def _filter_keywords_for_session(self, session): def _filter_keywords_for_session(self, session):
keywords = set() keywords = set()

View file

@ -15,7 +15,9 @@ def forward(apps, schema_editor):
), ),
timeslot__type__private=True, 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 # 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 # 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) # purposes will roll out at the same time as the on_agenda field)

View file

@ -1162,7 +1162,7 @@ class Session(models.Model):
meeting = ForeignKey(Meeting) 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.") 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.") 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) 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. 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) joint_with_groups = models.ManyToManyField(Group, related_name='sessions_joint_in',blank=True)

View file

@ -5,8 +5,6 @@
from django import template from django import template
from django.urls import reverse from django.urls import reverse
from ietf.utils.text import xslugify
register = template.Library() register = template.Library()

View file

@ -11,8 +11,9 @@ import debug # pyflakes:ignore
from ietf.doc.factories import DocumentFactory from ietf.doc.factories import DocumentFactory
from ietf.group.factories import GroupFactory, RoleFactory from ietf.group.factories import GroupFactory, RoleFactory
from ietf.group.models import Group from ietf.group.models import Group
from ietf.meeting.models import (Meeting, Room, TimeSlot, Session, Schedule, SchedTimeSessAssignment, from ietf.meeting.factories import SessionFactory
from ietf.meeting.models import (Meeting, Room, TimeSlot, Schedule, SchedTimeSessAssignment,
ResourceAssociation, SessionPresentation, UrlResource, SchedulingEvent) ResourceAssociation, SessionPresentation, UrlResource, SchedulingEvent)
from ietf.meeting.helpers import create_interim_meeting from ietf.meeting.helpers import create_interim_meeting
from ietf.name.models import RoomResourceName 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)") system_person = Person.objects.get(name="(System)")
time = datetime.datetime.combine(date, datetime.time(9)) time = datetime.datetime.combine(date, datetime.time(9))
meeting = create_interim_meeting(group=group,date=date) meeting = create_interim_meeting(group=group,date=date)
session = Session.objects.create(meeting=meeting, group=group, session = SessionFactory(meeting=meeting, group=group,
attendees=10, attendees=10,
requested_duration=datetime.timedelta(minutes=20), requested_duration=datetime.timedelta(minutes=20),
remote_instructions='http://webex.com', remote_instructions='http://webex.com',
type_id='regular') add_to_schedule=False)
SchedulingEvent.objects.create(session=session, status_id=status, by=system_person) SchedulingEvent.objects.create(session=session, status_id=status, by=system_person)
slot = TimeSlot.objects.create( slot = TimeSlot.objects.create(
meeting=meeting, 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))) time=datetime.datetime.combine(session_date, datetime.time(11,0)))
# mars WG # mars WG
mars = Group.objects.get(acronym='mars') mars = Group.objects.get(acronym='mars')
mars_session = Session.objects.create(meeting=meeting, group=mars, mars_session = SessionFactory(meeting=meeting, group=mars,
attendees=10, requested_duration=datetime.timedelta(minutes=50), attendees=10, requested_duration=datetime.timedelta(minutes=50),
type_id='regular') add_to_schedule=False)
SchedulingEvent.objects.create(session=mars_session, status_id='schedw', by=system_person) 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=slot1, session=mars_session, schedule=schedule)
SchedTimeSessAssignment.objects.create(timeslot=slot2, session=mars_session, schedule=unofficial_schedule) SchedTimeSessAssignment.objects.create(timeslot=slot2, session=mars_session, schedule=unofficial_schedule)
# ames WG # ames WG
ames_session = Session.objects.create(meeting=meeting, group=Group.objects.get(acronym="ames"), ames_session = SessionFactory(meeting=meeting, group=Group.objects.get(acronym="ames"),
attendees=10, attendees=10,
requested_duration=datetime.timedelta(minutes=60), requested_duration=datetime.timedelta(minutes=60),
type_id='regular') add_to_schedule=False)
SchedulingEvent.objects.create(session=ames_session, status_id='schedw', by=system_person) 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=slot2, session=ames_session, schedule=schedule)
SchedTimeSessAssignment.objects.create(timeslot=slot1, session=ames_session, schedule=unofficial_schedule) SchedTimeSessAssignment.objects.create(timeslot=slot1, session=ames_session, schedule=unofficial_schedule)
# IESG breakfast # IESG breakfast
iesg_session = Session.objects.create(meeting=meeting, group=Group.objects.get(acronym="iesg"), iesg_session = SessionFactory(meeting=meeting, group=Group.objects.get(acronym="iesg"),
name="IESG Breakfast", attendees=25, name="IESG Breakfast", attendees=25,
requested_duration=datetime.timedelta(minutes=60), requested_duration=datetime.timedelta(minutes=60),
type_id="lead") type_id="lead", purpose_id='closed_meeting', add_to_schedule=False)
SchedulingEvent.objects.create(session=iesg_session, status_id='schedw', by=system_person) SchedulingEvent.objects.create(session=iesg_session, status_id='schedw', by=system_person)
SchedTimeSessAssignment.objects.create(timeslot=breakfast_slot, session=iesg_session, schedule=schedule) SchedTimeSessAssignment.objects.create(timeslot=breakfast_slot, session=iesg_session, schedule=schedule)
# No breakfast on unofficial schedule # No breakfast on unofficial schedule
# Registration # Registration
reg_session = Session.objects.create(meeting=meeting, group=Group.objects.get(acronym="secretariat"), reg_session = SessionFactory(meeting=meeting, group=Group.objects.get(acronym="secretariat"),
name="Registration", attendees=250, name="Registration", attendees=250,
requested_duration=datetime.timedelta(minutes=480), requested_duration=datetime.timedelta(minutes=480),
type_id="reg") type_id="reg", purpose_id='admin', add_to_schedule=False)
SchedulingEvent.objects.create(session=reg_session, status_id='schedw', by=system_person) SchedulingEvent.objects.create(session=reg_session, status_id='schedw', by=system_person)
SchedTimeSessAssignment.objects.create(timeslot=reg_slot, session=reg_session, schedule=base_schedule) SchedTimeSessAssignment.objects.create(timeslot=reg_slot, session=reg_session, schedule=base_schedule)
# Break # Break
break_session = Session.objects.create(meeting=meeting, group=Group.objects.get(acronym="secretariat"), break_session = SessionFactory(meeting=meeting, group=Group.objects.get(acronym="secretariat"),
name="Morning Break", attendees=250, name="Morning Break", attendees=250,
requested_duration=datetime.timedelta(minutes=30), requested_duration=datetime.timedelta(minutes=30),
type_id="break") type_id="break", purpose_id='social', add_to_schedule=False)
SchedulingEvent.objects.create(session=break_session, status_id='schedw', by=system_person) SchedulingEvent.objects.create(session=break_session, status_id='schedw', by=system_person)
SchedTimeSessAssignment.objects.create(timeslot=break_slot, session=break_session, schedule=base_schedule) SchedTimeSessAssignment.objects.create(timeslot=break_slot, session=break_session, schedule=base_schedule)
# IETF Plenary # IETF Plenary
plenary_session = Session.objects.create(meeting=meeting, group=Group.objects.get(acronym="ietf"), plenary_session = SessionFactory(meeting=meeting, group=Group.objects.get(acronym="ietf"),
name="IETF Plenary", attendees=250, name="IETF Plenary", attendees=250,
requested_duration=datetime.timedelta(minutes=60), requested_duration=datetime.timedelta(minutes=60),
type_id="plenary") type_id="plenary", purpose_id='plenary', add_to_schedule=False)
SchedulingEvent.objects.create(session=plenary_session, status_id='schedw', by=system_person) SchedulingEvent.objects.create(session=plenary_session, status_id='schedw', by=system_person)
SchedTimeSessAssignment.objects.create(timeslot=plenary_slot, session=plenary_session, schedule=schedule) SchedTimeSessAssignment.objects.create(timeslot=plenary_slot, session=plenary_session, schedule=schedule)

View file

@ -1,13 +1,14 @@
# Copyright The IETF Trust 2020, All Rights Reserved # Copyright The IETF Trust 2020, All Rights Reserved
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import copy
from django.conf import settings
from django.test import override_settings from django.test import override_settings
from ietf.group.factories import GroupFactory from ietf.group.factories import GroupFactory
from ietf.group.models import Group from ietf.group.models import Group
from ietf.meeting.factories import SessionFactory, MeetingFactory, TimeSlotFactory from ietf.meeting.factories import SessionFactory, MeetingFactory, TimeSlotFactory
from ietf.meeting.helpers import AgendaFilterOrganizer, AgendaKeywordTagger 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.meeting.test_data import make_meeting_test_data
from ietf.utils.test_utils import TestCase 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 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. 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) # decide whether meeting should use legacy keywords (for office hours)
legacy_keywords = meeting_num <= 111 legacy_keywords = meeting_num <= 111
@ -44,71 +44,92 @@ class AgendaKeywordTaggerTests(TestCase):
expected_group = group expected_group = group
expected_area = group.parent expected_area = group.parent
# create the ordinary sessions # create sessions, etc
for session_type in session_types: session_data = [
sess = SessionFactory(group=group, meeting=meeting, type_id=session_type, add_to_schedule=False) {
sess.timeslotassignments.create( 'description': 'regular wg session',
timeslot=TimeSlotFactory(meeting=meeting, type_id=session_type), '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, 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() 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 # Set up historic groups if needed.
# so skip that session. The expected_group will already have its historic_parent set
# if historic == 'parent'
if historic: if historic:
for a in assignments: 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 # Execute the method under test
AgendaKeywordTagger(assignments=assignments).apply() AgendaKeywordTagger(assignments=assignments).apply()
# Assert expected results # Assert expected results
self.assertEqual(len(assignments), orig_num_assignments, 'Should not change number of assignments')
for assignment in assignments: # check the assignment count - paranoid, but the method mutates its input so let's be careful
expected_filter_keywords = {assignment.slot_type().slug, assignment.session.type.slug} self.assertEqual(len(assignments), len(session_data), 'Should not change number of assignments')
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])
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( self.assertCountEqual(
assignment.filter_keywords, assignment.filter_keywords,
expected_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): def test_tag_assignments_with_filter_keywords(self):
# use distinct meeting numbers > 111 for non-legacy keyword tests # use distinct meeting numbers > 111 for non-legacy keyword tests
self.do_test_tag_assignments_with_filter_keywords(112) 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') 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): def test_tag_assignments_with_filter_keywords_legacy(self):
# use distinct meeting numbers <= 111 for legacy keyword tests # use distinct meeting numbers <= 111 for legacy keyword tests
self.do_test_tag_assignments_with_filter_keywords(101) self.do_test_tag_assignments_with_filter_keywords(101)
@ -131,8 +153,19 @@ class AgendaKeywordTaggerTests(TestCase):
class AgendaFilterOrganizerTests(TestCase): class AgendaFilterOrganizerTests(TestCase):
def test_get_filter_categories(self): 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 # set up
meeting = make_meeting_test_data() 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 # create extra groups for testing
iab = Group.objects.get(acronym='iab') iab = Group.objects.get(acronym='iab')
@ -147,55 +180,97 @@ class AgendaFilterOrganizerTests(TestCase):
# office hours session # office hours session
SessionFactory( SessionFactory(
group=Group.objects.get(acronym='farfut'), group=Group.objects.get(acronym='farfut'),
purpose_id='officehours' if not legacy else 'none',
type_id='other',
name='FARFUT office hours', name='FARFUT office hours',
meeting=meeting meeting=meeting
) )
expected = [ if legacy:
[ expected = [
# area category [
{'label': 'FARFUT', 'keyword': 'farfut', 'is_bof': False, # area category
'children': [ {'label': 'FARFUT', 'keyword': 'farfut', 'is_bof': False, 'toggled_by': [],
{'label': 'ames', 'keyword': 'ames', 'is_bof': False}, 'children': [
{'label': 'mars', 'keyword': 'mars', 'is_bof': False}, {'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, # non-area category
'children': [ {'label': 'IAB', 'keyword': 'iab', 'is_bof': False, 'toggled_by': [],
{'label': iab_child.acronym, 'keyword': iab_child.acronym, 'is_bof': False}, 'children': [
]}, {'label': iab_child.acronym, 'keyword': iab_child.acronym, 'is_bof': False, 'toggled_by': ['iab']},
{'label': 'IRTF', 'keyword': 'irtf', 'is_bof': False, ]},
'children': [ {'label': 'IRTF', 'keyword': 'irtf', 'is_bof': False, 'toggled_by': [],
{'label': irtf_child.acronym, 'keyword': irtf_child.acronym, 'is_bof': True}, '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, # non-group category
'children': [ {'label': 'Office Hours', 'keyword': 'officehours', 'is_bof': False, 'toggled_by': [],
{'label': 'FARFUT', 'keyword': 'farfut-officehours', 'is_bof': False} 'children': [
]}, {'label': 'FARFUT', 'keyword': 'farfut-officehours', 'is_bof': False, 'toggled_by': ['officehours', 'farfut']}
{'label': None, 'keyword': None,'is_bof': False, ]},
'children': [ {'label': None, 'keyword': None,'is_bof': False, 'toggled_by': [],
{'label': 'BoF', 'keyword': 'bof', 'is_bof': False}, 'children': [
{'label': 'Plenary', 'keyword': 'plenary', 'is_bof': False}, {'label': 'BoF', 'keyword': 'bof', 'is_bof': False, 'toggled_by': []},
]}, {'label': 'Plenary', 'keyword': 'plenary', 'is_bof': False, 'toggled_by': []},
], ]},
] ],
]
# when using sessions instead of assignments, won't get timeslot-type based filters else:
expected_with_sessions = copy.deepcopy(expected) expected = [
expected_with_sessions[-1].pop(0) # pops 'office hours' column [
# 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 # put all the above together for single-column tests
expected_single_category = [ expected_single_category = [sum(expected, [])]
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')
]
### ###
# test using sessions # test using sessions
@ -204,15 +279,17 @@ class AgendaFilterOrganizerTests(TestCase):
# default # default
filter_organizer = AgendaFilterOrganizer(sessions=sessions) 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 # single-column
filter_organizer = AgendaFilterOrganizer(sessions=sessions, single_category=True) 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 # test again using assignments
assignments = meeting.schedule.assignments.all() assignments = SchedTimeSessAssignment.objects.filter(
schedule__in=(meeting.schedule, meeting.schedule.base)
)
AgendaKeywordTagger(assignments=assignments).apply() AgendaKeywordTagger(assignments=assignments).apply()
# default # default

View file

@ -89,7 +89,13 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase):
s2.save() s2.save()
SchedTimeSessAssignment.objects.filter(session=s1).delete() 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( SchedulingEvent.objects.create(
session=s2b, 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'))) 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 # select - show session info
s2_element = self.driver.find_element_by_css_selector('#session{}'.format(s2.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)) s2b_element = self.driver.find_element(By.CSS_SELECTOR, '#session{}'.format(s2b.pk))
self.assertNotIn('other-session-selected', s2b_element.get_attribute('class')) self.assertNotIn('other-session-selected', s2b_element.get_attribute('class'))
s2_element.click() s2_element.click()
# other session for group should be flagged for highlighting # 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')) self.assertIn('other-session-selected', s2b_element.get_attribute('class'))
# other session for group should appear in the info panel # other session for group should appear in the info panel
session_info_container = self.driver.find_element_by_css_selector('.session-info-container') 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.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") self.assertEqual(session_info_container.find_element(By.CSS_SELECTOR, ".other-session .time").text, "not yet scheduled")
# deselect # 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')) self.assertNotIn('other-session-selected', s2b_element.get_attribute('class'))
# unschedule # unschedule
# we would like to do # 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() # ActionChains(self.driver).drag_and_drop(s2_element, unassigned_sessions_element).perform()
# #
# but unfortunately, Selenium does not simulate drag and drop events, see # but unfortunately, Selenium does not simulate drag and drop events, see
@ -158,20 +164,20 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase):
# sorting unassigned # sorting unassigned
sorted_pks = [s.pk for s in sorted([s1, s2, s2b], key=lambda s: (s.group.acronym, s.requested_duration, s.pk))] 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.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.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))] 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.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.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))] 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.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.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))] 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.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.assertTrue(self.driver.find_element(By.CSS_SELECTOR, '.unassigned-sessions .drop-target #session{} + #session{}'.format(*sorted_pks)))
# schedule # schedule
self.driver.execute_script("jQuery('#session{}').simulateDragDrop({{dropTarget: '#timeslot{} .drop-target'}});".format(s2.pk, slot1.pk)) 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) self.assertEqual(assignment.timeslot, slot1)
# timeslot constraint hints when selected # 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() s1_element.click()
# violated due to constraints - both the timeslot and its timeslot label # 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 # 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 '.day-flow .day:first-child .room-group:nth-child(2)' # count from 2 - first-child is the day label
) )
self.assertTrue( 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' '.time-header > .time-label.would-violate-hint:first-child'
), ),
'Timeslot header label should show a would-violate hint for a constraint violation' 'Timeslot header label should show a would-violate hint for a constraint violation'
) )
# violated due to missing capacity # 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 # 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 '.day-flow .day:first-child .room-group:nth-child(3)' # count from 2 - first-child is the day label
) )
self.assertFalse( 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)' '.time-header > .time-label.would-violate-hint:nth-child(2)'
), ),
'Timeslot header label should not show a would-violate hint for room capacity violation' '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) self.assertEqual(assignment.timeslot, slot2)
# too many attendees warning # 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 # 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 # constraint hints
s1_element.click() s1_element.click()
self.assertIn('would-violate-hint', s2_element.get_attribute('class')) 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()) self.assertTrue(constraint_element.is_displayed())
# current constraint violations # 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)))) 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()) self.assertTrue(constraint_element.is_displayed())
# hide sessions in area # hide sessions in area
self.assertTrue(s1_element.is_displayed()) 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.assertTrue(s1_element.is_displayed()) # should still be displayed
self.assertIn('hidden-parent', s1_element.get_attribute('class'), self.assertIn('hidden-parent', s1_element.get_attribute('class'),
'Session should be hidden when parent disabled') 'Session should be hidden when parent disabled')
@ -249,7 +255,7 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase):
self.assertNotIn('selected', s1_element.get_attribute('class'), self.assertNotIn('selected', s1_element.get_attribute('class'),
'Session should not be selectable when parent disabled') '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.assertTrue(s1_element.is_displayed())
self.assertNotIn('hidden-parent', s1_element.get_attribute('class'), self.assertNotIn('hidden-parent', s1_element.get_attribute('class'),
'Session should not be hidden when parent enabled') 'Session should not be hidden when parent enabled')
@ -258,32 +264,32 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase):
'Session should be selectable when parent enabled') 'Session should be selectable when parent enabled')
# hide timeslots # hide timeslots
self.driver.find_element_by_css_selector(".timeslot-group-toggles button").click() 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.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 [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.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.assertTrue(not self.driver.find_element(By.CSS_SELECTOR, "#timeslot-group-toggles-modal").is_displayed())
# swap days # swap days
self.driver.find_element_by_css_selector(".day .swap-days[data-dayid=\"{}\"]".format(slot4.time.date().isoformat())).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.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 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, "#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') 'Session s1 should have moved to second meeting day')
# swap timeslot column - put session in a differently-timed timeslot # 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) '.day .swap-timeslot-col[data-timeslot-pk="{}"]'.format(slot1b.pk)
).click() # open modal on the second timeslot for room1 ).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.assertTrue(self.driver.find_element(By.CSS_SELECTOR, "#swap-timeslot-col-modal").is_displayed())
self.driver.find_element_by_css_selector( self.driver.find_element(By.CSS_SELECTOR,
'#swap-timeslot-col-modal input[name="target_timeslot"][value="{}"]'.format(slot4.pk) '#swap-timeslot-col-modal input[name="target_timeslot"][value="{}"]'.format(slot4.pk)
).click() # select room1 timeslot that has a session in it ).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') 'Session s1 should have moved to second timeslot on first meeting day')
def test_past_flags(self): def test_past_flags(self):
@ -351,19 +357,19 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase):
self.login(username=meeting.schedule.owner.user.username) self.login(username=meeting.schedule.owner.user.username)
self.driver.get(url) 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) ','.join('#timeslot{} .past-flag'.format(ts.pk) for ts in past_timeslots)
) )
self.assertGreaterEqual(len(past_flags), len(past_timeslots) + len(past_sessions), self.assertGreaterEqual(len(past_flags), len(past_timeslots) + len(past_sessions),
'Expected at least one flag for each past timeslot and session') '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) ','.join('#timeslot{} .past-flag'.format(ts.pk) for ts in now_timeslots)
) )
self.assertGreaterEqual(len(now_flags), len(now_timeslots) + len(now_sessions), self.assertGreaterEqual(len(now_flags), len(now_timeslots) + len(now_sessions),
'Expected at least one flag for each "now" timeslot and session') '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) ','.join('#timeslot{} .past-flag'.format(ts.pk) for ts in future_timeslots)
) )
self.assertGreaterEqual(len(future_flags), len(future_timeslots) + len(future_sessions), 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.login(username=meeting.schedule.owner.user.username)
self.driver.get(url) 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( ','.join(
'.swap-days[data-start="{}"]'.format(ts.time.date().isoformat()) for ts in past_timeslots '.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') 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( ','.join(
'.swap-days[data-start="{}"]'.format(ts.time.date().isoformat()) for ts in future_timeslots '.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') 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( ','.join(
'.swap-days[data-start="{}"]'.format(ts.time.date().isoformat()) for ts in now_timeslots '.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.fail('Modal never appeared')
self.assertFalse( self.assertFalse(
any(radio.is_enabled() 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) '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', '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]) enabled_timeslots = (ts for ts in future_timeslots if ts != future_timeslots[clicked_index])
self.assertTrue( self.assertTrue(
all(radio.is_enabled() 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) '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', 'Future day is not enabled in swap-days modal for official schedule',
) )
self.assertFalse( self.assertFalse(
any(radio.is_enabled() 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) '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', '"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.login(username=meeting.schedule.owner.user.username)
self.driver.get(url) 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( ','.join(
'.swap-timeslot-col[data-start="{}"]'.format(ts.utc_start_time().isoformat()) for ts in past_timeslots '.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') 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( ','.join(
'.swap-timeslot-col[data-start="{}"]'.format(ts.utc_start_time().isoformat()) for ts in future_timeslots '.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') 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( ','.join(
'.swap-timeslot-col[data-start="{}"]'.format(ts.utc_start_time().isoformat()) for ts in now_timeslots '.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.fail('Modal never appeared')
self.assertFalse( self.assertFalse(
any(radio.is_enabled() 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) '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', '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]) enabled_timeslots = (ts for ts in future_timeslots if ts != future_timeslots[clicked_index])
self.assertTrue( self.assertTrue(
all(radio.is_enabled() 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) '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', 'Future timeslot is not enabled in swap-timeslot-col modal for official schedule',
) )
self.assertFalse( self.assertFalse(
any(radio.is_enabled() 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) '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', '"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): def sort_by_position(driver, sessions):
"""Helper to sort sessions by the position of their session element in the unscheduled box""" """Helper to sort sessions by the position of their session element in the unscheduled box"""
def _sort_key(sess): 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 (elt.location['y'], elt.location['x'])
return sorted(sessions, key=_sort_key) return sorted(sessions, key=_sort_key)
@ -687,10 +693,10 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase):
self.login('secretary') self.login('secretary')
self.driver.get(url) self.driver.get(url)
select = self.driver.find_element_by_name('sort_unassigned') select = self.driver.find_element(By.NAME, 'sort_unassigned')
options = { options = {
opt.get_attribute('value'): opt 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 # check sorting by name
@ -760,18 +766,8 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase):
all of the events needed by the editor. all of the events needed by the editor.
""" """
# Set up a meeting and a schedule a plain user can edit # Set up a meeting and a schedule a plain user can edit
meeting = make_meeting_test_data() schedule = ScheduleFactory(owner__user__username="plain")
schedule = Schedule.objects.filter(meeting=meeting, owner__user__username="plain").first() meeting = schedule.meeting
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,
)
# Open the editor # Open the editor
self.login() self.login()
@ -780,12 +776,11 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase):
kwargs=dict(num=meeting.number, name=schedule.name, owner=schedule.owner_email()) kwargs=dict(num=meeting.number, name=schedule.name, owner=schedule.owner_email())
) )
self.driver.get(url) self.driver.get(url)
# Check that the drop target for unassigned sessions is actually empty # 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' '.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') 'Unassigned sessions box is not empty, test is broken')
# Check that the drop target has non-zero size # 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)) kwargs=dict(num=meeting.number, owner=schedule.owner.email(), name=schedule.name))
self.login(schedule.owner.user.username) self.login(schedule.owner.user.username)
self.driver.get(url) 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() session_elements[0].click()
# All conflicting sessions should be flagged with the would-violate-hint class. # 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 # driver.get() will wait for scripts to finish, but not ajax
# requests. Wait for completion of the permissions check: # 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") 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() s1 = Session.objects.filter(group__acronym='mars', meeting=meeting).first()
selector = "#session_{}".format(s1.pk) 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) 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)) element = self.driver.find_element(By.ID, 'session_{}'.format(s1.pk))
target = self.driver.find_element_by_id('sortable-list') target = self.driver.find_element(By.ID, 'sortable-list')
ActionChains(self.driver).drag_and_drop(element,target).perform() 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 time.sleep(0.1) # The API that modifies the database runs async
@ -905,8 +900,8 @@ class SlideReorderTests(IetfSeleniumTestCase):
self.secr_login() self.secr_login()
self.driver.get(url) self.driver.get(url)
#debug.show('unicode(self.driver.page_source)') #debug.show('unicode(self.driver.page_source)')
second = self.driver.find_element_by_css_selector('#slides tr:nth-child(2)') 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)') third = self.driver.find_element(By.CSS_SELECTOR, '#slides tr:nth-child(3)')
ActionChains(self.driver).drag_and_drop(second,third).perform() ActionChains(self.driver).drag_and_drop(second,third).perform()
time.sleep(0.1) # The API that modifies the database runs async 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.driver.get(self.absreverse('ietf.meeting.views.agenda') + querystring)
self.assert_agenda_item_visibility(visible_groups) self.assert_agenda_item_visibility(visible_groups)
self.assert_agenda_view_filter_matches_ics_filter(querystring) 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: if len(querystring) == 0:
self.assertFalse(weekview_iframe.is_displayed(), 'Weekview should be hidden when filters off') self.assertFalse(weekview_iframe.is_displayed(), 'Weekview should be hidden when filters off')
else: else:
@ -1184,7 +1179,7 @@ class AgendaTests(IetfSeleniumTestCase):
for item in self.get_expected_items(): for item in self.get_expected_items():
row_id = self.row_id_for_item(item) row_id = self.row_id_for_item(item)
try: try:
item_row = self.driver.find_element_by_id(row_id) item_row = self.driver.find_element(By.ID, row_id)
except NoSuchElementException: except NoSuchElementException:
item_row = None item_row = None
self.assertIsNotNone(item_row, 'No row for schedule item "%s"' % row_id) self.assertIsNotNone(item_row, 'No row for schedule item "%s"' % row_id)
@ -1205,7 +1200,7 @@ class AgendaTests(IetfSeleniumTestCase):
label = 'Free Slot' label = 'Free Slot'
try: 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: except NoSuchElementException:
item_div = None item_div = None
@ -1480,7 +1475,7 @@ class AgendaTests(IetfSeleniumTestCase):
ics_url = self.absreverse('ietf.meeting.views.agenda_ical') ics_url = self.absreverse('ietf.meeting.views.agenda_ical')
# parse out the events # 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()] visible_rows = [r for r in agenda_rows if r.is_displayed()]
sessions = [self.session_from_agenda_row_id(row.get_attribute("id")) sessions = [self.session_from_agenda_row_id(row.get_attribute("id"))
for row in visible_rows] for row in visible_rows]
@ -1510,7 +1505,7 @@ class AgendaTests(IetfSeleniumTestCase):
self.driver.get(url) self.driver.get(url)
# modal should start hidden # 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()) self.assertFalse(modal_div.is_displayed())
# Click the 'materials' button # 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 self.assertGreater(not_deleted_slides.count(), 0) # make sure this isn't a pointless test
for slide in 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) self.assertIsNotNone(anchor)
deleted_slides = session.materials.filter( 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 self.assertGreater(deleted_slides.count(), 0) # make sure this isn't a pointless test
for slide in deleted_slides: for slide in deleted_slides:
with self.assertRaises(NoSuchElementException): 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 # Now close the modal
close_modal_button = WebDriverWait(self.driver, 2).until( close_modal_button = WebDriverWait(self.driver, 2).until(
@ -1589,7 +1584,7 @@ class AgendaTests(IetfSeleniumTestCase):
self.assertNotIn(newly_deleted_slide, not_deleted_slides) self.assertNotIn(newly_deleted_slide, not_deleted_slides)
self.assertIn(newly_undeleted_slide, not_deleted_slides) self.assertIn(newly_undeleted_slide, not_deleted_slides)
for slide in 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) self.assertIsNotNone(anchor)
deleted_slides = session.materials.filter( deleted_slides = session.materials.filter(
@ -1599,7 +1594,7 @@ class AgendaTests(IetfSeleniumTestCase):
self.assertNotIn(newly_undeleted_slide, deleted_slides) self.assertNotIn(newly_undeleted_slide, deleted_slides)
for slide in deleted_slides: for slide in deleted_slides:
with self.assertRaises(NoSuchElementException): 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): def test_agenda_time_zone_selection(self):
self.assertNotEqual(self.meeting.time_zone, 'UTC', 'Meeting time zone must not be UTC') 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') tz_select_input = self.driver.find_element(By.ID, 'timezone-select')
meeting_tz_link = self.driver.find_element_by_id('meeting-timezone') meeting_tz_link = self.driver.find_element(By.ID, 'meeting-timezone')
local_tz_link = self.driver.find_element_by_id('local-timezone') local_tz_link = self.driver.find_element(By.ID, 'local-timezone')
utc_tz_link = self.driver.find_element_by_id('utc-timezone') utc_tz_link = self.driver.find_element(By.ID, 'utc-timezone')
tz_displays = self.driver.find_elements_by_css_selector('.current-tz') tz_displays = self.driver.find_elements(By.CSS_SELECTOR, '.current-tz')
self.assertGreaterEqual(len(tz_displays), 1) 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 # 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 # 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 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(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') 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) 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) 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"]') utc_tz_opt = tz_select_input.find_element(By.CSS_SELECTOR, 'option[value="UTC"]')
# Should start off in meeting time zone # Should start off in meeting time zone
self.assertTrue(meeting_tz_opt.is_selected()) 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 # Verify that elements are all updated when the filters change. That the correct elements
# have the appropriate classes is a separate test. # 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.assertGreater(len(elements_to_check), 0, 'No elements with agenda links to update were found')
self.assertFalse( self.assertFalse(
any(checkbox.is_selected() 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"]')), 'input.checkbox[name="selected-sessions"]')),
'Sessions were selected before being clicked', '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_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"]') 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"]') 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"]') 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"]') 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 mars_sessa_checkbox.click() # select mars session
try: try:
@ -1884,9 +1879,9 @@ class WeekviewTests(IetfSeleniumTestCase):
def _assert_wrapped(displayed, expected_time_string): def _assert_wrapped(displayed, expected_time_string):
self.assertEqual(len(displayed), 2) self.assertEqual(len(displayed), 2)
first = displayed[0] first = displayed[0]
first_parent = first.find_element_by_xpath('..') first_parent = first.find_element(By.XPATH, '..')
second = displayed[1] second = displayed[1]
second_parent = second.find_element_by_xpath('..') second_parent = second.find_element(By.XPATH, '..')
self.assertNotIn('continued', first.text) self.assertNotIn('continued', first.text)
self.assertIn(expected_time_string, first_parent.text) self.assertIn(expected_time_string, first_parent.text)
self.assertIn('continued', second.text) self.assertIn('continued', second.text)
@ -1895,7 +1890,7 @@ class WeekviewTests(IetfSeleniumTestCase):
def _assert_not_wrapped(displayed, expected_time_string): def _assert_not_wrapped(displayed, expected_time_string):
self.assertEqual(len(displayed), 1) self.assertEqual(len(displayed), 1)
first = displayed[0] first = displayed[0]
first_parent = first.find_element_by_xpath('..') first_parent = first.find_element(By.XPATH, '..')
self.assertNotIn('continued', first.text) self.assertNotIn('continued', first.text)
self.assertIn(expected_time_string, first_parent.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_interim = make_interim_meeting(somegroup, datetime.date.today() + datetime.timedelta(days=20))
sg_sess = sg_interim.session_set.first() sg_sess = sg_interim.session_set.first()
sg_slot = sg_sess.timeslotassignments.first().timeslot sg_slot = sg_sess.timeslotassignments.first().timeslot
sg_sess.purpose_id = 'plenary'
sg_sess.type_id = 'plenary' sg_sess.type_id = 'plenary'
sg_slot.type_id = 'plenary' sg_slot.type_id = 'plenary'
sg_sess.save() sg_sess.save()
@ -2069,7 +2065,7 @@ class InterimTests(IetfSeleniumTestCase):
return meetings return meetings
def find_upcoming_meeting_entries(self): 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' '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 # 12 in order to check the starting month of the following year, which
# will usually contain the day 1 year from the start date. # will usually contain the day 1 year from the start date.
for _ in range(13): 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' 'div#calendar div.fc-content'
) )
for entry in entries: for entry in entries:
@ -2150,9 +2146,9 @@ class InterimTests(IetfSeleniumTestCase):
if simplified_querystring in ['?show=', '?hide=', '?show=&hide=']: if simplified_querystring in ['?show=', '?hide=', '?show=&hide=']:
simplified_querystring = '' # these empty querystrings will be dropped (not an exhaustive list) 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')) 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')) self.assertIn(simplified_querystring, webcal_link.get_attribute('href'))
def assert_upcoming_view_filter_matches_ics_filter(self, filter_string): def assert_upcoming_view_filter_matches_ics_filter(self, filter_string):
@ -2314,8 +2310,8 @@ class InterimTests(IetfSeleniumTestCase):
ts = session.official_timeslotassignment().timeslot ts = session.official_timeslotassignment().timeslot
start = ts.utc_start_time().astimezone(zone).strftime('%Y-%m-%d %H:%M') start = ts.utc_start_time().astimezone(zone).strftime('%Y-%m-%d %H:%M')
end = ts.utc_end_time().astimezone(zone).strftime('%H:%M') end = ts.utc_end_time().astimezone(zone).strftime('%H:%M')
meeting_link = self.driver.find_element_by_link_text(session.meeting.number) meeting_link = self.driver.find_element(By.LINK_TEXT, session.meeting.number)
time_td = meeting_link.find_element_by_xpath('../../td[@class="session-time"]') time_td = meeting_link.find_element(By.XPATH, '../../td[@class="session-time"]')
self.assertIn('%s - %s' % (start, end), time_td.text) self.assertIn('%s - %s' % (start, end), time_td.text)
def _assert_ietf_tz_correct(meetings, tz): def _assert_ietf_tz_correct(meetings, tz):
@ -2333,8 +2329,8 @@ class InterimTests(IetfSeleniumTestCase):
start = start_dt.astimezone(zone).strftime('%Y-%m-%d') start = start_dt.astimezone(zone).strftime('%Y-%m-%d')
end = end_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) meeting_link = self.driver.find_element(By.LINK_TEXT, "IETF " + meeting.number)
time_td = meeting_link.find_element_by_xpath('../../td[@class="meeting-time"]') time_td = meeting_link.find_element(By.XPATH, '../../td[@class="meeting-time"]')
self.assertIn('%s - %s' % (start, end), time_td.text) self.assertIn('%s - %s' % (start, end), time_td.text)
sessions = [m.session_set.first() for m in self.displayed_interims()] 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.assertGreater(len(ietf_meetings), 0)
self.driver.get(self.absreverse('ietf.meeting.views.upcoming')) self.driver.get(self.absreverse('ietf.meeting.views.upcoming'))
tz_select_input = self.driver.find_element_by_id('timezone-select') tz_select_input = self.driver.find_element(By.ID, 'timezone-select')
tz_select_bottom_input = self.driver.find_element_by_id('timezone-select-bottom') tz_select_bottom_input = self.driver.find_element(By.ID, 'timezone-select-bottom')
local_tz_link = self.driver.find_element_by_id('local-timezone') local_tz_link = self.driver.find_element(By.ID, 'local-timezone')
utc_tz_link = self.driver.find_element_by_id('utc-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') 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') 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 # wait for the select box to be updated - look for an arbitrary time zone to be in
# its options list to detect this # its options list to detect this
@ -2358,18 +2354,18 @@ class InterimTests(IetfSeleniumTestCase):
(By.CSS_SELECTOR, '#timezone-select > option[value="%s"]' % arbitrary_tz) (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) 'option[value="%s"]' % arbitrary_tz)
utc_tz_opt = tz_select_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"]') 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 # 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 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. # 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 = 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_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_bottom_opt = tz_select_bottom_input.find_element(By.CSS_SELECTOR, 'option[value="%s"]' % local_tz)
# Should start off in local time zone # Should start off in local time zone
self.assertTrue(local_tz_opt.is_selected()) self.assertTrue(local_tz_opt.is_selected())
@ -2466,7 +2462,7 @@ class InterimTests(IetfSeleniumTestCase):
slug = assignment.slug() slug = assignment.slug()
# modal should start hidden # 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()) self.assertFalse(modal_div.is_displayed())
# Click the 'materials' button # Click the 'materials' button

View file

@ -322,12 +322,12 @@ class MeetingTests(BaseMeetingTestCase):
parent=iab, parent=iab,
list_email="venus@ietf.org", list_email="venus@ietf.org",
) )
venus_session = Session.objects.create( venus_session = SessionFactory(
meeting=meeting, meeting=meeting,
group=venus, group=venus,
attendees=10, attendees=10,
requested_duration=datetime.timedelta(minutes=60), requested_duration=datetime.timedelta(minutes=60),
type_id='regular', add_to_schedule=False,
) )
system_person = Person.objects.get(name="(System)") system_person = Person.objects.get(name="(System)")
SchedulingEvent.objects.create(session=venus_session, status_id='schedw', by=system_person) SchedulingEvent.objects.create(session=venus_session, status_id='schedw', by=system_person)
@ -784,7 +784,7 @@ class MeetingTests(BaseMeetingTestCase):
) )
self.do_ical_filter_test( self.do_ical_filter_test(
meeting, meeting,
querystring='?show=plenary,secretariat,ames&hide=reg', querystring='?show=plenary,secretariat,ames&hide=admin',
expected_session_summaries=[ expected_session_summaries=[
'Morning Break', 'Morning Break',
'IETF Plenary', 'IETF Plenary',
@ -3062,10 +3062,14 @@ class EditTests(TestCase):
def test_edit_schedule(self): def test_edit_schedule(self):
meeting = make_meeting_test_data() meeting = make_meeting_test_data()
self.client.login(username="secretary", password="secretary+password") self.client.login(username="secretary", password="secretary+password")
r = self.client.get(urlreverse("ietf.meeting.views.edit_schedule", kwargs=dict(num=meeting.number))) r = self.client.get(urlreverse("ietf.meeting.views.edit_schedule", kwargs={'num': meeting.number}))
self.assertContains(r, "load_assignments") 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 test_official_record_schedule_is_read_only(self):
def _set_date_offset_and_retrieve_page(meeting, days_offset, client): 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')) 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"), base_session = SessionFactory(meeting=meeting, group=Group.objects.get(acronym="irg"),
attendees=20, requested_duration=datetime.timedelta(minutes=30), attendees=20, requested_duration=datetime.timedelta(minutes=30),
type_id='regular') add_to_schedule=False)
SchedulingEvent.objects.create(session=base_session, status_id='schedw', by=Person.objects.get(user__username='secretary')) 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) 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() session1 = Session.objects.filter(meeting=meeting, group__acronym='mars').first()
session2 = Session.objects.filter(meeting=meeting, group__acronym='ames').first() session2 = Session.objects.filter(meeting=meeting, group__acronym='ames').first()
session3 = Session.objects.create(meeting=meeting, group=Group.objects.get(acronym='mars'), session3 = SessionFactory(meeting=meeting, group=Group.objects.get(acronym='mars'),
attendees=10, requested_duration=datetime.timedelta(minutes=70), attendees=10, requested_duration=datetime.timedelta(minutes=70),
type_id='regular') add_to_schedule=False)
SchedulingEvent.objects.create(session=session3, status_id='schedw', by=Person.objects.first()) SchedulingEvent.objects.create(session=session3, status_id='schedw', by=Person.objects.first())
slot2 = TimeSlot.objects.filter(meeting=meeting, type='regular').order_by('-time').first() slot2 = TimeSlot.objects.filter(meeting=meeting, type='regular').order_by('-time').first()
@ -6200,11 +6204,13 @@ class AgendaFilterTests(TestCase):
dict( dict(
label='child00', label='child00',
keyword='keyword00', keyword='keyword00',
toggled_by=['keyword0'],
is_bof=False, is_bof=False,
), ),
dict( dict(
label='child01', label='child01',
keyword='keyword01', keyword='keyword01',
toggled_by=['keyword0', 'bof'],
is_bof=True, is_bof=True,
), ),
]), ]),
@ -6215,11 +6221,13 @@ class AgendaFilterTests(TestCase):
dict( dict(
label='child10', label='child10',
keyword='keyword10', keyword='keyword10',
toggled_by=['keyword1'],
is_bof=False, is_bof=False,
), ),
dict( dict(
label='child11', label='child11',
keyword='keyword11', keyword='keyword11',
toggled_by=['keyword1', 'bof'],
is_bof=True, is_bof=True,
), ),
]), ]),
@ -6232,11 +6240,13 @@ class AgendaFilterTests(TestCase):
dict( dict(
label='child20', label='child20',
keyword='keyword20', keyword='keyword20',
toggled_by=['keyword2', 'bof'],
is_bof=True, is_bof=True,
), ),
dict( dict(
label='child21', label='child21',
keyword='keyword21', keyword='keyword21',
toggled_by=['keyword2'],
is_bof=False, is_bof=False,
), ),
]), ]),
@ -6249,11 +6259,13 @@ class AgendaFilterTests(TestCase):
dict( dict(
label='child30', label='child30',
keyword='keyword30', keyword='keyword30',
toggled_by=[],
is_bof=False, is_bof=False,
), ),
dict( dict(
label='child31', label='child31',
keyword='keyword31', keyword='keyword31',
toggled_by=['bof'],
is_bof=True, is_bof=True,
), ),
]), ]),
@ -6283,7 +6295,6 @@ class AgendaFilterTests(TestCase):
_assert_button_ok(header_cells.eq(0)('button.keyword0'), _assert_button_ok(header_cells.eq(0)('button.keyword0'),
expected_label='area0', expected_label='area0',
expected_filter_item='keyword0') expected_filter_item='keyword0')
buttons = button_cells.eq(0)('button.pickview') buttons = button_cells.eq(0)('button.pickview')
self.assertEqual(len(buttons), 2) # two children self.assertEqual(len(buttons), 2) # two children
_assert_button_ok(buttons('.keyword00'), _assert_button_ok(buttons('.keyword00'),

View file

@ -88,7 +88,7 @@ type_ietf_only_patterns_id_optional = [
url(r'^agenda(?P<ext>.csv)$', views.agenda), url(r'^agenda(?P<ext>.csv)$', views.agenda),
url(r'^agenda/edit$', url(r'^agenda/edit$',
RedirectView.as_view(pattern_name='ietf.meeting.views.edit_meeting_schedule', permanent=True), 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'^agenda/edit/$', views.edit_meeting_schedule),
url(r'^requests$', views.meeting_requests), url(r'^requests$', views.meeting_requests),
url(r'^agenda/agenda\.ics$', views.agenda_ical), url(r'^agenda/agenda\.ics$', views.agenda_ical),

View file

@ -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 swap_meeting_schedule_timeslot_assignments, bulk_create_timeslots
from ietf.meeting.utils import preprocess_meeting_important_dates from ietf.meeting.utils import preprocess_meeting_important_dates
from ietf.message.utils import infer_message 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.utils import handle_upload_file
from ietf.secr.proceedings.proc_utils import (get_progress_stats, post_process, import_audio_files, from ietf.secr.proceedings.proc_utils import (get_progress_stats, post_process, import_audio_files,
create_recording) create_recording)
@ -621,7 +621,7 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
s.scheduling_label = "???" s.scheduling_label = "???"
s.purpose_label = None 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.scheduling_label = s.group.acronym
s.purpose_label = 'BoF' if s.group.is_bof() else s.group.type.name s.purpose_label = 'BoF' if s.group.is_bof() else s.group.type.name
else: else:
@ -1058,6 +1058,7 @@ class TimeSlotForm(forms.Form):
location = RoomNameModelChoiceField(queryset=Room.objects.all(), required=False, empty_label="(No location)") location = RoomNameModelChoiceField(queryset=Room.objects.all(), required=False, empty_label="(No location)")
show_location = forms.BooleanField(initial=True, required=False) show_location = forms.BooleanField(initial=True, required=False)
type = forms.ModelChoiceField(queryset=TimeSlotTypeName.objects.filter(used=True), empty_label=None, 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) 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) 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'), 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 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: if timeslot:
self.initial = { self.initial = {
'day': timeslot.time.date(), 'day': timeslot.time.date(),
@ -1115,7 +1122,10 @@ class TimeSlotForm(forms.Form):
ts_type = self.cleaned_data.get('type') ts_type = self.cleaned_data.get('type')
short = self.cleaned_data.get('short') 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 in ['break', 'reg', 'reserved', 'unavail', 'regular']:
if ts_type.slug != 'regular': if ts_type.slug != 'regular':
self.cleaned_data['group'] = self.fields['group'].queryset.get(acronym='secretariat') 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: 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") 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 if (self.active_assignment
and self.active_assignment.session.group != self.cleaned_data.get('group') and self.active_assignment.session.group != self.cleaned_data.get('group')
and self.active_assignment.session.materials.exists() 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'], short=c['short'],
group=c['group'], group=c['group'],
type=c['type'], type=c['type'],
purpose=c['purpose'],
agenda_note=c.get('agenda_note') or "", agenda_note=c.get('agenda_note') or "",
) )

View file

@ -2620,6 +2620,7 @@
], ],
"req_subm_approval": true, "req_subm_approval": true,
"role_order": "[\n \"chair\",\n \"lead\",\n \"delegate\",\n \"matman\"\n]", "role_order": "[\n \"chair\",\n \"lead\",\n \"delegate\",\n \"matman\"\n]",
"session_purposes": "[\n \"presentation\"\n]",
"show_on_agenda": true "show_on_agenda": true
}, },
"model": "group.groupfeatures", "model": "group.groupfeatures",
@ -2656,6 +2657,7 @@
"parent_types": [], "parent_types": [],
"req_subm_approval": false, "req_subm_approval": false,
"role_order": "[\n \"chair\"\n]", "role_order": "[\n \"chair\"\n]",
"session_purposes": "[\n \"closed_meeting\",\n \"officehours\"\n]",
"show_on_agenda": false "show_on_agenda": false
}, },
"model": "group.groupfeatures", "model": "group.groupfeatures",
@ -2695,6 +2697,7 @@
], ],
"req_subm_approval": true, "req_subm_approval": true,
"role_order": "[\n \"chair\",\n \"secr\"\n]", "role_order": "[\n \"chair\",\n \"secr\"\n]",
"session_purposes": "[\n \"regular\"\n]",
"show_on_agenda": true "show_on_agenda": true
}, },
"model": "group.groupfeatures", "model": "group.groupfeatures",
@ -2733,6 +2736,7 @@
], ],
"req_subm_approval": true, "req_subm_approval": true,
"role_order": "[\n \"chair\",\n \"secr\"\n]", "role_order": "[\n \"chair\",\n \"secr\"\n]",
"session_purposes": "[\n \"regular\"\n]",
"show_on_agenda": false "show_on_agenda": false
}, },
"model": "group.groupfeatures", "model": "group.groupfeatures",
@ -2771,6 +2775,7 @@
], ],
"req_subm_approval": true, "req_subm_approval": true,
"role_order": "[\n \"chair\",\n \"secr\"\n]", "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 "show_on_agenda": false
}, },
"model": "group.groupfeatures", "model": "group.groupfeatures",
@ -2809,6 +2814,7 @@
], ],
"req_subm_approval": true, "req_subm_approval": true,
"role_order": "[\n \"chair\",\n \"secr\"\n]", "role_order": "[\n \"chair\",\n \"secr\"\n]",
"session_purposes": "[\n \"closed_meeting\",\n \"regular\"\n]",
"show_on_agenda": true "show_on_agenda": true
}, },
"model": "group.groupfeatures", "model": "group.groupfeatures",
@ -2819,6 +2825,7 @@
"about_page": "ietf.group.views.group_about", "about_page": "ietf.group.views.group_about",
"acts_like_wg": false, "acts_like_wg": false,
"admin_roles": "[\n \"lead\"\n]", "admin_roles": "[\n \"lead\"\n]",
"agenda_filter_type": "none",
"agenda_type": "ad", "agenda_type": "ad",
"create_wiki": false, "create_wiki": false,
"custom_group_roles": true, "custom_group_roles": true,
@ -2846,6 +2853,7 @@
], ],
"req_subm_approval": false, "req_subm_approval": false,
"role_order": "[\n \"lead\",\n \"chair\",\n \"secr\"\n]", "role_order": "[\n \"lead\",\n \"chair\",\n \"secr\"\n]",
"session_purposes": "[\n \"closed_meeting\",\n \"officehours\",\n \"open_meeting\"\n]",
"show_on_agenda": false "show_on_agenda": false
}, },
"model": "group.groupfeatures", "model": "group.groupfeatures",
@ -2882,6 +2890,7 @@
"parent_types": [], "parent_types": [],
"req_subm_approval": false, "req_subm_approval": false,
"role_order": "[\n \"chair\"\n]", "role_order": "[\n \"chair\"\n]",
"session_purposes": "[\n \"officehours\"\n]",
"show_on_agenda": false "show_on_agenda": false
}, },
"model": "group.groupfeatures", "model": "group.groupfeatures",
@ -2918,6 +2927,7 @@
"parent_types": [], "parent_types": [],
"req_subm_approval": true, "req_subm_approval": true,
"role_order": "[\n \"chair\",\n \"delegate\",\n \"member\"\n]", "role_order": "[\n \"chair\",\n \"delegate\",\n \"member\"\n]",
"session_purposes": "[\n \"closed_meeting\",\n \"open_meeting\"\n]",
"show_on_agenda": false "show_on_agenda": false
}, },
"model": "group.groupfeatures", "model": "group.groupfeatures",
@ -2956,6 +2966,7 @@
], ],
"req_subm_approval": true, "req_subm_approval": true,
"role_order": "[\n \"chair\",\n \"secr\"\n]", "role_order": "[\n \"chair\",\n \"secr\"\n]",
"session_purposes": "[\n \"admin\",\n \"plenary\",\n \"presentation\",\n \"social\"\n]",
"show_on_agenda": false "show_on_agenda": false
}, },
"model": "group.groupfeatures", "model": "group.groupfeatures",
@ -2994,6 +3005,7 @@
], ],
"req_subm_approval": false, "req_subm_approval": false,
"role_order": "[\n \"chair\",\n \"secr\"\n]", "role_order": "[\n \"chair\",\n \"secr\"\n]",
"session_purposes": "[]",
"show_on_agenda": false "show_on_agenda": false
}, },
"model": "group.groupfeatures", "model": "group.groupfeatures",
@ -3032,6 +3044,7 @@
], ],
"req_subm_approval": true, "req_subm_approval": true,
"role_order": "[\n \"chair\",\n \"secr\"\n]", "role_order": "[\n \"chair\",\n \"secr\"\n]",
"session_purposes": "[]",
"show_on_agenda": false "show_on_agenda": false
}, },
"model": "group.groupfeatures", "model": "group.groupfeatures",
@ -3068,6 +3081,7 @@
"parent_types": [], "parent_types": [],
"req_subm_approval": true, "req_subm_approval": true,
"role_order": "[\n \"chair\",\n \"delegate\"\n]", "role_order": "[\n \"chair\",\n \"delegate\"\n]",
"session_purposes": "[\n \"officehours\"\n]",
"show_on_agenda": false "show_on_agenda": false
}, },
"model": "group.groupfeatures", "model": "group.groupfeatures",
@ -3106,6 +3120,7 @@
], ],
"req_subm_approval": true, "req_subm_approval": true,
"role_order": "[\n \"chair\",\n \"secr\"\n]", "role_order": "[\n \"chair\",\n \"secr\"\n]",
"session_purposes": "[\n \"officehours\",\n \"open_meeting\",\n \"presentation\"\n]",
"show_on_agenda": false "show_on_agenda": false
}, },
"model": "group.groupfeatures", "model": "group.groupfeatures",
@ -3144,6 +3159,7 @@
], ],
"req_subm_approval": true, "req_subm_approval": true,
"role_order": "[\n \"chair\",\n \"member\",\n \"advisor\"\n]", "role_order": "[\n \"chair\",\n \"member\",\n \"advisor\"\n]",
"session_purposes": "[\n \"closed_meeting\",\n \"officehours\"\n]",
"show_on_agenda": false "show_on_agenda": false
}, },
"model": "group.groupfeatures", "model": "group.groupfeatures",
@ -3182,6 +3198,7 @@
], ],
"req_subm_approval": false, "req_subm_approval": false,
"role_order": "[\n \"lead\",\n \"chair\",\n \"secr\"\n]", "role_order": "[\n \"lead\",\n \"chair\",\n \"secr\"\n]",
"session_purposes": "[\n \"regular\",\n \"tutorial\"\n]",
"show_on_agenda": false "show_on_agenda": false
}, },
"model": "group.groupfeatures", "model": "group.groupfeatures",
@ -3220,6 +3237,7 @@
], ],
"req_subm_approval": true, "req_subm_approval": true,
"role_order": "[\n \"chair\",\n \"secr\"\n]", "role_order": "[\n \"chair\",\n \"secr\"\n]",
"session_purposes": "[\n \"regular\"\n]",
"show_on_agenda": true "show_on_agenda": true
}, },
"model": "group.groupfeatures", "model": "group.groupfeatures",
@ -3258,6 +3276,7 @@
], ],
"req_subm_approval": true, "req_subm_approval": true,
"role_order": "[\n \"chair\",\n \"secr\"\n]", "role_order": "[\n \"chair\",\n \"secr\"\n]",
"session_purposes": "[\n \"open_meeting\",\n \"social\"\n]",
"show_on_agenda": false "show_on_agenda": false
}, },
"model": "group.groupfeatures", "model": "group.groupfeatures",
@ -3294,6 +3313,7 @@
"parent_types": [], "parent_types": [],
"req_subm_approval": true, "req_subm_approval": true,
"role_order": "[\n \"chair\",\n \"secr\"\n]", "role_order": "[\n \"chair\",\n \"secr\"\n]",
"session_purposes": "[\n \"officehours\"\n]",
"show_on_agenda": false "show_on_agenda": false
}, },
"model": "group.groupfeatures", "model": "group.groupfeatures",
@ -3332,6 +3352,7 @@
], ],
"req_subm_approval": true, "req_subm_approval": true,
"role_order": "[\n \"chair\",\n \"delegate\",\n \"secr\"\n]", "role_order": "[\n \"chair\",\n \"delegate\",\n \"secr\"\n]",
"session_purposes": "[\n \"regular\"\n]",
"show_on_agenda": true "show_on_agenda": true
}, },
"model": "group.groupfeatures", "model": "group.groupfeatures",
@ -3371,6 +3392,7 @@
], ],
"req_subm_approval": true, "req_subm_approval": true,
"role_order": "[\n \"liaiman\"\n]", "role_order": "[\n \"liaiman\"\n]",
"session_purposes": "[]",
"show_on_agenda": false "show_on_agenda": false
}, },
"model": "group.groupfeatures", "model": "group.groupfeatures",
@ -3409,6 +3431,7 @@
], ],
"req_subm_approval": false, "req_subm_approval": false,
"role_order": "[\n \"chair\",\n \"member\",\n \"matman\"\n]", "role_order": "[\n \"chair\",\n \"member\",\n \"matman\"\n]",
"session_purposes": "[\n \"coding\",\n \"presentation\",\n \"social\",\n \"tutorial\"\n]",
"show_on_agenda": false "show_on_agenda": false
}, },
"model": "group.groupfeatures", "model": "group.groupfeatures",
@ -3447,6 +3470,7 @@
], ],
"req_subm_approval": true, "req_subm_approval": true,
"role_order": "[\n \"chair\",\n \"secr\",\n \"delegate\"\n]", "role_order": "[\n \"chair\",\n \"secr\",\n \"delegate\"\n]",
"session_purposes": "[\n \"regular\"\n]",
"show_on_agenda": true "show_on_agenda": true
}, },
"model": "group.groupfeatures", "model": "group.groupfeatures",
@ -12738,6 +12762,138 @@
"model": "name.roomresourcename", "model": "name.roomresourcename",
"pk": "webex" "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": { "fields": {
"desc": "", "desc": "",
@ -13163,7 +13319,6 @@
"desc": "", "desc": "",
"name": "Break", "name": "Break",
"order": 0, "order": 0,
"private": false,
"used": true "used": true
}, },
"model": "name.timeslottypename", "model": "name.timeslottypename",
@ -13174,7 +13329,6 @@
"desc": "Leadership Meetings", "desc": "Leadership Meetings",
"name": "Leadership", "name": "Leadership",
"order": 0, "order": 0,
"private": true,
"used": true "used": true
}, },
"model": "name.timeslottypename", "model": "name.timeslottypename",
@ -13185,29 +13339,16 @@
"desc": "Other Meetings Not Published on Agenda", "desc": "Other Meetings Not Published on Agenda",
"name": "Off Agenda", "name": "Off Agenda",
"order": 0, "order": 0,
"private": true, "used": false
"used": true
}, },
"model": "name.timeslottypename", "model": "name.timeslottypename",
"pk": "offagenda" "pk": "offagenda"
}, },
{
"fields": {
"desc": "Office hours timeslot",
"name": "Office Hours",
"order": 0,
"private": false,
"used": true
},
"model": "name.timeslottypename",
"pk": "officehours"
},
{ {
"fields": { "fields": {
"desc": "", "desc": "",
"name": "Other", "name": "Other",
"order": 0, "order": 0,
"private": false,
"used": true "used": true
}, },
"model": "name.timeslottypename", "model": "name.timeslottypename",
@ -13218,7 +13359,6 @@
"desc": "", "desc": "",
"name": "Plenary", "name": "Plenary",
"order": 0, "order": 0,
"private": false,
"used": true "used": true
}, },
"model": "name.timeslottypename", "model": "name.timeslottypename",
@ -13229,7 +13369,6 @@
"desc": "", "desc": "",
"name": "Registration", "name": "Registration",
"order": 0, "order": 0,
"private": false,
"used": true "used": true
}, },
"model": "name.timeslottypename", "model": "name.timeslottypename",
@ -13240,7 +13379,6 @@
"desc": "", "desc": "",
"name": "Regular", "name": "Regular",
"order": 0, "order": 0,
"private": false,
"used": true "used": true
}, },
"model": "name.timeslottypename", "model": "name.timeslottypename",
@ -13251,8 +13389,7 @@
"desc": "A room has been reserved for use by another body the timeslot indicated", "desc": "A room has been reserved for use by another body the timeslot indicated",
"name": "Room Reserved", "name": "Room Reserved",
"order": 0, "order": 0,
"private": false, "used": false
"used": true
}, },
"model": "name.timeslottypename", "model": "name.timeslottypename",
"pk": "reserved" "pk": "reserved"
@ -13262,7 +13399,6 @@
"desc": "A room was not booked for the timeslot indicated", "desc": "A room was not booked for the timeslot indicated",
"name": "Room Unavailable", "name": "Room Unavailable",
"order": 0, "order": 0,
"private": false,
"used": true "used": true
}, },
"model": "name.timeslottypename", "model": "name.timeslottypename",

View file

@ -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),
]

View file

@ -10,7 +10,7 @@ import jsonfield
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('name', '0034_add_officehours_timeslottypename'), ('name', '0033_populate_agendafiltertypename'),
] ]
operations = [ operations = [

View file

@ -9,17 +9,18 @@ def forward(apps, schema_editor):
SessionPurposeName = apps.get_model('name', 'SessionPurposeName') SessionPurposeName = apps.get_model('name', 'SessionPurposeName')
TimeSlotTypeName = apps.get_model('name', 'TimeSlotTypeName') TimeSlotTypeName = apps.get_model('name', 'TimeSlotTypeName')
for order, (slug, name, desc, tstypes, on_agenda) in enumerate(( for order, (slug, name, desc, tstypes, on_agenda, used) in enumerate((
('regular', 'Regular', 'Regular group session', ['regular'], True), ('none', 'None', 'Value not set (do not use for new sessions)', [], True, False),
('tutorial', 'Tutorial', 'Tutorial or training session', ['other'], True), ('regular', 'Regular', 'Regular group session', ['regular'], True, True),
('office_hours', 'Office hours', 'Office hours session', ['other'], True), ('tutorial', 'Tutorial', 'Tutorial or training session', ['other'], True, True),
('coding', 'Coding', 'Coding session', ['other'], True), ('officehours', 'Office hours', 'Office hours session', ['other'], True, True),
('admin', 'Administrative', 'Meeting administration', ['other', 'reg'], True), ('coding', 'Coding', 'Coding session', ['other'], True, True),
('social', 'Social', 'Social event or activity', ['break', 'other'], True), ('admin', 'Administrative', 'Meeting administration', ['other', 'reg'], True, True),
('plenary', 'Plenary', 'Plenary session', ['plenary'], True), ('social', 'Social', 'Social event or activity', ['break', 'other'], True, True),
('presentation', 'Presentation', 'Presentation session', ['other', 'regular'], True), ('plenary', 'Plenary', 'Plenary session', ['plenary'], True, True),
('open_meeting', 'Open meeting', 'Open meeting', ['other'], True), ('presentation', 'Presentation', 'Presentation session', ['other', 'regular'], True, True),
('closed_meeting', 'Closed meeting', 'Closed meeting', ['other', 'regular'], False), ('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 # verify that we're not about to use an invalid type
for ts_type in tstypes: for ts_type in tstypes:
@ -29,7 +30,7 @@ def forward(apps, schema_editor):
slug=slug, slug=slug,
name=name, name=name,
desc=desc, desc=desc,
used=True, used=used,
order=order, order=order,
timeslot_types = tstypes, timeslot_types = tstypes,
on_agenda=on_agenda, on_agenda=on_agenda,
@ -44,7 +45,7 @@ def reverse(apps, schema_editor):
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('name', '0035_sessionpurposename'), ('name', '0034_sessionpurposename'),
] ]
operations = [ operations = [

View file

@ -22,7 +22,7 @@ def reverse(apps, schema_editor):
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('name', '0036_populate_sessionpurposename'), ('name', '0035_populate_sessionpurposename'),
('meeting', '0050_populate_session_on_agenda'), ('meeting', '0050_populate_session_on_agenda'),
] ]

View file

@ -6,7 +6,7 @@ from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('name', '0037_depopulate_timeslottypename_private'), ('name', '0036_depopulate_timeslottypename_private'),
] ]
operations = [ operations = [

View file

@ -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),
]

View file

@ -18,7 +18,7 @@ from ietf.name.models import ( AgendaFilterTypeName, AgendaTypeName, BallotPosit
ReviewAssignmentStateName, ReviewRequestStateName, ReviewResultName, ReviewTypeName, ReviewAssignmentStateName, ReviewRequestStateName, ReviewResultName, ReviewTypeName,
RoleName, RoomResourceName, SessionStatusName, StdLevelName, StreamName, TimeSlotTypeName, RoleName, RoomResourceName, SessionStatusName, StdLevelName, StreamName, TimeSlotTypeName,
TopicAudienceName, ReviewerQueuePolicyName, TimerangeName, ExtResourceTypeName, ExtResourceName, TopicAudienceName, ReviewerQueuePolicyName, TimerangeName, ExtResourceTypeName, ExtResourceName,
SlideSubmissionStatusName, ProceedingsMaterialTypeName) SlideSubmissionStatusName, ProceedingsMaterialTypeName, SessionPurposeName )
class TimeSlotTypeNameResource(ModelResource): class TimeSlotTypeNameResource(ModelResource):
class Meta: class Meta:
@ -701,3 +701,22 @@ class AgendaFilterTypeNameResource(ModelResource):
"order": ALL, "order": ALL,
} }
api.name.register(AgendaFilterTypeNameResource()) 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())

View file

@ -336,7 +336,8 @@ class SecrMeetingTestCase(TestCase):
'duration':'02:00', 'duration':'02:00',
'name':'Testing', 'name':'Testing',
'short':'test', 'short':'test',
'type':'reg', 'purpose_0': 'admin', # purpose
'purpose_1':'reg', # type
'group':group.pk, 'group':group.pk,
'location': room.pk, 'location': room.pk,
'remote_instructions': 'http://webex.com/foobar', 'remote_instructions': 'http://webex.com/foobar',
@ -382,7 +383,8 @@ class SecrMeetingTestCase(TestCase):
'time':new_time.strftime('%H:%M'), 'time':new_time.strftime('%H:%M'),
'duration':'01:00', 'duration':'01:00',
'day':'2', 'day':'2',
'type':'other', 'purpose_0': 'coding', # purpose
'purpose_1': 'other', # type
'remote_instructions': 'http://webex.com/foobar', 'remote_instructions': 'http://webex.com/foobar',
}) })
self.assertRedirects(response, redirect_url) self.assertRedirects(response, redirect_url)

View file

@ -260,7 +260,7 @@ class SessionForm(forms.Form):
num_sessions_expected = int(data.get('num_session', '')) num_sessions_expected = int(data.get('num_session', ''))
except ValueError: except ValueError:
self.add_error('num_session', 'Invalid value for number of sessions') 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') 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 # if default (empty) option is selected, cleaned_data won't include num_session key

View file

@ -25,6 +25,8 @@ def display_duration(value):
""" """
Maps a session requested duration from select index to Maps a session requested duration from select index to
label.""" label."""
if value in (None, ''):
return 'unspecified'
value = int(value) value = int(value)
map = {0: 'None', map = {0: 'None',
1800: '30 Minutes', 1800: '30 Minutes',

View file

@ -4,6 +4,7 @@
import datetime import datetime
from django.test import override_settings
from django.urls import reverse from django.urls import reverse
import debug # pyflakes:ignore import debug # pyflakes:ignore
@ -76,6 +77,7 @@ class SessionRequestTestCase(TestCase):
self.assertRedirects(r,reverse('ietf.secr.sreq.views.main')) self.assertRedirects(r,reverse('ietf.secr.sreq.views.main'))
self.assertEqual(SchedulingEvent.objects.filter(session=session).order_by('-id')[0].status_id, 'deleted') 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): def test_edit(self):
meeting = MeetingFactory(type_id='ietf', date=datetime.date.today()) meeting = MeetingFactory(type_id='ietf', date=datetime.date.today())
mars = RoleFactory(name_id='chair', person__user__username='marschairman', group__acronym='mars').group 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") self.client.login(username="marschairman", password="marschairman+password")
r = self.client.get(url) r = self.client.get(url)
self.assertEqual(r.status_code, 200) 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', post_data = {'num_session':'2',
'length_session1':'3600', 'attendees': attendees,
'length_session2':'3600',
'attendees':'10',
'constraint_chair_conflict':iabprog.acronym, 'constraint_chair_conflict':iabprog.acronym,
'comments':'need lights',
'session_time_relation': 'subsequent-days', 'session_time_relation': 'subsequent-days',
'adjacent_with_wg': group2.acronym, 'adjacent_with_wg': group2.acronym,
'joint_with_groups': group3.acronym + ' ' + group4.acronym, 'joint_with_groups': group3.acronym + ' ' + group4.acronym,
'joint_for_session': '2', 'joint_for_session': '2',
'timeranges': ['thursday-afternoon-early', 'thursday-afternoon-late'], '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'} 'submit': 'Continue'}
r = self.client.post(url, post_data, HTTP_HOST='example.com') r = self.client.post(url, post_data, HTTP_HOST='example.com')
redirect_url = reverse('ietf.secr.sreq.views.view', kwargs={'acronym': 'mars'}) redirect_url = reverse('ietf.secr.sreq.views.view', kwargs={'acronym': 'mars'})
@ -133,11 +161,37 @@ class SessionRequestTestCase(TestCase):
post_data = {'num_session':'2', post_data = {'num_session':'2',
'length_session1':'3600', 'length_session1':'3600',
'length_session2':'3600', 'length_session2':'3600',
'attendees':'10', 'attendees':attendees,
'constraint_chair_conflict':'', 'constraint_chair_conflict':'',
'comments':'need lights', 'comments':'need lights',
'joint_with_groups': group2.acronym, 'joint_with_groups': group2.acronym,
'joint_for_session': '1', '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'} 'submit': 'Continue'}
r = self.client.post(url, post_data, HTTP_HOST='example.com') r = self.client.post(url, post_data, HTTP_HOST='example.com')
self.assertRedirects(r, redirect_url) self.assertRedirects(r, redirect_url)
@ -160,7 +214,7 @@ class SessionRequestTestCase(TestCase):
"""Inactive conflicts should be displayed and removable""" """Inactive conflicts should be displayed and removable"""
meeting = MeetingFactory(type_id='ietf', date=datetime.date.today(), group_conflicts=['chair_conflict']) 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 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() other_group = GroupFactory()
Constraint.objects.create( Constraint.objects.create(
meeting=meeting, meeting=meeting,
@ -184,16 +238,31 @@ class SessionRequestTestCase(TestCase):
# check that the target is displayed correctly in the UI # check that the target is displayed correctly in the UI
self.assertIn(other_group.acronym, delete_checkbox.find('../input[@type="text"]').value) self.assertIn(other_group.acronym, delete_checkbox.find('../input[@type="text"]').value)
attendees = '10'
post_data = { post_data = {
'num_session': '1', 'num_session': '1',
'length_session1': '3600', 'attendees': attendees,
'attendees': '10',
'constraint_chair_conflict':'', 'constraint_chair_conflict':'',
'comments':'', 'comments':'',
'joint_with_groups': '', 'joint_with_groups': '',
'joint_for_session': '', 'joint_for_session': '',
'submit': 'Save',
'delete_conflict': 'on', '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') r = self.client.post(url, post_data, HTTP_HOST='example.com')
redirect_url = reverse('ietf.secr.sreq.views.view', kwargs={'acronym': 'mars'}) 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}) url = reverse('ietf.secr.sreq.views.new',kwargs={'acronym':group.acronym})
confirm_url = reverse('ietf.secr.sreq.views.confirm',kwargs={'acronym':group.acronym}) confirm_url = reverse('ietf.secr.sreq.views.confirm',kwargs={'acronym':group.acronym})
main_url = reverse('ietf.secr.sreq.views.main') main_url = reverse('ietf.secr.sreq.views.main')
attendees = '10'
comments = 'need projector'
post_data = {'num_session':'1', post_data = {'num_session':'1',
'length_session1':'3600', 'attendees':attendees,
'attendees':'10',
'constraint_chair_conflict':'', 'constraint_chair_conflict':'',
'comments':'need projector', 'comments':comments,
'adjacent_with_wg': group2.acronym, 'adjacent_with_wg': group2.acronym,
'timeranges': ['thursday-afternoon-early', 'thursday-afternoon-late'], 'timeranges': ['thursday-afternoon-early', 'thursday-afternoon-late'],
'joint_with_groups': group3.acronym + ' ' + group4.acronym, 'joint_with_groups': group3.acronym + ' ' + group4.acronym,
'joint_for_session': '1', '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'} 'submit': 'Continue'}
self.client.login(username="secretary", password="secretary+password") self.client.login(username="secretary", password="secretary+password")
r = self.client.post(url,post_data) r = self.client.post(url,post_data)
@ -313,7 +398,7 @@ class SubmitRequestCase(TestCase):
self.assertRedirects(r, main_url) self.assertRedirects(r, main_url)
session_count_after = Session.objects.filter(meeting=meeting, group=group, type='regular').count() session_count_after = Session.objects.filter(meeting=meeting, group=group, type='regular').count()
self.assertEqual(session_count_after, session_count_before + 1) self.assertEqual(session_count_after, session_count_before + 1)
# Verify database content # Verify database content
session = Session.objects.get(meeting=meeting, group=group) session = Session.objects.get(meeting=meeting, group=group)
self.assertEqual(session.constraints().get(name='wg_adjacent').target.acronym, group2.acronym) 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 area = RoleFactory(name_id='ad', person=ad, group__type_id='area').group
group = GroupFactory(parent=area) group = GroupFactory(parent=area)
url = reverse('ietf.secr.sreq.views.new',kwargs={'acronym':group.acronym}) url = reverse('ietf.secr.sreq.views.new',kwargs={'acronym':group.acronym})
post_data = {'num_session':'2', attendees = '10'
'length_session1':'3600', comments = 'need projector'
'attendees':'10', post_data = {
'constraint_chair_conflict':'', 'num_session':'2',
'comments':'need projector'} '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") self.client.login(username="secretary", password="secretary+password")
r = self.client.post(url,post_data) r = self.client.post(url,post_data)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
q = PyQuery(r.content) q = PyQuery(r.content)
self.assertEqual(len(q('#session-request-form')),1) 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): def test_submit_request_check_constraints(self):
m1 = MeetingFactory(type_id='ietf', date=datetime.date.today() - datetime.timedelta(days=100)) m1 = MeetingFactory(type_id='ietf', date=datetime.date.today() - datetime.timedelta(days=100))
@ -363,7 +466,7 @@ class SubmitRequestCase(TestCase):
target=inactive_group, target=inactive_group,
name_id='chair_conflict', name_id='chair_conflict',
) )
SessionFactory(group=group, meeting=m1) session = SessionFactory(group=group, meeting=m1)
self.client.login(username="secretary", password="secretary+password") self.client.login(username="secretary", password="secretary+password")
@ -375,11 +478,27 @@ class SubmitRequestCase(TestCase):
self.assertIn(still_active_group.acronym, conflict1) self.assertIn(still_active_group.acronym, conflict1)
self.assertNotIn(inactive_group.acronym, conflict1) self.assertNotIn(inactive_group.acronym, conflict1)
attendees = '10'
comments = 'need projector'
post_data = {'num_session':'1', post_data = {'num_session':'1',
'length_session1':'3600', 'attendees':attendees,
'attendees':'10',
'constraint_chair_conflict': group.acronym, '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'} 'submit': 'Continue'}
r = self.client.post(url,post_data) r = self.client.post(url,post_data)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
@ -405,10 +524,9 @@ class SubmitRequestCase(TestCase):
url = reverse('ietf.secr.sreq.views.new',kwargs={'acronym':group.acronym}) url = reverse('ietf.secr.sreq.views.new',kwargs={'acronym':group.acronym})
confirm_url = reverse('ietf.secr.sreq.views.confirm',kwargs={'acronym':group.acronym}) confirm_url = reverse('ietf.secr.sreq.views.confirm',kwargs={'acronym':group.acronym})
len_before = len(outbox) len_before = len(outbox)
attendees = '10'
post_data = {'num_session':'2', post_data = {'num_session':'2',
'length_session1':'3600', 'attendees':attendees,
'length_session2':'3600',
'attendees':'10',
'bethere':str(ad.pk), 'bethere':str(ad.pk),
'constraint_chair_conflict':group4.acronym, 'constraint_chair_conflict':group4.acronym,
'comments':'', 'comments':'',
@ -418,6 +536,32 @@ class SubmitRequestCase(TestCase):
'joint_with_groups': group3.acronym, 'joint_with_groups': group3.acronym,
'joint_for_session': '2', 'joint_for_session': '2',
'timeranges': ['thursday-afternoon-early', 'thursday-afternoon-late'], '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'} 'submit': 'Continue'}
self.client.login(username="ameschairman", password="ameschairman+password") self.client.login(username="ameschairman", password="ameschairman+password")
# submit # submit
@ -539,23 +683,59 @@ class SessionFormTest(TestCase):
self.group5 = GroupFactory() self.group5 = GroupFactory()
self.group6 = GroupFactory() self.group6 = GroupFactory()
attendees = '10'
comments = 'need lights'
self.valid_form_data = { self.valid_form_data = {
'num_session': '2', 'num_session': '2',
'third_session': 'true', 'third_session': 'true',
'length_session1': '3600', 'attendees': attendees,
'length_session2': '3600',
'length_session3': '3600',
'attendees': '10',
'constraint_chair_conflict': self.group2.acronym, 'constraint_chair_conflict': self.group2.acronym,
'constraint_tech_overlap': self.group3.acronym, 'constraint_tech_overlap': self.group3.acronym,
'constraint_key_participant': self.group4.acronym, 'constraint_key_participant': self.group4.acronym,
'comments': 'need lights', 'comments': comments,
'session_time_relation': 'subsequent-days', 'session_time_relation': 'subsequent-days',
'adjacent_with_wg': self.group5.acronym, 'adjacent_with_wg': self.group5.acronym,
'joint_with_groups': self.group6.acronym, 'joint_with_groups': self.group6.acronym,
'joint_for_session': '3', 'joint_for_session': '3',
'timeranges': ['thursday-afternoon-early', 'thursday-afternoon-late'], '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): def test_valid(self):
@ -637,58 +817,65 @@ class SessionFormTest(TestCase):
def test_invalid_joint_for_session(self): def test_invalid_joint_for_session(self):
form = self._invalid_test_helper({ form = self._invalid_test_helper({
'third_session': '', 'third_session': '',
'session_set-TOTAL_FORMS': '2',
'num_session': 2, 'num_session': 2,
'joint_for_session': '3', 'joint_for_session': '3',
}) })
self.assertEqual(form.errors, self.assertEqual(form.errors,
{ {
'joint_for_session': ['The third session can not be the joint session, ' 'joint_for_session': [
'because you have not requested a third session.'] 'Session 3 can not be the joint session, the session has not been requested.']
}) })
form = self._invalid_test_helper({ form = self._invalid_test_helper({
'third_session': '', 'third_session': '',
'length_session2': '', 'session_set-TOTAL_FORMS': '1',
'num_session': 1, 'num_session': 1,
'joint_for_session': '2', 'joint_for_session': '2',
'session_time_relation': '', 'session_time_relation': '',
}) })
self.assertEqual(form.errors, self.assertEqual(form.errors,
{ {
'joint_for_session': ['The second session can not be the joint session, ' 'joint_for_session': [
'because you have not requested a second session.'] 'Session 2 can not be the joint session, the session has not been requested.']
}) })
def test_invalid_missing_session_length(self): def test_invalid_missing_session_length(self):
form = self._invalid_test_helper({ form = self._invalid_test_helper({
'length_session2': '', 'session_set-TOTAL_FORMS': '2',
'session_set-1-requested_duration': '',
'third_session': 'false', 'third_session': 'false',
'joint_for_session': None, 'joint_for_session': None,
}) })
self.assertEqual(form.errors, self.assertEqual(form.session_forms.errors,
{ [
'length_session2': ['You must enter a length for all sessions'], {},
}) {'requested_duration': ['This field is required.']},
])
form = self._invalid_test_helper({ form = self._invalid_test_helper({
'length_session2': '', 'session_set-1-requested_duration': '',
'length_session3': '', 'session_set-2-requested_duration': '',
'joint_for_session': None, 'joint_for_session': None,
}) })
self.assertEqual(form.errors, self.assertEqual(
{ form.session_forms.errors,
'length_session2': ['You must enter a length for all sessions'], [
'length_session3': ['You must enter a length for all sessions'], {},
}) {'requested_duration': ['This field is required.']},
{'requested_duration': ['This field is required.']},
])
form = self._invalid_test_helper({ form = self._invalid_test_helper({
'length_session3': '', 'session_set-2-requested_duration': '',
'joint_for_session': None, 'joint_for_session': None,
}) })
self.assertEqual(form.errors, self.assertEqual(form.session_forms.errors,
{ [
'length_session3': ['You must enter a length for all sessions'], {},
}) {},
{'requested_duration': ['This field is required.']},
])
def _invalid_test_helper(self, new_form_data): def _invalid_test_helper(self, new_form_data):
form_data = dict(self.valid_form_data, **new_form_data) form_data = dict(self.valid_form_data, **new_form_data)

View file

@ -336,7 +336,7 @@ def confirm(request, acronym):
jfs = form.data.get('joint_for_session', '-1') jfs = form.data.get('joint_for_session', '-1')
if not jfs: # jfs might be '' if not jfs: # jfs might be ''
jfs = '-1' 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() groups_split = form.cleaned_data.get('joint_with_groups').replace(',',' ').split()
joint = Group.objects.filter(acronym__in=groups_split) joint = Group.objects.filter(acronym__in=groups_split)
new_session.joint_with_groups.set(joint) new_session.joint_with_groups.set(joint)
@ -645,7 +645,7 @@ def new(request, acronym):
raise Http404(f'Cannot request sessions for group "{acronym}"') raise Http404(f'Cannot request sessions for group "{acronym}"')
meeting = get_meeting(days=14) meeting = get_meeting(days=14)
session_conflicts = dict(inbound=inbound_session_conflicts_as_string(group, meeting)) 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() FormClass = get_session_form_class()
# check if app is locked # check if app is locked
@ -723,6 +723,7 @@ def no_session(request, acronym):
meeting=meeting, meeting=meeting,
requested_duration=datetime.timedelta(0), requested_duration=datetime.timedelta(0),
type_id='regular', type_id='regular',
purpose_id='regular',
) )
SchedulingEvent.objects.create( SchedulingEvent.objects.create(
session=session, session=session,
@ -845,7 +846,8 @@ def view(request, acronym, num = None):
return render(request, 'sreq/view.html', { return render(request, 'sreq/view.html', {
'is_locked': is_locked, 'is_locked': is_locked,
'is_virtual': meeting.number in settings.SECR_VIRTUAL_MEETINGS, 'is_virtual': meeting.number in settings.SECR_VIRTUAL_MEETINGS,
'session': session, 'session': session, # legacy processed data
'sessions': sessions, # actual session instances
'activities': activities, 'activities': activities,
'meeting': meeting, 'meeting': meeting,
'group': group, 'group': group,

View file

@ -6,7 +6,7 @@ Session Requester: {{ login }}
{% if session.joint_with_groups %}{{ session.joint_for_session_display }} joint with: {{ session.joint_with_groups }}{% endif %} {% if session.joint_with_groups %}{{ session.joint_for_session_display }} joint with: {{ session.joint_with_groups }}{% endif %}
Number of Sessions: {{ session.num_session }} 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 }} Number of Attendees: {{ session.attendees }}
Conflicts to Avoid: Conflicts to Avoid:
{% for line in session.outbound_conflicts %} {{line}} {% for line in session.outbound_conflicts %} {{line}}

View file

@ -3,25 +3,12 @@
<col width="200"> <col width="200">
<tr class="row1"><td>Working Group Name:</td><td>{{ group.name }} ({{ group.acronym }})</td></tr> <tr class="row1"><td>Working Group Name:</td><td>{{ group.name }} ({{ group.acronym }})</td></tr>
<tr class="row2"><td>Area Name:</td><td>{{ group.parent }}</td></tr> <tr class="row2"><td>Area Name:</td><td>{{ group.parent }}</td></tr>
<tr class="row1"><td>Number of Sessions Requested:</td><td>{% if session.length_session3 %}3{% else %}{{ session.num_session }}{% endif %}</td></tr> <tr class="row1"><td>Number of Sessions Requested:</td><td>{% if session.third_session %}3{% else %}{{ session.num_session }}{% endif %}</td></tr>
{% for sess_form in form.session_forms %}{% if sess_form.cleaned_data and not sess_form.cleaned_data.DELETE %} {% if form %}
<tr class="row2"><td>Session {{ forloop.counter }}:</td><td> {% include 'includes/sessions_request_view_formset.html' with formset=form.session_forms group=group session=session only %}
<dl> {% else %}
<dt>Length</dt><dd>{{ sess_form.cleaned_data.requested_duration.total_seconds|display_duration }}</dd> {% include 'includes/sessions_request_view_session_set.html' with session_set=sessions group=group session=session only %}
{% if sess_form.cleaned_data.name %}<dt>Name</dt><dd>{{ sess_form.cleaned_data.name }}</dd>{% endif %} {% endif %}
{% if sess_form.cleaned_data.purpose.slug != 'regular' %}
<dt>Purpose</dt>
<dd>
{{ sess_form.cleaned_data.purpose }}
{% if sess_form.cleaned_data.purpose.timeslot_types|length > 1 %}({{ sess_form.cleaned_data.type }}){% endif %}
</dd>
{% endif %}
</dl>
</td></tr>
{% if group.features.acts_like_wg and forloop.counter == 2 and not is_virtual %}
<tr class="row2"><td>Time between sessions:</td><td>{% if session.session_time_relation_display %}{{ session.session_time_relation_display }}{% else %}No preference{% endif %}</td></tr>
{% endif %}
{% endif %}{% endfor %}
<tr class="row1"><td>Number of Attendees:</td><td>{{ session.attendees }}</td></tr> <tr class="row1"><td>Number of Attendees:</td><td>{{ session.attendees }}</td></tr>
<tr class="row2"> <tr class="row2">
<td>Conflicts to Avoid:</td> <td>Conflicts to Avoid:</td>

View file

@ -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 %}
<tr class="row2">
<td>Session {{ forloop.counter }}:</td>
<td>
<dl>
<dt>Length</dt>
<dd>{{ sess_form.cleaned_data.requested_duration.total_seconds|display_duration }}</dd>
{% if sess_form.cleaned_data.name %}
<dt>Name</dt>
<dd>{{ sess_form.cleaned_data.name }}</dd>{% endif %}
{% if sess_form.cleaned_data.purpose.slug != 'regular' %}
<dt>Purpose</dt>
<dd>
{{ sess_form.cleaned_data.purpose }}
{% if sess_form.cleaned_data.purpose.timeslot_types|length > 1 %}({{ sess_form.cleaned_data.type }}
){% endif %}
</dd>
{% endif %}
</dl>
</td>
</tr>
{% if group.features.acts_like_wg and forloop.counter == 2 and not is_virtual %}
<tr class="row2">
<td>Time between sessions:</td>
<td>{% if session.session_time_relation_display %}{{ session.session_time_relation_display }}{% else %}No
preference{% endif %}</td>
</tr>
{% endif %}
{% endif %}{% endfor %}

View file

@ -0,0 +1,30 @@
{% load ams_filters %}{# keep this in sync with sessions_request_view_formset.html #}
{% for sess in session_set %}
<tr class="row2">
<td>Session {{ forloop.counter }}:</td>
<td>
<dl>
<dt>Length</dt>
<dd>{{ sess.requested_duration.total_seconds|display_duration }}</dd>
{% if sess.name %}
<dt>Name</dt>
<dd>{{ sess.name }}</dd>{% endif %}
{% if sess.purpose.slug != 'regular' %}
<dt>Purpose</dt>
<dd>
{{ sess.purpose }}
{% if sess.purpose.timeslot_types|length > 1 %}({{ sess.type }}
){% endif %}
</dd>
{% endif %}
</dl>
</td>
</tr>
{% if group.features.acts_like_wg and forloop.counter == 2 and not is_virtual %}
<tr class="row2">
<td>Time between sessions:</td>
<td>{% if session.session_time_relation_display %}{{ session.session_time_relation_display }}{% else %}No
preference{% endif %}</td>
</tr>
{% endif %}
{% endfor %}

View file

@ -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 FLOORPLAN_LAST_LEGACY_MEETING = 95 # last meeting to use FLOORPLAN_LEGACY_BASE_URL
MEETING_USES_CODIMD_DATE = datetime.date(2020,7,6) 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 # Maximum dimensions to accept at all
MEETINGHOST_LOGO_MAX_UPLOAD_WIDTH = 400 MEETINGHOST_LOGO_MAX_UPLOAD_WIDTH = 400

View file

@ -740,6 +740,18 @@ jQuery(document).ready(function () {
if (timeSlotTypeInputs.length > 0) { if (timeSlotTypeInputs.length > 0) {
timeSlotTypeInputs.on("change", updateTimeSlotTypeToggling); timeSlotTypeInputs.on("change", updateTimeSlotTypeToggling);
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 // Toggling session purposes
@ -771,10 +783,10 @@ jQuery(document).ready(function () {
} }
// toggling visible timeslots // toggling visible timeslots
let timeslotGroupInputs = content.find("#timeslot-group-toggles-modal .modal-body .individual-timeslots input"); let timeSlotGroupInputs = content.find("#timeslot-group-toggles-modal .modal-body .individual-timeslots input");
function updateTimeslotGroupToggling() { function updateTimeSlotGroupToggling() {
let checked = []; let checked = [];
timeslotGroupInputs.filter(":checked").each(function () { timeSlotGroupInputs.filter(":checked").each(function () {
checked.push("." + this.value); checked.push("." + this.value);
}); });
@ -786,8 +798,21 @@ jQuery(document).ready(function () {
}); });
} }
timeslotGroupInputs.on("click change", updateTimeslotGroupToggling); timeSlotGroupInputs.on("click change", updateTimeSlotGroupToggling);
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(); updatePastTimeslots();
setInterval(updatePastTimeslots, 10 * 1000 /* ms */); setInterval(updatePastTimeslots, 10 * 1000 /* ms */);

View file

@ -195,9 +195,9 @@
</span> </span>
{% if session_purposes|length > 1 %} {% if session_purposes|length > 1 %}
<button class="btn btn-default" data-toggle="modal" data-target="#session-toggles-modal"><input type="checkbox" checked="checked" disabled> Sessions</button> <button id="session-toggle-modal-open" class="btn btn-default" data-toggle="modal" data-target="#session-toggles-modal"><input type="checkbox" checked="checked" disabled> Sessions</button>
{% endif %} {% endif %}
<button class="btn btn-default" data-toggle="modal" data-target="#timeslot-group-toggles-modal"><input type="checkbox" checked="checked" disabled> Timeslots</button> <button id="timeslot-toggle-modal-open" class="btn btn-default" data-toggle="modal" data-target="#timeslot-group-toggles-modal"><input type="checkbox" checked="checked" disabled> Timeslots</button>
</div> </div>
</div> </div>
@ -216,7 +216,12 @@
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="timeslot-group-buttons">
<button type="button" class="btn btn-default select-all">Select all times</button>
<button type="button" class="btn btn-default clear-all">Clear times</button>
</div>
<div class="individual-timeslots"> <div class="individual-timeslots">
{% for day, t_groups in timeslot_groups %} {% for day, t_groups in timeslot_groups %}
<div> <div>
<div><strong>{{ day|date:"M. d" }}</strong></div> <div><strong>{{ day|date:"M. d" }}</strong></div>
@ -231,6 +236,8 @@
{% for type in timeslot_types %} {% for type in timeslot_types %}
<label class="timeslot-type-{{ type.slug }}"><input type="checkbox" checked value="{{ type.slug }}"> {{ type }}</label> <label class="timeslot-type-{{ type.slug }}"><input type="checkbox" checked value="{{ type.slug }}"> {{ type }}</label>
{% endfor %} {% endfor %}
<button type="button" class="btn btn-default select-all">Select all types</button>
<button type="button" class="btn btn-default clear-all">Clear types</button>
</div> </div>
</div> </div>
@ -256,7 +263,7 @@
<div class="session-purpose-toggles"> <div class="session-purpose-toggles">
{% for purpose in session_purposes %} {% for purpose in session_purposes %}
<div> <div>
<label class="purpose-{{ purpose.slug }}"><input type="checkbox" checked value="{{ purpose.slug }}"> {{ purpose }}</label> <label class="purpose-{{ purpose.slug }}"><input type="checkbox" checked value="{% firstof purpose.slug 'none' %}"> {{ purpose }}</label>
</div> </div>
{% endfor %} {% endfor %}
<button type="button" class="btn btn-default select-all">Select all</button> <button type="button" class="btn btn-default select-all">Select all</button>

View file

@ -1,5 +1,5 @@
<div id="session{{ session.pk }}" <div id="session{{ session.pk }}"
class="session {% if not session.group.parent.scheduling_color %}untoggleable-by-parent{% endif %} {% if session.parent_acronym %}parent-{{ session.parent_acronym }}{% endif %} purpose-{% firstof session.purpose.slug 'regular' %} {% if session.readonly %}readonly{% endif %} {% if not session.on_agenda %}off-agenda{% endif %}" class="session {% if not session.group.parent.scheduling_color %}untoggleable-by-parent{% endif %} {% if session.parent_acronym %}parent-{{ session.parent_acronym }}{% endif %} purpose-{{ session.purpose.slug }} {% if session.readonly %}readonly{% endif %} {% if not session.on_agenda %}off-agenda{% endif %}"
style="width:{{ session.layout_width }}em;" style="width:{{ session.layout_width }}em;"
data-duration="{{ session.requested_duration.total_seconds }}" {% if session.attendees != None %} data-duration="{{ session.requested_duration.total_seconds }}" {% if session.attendees != None %}
data-attendees="{{ session.attendees }}"{% endif %} data-attendees="{{ session.attendees }}"{% endif %}