Merged in ^/personal/jennifer/7.17.1.dev0 from jennifer@painless-security.com:

This adds support for the simpler show/hide filtering to the ical agenda
views. It also significantly rearranges (and, I hope, improves the
organization of) the tests. In particular, it specifically tests that
the ical and HTML views include equivalent sets of events. Finally, the
agenda_filter.html template is reworked to be more modular.
 - Legacy-Id: 18631
This commit is contained in:
Henrik Levkowetz 2020-10-23 18:50:42 +00:00
commit 7384c03859
12 changed files with 1176 additions and 868 deletions

View file

@ -240,6 +240,30 @@ def preprocess_assignments_for_agenda(assignments_queryset, meeting, extra_prefe
return assignments
def tag_assignments_with_filter_keywords(assignments):
"""Add keywords for agenda filtering
Keywords are all lower case.
"""
for a in assignments:
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 = {session.type.slug.lower()}
group = getattr(session, 'historic_group', session.group)
if group is not None:
if group.state_id == 'bof':
keywords.add('bof')
keywords.add(group.acronym.lower())
area = getattr(group, 'historic_parent', group.parent)
if area is not None:
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):
# XXXX FIXME: the path fragment in the code below should be moved to
# settings.py. The *_PATH settings should be generalized to format()

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

@ -0,0 +1,97 @@
# Copyright The IETF Trust 2020, All Rights Reserved
# -*- coding: utf-8 -*-
from ietf.group.factories import GroupFactory
from ietf.meeting.factories import SessionFactory, MeetingFactory
from ietf.meeting.helpers import tag_assignments_with_filter_keywords
from ietf.utils.test_utils import TestCase
class HelpersTests(TestCase):
def do_test_tag_assignments_with_filter_keywords(self, bof=False, historic=None):
"""Assignments should be tagged properly
The historic param can be None, group, or parent, to specify whether to test
with no historic_group, a historic_group but no historic_parent, or both.
"""
meeting_types = ['regular', 'plenary']
group_state_id = 'bof' if bof else 'active'
group = GroupFactory(state_id=group_state_id)
historic_group = GroupFactory(state_id=group_state_id)
historic_parent = GroupFactory(type_id='area')
if historic == 'parent':
historic_group.historic_parent = historic_parent
# Create meeting and sessions
meeting = MeetingFactory()
for meeting_type in meeting_types:
sess = SessionFactory(group=group, meeting=meeting, type_id=meeting_type)
ts = sess.timeslotassignments.first().timeslot
ts.type = sess.type
ts.save()
# Create an office hours session in the group's area (i.e., parent). This is not
# currently really needed, but will protect against areas and groups diverging
# in a way that breaks keywording.
office_hours = SessionFactory(
name='some office hours',
group=group.parent,
meeting=meeting,
type_id='other'
)
ts = office_hours.timeslotassignments.first().timeslot
ts.type = office_hours.type
ts.save()
assignments = meeting.schedule.assignments.all()
orig_num_assignments = len(assignments)
# Set up historic groups if needed
if historic:
for a in assignments:
if a.session != office_hours:
a.session.historic_group = historic_group
# Execute the method under test
tag_assignments_with_filter_keywords(assignments)
# Assert expected results
self.assertEqual(len(assignments), orig_num_assignments, 'Should not change number of assignments')
if historic:
expected_group = historic_group
expected_area = historic_parent if historic == 'parent' else historic_group.parent
else:
expected_group = group
expected_area = group.parent
for assignment in assignments:
expected_filter_keywords = {assignment.timeslot.type.slug, assignment.session.type.slug}
if assignment.session == office_hours:
expected_filter_keywords.update([
group.parent.acronym,
'officehours',
'someofficehours',
])
else:
expected_filter_keywords.update([
expected_group.acronym,
expected_area.acronym
])
if bof:
expected_filter_keywords.add('bof')
self.assertCountEqual(
assignment.filter_keywords,
expected_filter_keywords,
'Assignment has incorrect filter keywords'
)
def test_tag_assignments_with_filter_keywords(self):
self.do_test_tag_assignments_with_filter_keywords()
self.do_test_tag_assignments_with_filter_keywords(historic='group')
self.do_test_tag_assignments_with_filter_keywords(historic='parent')
self.do_test_tag_assignments_with_filter_keywords(bof=True)
self.do_test_tag_assignments_with_filter_keywords(bof=True, historic='group')
self.do_test_tag_assignments_with_filter_keywords(bof=True, historic='parent')

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
@ -356,20 +359,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"""
@ -445,6 +434,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')
@ -454,72 +444,170 @@ 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_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_type(self):
self.do_agenda_view_filter_test('?show=reg,break', ['secretariat'])
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_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,ames', ['ames', 'mars'])
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_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,
)
def test_agenda_view_filter_show_and_hide(self):
self.do_agenda_view_filter_test('?show=mars&hide=ietf', ['mars'])
# 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), [])
def test_agenda_view_filter_show_and_hide_same_group(self):
self.do_agenda_view_filter_test('?show=mars&hide=mars', [])
# Type shown
self.do_agenda_view_filter_test('?show=plenary,regular&hide=%s' % area.acronym, ['ietf'])
def test_agenda_view_filter_showtypes(self):
self.do_agenda_view_filter_test('?showtypes=plenary', ['ietf']) # ietf has a plenary session
# 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 test_agenda_view_filter_hidetypes(self):
self.do_agenda_view_filter_test('?hidetypes=plenary', [])
def test_agenda_view_filter_showtypes_and_hidetypes(self):
self.do_agenda_view_filter_test('?showtypes=plenary&hidetypes=regular', ['ietf']) # ietf has a plenary session
def test_agenda_view_filter_showtypes_and_hidetypes_same_type(self):
self.do_agenda_view_filter_test('?showtypes=plenary&hidetypes=plenary', [])
def test_agenda_view_filter_show_and_showtypes(self):
self.do_agenda_view_filter_test('?show=mars&showtypes=plenary', ['mars', 'ietf']) # ietf has a plenary session
def test_agenda_view_filter_show_and_hidetypes(self):
self.do_agenda_view_filter_test('?show=ietf,mars&hidetypes=plenary', ['mars']) # ietf has a plenary session
def test_agenda_view_filter_hide_and_hidetypes(self):
self.do_agenda_view_filter_test('?hide=ietf,mars&hidetypes=plenary', [])
def test_agenda_view_filter_show_hide_and_showtypes(self):
self.do_agenda_view_filter_test('?show=mars&hide=ames&showtypes=plenary,regular', ['mars', 'ietf']) # ietf has plenary session
def test_agenda_view_filter_show_hide_and_hidetypes(self):
self.do_agenda_view_filter_test('?show=mars,ietf&hide=ames&hidetypes=plenary', ['mars']) # ietf has plenary session
def test_agenda_view_filter_all_params(self):
self.do_agenda_view_filter_test('?show=secretariat,ietf&hide=ames&showtypes=regular&hidetypes=plenary',
['secretariat', 'mars'])
def assert_agenda_item_visibility(self, visible_groups=None):
"""Assert that correct items are visible in current browser window
@ -634,6 +722,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):
@ -644,6 +807,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)
@ -684,6 +858,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
@ -692,20 +871,19 @@ 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 > tbody > tr.entry'
)
entries = self.find_upcoming_meeting_entries()
for entry in entries:
nums = [n for n in expected if n in entry.text]
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]
self.assertLessEqual(len(nums), 1, 'Multiple matching meeting numbers')
if len(nums) > 0: # asserted that it's at most 1, so if it's not 0, it's 1.
expected.remove(nums[0])
if not entry.is_displayed():
not_visible.add(nums[0])
continue
# Found an unexpected row - this is ok as long as it's hidden
# Found an unexpected row - this is only a problem if it is visible
if entry.is_displayed():
unexpected.add(entry.text)
unexpected.add(entry_text)
self.assertEqual(expected, set(), "Missing entries for expected iterim meetings.")
self.assertEqual(not_visible, set(), "Hidden rows for expected interim meetings.")
@ -754,6 +932,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'
@ -762,32 +941,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"""
@ -795,24 +1093,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,179 +739,13 @@ 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_area(self):
meeting = make_meeting_test_data()
mars = Group.objects.get(acronym='mars')
area = mars.parent
self.do_ical_filter_test(
meeting,
querystring='?show=%s' % area.acronym,
querystring='?show=plenary,secretariat,ames&hide=reg',
expected_session_summaries=[
'Morning Break',
'IETF Plenary',
'ames - Asteroid Mining Equipment Standardization Group',
'mars - Martian Special Interest Group',
]
)
def test_ical_filter_hide_area(self):
meeting = make_meeting_test_data()
mars = Group.objects.get(acronym='mars')
area = mars.parent
self.do_ical_filter_test(
meeting,
querystring='?show=mars&hide=%s' % area.acronym,
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',
expected_session_summaries=[
'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 test_ical_filter_showtypes(self):
meeting = make_meeting_test_data()
# Show break/plenary types
self.do_ical_filter_test(
meeting,
querystring='?showtypes=break,plenary',
expected_session_summaries=[
'IETF Plenary',
'Morning Break',
]
)
def test_ical_filter_hidetypes(self):
meeting = make_meeting_test_data()
self.do_ical_filter_test(
meeting,
querystring='?hidetypes=plenary',
expected_session_summaries=[]
)
def test_ical_filter_showtypes_and_hidetypes(self):
meeting = make_meeting_test_data()
self.do_ical_filter_test(
meeting,
querystring='?showtypes=break&hidetypes=plenary',
expected_session_summaries=[
'Morning Break',
]
)
def test_ical_filter_showtypes_and_hidetypes_same_type(self):
meeting = make_meeting_test_data()
self.do_ical_filter_test(
meeting,
querystring='?showtypes=plenary&hidetypes=plenary',
expected_session_summaries=[]
)
def test_ical_filter_show_and_showtypes(self):
meeting = make_meeting_test_data()
self.do_ical_filter_test(
meeting,
querystring='?show=mars&showtypes=plenary',
expected_session_summaries=[
'IETF Plenary',
'mars - Martian Special Interest Group',
]
)
def test_ical_filter_hide_and_showtypes(self):
meeting = make_meeting_test_data()
self.do_ical_filter_test(
meeting,
querystring='?hide=ames&showtypes=regular',
expected_session_summaries=[
'mars - Martian Special Interest Group',
]
)
def test_ical_filter_show_and_hidetypes(self):
meeting = make_meeting_test_data()
self.do_ical_filter_test(
meeting,
querystring='?show=ietf,mars&hidetypes=plenary',
expected_session_summaries=[
'mars - Martian Special Interest Group',
]
)
def test_ical_filter_hide_and_hidetypes(self):
meeting = make_meeting_test_data()
self.do_ical_filter_test(
meeting,
querystring='?hide=ietf,mars&hidetypes=plenary',
expected_session_summaries=[]
)
def test_ical_filter_show_hide_and_showtypes(self):
meeting = make_meeting_test_data()
# ames regular session should be suppressed
self.do_ical_filter_test(
meeting,
querystring='?show=ietf&hide=ames&showtypes=regular',
expected_session_summaries=[
'IETF Plenary',
'mars - Martian Special Interest Group',
]
)
def test_ical_filter_show_hide_and_hidetypes(self):
meeting = make_meeting_test_data()
# ietf plenary session should be suppressed
self.do_ical_filter_test(
meeting,
querystring='?show=mars,ietf&hide=ames&hidetypes=plenary',
expected_session_summaries=[
'mars - Martian Special Interest Group',
]
)
def test_ical_filter_all_params(self):
meeting = make_meeting_test_data()
# should include Morning Break / Registration due to secretariat in show list
# should include mars SIG because regular in showtypes list
# should not include IETF plenary because plenary in hidetypes list
# should not show ames SIG because ames in hide list
self.do_ical_filter_test(
meeting,
querystring='?show=secretariat,ietf&hide=ames&showtypes=regular&hidetypes=plenary',
expected_session_summaries=[
'Morning Break',
'Registration',
'mars - Martian Special Interest Group',
]
)
@ -2197,141 +2034,30 @@ class InterimTests(TestCase):
# test_upcoming_filters_ignored removed - we _don't_ want to ignore filters now, and the test passed because it wasn't testing the filtering anyhow (which requires testing the js).
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)
# Expect events for important dates plus 4 - 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())
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_ical_filter_showtypes(self):
r = self.do_upcoming_ical_test('showtypes=regular')
assert_ical_response_is_valid(self, r,
expected_event_summaries=[
'ames - Asteroid Mining Equipment Standardization Group',
'mars - Martian Special Interest Group',
'IETF 72',
],
expected_event_count=3)
def test_upcoming_ical_filter_hidetypes(self):
r = self.do_upcoming_ical_test('hidetypes=regular')
assert_ical_response_is_valid(self, r, expected_event_summaries=['IETF 72'])
def test_upcoming_ical_filter_showtypes_and_hidetypes(self):
r = self.do_upcoming_ical_test('showtypes=plenary,regular&hidetypes=regular')
assert_ical_response_is_valid(self, r,
expected_event_summaries=[
'sg - Some Group',
'IETF 72',
],
expected_event_count=2)
def test_upcoming_ical_filter_show_and_showtypes(self):
r = self.do_upcoming_ical_test('show=mars&showtypes=plenary')
assert_ical_response_is_valid(self, r,
expected_event_summaries=[
'mars - Martian Special Interest Group',
'sg - Some Group',
'IETF 72',
],
expected_event_count=3)
url = urlreverse("ietf.meeting.views.upcoming_ical")
def test_upcoming_ical_filter_show_and_hidetypes(self):
r = self.do_upcoming_ical_test('show=mars,sg&hidetypes=regular')
assert_ical_response_is_valid(self, r,
expected_event_summaries=[
'sg - Some Group',
'IETF 72',
],
expected_event_count=2)
r = self.client.get(url)
def test_upcoming_ical_filter_hide_and_showtypes(self):
r = self.do_upcoming_ical_test('hide=mars&showtypes=regular')
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',
'IETF 72',
],
expected_event_count=2)
def test_upcoming_ical_filter_hide_and_hidetypes(self):
r = self.do_upcoming_ical_test('hide=mars&hidetypes=regular')
assert_ical_response_is_valid(self, r, expected_event_summaries=['IETF 72'], expected_event_count=1)
expected_event_count=3 + meeting.importantdate_set.count())
def test_upcoming_ical_filter_show_hide_and_showtypes(self):
r = self.do_upcoming_ical_test('show=ames&hide=mars&showtypes=regular,plenary')
assert_ical_response_is_valid(self, r,
expected_event_summaries=[
'ames - Asteroid Mining Equipment Standardization Group',
'sg - Some Group',
'IETF 72',
],
expected_event_count=3)
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')
def test_upcoming_ical_filter_show_hide_and_hidetypes(self):
r = self.do_upcoming_ical_test('show=ames,sg&hide=mars&hidetypes=regular')
assert_ical_response_is_valid(self, r,
expected_event_summaries=[
'sg - Some Group',
'IETF 72'
],
expected_event_count=2)
def test_upcoming_ical_filter_all_params(self):
r = self.do_upcoming_ical_test('show=sg&hide=ames&showtypes=regular&hidetypes=plenary')
self.assertEqual(r.status_code, 200)
assert_ical_response_is_valid(self, r,
expected_event_summaries=[
'mars - Martian Special Interest Group',
@ -2339,27 +2065,16 @@ class InterimTests(TestCase):
],
expected_event_count=2)
def test_upcoming_ical_filter_show_area(self):
make_meeting_test_data(create_interims=True)
mars = Group.objects.get(acronym='mars')
area = mars.parent
r = self.do_upcoming_ical_test('show=%s' % area.acronym,
create_meeting=False)
assert_ical_response_is_valid(self, r,
expected_event_summaries=[
'ames - Asteroid Mining Equipment Standardization Group',
'mars - Martian Special Interest Group',
'IETF 72',
],
expected_event_count=3)
def test_upcoming_ical_filter_hide_area(self):
make_meeting_test_data(create_interims=True)
mars = Group.objects.get(acronym='mars')
area = mars.parent
r = self.do_upcoming_ical_test('show=mars&hide=%s' % area.acronym,
create_meeting=False)
assert_ical_response_is_valid(self, r, expected_event_summaries=['IETF 72'], expected_event_count=1)
def test_upcoming_ical_filter_invalid_syntaxes(self):
make_meeting_test_data()
url = urlreverse('ietf.meeting.views.upcoming_ical')
r = self.client.get(url + '?unknownparam=mars')
self.assertEqual(r.status_code, 400, 'Unknown parameter should be rejected')
r = self.client.get(url + '?mars')
self.assertEqual(r.status_code, 400, 'Missing parameter name should be rejected')
def test_upcoming_json(self):
make_meeting_test_data(create_interims=True)
@ -3872,3 +3587,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

@ -66,6 +66,7 @@ from ietf.meeting.helpers import get_wg_list, find_ads_for_meeting
from ietf.meeting.helpers import get_meeting, get_ietf_meeting, get_current_ietf_meeting_num
from ietf.meeting.helpers import get_schedule, schedule_permissions
from ietf.meeting.helpers import preprocess_assignments_for_agenda, read_agenda_file
from ietf.meeting.helpers import filter_keywords_for_session, tag_assignments_with_filter_keywords
from ietf.meeting.helpers import convert_draft_to_pdf, get_earliest_session_date
from ietf.meeting.helpers import can_view_interim_request, can_approve_interim_request
from ietf.meeting.helpers import can_edit_interim_request
@ -1338,6 +1339,7 @@ def agenda(request, num=None, name=None, base=None, ext=None, owner=None, utc=""
timeslot__type__private=False,
)
filtered_assignments = preprocess_assignments_for_agenda(filtered_assignments, meeting)
tag_assignments_with_filter_keywords(filtered_assignments)
if ext == ".csv":
return agenda_csv(schedule, filtered_assignments)
@ -1366,12 +1368,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,
@ -1641,7 +1718,8 @@ def week_view(request, num=None, name=None, owner=None):
# saturday_after = saturday_before + datetime.timedelta(days=7)
# filtered_assignments = filtered_assignments.filter(timeslot__time__gte=saturday_before,timeslot__time__lt=saturday_after)
filtered_assignments = preprocess_assignments_for_agenda(filtered_assignments, meeting)
tag_assignments_with_filter_keywords(filtered_assignments)
items = []
for a in filtered_assignments:
# we don't HTML escape any of these as the week-view code is using createTextNode
@ -1657,7 +1735,8 @@ def week_view(request, num=None, name=None, owner=None):
day_of_month=a.timeslot.time.strftime("%d").lstrip("0"),
year=a.timeslot.time.strftime("%Y"),
),
"type": a.timeslot.type.name
"type": a.timeslot.type.name,
"filter_keywords": ",".join(a.filter_keywords),
}
if a.session:
@ -1790,6 +1869,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
@ -1801,31 +1881,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
@ -1857,6 +1915,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)
@ -3261,13 +3320,27 @@ 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)
entries = list(ietf_meetings)
entries.extend(list(interim_sessions))
entries.sort(key = lambda o: pytz.utc.localize(datetime.datetime.combine(o.date, datetime.datetime.min.time())) if isinstance(o,Meeting) else o.official_timeslotassignment().timeslot.utc_start_time())
# add menu entries
menu_entries = get_interim_menu_entries(request)
selected_menu_entry = 'upcoming'
@ -3293,7 +3366,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,
@ -3327,6 +3400,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

@ -1,16 +1,11 @@
var agenda_filter_for_testing = {}; // methods to be accessed for automated testing
var agenda_filter = function () {
var agenda_filter; // public interface
var agenda_filter_for_testing; // methods to be accessed for automated testing
// closure to create private scope
(function () {
'use strict'
var update_callback // function(filter_params)
var enable_non_area = false // if true, show the non-area filters
/* Add to list without duplicates */
function add_list_item (list, item) {
if (list.indexOf(item) === -1) {
list.push(item);
}
}
var update_callback; // function(filter_params)
/* Remove from list, if present */
function remove_list_item (list, item) {
@ -20,13 +15,18 @@ var agenda_filter = function () {
}
}
/* Add to list if not present, remove if present */
/* Add to list if not present, remove if present
*
* Returns true if added to the list, otherwise false.
*/
function toggle_list_item (list, item) {
var item_index = list.indexOf(item);
if (item_index === -1) {
list.push(item)
return true;
} else {
list.splice(item_index, 1)
return false;
}
}
@ -48,101 +48,83 @@ var agenda_filter = function () {
if (!qparams[filt] || (qparams[filt] === true)) {
return [];
}
return $.map(qparams[filt].split(','), function(s){return s.trim();});
var result = [];
var qp = qparams[filt].split(',');
for (var ii = 0; ii < qp.length; ii++) {
result.push(qp[ii].trim());
}
return result;
}
function get_filter_params (qparams) {
var enabled = !!(qparams.show || qparams.hide || qparams.showtypes || qparams.hidetypes);
var enabled = !!(qparams.show || qparams.hide);
return {
enabled: enabled,
show_groups: get_filter_from_qparams(qparams, 'show'),
hide_groups: get_filter_from_qparams(qparams, 'hide'),
show_types: get_filter_from_qparams(qparams, 'showtypes'),
hide_types: get_filter_from_qparams(qparams, 'hidetypes'),
show: get_filter_from_qparams(qparams, 'show'),
hide: get_filter_from_qparams(qparams, 'hide')
}
}
function filtering_is_enabled (filter_params) {
return filter_params['enabled'];
function get_keywords(elt) {
var keywords = $(elt).attr('data-filter-keywords');
if (keywords) {
return keywords.toLowerCase().split(',');
}
return [];
}
function get_area_items (area) {
var types = [];
var groups = [];
var neg_groups = [];
function get_item(elt) {
return $(elt).attr('data-filter-item');
}
$('.view.' + area).find('button').each(function (index, elt) {
elt = $(elt) // jquerify
var item = elt.text().trim().toLowerCase()
if (elt.hasClass('picktype')) {
types.push(item)
} else if (elt.hasClass('pickview')) {
groups.push(item);
} else if (elt.hasClass('pickviewneg')) {
neg_groups.push(item)
// utility method - is there a match between two lists of keywords?
function keyword_match(list1, list2) {
for (var ii = 0; ii < list1.length; ii++) {
if (list2.indexOf(list1[ii]) !== -1) {
return true;
}
}
return false;
}
// Find the items corresponding to a keyword
function get_items_with_keyword (keyword) {
var items = [];
$('.view button.pickview').filter(function(index, elt) {
return keyword_match(get_keywords(elt), [keyword]);
}).each(function (index, elt) {
items.push(get_item($(elt)));
});
return { 'groups': groups, 'neg_groups': neg_groups, 'types': types };
return items;
}
function filtering_is_enabled (filter_params) {
return filter_params.enabled;
}
// Update the filter / customization UI to match the current filter parameters
function update_filter_ui (filter_params) {
var area_group_buttons = $('.view .pickview, .pick-area');
var non_area_header_button = $('button.pick-non-area');
var non_area_type_buttons = $('.view.non-area .picktype');
var non_area_group_buttons = $('.view.non-area button.pickviewneg');
var buttons = $('.pickview');
if (!filtering_is_enabled(filter_params)) {
// Not filtering - set everything to defaults and exit
area_group_buttons.removeClass('active');
non_area_header_button.removeClass('active');
non_area_type_buttons.removeClass('active');
non_area_group_buttons.removeClass('active');
non_area_group_buttons.addClass('disabled');
// Not filtering - set to default and exit
buttons.removeClass('active');
return;
}
// show the customizer - it will stay visible even if filtering is disabled
$('#customize').collapse('show')
// Group and area buttons - these are all positive selections
area_group_buttons.each(function (index, elt) {
// Update button state to match visibility
buttons.each(function (index, elt) {
elt = $(elt);
var item = elt.text().trim().toLowerCase();
var area = elt.attr('data-group-area');
if ((filter_params['hide_groups'].indexOf(item) === -1) // not hidden...
&& ((filter_params['show_groups'].indexOf(item) !== -1) // AND shown...
|| (area && (filter_params['show_groups'].indexOf(area.trim().toLowerCase()) !== -1))) // OR area shown
) {
elt.addClass('active');
} else {
elt.removeClass('active');
}
});
// Non-area buttons need special handling. Only have positive type and negative group buttons.
// Assume non-area heading is disabled, then enable if one of the types is active
non_area_header_button.removeClass('active');
non_area_group_buttons.addClass('disabled');
non_area_type_buttons.each(function (index, elt) {
// Positive type selection buttons
elt = $(elt);
var item = elt.text().trim().toLowerCase();
if ((filter_params['show_types'].indexOf(item) !== -1)
&& (filter_params['hide_types'].indexOf(item) === -1)){
elt.addClass('active');
non_area_header_button.addClass('active');
non_area_group_buttons.removeClass('disabled');
} else {
elt.removeClass('active');
}
});
non_area_group_buttons.each(function (index, elt) {
// Negative group selection buttons
elt = $(elt);
var item = elt.text().trim().toLowerCase();
if (filter_params['hide_groups'].indexOf(item) === -1) {
var keywords = get_keywords(elt);
keywords.push(get_item(elt)); // treat item as one of its keywords
var hidden = keyword_match(filter_params.hide, keywords);
var shown = keyword_match(filter_params.show, keywords);
if (shown && !hidden) {
elt.addClass('active');
} else {
elt.removeClass('active');
@ -163,7 +145,6 @@ var agenda_filter = function () {
}
}
/* Trigger an update so the user will see the page appropriate for given filter_params
*
* Updates the URL to match filter_params, then updates the history / display to match
@ -172,17 +153,11 @@ var agenda_filter = function () {
function update_filters (filter_params) {
var qparams = []
var search = ''
if (filter_params['show_groups'].length > 0) {
qparams.push('show=' + filter_params['show_groups'].join())
if (filter_params.show.length > 0) {
qparams.push('show=' + filter_params.show.join())
}
if (filter_params['hide_groups'].length > 0) {
qparams.push('hide=' + filter_params['hide_groups'].join())
}
if (filter_params['show_types'].length > 0) {
qparams.push('showtypes=' + filter_params['show_types'].join())
}
if (filter_params['hide_types'].length > 0) {
qparams.push('hidetypes=' + filter_params['hide_types'].join())
if (filter_params.hide.length > 0) {
qparams.push('hide=' + filter_params.hide.join())
}
if (qparams.length > 0) {
search = '?' + qparams.join('&')
@ -202,27 +177,40 @@ var agenda_filter = function () {
/* 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_groups, show_types, etc)
*/
function handle_pick_button (elt, param_type) {
var area = elt.attr('data-group-area');
var item = elt.text().trim().toLowerCase();
function handle_pick_button (elt) {
var fp = get_filter_params(parse_query_params(window.location.search));
var neg_param_type = {
show_groups: 'hide_groups',
hide_groups: 'show_groups',
show_types: 'hide_types',
hide_types: 'show_types'
}[param_type];
var item = get_item(elt);
if (area && (fp[param_type].indexOf(area.trim().toLowerCase()) !== -1)) {
// Area is shown - toggle hide list
toggle_list_item(fp[neg_param_type], item);
remove_list_item(fp[param_type], item);
/* Normally toggle in and out of the 'show' list. If this item is active because
* one of its keywords is active, invert the sense and toggle in and out of the
* 'hide' list instead. */
var inverted = keyword_match(fp.show, get_keywords(elt));
var just_showed_item = false;
if (inverted) {
toggle_list_item(fp.hide, item);
remove_list_item(fp.show, item);
} else {
toggle_list_item(fp[param_type], item);
remove_list_item(fp[neg_param_type], item);
just_showed_item = toggle_list_item(fp.show, item);
remove_list_item(fp.hide, item);
}
/* If we just showed an item, remove its children from the
* show/hide lists to keep things consistent. This way, selecting
* an area will enable all items in the row as one would expect. */
if (just_showed_item) {
var children = get_items_with_keyword(item);
$.each(children, function(index, child) {
remove_list_item(fp.show, child);
remove_list_item(fp.hide, child);
});
}
// If the show list is empty, clear the hide list because there is nothing to hide
if (fp.show.length === 0) {
fp.hide = [];
}
return fp;
}
@ -230,88 +218,48 @@ var agenda_filter = function () {
return elt.hasClass('disabled');
}
// Various "pick" button handlers
$('.pickview').click(function () {
if (is_disabled($(this))) { return; }
update_filters(handle_pick_button($(this), 'show_groups'))
});
$('.pickviewneg').click(function () {
if (is_disabled($(this))) { return; }
update_filters(handle_pick_button($(this), 'hide_groups'))
});
$('.picktype').click(function () {
if (is_disabled($(this))) { return; }
var fp = handle_pick_button($(this), 'show_types')
// If we just disabled the last non-area type, clear out the hide groups list.
var items = get_area_items('non-area')
var any_left = false
$.each(items.types, function (index, session_type) {
if (fp['show_types'].indexOf(session_type) !== -1) {
any_left = true
}
})
if (!any_left) {
fp['hide_groups'] = []
}
update_filters(fp);
});
// Click handler for an area header button
$('.pick-area').click(function() {
if (is_disabled($(this))) { return; }
var fp = handle_pick_button($(this), 'show_groups');
var items = get_area_items($(this).text().trim().toLowerCase());
// Clear all the individual group show/hide options
$.each(items.groups, function(index, group) {
remove_list_item(fp['show_groups'], group);
remove_list_item(fp['hide_groups'], group);
function register_handlers() {
$('.pickview').click(function () {
if (is_disabled($(this))) { return; }
var fp = handle_pick_button($(this));
update_filters(fp);
});
update_filters(fp);
});
// Click handler for the "Non-Area" header button
$('.pick-non-area').click(function () {
var items = get_area_items('non-area');
var fp = get_filter_params(parse_query_params(window.location.search))
if ($(this).hasClass('active')) {
// Were active - disable or hide everything
$.each(items.types, function (index, session_type) {
remove_list_item(fp['show_types'], session_type)
})
// When no types are shown, no need to hide groups. Empty hide_groups list.
fp['hide_groups'] = []
} else {
// Were not active - enable or stop hiding everything
$.each(items.types, function (index, session_type) {
add_list_item(fp['show_types'], session_type)
})
$.each(items.neg_groups, function (index, group) {
remove_list_item(fp['hide_groups'], group)
})
}
update_filters(fp);
});
// Entry point to filtering code when page loads
}
/* Entry point to filtering code when page loads
*
* This must be called if you are using the HTML template to provide a customization
* button UI. Do not call if you only want to use the parameter parsing routines.
*/
function enable () {
$(document).ready(function () {
update_view()
register_handlers();
update_view();
})
}
// utility method - filter a jquery set to those matching a keyword
function rows_matching_filter_keyword(rows, kw) {
return rows.filter(function(index, element) {
var row_kws = get_keywords(element);
return keyword_match(row_kws, [kw.toLowerCase()]);
});
}
// Make private functions available for unit testing
agenda_filter_for_testing.toggle_list_item = toggle_list_item;
agenda_filter_for_testing.parse_query_params = parse_query_params;
agenda_filter_for_testing = {
parse_query_params: parse_query_params,
toggle_list_item: toggle_list_item
};
// Public interface methods
return {
// Make public interface methods accessible
agenda_filter = {
enable: enable,
filtering_is_enabled: filtering_is_enabled,
include_non_area_selectors: function () {enable_non_area = true},
get_filter_params: get_filter_params,
keyword_match: keyword_match,
parse_query_params: parse_query_params,
rows_matching_filter_keyword: rows_matching_filter_keyword,
set_update_callback: function (cb) {update_callback = cb}
}
}();
};
})();

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 %}
@ -125,10 +132,7 @@
{% endif %}
{% if item.timeslot.type.slug == 'break' or item.timeslot.type.slug == 'reg' or item.timeslot.type.slug == 'other' %}
<tr id="row-{{ item.slug }}"
data-item-group="{% if item.session.historic_group %}{{ item.session.historic_group.acronym }}{% endif %}"
data-item-area="{% if item.session.historic_group and item.session.historic_group.historic_parent %}{{ item.session.historic_group.historic_parent.acronym }}{% endif %}"
data-timeslot-type="{{item.timeslot.type.slug}}">
<tr id="row-{{ item.slug }}" data-filter-keywords="{{ item.filter_keywords|join:',' }}">
<td class="text-nowrap text-right">
<span class="hidden-xs">
{% include "meeting/timeslot_start_end.html" %}
@ -187,11 +191,9 @@
{% if item.timeslot.type_id == 'regular' or item.timeslot.type.slug == 'plenary' %}
{% if item.session.historic_group %}
<tr id="row-{{item.slug}}"
data-item-group="{{ item.session.historic_group.acronym }}"
data-item-area="{% if item.session.historic_group.historic_parent %}{{ item.session.historic_group.historic_parent.acronym }}{% endif %}"
data-timeslot-type="{{item.timeslot.type.slug}}"
data-ske="row-{{ item.slug }}" {% if item.timeslot.type.slug == 'plenary' %}class="{{item.timeslot.type.slug}}danger"{% endif %}>
<tr id="row-{{item.slug}}"
{% if item.timeslot.type.slug == 'plenary' %}class="{{item.timeslot.type.slug}}danger"{% endif %}
data-filter-keywords="{{ item.filter_keywords|join:',' }}">
{% if item.timeslot.type.slug == 'plenary' %}
<th class="text-nowrap text-right">
<span class="hidden-xs">
@ -328,26 +330,18 @@
}
// if groups were selected for filtering, hide all rows by default
agenda_rows.hide();
agenda_rows.filter(function(index, row) {
return !!$(row).attr('data-filter-keywords');
}).hide();
// loop through the has items and change the UI element and row visibilities accordingly
$.each(filter_params['show_groups'], function (i, v) {
$.each(filter_params.show, function (i, v) {
// this is a regular item by wg: when present, show these rows
agenda_rows.filter('[data-item-group="'+ v +'"]').show();
agenda_rows.filter('[data-item-area="'+ v +'"]').show();
agenda_filter.rows_matching_filter_keyword(agenda_rows, v).show();
});
$.each(filter_params['show_types'], function (i, v) {
// this is a regular item by type: when present, show these rows
agenda_rows.filter('[data-timeslot-type*="' + v + '"]').show();
});
$.each(filter_params['hide_groups'], function (i, v) {
$.each(filter_params.hide, function (i, v) {
// this is a "negative" item by wg: when present, hide these rows
agenda_rows.filter('[data-item-group="'+ v +'"]').hide();
agenda_rows.filter('[data-item-area="'+ v +'"]').hide();
});
$.each(filter_params['hide_types'], function (i, v) {
// this is a "negative" item by type: when present, hide these rows
agenda_rows.filter('[data-timeslot-type*="' + v + '"]').hide();
agenda_filter.rows_matching_filter_keyword(agenda_rows, v).hide();
});
}
@ -381,7 +375,7 @@
wv_window.history.replaceState({}, '', new_url);
wv_window.draw_calendar()
} else {
// ho history.replaceState, page reload required
// either have not yet loaded the iframe or we do not support history replacement
wv_iframe.src = new_url;
}
}

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,85 +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 pick-area {{ p.acronym|lower }}">{{ p.acronym|upper }}</button>
</th>
{% endfor %}
{% if non_area_filters %}
<th style="width:{% widthratio 1 group_parents|length|add:1 100 %}">
<button class="btn btn-default btn-block pick-non-area">Non-Area</button>
</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-group-area="{{ p.acronym|lower }}">
{% 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 picktype plenary">Plenary</button>
</div>
<div class="btn-group btn-group-xs btn-group-justified">
<button class="btn btn-default picktype other">Other</button>
</div>
</div>
<div class="btn-group-vertical btn-block">
<div class="btn-group btn-group-xs btn-group-justified">
<button class="btn btn-default pickviewneg edu"> EDU</button>
</div>
<div class="btn-group btn-group-xs btn-group-justified">
<button class="btn btn-default pickviewneg hackathon"> Hackathon</button>
</div>
<div class="btn-group btn-group-xs btn-group-justified">
<button class="btn btn-default pickviewneg iab"> IAB</button>
</div>
<div class="btn-group btn-group-xs btn-group-justified">
<button class="btn btn-default pickviewneg iepg"> IEPG</button>
</div>
<div class="btn-group btn-group-xs btn-group-justified">
<button class="btn btn-default pickviewneg iesg"> IESG</button>
</div>
<div class="btn-group btn-group-xs btn-group-justified">
<button class="btn btn-default pickviewneg ietf"> IETF</button>
</div>
<div class="btn-group btn-group-xs btn-group-justified">
<button class="btn btn-default pickviewneg secretariat"> Secretariat
</button>
</div>
<div class="btn-group btn-group-xs btn-group-justified">
<button class="btn btn-default pickviewneg 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">
@ -66,9 +66,8 @@
</thead>
<tbody>
{% for entry in entries %}
<tr class="entry"
data-item-group="{% if entry.group %}{{ entry.group.acronym|lower }}{% endif %}"
data-item-area="{% if entry.group and entry.group.parent %}{{ entry.group.parent.acronym|lower }}{% endif %}">
<tr class="entry"
{% if entry|classname == 'Session' %}data-filter-keywords="{{ entry.filter_keywords|join:',' }}"{% endif %}>
{% if entry|classname == 'Meeting' %}
{% with meeting=entry %}
<td>{{ meeting.date }} - {{ meeting.end }}</td>
@ -77,11 +76,11 @@
<td></td>
{% endwith %}
{% elif entry|classname == 'Session' %}
{% with session=entry meeting=entry.meeting%}
{% with session=entry group=entry.group meeting=entry.meeting%}
<td>{{ session.official_timeslotassignment.timeslot.utc_start_time | date:"Y-m-d H:i"}} - {{ session.official_timeslotassignment.timeslot.utc_end_time | date:"H:i e" }}</td>
<td><a href="{% url 'ietf.group.views.group_home' acronym=session.group.acronym %}">{{ session.group.acronym }}</a></td>
<td><a href="{% url 'ietf.group.views.group_home' acronym=group.acronym %}">{{ group.acronym }}</a></td>
<td>
<a class="interim-meeting-link" href="{% url 'ietf.meeting.views.session_details' num=session.meeting.number acronym=session.group.acronym %}">{{ session.meeting.number }}</a>
<a class="interim-meeting-link" href="{% url 'ietf.meeting.views.session_details' num=meeting.number acronym=group.acronym %}"> {{ meeting.number }}</a>
</td>
{% if session.current_status == 'canceled' %}
<td class='text-right'>
@ -134,7 +133,7 @@
{
title: '{{session.official_timeslotassignment.timeslot.utc_start_time|date:"H:i"}}-{{session.official_timeslotassignment.timeslot.utc_end_time|date:"H:i"}}',
group: '{% if session.group %}{{session.group.acronym}}{% endif %}',
area: '{% if session.group and session.group.parent %}{{ session.group.parent.acronym }}{% endif %}',
filter_keywords: ["{{ session.filter_keywords|join:'","' }}"],
start: '{{session.official_timeslotassignment.timeslot.utc_start_time | date:"Y-m-d H:i"}}',
end: '{{session.official_timeslotassignment.timeslot.utc_end_time | date:"Y-m-d H:i"}}',
url: '{% url 'ietf.meeting.views.session_details' num=session.meeting.number acronym=session.group.acronym %}'
@ -148,29 +147,14 @@
// Test whether an event should be visible given a set of filter parameters
function calendar_event_visible(filter_params, event) {
// Visible if filtering is disabled or event has no group
if (!agenda_filter.filtering_is_enabled(filter_params) || !event.group) {
// Visible if filtering is disabled or event has no keywords
if (!agenda_filter.filtering_is_enabled(filter_params) || !event.filter_keywords) {
return true;
}
// Exclude if group or area is in the hide_groups list
if (filter_params['hide_groups'].indexOf(event.group) !== -1) {
return false;
}
if (event.area && (filter_params['hide_groups'].indexOf(event.area) !== -1)) {
return false;
}
// Include if group or area is in the show_groups list
if (filter_params['show_groups'].indexOf(event.group) !== -1) {
return true;
}
if (event.area && (filter_params['show_groups'].indexOf(event.area) !== -1)) {
return true;
}
// Not selected, exclude by default
return false;
// Visible if shown and not hidden
return (!agenda_filter.keyword_match(filter_params.hide, event.filter_keywords)
&& agenda_filter.keyword_match(filter_params.show, event.filter_keywords));
}
// Apply filter_params to the event list and format data for the calendar
@ -217,24 +201,22 @@
}
function update_meeting_display(filter_params) {
var meeting_rows = $("#upcoming-meeting-table tr.entry")
var meeting_rows = $("#upcoming-meeting-table tr.entry");
if (!agenda_filter.filtering_is_enabled(filter_params)) {
meeting_rows.show();
return;
}
// hide everything that has a group
meeting_rows.filter("[data-item-group!='']").hide();
// hide everything that has keywords
meeting_rows.filter(function(index, row){
return !!$(row).attr('data-filter-keywords');
}).hide();
$.each(filter_params['show_groups'], function (i, v) {
// this is a regular item by wg: when present, show these rows
meeting_rows.filter('[data-item-group="'+ v +'"]').show();
meeting_rows.filter('[data-item-area="'+ v +'"]').show();
$.each(filter_params['show'], function (i, v) {
agenda_filter.rows_matching_filter_keyword(meeting_rows, v).show();
});
$.each(filter_params['hide_groups'], function (i, v) {
// this is a "negative" item by wg: when present, hide these rows
meeting_rows.filter('[data-item-group="'+ v +'"]').hide();
meeting_rows.filter('[data-item-area="'+ v +'"]').hide();
$.each(filter_params['hide'], function (i, v) {
agenda_filter.rows_matching_filter_keyword(meeting_rows, v).hide();
});
}

View file

@ -2,6 +2,7 @@
{% load origin %}{% origin %}
{% load static %}
<html> <head>
<script src="{% static 'ietf/js/agenda/agenda_filter.js' %}"></script>
<script type="text/javascript">
var all_items = {{ items|safe }};
@ -106,62 +107,17 @@
}
}
}
//===========================================================================
function parse_query_params(qs) {
var params = {};
qs = qs.replace(/^\?/, '').toLowerCase();
if (qs) {
var param_strs = qs.split('&');
for (var ii = 0; ii < param_strs.length; ii++) {
var toks = param_strs[ii].split('=', 2)
params[toks[0]] = toks[1] || true;
}
}
return params;
}
//===========================================================================
function get_filter_from_qparams(qparams, filt) {
return qparams[filt] ? qparams[filt].split(',') : [];
}
function get_filter_params(qparams) {
return {
show_groups: get_filter_from_qparams(qparams, 'show'),
hide_groups: get_filter_from_qparams(qparams, 'hide'),
show_types: get_filter_from_qparams(qparams, 'showtypes'),
hide_types: get_filter_from_qparams(qparams, 'hidetypes'),
};
}
//===========================================================================
function is_visible(query_params) {
function is_visible(filter_params) {
// Returns a method to filter objects for visibility
// Accepts show, hide, showtypes, and hidetypes filters. Also accepts
// Accepts show and hide filters. No longer accepts
// '@<state>' to show sessions in a particular state (e.g., @bof).
// Current types are:
// Session, Other, Break, Plenary
var fp = get_filter_params(query_params);
return function (item) {
var item_group = (item.group || '').toLowerCase();
var item_type = (item.type || '').toLowerCase();
var item_area = (item.area || '').toLowerCase();
var item_state = (item.state || '').toLowerCase();
if ((fp['hide_groups'].indexOf(item_group) >= 0) ||
(fp['hide_groups'].indexOf(item_area) >= 0) ||
(fp['hide_types'].indexOf(item_type) >= 0)) {
return false;
}
return ((fp['show_groups'].indexOf(item_group) >= 0) ||
(fp['show_groups'].indexOf(item_area) >= 0) ||
(fp['show_types'].indexOf(item_type) >= 0) ||
query_params['@'+item_state]);
var filter_keywords = item.filter_keywords.split(',');
return (!agenda_filter.keyword_match(filter_keywords, filter_params.hide)
&& agenda_filter.keyword_match(filter_keywords, filter_params.show));
}
}
@ -172,9 +128,12 @@
var height = document.body.clientHeight;
var visible_items = all_items;
var qs = window.location.search;
if (qs.length > 1) {
visible_items = visible_items.filter(is_visible(parse_query_params(qs)));
var filter_params = agenda_filter.get_filter_params(
agenda_filter.parse_query_params(window.location.search)
);
if (agenda_filter.filtering_is_enabled(filter_params)) {
visible_items = visible_items.filter(is_visible(filter_params));
}
var start_day;
@ -539,9 +498,13 @@
//===========================================================================
// Set up events for drawing the calendar
window.addEventListener("resize", draw_calendar, false);
window.addEventListener("load", draw_calendar, false);
window.addEventListener("hashchange", draw_calendar, false);
function redraw_weekview() {
draw_calendar();
}
window.addEventListener("resize", redraw_weekview, false);
window.addEventListener("load", redraw_weekview, false);
window.addEventListener("hashchange", redraw_weekview, false);
</script>
</head>

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