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
- Customized schedule
+ {% 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 %}
+
@@ -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 %}
- {% 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 %}
diff --git a/ietf/utils/test_utils.py b/ietf/utils/test_utils.py
index b4b28419d..6409984c8 100644
--- a/ietf/utils/test_utils.py
+++ b/ietf/utils/test_utils.py
@@ -92,6 +92,39 @@ def reload_db_objects(*objects):
else:
return t
+def assert_ical_response_is_valid(test_inst, response, expected_event_summaries=None,
+ expected_event_uids=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/_uids are found, but other events are allowed to be present. Specify the
+ expected_event_count if you want to reject additional events. If any of these are None,
+ the check for that property is skipped.
+ """
+ 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_uids is not None:
+ for uid in expected_event_uids:
+ test_inst.assertContains(response, 'UID:' + uid)
+
+ 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 ReverseLazyTest(django.test.TestCase):
def test_redirect_with_lazy_reverse(self):
response = self.client.get('/ipr/update/')