Use reworked filtering for ical agendas; refactor filter UI with office hours buttons and nicer formatting

- Legacy-Id: 18619
This commit is contained in:
Jennifer Richards 2020-10-16 16:06:07 +00:00
parent 1b1bc24744
commit d67b298512
11 changed files with 880 additions and 309 deletions

View file

@ -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):

View file

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

View file

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

View file

@ -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-<meeting#>-<year>-<month>-<day>-<DoW>-<HHMM>-<parent acro>-<group acro>
# for plenary session:
# row-<meeting#>-<year>-<month>-<day>-<DoW>-<HHMM>-1plenary-<group acro>
# for others (break, reg, other):
# row-<meeting#>-<year>-<month>-<day>-<DoW>-<HHMM>-<group acro>-<session name slug>
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'(?P<ietf>IETF\s+)?(?P<number>\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

View file

@ -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')

View file

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

View file

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

View file

@ -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 %}
<h2>Download as .ics</h2>
<p class="buttonlist">
{% for p in group_parents %}
<a class="btn btn-default" href="{% url "ietf.meeting.views.agenda_ical" num=schedule.meeting.number %}?show={{p.acronym|upper}}">{{p.acronym|upper}}</a>
{% endfor %}
<a class="btn btn-default" href="{% url "ietf.meeting.views.agenda_ical" num=schedule.meeting.number %}?showtypes=plenary,other">Non-area events</a>
<a id="ical-link" class="hidden btn btn-primary" href="{% url "ietf.meeting.views.agenda_ical" num=schedule.meeting.number %}">Customized schedule</a>
{% for fc in filter_categories %}
{% if not forloop.last %} {# skip the last group, it's the office hours/misc #}
<div style="display:inline-block;margin-right:1em">
{% for p in fc|dictsort:"label" %}
<a class="btn btn-default" href="{% url "ietf.meeting.views.agenda_ical" num=schedule.meeting.number %}?show={{p.keyword}}">{{p.label}}</a>
{% endfor %}
</div>
{% endif %}
{% endfor %}
<div style="display:inline-block">
<a class="btn btn-default" href="{% url "ietf.meeting.views.agenda_ical" num=schedule.meeting.number %}?show={{ non_area_keywords|join:',' }}">Non-area events</a>
<a id="ical-link" class="hidden btn btn-primary" href="{% url "ietf.meeting.views.agenda_ical" num=schedule.meeting.number %}">Customized schedule</a>
</div>
</p>
<h2>
Schedule
{% if schedule.meeting.agenda_warning_note %}

View file

@ -1,3 +1,4 @@
{% load agenda_filter_tags %}
<div class="panel-group" id="accordion">
<div class="panel panel-default">
<div class="panel-heading">
@ -16,80 +17,60 @@
To be able to return to the customized view later, bookmark the resulting URL.
</p>
{% if group_parents|length %}
{% if filter_categories|length %}
<p>Groups displayed in <b><i>italics</i></b> are BOFs.</p>
<table class="table table-condensed">
<thead>
<tr>
{% for p in group_parents %}
<th style="width:{% widthratio 1 group_parents|length|add:1 100 %}%">
<button class="btn btn-default btn-block pickview {{ p.acronym|lower }}">{{ p.acronym|upper }}</button>
</th>
{% endfor %}
{% if non_area_filters %}
<th style="width:{% widthratio 1 group_parents|length|add:1 100 %}"></th>
{% endif %}
</tr>
</thead>
<tbody>
<tr>
{% for p in group_parents %}
<td class="view {{ p.acronym|lower }}">
<div class="btn-group-vertical btn-block">
{% for group in p.group_list %}
<div class="btn-group btn-group-xs btn-group-justified">
<button class="btn btn-default pickview {{ group.acronym }}"
data-filter-keywords="{{ p.acronym|lower }}{% if group.is_bof %},bof{% endif %}">
{% if group.is_bof %}
<i>{{ group.acronym }}</i>
{% else %}
{{ group.acronym }}
{% endif %}
</button>
</div>
{% endfor %}
{% endfor %}
{% if non_area_filters %}
<!-- Non-Area buttons -->
<td class="view non-area">
<div class="btn-group-vertical btn-block">
<div class="btn-group btn-group-xs btn-group-justified">
<button class="btn btn-default pickview adofficehours"> AD Office Hours</button>
</div>
<div class="btn-group btn-group-xs btn-group-justified">
<button class="btn btn-default pickview bof"> BOF</button>
</div>
<div class="btn-group btn-group-xs btn-group-justified">
<button class="btn btn-default pickview edu"> EDU</button>
</div>
<div class="btn-group btn-group-xs btn-group-justified">
<button class="btn btn-default pickview hackathon"> Hackathon</button>
</div>
<div class="btn-group btn-group-xs btn-group-justified">
<button class="btn btn-default pickview iepg"> IEPG</button>
</div>
<div class="btn-group btn-group-xs btn-group-justified">
<button class="btn btn-default pickview iesg"> IESG</button>
</div>
<div class="btn-group btn-group-xs btn-group-justified">
<button class="btn btn-default pickview ietf"> IETF</button>
</div>
<div class="btn-group btn-group-xs btn-group-justified">
<button class="btn btn-default pickview plenary"> Plenary</button>
</div>
<div class="btn-group btn-group-xs btn-group-justified">
<button class="btn btn-default pickview secretariat"> Secretariat</button>
</div>
<div class="btn-group btn-group-xs btn-group-justified">
<button class="btn btn-default pickview tools"> Tools</button>
</div>
</div>
</td>
{% endif %}
</tr>
</tbody>
</table>
{% with spacer_scale=5 %}
{% with width_scale=filter_categories|agenda_width_scale:spacer_scale %}
<table class="table table-condensed">
<thead>
<tr>
{% for fc in filter_categories %}
{% if not forloop.first %}
<th style="width:{% widthratio 1 width_scale 100 %}%"></th>
{% endif %}
{% for area in fc %}
<th style="width:{% widthratio spacer_scale width_scale 100 %}%">
{% if area.keyword %}
<button class="btn btn-default btn-block pickview {{ area.keyword }}"
data-filter-item="{{ area.keyword }}">
{% firstof area.label area.keyword %}
</button>
{% endif %}
</th>
{% endfor %}
{% endfor %}
</tr>
</thead>
<tbody>
<tr>
{% for fc in filter_categories %}
{% if not forloop.first %} <td></td> {% endif %}
{% for p in fc %}
<td class="view {{ p.keyword }}">
<div class="btn-group-vertical btn-block">
{% for button in p.children|dictsort:"label" %}
<div class="btn-group btn-group-xs btn-group-justified">
<button class="btn btn-default pickview {{ button.keyword }}"
{% if p.keyword or button.is_bof %}data-filter-keywords="{% if p.keyword %}{{ p.keyword }}{% if button.is_bof %},{% endif %}{% endif %}{% if button.is_bof %}bof{% endif %}"{% endif %}
data-filter-item="{{ button.keyword }}">
{% if button.is_bof %}
<i>{{ button.label }}</i>
{% else %}
{{ button.label }}
{% endif %}
</button>
</div>
{% endfor %}
</div>
</td>
{% endfor %}
{% endfor %}
</tr>
</tbody>
</table>
{% endwith %}
{% endwith %}
{% else %}
<blockquote><i>No WG / RG data available -- no WG / RG sessions have been scheduled yet.</i>
</blockquote>

View file

@ -31,7 +31,7 @@
<p>For more on regular IETF meetings see <a href="https://www.ietf.org/meeting/upcoming.html">here</a></p>
{% 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 %}
<ul class="nav nav-tabs" role="tablist">

View file

@ -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/')