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

View file

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

View file

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

View file

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

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.meeting.helpers import get_meeting, get_schedule, schedule_permissions, get_person_by_email, get_schedule_by_name
from ietf.meeting.models import TimeSlot, Session, Schedule, Room, Constraint, SchedTimeSessAssignment, ResourceAssociation
from ietf.meeting.views import edit_timeslots, edit_schedule
from ietf.meeting.views import edit_timeslots, edit_meeting_schedule
import debug # pyflakes:ignore
@ -286,7 +286,7 @@ def schedule_add(request, meeting):
if "HTTP_ACCEPT" in request.META and "application/json" in request.META['HTTP_ACCEPT']:
return redirect(schedule_infourl, meeting.number, newschedule.owner_email(), newschedule.name)
else:
return redirect(edit_schedule, meeting.number, newschedule.owner_email(), newschedule.name)
return redirect(edit_meeting_schedule, meeting.number, newschedule.owner_email(), newschedule.name)
@require_POST
def schedule_update(request, meeting, schedule):
@ -325,7 +325,7 @@ def schedule_update(request, meeting, schedule):
return HttpResponse(json.dumps(schedule.json_dict(request.build_absolute_uri('/'))),
content_type="application/json")
else:
return redirect(edit_schedule, meeting.number, schedule.owner_email(), schedule.name)
return redirect(edit_meeting_schedule, meeting.number, schedule.owner_email(), schedule.name)
@role_required('Secretariat')
def schedule_del(request, meeting, schedule):

View file

@ -12,7 +12,8 @@ from django.db.models import Q
from ietf.meeting.models import (Meeting, Session, SchedulingEvent, Schedule,
TimeSlot, SessionPresentation, FloorPlan, Room, SlideSubmission, Constraint,
MeetingHost, ProceedingsMaterial)
from ietf.name.models import ConstraintName, SessionStatusName, ProceedingsMaterialTypeName, TimerangeName
from ietf.name.models import (ConstraintName, SessionStatusName, ProceedingsMaterialTypeName,
TimerangeName, SessionPurposeName)
from ietf.doc.factories import ProceedingsMaterialDocFactory
from ietf.group.factories import GroupFactory
from ietf.person.factories import PersonFactory
@ -104,9 +105,11 @@ class SessionFactory(factory.django.DjangoModelFactory):
model = Session
meeting = factory.SubFactory(MeetingFactory)
purpose_id = 'regular'
type_id = 'regular'
group = factory.SubFactory(GroupFactory)
requested_duration = datetime.timedelta(hours=1)
on_agenda = factory.lazy_attribute(lambda obj: SessionPurposeName.objects.get(pk=obj.purpose_id).on_agenda)
@factory.post_generation
def status_id(obj, create, extracted, **kwargs):

View file

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

View file

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

View file

@ -15,7 +15,9 @@ def forward(apps, schema_editor):
),
timeslot__type__private=True,
)
Session.objects.filter(timeslotassignments__in=private_assignments).update(on_agenda=False)
for pa in private_assignments:
pa.session.on_agenda = False
pa.session.save()
# Also update any sessions to match their purpose's default setting (this intentionally
# overrides the timeslot settings above, but that is unlikely to matter because the
# purposes will roll out at the same time as the on_agenda field)

View file

@ -1162,7 +1162,7 @@ class Session(models.Model):
meeting = ForeignKey(Meeting)
name = models.CharField(blank=True, max_length=255, help_text="Name of session, in case the session has a purpose rather than just being a group meeting.")
short = models.CharField(blank=True, max_length=32, help_text="Short version of 'name' above, for use in filenames.")
purpose = ForeignKey(SessionPurposeName, null=True, help_text='Purpose of the session')
purpose = ForeignKey(SessionPurposeName, null=False, help_text='Purpose of the session')
type = ForeignKey(TimeSlotTypeName)
group = ForeignKey(Group) # The group type historically determined the session type. BOFs also need to be added as a group. Note that not all meeting requests have a natural group to associate with.
joint_with_groups = models.ManyToManyField(Group, related_name='sessions_joint_in',blank=True)

View file

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

View file

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

View file

