Skip to content

Commit b1e3c1f

Browse files
Use new querystring format for agenda_ical and add tests. Ensure querystring precedes URL fragment in agenda JS.
- Legacy-Id: 18413
1 parent ea38820 commit b1e3c1f

3 files changed

Lines changed: 301 additions & 49 deletions

File tree

ietf/meeting/tests_views.py

Lines changed: 236 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ def test_meeting_agenda(self):
197197

198198
# iCal
199199
r = self.client.get(urlreverse("ietf.meeting.views.agenda_ical", kwargs=dict(num=meeting.number))
200-
+ "?" + session.group.parent.acronym.upper())
200+
+ "?show=" + session.group.parent.acronym.upper())
201201
self.assertContains(r, session.group.acronym)
202202
self.assertContains(r, session.group.name)
203203
self.assertContains(r, slot.location.name)
@@ -580,6 +580,28 @@ def test_important_dates(self):
580580
post_date = meeting.importantdate_set.get(name=idn).date
581581
self.assertEqual(pre_date, post_date+datetime.timedelta(days=1))
582582

583+
def assert_ical_response_is_valid(self, response, expected_event_summaries=None, expected_event_count=None):
584+
"""Validate an HTTP response containing iCal data
585+
586+
Based on RFC2445, but not exhaustive by any means. Assumes a single iCalendar object.
587+
"""
588+
self.assertEqual(response.get('Content-Type'), "text/calendar")
589+
590+
# Validate iCalendar object
591+
self.assertContains(response, 'BEGIN:VCALENDAR', count=1)
592+
self.assertContains(response, 'END:VCALENDAR', count=1)
593+
self.assertContains(response, 'PRODID:', count=1)
594+
self.assertContains(response, 'VERSION', count=1)
595+
596+
# Validate event objects
597+
if expected_event_count is None:
598+
expected_event_count = len(expected_event_summaries)
599+
self.assertContains(response, 'BEGIN:VEVENT', count=expected_event_count)
600+
self.assertContains(response, 'END:VEVENT', count=expected_event_count)
601+
self.assertContains(response, 'UID', count=expected_event_count)
602+
for summary in expected_event_summaries:
603+
self.assertContains(response, 'SUMMARY:' + summary)
604+
583605
def test_group_ical(self):
584606
meeting = make_meeting_test_data()
585607
s1 = Session.objects.filter(meeting=meeting, group__acronym="mars").first()
@@ -592,23 +614,226 @@ def test_group_ical(self):
592614
#
593615
url = urlreverse('ietf.meeting.views.agenda_ical', kwargs={'num':meeting.number, 'acronym':s1.group.acronym, })
594616
r = self.client.get(url)
595-
self.assertEqual(r.get('Content-Type'), "text/calendar")
596-
self.assertContains(r, 'BEGIN:VEVENT')
597-
self.assertEqual(r.content.count(b'UID'), 2)
598-
self.assertContains(r, 'SUMMARY:mars - Martian Special Interest Group')
617+
self.assert_ical_response_is_valid(r,
618+
expected_event_summaries=['mars - Martian Special Interest Group'],
619+
expected_event_count=2)
599620
self.assertContains(r, t1.time.strftime('%Y%m%dT%H%M%S'))
600621
self.assertContains(r, t2.time.strftime('%Y%m%dT%H%M%S'))
601-
self.assertContains(r, 'END:VEVENT')
602622
#
603623
url = urlreverse('ietf.meeting.views.agenda_ical', kwargs={'num':meeting.number, 'session_id':s1.id, })
604624
r = self.client.get(url)
605-
self.assertEqual(r.get('Content-Type'), "text/calendar")
606-
self.assertContains(r, 'BEGIN:VEVENT')
607-
self.assertEqual(r.content.count(b'UID'), 1)
608-
self.assertContains(r, 'SUMMARY:mars - Martian Special Interest Group')
625+
self.assert_ical_response_is_valid(r,
626+
expected_event_summaries=['mars - Martian Special Interest Group'],
627+
expected_event_count=1)
609628
self.assertContains(r, t1.time.strftime('%Y%m%dT%H%M%S'))
610629
self.assertNotContains(r, t2.time.strftime('%Y%m%dT%H%M%S'))
611-
self.assertContains(r, 'END:VEVENT')
630+
631+
def test_meeting_agenda_has_static_ical_links(self):
632+
"""Links to the agenda_ical view must appear on the agenda page
633+
634+
Confirms that these have the correct querystrings. Does not test the JS-based
635+
'Customized schedule' button.
636+
"""
637+
meeting = make_meeting_test_data()
638+
639+
# get the agenda
640+
url = urlreverse('ietf.meeting.views.agenda', kwargs=dict(num=meeting.number))
641+
r = self.client.get(url)
642+
643+
# Check that it has the links we expect
644+
ical_url = urlreverse('ietf.meeting.views.agenda_ical', kwargs=dict(num=meeting.number))
645+
q = PyQuery(r.content)
646+
content = q('#content').html().lower() # don't care about case
647+
# Should be a 'non-area events' link showing appropriate types
648+
self.assertIn('%s?showtypes=plenary,other' % ical_url, content)
649+
assignments = meeting.schedule.assignments.exclude(timeslot__type__in=['lead', 'offagenda'])
650+
# Assume the test meeting is not using historic groups
651+
groups = [a.session.group for a in assignments if a.session is not None]
652+
for g in groups:
653+
if g.parent_id is not None:
654+
self.assertIn('%s?show=%s' % (ical_url, g.parent.acronym.lower()), content)
655+
656+
def test_ical_filter_invalid_syntaxes(self):
657+
meeting = make_meeting_test_data()
658+
url = urlreverse('ietf.meeting.views.agenda_ical', kwargs={'num':meeting.number})
659+
660+
r = self.client.get(url + '?unknownparam=mars')
661+
self.assertEqual(r.status_code, 400, 'Unknown parameter should be rejected')
662+
663+
r = self.client.get(url + '?mars')
664+
self.assertEqual(r.status_code, 400, 'Missing parameter name should be rejected')
665+
666+
def do_ical_filter_test(self, meeting, querystring, expected_session_summaries):
667+
url = urlreverse('ietf.meeting.views.agenda_ical', kwargs={'num':meeting.number})
668+
r = self.client.get(url + querystring)
669+
self.assertEqual(r.status_code, 200)
670+
self.assert_ical_response_is_valid(r, expected_event_summaries=expected_session_summaries)
671+
672+
def test_ical_filter_default(self):
673+
meeting = make_meeting_test_data()
674+
self.do_ical_filter_test(
675+
meeting,
676+
querystring='',
677+
expected_session_summaries=[
678+
'Morning Break',
679+
'Registration',
680+
'IETF Plenary',
681+
'ames - Asteroid Mining Equipment Standardization Group',
682+
'mars - Martian Special Interest Group',
683+
]
684+
)
685+
686+
def test_ical_filter_show(self):
687+
meeting = make_meeting_test_data()
688+
self.do_ical_filter_test(
689+
meeting,
690+
querystring='?show=mars',
691+
expected_session_summaries=[
692+
'mars - Martian Special Interest Group',
693+
]
694+
)
695+
696+
def test_ical_filter_hide(self):
697+
meeting = make_meeting_test_data()
698+
self.do_ical_filter_test(
699+
meeting,
700+
querystring='?hide=ietf',
701+
expected_session_summaries=[]
702+
)
703+
704+
def test_ical_filter_show_and_hide(self):
705+
meeting = make_meeting_test_data()
706+
self.do_ical_filter_test(
707+
meeting,
708+
querystring='?show=ames&hide=mars',
709+
expected_session_summaries=[
710+
'ames - Asteroid Mining Equipment Standardization Group',
711+
]
712+
)
713+
714+
def test_ical_filter_show_and_hide_same_group(self):
715+
meeting = make_meeting_test_data()
716+
self.do_ical_filter_test(
717+
meeting,
718+
querystring='?show=ames&hide=ames',
719+
expected_session_summaries=[]
720+
)
721+
722+
def test_ical_filter_showtypes(self):
723+
meeting = make_meeting_test_data()
724+
# Show break/plenary types
725+
self.do_ical_filter_test(
726+
meeting,
727+
querystring='?showtypes=break,plenary',
728+
expected_session_summaries=[
729+
'IETF Plenary',
730+
'Morning Break',
731+
]
732+
)
733+
734+
def test_ical_filter_hidetypes(self):
735+
meeting = make_meeting_test_data()
736+
self.do_ical_filter_test(
737+
meeting,
738+
querystring='?hidetypes=plenary',
739+
expected_session_summaries=[]
740+
)
741+
742+
def test_ical_filter_showtypes_and_hidetypes(self):
743+
meeting = make_meeting_test_data()
744+
self.do_ical_filter_test(
745+
meeting,
746+
querystring='?showtypes=break&hidetypes=plenary',
747+
expected_session_summaries=[
748+
'Morning Break',
749+
]
750+
)
751+
752+
def test_ical_filter_showtypes_and_hidetypes_same_type(self):
753+
meeting = make_meeting_test_data()
754+
self.do_ical_filter_test(
755+
meeting,
756+
querystring='?showtypes=plenary&hidetypes=plenary',
757+
expected_session_summaries=[]
758+
)
759+
760+
def test_ical_filter_show_and_showtypes(self):
761+
meeting = make_meeting_test_data()
762+
self.do_ical_filter_test(
763+
meeting,
764+
querystring='?show=mars&showtypes=plenary',
765+
expected_session_summaries=[
766+
'IETF Plenary',
767+
'mars - Martian Special Interest Group',
768+
]
769+
)
770+
771+
def test_ical_filter_hide_and_showtypes(self):
772+
meeting = make_meeting_test_data()
773+
self.do_ical_filter_test(
774+
meeting,
775+
querystring='?hide=ames&showtypes=regular',
776+
expected_session_summaries=[
777+
'mars - Martian Special Interest Group',
778+
]
779+
)
780+
781+
def test_ical_filter_show_and_hidetypes(self):
782+
meeting = make_meeting_test_data()
783+
self.do_ical_filter_test(
784+
meeting,
785+
querystring='?show=ietf,mars&hidetypes=plenary',
786+
expected_session_summaries=[
787+
'mars - Martian Special Interest Group',
788+
]
789+
)
790+
791+
def test_ical_filter_hide_and_hidetypes(self):
792+
meeting = make_meeting_test_data()
793+
self.do_ical_filter_test(
794+
meeting,
795+
querystring='?hide=ietf,mars&hidetypes=plenary',
796+
expected_session_summaries=[]
797+
)
798+
799+
def test_ical_filter_show_hide_and_showtypes(self):
800+
meeting = make_meeting_test_data()
801+
# ames regular session should be suppressed
802+
self.do_ical_filter_test(
803+
meeting,
804+
querystring='?show=mars&hide=ames&showtypes=plenary,regular',
805+
expected_session_summaries=[
806+
'IETF Plenary',
807+
'mars - Martian Special Interest Group',
808+
]
809+
)
810+
811+
def test_ical_filter_show_hide_and_hidetypes(self):
812+
meeting = make_meeting_test_data()
813+
# ietf plenary session should be suppressed
814+
self.do_ical_filter_test(
815+
meeting,
816+
querystring='?show=mars,ietf&hide=ames&hidetypes=plenary',
817+
expected_session_summaries=[
818+
'mars - Martian Special Interest Group',
819+
]
820+
)
821+
822+
def test_ical_filter_all_params(self):
823+
meeting = make_meeting_test_data()
824+
# should include Morning Break / Registration due to secretariat in show list
825+
# should include mars SIG because regular in showtypes list
826+
# should not include IETF plenary because plenary in hidetypes list
827+
# should not show ames SIG because ames in hide list
828+
self.do_ical_filter_test(
829+
meeting,
830+
querystring='?show=secretariat,ietf&hide=ames&showtypes=regular&hidetypes=plenary',
831+
expected_session_summaries=[
832+
'Morning Break',
833+
'Registration',
834+
'mars - Martian Special Interest Group',
835+
]
836+
)
612837

