From 7bee3020fd676c78a014dcde6ea69221e4f6827c Mon Sep 17 00:00:00 2001 From: Jennifer Richards Date: Wed, 23 Sep 2020 15:03:40 +0000 Subject: [PATCH] Rearrange agenda customization UI and add customization UI to upcoming meetings - Legacy-Id: 18513 --- ietf/meeting/tests_js.py | 239 ++++++++++++++- ietf/meeting/tests_views.py | 84 ++++-- ietf/meeting/views.py | 65 ++-- ietf/static/ietf/js/agenda/agenda_filter.js | 317 ++++++++++++++++++++ ietf/templates/meeting/agenda.html | 295 +++++------------- ietf/templates/meeting/agenda_filter.html | 105 +++++++ ietf/templates/meeting/upcoming.html | 195 +++++++++--- ietf/templates/meeting/week-view.html | 3 +- requirements.txt | 2 +- 9 files changed, 973 insertions(+), 332 deletions(-) create mode 100644 ietf/static/ietf/js/agenda/agenda_filter.js create mode 100644 ietf/templates/meeting/agenda_filter.html diff --git a/ietf/meeting/tests_js.py b/ietf/meeting/tests_js.py index 60384b6aa..9dc1b996a 100644 --- a/ietf/meeting/tests_js.py +++ b/ietf/meeting/tests_js.py @@ -4,20 +4,28 @@ import time import datetime +import shutil +import os from pyquery import PyQuery from unittest import skipIf import django from django.urls import reverse as urlreverse +from django.utils.text import slugify +from django.db.models import F #from django.test.utils import override_settings import debug # pyflakes:ignore from ietf.doc.factories import DocumentFactory from ietf.group import colors +from ietf.group.models import Group from ietf.meeting.factories import SessionFactory from ietf.meeting.test_data import make_meeting_test_data -from ietf.meeting.models import Schedule, SchedTimeSessAssignment, Session, Room, TimeSlot, Constraint, ConstraintName +from ietf.meeting.models import (Schedule, SchedTimeSessAssignment, Session, + Room, TimeSlot, Constraint, ConstraintName, + Meeting) +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 import settings @@ -314,25 +322,33 @@ class AgendaTests(MeetingTestCase): def test_agenda_view_js_func_parse_query_params(self): """Test parse_query_params() function""" self.driver.get(self.absreverse('ietf.meeting.views.agenda')) - + + parse_query_params = 'return agenda_filter_for_testing.parse_query_params' + # Only 'show' param result = self.driver.execute_script( - 'return parse_query_params("?show=group1,group2,group3");' + parse_query_params + '("?show=group1,group2,group3");' ) self.assertEqual(result, dict(show='group1,group2,group3')) # Only 'hide' param result = self.driver.execute_script( - 'return parse_query_params("?hide=group4,group5,group6");' + parse_query_params + '("?hide=group4,group5,group6");' ) self.assertEqual(result, dict(hide='group4,group5,group6')) - + # Both 'show' and 'hide' result = self.driver.execute_script( - 'return parse_query_params("?show=group1,group2,group3&hide=group4,group5,group6");' + parse_query_params + '("?show=group1,group2,group3&hide=group4,group5,group6");' ) self.assertEqual(result, dict(show='group1,group2,group3', hide='group4,group5,group6')) + # Encoded + result = self.driver.execute_script( + parse_query_params + '("?show=%20group1,%20group2,%20group3&hide=group4,group5,group6");' + ) + self.assertEqual(result, dict(show=' group1, group2, group3', hide='group4,group5,group6')) + def test_agenda_view_js_func_toggle_list_item(self): """Test toggle_list_item() function""" self.driver.get(self.absreverse('ietf.meeting.views.agenda')) @@ -341,30 +357,30 @@ class AgendaTests(MeetingTestCase): """ // start empty, add item var list0=[]; - toggle_list_item(list0, 'item'); + %(toggle_list_item)s(list0, 'item'); // one item, remove it var list1=['item']; - toggle_list_item(list1, 'item'); + %(toggle_list_item)s(list1, 'item'); // one item, add another var list2=['item1']; - toggle_list_item(list2, 'item2'); + %(toggle_list_item)s(list2, 'item2'); // multiple items, remove first var list3=['item1', 'item2', 'item3']; - toggle_list_item(list3, 'item1'); + %(toggle_list_item)s(list3, 'item1'); // multiple items, remove middle var list4=['item1', 'item2', 'item3']; - toggle_list_item(list4, 'item2'); + %(toggle_list_item)s(list4, 'item2'); // multiple items, remove last var list5=['item1', 'item2', 'item3']; - toggle_list_item(list5, 'item3'); + %(toggle_list_item)s(list5, 'item3'); return [list0, list1, list2, list3, list4, list5]; - """ + """ % {'toggle_list_item': 'agenda_filter_for_testing.toggle_list_item'} ) self.assertEqual(result[0], ['item'], 'Adding item to empty list failed') self.assertEqual(result[1], [], 'Removing only item in a list failed') @@ -385,11 +401,20 @@ class AgendaTests(MeetingTestCase): self.driver.switch_to.frame(weekview_iframe) 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_show_one(self): """Filtered agenda view should display only matching rows (one group selected)""" self.do_agenda_view_filter_test('?show=mars', ['mars']) + def test_agenda_view_filter_show_area(self): + mars = Group.objects.get(acronym='mars') + area = mars.parent + self.do_agenda_view_filter_test('?show=%s' % area.acronym, ['ames', 'mars']) + def test_agenda_view_filter_show_two(self): """Filtered agenda view should display only matching rows (two groups selected)""" self.do_agenda_view_filter_test('?show=mars,ames', ['mars', 'ames']) @@ -401,6 +426,11 @@ class AgendaTests(MeetingTestCase): 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') + area = mars.parent + self.do_agenda_view_filter_test('?show=mars&hide=%s' % area.acronym, []) + def test_agenda_view_filter_show_and_hide(self): self.do_agenda_view_filter_test('?show=mars&hide=ietf', ['mars']) @@ -551,7 +581,186 @@ class AgendaTests(MeetingTestCase): WebDriverWait(self.driver, 2).until(expected_conditions.url_to_be(expected_url)) # no assertion here - if WebDriverWait raises an exception, the test will fail. # We separately test whether this URL will filter correctly. - + + +@skipIf(skip_selenium, skip_message) +class InterimTests(MeetingTestCase): + def setUp(self): + super(InterimTests, self).setUp() + self.materials_dir = self.tempdir('materials') + self.saved_agenda_path = settings.AGENDA_PATH + settings.AGENDA_PATH = self.materials_dir + self.meeting = make_meeting_test_data(create_interims=True) + + def tearDown(self): + settings.AGENDA_PATH = self.saved_agenda_path + shutil.rmtree(self.materials_dir) + super(InterimTests, self).tearDown() + + def tempdir(self, label): + # Borrowed from test_utils.TestCase + slug = slugify(self.__class__.__name__.replace('.','-')) + dirname = "tmp-{label}-{slug}-dir".format(**locals()) + if 'VIRTUAL_ENV' in os.environ: + dirname = os.path.join(os.environ['VIRTUAL_ENV'], dirname) + path = os.path.abspath(dirname) + if not os.path.exists(path): + os.mkdir(path) + return path + + def displayed_interims(self, groups=None): + sessions = add_event_info_to_session_qs( + Session.objects.filter( + meeting__type_id='interim', + timeslotassignments__schedule=F('meeting__schedule'), + timeslotassignments__timeslot__time__gte=datetime.datetime.today() + ) + ).filter(current_status__in=('sched','canceled')) + meetings = [] + for s in sessions: + if groups is None or s.group.acronym in groups: + s.meeting.calendar_label = s.group.acronym # annotate with group + meetings.append(s.meeting) + return meetings + + def all_ietf_meetings(self): + meetings = Meeting.objects.filter( + type_id='ietf', + date__gte=datetime.datetime.today()-datetime.timedelta(days=7) + ) + for m in meetings: + m.calendar_label = 'IETF %s' % m.number + return meetings + + def assert_upcoming_meeting_visibility(self, visible_meetings=None): + """Assert that correct items are visible in current browser window + + If visible_meetings is None (the default), expects all items to be visible. + """ + expected = {mtg.number for mtg in visible_meetings} + not_visible = set() + unexpected = set() + entries = self.driver.find_elements_by_css_selector( + 'table#upcoming-meeting-table > tbody > tr.entry' + ) + for entry in entries: + nums = [n for n in expected if n in entry.text] + self.assertLessEqual(len(nums), 1, 'Multiple matching meeting numbers') + if len(nums) > 0: # asserted that it's at most 1, so if it's not 0, it's 1. + expected.remove(nums[0]) + if not entry.is_displayed(): + not_visible.add(nums[0]) + continue + # Found an unexpected row - this is ok as long as it's hidden + if entry.is_displayed(): + unexpected.add(entry.text) + + self.assertEqual(expected, set(), "Missing entries for expected iterim meetings.") + self.assertEqual(not_visible, set(), "Hidden rows for expected interim meetings.") + self.assertEqual(unexpected, set(), "Unexpected row visible") + + def assert_upcoming_meeting_calendar(self, visible_meetings=None): + """Assert that correct items are sent to the calendar""" + def advance_month(): + button = WebDriverWait(self.driver, 2).until( + expected_conditions.element_to_be_clickable( + (By.CSS_SELECTOR, 'div#calendar button.fc-next-button'))) + button.click() + + seen = set() + not_visible = set() + unexpected = set() + + # Test that we see all the expected meetings when we scroll through the + # entire year. We only check the group names / IETF numbers. This should + # be good enough to catch filtering errors but does not validate the + # details of what's shown on the calendar. Need 13 iterations instead of + # 12 in order to check the starting month of the following year, which + # will usually contain the day 1 year from the start date. + for _ in range(13): + entries = self.driver.find_elements_by_css_selector( + 'div#calendar div.fc-content' + ) + for entry in entries: + meetings = [m for m in visible_meetings if m.calendar_label in entry.text] + if len(meetings) > 0: + seen.add(meetings[0]) + if not entry.is_displayed(): + not_visible.add(meetings[0]) + continue + # Found an unexpected row - this is ok as long as it's hidden + if entry.is_displayed(): + unexpected.add(entry.text) + advance_month() + + self.assertEqual(seen, visible_meetings, "Expected calendar entries not shown.") + self.assertEqual(not_visible, set(), "Hidden calendar entries for expected interim meetings.") + self.assertEqual(unexpected, set(), "Unexpected calendar entries visible") + + def do_upcoming_view_filter_test(self, querystring, visible_meetings=()): + self.login() + self.driver.get(self.absreverse('ietf.meeting.views.upcoming') + querystring) + self.assert_upcoming_meeting_visibility(visible_meetings) + self.assert_upcoming_meeting_calendar(visible_meetings) + + # Check the ical links + simplified_querystring = querystring.replace(' ', '%20') # encode spaces' + ics_link = self.driver.find_element_by_link_text('Download as .ics') + self.assertIn(simplified_querystring, ics_link.get_attribute('href')) + 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): + """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) + + 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_one(self): + meetings = set(self.all_ietf_meetings()) + meetings.update(self.displayed_interims(groups=['mars'])) + self.do_upcoming_view_filter_test('?show=mars', meetings) + + 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) + + 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_whitespace(self): + """Whitespace in filter lists should be ignored""" + 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) + + 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 diff --git a/ietf/meeting/tests_views.py b/ietf/meeting/tests_views.py index 8376c8292..ff7179a6c 100644 --- a/ietf/meeting/tests_views.py +++ b/ietf/meeting/tests_views.py @@ -738,6 +738,29 @@ class MeetingTests(TestCase): expected_session_summaries=[] ) + def test_ical_filter_show_area(self): + meeting = make_meeting_test_data() + mars = Group.objects.get(acronym='mars') + area = mars.parent + self.do_ical_filter_test( + meeting, + querystring='?show=%s' % area.acronym, + expected_session_summaries=[ + 'ames - Asteroid Mining Equipment Standardization Group', + 'mars - Martian Special Interest Group', + ] + ) + + def test_ical_filter_hide_area(self): + meeting = make_meeting_test_data() + mars = Group.objects.get(acronym='mars') + area = mars.parent + self.do_ical_filter_test( + meeting, + querystring='?show=mars&hide=%s' % area.acronym, + expected_session_summaries=[] + ) + def test_ical_filter_show_and_hide(self): meeting = make_meeting_test_data() self.do_ical_filter_test( @@ -1861,40 +1884,26 @@ class InterimTests(TestCase): q = PyQuery(r.content) self.assertIn('CANCELLED', q('tr>td.text-right>span').text()) - def test_upcoming_filter_show(self): - r, interims = self.do_upcoming_test('show=ames') - self.assertNotContains(r, interims['mars'].number) - self.assertContains(r, interims['ames'].number) - self.assertContains(r, 'IETF 72') - # cancelled session - q = PyQuery(r.content) - self.assertIn('CANCELLED', q('tr>td.text-right>span').text()) - - def test_upcoming_filter_show_area(self): - make_meeting_test_data(create_interims=True) - area = Group.objects.get(acronym='mars').parent - self.assertEqual(area, - Group.objects.get(acronym='ames').parent, - 'The mars and ames groups have different areas; this breaks this test') - r, interims = self.do_upcoming_test('show=%s' % area.acronym, create_meeting=False) + def test_upcoming_filters_ignored(self): + """The upcoming view should ignore filter querystrings""" + r, interims = self.do_upcoming_test() self.assertContains(r, interims['mars'].number) self.assertContains(r, interims['ames'].number) self.assertContains(r, 'IETF 72') - def test_upcoming_filter_hide(self): - r, interims = self.do_upcoming_test('hide=mars') - self.assertNotContains(r, interims['mars'].number) - self.assertNotContains(r, interims['ames'].number) - self.assertContains(r, 'IETF 72') - - def test_upcoming_filter_show_and_hide(self): - r, interims = self.do_upcoming_test('show=mars,ames&hide=ames') + r, interims = self.do_upcoming_test('show=ames', create_meeting=False) self.assertContains(r, interims['mars'].number) - self.assertNotContains(r, interims['ames'].number) + self.assertContains(r, interims['ames'].number) self.assertContains(r, 'IETF 72') - def do_upcoming_ical_test(self, querystring=None): - make_meeting_test_data(create_interims=True) + r, interims = self.do_upcoming_test('show=ames&hide=ames,mars', create_meeting=False) + self.assertContains(r, interims['mars'].number) + 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') @@ -2010,6 +2019,27 @@ class InterimTests(TestCase): 'mars - Martian Special Interest Group', ]) + def test_upcoming_ical_filter_show_area(self): + make_meeting_test_data(create_interims=True) + mars = Group.objects.get(acronym='mars') + area = mars.parent + r = self.do_upcoming_ical_test('show=%s' % area.acronym, + create_meeting=False) + assert_ical_response_is_valid(self, r, + expected_event_summaries=[ + 'ames - Asteroid Mining Equipment Standardization Group', + 'mars - Martian Special Interest Group', + ]) + + def test_upcoming_ical_filter_hide_area(self): + make_meeting_test_data(create_interims=True) + mars = Group.objects.get(acronym='mars') + area = mars.parent + r = self.do_upcoming_ical_test('show=mars&hide=%s' % area.acronym, + create_meeting=False) + assert_ical_response_is_valid(self, r, + expected_event_summaries=[]) + def test_upcoming_json(self): make_meeting_test_data(create_interims=True) url = urlreverse("ietf.meeting.views.upcoming_json") diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py index c81250cbc..d5445f732 100644 --- a/ietf/meeting/views.py +++ b/ietf/meeting/views.py @@ -1379,7 +1379,9 @@ def should_include_assignment(filter_params, assignment): session_type = assignment.timeslot.type_id # Hide if wg or type hide lists apply - if (group_acronym in filter_params['hide']) or (session_type in filter_params['hidetypes']): + 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 @@ -2774,14 +2776,8 @@ def past(request): }) def upcoming(request): - """List of upcoming meetings - - Only querystring filters by wg name are supported. Always includes IETF meetings; - filters 'interim' type meetings by wg name as requested. The showtypes/hidetypes - filters are ignored.. - """ + """List of upcoming meetings""" today = datetime.date.today() - filter_params = parse_agenda_filter_params(request.GET) # Get ietf meetings starting 7 days ago, and interim meetings starting today ietf_meetings = Meeting.objects.filter(type_id='ietf', date__gte=today-datetime.timedelta(days=7)) @@ -2796,22 +2792,21 @@ def upcoming(request): timeslotassignments__timeslot__time__gte=today ) ).filter(current_status__in=('sched','canceled')) - if filter_params is not None: - group_shown = interim_sessions.filter( - group__acronym__in=filter_params['show'] - ) - parent_group_shown = interim_sessions.filter( - group__parent__acronym__in=filter_params['show'] - ) - # The '|' combines querysets with OR - qs.filter(x=1) | qs.filter(y=2) - # translates to a 'WHERE x=1 OR y=2' in the SQL. - interim_sessions = ( - group_shown | parent_group_shown - ).exclude( - # N.B., we only consider parent group (area) for show, not for hide. - # This is consistent with previous behavior but is worth revisiting. - group__acronym__in=filter_params['hide'] - ) + + # get groups for group UI display - same algorithm as in agenda(), but + # using group / parent instead of historic_group / historic_parent + groups = [s.group for s in interim_sessions + if s.group + and s.group.type_id in ('wg', 'rg', 'ag', 'rag', 'iab', 'program') + and s.group.parent] + group_parents = {g.parent for g in groups if g.parent} + seen = set() + for p in group_parents: + p.group_list = [] + for g in groups: + if g.acronym not in seen and g.parent.acronym == p.acronym: + p.group_list.append(g) + seen.add(g.acronym) for session in interim_sessions: session.historic_group = session.group @@ -2825,15 +2820,25 @@ def upcoming(request): # add menu actions actions = [] if can_request_interim_meeting(request.user): - actions.append(('Request new interim meeting', - reverse('ietf.meeting.views.interim_request'))) - actions.append(('Download as .ics', - reverse('ietf.meeting.views.upcoming_ical'))) - actions.append(('Subscribe with webcal', - 'webcal://'+request.get_host()+reverse('ietf.meeting.views.upcoming_ical'))) + actions.append(dict( + label='Request new interim meeting', + url=reverse('ietf.meeting.views.interim_request'), + append_filter=False) + ) + actions.append(dict( + label='Download as .ics', + url=reverse('ietf.meeting.views.upcoming_ical'), + append_filter=True) + ) + actions.append(dict( + label='Subscribe with webcal', + url='webcal://'+request.get_host()+reverse('ietf.meeting.views.upcoming_ical'), + append_filter=True) + ) return render(request, 'meeting/upcoming.html', { 'entries': entries, + 'group_parents': group_parents, 'menu_actions': actions, 'menu_entries': menu_entries, 'selected_menu_entry': selected_menu_entry, diff --git a/ietf/static/ietf/js/agenda/agenda_filter.js b/ietf/static/ietf/js/agenda/agenda_filter.js new file mode 100644 index 000000000..264c77690 --- /dev/null +++ b/ietf/static/ietf/js/agenda/agenda_filter.js @@ -0,0 +1,317 @@ +var agenda_filter_for_testing = {}; // methods to be accessed for automated testing +var agenda_filter = function () { + 'use strict' + + var update_callback // function(filter_params) + var enable_non_area = false // if true, show the non-area filters + + /* Add to list without duplicates */ + function add_list_item (list, item) { + if (list.indexOf(item) === -1) { + list.push(item); + } + } + + /* Remove from list, if present */ + function remove_list_item (list, item) { + var item_index = list.indexOf(item); + if (item_index !== -1) { + list.splice(item_index, 1) + } + } + + /* Add to list if not present, remove if present */ + function toggle_list_item (list, item) { + var item_index = list.indexOf(item); + if (item_index === -1) { + list.push(item) + } else { + list.splice(item_index, 1) + } + } + + function parse_query_params (qs) { + var params = {} + qs = decodeURI(qs).replace(/^\?/, '').toLowerCase() + if (qs) { + var param_strs = qs.split('&') + for (var ii = 0; ii < param_strs.length; ii++) { + var toks = param_strs[ii].split('=', 2) + params[toks[0]] = toks[1] || true + } + } + return params + } + + /* filt = 'show' or 'hide' */ + function get_filter_from_qparams (qparams, filt) { + if (!qparams[filt] || (qparams[filt] === true)) { + return []; + } + return $.map(qparams[filt].split(','), function(s){return s.trim();}); + } + + function get_filter_params (qparams) { + var enabled = !!(qparams.show || qparams.hide || qparams.showtypes || qparams.hidetypes); + return { + enabled: enabled, + show_groups: get_filter_from_qparams(qparams, 'show'), + hide_groups: get_filter_from_qparams(qparams, 'hide'), + show_types: get_filter_from_qparams(qparams, 'showtypes'), + hide_types: get_filter_from_qparams(qparams, 'hidetypes'), + } + } + + function filtering_is_enabled (filter_params) { + return filter_params['enabled']; + } + + function get_area_items (area) { + var types = []; + var groups = []; + var neg_groups = []; + + $('.view.' + area).find('button').each(function (index, elt) { + elt = $(elt) // jquerify + var item = elt.text().trim().toLowerCase() + if (elt.hasClass('picktype')) { + types.push(item) + } else if (elt.hasClass('pickview')) { + groups.push(item); + } else if (elt.hasClass('pickviewneg')) { + neg_groups.push(item) + } + }); + return { 'groups': groups, 'neg_groups': neg_groups, 'types': types }; + } + + // Update the filter / customization UI to match the current filter parameters + function update_filter_ui (filter_params) { + var area_group_buttons = $('.view .pickview, .pick-area'); + var non_area_header_button = $('button.pick-non-area'); + var non_area_type_buttons = $('.view.non-area .picktype'); + var non_area_group_buttons = $('.view.non-area button.pickviewneg'); + + if (!filtering_is_enabled(filter_params)) { + // Not filtering - set everything to defaults and exit + area_group_buttons.removeClass('active'); + non_area_header_button.removeClass('active'); + non_area_type_buttons.removeClass('active'); + non_area_group_buttons.removeClass('active'); + non_area_group_buttons.addClass('disabled'); + return; + } + + // show the customizer - it will stay visible even if filtering is disabled + $('#customize').collapse('show') + + // Group and area buttons - these are all positive selections + area_group_buttons.each(function (index, elt) { + elt = $(elt); + var item = elt.text().trim().toLowerCase(); + var area = elt.attr('data-group-area'); + if ((filter_params['hide_groups'].indexOf(item) === -1) // not hidden... + && ((filter_params['show_groups'].indexOf(item) !== -1) // AND shown... + || (area && (filter_params['show_groups'].indexOf(area.trim().toLowerCase()) !== -1))) // OR area shown + ) { + elt.addClass('active'); + } else { + elt.removeClass('active'); + } + }); + + // Non-area buttons need special handling. Only have positive type and negative group buttons. + // Assume non-area heading is disabled, then enable if one of the types is active + non_area_header_button.removeClass('active'); + non_area_group_buttons.addClass('disabled'); + non_area_type_buttons.each(function (index, elt) { + // Positive type selection buttons + elt = $(elt); + var item = elt.text().trim().toLowerCase(); + if ((filter_params['show_types'].indexOf(item) !== -1) + && (filter_params['hide_types'].indexOf(item) === -1)){ + elt.addClass('active'); + non_area_header_button.addClass('active'); + non_area_group_buttons.removeClass('disabled'); + } else { + elt.removeClass('active'); + } + }); + + non_area_group_buttons.each(function (index, elt) { + // Negative group selection buttons + elt = $(elt); + var item = elt.text().trim().toLowerCase(); + if (filter_params['hide_groups'].indexOf(item) === -1) { + elt.addClass('active'); + } else { + elt.removeClass('active'); + } + }); + } + + /* Update state of the view to match the filters + * + * Calling the individual update_* functions outside of this method will likely cause + * various parts of the page to get out of sync. + */ + function update_view () { + var filter_params = get_filter_params(parse_query_params(window.location.search)) + update_filter_ui(filter_params) + if (update_callback) { + update_callback(filter_params) + } + } + + + /* Trigger an update so the user will see the page appropriate for given filter_params + * + * Updates the URL to match filter_params, then updates the history / display to match + * (if supported) or loads the new URL. + */ + function update_filters (filter_params) { + var qparams = [] + var search = '' + if (filter_params['show_groups'].length > 0) { + qparams.push('show=' + filter_params['show_groups'].join()) + } + if (filter_params['hide_groups'].length > 0) { + qparams.push('hide=' + filter_params['hide_groups'].join()) + } + if (filter_params['show_types'].length > 0) { + qparams.push('showtypes=' + filter_params['show_types'].join()) + } + if (filter_params['hide_types'].length > 0) { + qparams.push('hidetypes=' + filter_params['hide_types'].join()) + } + if (qparams.length > 0) { + search = '?' + qparams.join('&') + } + + // 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) + update_view() + } else { + // No window.history.replaceState support, page reload required + window.location = new_url + } + } + + /* Helper for pick group/type button handlers - toggles the appropriate parameter entry + * elt - the jquery element that was clicked + * param_type - key of the filter param to update (show_groups, show_types, etc) + */ + function handle_pick_button (elt, param_type) { + var area = elt.attr('data-group-area'); + var item = elt.text().trim().toLowerCase(); + var fp = get_filter_params(parse_query_params(window.location.search)); + var neg_param_type = { + show_groups: 'hide_groups', + hide_groups: 'show_groups', + show_types: 'hide_types', + hide_types: 'show_types' + }[param_type]; + + if (area && (fp[param_type].indexOf(area.trim().toLowerCase()) !== -1)) { + // Area is shown - toggle hide list + toggle_list_item(fp[neg_param_type], item); + remove_list_item(fp[param_type], item); + } else { + toggle_list_item(fp[param_type], item); + remove_list_item(fp[neg_param_type], item); + } + return fp; + } + + function is_disabled(elt) { + return elt.hasClass('disabled'); + } + + // Various "pick" button handlers + $('.pickview').click(function () { + if (is_disabled($(this))) { return; } + update_filters(handle_pick_button($(this), 'show_groups')) + }); + + $('.pickviewneg').click(function () { + if (is_disabled($(this))) { return; } + update_filters(handle_pick_button($(this), 'hide_groups')) + }); + + $('.picktype').click(function () { + if (is_disabled($(this))) { return; } + var fp = handle_pick_button($(this), 'show_types') + // If we just disabled the last non-area type, clear out the hide groups list. + var items = get_area_items('non-area') + var any_left = false + $.each(items.types, function (index, session_type) { + if (fp['show_types'].indexOf(session_type) !== -1) { + any_left = true + } + }) + if (!any_left) { + fp['hide_groups'] = [] + } + update_filters(fp); + }); + + // Click handler for an area header button + $('.pick-area').click(function() { + if (is_disabled($(this))) { return; } + var fp = handle_pick_button($(this), 'show_groups'); + var items = get_area_items($(this).text().trim().toLowerCase()); + + // Clear all the individual group show/hide options + $.each(items.groups, function(index, group) { + remove_list_item(fp['show_groups'], group); + remove_list_item(fp['hide_groups'], group); + }); + update_filters(fp); + }); + + // Click handler for the "Non-Area" header button + $('.pick-non-area').click(function () { + var items = get_area_items('non-area'); + + var fp = get_filter_params(parse_query_params(window.location.search)) + if ($(this).hasClass('active')) { + // Were active - disable or hide everything + $.each(items.types, function (index, session_type) { + remove_list_item(fp['show_types'], session_type) + }) + // When no types are shown, no need to hide groups. Empty hide_groups list. + fp['hide_groups'] = [] + } else { + // Were not active - enable or stop hiding everything + $.each(items.types, function (index, session_type) { + add_list_item(fp['show_types'], session_type) + }) + $.each(items.neg_groups, function (index, group) { + remove_list_item(fp['hide_groups'], group) + }) + } + update_filters(fp); + }); + + // Entry point to filtering code when page loads + function enable () { + $(document).ready(function () { + update_view() + }) + } + + // Make private functions available for unit testing + agenda_filter_for_testing.toggle_list_item = toggle_list_item; + agenda_filter_for_testing.parse_query_params = parse_query_params; + + // Public interface methods + return { + enable: enable, + filtering_is_enabled: filtering_is_enabled, + include_non_area_selectors: function () {enable_non_area = true}, + set_update_callback: function (cb) {update_callback = cb} + } +}(); \ No newline at end of file diff --git a/ietf/templates/meeting/agenda.html b/ietf/templates/meeting/agenda.html index 328fc003b..6b785afb3 100644 --- a/ietf/templates/meeting/agenda.html +++ b/ietf/templates/meeting/agenda.html @@ -62,76 +62,7 @@ {% endif %} -
-
- -
-
- -

