From d67b298512bd66040bae4d15c7d5c436c5fbfd5c Mon Sep 17 00:00:00 2001 From: Jennifer Richards Date: Fri, 16 Oct 2020 16:06:07 +0000 Subject: [PATCH] Use reworked filtering for ical agendas; refactor filter UI with office hours buttons and nicer formatting - Legacy-Id: 18619 --- ietf/meeting/helpers.py | 17 +- .../templatetags/agenda_filter_tags.py | 20 + ietf/meeting/tests_helpers.py | 11 +- ietf/meeting/tests_js.py | 452 +++++++++++++++--- ietf/meeting/tests_views.py | 377 ++++++++++----- ietf/meeting/views.py | 124 +++-- ietf/static/ietf/js/agenda/agenda_filter.js | 7 +- ietf/templates/meeting/agenda.html | 21 +- ietf/templates/meeting/agenda_filter.html | 125 ++--- ietf/templates/meeting/upcoming.html | 2 +- ietf/utils/test_utils.py | 33 ++ 11 files changed, 880 insertions(+), 309 deletions(-) create mode 100644 ietf/meeting/templatetags/agenda_filter_tags.py diff --git a/ietf/meeting/helpers.py b/ietf/meeting/helpers.py index d99e5fe89..eb9e75b8b 100644 --- a/ietf/meeting/helpers.py +++ b/ietf/meeting/helpers.py @@ -245,21 +245,22 @@ def tag_assignments_with_filter_keywords(assignments): Keywords are all lower case. """ for a in assignments: - a.filter_keywords = [a.timeslot.type.slug.lower()] - a.filter_keywords.extend(filter_keywords_for_session(a.session)) + a.filter_keywords = {a.timeslot.type.slug.lower()} + a.filter_keywords.update(filter_keywords_for_session(a.session)) def filter_keywords_for_session(session): - keywords = [] + keywords = {session.type.slug.lower()} group = getattr(session, 'historic_group', session.group) if group is not None: if group.state_id == 'bof': - keywords.append('bof') - keywords.append(group.acronym.lower()) + keywords.add('bof') + keywords.add(group.acronym.lower()) area = getattr(group, 'historic_parent', group.parent) if area is not None: - keywords.append(area.acronym.lower()) - if session.name.lower().endswith('office hours'): - keywords.append('adofficehours') + keywords.add(area.acronym.lower()) + office_hours_match = re.match(r'^ *\w+(?: +\w+)* +office hours *$', session.name, re.IGNORECASE) + if office_hours_match is not None: + keywords.update(['officehours', session.name.lower().replace(' ', '')]) return keywords def read_session_file(type, num, doc): diff --git a/ietf/meeting/templatetags/agenda_filter_tags.py b/ietf/meeting/templatetags/agenda_filter_tags.py new file mode 100644 index 000000000..8f9851815 --- /dev/null +++ b/ietf/meeting/templatetags/agenda_filter_tags.py @@ -0,0 +1,20 @@ +# Copyright The IETF Trust 2020, All Rights Reserved +# -*- coding: utf-8 -*- + +"""Custom tags for the agenda filter template""" + +from django import template + +register = template.Library() + +@register.filter +def agenda_width_scale(filter_categories, spacer_scale): + """Compute the width scale for the agenda filter button table + + Button columns are spacer_scale times as wide as the spacer columns between + categories. There is one fewer spacer column than categories. + """ + category_count = len(filter_categories) + column_count = sum([len(cat) for cat in filter_categories]) + # Refuse to return less than 1 to avoid width calculation problems. + return max(spacer_scale * column_count + category_count - 1, 1) diff --git a/ietf/meeting/tests_helpers.py b/ietf/meeting/tests_helpers.py index b0cee9e5d..1ba18f62d 100644 --- a/ietf/meeting/tests_helpers.py +++ b/ietf/meeting/tests_helpers.py @@ -66,20 +66,21 @@ class HelpersTests(TestCase): expected_area = group.parent for assignment in assignments: - expected_filter_keywords = [assignment.timeslot.type.slug] + expected_filter_keywords = {assignment.timeslot.type.slug, assignment.session.type.slug} if assignment.session == office_hours: - expected_filter_keywords.extend([ + expected_filter_keywords.update([ group.parent.acronym, - 'adofficehours', + 'officehours', + 'someofficehours', ]) else: - expected_filter_keywords.extend([ + expected_filter_keywords.update([ expected_group.acronym, expected_area.acronym ]) if bof: - expected_filter_keywords.append('bof') + expected_filter_keywords.add('bof') self.assertCountEqual( assignment.filter_keywords, diff --git a/ietf/meeting/tests_js.py b/ietf/meeting/tests_js.py index f797801f4..4951cb2dc 100644 --- a/ietf/meeting/tests_js.py +++ b/ietf/meeting/tests_js.py @@ -6,6 +6,7 @@ import time import datetime import shutil import os +import re from unittest import skipIf import django @@ -20,14 +21,16 @@ from ietf.doc.factories import DocumentFactory from ietf.group import colors from ietf.person.models import Person from ietf.group.models import Group +from ietf.group.factories import GroupFactory from ietf.meeting.factories import SessionFactory -from ietf.meeting.test_data import make_meeting_test_data +from ietf.meeting.test_data import make_meeting_test_data, make_interim_meeting from ietf.meeting.models import (Schedule, SchedTimeSessAssignment, Session, Room, TimeSlot, Constraint, ConstraintName, Meeting, SchedulingEvent, SessionStatusName) from ietf.meeting.utils import add_event_info_to_session_qs -from ietf.utils.test_runner import IetfLiveServerTestCase from ietf.utils.pipe import pipe +from ietf.utils.test_runner import IetfLiveServerTestCase +from ietf.utils.test_utils import assert_ical_response_is_valid from ietf import settings skip_selenium = False @@ -355,20 +358,6 @@ class AgendaTests(MeetingTestCase): expected_items = self.meeting.schedule.assignments.exclude(timeslot__type__in=['lead','offagenda']) self.assertGreater(len(expected_items), 0, 'Test setup generated an empty schedule') return expected_items - - def test_agenda_view_displays_all_items(self): - """By default, all agenda items should be displayed""" - self.login() - self.driver.get(self.absreverse('ietf.meeting.views.agenda')) - - for item in self.get_expected_items(): - row_id = 'row-%s' % item.slug() - try: - 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) - self.assertTrue(item_row.is_displayed(), 'Row for schedule item "%s" is not displayed' % row_id) def test_agenda_view_js_func_parse_query_params(self): """Test parse_query_params() function""" @@ -444,6 +433,7 @@ class AgendaTests(MeetingTestCase): self.login() 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') if len(querystring) == 0: self.assertFalse(weekview_iframe.is_displayed(), 'Weekview should be hidden when filters off') @@ -453,42 +443,169 @@ class AgendaTests(MeetingTestCase): self.assert_weekview_item_visibility(visible_groups) self.driver.switch_to.default_content() - def test_agenda_view_filter_show_none(self): - """Filtered agenda view should display only matching rows (no group selected)""" - self.do_agenda_view_filter_test('?show=', []) + def test_agenda_view_filter_default(self): + """Filtered agenda view should display only matching rows (all groups selected)""" + self.do_agenda_view_filter_test('', None) # None means all should be visible - def test_agenda_view_filter_show_one(self): - """Filtered agenda view should display only matching rows (one group selected)""" + def test_agenda_view_filter_show_group(self): + self.do_agenda_view_filter_test('?show=', []) self.do_agenda_view_filter_test('?show=mars', ['mars']) + self.do_agenda_view_filter_test('?show=mars,ames', ['mars', 'ames']) def test_agenda_view_filter_show_area(self): mars = Group.objects.get(acronym='mars') area = mars.parent self.do_agenda_view_filter_test('?show=%s' % area.acronym, ['ames', 'mars']) - def test_agenda_view_filter_bof(self): + def test_agenda_view_filter_show_type(self): + self.do_agenda_view_filter_test('?show=reg,break', ['secretariat']) + + def test_agenda_view_filter_show_bof(self): mars = Group.objects.get(acronym='mars') mars.state_id = 'bof' mars.save() self.do_agenda_view_filter_test('?show=bof', ['mars']) - self.do_agenda_view_filter_test('?show=bof,mars', ['mars']) - self.do_agenda_view_filter_test('?show=bof,ames', ['mars','ames']) + self.do_agenda_view_filter_test('?show=bof,ames', ['ames', 'mars']) - def test_agenda_view_filter_show_two(self): - """Filtered agenda view should display only matching rows (two groups selected)""" - self.do_agenda_view_filter_test('?show=mars,ames', ['mars', 'ames']) + def test_agenda_view_filter_show_ad_office_hours(self): + area = GroupFactory(type_id='area') + SessionFactory( + meeting__type_id='ietf', + type_id='other', + group=area, + name='%s Office Hours' % area.acronym, + ) + self.do_agenda_view_filter_test('?show=adofficehours', [area.acronym]) - def test_agenda_view_filter_all(self): - """Filtered agenda view should display only matching rows (all groups selected)""" - self.do_agenda_view_filter_test('', None) # None means all should be visible + def test_agenda_view_filter_hide_group(self): + mars = Group.objects.get(acronym='mars') + mars.state_id = 'bof' + mars.save() + area = mars.parent + + # Nothing shown, nothing visible + self.do_agenda_view_filter_test('?hide=mars', []) + + # Group shown + self.do_agenda_view_filter_test('?show=ames,mars&hide=mars', ['ames']) + + # Area shown + self.do_agenda_view_filter_test('?show=%s&hide=mars' % area.acronym, ['ames']) + + # Type shown + self.do_agenda_view_filter_test('?show=plenary,regular&hide=mars', ['ames','ietf']) + + # bof shown + self.do_agenda_view_filter_test('?show=bof&hide=mars', []) - def test_agenda_view_filter_hide(self): - self.do_agenda_view_filter_test('?hide=ietf', []) def test_agenda_view_filter_hide_area(self): mars = Group.objects.get(acronym='mars') + mars.state_id = 'bof' + mars.save() area = mars.parent - self.do_agenda_view_filter_test('?show=mars&hide=%s' % area.acronym, []) + SessionFactory( + meeting__type_id='ietf', + type_id='other', + group=area, + name='%s Office Hours' % area.acronym, + ) + + # Nothing shown + self.do_agenda_view_filter_test('?hide=%s' % area.acronym, []) + + # Group shown + self.do_agenda_view_filter_test('?show=ames,mars&hide=%s' % area.acronym, []) + + # Area shown + self.do_agenda_view_filter_test('?show=%s&hide=%s' % (area.acronym, area.acronym), []) + + # Type shown + self.do_agenda_view_filter_test('?show=plenary,regular&hide=%s' % area.acronym, ['ietf']) + + # bof shown + self.do_agenda_view_filter_test('?show=bof&hide=%s' % area.acronym, []) + + # AD office hours shown + self.do_agenda_view_filter_test('?show=adofficehours&hide=%s' % area.acronym, []) + + def test_agenda_view_filter_hide_type(self): + mars = Group.objects.get(acronym='mars') + mars.state_id = 'bof' + mars.save() + area = mars.parent + SessionFactory( + meeting__type_id='ietf', + type_id='other', + group=area, + name='%s Office Hours' % area.acronym, + ) + + # Nothing shown + self.do_agenda_view_filter_test('?hide=plenary', []) + + # Group shown + self.do_agenda_view_filter_test('?show=ietf,ames&hide=plenary', ['ames']) + + # Area shown + self.do_agenda_view_filter_test('?show=%s&hide=regular' % area.acronym, []) + + # Type shown + self.do_agenda_view_filter_test('?show=plenary,regular&hide=plenary', ['ames', 'mars']) + + # bof shown + self.do_agenda_view_filter_test('?show=bof&hide=regular', []) + + # AD office hours shown + self.do_agenda_view_filter_test('?show=adofficehours&hide=other', []) + + def test_agenda_view_filter_hide_bof(self): + mars = Group.objects.get(acronym='mars') + mars.state_id = 'bof' + mars.save() + area = mars.parent + + # Nothing shown + self.do_agenda_view_filter_test('?hide=bof', []) + + # Group shown + self.do_agenda_view_filter_test('?show=mars,ames&hide=bof', ['ames']) + + # Area shown + self.do_agenda_view_filter_test('?show=%s&hide=bof' % area.acronym, ['ames']) + + # Type shown + self.do_agenda_view_filter_test('?show=regular&hide=bof', ['ames']) + + # bof shown + self.do_agenda_view_filter_test('?show=bof&hide=bof', []) + + def test_agenda_view_filter_hide_ad_office_hours(self): + mars = Group.objects.get(acronym='mars') + mars.state_id = 'bof' + mars.save() + area = mars.parent + SessionFactory( + meeting__type_id='ietf', + type_id='other', + group=area, + name='%s Office Hours' % area.acronym, + ) + + # Nothing shown + self.do_agenda_view_filter_test('?hide=adofficehours', []) + + # Area shown + self.do_agenda_view_filter_test('?show=%s&hide=adofficehours' % area.acronym, ['ames', 'mars']) + + # Type shown + self.do_agenda_view_filter_test('?show=plenary,other&hide=adofficehours', ['ietf']) + + # AD office hours shown + self.do_agenda_view_filter_test('?show=adofficehours&hide=adofficehours', []) + + def test_agenda_view_filter_whitespace(self): + self.do_agenda_view_filter_test('?show= ames , mars &hide= mars ', ['ames']) def assert_agenda_item_visibility(self, visible_groups=None): """Assert that correct items are visible in current browser window @@ -604,6 +721,81 @@ class AgendaTests(MeetingTestCase): # no assertion here - if WebDriverWait raises an exception, the test will fail. # We separately test whether this URL will filter correctly. + def session_from_agenda_row_id(self, row_id): + """Find session corresponding to a row in the agenda HTML""" + components = row_id.split('-', 8) + # for regular session: + # row-------- + # for plenary session: + # row-------1plenary- + # for others (break, reg, other): + # row-------- + meeting_number = components[1] + start_time = datetime.datetime( + year=int(components[2]), + month=int(components[3]), + day=int(components[4]), + hour=int(components[6][0:2]), + minute=int(components[6][2:4]), + ) + # If labeled as plenary, it's plenary... + if components[7] == '1plenary': + session_type = 'plenary' + group = Group.objects.get(acronym=components[8]) + else: + # If not a plenary, see if the last component is a group + try: + group = Group.objects.get(acronym=components[8]) + except Group.DoesNotExist: + # Last component was not a group, so this must not be a regular session + session_type = 'other' + group = Group.objects.get(acronym=components[7]) + else: + # Last component was a group, this is a regular session + session_type = 'regular' + + meeting = Meeting.objects.get(number=meeting_number) + possible_assignments = SchedTimeSessAssignment.objects.filter( + schedule__in=[meeting.schedule, meeting.schedule.base], + timeslot__time=start_time, + ) + if session_type == 'other': + possible_sessions = [pa.session for pa in possible_assignments.filter( + timeslot__type_id__in=['break', 'reg', 'other'], session__group=group + ) if slugify(pa.session.name) == components[8]] + if len(possible_sessions) != 1: + raise ValueError('No unique matching session for row %s (found %d)' % ( + row_id, len(possible_sessions) + )) + session = possible_sessions[0] + else: + session = possible_assignments.filter( + timeslot__type_id=session_type + ).get(session__group=group).session + return session, possible_assignments.get(session=session).timeslot + + def assert_agenda_view_filter_matches_ics_filter(self, filter_string): + """The agenda view and ics view should show the same events for a given filter + + This must be called after using self.driver.get to load the agenda page + to be checked. + """ + ics_url = self.absreverse('ietf.meeting.views.agenda_ical') + + # parse out the events + agenda_rows = self.driver.find_elements_by_css_selector('[id^="row-"') + 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] + r = self.client.get(ics_url + filter_string) + # verify that all expected sessions are found + expected_uids = [ + 'ietf-%s-%s-%s' % (session.meeting.number, timeslot.pk, session.group.acronym) + for (session, timeslot) in sessions + ] + assert_ical_response_is_valid(self, r, + expected_event_uids=expected_uids, + expected_event_count=len(sessions)) @skipIf(skip_selenium, skip_message) class InterimTests(MeetingTestCase): @@ -614,6 +806,17 @@ class InterimTests(MeetingTestCase): settings.AGENDA_PATH = self.materials_dir self.meeting = make_meeting_test_data(create_interims=True) + # Create a group with a plenary interim session for testing type filters + somegroup = GroupFactory(acronym='sg', name='Some Group') + 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.type_id = 'plenary' + sg_slot.type_id = 'plenary' + sg_sess.save() + sg_slot.save() + + def tearDown(self): settings.AGENDA_PATH = self.saved_agenda_path shutil.rmtree(self.materials_dir) @@ -654,6 +857,11 @@ class InterimTests(MeetingTestCase): m.calendar_label = 'IETF %s' % m.number return meetings + def find_upcoming_meeting_entries(self): + return self.driver.find_elements_by_css_selector( + 'table#upcoming-meeting-table a.ietf-meeting-link, table#upcoming-meeting-table a.interim-meeting-link' + ) + def assert_upcoming_meeting_visibility(self, visible_meetings=None): """Assert that correct items are visible in current browser window @@ -662,9 +870,7 @@ class InterimTests(MeetingTestCase): expected = {mtg.number for mtg in visible_meetings} not_visible = set() unexpected = set() - entries = self.driver.find_elements_by_css_selector( - 'table#upcoming-meeting-table a.ietf-meeting-link, table#upcoming-meeting-table a.interim-meeting-link' - ) + entries = self.find_upcoming_meeting_entries() for entry in entries: entry_text = entry.get_attribute('innerHTML').strip() # gets text, even if element is hidden nums = [n for n in expected if n in entry_text] @@ -725,6 +931,7 @@ class InterimTests(MeetingTestCase): self.driver.get(self.absreverse('ietf.meeting.views.upcoming') + querystring) self.assert_upcoming_meeting_visibility(visible_meetings) self.assert_upcoming_meeting_calendar(visible_meetings) + self.assert_upcoming_view_filter_matches_ics_filter(querystring) # Check the ical links simplified_querystring = querystring.replace(' ', '%20') # encode spaces' @@ -733,32 +940,151 @@ class InterimTests(MeetingTestCase): webcal_link = self.driver.find_element_by_link_text('Subscribe with webcal') self.assertIn(simplified_querystring, webcal_link.get_attribute('href')) - def test_upcoming_view_displays_all_interims(self): + def assert_upcoming_view_filter_matches_ics_filter(self, filter_string): + """The upcoming view and ics view should show matching events for a given filter + + The upcoming ics view shows more detail than the upcoming view, so this + test expands the upcoming meeting list into the corresponding set of expected + sessions. + + This must be called after using self.driver.get to load the upcoming page + to be checked. + """ + ics_url = self.absreverse('ietf.meeting.views.upcoming_ical') + + # parse out the meetings shown on the upcoming view + upcoming_meetings = self.find_upcoming_meeting_entries() + visible_meetings = [mtg for mtg in upcoming_meetings if mtg.is_displayed()] + + # Have list of meetings, now get sessions that should be shown + expected_ietfs = [] + expected_interim_sessions = [] + expected_schedules = [] + for meeting_elt in visible_meetings: + # meeting_elt is an anchor element + label_text = meeting_elt.get_attribute('innerHTML') + match = re.search(r'(?PIETF\s+)?(?P\S+)', label_text) + meeting = Meeting.objects.get(number=match.group('number')) + if match.group('ietf'): + expected_ietfs.append(meeting) + else: + expected_interim_sessions.extend([s.pk for s in meeting.session_set.all()]) + if meeting.schedule: + expected_schedules.extend([meeting.schedule, meeting.schedule.base]) + + # Now find the sessions we expect to see - should match the upcoming_ical view + expected_assignments = list(SchedTimeSessAssignment.objects.filter( + schedule__in=expected_schedules, + session__in=expected_interim_sessions, + timeslot__time__gte=datetime.date.today(), + )) + # The UID formats should match those in the upcoming.ics template + expected_uids = [ + 'ietf-%s-%s' % (item.session.meeting.number, item.timeslot.pk) + for item in expected_assignments + ] + [ + 'ietf-%s' % (ietf.number) for ietf in expected_ietfs + ] + r = self.client.get(ics_url + filter_string) + assert_ical_response_is_valid(self, r, + expected_event_uids=expected_uids, + expected_event_count=len(expected_uids)) + + def test_upcoming_view_default(self): """By default, all upcoming interims and IETF meetings should be displayed""" - meetings = set(self.all_ietf_meetings()) - meetings.update(self.displayed_interims()) - self.do_upcoming_view_filter_test('', meetings) + ietf_meetings = set(self.all_ietf_meetings()) + self.do_upcoming_view_filter_test('', ietf_meetings.union(self.displayed_interims())) - def test_upcoming_view_filter_show_none(self): - meetings = set(self.all_ietf_meetings()) - self.do_upcoming_view_filter_test('?show=', meetings) + def test_upcoming_view_filter_show_group(self): + # Show none + ietf_meetings = set(self.all_ietf_meetings()) + self.do_upcoming_view_filter_test('?show=', ietf_meetings) - def test_upcoming_view_filter_show_one(self): - meetings = set(self.all_ietf_meetings()) - meetings.update(self.displayed_interims(groups=['mars'])) - self.do_upcoming_view_filter_test('?show=mars', meetings) + # Show one + self.do_upcoming_view_filter_test('?show=mars', + ietf_meetings.union( + self.displayed_interims(groups=['mars']) + )) + + # Show two + self.do_upcoming_view_filter_test('?show=mars,ames', + ietf_meetings.union( + self.displayed_interims(groups=['mars', 'ames']) + )) def test_upcoming_view_filter_show_area(self): mars = Group.objects.get(acronym='mars') area = mars.parent - meetings = set(self.all_ietf_meetings()) - meetings.update(self.displayed_interims(groups=['mars', 'ames'])) - self.do_upcoming_view_filter_test('?show=%s' % area.acronym, meetings) + ietf_meetings = set(self.all_ietf_meetings()) + self.do_upcoming_view_filter_test('?show=%s' % area.acronym, + ietf_meetings.union( + self.displayed_interims(groups=['mars', 'ames']) + )) - def test_upcoming_view_filter_show_two(self): - meetings = set(self.all_ietf_meetings()) - meetings.update(self.displayed_interims(groups=['mars', 'ames'])) - self.do_upcoming_view_filter_test('?show=mars,ames', meetings) + def test_upcoming_view_filter_show_type(self): + ietf_meetings = set(self.all_ietf_meetings()) + self.do_upcoming_view_filter_test('?show=plenary', + ietf_meetings.union( + self.displayed_interims(groups=['sg']) + )) + + def test_upcoming_view_filter_hide_group(self): + mars = Group.objects.get(acronym='mars') + area = mars.parent + + # Without anything shown, should see only ietf meetings + ietf_meetings = set(self.all_ietf_meetings()) + self.do_upcoming_view_filter_test('?hide=mars', ietf_meetings) + + # With group shown + self.do_upcoming_view_filter_test('?show=ames,mars&hide=mars', + ietf_meetings.union( + self.displayed_interims(groups=['ames']) + )) + # With area shown + self.do_upcoming_view_filter_test('?show=%s&hide=mars' % area.acronym, + ietf_meetings.union( + self.displayed_interims(groups=['ames']) + )) + + # With type shown + self.do_upcoming_view_filter_test('?show=plenary&hide=sg', + ietf_meetings) + + def test_upcoming_view_filter_hide_area(self): + mars = Group.objects.get(acronym='mars') + area = mars.parent + + # Without anything shown, should see only ietf meetings + ietf_meetings = set(self.all_ietf_meetings()) + self.do_upcoming_view_filter_test('?hide=%s' % area.acronym, ietf_meetings) + + # With area shown + self.do_upcoming_view_filter_test('?show=%s&hide=%s' % (area.acronym, area.acronym), + ietf_meetings) + + # With group shown + self.do_upcoming_view_filter_test('?show=mars&hide=%s' % area.acronym, ietf_meetings) + + # With type shown + self.do_upcoming_view_filter_test('?show=regular&hide=%s' % area.acronym, ietf_meetings) + + def test_upcoming_view_filter_hide_type(self): + mars = Group.objects.get(acronym='mars') + area = mars.parent + + # Without anything shown, should see only ietf meetings + ietf_meetings = set(self.all_ietf_meetings()) + self.do_upcoming_view_filter_test('?hide=regular', ietf_meetings) + + # With group shown + self.do_upcoming_view_filter_test('?show=mars&hide=regular', ietf_meetings) + + # With type shown + self.do_upcoming_view_filter_test('?show=plenary,regular&hide=%s' % area.acronym, + ietf_meetings.union( + self.displayed_interims(groups=['sg']) + )) def test_upcoming_view_filter_whitespace(self): """Whitespace in filter lists should be ignored""" @@ -766,24 +1092,6 @@ class InterimTests(MeetingTestCase): meetings.update(self.displayed_interims(groups=['mars'])) self.do_upcoming_view_filter_test('?show=mars , ames &hide= ames', meetings) - def test_upcoming_view_filter_hide(self): - # Not a useful application, but test for completeness... - meetings = set(self.all_ietf_meetings()) - self.do_upcoming_view_filter_test('?hide=mars', meetings) - - def test_upcoming_view_filter_hide_area(self): - mars = Group.objects.get(acronym='mars') - area = mars.parent - meetings = set(self.all_ietf_meetings()) - self.do_upcoming_view_filter_test('?show=mars&hide=%s' % area.acronym, meetings) - - def test_upcoming_view_filter_show_and_hide_same_group(self): - meetings = set(self.all_ietf_meetings()) - meetings.update(self.displayed_interims(groups=['mars'])) - self.do_upcoming_view_filter_test('?show=mars,ames&hide=ames', meetings) - - # The upcoming meetings view does not handle showtypes / hidetypes - # The following are useful debugging tools # If you add this to a LiveServerTestCase and run just this test, you can browse diff --git a/ietf/meeting/tests_views.py b/ietf/meeting/tests_views.py index 12825e0f2..55706a653 100644 --- a/ietf/meeting/tests_views.py +++ b/ietf/meeting/tests_views.py @@ -23,6 +23,7 @@ from django.contrib.auth.models import User from django.test import Client, override_settings from django.db.models import F from django.http import QueryDict +from django.template import Context, Template import debug # pyflakes:ignore @@ -51,7 +52,7 @@ from ietf.meeting.factories import ( SessionFactory, SessionPresentationFactory, MeetingFactory, FloorPlanFactory, TimeSlotFactory, SlideSubmissionFactory ) from ietf.doc.factories import DocumentFactory, WgDraftFactory from ietf.submit.tests import submission_file - +from ietf.utils.test_utils import assert_ical_response_is_valid if os.path.exists(settings.GHOSTSCRIPT_COMMAND): skip_pdf_tests = False @@ -63,32 +64,6 @@ else: print(" "+skip_message) -def assert_ical_response_is_valid(test_inst, response, expected_event_summaries=None, expected_event_count=None): - """Validate an HTTP response containing iCal data - - Based on RFC2445, but not exhaustive by any means. Assumes a single iCalendar object. Checks that - expected_event_summaries are found, but other events are allowed to be present. Specify the - expected_event_count if you want to reject additional events. - """ - test_inst.assertEqual(response.get('Content-Type'), "text/calendar") - - # Validate iCalendar object - test_inst.assertContains(response, 'BEGIN:VCALENDAR', count=1) - test_inst.assertContains(response, 'END:VCALENDAR', count=1) - test_inst.assertContains(response, 'PRODID:', count=1) - test_inst.assertContains(response, 'VERSION', count=1) - - # Validate event objects - if expected_event_summaries is not None: - for summary in expected_event_summaries: - test_inst.assertContains(response, 'SUMMARY:' + summary) - - if expected_event_count is not None: - test_inst.assertContains(response, 'BEGIN:VEVENT', count=expected_event_count) - test_inst.assertContains(response, 'END:VEVENT', count=expected_event_count) - test_inst.assertContains(response, 'UID', count=expected_event_count) - - class MeetingTests(TestCase): def setUp(self): self.materials_dir = self.tempdir('materials') @@ -257,7 +232,28 @@ class MeetingTests(TestCase): r = self.client.get(urlreverse("ietf.meeting.views.week_view", kwargs=dict(num=meeting.number))) self.assertContains(r, 'CANCELLED') self.assertContains(r, session.group.acronym) - self.assertContains(r, slot.location.name) + self.assertContains(r, slot.location.name) + + def test_meeting_agenda_filters_ignored(self): + """The agenda view should ignore filter querystrings + + (They are handled by javascript on the front end) + """ + meeting = make_meeting_test_data() + expected_items = meeting.schedule.assignments.exclude(timeslot__type__in=['lead','offagenda']) + expected_rows = ['row-%s' % item.slug() for item in expected_items] + + r = self.client.get(urlreverse('ietf.meeting.views.agenda')) + for row_id in expected_rows: + self.assertContains(r, row_id) + + r = self.client.get(urlreverse('ietf.meeting.views.agenda') + '?show=mars') + for row_id in expected_rows: + self.assertContains(r, row_id) + + r = self.client.get(urlreverse('ietf.meeting.views.agenda') + '?show=mars&hide=ames,mars,plenary,ietf,bof') + for row_id in expected_rows: + self.assertContains(r, row_id) def test_agenda_iab_session(self): date = datetime.date.today() @@ -657,7 +653,7 @@ class MeetingTests(TestCase): 'Customized schedule' button. """ meeting = make_meeting_test_data() - + # get the agenda url = urlreverse('ietf.meeting.views.agenda', kwargs=dict(num=meeting.number)) r = self.client.get(url) @@ -665,16 +661,22 @@ class MeetingTests(TestCase): # Check that it has the links we expect ical_url = urlreverse('ietf.meeting.views.agenda_ical', kwargs=dict(num=meeting.number)) q = PyQuery(r.content) - content = q('#content').html().lower() # don't care about case - # Should be a 'non-area events' link showing appropriate types - self.assertIn('%s?showtypes=plenary,other' % ical_url, content) + content = q('#content').html() + assignments = meeting.schedule.assignments.exclude(timeslot__type__in=['lead', 'offagenda']) + # Assume the test meeting is not using historic groups groups = [a.session.group for a in assignments if a.session is not None] for g in groups: if g.parent_id is not None: self.assertIn('%s?show=%s' % (ical_url, g.parent.acronym.lower()), content) + # Should be a 'non-area events' link showing appropriate types + non_area_labels = [ + 'BoF', 'EDU', 'Hackathon', 'IEPG', 'IESG', 'IETF', 'Plenary', 'Secretariat', 'Tools', + ] + self.assertIn('%s?show=%s' % (ical_url, ','.join(non_area_labels).lower()), content) + def test_parse_agenda_filter_params(self): def _r(show=(), hide=(), showtypes=(), hidetypes=()): """Helper to create expected result dict""" @@ -723,7 +725,8 @@ class MeetingTests(TestCase): expected_event_summaries=expected_session_summaries, expected_event_count=len(expected_session_summaries)) - def test_ical_filter_default(self): + def test_ical_filter(self): + # Just a quick check of functionality - permutations tested via tests_js.AgendaTests meeting = make_meeting_test_data() self.do_ical_filter_test( meeting, @@ -736,43 +739,16 @@ class MeetingTests(TestCase): 'mars - Martian Special Interest Group', ] ) - - def test_ical_filter_show(self): - meeting = make_meeting_test_data() self.do_ical_filter_test( meeting, - querystring='?show=mars', - expected_session_summaries=[ - 'mars - Martian Special Interest Group', - ] - ) - - def test_ical_filter_hide(self): - meeting = make_meeting_test_data() - self.do_ical_filter_test( - meeting, - querystring='?hide=ietf', - expected_session_summaries=[] - ) - - def test_ical_filter_show_and_hide(self): - meeting = make_meeting_test_data() - self.do_ical_filter_test( - meeting, - querystring='?show=ames&hide=mars', + querystring='?show=plenary,secretariat,ames&hide=reg', expected_session_summaries=[ + 'Morning Break', + 'IETF Plenary', 'ames - Asteroid Mining Equipment Standardization Group', ] ) - def test_ical_filter_show_and_hide_same_group(self): - meeting = make_meeting_test_data() - self.do_ical_filter_test( - meeting, - querystring='?show=ames&hide=ames', - expected_session_summaries=[] - ) - def build_session_setup(self): # This setup is intentionally unusual - the session has one draft attached as a session presentation, # but lists a different on in its agenda. The expectation is that the pdf and tgz views will return both. @@ -2100,7 +2076,10 @@ class InterimTests(TestCase): self.assertIn('CANCELLED', q('tr>td.text-right>span').text()) def test_upcoming_filters_ignored(self): - """The upcoming view should ignore filter querystrings""" + """The upcoming view should ignore filter querystrings + + (They are handled by javascript on the front end) + """ r, interims = self.do_upcoming_test() self.assertContains(r, interims['mars'].number) self.assertContains(r, interims['ames'].number) @@ -2116,41 +2095,37 @@ class InterimTests(TestCase): self.assertContains(r, interims['ames'].number) self.assertContains(r, 'IETF 72') - def do_upcoming_ical_test(self, querystring=None, create_meeting=True): - if create_meeting: - make_meeting_test_data(create_interims=True) - - # Create a group with a plenary interim session for testing type filters - somegroup = GroupFactory(acronym='sg', name='Some Group') - 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.type_id = 'plenary' - sg_slot.type_id = 'plenary' - sg_sess.save() - sg_slot.save() - - url = urlreverse("ietf.meeting.views.upcoming_ical") - if querystring is not None: - url += '?' + querystring - r = self.client.get(url) - self.assertEqual(r.status_code, 200) - return r - def test_upcoming_ical(self): meeting = make_meeting_test_data(create_interims=True) populate_important_dates(meeting) - r = self.do_upcoming_ical_test(create_meeting=False) + url = urlreverse("ietf.meeting.views.upcoming_ical") + + r = self.client.get(url) - # Expect events for important dates plus 4 - one for each WG and one for the IETF meeting + self.assertEqual(r.status_code, 200) + # Expect events for important dates plus 3 - one for each WG and one for the IETF meeting assert_ical_response_is_valid(self, r, expected_event_summaries=[ 'ames - Asteroid Mining Equipment Standardization Group', 'mars - Martian Special Interest Group', - 'sg - Some Group', 'IETF 72', ], - expected_event_count=4 + meeting.importantdate_set.count()) + expected_event_count=3 + meeting.importantdate_set.count()) + + def test_upcoming_ical_filter(self): + # Just a quick check of functionality - details tested by test_js.InterimTests + make_meeting_test_data(create_interims=True) + url = urlreverse("ietf.meeting.views.upcoming_ical") + r = self.client.get(url + '?show=mars') + + self.assertEqual(r.status_code, 200) + assert_ical_response_is_valid(self, r, + expected_event_summaries=[ + 'mars - Martian Special Interest Group', + 'IETF 72', + ], + expected_event_count=2) + def test_upcoming_ical_filter_invalid_syntaxes(self): make_meeting_test_data() @@ -2162,29 +2137,6 @@ class InterimTests(TestCase): r = self.client.get(url + '?mars') self.assertEqual(r.status_code, 400, 'Missing parameter name should be rejected') - def test_upcoming_ical_filter_show(self): - r = self.do_upcoming_ical_test('show=mars,ames') - assert_ical_response_is_valid(self, r, - expected_event_summaries=[ - 'mars - Martian Special Interest Group', - 'ames - Asteroid Mining Equipment Standardization Group', - 'IETF 72', - ], - expected_event_count=3) - - def test_upcoming_ical_filter_hide(self): - r = self.do_upcoming_ical_test('hide=mars') - assert_ical_response_is_valid(self, r, expected_event_summaries=['IETF 72'], expected_event_count=1) - - def test_upcoming_ical_filter_show_and_hide(self): - r = self.do_upcoming_ical_test('show=mars,ames&hide=mars') - assert_ical_response_is_valid(self, r, - expected_event_summaries=[ - 'ames - Asteroid Mining Equipment Standardization Group', - 'IETF 72', - ], - expected_event_count=2) - def test_upcoming_json(self): make_meeting_test_data(create_interims=True) url = urlreverse("ietf.meeting.views.upcoming_json") @@ -3692,3 +3644,204 @@ class HasMeetingsTests(TestCase): for session in sessions: self.assertIn(session.meeting.number, q('.interim-meeting-link').text()) + +class AgendaFilterTests(TestCase): + """Tests for the AgendaFilter template""" + def test_agenda_width_scale_filter(self): + """Test calculation of UI column width by agenda_width_scale filter""" + template = Template('{% load agenda_filter_tags %}{{ categories|agenda_width_scale:spacing }}') + + # Should get '1' as min value when input is empty + context = Context({'categories': [], 'spacing': 7}) + self.assertEqual(template.render(context), '1') + + # 3 columns, no spacers + context = Context({'categories': [range(3)], 'spacing': 7}) + self.assertEqual(template.render(context), '21') + + # 6 columns, 1 spacer + context = Context({'categories': [range(3), range(3)], 'spacing': 7}) + self.assertEqual(template.render(context), '43') + + # 10 columns, 2 spacers + context = Context({'categories': [range(3), range(3), range(4)], 'spacing': 7}) + self.assertEqual(template.render(context), '72') + + # 10 columns, 2 spacers, different spacer scale + context = Context({'categories': [range(3), range(3), range(4)], 'spacing': 5}) + self.assertEqual(template.render(context), '52') + + def test_agenda_filter_template(self): + """Test rendering of input data by the agenda filter template""" + def _assert_button_ok(btn, expected_label=None, expected_filter_item=None, + expected_filter_keywords=None): + """Test button properties""" + self.assertIn(btn.text(), expected_label) + self.assertEqual(btn.attr('data-filter-item'), expected_filter_item) + self.assertEqual(btn.attr('data-filter-keywords'), expected_filter_keywords) + + template = Template('{% include "meeting/agenda_filter.html" %}') + + # Test with/without custom button text + context = Context({'customize_button_text': None, 'filter_categories': []}) + q = PyQuery(template.render(context)) + self.assertIn('Customize...', q('h4.panel-title').text()) + self.assertEqual(q('table'), []) # no filter_categories, so no button table + + context['customize_button_text'] = 'My custom text...' + q = PyQuery(template.render(context)) + self.assertIn(context['customize_button_text'], q('h4.panel-title').text()) + self.assertEqual(q('table'), []) # no filter_categories, so no button table + + # Now add a non-trivial set of filters + context['filter_categories'] = [ + [ # first category + dict( + label='area0', + keyword='keyword0', + children=[ + dict( + label='child00', + keyword='keyword00', + is_bof=False, + ), + dict( + label='child01', + keyword='keyword01', + is_bof=True, + ), + ]), + dict( + label='area1', + keyword='keyword1', + children=[ + dict( + label='child10', + keyword='keyword10', + is_bof=False, + ), + dict( + label='child11', + keyword='keyword11', + is_bof=True, + ), + ]), + ], + [ # second category + dict( + label='area2', + keyword='keyword2', + children=[ + dict( + label='child20', + keyword='keyword20', + is_bof=True, + ), + dict( + label='child21', + keyword='keyword21', + is_bof=False, + ), + ]), + ], + [ # third category + dict( + label=None, + keyword=None, + children=[ + dict( + label='child30', + keyword='keyword30', + is_bof=False, + ), + dict( + label='child31', + keyword='keyword31', + is_bof=True, + ), + ]), + ], + ] + + q = PyQuery(template.render(context)) + self.assertIn(context['customize_button_text'], q('h4.panel-title').text()) + self.assertNotEqual(q('table'), []) # should now have table + + # Check that buttons are present for the expected things + header_row = q('thead tr') + self.assertEqual(len(header_row), 1) + button_row = q('tbody tr') + self.assertEqual(len(button_row), 1) + + # verify correct headers + header_cells = header_row('th') + self.assertEqual(len(header_cells), 6) # 4 columns and 2 spacers + header_buttons = header_cells('button.pickview') + self.assertEqual(len(header_buttons), 3) # last column has blank header, so only 3 + + # verify buttons + button_cells = button_row('td') + + # area0 + _assert_button_ok(header_cells.eq(0)('button.keyword0'), + expected_label='area0', + expected_filter_item='keyword0') + + buttons = button_cells.eq(0)('button.pickview') + self.assertEqual(len(buttons), 2) # two children + _assert_button_ok(buttons('.keyword00'), + expected_label='child00', + expected_filter_item='keyword00', + expected_filter_keywords='keyword0') + _assert_button_ok(buttons('.keyword01'), + expected_label='child01', + expected_filter_item='keyword01', + expected_filter_keywords='keyword0,bof') + + # area1 + _assert_button_ok(header_cells.eq(1)('button.keyword1'), + expected_label='area1', + expected_filter_item='keyword1') + + buttons = button_cells.eq(1)('button.pickview') + self.assertEqual(len(buttons), 2) # two children + _assert_button_ok(buttons('.keyword10'), + expected_label='child10', + expected_filter_item='keyword10', + expected_filter_keywords='keyword1') + _assert_button_ok(buttons('.keyword11'), + expected_label='child11', + expected_filter_item='keyword11', + expected_filter_keywords='keyword1,bof') + + # area2 + # Skip column index 2, which is a spacer column + _assert_button_ok(header_cells.eq(3)('button.keyword2'), + expected_label='area2', + expected_filter_item='keyword2') + + buttons = button_cells.eq(3)('button.pickview') + self.assertEqual(len(buttons), 2) # two children + _assert_button_ok(buttons('.keyword20'), + expected_label='child20', + expected_filter_item='keyword20', + expected_filter_keywords='keyword2,bof') + _assert_button_ok(buttons('.keyword21'), + expected_label='child21', + expected_filter_item='keyword21', + expected_filter_keywords='keyword2') + + # area3 (no label for this one) + # Skip column index 4, which is a spacer column + self.assertEqual([], header_cells.eq(5)('button')) # no header button + buttons = button_cells.eq(5)('button.pickview') + self.assertEqual(len(buttons), 2) # two children + _assert_button_ok(buttons('.keyword30'), + expected_label='child30', + expected_filter_item='keyword30', + expected_filter_keywords=None) + _assert_button_ok(buttons('.keyword31'), + expected_label='child31', + expected_filter_item='keyword31', + expected_filter_keywords='bof') + diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py index 6b6939ff5..0a7a0966c 100644 --- a/ietf/meeting/views.py +++ b/ietf/meeting/views.py @@ -1369,12 +1369,87 @@ def agenda(request, num=None, name=None, base=None, ext=None, owner=None, utc="" p.group_list.sort(key=lambda g: g.acronym) + # Groups gathered and processed. Now arrange for the filter UI. + # + # The agenda_filter template expects a list of categorized header buttons, each + # with a list of children. Make two categories: the IETF areas and the other parent groups. + # We also pass a list of 'extra' buttons - currently Office Hours and miscellaneous filters. + # All but the last of these are additionally used by the agenda.html template to make + # a list of filtered ical buttons. The last group is ignored for this. + area_group_filters = [] + other_group_filters = [] + extra_filters = [] + + for p in group_parents: + new_filter = dict( + label=p.acronym.upper(), + keyword=p.acronym.lower(), + children=[ + dict( + label=g.acronym, + keyword=g.acronym.lower(), + is_bof=g.is_bof(), + ) for g in p.group_list + ] + ) + if p.type.slug == 'area': + area_group_filters.append(new_filter) + else: + other_group_filters.append(new_filter) + + office_hours_labels = set() + for a in filtered_assignments: + suffix = ' office hours' + if a.session.name.lower().endswith(suffix): + office_hours_labels.add(a.session.name[:-len(suffix)].strip()) + + if len(office_hours_labels) > 0: + # keyword needs to match what's tagged in filter_keywords_for_session() + extra_filters.append(dict( + label='Office Hours', + keyword='officehours', + children=[ + dict( + label=label, + keyword=label.lower().replace(' ', '')+'officehours', + is_bof=False, + ) for label in office_hours_labels + ] + )) + + # Keywords that should appear in 'non-area' column + non_area_labels = [ + 'BoF', 'EDU', 'Hackathon', 'IEPG', 'IESG', 'IETF', 'Plenary', 'Secretariat', 'Tools', + ] + # Remove any unused non-area keywords + non_area_filters = [ + dict(label=label, keyword=label.lower(), is_bof=False) + for label in non_area_labels if any([ + label.lower() in assignment.filter_keywords + for assignment in filtered_assignments + ]) + ] + if len(non_area_filters) > 0: + extra_filters.append(dict( + label=None, + keyword=None, + children=non_area_filters, + )) + + area_group_filters.sort(key=lambda p:p['label']) + other_group_filters.sort(key=lambda p:p['label']) + filter_categories = [category + for category in [area_group_filters, other_group_filters, extra_filters] + if len(category) > 0] + is_current_meeting = (num is None) or (num == get_current_ietf_meeting_num()) + rendered_page = render(request, "meeting/"+base+ext, { "schedule": schedule, "filtered_assignments": filtered_assignments, "updated": updated, - "group_parents": group_parents, + "filter_categories": filter_categories, + "non_area_keywords": [label.lower() for label in non_area_labels], "now": datetime.datetime.now(), "is_current_meeting": is_current_meeting, "use_codimd": True if meeting.date>=settings.MEETING_USES_CODIMD_DATE else False, @@ -1795,6 +1870,7 @@ def parse_agenda_filter_params(querydict): 'Parameter "%s" is not assigned a value (use "key=" for an empty value)' % key ) vals = unquote(value).lower().split(',') + vals = [v.strip() for v in vals] filt_params[key] = set([v for v in vals if len(v) > 0]) # remove empty strings return filt_params @@ -1806,31 +1882,9 @@ def should_include_assignment(filter_params, assignment): When filtering by wg, uses historic_group if available as an attribute on the session, otherwise falls back to using group. """ - historic_group = getattr(assignment.session, 'historic_group', None) - if historic_group: - group_acronym = historic_group.acronym - parent = historic_group.historic_parent - parent_acronym = parent.acronym if parent else None - else: - group = assignment.session.group - group_acronym = group.acronym - if group.parent: - parent_acronym = group.parent.acronym - else: - parent_acronym = None - session_type = assignment.timeslot.type_id - - # Hide if wg or type hide lists apply - if ((group_acronym in filter_params['hide']) or - (parent_acronym in filter_params['hide']) or - (session_type in filter_params['hidetypes'])): - return False - - # Show if any of the show lists apply, including showing by parent group - return ((group_acronym in filter_params['show']) or - (parent_acronym in filter_params['show']) or - (session_type in filter_params['showtypes'])) - + shown = len(set(filter_params['show']).intersection(assignment.filter_keywords)) > 0 + hidden = len(set(filter_params['hide']).intersection(assignment.filter_keywords)) > 0 + return shown and not hidden def agenda_ical(request, num=None, name=None, acronym=None, session_id=None): """Agenda ical view @@ -1862,6 +1916,7 @@ def agenda_ical(request, num=None, name=None, acronym=None, session_id=None): timeslot__type__private=False, ) assignments = preprocess_assignments_for_agenda(assignments, meeting) + tag_assignments_with_filter_keywords(assignments) try: filt_params = parse_agenda_filter_params(request.GET) @@ -3269,6 +3324,19 @@ def upcoming(request): p.group_list.append(g) seen.add(g.acronym) + # only one category + filter_categories = [[ + dict( + label=p.acronym, + keyword=p.acronym.lower(), + children=[dict( + label=g.acronym, + keyword=g.acronym.lower(), + is_bof=g.is_bof(), + ) for g in p.group_list] + ) for p in group_parents + ]] + for session in interim_sessions: session.historic_group = session.group session.filter_keywords = filter_keywords_for_session(session) @@ -3301,7 +3369,7 @@ def upcoming(request): return render(request, 'meeting/upcoming.html', { 'entries': entries, - 'group_parents': group_parents, + 'filter_categories': filter_categories, 'menu_actions': actions, 'menu_entries': menu_entries, 'selected_menu_entry': selected_menu_entry, @@ -3334,6 +3402,8 @@ def upcoming_ical(request): 'session__group', 'session__group__parent', 'timeslot', 'schedule', 'schedule__meeting' ).distinct()) + tag_assignments_with_filter_keywords(assignments) + # apply filters if filter_params is not None: assignments = [a for a in assignments if should_include_assignment(filter_params, a)] diff --git a/ietf/static/ietf/js/agenda/agenda_filter.js b/ietf/static/ietf/js/agenda/agenda_filter.js index 3067ef98c..9840303df 100644 --- a/ietf/static/ietf/js/agenda/agenda_filter.js +++ b/ietf/static/ietf/js/agenda/agenda_filter.js @@ -5,8 +5,7 @@ var agenda_filter_for_testing; // methods to be accessed for automated testing (function () { 'use strict' - var update_callback // function(filter_params) - var enable_non_area = false // if true, show the non-area filters + var update_callback; // function(filter_params) /* Remove from list, if present */ function remove_list_item (list, item) { @@ -76,7 +75,7 @@ var agenda_filter_for_testing; // methods to be accessed for automated testing } function get_item(elt) { - return elt.text().trim().toLowerCase().replace(/ /g, ''); + return $(elt).attr('data-filter-item'); } // utility method - is there a match between two lists of keywords? @@ -178,7 +177,6 @@ var agenda_filter_for_testing; // methods to be accessed for automated testing /* Helper for pick group/type button handlers - toggles the appropriate parameter entry * elt - the jquery element that was clicked - * param_type - key of the filter param to update (show, hide) */ function handle_pick_button (elt) { var fp = get_filter_params(parse_query_params(window.location.search)); @@ -259,7 +257,6 @@ var agenda_filter_for_testing; // methods to be accessed for automated testing enable: enable, filtering_is_enabled: filtering_is_enabled, get_filter_params: get_filter_params, - include_non_area_selectors: function () {enable_non_area = true}, keyword_match: keyword_match, parse_query_params: parse_query_params, rows_matching_filter_keyword: rows_matching_filter_keyword, diff --git a/ietf/templates/meeting/agenda.html b/ietf/templates/meeting/agenda.html index d800f33bd..1e5c20cbf 100644 --- a/ietf/templates/meeting/agenda.html +++ b/ietf/templates/meeting/agenda.html @@ -62,18 +62,25 @@ {% endif %} - {% include "meeting/agenda_filter.html" with group_parents=group_parents non_area_filters=True customize_button_text="Customize the agenda view..." only %} + {% include "meeting/agenda_filter.html" with filter_categories=filter_categories customize_button_text="Customize the agenda view..." only %}