613838
def build_session_setup(self):
614839
# This setup is intentionally unusual - the session has one draft attached as a session presentation,

ietf/meeting/views.py

Lines changed: 58 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727

2828
from django import forms
2929
from django.shortcuts import render, redirect, get_object_or_404
30-
from django.http import HttpResponse, HttpResponseRedirect, Http404
30+
from django.http import HttpResponse, HttpResponseRedirect, Http404, HttpResponseBadRequest
3131
from django.conf import settings
3232
from django.contrib import messages
3333
from django.contrib.auth.decorators import login_required
@@ -1337,45 +1337,73 @@ def ical_session_status(session_with_current_status):
13371337
return "CONFIRMED"
13381338

13391339
def agenda_ical(request, num=None, name=None, acronym=None, session_id=None):
1340+
"""Agenda ical view
1341+
1342+
By default, all agenda items will be shown. A filter can be specified in
1343+
the querystring. It has the format
1344+
1345+
?show=...&hide=...&showtypes=...&hidetypes=...
1346+
1347+
where any of the parameters can be omitted. The right-hand side of each
1348+
'=' is a comma separated list, which can be empty. If none of the filter
1349+
parameters are specified, no filtering will be applied, even if the query
1350+
string is not empty.
1351+
1352+
The show and hide parameters each take a list of working group (wg) acronyms.
1353+
The showtypes and hidetypes parameters take a list of session types.
1354+
1355+
Hiding (by wg or type) takes priority over showing.
1356+
"""
13401357
meeting = get_meeting(num, type_in=None)
13411358
schedule = get_schedule(meeting, name)
13421359
updated = meeting.updated()
13431360