- You can customize the agenda view to show only selected sessions, - by clicking on groups and areas in the table below. - To be able to return to the customized view later, bookmark the resulting URL. -

- - {% if group_parents|length %} -

Groups displayed in italics are BOFs.

- - - - - {% for p in group_parents %} - - {% endfor %} - - - - - {% for p in group_parents %} - - {% endfor %} - - -
- -
-
- {% for group in p.group_list %} -
- -
- {% endfor %} -
-
- {% else %} -
No WG / RG data available -- no WG / RG sessions have been scheduled yet.
- {% endif %} -

Also show special sessions of these groups:

-
-
-
-
-
-
-
-
-
-
-
-
-
+ {% include "meeting/agenda_filter.html" with group_parents=group_parents non_area_filters=True only %}

Download as .ics

@@ -193,7 +124,10 @@ {% endif %} {% if item.timeslot.type.slug == 'break' or item.timeslot.type.slug == 'reg' or item.timeslot.type.slug == 'other' %} - + {% if "-utc" in request.path %} {{item.timeslot.utc_start_time|date:"G:i"}}-{{item.timeslot.utc_end_time|date:"G:i"}} @@ -251,7 +185,11 @@ {% if item.timeslot.type_id == 'regular' or item.timeslot.type.slug == 'plenary' %} {% if item.session.historic_group %} - + {% if item.timeslot.type.slug == 'plenary' %} {% if "-utc" in request.path %} @@ -359,161 +297,86 @@ {% endblock %} {% block js %} + + +