Download as .ics

- {% for p in group_parents %} - {{p.acronym|upper}} - {% endfor %} - Non-area events - + {% for fc in filter_categories %} + {% if not forloop.last %} {# skip the last group, it's the office hours/misc #} +

+ {% for p in fc|dictsort:"label" %} + {{p.label}} + {% endfor %} +
+ {% endif %} + {% endfor %} +

-

Schedule {% if schedule.meeting.agenda_warning_note %} diff --git a/ietf/templates/meeting/agenda_filter.html b/ietf/templates/meeting/agenda_filter.html index b7de65c03..b3a9222c2 100644 --- a/ietf/templates/meeting/agenda_filter.html +++ b/ietf/templates/meeting/agenda_filter.html @@ -1,3 +1,4 @@ +{% load agenda_filter_tags %}
@@ -16,80 +17,60 @@ To be able to return to the customized view later, bookmark the resulting URL.

- {% if group_parents|length %} + {% if filter_categories|length %}

Groups displayed in italics are BOFs.

- - - - {% for p in group_parents %} - - {% endfor %} - {% if non_area_filters %} - - {% endif %} - - - - - {% for p in group_parents %} - - {% endif %} - - -
- -
-
- {% for group in p.group_list %} -
- -
- {% endfor %} - {% endfor %} - {% if non_area_filters %} - -
-
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
-
+ {% with spacer_scale=5 %} + {% with width_scale=filter_categories|agenda_width_scale:spacer_scale %} + + + + {% for fc in filter_categories %} + {% if not forloop.first %} + + {% endif %} + {% for area in fc %} + + {% endfor %} + {% endfor %} + + + + + {% for fc in filter_categories %} + {% if not forloop.first %} {% endif %} + {% for p in fc %} + + {% endfor %} + {% endfor %} + + +
+ {% if area.keyword %} + + {% endif %} +
+
+ {% for button in p.children|dictsort:"label" %} +
+ +
+ {% endfor %} +
+
+ {% endwith %} + {% endwith %} {% else %}
No WG / RG data available -- no WG / RG sessions have been scheduled yet.
diff --git a/ietf/templates/meeting/upcoming.html b/ietf/templates/meeting/upcoming.html index 27c501df7..9be988a12 100644 --- a/ietf/templates/meeting/upcoming.html +++ b/ietf/templates/meeting/upcoming.html @@ -31,7 +31,7 @@

For more on regular IETF meetings see here

- {% include 'meeting/agenda_filter.html' with group_parents=group_parents customize_button_text="Customize the meeting list..." only%} + {% include 'meeting/agenda_filter.html' with filter_categories=filter_categories customize_button_text="Customize the meeting list..." only%} {% if menu_entries %}