13441361
if schedule is None and acronym is None and session_id is None:
13451362
raise Http404
13461363

1347-
q = request.META.get('QUERY_STRING','') or ""
1348-
filter = set(unquote(q).lower().split(','))
1349-
include = [ i for i in filter if not (i.startswith('-') or i.startswith('~')) ]
1350-
include_types = set(["plenary","other"])
1351-
exclude = []
1352-
1353-
# Process the special flags.
1354-
# "-wgname" will remove a working group from the output.
1355-
# "~Type" will add that type to the output.
1356-
# "-~Type" will remove that type from the output
1357-
# Current types are:
1358-
# Session, Other (default on), Break, Plenary (default on)
1359-
# Non-Working Group "wg names" include:
1360-
# edu, ietf, tools, iesg, iab
1361-
1362-
for item in filter:
1363-
if len(item) > 2 and item[0] == '-' and item[1] == '~':
1364-
include_types -= set([item[2:]])
1365-
elif len(item) > 1 and item[0] == '-':
1366-
exclude.append(item[1:])
1367-
elif len(item) > 1 and item[0] == '~':
1368-
include_types |= set([item[1:]])
1369-
13701364
assignments = schedule.assignments.exclude(timeslot__type__in=['lead','offagenda'])
13711365
assignments = preprocess_assignments_for_agenda(assignments, meeting)
13721366

