Use reworked filtering for ical agendas; refactor filter UI with office hours buttons and nicer formatting
- Legacy-Id: 18619
This commit is contained in:
parent
1b1bc24744
commit
d67b298512
|
@ -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):
|
||||
|
|
20
ietf/meeting/templatetags/agenda_filter_tags.py
Normal file
20
ietf/meeting/templatetags/agenda_filter_tags.py
Normal 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)
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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/')
|
||||
|
|
Loading…
Reference in a new issue