Use new querystring format for agenda_ical and add tests. Ensure querystring precedes URL fragment in agenda JS.

- Legacy-Id: 18413
This commit is contained in:
Jennifer Richards 2020-08-25 14:35:44 +00:00
parent ea3882034a
commit b1e3c1fe92
3 changed files with 301 additions and 49 deletions

View file

@ -197,7 +197,7 @@ class MeetingTests(TestCase):
# iCal
r = self.client.get(urlreverse("ietf.meeting.views.agenda_ical", kwargs=dict(num=meeting.number))
+ "?" + session.group.parent.acronym.upper())
+ "?show=" + session.group.parent.acronym.upper())
self.assertContains(r, session.group.acronym)
self.assertContains(r, session.group.name)
self.assertContains(r, slot.location.name)
@ -580,6 +580,28 @@ class MeetingTests(TestCase):
post_date = meeting.importantdate_set.get(name=idn).date
self.assertEqual(pre_date, post_date+datetime.timedelta(days=1))
def assert_ical_response_is_valid(self, 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.
"""
self.assertEqual(response.get('Content-Type'), "text/calendar")
# Validate iCalendar object
self.assertContains(response, 'BEGIN:VCALENDAR', count=1)
self.assertContains(response, 'END:VCALENDAR', count=1)
self.assertContains(response, 'PRODID:', count=1)
self.assertContains(response, 'VERSION', count=1)
# Validate event objects
if expected_event_count is None:
expected_event_count = len(expected_event_summaries)
self.assertContains(response, 'BEGIN:VEVENT', count=expected_event_count)
self.assertContains(response, 'END:VEVENT', count=expected_event_count)
self.assertContains(response, 'UID', count=expected_event_count)
for summary in expected_event_summaries:
self.assertContains(response, 'SUMMARY:' + summary)
def test_group_ical(self):
meeting = make_meeting_test_data()
s1 = Session.objects.filter(meeting=meeting, group__acronym="mars").first()
@ -592,23 +614,226 @@ class MeetingTests(TestCase):
#
url = urlreverse('ietf.meeting.views.agenda_ical', kwargs={'num':meeting.number, 'acronym':s1.group.acronym, })
r = self.client.get(url)
self.assertEqual(r.get('Content-Type'), "text/calendar")
self.assertContains(r, 'BEGIN:VEVENT')
self.assertEqual(r.content.count(b'UID'), 2)
self.assertContains(r, 'SUMMARY:mars - Martian Special Interest Group')
self.assert_ical_response_is_valid(r,
expected_event_summaries=['mars - Martian Special Interest Group'],
expected_event_count=2)
self.assertContains(r, t1.time.strftime('%Y%m%dT%H%M%S'))
self.assertContains(r, t2.time.strftime('%Y%m%dT%H%M%S'))
self.assertContains(r, 'END:VEVENT')
#
url = urlreverse('ietf.meeting.views.agenda_ical', kwargs={'num':meeting.number, 'session_id':s1.id, })
r = self.client.get(url)
self.assertEqual(r.get('Content-Type'), "text/calendar")
self.assertContains(r, 'BEGIN:VEVENT')
self.assertEqual(r.content.count(b'UID'), 1)
self.assertContains(r, 'SUMMARY:mars - Martian Special Interest Group')
self.assert_ical_response_is_valid(r,
expected_event_summaries=['mars - Martian Special Interest Group'],
expected_event_count=1)
self.assertContains(r, t1.time.strftime('%Y%m%dT%H%M%S'))
self.assertNotContains(r, t2.time.strftime('%Y%m%dT%H%M%S'))
self.assertContains(r, 'END:VEVENT')
def test_meeting_agenda_has_static_ical_links(self):
"""Links to the agenda_ical view must appear on the agenda page
Confirms that these have the correct querystrings. Does not test the JS-based
'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)
# 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)
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)
def test_ical_filter_invalid_syntaxes(self):
meeting = make_meeting_test_data()
url = urlreverse('ietf.meeting.views.agenda_ical', kwargs={'num':meeting.number})
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 do_ical_filter_test(self, meeting, querystring, expected_session_summaries):
url = urlreverse('ietf.meeting.views.agenda_ical', kwargs={'num':meeting.number})
r = self.client.get(url + querystring)
self.assertEqual(r.status_code, 200)
self.assert_ical_response_is_valid(r, expected_event_summaries=expected_session_summaries)
def test_ical_filter_default(self):
meeting = make_meeting_test_data()
self.do_ical_filter_test(
meeting,
querystring='',
expected_session_summaries=[
'Morning Break',
'Registration',
'IETF Plenary',
'ames - Asteroid Mining Equipment Standardization Group',
'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',
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=mars&hide=ames&showtypes=plenary,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',
]
)
def build_session_setup(self):
# This setup is intentionally unusual - the session has one draft attached as a session presentation,

View file