1373-
if q:
1374-
assignments = [a for a in assignments if
1375-
(a.timeslot.type_id in include_types
1376-
or (a.session.historic_group and a.session.historic_group.acronym in include)
1377-
or (a.session.historic_group and a.session.historic_group.historic_parent and a.session.historic_group.historic_parent.acronym in include))
1378-
and (not a.session.historic_group or a.session.historic_group.acronym not in exclude)]
1367+
if len(request.GET) > 0:
1368+
# Parse group filters from GET parameters. The keys in this dict define the
1369+
# allowed querystring parameters.
1370+
filt_params = {'show': set(), 'hide': set(), 'showtypes': set(), 'hidetypes': set()}
1371+
1372+
for key, value in request.GET.items():
1373+
if key not in filt_params:
1374+
return HttpResponseBadRequest('Unrecognized parameter "%s"' % key)
1375+
if value is None:
1376+
return HttpResponseBadRequest(
1377+
'Parameter "%s" is not assigned a value (use "key=" for an empty value)' % key
1378+
)
1379+
filt_params[key] = set(unquote(value).lower().split(','))
1380+
1381+
def _should_include_assignment(assignment):
1382+
"""Decide whether to include an assignment
1383+
1384+
Relies on filt_params from parent scope.
1385+
"""
1386+
historic_group = assignment.session.historic_group
1387+
if historic_group:
1388+
group_acronym = historic_group.acronym
1389+
parent = historic_group.historic_parent
1390+
parent_acronym = parent.acronym if parent else None
1391+
else:
1392+
group_acronym = None
1393+
parent_acronym = None
1394+
session_type = assignment.timeslot.type_id
1395+
1396+
# Hide if wg or type hide lists apply
1397+
if (group_acronym in filt_params['hide']) or (session_type in filt_params['hidetypes']):
1398+
return False
1399+
1400+
# Show if any of the show lists apply, including showing by parent group
1401+
return ((group_acronym in filt_params['show']) or
1402+
(parent_acronym in filt_params['show']) or
1403+
(session_type in filt_params['showtypes']))
1404+
1405+
# Apply the filter
1406+
assignments = [a for a in assignments if _should_include_assignment(a)]
13791407

13801408
if acronym:
13811409
assignments = [ a for a in assignments if a.session.historic_group and a.session.historic_group.acronym == acronym ]

0 commit comments

Comments
 (0)