From 1b1bc247445a636709d0e8b822318d81e48670da Mon Sep 17 00:00:00 2001 From: Jennifer Richards Date: Mon, 5 Oct 2020 13:55:52 +0000 Subject: [PATCH] Rework agenda filters with show/hide keywords in place of 'types' and add bof / AD office hours buttons - Legacy-Id: 18563 --- ietf/meeting/helpers.py | 23 ++ ietf/meeting/tests_helpers.py | 96 ++++++ ietf/meeting/tests_js.py | 54 +--- ietf/meeting/tests_views.py | 254 +--------------- ietf/meeting/views.py | 17 +- ietf/static/ietf/js/agenda/agenda_filter.js | 309 ++++++++------------ ietf/templates/meeting/agenda.html | 37 +-- ietf/templates/meeting/agenda_filter.html | 35 +-- ietf/templates/meeting/upcoming.html | 58 ++-- ietf/templates/meeting/week-view.html | 77 ++--- 10 files changed, 352 insertions(+), 608 deletions(-) create mode 100644 ietf/meeting/tests_helpers.py diff --git a/ietf/meeting/helpers.py b/ietf/meeting/helpers.py index 04a3db3da..d99e5fe89 100644 --- a/ietf/meeting/helpers.py +++ b/ietf/meeting/helpers.py @@ -239,6 +239,29 @@ def preprocess_assignments_for_agenda(assignments_queryset, meeting, extra_prefe return assignments +def tag_assignments_with_filter_keywords(assignments): + """Add keywords for agenda filtering + + Keywords are all lower case. + """ + for a in assignments: + a.filter_keywords = [a.timeslot.type.slug.lower()] + a.filter_keywords.extend(filter_keywords_for_session(a.session)) + +def filter_keywords_for_session(session): + keywords = [] + 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()) + 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') + return keywords + def read_session_file(type, num, doc): # XXXX FIXME: the path fragment in the code below should be moved to # settings.py. The *_PATH settings should be generalized to format() diff --git a/ietf/meeting/tests_helpers.py b/ietf/meeting/tests_helpers.py new file mode 100644 index 000000000..b0cee9e5d --- /dev/null +++ b/ietf/meeting/tests_helpers.py @@ -0,0 +1,96 @@ +# Copyright The IETF Trust 2020, All Rights Reserved +# -*- coding: utf-8 -*- +from ietf.group.factories import GroupFactory +from ietf.meeting.factories import SessionFactory, MeetingFactory +from ietf.meeting.helpers import tag_assignments_with_filter_keywords +from ietf.utils.test_utils import TestCase + + +class HelpersTests(TestCase): + def do_test_tag_assignments_with_filter_keywords(self, bof=False, historic=None): + """Assignments should be tagged properly + + The historic param can be None, group, or parent, to specify whether to test + with no historic_group, a historic_group but no historic_parent, or both. + """ + meeting_types = ['regular', 'plenary'] + group_state_id = 'bof' if bof else 'active' + group = GroupFactory(state_id=group_state_id) + historic_group = GroupFactory(state_id=group_state_id) + historic_parent = GroupFactory(type_id='area') + + if historic == 'parent': + historic_group.historic_parent = historic_parent + + # Create meeting and sessions + meeting = MeetingFactory() + for meeting_type in meeting_types: + sess = SessionFactory(group=group, meeting=meeting, type_id=meeting_type) + ts = sess.timeslotassignments.first().timeslot + ts.type = sess.type + ts.save() + + # Create an office hours session in the group's area (i.e., parent). This is not + # currently really needed, but will protect against areas and groups diverging + # in a way that breaks keywording. + office_hours = SessionFactory( + name='some office hours', + group=group.parent, + meeting=meeting, + type_id='other' + ) + ts = office_hours.timeslotassignments.first().timeslot + ts.type = office_hours.type + ts.save() + + assignments = meeting.schedule.assignments.all() + orig_num_assignments = len(assignments) + + # Set up historic groups if needed + if historic: + for a in assignments: + if a.session != office_hours: + a.session.historic_group = historic_group + + # Execute the method under test + tag_assignments_with_filter_keywords(assignments) + + # Assert expected results + self.assertEqual(len(assignments), orig_num_assignments, 'Should not change number of assignments') + + if historic: + expected_group = historic_group + expected_area = historic_parent if historic == 'parent' else historic_group.parent + else: + expected_group = group + expected_area = group.parent + + for assignment in assignments: + expected_filter_keywords = [assignment.timeslot.type.slug] + + if assignment.session == office_hours: + expected_filter_keywords.extend([ + group.parent.acronym, + 'adofficehours', + ]) + else: + expected_filter_keywords.extend([ + expected_group.acronym, + expected_area.acronym + ]) + if bof: + expected_filter_keywords.append('bof') + + self.assertCountEqual( + assignment.filter_keywords, + expected_filter_keywords, + 'Assignment has incorrect filter keywords' + ) + + def test_tag_assignments_with_filter_keywords(self): + self.do_test_tag_assignments_with_filter_keywords() + self.do_test_tag_assignments_with_filter_keywords(historic='group') + self.do_test_tag_assignments_with_filter_keywords(historic='parent') + self.do_test_tag_assignments_with_filter_keywords(bof=True) + self.do_test_tag_assignments_with_filter_keywords(bof=True, historic='group') + self.do_test_tag_assignments_with_filter_keywords(bof=True, historic='parent') diff --git a/ietf/meeting/tests_js.py b/ietf/meeting/tests_js.py index c840153a1..f797801f4 100644 --- a/ietf/meeting/tests_js.py +++ b/ietf/meeting/tests_js.py @@ -466,6 +466,14 @@ class AgendaTests(MeetingTestCase): area = mars.parent self.do_agenda_view_filter_test('?show=%s' % area.acronym, ['ames', 'mars']) + def test_agenda_view_filter_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']) + 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']) @@ -482,43 +490,6 @@ class AgendaTests(MeetingTestCase): 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']) - - def test_agenda_view_filter_show_and_hide_same_group(self): - self.do_agenda_view_filter_test('?show=mars&hide=mars', []) - - def test_agenda_view_filter_showtypes(self): - self.do_agenda_view_filter_test('?showtypes=plenary', ['ietf']) # ietf has a plenary session - - def test_agenda_view_filter_hidetypes(self): - self.do_agenda_view_filter_test('?hidetypes=plenary', []) - - def test_agenda_view_filter_showtypes_and_hidetypes(self): - self.do_agenda_view_filter_test('?showtypes=plenary&hidetypes=regular', ['ietf']) # ietf has a plenary session - - def test_agenda_view_filter_showtypes_and_hidetypes_same_type(self): - self.do_agenda_view_filter_test('?showtypes=plenary&hidetypes=plenary', []) - - def test_agenda_view_filter_show_and_showtypes(self): - self.do_agenda_view_filter_test('?show=mars&showtypes=plenary', ['mars', 'ietf']) # ietf has a plenary session - - def test_agenda_view_filter_show_and_hidetypes(self): - self.do_agenda_view_filter_test('?show=ietf,mars&hidetypes=plenary', ['mars']) # ietf has a plenary session - - def test_agenda_view_filter_hide_and_hidetypes(self): - self.do_agenda_view_filter_test('?hide=ietf,mars&hidetypes=plenary', []) - - def test_agenda_view_filter_show_hide_and_showtypes(self): - self.do_agenda_view_filter_test('?show=mars&hide=ames&showtypes=plenary,regular', ['mars', 'ietf']) # ietf has plenary session - - def test_agenda_view_filter_show_hide_and_hidetypes(self): - self.do_agenda_view_filter_test('?show=mars,ietf&hide=ames&hidetypes=plenary', ['mars']) # ietf has plenary session - - def test_agenda_view_filter_all_params(self): - self.do_agenda_view_filter_test('?show=secretariat,ietf&hide=ames&showtypes=regular&hidetypes=plenary', - ['secretariat', 'mars']) - def assert_agenda_item_visibility(self, visible_groups=None): """Assert that correct items are visible in current browser window @@ -692,19 +663,20 @@ class InterimTests(MeetingTestCase): not_visible = set() unexpected = set() entries = self.driver.find_elements_by_css_selector( - 'table#upcoming-meeting-table > tbody > tr.entry' + 'table#upcoming-meeting-table a.ietf-meeting-link, table#upcoming-meeting-table a.interim-meeting-link' ) for entry in entries: - nums = [n for n in expected if n in entry.text] + entry_text = entry.get_attribute('innerHTML').strip() # gets text, even if element is hidden + nums = [n for n in expected if n in entry_text] self.assertLessEqual(len(nums), 1, 'Multiple matching meeting numbers') if len(nums) > 0: # asserted that it's at most 1, so if it's not 0, it's 1. expected.remove(nums[0]) if not entry.is_displayed(): not_visible.add(nums[0]) continue - # Found an unexpected row - this is ok as long as it's hidden + # Found an unexpected row - this is only a problem if it is visible if entry.is_displayed(): - unexpected.add(entry.text) + unexpected.add(entry_text) self.assertEqual(expected, set(), "Missing entries for expected iterim meetings.") self.assertEqual(not_visible, set(), "Hidden rows for expected interim meetings.") diff --git a/ietf/meeting/tests_views.py b/ietf/meeting/tests_views.py index e7b22fa3f..12825e0f2 100644 --- a/ietf/meeting/tests_views.py +++ b/ietf/meeting/tests_views.py @@ -755,29 +755,6 @@ 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( @@ -796,122 +773,6 @@ class MeetingTests(TestCase): expected_session_summaries=[] ) - def test_ical_filter_showtypes(self): - meeting = make_meeting_test_data() - # Show break/plenary types - self.do_ical_filter_test( - meeting, - querystring='?showtypes=break,plenary', - expected_session_summaries=[ - 'IETF Plenary', - 'Morning Break', - ] - ) - - def test_ical_filter_hidetypes(self): - meeting = make_meeting_test_data() - self.do_ical_filter_test( - meeting, - querystring='?hidetypes=plenary', - expected_session_summaries=[] - ) - - def test_ical_filter_showtypes_and_hidetypes(self): - meeting = make_meeting_test_data() - self.do_ical_filter_test( - meeting, - querystring='?showtypes=break&hidetypes=plenary', - expected_session_summaries=[ - 'Morning Break', - ] - ) - - def test_ical_filter_showtypes_and_hidetypes_same_type(self): - meeting = make_meeting_test_data() - self.do_ical_filter_test( - meeting, - querystring='?showtypes=plenary&hidetypes=plenary', - expected_session_summaries=[] - ) - - def test_ical_filter_show_and_showtypes(self): - meeting = make_meeting_test_data() - self.do_ical_filter_test( - meeting, - querystring='?show=mars&showtypes=plenary', - expected_session_summaries=[ - 'IETF Plenary', - 'mars - Martian Special Interest Group', - ] - ) - - def test_ical_filter_hide_and_showtypes(self): - meeting = make_meeting_test_data() - self.do_ical_filter_test( - meeting, - querystring='?hide=ames&showtypes=regular', - expected_session_summaries=[ - 'mars - Martian Special Interest Group', - ] - ) - - def test_ical_filter_show_and_hidetypes(self): - meeting = make_meeting_test_data() - self.do_ical_filter_test( - meeting, - querystring='?show=ietf,mars&hidetypes=plenary', - expected_session_summaries=[ - 'mars - Martian Special Interest Group', - ] - ) - - def test_ical_filter_hide_and_hidetypes(self): - meeting = make_meeting_test_data() - self.do_ical_filter_test( - meeting, - querystring='?hide=ietf,mars&hidetypes=plenary', - expected_session_summaries=[] - ) - - def test_ical_filter_show_hide_and_showtypes(self): - meeting = make_meeting_test_data() - # ames regular session should be suppressed - self.do_ical_filter_test( - meeting, - querystring='?show=ietf&hide=ames&showtypes=regular', - expected_session_summaries=[ - 'IETF Plenary', - 'mars - Martian Special Interest Group', - ] - ) - - def test_ical_filter_show_hide_and_hidetypes(self): - meeting = make_meeting_test_data() - # ietf plenary session should be suppressed - self.do_ical_filter_test( - meeting, - querystring='?show=mars,ietf&hide=ames&hidetypes=plenary', - expected_session_summaries=[ - 'mars - Martian Special Interest Group', - ] - ) - - def test_ical_filter_all_params(self): - meeting = make_meeting_test_data() - # should include Morning Break / Registration due to secretariat in show list - # should include mars SIG because regular in showtypes list - # should not include IETF plenary because plenary in hidetypes list - # should not show ames SIG because ames in hide list - self.do_ical_filter_test( - meeting, - querystring='?show=secretariat,ietf&hide=ames&showtypes=regular&hidetypes=plenary', - expected_session_summaries=[ - 'Morning Break', - 'Registration', - 'mars - Martian Special Interest Group', - ] - ) - 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. @@ -2291,6 +2152,16 @@ class InterimTests(TestCase): ], expected_event_count=4 + meeting.importantdate_set.count()) + def test_upcoming_ical_filter_invalid_syntaxes(self): + make_meeting_test_data() + url = urlreverse('ietf.meeting.views.upcoming_ical') + + r = self.client.get(url + '?unknownparam=mars') + self.assertEqual(r.status_code, 400, 'Unknown parameter should be rejected') + + r = self.client.get(url + '?mars') + self.assertEqual(r.status_code, 400, 'Missing parameter name should be rejected') + def test_upcoming_ical_filter_show(self): r = self.do_upcoming_ical_test('show=mars,ames') assert_ical_response_is_valid(self, r, @@ -2314,111 +2185,6 @@ class InterimTests(TestCase): ], expected_event_count=2) - def test_upcoming_ical_filter_showtypes(self): - r = self.do_upcoming_ical_test('showtypes=regular') - assert_ical_response_is_valid(self, r, - expected_event_summaries=[ - 'ames - Asteroid Mining Equipment Standardization Group', - 'mars - Martian Special Interest Group', - 'IETF 72', - ], - expected_event_count=3) - - def test_upcoming_ical_filter_hidetypes(self): - r = self.do_upcoming_ical_test('hidetypes=regular') - assert_ical_response_is_valid(self, r, expected_event_summaries=['IETF 72']) - - def test_upcoming_ical_filter_showtypes_and_hidetypes(self): - r = self.do_upcoming_ical_test('showtypes=plenary,regular&hidetypes=regular') - assert_ical_response_is_valid(self, r, - expected_event_summaries=[ - 'sg - Some Group', - 'IETF 72', - ], - expected_event_count=2) - - def test_upcoming_ical_filter_show_and_showtypes(self): - r = self.do_upcoming_ical_test('show=mars&showtypes=plenary') - assert_ical_response_is_valid(self, r, - expected_event_summaries=[ - 'mars - Martian Special Interest Group', - 'sg - Some Group', - 'IETF 72', - ], - expected_event_count=3) - - def test_upcoming_ical_filter_show_and_hidetypes(self): - r = self.do_upcoming_ical_test('show=mars,sg&hidetypes=regular') - assert_ical_response_is_valid(self, r, - expected_event_summaries=[ - 'sg - Some Group', - 'IETF 72', - ], - expected_event_count=2) - - def test_upcoming_ical_filter_hide_and_showtypes(self): - r = self.do_upcoming_ical_test('hide=mars&showtypes=regular') - assert_ical_response_is_valid(self, r, - expected_event_summaries=[ - 'ames - Asteroid Mining Equipment Standardization Group', - 'IETF 72', - ], - expected_event_count=2) - - def test_upcoming_ical_filter_hide_and_hidetypes(self): - r = self.do_upcoming_ical_test('hide=mars&hidetypes=regular') - assert_ical_response_is_valid(self, r, expected_event_summaries=['IETF 72'], expected_event_count=1) - - def test_upcoming_ical_filter_show_hide_and_showtypes(self): - r = self.do_upcoming_ical_test('show=ames&hide=mars&showtypes=regular,plenary') - assert_ical_response_is_valid(self, r, - expected_event_summaries=[ - 'ames - Asteroid Mining Equipment Standardization Group', - 'sg - Some Group', - 'IETF 72', - ], - expected_event_count=3) - - def test_upcoming_ical_filter_show_hide_and_hidetypes(self): - r = self.do_upcoming_ical_test('show=ames,sg&hide=mars&hidetypes=regular') - assert_ical_response_is_valid(self, r, - expected_event_summaries=[ - 'sg - Some Group', - 'IETF 72' - ], - expected_event_count=2) - - def test_upcoming_ical_filter_all_params(self): - r = self.do_upcoming_ical_test('show=sg&hide=ames&showtypes=regular&hidetypes=plenary') - 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_show_area(self): - make_meeting_test_data(create_interims=True) - mars = Group.objects.get(acronym='mars') - area = mars.parent - r = self.do_upcoming_ical_test('show=%s' % area.acronym, - create_meeting=False) - assert_ical_response_is_valid(self, r, - expected_event_summaries=[ - 'ames - Asteroid Mining Equipment Standardization Group', - 'mars - Martian Special Interest Group', - 'IETF 72', - ], - expected_event_count=3) - - def test_upcoming_ical_filter_hide_area(self): - make_meeting_test_data(create_interims=True) - mars = Group.objects.get(acronym='mars') - area = mars.parent - r = self.do_upcoming_ical_test('show=mars&hide=%s' % area.acronym, - create_meeting=False) - assert_ical_response_is_valid(self, r, expected_event_summaries=['IETF 72'], expected_event_count=1) - def test_upcoming_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 6241f1b4d..6b6939ff5 100644 --- a/ietf/meeting/views.py +++ b/ietf/meeting/views.py @@ -66,6 +66,7 @@ from ietf.meeting.helpers import get_wg_list, find_ads_for_meeting from ietf.meeting.helpers import get_meeting, get_ietf_meeting, get_current_ietf_meeting_num from ietf.meeting.helpers import get_schedule, schedule_permissions from ietf.meeting.helpers import preprocess_assignments_for_agenda, read_agenda_file +from ietf.meeting.helpers import filter_keywords_for_session, tag_assignments_with_filter_keywords from ietf.meeting.helpers import convert_draft_to_pdf, get_earliest_session_date from ietf.meeting.helpers import can_view_interim_request, can_approve_interim_request from ietf.meeting.helpers import can_edit_interim_request @@ -1339,6 +1340,7 @@ def agenda(request, num=None, name=None, base=None, ext=None, owner=None, utc="" timeslot__type__private=False, ) filtered_assignments = preprocess_assignments_for_agenda(filtered_assignments, meeting) + tag_assignments_with_filter_keywords(filtered_assignments) if ext == ".csv": return agenda_csv(schedule, filtered_assignments) @@ -1642,7 +1644,8 @@ def week_view(request, num=None, name=None, owner=None): # saturday_after = saturday_before + datetime.timedelta(days=7) # filtered_assignments = filtered_assignments.filter(timeslot__time__gte=saturday_before,timeslot__time__lt=saturday_after) filtered_assignments = preprocess_assignments_for_agenda(filtered_assignments, meeting) - + tag_assignments_with_filter_keywords(filtered_assignments) + items = [] for a in filtered_assignments: # we don't HTML escape any of these as the week-view code is using createTextNode @@ -1658,7 +1661,8 @@ def week_view(request, num=None, name=None, owner=None): day_of_month=a.timeslot.time.strftime("%d").lstrip("0"), year=a.timeslot.time.strftime("%Y"), ), - "type": a.timeslot.type.name + "type": a.timeslot.type.name, + "filter_keywords": ",".join(a.filter_keywords), } if a.session: @@ -3267,9 +3271,11 @@ def upcoming(request): for session in interim_sessions: session.historic_group = session.group + session.filter_keywords = filter_keywords_for_session(session) + entries.extend(list(interim_sessions)) entries.sort(key = lambda o: pytz.utc.localize(datetime.datetime.combine(o.date, datetime.datetime.min.time())) if isinstance(o,Meeting) else o.official_timeslotassignment().timeslot.utc_start_time()) - + # add menu entries menu_entries = get_interim_menu_entries(request) selected_menu_entry = 'upcoming' @@ -3309,7 +3315,10 @@ def upcoming_ical(request): Filters by wg name and session type. """ - filter_params = parse_agenda_filter_params(request.GET) + try: + filter_params = parse_agenda_filter_params(request.GET) + except ValueError as e: + return HttpResponseBadRequest(str(e)) today = datetime.date.today() # get meetings starting 7 days ago -- we'll filter out sessions in the past further down diff --git a/ietf/static/ietf/js/agenda/agenda_filter.js b/ietf/static/ietf/js/agenda/agenda_filter.js index 264c77690..3067ef98c 100644 --- a/ietf/static/ietf/js/agenda/agenda_filter.js +++ b/ietf/static/ietf/js/agenda/agenda_filter.js @@ -1,17 +1,13 @@ -var agenda_filter_for_testing = {}; // methods to be accessed for automated testing -var agenda_filter = function () { +var agenda_filter; // public interface +var agenda_filter_for_testing; // methods to be accessed for automated testing + +// closure to create private scope +(function () { 'use strict' var update_callback // function(filter_params) var enable_non_area = false // if true, show the non-area filters - /* Add to list without duplicates */ - function add_list_item (list, item) { - if (list.indexOf(item) === -1) { - list.push(item); - } - } - /* Remove from list, if present */ function remove_list_item (list, item) { var item_index = list.indexOf(item); @@ -20,13 +16,18 @@ var agenda_filter = function () { } } - /* Add to list if not present, remove if present */ + /* Add to list if not present, remove if present + * + * Returns true if added to the list, otherwise false. + */ function toggle_list_item (list, item) { var item_index = list.indexOf(item); if (item_index === -1) { list.push(item) + return true; } else { list.splice(item_index, 1) + return false; } } @@ -48,101 +49,83 @@ var agenda_filter = function () { if (!qparams[filt] || (qparams[filt] === true)) { return []; } - return $.map(qparams[filt].split(','), function(s){return s.trim();}); + var result = []; + var qp = qparams[filt].split(','); + + for (var ii = 0; ii < qp.length; ii++) { + result.push(qp[ii].trim()); + } + return result; } function get_filter_params (qparams) { - var enabled = !!(qparams.show || qparams.hide || qparams.showtypes || qparams.hidetypes); + var enabled = !!(qparams.show || qparams.hide); return { enabled: enabled, - show_groups: get_filter_from_qparams(qparams, 'show'), - hide_groups: get_filter_from_qparams(qparams, 'hide'), - show_types: get_filter_from_qparams(qparams, 'showtypes'), - hide_types: get_filter_from_qparams(qparams, 'hidetypes'), + show: get_filter_from_qparams(qparams, 'show'), + hide: get_filter_from_qparams(qparams, 'hide') } } - function filtering_is_enabled (filter_params) { - return filter_params['enabled']; + function get_keywords(elt) { + var keywords = $(elt).attr('data-filter-keywords'); + if (keywords) { + return keywords.toLowerCase().split(','); + } + return []; } - function get_area_items (area) { - var types = []; - var groups = []; - var neg_groups = []; + function get_item(elt) { + return elt.text().trim().toLowerCase().replace(/ /g, ''); + } - $('.view.' + area).find('button').each(function (index, elt) { - elt = $(elt) // jquerify - var item = elt.text().trim().toLowerCase() - if (elt.hasClass('picktype')) { - types.push(item) - } else if (elt.hasClass('pickview')) { - groups.push(item); - } else if (elt.hasClass('pickviewneg')) { - neg_groups.push(item) + // utility method - is there a match between two lists of keywords? + function keyword_match(list1, list2) { + for (var ii = 0; ii < list1.length; ii++) { + if (list2.indexOf(list1[ii]) !== -1) { + return true; } + } + return false; + } + + // Find the items corresponding to a keyword + function get_items_with_keyword (keyword) { + var items = []; + + $('.view button.pickview').filter(function(index, elt) { + return keyword_match(get_keywords(elt), [keyword]); + }).each(function (index, elt) { + items.push(get_item($(elt))); }); - return { 'groups': groups, 'neg_groups': neg_groups, 'types': types }; + return items; + } + + function filtering_is_enabled (filter_params) { + return filter_params.enabled; } // Update the filter / customization UI to match the current filter parameters function update_filter_ui (filter_params) { - var area_group_buttons = $('.view .pickview, .pick-area'); - var non_area_header_button = $('button.pick-non-area'); - var non_area_type_buttons = $('.view.non-area .picktype'); - var non_area_group_buttons = $('.view.non-area button.pickviewneg'); + var buttons = $('.pickview'); if (!filtering_is_enabled(filter_params)) { - // Not filtering - set everything to defaults and exit - area_group_buttons.removeClass('active'); - non_area_header_button.removeClass('active'); - non_area_type_buttons.removeClass('active'); - non_area_group_buttons.removeClass('active'); - non_area_group_buttons.addClass('disabled'); + // Not filtering - set to default and exit + buttons.removeClass('active'); return; } // show the customizer - it will stay visible even if filtering is disabled $('#customize').collapse('show') - // Group and area buttons - these are all positive selections - area_group_buttons.each(function (index, elt) { + // Update button state to match visibility + buttons.each(function (index, elt) { elt = $(elt); - var item = elt.text().trim().toLowerCase(); - var area = elt.attr('data-group-area'); - if ((filter_params['hide_groups'].indexOf(item) === -1) // not hidden... - && ((filter_params['show_groups'].indexOf(item) !== -1) // AND shown... - || (area && (filter_params['show_groups'].indexOf(area.trim().toLowerCase()) !== -1))) // OR area shown - ) { - elt.addClass('active'); - } else { - elt.removeClass('active'); - } - }); - - // Non-area buttons need special handling. Only have positive type and negative group buttons. - // Assume non-area heading is disabled, then enable if one of the types is active - non_area_header_button.removeClass('active'); - non_area_group_buttons.addClass('disabled'); - non_area_type_buttons.each(function (index, elt) { - // Positive type selection buttons - elt = $(elt); - var item = elt.text().trim().toLowerCase(); - if ((filter_params['show_types'].indexOf(item) !== -1) - && (filter_params['hide_types'].indexOf(item) === -1)){ - elt.addClass('active'); - non_area_header_button.addClass('active'); - non_area_group_buttons.removeClass('disabled'); - } else { - elt.removeClass('active'); - } - }); - - non_area_group_buttons.each(function (index, elt) { - // Negative group selection buttons - elt = $(elt); - var item = elt.text().trim().toLowerCase(); - if (filter_params['hide_groups'].indexOf(item) === -1) { + var keywords = get_keywords(elt); + keywords.push(get_item(elt)); // treat item as one of its keywords + var hidden = keyword_match(filter_params.hide, keywords); + var shown = keyword_match(filter_params.show, keywords); + if (shown && !hidden) { elt.addClass('active'); } else { elt.removeClass('active'); @@ -163,7 +146,6 @@ var agenda_filter = function () { } } - /* Trigger an update so the user will see the page appropriate for given filter_params * * Updates the URL to match filter_params, then updates the history / display to match @@ -172,17 +154,11 @@ var agenda_filter = function () { function update_filters (filter_params) { var qparams = [] var search = '' - if (filter_params['show_groups'].length > 0) { - qparams.push('show=' + filter_params['show_groups'].join()) + if (filter_params.show.length > 0) { + qparams.push('show=' + filter_params.show.join()) } - if (filter_params['hide_groups'].length > 0) { - qparams.push('hide=' + filter_params['hide_groups'].join()) - } - if (filter_params['show_types'].length > 0) { - qparams.push('showtypes=' + filter_params['show_types'].join()) - } - if (filter_params['hide_types'].length > 0) { - qparams.push('hidetypes=' + filter_params['hide_types'].join()) + if (filter_params.hide.length > 0) { + qparams.push('hide=' + filter_params.hide.join()) } if (qparams.length > 0) { search = '?' + qparams.join('&') @@ -202,27 +178,41 @@ var agenda_filter = function () { /* Helper for pick group/type button handlers - toggles the appropriate parameter entry * elt - the jquery element that was clicked - * param_type - key of the filter param to update (show_groups, show_types, etc) + * param_type - key of the filter param to update (show, hide) */ - function handle_pick_button (elt, param_type) { - var area = elt.attr('data-group-area'); - var item = elt.text().trim().toLowerCase(); + function handle_pick_button (elt) { var fp = get_filter_params(parse_query_params(window.location.search)); - var neg_param_type = { - show_groups: 'hide_groups', - hide_groups: 'show_groups', - show_types: 'hide_types', - hide_types: 'show_types' - }[param_type]; + var item = get_item(elt); - if (area && (fp[param_type].indexOf(area.trim().toLowerCase()) !== -1)) { - // Area is shown - toggle hide list - toggle_list_item(fp[neg_param_type], item); - remove_list_item(fp[param_type], item); + /* Normally toggle in and out of the 'show' list. If this item is active because + * one of its keywords is active, invert the sense and toggle in and out of the + * 'hide' list instead. */ + var inverted = keyword_match(fp.show, get_keywords(elt)); + var just_showed_item = false; + if (inverted) { + toggle_list_item(fp.hide, item); + remove_list_item(fp.show, item); } else { - toggle_list_item(fp[param_type], item); - remove_list_item(fp[neg_param_type], item); + just_showed_item = toggle_list_item(fp.show, item); + remove_list_item(fp.hide, item); } + + /* If we just showed an item, remove its children from the + * show/hide lists to keep things consistent. This way, selecting + * an area will enable all items in the row as one would expect. */ + if (just_showed_item) { + var children = get_items_with_keyword(item); + $.each(children, function(index, child) { + remove_list_item(fp.show, child); + remove_list_item(fp.hide, child); + }); + } + + // If the show list is empty, clear the hide list because there is nothing to hide + if (fp.show.length === 0) { + fp.hide = []; + } + return fp; } @@ -230,88 +220,49 @@ var agenda_filter = function () { return elt.hasClass('disabled'); } - // Various "pick" button handlers - $('.pickview').click(function () { - if (is_disabled($(this))) { return; } - update_filters(handle_pick_button($(this), 'show_groups')) - }); - - $('.pickviewneg').click(function () { - if (is_disabled($(this))) { return; } - update_filters(handle_pick_button($(this), 'hide_groups')) - }); - - $('.picktype').click(function () { - if (is_disabled($(this))) { return; } - var fp = handle_pick_button($(this), 'show_types') - // If we just disabled the last non-area type, clear out the hide groups list. - var items = get_area_items('non-area') - var any_left = false - $.each(items.types, function (index, session_type) { - if (fp['show_types'].indexOf(session_type) !== -1) { - any_left = true - } - }) - if (!any_left) { - fp['hide_groups'] = [] - } - update_filters(fp); - }); - - // Click handler for an area header button - $('.pick-area').click(function() { - if (is_disabled($(this))) { return; } - var fp = handle_pick_button($(this), 'show_groups'); - var items = get_area_items($(this).text().trim().toLowerCase()); - - // Clear all the individual group show/hide options - $.each(items.groups, function(index, group) { - remove_list_item(fp['show_groups'], group); - remove_list_item(fp['hide_groups'], group); + function register_handlers() { + $('.pickview').click(function () { + if (is_disabled($(this))) { return; } + var fp = handle_pick_button($(this)); + update_filters(fp); }); - update_filters(fp); - }); - - // Click handler for the "Non-Area" header button - $('.pick-non-area').click(function () { - var items = get_area_items('non-area'); - - var fp = get_filter_params(parse_query_params(window.location.search)) - if ($(this).hasClass('active')) { - // Were active - disable or hide everything - $.each(items.types, function (index, session_type) { - remove_list_item(fp['show_types'], session_type) - }) - // When no types are shown, no need to hide groups. Empty hide_groups list. - fp['hide_groups'] = [] - } else { - // Were not active - enable or stop hiding everything - $.each(items.types, function (index, session_type) { - add_list_item(fp['show_types'], session_type) - }) - $.each(items.neg_groups, function (index, group) { - remove_list_item(fp['hide_groups'], group) - }) - } - update_filters(fp); - }); - - // Entry point to filtering code when page loads + } + + /* Entry point to filtering code when page loads + * + * This must be called if you are using the HTML template to provide a customization + * button UI. Do not call if you only want to use the parameter parsing routines. + */ function enable () { $(document).ready(function () { - update_view() + register_handlers(); + update_view(); }) } + // utility method - filter a jquery set to those matching a keyword + function rows_matching_filter_keyword(rows, kw) { + return rows.filter(function(index, element) { + var row_kws = get_keywords(element); + return keyword_match(row_kws, [kw.toLowerCase()]); + }); + } + // Make private functions available for unit testing - agenda_filter_for_testing.toggle_list_item = toggle_list_item; - agenda_filter_for_testing.parse_query_params = parse_query_params; + agenda_filter_for_testing = { + parse_query_params: parse_query_params, + toggle_list_item: toggle_list_item + }; - // Public interface methods - return { + // Make public interface methods accessible + agenda_filter = { enable: enable, filtering_is_enabled: filtering_is_enabled, + 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, set_update_callback: function (cb) {update_callback = cb} - } -}(); \ No newline at end of file + }; +})(); \ No newline at end of file diff --git a/ietf/templates/meeting/agenda.html b/ietf/templates/meeting/agenda.html index 78dd6f76e..d800f33bd 100644 --- a/ietf/templates/meeting/agenda.html +++ b/ietf/templates/meeting/agenda.html @@ -125,10 +125,7 @@ {% endif %} {% if item.timeslot.type.slug == 'break' or item.timeslot.type.slug == 'reg' or item.timeslot.type.slug == 'other' %} - +