@ -1,13 +1,14 @@
# Copyright The IETF Trust 2020, All Rights Reserved
# -*- coding: utf-8 -*-
import copy
from django.conf import settings
from django.test import override_settings
from ietf.group.factories import GroupFactory
from ietf.group.models import Group
from ietf.meeting.factories import SessionFactory, MeetingFactory, TimeSlotFactory
from ietf.meeting.helpers import AgendaFilterOrganizer, AgendaKeywordTagger
from ietf.meeting.models import SchedTimeSessAssignment
from ietf.meeting.test_data import make_meeting_test_data
from ietf.utils.test_utils import TestCase
@ -21,7 +22,6 @@ class AgendaKeywordTaggerTests(TestCase):
The historic param can be None, group, or parent, to specify whether to test
with no historic_group, a historic_group but no historic_parent, or both.
"""
session_types = ['regular', 'plenary']
# decide whether meeting should use legacy keywords (for office hours)
legacy_keywords = meeting_num <= 111
@ -44,71 +44,92 @@ class AgendaKeywordTaggerTests(TestCase):
expected_group = group
expected_area = group.parent
# create the ordinary sessions
for session_type in session_types:
sess = SessionFactory(group=group, meeting=meeting, type_id=session_type, add_to_schedule=False)
sess.timeslotassignments.create(
timeslot=TimeSlotFactory(meeting=meeting, type_id=session_type),
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),
# create sessions, etc
session_data = [
{
'description': 'regular wg session',
'session': SessionFactory(
group=group, meeting=meeting, add_to_schedule=False,
purpose_id='none' if legacy_keywords else 'regular',
type_id='regular',
),
'expected_keywords': {
expected_group.acronym,
expected_area.acronym,
# if legacy_keywords, next line repeats a previous entry to avoid adding anything to the set
expected_group.acronym if legacy_keywords else 'regular',
f'{expected_group.acronym}-sessa',
},
},
{
'description': 'plenary session',
'session': SessionFactory(
group=group, meeting=meeting, add_to_schedule=False,
name=f'{group.acronym} plenary',
purpose_id='none' if legacy_keywords else 'plenary',
type_id='plenary',
),
'expected_keywords': {
expected_group.acronym,
expected_area.acronym,
f'{expected_group.acronym}-sessb',
'plenary',
f'{group.acronym}-plenary',
},
},
{
'description': 'office hours session',
'session': SessionFactory(
group=group, meeting=meeting, add_to_schedule=False,
name=f'{group.acronym} office hours',
purpose_id='none' if legacy_keywords else 'officehours',
type_id='other',
),
'expected_keywords': {
expected_group.acronym,
expected_area.acronym,
f'{expected_group.acronym}-sessc',
'officehours',
f'{group.acronym}-officehours' if legacy_keywords else 'officehours',
# officehours in prev line is a repeated value - since this is a set, it will be ignored
f'{group.acronym}-office-hours',
},
}
]
for sd in session_data:
sd['session'].timeslotassignments.create(
timeslot=TimeSlotFactory(meeting=meeting, type=sd['session'].type),
schedule=meeting.schedule,
)
assignments = meeting.schedule.assignments.all()
orig_num_assignments = len(assignments)
# Set up historic groups if needed. We've already set the office hours group properly
# so skip that session. The expected_group will already have its historic_parent set
# if historic == 'parent'
# Set up historic groups if needed.
if historic:
for a in assignments:
if a.session != office_hours:
a.session.historic_group = expected_group
# Execute the method under test
AgendaKeywordTagger(assignments=assignments).apply()
# Assert expected results
self.assertEqual(len(assignments), orig_num_assignments, 'Should not change number of assignments')
for assignment in assignments:
expected_filter_keywords = {assignment.slot_type().slug, assignment.session.type.slug}
# check the assignment count - paranoid, but the method mutates its input so let's be careful
self.assertEqual(len(assignments), len(session_data), 'Should not change number of assignments')
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
])
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')
token = assignment.session.docname_token_only_for_multiple()
if token is not None:
expected_filter_keywords.update([expected_group.acronym + "-" + token])
self.assertCountEqual(
assignment.filter_keywords,
expected_filter_keywords,
'Assignment has incorrect filter keywords'
f'Assignment for "{sd["description"]}" has incorrect filter keywords'
)
@override_settings(MEETING_LEGACY_OFFICE_HOURS_END=111)
def test_tag_assignments_with_filter_keywords(self):
# use distinct meeting numbers > 111 for non-legacy keyword tests
self.do_test_tag_assignments_with_filter_keywords(112)
@ -119,6 +140,7 @@ class AgendaKeywordTaggerTests(TestCase):
self.do_test_tag_assignments_with_filter_keywords(117, bof=True, historic='parent')
@override_settings(MEETING_LEGACY_OFFICE_HOURS_END=111)
def test_tag_assignments_with_filter_keywords_legacy(self):
# use distinct meeting numbers <= 111 for legacy keyword tests
self.do_test_tag_assignments_with_filter_keywords(101)
@ -131,8 +153,19 @@ class AgendaKeywordTaggerTests(TestCase):
class AgendaFilterOrganizerTests(TestCase):
def test_get_filter_categories(self):
self.do_get_filter_categories_test(False)
def test_get_legacy_filter_categories(self):
self.do_get_filter_categories_test(True)
def do_get_filter_categories_test(self, legacy):
# set up
meeting = make_meeting_test_data()
if legacy:
meeting.session_set.all().update(purpose_id='none') # legacy meetings did not have purposes
else:
meeting.number = str(settings.MEETING_LEGACY_OFFICE_HOURS_END + 1)
meeting.save()
# create extra groups for testing
iab = Group.objects.get(acronym='iab')
@ -147,55 +180,97 @@ class AgendaFilterOrganizerTests(TestCase):
# office hours session
SessionFactory(
group=Group.objects.get(acronym='farfut'),
purpose_id='officehours' if not legacy else 'none',
type_id='other',
name='FARFUT office hours',
meeting=meeting
)
if legacy:
expected = [
[
# area category
{'label': 'FARFUT', 'keyword': 'farfut', 'is_bof': False,
{'label': 'FARFUT', 'keyword': 'farfut', 'is_bof': False, 'toggled_by': [],
'children': [
{'label': 'ames', 'keyword': 'ames', 'is_bof': False},
{'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,
{'label': 'IAB', 'keyword': 'iab', 'is_bof': False, 'toggled_by': [],
'children': [
{'label': iab_child.acronym, 'keyword': iab_child.acronym, 'is_bof': False},
{'label': iab_child.acronym, 'keyword': iab_child.acronym, 'is_bof': False, 'toggled_by': ['iab']},
]},
{'label': 'IRTF', 'keyword': 'irtf', 'is_bof': False,
{'label': 'IRTF', 'keyword': 'irtf', 'is_bof': False, 'toggled_by': [],
'children': [
{'label': irtf_child.acronym, 'keyword': irtf_child.acronym, 'is_bof': True},
{'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,
{'label': 'Office Hours', 'keyword': 'officehours', 'is_bof': False, 'toggled_by': [],
'children': [
{'label': 'FARFUT', 'keyword': 'farfut-officehours', 'is_bof': False}
{'label': 'FARFUT', 'keyword': 'farfut-officehours', 'is_bof': False, 'toggled_by': ['officehours', 'farfut']}
]},
{'label': None, 'keyword': None,'is_bof': False,
{'label': None, 'keyword': None,'is_bof': False, 'toggled_by': [],
'children': [
{'label': 'BoF', 'keyword': 'bof', 'is_bof': False},
{'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': []},
]},
],
]
else:
expected = [
[
# area category
{'label': 'FARFUT', 'keyword': 'farfut', 'is_bof': False, 'toggled_by': [],
'children': [
{'label': 'ames', 'keyword': 'ames', 'is_bof': False, 'toggled_by': ['farfut']},
{'label': 'mars', 'keyword': 'mars', 'is_bof': False, 'toggled_by': ['farfut']},
]},
],
[
# non-area category
{'label': 'IAB', 'keyword': 'iab', 'is_bof': False, 'toggled_by': [],
'children': [
{'label': iab_child.acronym, 'keyword': iab_child.acronym, 'is_bof': False, 'toggled_by': ['iab']},
]},
{'label': 'IRTF', 'keyword': 'irtf', 'is_bof': False, 'toggled_by': [],
'children': [
{'label': irtf_child.acronym, 'keyword': irtf_child.acronym, 'is_bof': True, 'toggled_by': ['bof', 'irtf']},
]},
],
[
# non-group category
{'label': 'Administrative', 'keyword': 'admin', 'is_bof': False, 'toggled_by': [],
'children': [
{'label': 'Registration', 'keyword': 'registration', 'is_bof': False, 'toggled_by': ['admin', 'secretariat']},
]},
{'label': 'Closed meeting', 'keyword': 'closed_meeting', 'is_bof': False, 'toggled_by': [],
'children': [
{'label': 'IESG Breakfast', 'keyword': 'iesg-breakfast', 'is_bof': False, 'toggled_by': ['closed_meeting', 'iesg']},
]},
{'label': 'Office hours', 'keyword': 'officehours', 'is_bof': False, 'toggled_by': [],
'children': [
{'label': 'FARFUT office hours', 'keyword': 'farfut-office-hours', 'is_bof': False, 'toggled_by': ['officehours', 'farfut']}
]},
{'label': 'Plenary', 'keyword': 'plenary', 'is_bof': False, 'toggled_by': [],
'children': [
{'label': 'IETF Plenary', 'keyword': 'ietf-plenary', 'is_bof': False, 'toggled_by': ['plenary', 'ietf']},
]},
{'label': 'Social', 'keyword': 'social', 'is_bof': False, 'toggled_by': [],
'children': [
{'label': 'Morning Break', 'keyword': 'morning-break', 'is_bof': False, 'toggled_by': ['social', 'secretariat']},
]},
{'label': None, 'keyword': None,'is_bof': False, 'toggled_by': [],
'children': [
{'label': 'BoF', 'keyword': 'bof', 'is_bof': False, 'toggled_by': []},
]},
],
]
# when using sessions instead of assignments, won't get timeslot-type based filters
expected_with_sessions = copy.deepcopy(expected)
expected_with_sessions[-1].pop(0) # pops 'office hours' column
# put all the above together for single-column tests
expected_single_category = [
sorted(sum(expected, []), key=lambda col: col['label'] or 'zzzzz')
]
expected_single_category_with_sessions = [
sorted(sum(expected_with_sessions, []), key=lambda col: col['label'] or 'zzzzz')
]
expected_single_category = [sum(expected, [])]
###
# test using sessions
@ -204,15 +279,17 @@ class AgendaFilterOrganizerTests(TestCase):
# default
filter_organizer = AgendaFilterOrganizer(sessions=sessions)
self.assertEqual(filter_organizer.get_filter_categories(), expected_with_sessions)
self.assertEqual(filter_organizer.get_filter_categories(), expected)
# single-column
filter_organizer = AgendaFilterOrganizer(sessions=sessions, single_category=True)
self.assertEqual(filter_organizer.get_filter_categories(), expected_single_category_with_sessions)
self.assertEqual(filter_organizer.get_filter_categories(), expected_single_category)
###
# test again using assignments
assignments = meeting.schedule.assignments.all()
assignments = SchedTimeSessAssignment.objects.filter(
schedule__in=(meeting.schedule, meeting.schedule.base)
)
AgendaKeywordTagger(assignments=assignments).apply()
# default

View file

@ -89,7 +89,13 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase):
s2.save()
SchedTimeSessAssignment.objects.filter(session=s1).delete()
s2b = Session.objects.create(meeting=meeting, group=s2.group, attendees=10, requested_duration=datetime.timedelta(minutes=60), type_id='regular')
s2b = SessionFactory(
meeting=meeting,
group=s2.group,
attendees=10,
requested_duration=datetime.timedelta(minutes=60),
add_to_schedule=False,
)
SchedulingEvent.objects.create(
session=s2b,
@ -110,34 +116,34 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase):
WebDriverWait(self.driver, 2).until(expected_conditions.presence_of_element_located((By.CSS_SELECTOR, '.edit-meeting-schedule')))
self.assertEqual(len(self.driver.find_elements_by_css_selector('.session')), 3)
self.assertEqual(len(self.driver.find_elements(By.CSS_SELECTOR, '.session.purpose-regular')), 3)
# select - show session info
s2_element = self.driver.find_element_by_css_selector('#session{}'.format(s2.pk))
s2b_element = self.driver.find_element_by_css_selector('#session{}'.format(s2b.pk))
s2_element = self.driver.find_element(By.CSS_SELECTOR, '#session{}'.format(s2.pk))
s2b_element = self.driver.find_element(By.CSS_SELECTOR, '#session{}'.format(s2b.pk))
self.assertNotIn('other-session-selected', s2b_element.get_attribute('class'))
s2_element.click()
# other session for group should be flagged for highlighting
s2b_element = self.driver.find_element_by_css_selector('#session{}'.format(s2b.pk))
s2b_element = self.driver.find_element(By.CSS_SELECTOR, '#session{}'.format(s2b.pk))
self.assertIn('other-session-selected', s2b_element.get_attribute('class'))
# other session for group should appear in the info panel
session_info_container = self.driver.find_element_by_css_selector('.session-info-container')
self.assertIn(s2.group.acronym, session_info_container.find_element_by_css_selector(".title").text)
self.assertEqual(session_info_container.find_element_by_css_selector(".other-session .time").text, "not yet scheduled")
session_info_container = self.driver.find_element(By.CSS_SELECTOR, '.session-info-container')
self.assertIn(s2.group.acronym, session_info_container.find_element(By.CSS_SELECTOR, ".title").text)
self.assertEqual(session_info_container.find_element(By.CSS_SELECTOR, ".other-session .time").text, "not yet scheduled")
# deselect
self.driver.find_element_by_css_selector('.scheduling-panel').click()
self.driver.find_element(By.CSS_SELECTOR, '.scheduling-panel').click()
self.assertEqual(session_info_container.find_elements_by_css_selector(".title"), [])
self.assertEqual(session_info_container.find_elements(By.CSS_SELECTOR, ".title"), [])
self.assertNotIn('other-session-selected', s2b_element.get_attribute('class'))
# unschedule
# we would like to do
#
# unassigned_sessions_element = self.driver.find_element_by_css_selector('.unassigned-sessions')
# unassigned_sessions_element = self.driver.find_element(By.CSS_SELECTOR, '.unassigned-sessions')
# ActionChains(self.driver).drag_and_drop(s2_element, unassigned_sessions_element).perform()
#
# but unfortunately, Selenium does not simulate drag and drop events, see
@ -158,20 +164,20 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase):
# sorting unassigned
sorted_pks = [s.pk for s in sorted([s1, s2, s2b], key=lambda s: (s.group.acronym, s.requested_duration, s.pk))]
self.driver.find_element_by_css_selector('[name=sort_unassigned] option[value=name]').click()
self.assertTrue(self.driver.find_element_by_css_selector('.unassigned-sessions .drop-target #session{} + #session{} + #session{}'.format(*sorted_pks)))
self.driver.find_element(By.CSS_SELECTOR, '[name=sort_unassigned] option[value=name]').click()
self.assertTrue(self.driver.find_element(By.CSS_SELECTOR, '.unassigned-sessions .drop-target #session{} + #session{} + #session{}'.format(*sorted_pks)))
sorted_pks = [s.pk for s in sorted([s1, s2, s2b], key=lambda s: (s.group.parent.acronym, s.group.acronym, s.requested_duration, s.pk))]
self.driver.find_element_by_css_selector('[name=sort_unassigned] option[value=parent]').click()
self.assertTrue(self.driver.find_element_by_css_selector('.unassigned-sessions .drop-target #session{} + #session{}'.format(*sorted_pks)))
self.driver.find_element(By.CSS_SELECTOR, '[name=sort_unassigned] option[value=parent]').click()
self.assertTrue(self.driver.find_element(By.CSS_SELECTOR, '.unassigned-sessions .drop-target #session{} + #session{}'.format(*sorted_pks)))
sorted_pks = [s.pk for s in sorted([s1, s2, s2b], key=lambda s: (s.requested_duration, s.group.parent.acronym, s.group.acronym, s.pk))]
self.driver.find_element_by_css_selector('[name=sort_unassigned] option[value=duration]').click()
self.assertTrue(self.driver.find_element_by_css_selector('.unassigned-sessions .drop-target #session{} + #session{}'.format(*sorted_pks)))
self.driver.find_element(By.CSS_SELECTOR, '[name=sort_unassigned] option[value=duration]').click()
self.assertTrue(self.driver.find_element(By.CSS_SELECTOR, '.unassigned-sessions .drop-target #session{} ~ #session{}'.format(*sorted_pks)))
sorted_pks = [s.pk for s in sorted([s1, s2, s2b], key=lambda s: (int(bool(s.comments)), s.group.parent.acronym, s.group.acronym, s.requested_duration, s.pk))]
self.driver.find_element_by_css_selector('[name=sort_unassigned] option[value=comments]').click()
self.assertTrue(self.driver.find_element_by_css_selector('.unassigned-sessions .drop-target #session{} + #session{}'.format(*sorted_pks)))
self.driver.find_element(By.CSS_SELECTOR, '[name=sort_unassigned] option[value=comments]').click()
self.assertTrue(self.driver.find_element(By.CSS_SELECTOR, '.unassigned-sessions .drop-target #session{} + #session{}'.format(*sorted_pks)))
# schedule
self.driver.execute_script("jQuery('#session{}').simulateDragDrop({{dropTarget: '#timeslot{} .drop-target'}});".format(s2.pk, slot1.pk))
@ -182,30 +188,30 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase):
self.assertEqual(assignment.timeslot, slot1)
# timeslot constraint hints when selected
s1_element = self.driver.find_element_by_css_selector('#session{}'.format(s1.pk))
s1_element = self.driver.find_element(By.CSS_SELECTOR, '#session{}'.format(s1.pk))
s1_element.click()
# violated due to constraints - both the timeslot and its timeslot label
self.assertTrue(self.driver.find_elements_by_css_selector('#timeslot{}.would-violate-hint'.format(slot1.pk)))
self.assertTrue(self.driver.find_elements(By.CSS_SELECTOR, '#timeslot{}.would-violate-hint'.format(slot1.pk)))
# Find the timeslot label for slot1 - it's the first timeslot in the first room group
slot1_roomgroup_elt = self.driver.find_element_by_css_selector(
slot1_roomgroup_elt = self.driver.find_element(By.CSS_SELECTOR,
'.day-flow .day:first-child .room-group:nth-child(2)' # count from 2 - first-child is the day label
)
self.assertTrue(
slot1_roomgroup_elt.find_elements_by_css_selector(
slot1_roomgroup_elt.find_elements(By.CSS_SELECTOR,
'.time-header > .time-label.would-violate-hint:first-child'
),
'Timeslot header label should show a would-violate hint for a constraint violation'
)
# violated due to missing capacity
self.assertTrue(self.driver.find_elements_by_css_selector('#timeslot{}.would-violate-hint'.format(slot3.pk)))
self.assertTrue(self.driver.find_elements(By.CSS_SELECTOR, '#timeslot{}.would-violate-hint'.format(slot3.pk)))
# Find the timeslot label for slot3 - it's the second timeslot in the second room group
slot3_roomgroup_elt = self.driver.find_element_by_css_selector(
slot3_roomgroup_elt = self.driver.find_element(By.CSS_SELECTOR,
'.day-flow .day:first-child .room-group:nth-child(3)' # count from 2 - first-child is the day label
)
self.assertFalse(
slot3_roomgroup_elt.find_elements_by_css_selector(
slot3_roomgroup_elt.find_elements(By.CSS_SELECTOR,
'.time-header > .time-label.would-violate-hint:nth-child(2)'
),
'Timeslot header label should not show a would-violate hint for room capacity violation'
@ -220,15 +226,15 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase):
self.assertEqual(assignment.timeslot, slot2)
# too many attendees warning
self.assertTrue(self.driver.find_elements_by_css_selector('#session{}.too-many-attendees'.format(s2.pk)))
self.assertTrue(self.driver.find_elements(By.CSS_SELECTOR, '#session{}.too-many-attendees'.format(s2.pk)))
# overfull timeslot
self.assertTrue(self.driver.find_elements_by_css_selector('#timeslot{}.overfull'.format(slot2.pk)))
self.assertTrue(self.driver.find_elements(By.CSS_SELECTOR, '#timeslot{}.overfull'.format(slot2.pk)))
# constraint hints
s1_element.click()
self.assertIn('would-violate-hint', s2_element.get_attribute('class'))
constraint_element = s2_element.find_element_by_css_selector(".constraints span[data-sessions=\"{}\"].would-violate-hint".format(s1.pk))
constraint_element = s2_element.find_element(By.CSS_SELECTOR, ".constraints span[data-sessions=\"{}\"].would-violate-hint".format(s1.pk))
self.assertTrue(constraint_element.is_displayed())
# current constraint violations
@ -236,12 +242,12 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase):
WebDriverWait(self.driver, 2).until(expected_conditions.presence_of_element_located((By.CSS_SELECTOR, '#timeslot{} #session{}'.format(slot1.pk, s1.pk))))
constraint_element = s2_element.find_element_by_css_selector(".constraints span[data-sessions=\"{}\"].violated-hint".format(s1.pk))
constraint_element = s2_element.find_element(By.CSS_SELECTOR, ".constraints span[data-sessions=\"{}\"].violated-hint".format(s1.pk))
self.assertTrue(constraint_element.is_displayed())
# hide sessions in area
self.assertTrue(s1_element.is_displayed())
self.driver.find_element_by_css_selector(".session-parent-toggles [value=\"{}\"]".format(s1.group.parent.acronym)).click()
self.driver.find_element(By.CSS_SELECTOR, ".session-parent-toggles [value=\"{}\"]".format(s1.group.parent.acronym)).click()
self.assertTrue(s1_element.is_displayed()) # should still be displayed
self.assertIn('hidden-parent', s1_element.get_attribute('class'),
'Session should be hidden when parent disabled')
@ -249,7 +255,7 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase):
self.assertNotIn('selected', s1_element.get_attribute('class'),
'Session should not be selectable when parent disabled')
self.driver.find_element_by_css_selector(".session-parent-toggles [value=\"{}\"]".format(s1.group.parent.acronym)).click()
self.driver.find_element(By.CSS_SELECTOR, ".session-parent-toggles [value=\"{}\"]".format(s1.group.parent.acronym)).click()
self.assertTrue(s1_element.is_displayed())
self.assertNotIn('hidden-parent', s1_element.get_attribute('class'),
'Session should not be hidden when parent enabled')
@ -258,32 +264,32 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase):
'Session should be selectable when parent enabled')
# hide timeslots
self.driver.find_element_by_css_selector(".timeslot-group-toggles button").click()
self.assertTrue(self.driver.find_element_by_css_selector("#timeslot-group-toggles-modal").is_displayed())
self.driver.find_element_by_css_selector("#timeslot-group-toggles-modal [value=\"{}\"]".format("ts-group-{}-{}".format(slot2.time.strftime("%Y%m%d-%H%M"), int(slot2.duration.total_seconds() / 60)))).click()
self.driver.find_element_by_css_selector("#timeslot-group-toggles-modal [data-dismiss=\"modal\"]").click()
self.assertTrue(not self.driver.find_element_by_css_selector("#timeslot-group-toggles-modal").is_displayed())
self.driver.find_element(By.CSS_SELECTOR, "#timeslot-toggle-modal-open").click()
self.assertTrue(self.driver.find_element(By.CSS_SELECTOR, "#timeslot-group-toggles-modal").is_displayed())
self.driver.find_element(By.CSS_SELECTOR, "#timeslot-group-toggles-modal [value=\"{}\"]".format("ts-group-{}-{}".format(slot2.time.strftime("%Y%m%d-%H%M"), int(slot2.duration.total_seconds() / 60)))).click()
self.driver.find_element(By.CSS_SELECTOR, "#timeslot-group-toggles-modal [data-dismiss=\"modal\"]").click()
self.assertTrue(not self.driver.find_element(By.CSS_SELECTOR, "#timeslot-group-toggles-modal").is_displayed())
# swap days
self.driver.find_element_by_css_selector(".day .swap-days[data-dayid=\"{}\"]".format(slot4.time.date().isoformat())).click()
self.assertTrue(self.driver.find_element_by_css_selector("#swap-days-modal").is_displayed())
self.driver.find_element_by_css_selector("#swap-days-modal input[name=\"target_day\"][value=\"{}\"]".format(slot1.time.date().isoformat())).click()
self.driver.find_element_by_css_selector("#swap-days-modal button[type=\"submit\"]").click()
self.driver.find_element(By.CSS_SELECTOR, ".day .swap-days[data-dayid=\"{}\"]".format(slot4.time.date().isoformat())).click()
self.assertTrue(self.driver.find_element(By.CSS_SELECTOR, "#swap-days-modal").is_displayed())
self.driver.find_element(By.CSS_SELECTOR, "#swap-days-modal input[name=\"target_day\"][value=\"{}\"]".format(slot1.time.date().isoformat())).click()
self.driver.find_element(By.CSS_SELECTOR, "#swap-days-modal button[type=\"submit\"]").click()
self.assertTrue(self.driver.find_elements_by_css_selector('#timeslot{} #session{}'.format(slot4.pk, s1.pk)),
self.assertTrue(self.driver.find_elements(By.CSS_SELECTOR, '#timeslot{} #session{}'.format(slot4.pk, s1.pk)),
'Session s1 should have moved to second meeting day')
# swap timeslot column - put session in a differently-timed timeslot
self.driver.find_element_by_css_selector(
self.driver.find_element(By.CSS_SELECTOR,
'.day .swap-timeslot-col[data-timeslot-pk="{}"]'.format(slot1b.pk)
).click() # open modal on the second timeslot for room1
self.assertTrue(self.driver.find_element_by_css_selector("#swap-timeslot-col-modal").is_displayed())
self.driver.find_element_by_css_selector(
self.assertTrue(self.driver.find_element(By.CSS_SELECTOR, "#swap-timeslot-col-modal").is_displayed())
self.driver.find_element(By.CSS_SELECTOR,
'#swap-timeslot-col-modal input[name="target_timeslot"][value="{}"]'.format(slot4.pk)
).click() # select room1 timeslot that has a session in it
self.driver.find_element_by_css_selector('#swap-timeslot-col-modal button[type="submit"]').click()
self.driver.find_element(By.CSS_SELECTOR, '#swap-timeslot-col-modal button[type="submit"]').click()
self.assertTrue(self.driver.find_elements_by_css_selector('#timeslot{} #session{}'.format(slot1b.pk, s1.pk)),
self.assertTrue(self.driver.find_elements(By.CSS_SELECTOR, '#timeslot{} #session{}'.format(slot1b.pk, s1.pk)),
'Session s1 should have moved to second timeslot on first meeting day')
def test_past_flags(self):
@ -351,19 +357,19 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase):
self.login(username=meeting.schedule.owner.user.username)
self.driver.get(url)
past_flags = self.driver.find_elements_by_css_selector(
past_flags = self.driver.find_elements(By.CSS_SELECTOR,
','.join('#timeslot{} .past-flag'.format(ts.pk) for ts in past_timeslots)
)
self.assertGreaterEqual(len(past_flags), len(past_timeslots) + len(past_sessions),
'Expected at least one flag for each past timeslot and session')
now_flags = self.driver.find_elements_by_css_selector(
now_flags = self.driver.find_elements(By.CSS_SELECTOR,
','.join('#timeslot{} .past-flag'.format(ts.pk) for ts in now_timeslots)
)
self.assertGreaterEqual(len(now_flags), len(now_timeslots) + len(now_sessions),
'Expected at least one flag for each "now" timeslot and session')
future_flags = self.driver.find_elements_by_css_selector(
future_flags = self.driver.find_elements(By.CSS_SELECTOR,
','.join('#timeslot{} .past-flag'.format(ts.pk) for ts in future_timeslots)
)
self.assertGreaterEqual(len(future_flags), len(future_timeslots) + len(future_sessions),
@ -417,21 +423,21 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase):
self.login(username=meeting.schedule.owner.user.username)
self.driver.get(url)
past_swap_days_buttons = self.driver.find_elements_by_css_selector(
past_swap_days_buttons = self.driver.find_elements(By.CSS_SELECTOR,
','.join(
'.swap-days[data-start="{}"]'.format(ts.time.date().isoformat()) for ts in past_timeslots
)
)
self.assertEqual(len(past_swap_days_buttons), len(past_timeslots), 'Missing past swap days buttons')
future_swap_days_buttons = self.driver.find_elements_by_css_selector(
future_swap_days_buttons = self.driver.find_elements(By.CSS_SELECTOR,
','.join(
'.swap-days[data-start="{}"]'.format(ts.time.date().isoformat()) for ts in future_timeslots
)
)
self.assertEqual(len(future_swap_days_buttons), len(future_timeslots), 'Missing future swap days buttons')
now_swap_days_buttons = self.driver.find_elements_by_css_selector(
now_swap_days_buttons = self.driver.find_elements(By.CSS_SELECTOR,
','.join(
'.swap-days[data-start="{}"]'.format(ts.time.date().isoformat()) for ts in now_timeslots
)
@ -475,7 +481,7 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase):
self.fail('Modal never appeared')
self.assertFalse(
any(radio.is_enabled()
for radio in modal.find_elements_by_css_selector(','.join(
for radio in modal.find_elements(By.CSS_SELECTOR, ','.join(
'input[name="target_day"][value="{}"]'.format(ts.time.date().isoformat()) for ts in past_timeslots)
)),
'Past day is enabled in swap-days modal for official schedule',
@ -484,14 +490,14 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase):
enabled_timeslots = (ts for ts in future_timeslots if ts != future_timeslots[clicked_index])
self.assertTrue(
all(radio.is_enabled()
for radio in modal.find_elements_by_css_selector(','.join(
for radio in modal.find_elements(By.CSS_SELECTOR, ','.join(
'input[name="target_day"][value="{}"]'.format(ts.time.date().isoformat()) for ts in enabled_timeslots)
)),
'Future day is not enabled in swap-days modal for official schedule',
)
self.assertFalse(
any(radio.is_enabled()
for radio in modal.find_elements_by_css_selector(','.join(
for radio in modal.find_elements(By.CSS_SELECTOR, ','.join(
'input[name="target_day"][value="{}"]'.format(ts.time.date().isoformat()) for ts in now_timeslots)
)),
'"Now" day is enabled in swap-days modal for official schedule',
@ -533,21 +539,21 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase):
self.login(username=meeting.schedule.owner.user.username)
self.driver.get(url)
past_swap_ts_buttons = self.driver.find_elements_by_css_selector(
past_swap_ts_buttons = self.driver.find_elements(By.CSS_SELECTOR,
','.join(
'.swap-timeslot-col[data-start="{}"]'.format(ts.utc_start_time().isoformat()) for ts in past_timeslots
)
)
self.assertEqual(len(past_swap_ts_buttons), len(past_timeslots), 'Missing past swap timeslot col buttons')
future_swap_ts_buttons = self.driver.find_elements_by_css_selector(
future_swap_ts_buttons = self.driver.find_elements(By.CSS_SELECTOR,
','.join(
'.swap-timeslot-col[data-start="{}"]'.format(ts.utc_start_time().isoformat()) for ts in future_timeslots
)
)
self.assertEqual(len(future_swap_ts_buttons), len(future_timeslots), 'Missing future swap timeslot col buttons')
now_swap_ts_buttons = self.driver.find_elements_by_css_selector(
now_swap_ts_buttons = self.driver.find_elements(By.CSS_SELECTOR,
','.join(
'.swap-timeslot-col[data-start="{}"]'.format(ts.utc_start_time().isoformat()) for ts in now_timeslots
)
@ -590,7 +596,7 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase):
self.fail('Modal never appeared')
self.assertFalse(
any(radio.is_enabled()
for radio in modal.find_elements_by_css_selector(','.join(
for radio in modal.find_elements(By.CSS_SELECTOR, ','.join(
'input[name="target_timeslot"][value="{}"]'.format(ts.pk) for ts in past_timeslots)
)),
'Past timeslot is enabled in swap-timeslot-col modal for official schedule',
@ -599,14 +605,14 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase):
enabled_timeslots = (ts for ts in future_timeslots if ts != future_timeslots[clicked_index])
self.assertTrue(
all(radio.is_enabled()
for radio in modal.find_elements_by_css_selector(','.join(
for radio in modal.find_elements(By.CSS_SELECTOR, ','.join(
'input[name="target_timeslot"][value="{}"]'.format(ts.pk) for ts in enabled_timeslots)
)),
'Future timeslot is not enabled in swap-timeslot-col modal for official schedule',
)
self.assertFalse(
any(radio.is_enabled()
for radio in modal.find_elements_by_css_selector(','.join(
for radio in modal.find_elements(By.CSS_SELECTOR, ','.join(
'input[name="target_timeslot"][value="{}"]'.format(ts.pk) for ts in now_timeslots)
)),
'"Now" timeslot is enabled in swap-timeslot-col modal for official schedule',
@ -625,7 +631,7 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase):
def sort_by_position(driver, sessions):
"""Helper to sort sessions by the position of their session element in the unscheduled box"""
def _sort_key(sess):
elt = driver.find_element_by_id('session{}'.format(sess.pk))
elt = driver.find_element(By.ID, 'session{}'.format(sess.pk))
return (elt.location['y'], elt.location['x'])
return sorted(sessions, key=_sort_key)
@ -687,10 +693,10 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase):
self.login('secretary')
self.driver.get(url)
select = self.driver.find_element_by_name('sort_unassigned')
select = self.driver.find_element(By.NAME, 'sort_unassigned')
options = {
opt.get_attribute('value'): opt
for opt in select.find_elements_by_tag_name('option')
for opt in select.find_elements(By.TAG_NAME, 'option')
}
# check sorting by name
@ -760,18 +766,8 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase):
all of the events needed by the editor.
"""
# Set up a meeting and a schedule a plain user can edit
meeting = make_meeting_test_data()
schedule = Schedule.objects.filter(meeting=meeting, owner__user__username="plain").first()
sessions = meeting.session_set.filter(type_id='regular')
timeslots = meeting.timeslot_set.filter(type_id='regular')
self.assertGreaterEqual(timeslots.count(), sessions.count(),
'Need a timeslot for each session')
for index, session in enumerate(sessions):
SchedTimeSessAssignment.objects.create(
schedule=schedule,
timeslot=timeslots[index],
session=session,
)
schedule = ScheduleFactory(owner__user__username="plain")
meeting = schedule.meeting
# Open the editor
self.login()
@ -780,12 +776,11 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase):
kwargs=dict(num=meeting.number, name=schedule.name, owner=schedule.owner_email())
)
self.driver.get(url)
# Check that the drop target for unassigned sessions is actually empty
drop_target = self.driver.find_element_by_css_selector(
drop_target = self.driver.find_element(By.CSS_SELECTOR,
'.unassigned-sessions .drop-target'
)
self.assertEqual(len(drop_target.find_elements_by_class_name('session')), 0,
self.assertEqual(len(drop_target.find_elements(By.CLASS_NAME, 'session')), 0,
'Unassigned sessions box is not empty, test is broken')
# Check that the drop target has non-zero size
@ -829,7 +824,7 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase):
kwargs=dict(num=meeting.number, owner=schedule.owner.email(), name=schedule.name))
self.login(schedule.owner.user.username)
self.driver.get(url)
session_elements = [self.driver.find_element_by_css_selector(f'#session{sess.pk}') for sess in sessions]
session_elements = [self.driver.find_element(By.CSS_SELECTOR, f'#session{sess.pk}') for sess in sessions]
session_elements[0].click()
# All conflicting sessions should be flagged with the would-violate-hint class.
@ -864,20 +859,20 @@ class ScheduleEditTests(IetfSeleniumTestCase):
# driver.get() will wait for scripts to finish, but not ajax
# requests. Wait for completion of the permissions check:
read_only_note = self.driver.find_element_by_id('read_only')
read_only_note = self.driver.find_element(By.ID, 'read_only')
WebDriverWait(self.driver, 10).until(expected_conditions.invisibility_of_element(read_only_note), "Read-only schedule")
s1 = Session.objects.filter(group__acronym='mars', meeting=meeting).first()
selector = "#session_{}".format(s1.pk)
WebDriverWait(self.driver, 30).until(expected_conditions.presence_of_element_located((By.CSS_SELECTOR, selector)), "Did not find %s"%selector)
self.assertEqual(self.driver.find_elements_by_css_selector("#sortable-list #session_{}".format(s1.pk)), [])
self.assertEqual(self.driver.find_elements(By.CSS_SELECTOR, "#sortable-list #session_{}".format(s1.pk)), [])
element = self.driver.find_element_by_id('session_{}'.format(s1.pk))
target = self.driver.find_element_by_id('sortable-list')
element = self.driver.find_element(By.ID, 'session_{}'.format(s1.pk))
target = self.driver.find_element(By.ID, 'sortable-list')
ActionChains(self.driver).drag_and_drop(element,target).perform()
self.assertTrue(self.driver.find_elements_by_css_selector("#sortable-list #session_{}".format(s1.pk)))
self.assertTrue(self.driver.find_elements(By.CSS_SELECTOR, "#sortable-list #session_{}".format(s1.pk)))
time.sleep(0.1) # The API that modifies the database runs async
@ -905,8 +900,8 @@ class SlideReorderTests(IetfSeleniumTestCase):
self.secr_login()
self.driver.get(url)
#debug.show('unicode(self.driver.page_source)')
second = self.driver.find_element_by_css_selector('#slides tr:nth-child(2)')
third = self.driver.find_element_by_css_selector('#slides tr:nth-child(3)')
second = self.driver.find_element(By.CSS_SELECTOR, '#slides tr:nth-child(2)')
third = self.driver.find_element(By.CSS_SELECTOR, '#slides tr:nth-child(3)')
ActionChains(self.driver).drag_and_drop(second,third).perform()
time.sleep(0.1) # The API that modifies the database runs async
@ -1003,7 +998,7 @@ class AgendaTests(IetfSeleniumTestCase):
self.driver.get(self.absreverse('ietf.meeting.views.agenda') + querystring)
self.assert_agenda_item_visibility(visible_groups)
self.assert_agenda_view_filter_matches_ics_filter(querystring)
weekview_iframe = self.driver.find_element_by_id('weekview')
weekview_iframe = self.driver.find_element(By.ID, 'weekview')
if len(querystring) == 0:
self.assertFalse(weekview_iframe.is_displayed(), 'Weekview should be hidden when filters off')
else:
@ -1184,7 +1179,7 @@ class AgendaTests(IetfSeleniumTestCase):
for item in self.get_expected_items():
row_id = self.row_id_for_item(item)
try:
item_row = self.driver.find_element_by_id(row_id)
item_row = self.driver.find_element(By.ID, row_id)
except NoSuchElementException:
item_row = None
self.assertIsNotNone(item_row, 'No row for schedule item "%s"' % row_id)
@ -1205,7 +1200,7 @@ class AgendaTests(IetfSeleniumTestCase):
label = 'Free Slot'
try:
item_div = self.driver.find_element_by_xpath('//div/span[contains(text(),"%s")]/..' % label)
item_div = self.driver.find_element(By.XPATH, '//div/span[contains(text(),"%s")]/..' % label)
except NoSuchElementException:
item_div = None
@ -1480,7 +1475,7 @@ class AgendaTests(IetfSeleniumTestCase):
ics_url = self.absreverse('ietf.meeting.views.agenda_ical')
# parse out the events
agenda_rows = self.driver.find_elements_by_css_selector('[id^="row-"]:not(.info)')
agenda_rows = self.driver.find_elements(By.CSS_SELECTOR, '[id^="row-"]:not(.info)')
visible_rows = [r for r in agenda_rows if r.is_displayed()]
sessions = [self.session_from_agenda_row_id(row.get_attribute("id"))
for row in visible_rows]
@ -1510,7 +1505,7 @@ class AgendaTests(IetfSeleniumTestCase):
self.driver.get(url)
# modal should start hidden
modal_div = self.driver.find_element_by_css_selector('div#modal-%s' % slug)
modal_div = self.driver.find_element(By.CSS_SELECTOR, 'div#modal-%s' % slug)
self.assertFalse(modal_div.is_displayed())
# Click the 'materials' button
@ -1535,7 +1530,7 @@ class AgendaTests(IetfSeleniumTestCase):
)
self.assertGreater(not_deleted_slides.count(), 0) # make sure this isn't a pointless test
for slide in not_deleted_slides:
anchor = self.driver.find_element_by_xpath('//a[text()="%s"]' % slide.title)
anchor = self.driver.find_element(By.XPATH, '//a[text()="%s"]' % slide.title)
self.assertIsNotNone(anchor)
deleted_slides = session.materials.filter(
@ -1544,7 +1539,7 @@ class AgendaTests(IetfSeleniumTestCase):
self.assertGreater(deleted_slides.count(), 0) # make sure this isn't a pointless test
for slide in deleted_slides:
with self.assertRaises(NoSuchElementException):
self.driver.find_element_by_xpath('//a[text()="%s"]' % slide.title)
self.driver.find_element(By.XPATH, '//a[text()="%s"]' % slide.title)
# Now close the modal
close_modal_button = WebDriverWait(self.driver, 2).until(
@ -1589,7 +1584,7 @@ class AgendaTests(IetfSeleniumTestCase):
self.assertNotIn(newly_deleted_slide, not_deleted_slides)
self.assertIn(newly_undeleted_slide, not_deleted_slides)
for slide in not_deleted_slides:
anchor = self.driver.find_element_by_xpath('//a[text()="%s"]' % slide.title)
anchor = self.driver.find_element(By.XPATH, '//a[text()="%s"]' % slide.title)
self.assertIsNotNone(anchor)
deleted_slides = session.materials.filter(
@ -1599,7 +1594,7 @@ class AgendaTests(IetfSeleniumTestCase):
self.assertNotIn(newly_undeleted_slide, deleted_slides)
for slide in deleted_slides:
with self.assertRaises(NoSuchElementException):
self.driver.find_element_by_xpath('//a[text()="%s"]' % slide.title)
self.driver.find_element(By.XPATH, '//a[text()="%s"]' % slide.title)
def test_agenda_time_zone_selection(self):
self.assertNotEqual(self.meeting.time_zone, 'UTC', 'Meeting time zone must not be UTC')
@ -1616,14 +1611,14 @@ class AgendaTests(IetfSeleniumTestCase):
)
)
tz_select_input = self.driver.find_element_by_id('timezone-select')
meeting_tz_link = self.driver.find_element_by_id('meeting-timezone')
local_tz_link = self.driver.find_element_by_id('local-timezone')
utc_tz_link = self.driver.find_element_by_id('utc-timezone')
tz_displays = self.driver.find_elements_by_css_selector('.current-tz')
tz_select_input = self.driver.find_element(By.ID, 'timezone-select')
meeting_tz_link = self.driver.find_element(By.ID, 'meeting-timezone')
local_tz_link = self.driver.find_element(By.ID, 'local-timezone')
utc_tz_link = self.driver.find_element(By.ID, 'utc-timezone')
tz_displays = self.driver.find_elements(By.CSS_SELECTOR, '.current-tz')
self.assertGreaterEqual(len(tz_displays), 1)
# we'll check that all current-tz elements are updated, but first check that at least one is in the nav sidebar
self.assertIsNotNone(self.driver.find_element_by_css_selector('.nav .current-tz'))
self.assertIsNotNone(self.driver.find_element(By.CSS_SELECTOR, '.nav .current-tz'))
# Moment.js guesses local time zone based on the behavior of Selenium's web client. This seems
# to inherit Django's settings.TIME_ZONE but I don't know whether that's guaranteed to be consistent.
@ -1632,9 +1627,9 @@ class AgendaTests(IetfSeleniumTestCase):
self.assertNotEqual(self.meeting.time_zone, local_tz, 'Meeting time zone must not be local time zone')
self.assertNotEqual(local_tz, 'UTC', 'Local time zone must not be UTC')
meeting_tz_opt = tz_select_input.find_element_by_css_selector('option[value="%s"]' % self.meeting.time_zone)
local_tz_opt = tz_select_input.find_element_by_css_selector('option[value="%s"]' % local_tz)
utc_tz_opt = tz_select_input.find_element_by_css_selector('option[value="UTC"]')
meeting_tz_opt = tz_select_input.find_element(By.CSS_SELECTOR, 'option[value="%s"]' % self.meeting.time_zone)
local_tz_opt = tz_select_input.find_element(By.CSS_SELECTOR, 'option[value="%s"]' % local_tz)
utc_tz_opt = tz_select_input.find_element(By.CSS_SELECTOR, 'option[value="UTC"]')
# Should start off in meeting time zone
self.assertTrue(meeting_tz_opt.is_selected())
@ -1753,21 +1748,21 @@ class AgendaTests(IetfSeleniumTestCase):
# Verify that elements are all updated when the filters change. That the correct elements
# have the appropriate classes is a separate test.
elements_to_check = self.driver.find_elements_by_css_selector('.agenda-link.filterable')
elements_to_check = self.driver.find_elements(By.CSS_SELECTOR, '.agenda-link.filterable')
self.assertGreater(len(elements_to_check), 0, 'No elements with agenda links to update were found')
self.assertFalse(
any(checkbox.is_selected()
for checkbox in self.driver.find_elements_by_css_selector(
for checkbox in self.driver.find_elements(By.CSS_SELECTOR,
'input.checkbox[name="selected-sessions"]')),
'Sessions were selected before being clicked',
)
mars_sessa_checkbox = self.driver.find_element_by_css_selector('input[type="checkbox"][name="selected-sessions"][data-filter-item="mars-sessa"]')
mars_sessb_checkbox = self.driver.find_element_by_css_selector('input[type="checkbox"][name="selected-sessions"][data-filter-item="mars-sessb"]')
farfut_button = self.driver.find_element_by_css_selector('button[data-filter-item="farfut"]')
break_checkbox = self.driver.find_element_by_css_selector('input[type="checkbox"][name="selected-sessions"][data-filter-item="secretariat-sessb"]')
registration_checkbox = self.driver.find_element_by_css_selector('input[type="checkbox"][name="selected-sessions"][data-filter-item="secretariat-sessa"]')
mars_sessa_checkbox = self.driver.find_element(By.CSS_SELECTOR, 'input[type="checkbox"][name="selected-sessions"][data-filter-item="mars-sessa"]')
mars_sessb_checkbox = self.driver.find_element(By.CSS_SELECTOR, 'input[type="checkbox"][name="selected-sessions"][data-filter-item="mars-sessb"]')
farfut_button = self.driver.find_element(By.CSS_SELECTOR, 'button[data-filter-item="farfut"]')
break_checkbox = self.driver.find_element(By.CSS_SELECTOR, 'input[type="checkbox"][name="selected-sessions"][data-filter-item="secretariat-sessb"]')
registration_checkbox = self.driver.find_element(By.CSS_SELECTOR, 'input[type="checkbox"][name="selected-sessions"][data-filter-item="secretariat-sessa"]')
mars_sessa_checkbox.click() # select mars session
try:
@ -1884,9 +1879,9 @@ class WeekviewTests(IetfSeleniumTestCase):
def _assert_wrapped(displayed, expected_time_string):
self.assertEqual(len(displayed), 2)
first = displayed[0]
first_parent = first.find_element_by_xpath('..')
first_parent = first.find_element(By.XPATH, '..')
second = displayed[1]
second_parent = second.find_element_by_xpath('..')
second_parent = second.find_element(By.XPATH, '..')
self.assertNotIn('continued', first.text)
self.assertIn(expected_time_string, first_parent.text)
self.assertIn('continued', second.text)
@ -1895,7 +1890,7 @@ class WeekviewTests(IetfSeleniumTestCase):
def _assert_not_wrapped(displayed, expected_time_string):
self.assertEqual(len(displayed), 1)
first = displayed[0]
first_parent = first.find_element_by_xpath('..')
first_parent = first.find_element(By.XPATH, '..')
self.assertNotIn('continued', first.text)
self.assertIn(expected_time_string, first_parent.text)
@ -2021,6 +2016,7 @@ class InterimTests(IetfSeleniumTestCase):
sg_interim = make_interim_meeting(somegroup, datetime.date.today() + datetime.timedelta(days=20))
sg_sess = sg_interim.session_set.first()
sg_slot = sg_sess.timeslotassignments.first().timeslot
sg_sess.purpose_id = 'plenary'
sg_sess.type_id = 'plenary'
sg_slot.type_id = 'plenary'
sg_sess.save()
@ -2069,7 +2065,7 @@ class InterimTests(IetfSeleniumTestCase):
return meetings
def find_upcoming_meeting_entries(self):
return self.driver.find_elements_by_css_selector(
return self.driver.find_elements(By.CSS_SELECTOR,
'table#upcoming-meeting-table a.ietf-meeting-link, table#upcoming-meeting-table a.interim-meeting-link'
)
@ -2119,7 +2115,7 @@ class InterimTests(IetfSeleniumTestCase):
# 12 in order to check the starting month of the following year, which
# will usually contain the day 1 year from the start date.
for _ in range(13):
entries = self.driver.find_elements_by_css_selector(
entries = self.driver.find_elements(By.CSS_SELECTOR,
'div#calendar div.fc-content'
)
for entry in entries:
@ -2150,9 +2146,9 @@ class InterimTests(IetfSeleniumTestCase):
if simplified_querystring in ['?show=', '?hide=', '?show=&hide=']:
simplified_querystring = '' # these empty querystrings will be dropped (not an exhaustive list)
ics_link = self.driver.find_element_by_link_text('Download as .ics')
ics_link = self.driver.find_element(By.LINK_TEXT, 'Download as .ics')
self.assertIn(simplified_querystring, ics_link.get_attribute('href'))
webcal_link = self.driver.find_element_by_link_text('Subscribe with webcal')
webcal_link = self.driver.find_element(By.LINK_TEXT, 'Subscribe with webcal')
self.assertIn(simplified_querystring, webcal_link.get_attribute('href'))
def assert_upcoming_view_filter_matches_ics_filter(self, filter_string):
@ -2314,8 +2310,8 @@ class InterimTests(IetfSeleniumTestCase):
ts = session.official_timeslotassignment().timeslot
start = ts.utc_start_time().astimezone(zone).strftime('%Y-%m-%d %H:%M')
end = ts.utc_end_time().astimezone(zone).strftime('%H:%M')
meeting_link = self.driver.find_element_by_link_text(session.meeting.number)
time_td = meeting_link.find_element_by_xpath('../../td[@class="session-time"]')
meeting_link = self.driver.find_element(By.LINK_TEXT, session.meeting.number)
time_td = meeting_link.find_element(By.XPATH, '../../td[@class="session-time"]')
self.assertIn('%s - %s' % (start, end), time_td.text)
def _assert_ietf_tz_correct(meetings, tz):
@ -2333,8 +2329,8 @@ class InterimTests(IetfSeleniumTestCase):
start = start_dt.astimezone(zone).strftime('%Y-%m-%d')
end = end_dt.astimezone(zone).strftime('%Y-%m-%d')
meeting_link = self.driver.find_element_by_link_text("IETF " + meeting.number)
time_td = meeting_link.find_element_by_xpath('../../td[@class="meeting-time"]')
meeting_link = self.driver.find_element(By.LINK_TEXT, "IETF " + meeting.number)
time_td = meeting_link.find_element(By.XPATH, '../../td[@class="meeting-time"]')
self.assertIn('%s - %s' % (start, end), time_td.text)
sessions = [m.session_set.first() for m in self.displayed_interims()]
@ -2343,12 +2339,12 @@ class InterimTests(IetfSeleniumTestCase):
self.assertGreater(len(ietf_meetings), 0)
self.driver.get(self.absreverse('ietf.meeting.views.upcoming'))
tz_select_input = self.driver.find_element_by_id('timezone-select')
tz_select_bottom_input = self.driver.find_element_by_id('timezone-select-bottom')
local_tz_link = self.driver.find_element_by_id('local-timezone')
utc_tz_link = self.driver.find_element_by_id('utc-timezone')
local_tz_bottom_link = self.driver.find_element_by_id('local-timezone-bottom')
utc_tz_bottom_link = self.driver.find_element_by_id('utc-timezone-bottom')
tz_select_input = self.driver.find_element(By.ID, 'timezone-select')
tz_select_bottom_input = self.driver.find_element(By.ID, 'timezone-select-bottom')
local_tz_link = self.driver.find_element(By.ID, 'local-timezone')
utc_tz_link = self.driver.find_element(By.ID, 'utc-timezone')
local_tz_bottom_link = self.driver.find_element(By.ID, 'local-timezone-bottom')
utc_tz_bottom_link = self.driver.find_element(By.ID, 'utc-timezone-bottom')
# wait for the select box to be updated - look for an arbitrary time zone to be in
# its options list to detect this
@ -2358,18 +2354,18 @@ class InterimTests(IetfSeleniumTestCase):
(By.CSS_SELECTOR, '#timezone-select > option[value="%s"]' % arbitrary_tz)
)
)
arbitrary_tz_bottom_opt = tz_select_bottom_input.find_element_by_css_selector(
arbitrary_tz_bottom_opt = tz_select_bottom_input.find_element(By.CSS_SELECTOR,
'option[value="%s"]' % arbitrary_tz)
utc_tz_opt = tz_select_input.find_element_by_css_selector('option[value="UTC"]')
utc_tz_bottom_opt= tz_select_bottom_input.find_element_by_css_selector('option[value="UTC"]')
utc_tz_opt = tz_select_input.find_element(By.CSS_SELECTOR, 'option[value="UTC"]')
utc_tz_bottom_opt= tz_select_bottom_input.find_element(By.CSS_SELECTOR, 'option[value="UTC"]')
# Moment.js guesses local time zone based on the behavior of Selenium's web client. This seems
# to inherit Django's settings.TIME_ZONE but I don't know whether that's guaranteed to be consistent.
# To avoid test fragility, ask Moment what it considers local and expect that.
local_tz = self.driver.execute_script('return moment.tz.guess();')
local_tz_opt = tz_select_input.find_element_by_css_selector('option[value=%s]' % local_tz)
local_tz_bottom_opt = tz_select_bottom_input.find_element_by_css_selector('option[value="%s"]' % local_tz)
local_tz_opt = tz_select_input.find_element(By.CSS_SELECTOR, 'option[value=%s]' % local_tz)
local_tz_bottom_opt = tz_select_bottom_input.find_element(By.CSS_SELECTOR, 'option[value="%s"]' % local_tz)
# Should start off in local time zone
self.assertTrue(local_tz_opt.is_selected())
@ -2466,7 +2462,7 @@ class InterimTests(IetfSeleniumTestCase):
slug = assignment.slug()
# modal should start hidden
modal_div = self.driver.find_element_by_css_selector('div#modal-%s' % slug)
modal_div = self.driver.find_element(By.CSS_SELECTOR, 'div#modal-%s' % slug)
self.assertFalse(modal_div.is_displayed())
# Click the 'materials' button

View file

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

View file

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

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 preprocess_meeting_important_dates
from ietf.message.utils import infer_message
from ietf.name.models import SlideSubmissionStatusName, ProceedingsMaterialTypeName
from ietf.name.models import SlideSubmissionStatusName, ProceedingsMaterialTypeName, SessionPurposeName
from ietf.secr.proceedings.utils import handle_upload_file
from ietf.secr.proceedings.proc_utils import (get_progress_stats, post_process, import_audio_files,
create_recording)
@ -621,7 +621,7 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
s.scheduling_label = "???"
s.purpose_label = None
if (s.purpose is None or s.purpose.slug == 'regular') and s.group:
if (s.purpose.slug in ('none', 'regular')) and s.group:
s.scheduling_label = s.group.acronym
s.purpose_label = 'BoF' if s.group.is_bof() else s.group.type.name
else:
@ -1058,6 +1058,7 @@ class TimeSlotForm(forms.Form):
location = RoomNameModelChoiceField(queryset=Room.objects.all(), required=False, empty_label="(No location)")
show_location = forms.BooleanField(initial=True, required=False)
type = forms.ModelChoiceField(queryset=TimeSlotTypeName.objects.filter(used=True), empty_label=None, required=False)
purpose = forms.ModelChoiceField(queryset=SessionPurposeName.objects.filter(used=True), required=False, widget=forms.HiddenInput)
name = forms.CharField(help_text='Name that appears on the agenda', required=False)
short = forms.CharField(max_length=32,label='Short name', help_text='Abbreviated session name used for material file names', required=False)
group = forms.ModelChoiceField(queryset=Group.objects.filter(type__in=['ietf', 'team'], state='active'),
@ -1083,6 +1084,12 @@ class TimeSlotForm(forms.Form):
self.active_assignment = None
# only allow timeslots with at least one purpose
timeslot_types_with_purpose = set()
for spn in SessionPurposeName.objects.filter(used=True):
timeslot_types_with_purpose.update(spn.timeslot_types)
self.fields['type'].queryset = self.fields['type'].queryset.filter(pk__in=timeslot_types_with_purpose)
if timeslot:
self.initial = {
'day': timeslot.time.date(),
@ -1115,7 +1122,10 @@ class TimeSlotForm(forms.Form):
ts_type = self.cleaned_data.get('type')
short = self.cleaned_data.get('short')
if ts_type:
if not ts_type:
# assign a generic purpose if no type has been set
self.cleaned_data['purpose'] = SessionPurposeName.objects.get(slug='open_meeting')
else:
if ts_type.slug in ['break', 'reg', 'reserved', 'unavail', 'regular']:
if ts_type.slug != 'regular':
self.cleaned_data['group'] = self.fields['group'].queryset.get(acronym='secretariat')
@ -1128,6 +1138,15 @@ class TimeSlotForm(forms.Form):
if self.timeslot and self.timeslot.type.slug == 'regular' and self.active_assignment and ts_type.slug != self.timeslot.type.slug:
self.add_error('type', "Can't change type on time slots for regular sessions when a session has been assigned")
# find an allowed session purpose (guaranteed by TimeSlotForm)
for purpose in SessionPurposeName.objects.filter(used=True):
if ts_type.pk in purpose.timeslot_types:
self.cleaned_data['purpose'] = purpose
break
if self.cleaned_data['purpose'] is None:
self.add_error('type', f'{ts_type} has no allowed purposes')
if (self.active_assignment
and self.active_assignment.session.group != self.cleaned_data.get('group')
and self.active_assignment.session.materials.exists()
@ -1211,6 +1230,7 @@ def edit_meeting_timeslots_and_misc_sessions(request, num=None, owner=None, name
short=c['short'],
group=c['group'],
type=c['type'],
purpose=c['purpose'],
agenda_note=c.get('agenda_note') or "",
)

View file

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

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):
dependencies = [
('name', '0034_add_officehours_timeslottypename'),
('name', '0033_populate_agendafiltertypename'),
]
operations = [

View file

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

View file

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

View file

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

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,
RoleName, RoomResourceName, SessionStatusName, StdLevelName, StreamName, TimeSlotTypeName,
TopicAudienceName, ReviewerQueuePolicyName, TimerangeName, ExtResourceTypeName, ExtResourceName,
SlideSubmissionStatusName, ProceedingsMaterialTypeName)
SlideSubmissionStatusName, ProceedingsMaterialTypeName, SessionPurposeName )
class TimeSlotTypeNameResource(ModelResource):
class Meta:
@ -701,3 +701,22 @@ class AgendaFilterTypeNameResource(ModelResource):
"order": ALL,
}
api.name.register(AgendaFilterTypeNameResource())
class SessionPurposeNameResource(ModelResource):
class Meta:
queryset = SessionPurposeName.objects.all()
serializer = api.Serializer()
cache = SimpleCache()
#resource_name = 'sessionpurposename'
ordering = ['slug', ]
filtering = {
"slug": ALL,
"name": ALL,
"desc": ALL,
"used": ALL,
"order": ALL,
"timeslot_types": ALL,
"on_agenda": ALL,
}
api.name.register(SessionPurposeNameResource())

View file

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

View file

@ -260,7 +260,7 @@ class SessionForm(forms.Form):
num_sessions_expected = int(data.get('num_session', ''))
except ValueError:
self.add_error('num_session', 'Invalid value for number of sessions')
if len(self.session_forms.errors) == 0 and num_sessions_with_data < num_sessions_expected:
if num_sessions_with_data < num_sessions_expected:
self.add_error('num_session', 'Must provide data for all sessions')
# if default (empty) option is selected, cleaned_data won't include num_session key

View file

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

View file

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

View file

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

View file

@ -3,25 +3,12 @@
<col width="200">
<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="row1"><td>Number of Sessions Requested:</td><td>{% if session.length_session3 %}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 %}
<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>
<tr class="row1"><td>Number of Sessions Requested:</td><td>{% if session.third_session %}3{% else %}{{ session.num_session }}{% endif %}</td></tr>
{% if form %}
{% include 'includes/sessions_request_view_formset.html' with formset=form.session_forms group=group session=session only %}
{% else %}
{% include 'includes/sessions_request_view_session_set.html' with session_set=sessions group=group session=session only %}
{% endif %}
</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="row2">
<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
MEETING_USES_CODIMD_DATE = datetime.date(2020,7,6)
MEETING_LEGACY_OFFICE_HOURS_END = 111 # last meeting to use legacy office hours representation
MEETING_LEGACY_OFFICE_HOURS_END = 112 # last meeting to use legacy office hours representation
# Maximum dimensions to accept at all
MEETINGHOST_LOGO_MAX_UPLOAD_WIDTH = 400

View file

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

View file

@ -195,9 +195,9 @@
</span>
{% 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 %}
<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>
@ -216,7 +216,12 @@
</div>
<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">
{% for day, t_groups in timeslot_groups %}
<div>
<div><strong>{{ day|date:"M. d" }}</strong></div>
@ -231,6 +236,8 @@
{% for type in timeslot_types %}
<label class="timeslot-type-{{ type.slug }}"><input type="checkbox" checked value="{{ type.slug }}"> {{ type }}</label>
{% 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>
@ -256,7 +263,7 @@
<div class="session-purpose-toggles">
{% for purpose in session_purposes %}
<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>
{% endfor %}
<button type="button" class="btn btn-default select-all">Select all</button>

View file

@ -1,5 +1,5 @@
<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;"
data-duration="{{ session.requested_duration.total_seconds }}" {% if session.attendees != None %}
data-attendees="{{ session.attendees }}"{% endif %}