@ -27,7 +27,7 @@ import debug # pyflakes:ignore
from django import forms
from django.shortcuts import render, redirect, get_object_or_404
from django.http import HttpResponse, HttpResponseRedirect, Http404
from django.http import HttpResponse, HttpResponseRedirect, Http404, HttpResponseBadRequest
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.decorators import login_required
@ -1337,6 +1337,23 @@ def ical_session_status(session_with_current_status):
return "CONFIRMED"
def agenda_ical(request, num=None, name=None, acronym=None, session_id=None):
"""Agenda ical view
By default, all agenda items will be shown. A filter can be specified in
the querystring. It has the format
?show=...&hide=...&showtypes=...&hidetypes=...
where any of the parameters can be omitted. The right-hand side of each
'=' is a comma separated list, which can be empty. If none of the filter
parameters are specified, no filtering will be applied, even if the query
string is not empty.
The show and hide parameters each take a list of working group (wg) acronyms.
The showtypes and hidetypes parameters take a list of session types.
Hiding (by wg or type) takes priority over showing.
"""
meeting = get_meeting(num, type_in=None)
schedule = get_schedule(meeting, name)
updated = meeting.updated()
@ -1344,38 +1361,49 @@ def agenda_ical(request, num=None, name=None, acronym=None, session_id=None):
if schedule is None and acronym is None and session_id is None:
raise Http404
q = request.META.get('QUERY_STRING','') or ""
filter = set(unquote(q).lower().split(','))
include = [ i for i in filter if not (i.startswith('-') or i.startswith('~')) ]
include_types = set(["plenary","other"])
exclude = []
# Process the special flags.
# "-wgname" will remove a working group from the output.
# "~Type" will add that type to the output.
# "-~Type" will remove that type from the output
# Current types are:
# Session, Other (default on), Break, Plenary (default on)
# Non-Working Group "wg names" include:
# edu, ietf, tools, iesg, iab
for item in filter:
if len(item) > 2 and item[0] == '-' and item[1] == '~':
include_types -= set([item[2:]])
elif len(item) > 1 and item[0] == '-':
exclude.append(item[1:])
elif len(item) > 1 and item[0] == '~':
include_types |= set([item[1:]])
assignments = schedule.assignments.exclude(timeslot__type__in=['lead','offagenda'])
assignments = preprocess_assignments_for_agenda(assignments, meeting)
if q:
assignments = [a for a in assignments if
(a.timeslot.type_id in include_types
or (a.session.historic_group and a.session.historic_group.acronym in include)
or (a.session.historic_group and a.session.historic_group.historic_parent and a.session.historic_group.historic_parent.acronym in include))
and (not a.session.historic_group or a.session.historic_group.acronym not in exclude)]
if len(request.GET) > 0:
# Parse group filters from GET parameters. The keys in this dict define the
# allowed querystring parameters.
filt_params = {'show': set(), 'hide': set(), 'showtypes': set(), 'hidetypes': set()}
for key, value in request.GET.items():
if key not in filt_params:
return HttpResponseBadRequest('Unrecognized parameter "%s"' % key)
if value is None:
return HttpResponseBadRequest(
'Parameter "%s" is not assigned a value (use "key=" for an empty value)' % key
)
filt_params[key] = set(unquote(value).lower().split(','))
def _should_include_assignment(assignment):
"""Decide whether to include an assignment
Relies on filt_params from parent scope.
"""
historic_group = assignment.session.historic_group
if historic_group:
group_acronym = historic_group.acronym
parent = historic_group.historic_parent
parent_acronym = parent.acronym if parent else None
else:
group_acronym = None
parent_acronym = None
session_type = assignment.timeslot.type_id
# Hide if wg or type hide lists apply
if (group_acronym in filt_params['hide']) or (session_type in filt_params['hidetypes']):
return False
# Show if any of the show lists apply, including showing by parent group
return ((group_acronym in filt_params['show']) or
(parent_acronym in filt_params['show']) or
(session_type in filt_params['showtypes']))
# Apply the filter
assignments = [a for a in assignments if _should_include_assignment(a)]
if acronym:
assignments = [ a for a in assignments if a.session.historic_group and a.session.historic_group.acronym == acronym ]

View file

@ -135,9 +135,9 @@
<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 %}?{{p.acronym|upper}},-~Other,-~Plenary">{{p.acronym|upper}}</a>
<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 %}?~Plenary,~Other">Non-area events</a>
<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>
</p>
@ -364,7 +364,7 @@
qs = qs.replace(/^\?/, '');
$.each(qs.split('&'), function(i, v) {
var toks = v.split('=', 2)
params[toks[0]] = toks[1];
params[toks[0]] = toks[1].toLowerCase();
});
return params;
}
@ -397,9 +397,7 @@
// loop through the has items and change the UI element and row visibilities accordingly
$.each(hide_groups, function (i, v) {
// if (v.indexOf("-") == 0) {
// this is a "negative" item: when present, hide these rows
// v = v.replace(/^-/, '');
$('[id^="row-"]').filter('[id*="-' + v + '"]').hide();
$(".view." + v).find("button").removeClass("active disabled");
$("button.pickviewneg." + v).removeClass("active");
@ -412,14 +410,14 @@
});
// show the week view
$("#weekview").attr("src", "week-view.html" + window.location.hash).removeClass("hidden");
$("#weekview").attr("src", "week-view.html" + window.location.search).removeClass("hidden");
// show the custom .ics link
$("#ical-link").attr("href",$("#ical-link").attr("href").split("?")[0]+window.location.search);
$("#ical-link").removeClass("hidden");
} else {
// if the hash is empty, show all and hide weekview
// if the hash is empty, show all and hide weekview / custom ical link
$('[id^="row-"]').show();
$("#ical-link, #weekview").addClass("hidden");
}
@ -463,7 +461,8 @@
search = '?' + qparams.join('&');
}
var new_url = window.location.href.replace(/(\?.*)?$/, search);
// strip out the search / hash, then add back
var new_url = window.location.href.replace(/(\?.*)?(#.*)?$/, search + window.location.hash);
if (window.history && window.history.replaceState) {
// Keep current origin, replace search string, no page reload
history.replaceState({}, document.title, new_url);