From 82ad0402f8acb703d1cba2e4d86a89b8bce44d61 Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Thu, 1 Jul 2021 23:45:25 +0000
Subject: [PATCH] Add 'Select Sessions" tab to agenda pages. Commit ready for
merge. - Legacy-Id: 19183
---
ietf/meeting/helpers.py | 28 +-
.../templatetags/agenda_custom_tags.py | 8 +
ietf/meeting/tests_js.py | 65 ++-
ietf/meeting/tests_views.py | 58 +++
ietf/meeting/urls.py | 1 +
ietf/meeting/views.py | 266 +++++++-----
ietf/static/ietf/js/agenda/agenda_filter.js | 86 +++-
.../ietf/js/agenda/agenda_personalize.js | 82 ++++
ietf/templates/meeting/agenda.html | 11 +-
ietf/templates/meeting/agenda_filter.html | 18 +-
.../templates/meeting/agenda_personalize.html | 405 ++++++++++++++++++
.../agenda_personalize_buttonlist.html | 20 +
ietf/templates/meeting/meeting_heading.html | 75 ++--
ietf/templates/meeting/upcoming.html | 19 +-
14 files changed, 962 insertions(+), 180 deletions(-)
create mode 100644 ietf/static/ietf/js/agenda/agenda_personalize.js
create mode 100644 ietf/templates/meeting/agenda_personalize.html
create mode 100644 ietf/templates/meeting/agenda_personalize_buttonlist.html
diff --git a/ietf/meeting/helpers.py b/ietf/meeting/helpers.py
index 85c687d42..d0fbbdbfd 100644
--- a/ietf/meeting/helpers.py
+++ b/ietf/meeting/helpers.py
@@ -166,6 +166,16 @@ def get_schedule_by_name(meeting, owner, name):
return meeting.schedule_set.filter(name = name).first()
def preprocess_assignments_for_agenda(assignments_queryset, meeting, extra_prefetches=()):
+ """Add computed properties to assignments
+
+ For each assignment a, adds
+ a.start_timestamp
+ a.end_timestamp
+ a.session.historic_group
+ a.session.historic_parent
+ a.session.rescheduled_to (if rescheduled)
+ a.session.prefetched_active_materials
+ """
assignments_queryset = assignments_queryset.prefetch_related(
'timeslot', 'timeslot__type', 'timeslot__meeting',
'timeslot__location', 'timeslot__location__floorplan', 'timeslot__location__urlresource_set',
@@ -260,9 +270,9 @@ def filter_keywords_for_session(session):
if group.state_id == 'bof':
keywords.add('bof')
keywords.add(group.acronym.lower())
- token = session.docname_token_only_for_multiple()
- if token is not None:
- keywords.add(group.acronym.lower() + "-" + token)
+ specific_kw = filter_keyword_for_specific_session(session)
+ if specific_kw is not None:
+ keywords.add(specific_kw)
area = getattr(group, 'historic_parent', group.parent)
# Only sessions belonging to "regular" groups should respond to the
@@ -276,6 +286,18 @@ def filter_keywords_for_session(session):
keywords.update(['officehours', session.name.lower().replace(' ', '')])
return sorted(list(keywords))
+def filter_keyword_for_specific_session(session):
+ """Get keyword that identifies a specific session
+
+ Returns None if the session cannot be selected individually.
+ """
+ group = getattr(session, 'historic_group', session.group)
+ if group is None:
+ return None
+ kw = group.acronym.lower() # start with this
+ token = session.docname_token_only_for_multiple()
+ return kw if token is None else '{}-{}'.format(kw, token)
+
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/templatetags/agenda_custom_tags.py b/ietf/meeting/templatetags/agenda_custom_tags.py
index b04227f23..30b18e0f3 100644
--- a/ietf/meeting/templatetags/agenda_custom_tags.py
+++ b/ietf/meeting/templatetags/agenda_custom_tags.py
@@ -3,6 +3,7 @@
from django import template
+from django.urls import reverse
register = template.Library()
@@ -60,3 +61,10 @@ def args(obj, arg):
obj.__callArg += [arg]
return obj
+@register.simple_tag(name='webcal_url', takes_context=True)
+def webcal_url(context, viewname, *args, **kwargs):
+ """webcal URL for a view"""
+ return 'webcal://{}{}'.format(
+ context.request.get_host(),
+ reverse(viewname, args=args, kwargs=kwargs)
+ )
\ No newline at end of file
diff --git a/ietf/meeting/tests_js.py b/ietf/meeting/tests_js.py
index f0c616fdb..254db7704 100644
--- a/ietf/meeting/tests_js.py
+++ b/ietf/meeting/tests_js.py
@@ -1355,7 +1355,65 @@ class AgendaTests(IetfSeleniumTestCase):
wait.until(in_iframe_href('tz=america/halifax', 'weekview'))
except:
self.fail('iframe href not updated to contain selected time zone')
-
+
+ def test_agenda_session_selection(self):
+ wait = WebDriverWait(self.driver, 2)
+ url = self.absreverse('ietf.meeting.views.agenda_personalize', kwargs={'num': self.meeting.number})
+ self.driver.get(url)
+
+ # Verify that elements are all updated when the filters change. That the correct elements
+ # have the appropriate classes is a separate test.
+ elements_to_check = self.driver.find_elements_by_css_selector('.agenda-link.filterable')
+ self.assertGreater(len(elements_to_check), 0, 'No elements with agenda links to update were found')
+
+ self.assertFalse(
+ any(checkbox.is_selected()
+ for checkbox in self.driver.find_elements_by_css_selector(
+ 'input.checkbox[name="selected-sessions"]')),
+ 'Sessions were selected before being clicked',
+ )
+
+ mars_checkbox = self.driver.find_element_by_css_selector('input[type="checkbox"][name="selected-sessions"][data-filter-item="mars"]')
+ break_checkbox = self.driver.find_element_by_css_selector('input[type="checkbox"][name="selected-sessions"][data-filter-item="secretariat-sessb"]')
+ registration_checkbox = self.driver.find_element_by_css_selector('input[type="checkbox"][name="selected-sessions"][data-filter-item="secretariat-sessa"]')
+ secretariat_button = self.driver.find_element_by_css_selector('button[data-filter-item="secretariat"]')
+
+ mars_checkbox.click() # select mars session
+ try:
+ wait.until(
+ lambda driver: all('?show=mars' in el.get_attribute('href') for el in elements_to_check)
+ )
+ except TimeoutException:
+ self.fail('Some agenda links were not updated when mars session was selected')
+ self.assertTrue(mars_checkbox.is_selected(), 'mars session checkbox was not selected after being clicked')
+ self.assertFalse(break_checkbox.is_selected(), 'break checkbox was selected without being clicked')
+ self.assertFalse(registration_checkbox.is_selected(), 'registration checkbox was selected without being clicked')
+
+ mars_checkbox.click() # deselect mars session
+ try:
+ wait.until(
+ lambda driver: not any('?show=mars' in el.get_attribute('href') for el in elements_to_check)
+ )
+ except TimeoutException:
+ self.fail('Some agenda links were not updated when mars session was de-selected')
+ self.assertFalse(mars_checkbox.is_selected(), 'mars session checkbox was still selected after being clicked')
+ self.assertFalse(break_checkbox.is_selected(), 'break checkbox was selected without being clicked')
+ self.assertFalse(registration_checkbox.is_selected(), 'registration checkbox was selected without being clicked')
+
+ secretariat_button.click() # turn on all secretariat sessions
+ break_checkbox.click() # also select the break
+
+ try:
+ wait.until(
+ lambda driver: all(
+ '?show=secretariat&hide=secretariat-sessb' in el.get_attribute('href')
+ for el in elements_to_check
+ ))
+ except TimeoutException:
+ self.fail('Some agenda links were not updated when secretariat group but not break was selected')
+ self.assertFalse(mars_checkbox.is_selected(), 'mars session checkbox was unexpectedly selected')
+ self.assertFalse(break_checkbox.is_selected(), 'break checkbox was unexpectedly selected')
+ self.assertTrue(registration_checkbox.is_selected(), 'registration checkbox was expected to be selected')
@ifSeleniumEnabled
class WeekviewTests(IetfSeleniumTestCase):
@@ -1693,7 +1751,10 @@ class InterimTests(IetfSeleniumTestCase):
self.assert_upcoming_view_filter_matches_ics_filter(querystring)
# Check the ical links
- simplified_querystring = querystring.replace(' ', '%20') # encode spaces'
+ simplified_querystring = querystring.replace(' ', '') # remove spaces
+ if simplified_querystring in ['?show=', '?hide=', '?show=&hide=']:
+ simplified_querystring = '' # these empty querystrings will be dropped (not an exhaustive list)
+
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')
diff --git a/ietf/meeting/tests_views.py b/ietf/meeting/tests_views.py
index 85aa9dd31..590149a6c 100644
--- a/ietf/meeting/tests_views.py
+++ b/ietf/meeting/tests_views.py
@@ -35,6 +35,7 @@ from ietf.meeting.helpers import can_approve_interim_request, can_view_interim_r
from ietf.meeting.helpers import send_interim_approval_request
from ietf.meeting.helpers import send_interim_meeting_cancellation_notice, send_interim_session_cancellation_notice
from ietf.meeting.helpers import send_interim_minutes_reminder, populate_important_dates, update_important_dates
+from ietf.meeting.helpers import filter_keyword_for_specific_session
from ietf.meeting.models import Session, TimeSlot, Meeting, SchedTimeSessAssignment, Schedule, SessionPresentation, SlideSubmission, SchedulingEvent, Room, Constraint, ConstraintName
from ietf.meeting.test_data import make_meeting_test_data, make_interim_meeting, make_interim_test_data
from ietf.meeting.utils import finalize, condition_slide_order
@@ -375,6 +376,63 @@ class MeetingTests(TestCase):
self.assertEqual(r_with_tz.status_code,200)
self.assertEqual(r.content, r_with_tz.content)
+ def test_agenda_personalize(self):
+ """Session selection page should have a checkbox for each session with appropriate keywords"""
+ meeting = make_meeting_test_data()
+ url = urlreverse("ietf.meeting.views.agenda_personalize",kwargs=dict(num=meeting.number))
+ r = self.client.get(url)
+ self.assertEqual(r.status_code,200)
+ q = PyQuery(r.content)
+ for assignment in SchedTimeSessAssignment.objects.filter(
+ schedule__in=[meeting.schedule, meeting.schedule.base],
+ timeslot__type__private=False,
+ ):
+ row = q('#row-{}'.format(assignment.slug()))
+ self.assertIsNotNone(row, 'No row for assignment {}'.format(assignment))
+ checkboxes = row('input[type="checkbox"][name="selected-sessions"]')
+ self.assertEqual(len(checkboxes), 1,
+ 'Row for assignment {} does not have a checkbox input'.format(assignment))
+ checkbox = checkboxes.eq(0)
+ self.assertEqual(
+ checkbox.attr('data-filter-item'),
+ filter_keyword_for_specific_session(assignment.session),
+ )
+
+ def test_agenda_personalize_updates_urls(self):
+ """The correct URLs should be updated when filter settings change on the personalize agenda view
+
+ Tests that the expected elements have the necessary classes. The actual update of these fields
+ is tested in the JS tests
+ """
+ meeting = make_meeting_test_data()
+ url = urlreverse("ietf.meeting.views.agenda_personalize",kwargs=dict(num=meeting.number))
+ r = self.client.get(url)
+ self.assertEqual(r.status_code,200)
+ q = PyQuery(r.content)
+
+ # Find all the elements expected to be updated
+ expected_elements = []
+ nav_tab_anchors = q('ul.nav.nav-tabs > li > a')
+ for anchor in nav_tab_anchors.items():
+ text = anchor.text().strip()
+ if text in ['Agenda', 'UTC Agenda', 'Select Sessions']:
+ expected_elements.append(anchor)
+ for btn in q('.buttonlist a.btn').items():
+ text = btn.text().strip()
+ if text in ['View customized agenda', 'Download as .ics', 'Subscribe with webcal']:
+ expected_elements.append(btn)
+
+ # Check that all the expected elements have the correct classes
+ for elt in expected_elements:
+ self.assertTrue(elt.has_class('agenda-link'))
+ self.assertTrue(elt.has_class('filterable'))
+
+ # Finally, check that there are no unexpected elements marked to be updated.
+ # If there are, they should be added to the test above.
+ self.assertEqual(len(expected_elements),
+ len(q('.agenda-link.filterable')),
+ 'Unexpected elements updated')
+
@override_settings(MEETING_MATERIALS_SERVE_LOCALLY=False, MEETING_DOC_HREFS = settings.MEETING_DOC_CDN_HREFS)
def test_materials_through_cdn(self):
meeting = make_meeting_test_data(create_interims=True)
diff --git a/ietf/meeting/urls.py b/ietf/meeting/urls.py
index d7896d5a0..5d7214ceb 100644
--- a/ietf/meeting/urls.py
+++ b/ietf/meeting/urls.py
@@ -47,6 +47,7 @@ type_ietf_only_patterns = [
url(r'^agenda/by-type$', views.agenda_by_type),
url(r'^agenda/by-type/(?P[a-z]+)$', views.agenda_by_type),
url(r'^agenda/by-type/(?P[a-z]+)/ics$', views.agenda_by_type_ics),
+ url(r'^agenda/personalize', views.agenda_personalize),
url(r'^agendas/list$', views.list_schedules),
url(r'^agendas/edit$', RedirectView.as_view(pattern_name='ietf.meeting.views.list_schedules', permanent=True)),
url(r'^agendas/diff/$', views.diff_schedules),
diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py
index 986bbe66d..5ea2f09a1 100644
--- a/ietf/meeting/views.py
+++ b/ietf/meeting/views.py
@@ -65,7 +65,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, is_regular_agenda_filter_group
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 filter_keywords_for_session, tag_assignments_with_filter_keywords, filter_keyword_for_specific_session
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
@@ -1464,6 +1464,119 @@ def session_materials(request, session_id):
assignment = assignments[0]
return render(request, 'meeting/session_materials.html', dict(item=assignment))
+
+def get_assignments_for_agenda(schedule):
+ """Get queryset containing assignments to show on the agenda"""
+ return SchedTimeSessAssignment.objects.filter(
+ schedule__in=[schedule, schedule.base],
+ timeslot__type__private=False,
+ )
+
+
+def extract_groups_hierarchy(prepped_assignments):
+ """Extract groups hierarchy for agenda display
+
+ It's a little bit complicated because we can be dealing with historic groups.
+ """
+ seen = set()
+ groups = [a.session.historic_group for a in prepped_assignments
+ if a.session
+ and a.session.historic_group
+ and is_regular_agenda_filter_group(a.session.historic_group)
+ and a.session.historic_group.historic_parent]
+ group_parents = []
+ for g in groups:
+ if g.historic_parent.acronym not in seen:
+ group_parents.append(g.historic_parent)
+ seen.add(g.historic_parent.acronym)
+
+ seen = set()
+ for p in group_parents:
+ p.group_list = []
+ for g in groups:
+ if g.acronym not in seen and g.historic_parent.acronym == p.acronym:
+ p.group_list.append(g)
+ seen.add(g.acronym)
+
+ p.group_list.sort(key=lambda g: g.acronym)
+ return group_parents
+
+
+def prepare_filter_keywords(tagged_assignments, group_parents):
+ #
+ # The agenda_filter template expects a list of categorized header buttons, each
+ # with a list of children. Make two categories: the IETF areas and the other parent groups.
+ # We also pass a list of 'extra' buttons - currently Office Hours and miscellaneous filters.
+ # All but the last of these are additionally used by the agenda.html template to make
+ # a list of filtered ical buttons. The last group is ignored for this.
+ area_group_filters = []
+ other_group_filters = []
+ extra_filters = []
+
+ for p in group_parents:
+ new_filter = dict(
+ label=p.acronym.upper(),
+ keyword=p.acronym.lower(),
+ children=[
+ dict(
+ label=g.acronym,
+ keyword=g.acronym.lower(),
+ is_bof=g.is_bof(),
+ ) for g in p.group_list
+ ]
+ )
+ if p.type.slug == 'area':
+ area_group_filters.append(new_filter)
+ else:
+ other_group_filters.append(new_filter)
+
+ office_hours_labels = set()
+ for a in tagged_assignments:
+ suffix = ' office hours'
+ if a.session.name.lower().endswith(suffix):
+ office_hours_labels.add(a.session.name[:-len(suffix)].strip())
+
+ if len(office_hours_labels) > 0:
+ # keyword needs to match what's tagged in filter_keywords_for_session()
+ extra_filters.append(dict(
+ label='Office Hours',
+ keyword='officehours',
+ children=[
+ dict(
+ label=label,
+ keyword=label.lower().replace(' ', '')+'officehours',
+ is_bof=False,
+ ) for label in office_hours_labels
+ ]
+ ))
+
+ # Keywords that should appear in 'non-area' column
+ non_area_labels = [
+ 'BoF', 'EDU', 'Hackathon', 'IEPG', 'IESG', 'IETF', 'Plenary', 'Secretariat', 'Tools',
+ ]
+ # Remove any unused non-area keywords
+ non_area_filters = [
+ dict(label=label, keyword=label.lower(), is_bof=False)
+ for label in non_area_labels if any([
+ label.lower() in assignment.filter_keywords
+ for assignment in tagged_assignments
+ ])
+ ]
+ if len(non_area_filters) > 0:
+ extra_filters.append(dict(
+ label=None,
+ keyword=None,
+ children=non_area_filters,
+ ))
+
+ area_group_filters.sort(key=lambda p:p['label'])
+ other_group_filters.sort(key=lambda p:p['label'])
+ filter_categories = [category
+ for category in [area_group_filters, other_group_filters, extra_filters]
+ if len(category) > 0]
+ return filter_categories, non_area_labels
+
+
@ensure_csrf_cookie
def agenda(request, num=None, name=None, base=None, ext=None, owner=None, utc=""):
base = base if base else 'agenda'
@@ -1486,6 +1599,7 @@ def agenda(request, num=None, name=None, base=None, ext=None, owner=None, utc=""
else:
raise Http404("No such meeting")
+ # Select the schedule to show
if name is None:
schedule = get_schedule(meeting, name)
else:
@@ -1497,112 +1611,23 @@ def agenda(request, num=None, name=None, base=None, ext=None, owner=None, utc=""
return render(request, "meeting/no-"+base+ext, {'meeting':meeting }, content_type=mimetype[ext])
updated = meeting.updated()
- filtered_assignments = SchedTimeSessAssignment.objects.filter(
- schedule__in=[schedule, schedule.base],
- timeslot__type__private=False,
+
+ # Select and prepare sessions that should be included
+ filtered_assignments = preprocess_assignments_for_agenda(
+ get_assignments_for_agenda(schedule),
+ meeting
)
- filtered_assignments = preprocess_assignments_for_agenda(filtered_assignments, meeting)
tag_assignments_with_filter_keywords(filtered_assignments)
+ # Done processing for CSV output
if ext == ".csv":
return agenda_csv(schedule, filtered_assignments)
- # extract groups hierarchy, it's a little bit complicated because
- # we can be dealing with historic groups
- seen = set()
- groups = [a.session.historic_group for a in filtered_assignments
- if a.session
- and a.session.historic_group
- and is_regular_agenda_filter_group(a.session.historic_group)
- and a.session.historic_group.historic_parent]
- group_parents = []
- for g in groups:
- if g.historic_parent.acronym not in seen:
- group_parents.append(g.historic_parent)
- seen.add(g.historic_parent.acronym)
-
- seen = set()
- for p in group_parents:
- p.group_list = []
- for g in groups:
- if g.acronym not in seen and g.historic_parent.acronym == p.acronym:
- p.group_list.append(g)
- seen.add(g.acronym)
-
- p.group_list.sort(key=lambda g: g.acronym)
-
- # Groups gathered and processed. Now arrange for the filter UI.
- #
- # The agenda_filter template expects a list of categorized header buttons, each
- # with a list of children. Make two categories: the IETF areas and the other parent groups.
- # We also pass a list of 'extra' buttons - currently Office Hours and miscellaneous filters.
- # All but the last of these are additionally used by the agenda.html template to make
- # a list of filtered ical buttons. The last group is ignored for this.
- area_group_filters = []
- other_group_filters = []
- extra_filters = []
-
- for p in group_parents:
- new_filter = dict(
- label=p.acronym.upper(),
- keyword=p.acronym.lower(),
- children=[
- dict(
- label=g.acronym,
- keyword=g.acronym.lower(),
- is_bof=g.is_bof(),
- ) for g in p.group_list
- ]
- )
- if p.type.slug == 'area':
- area_group_filters.append(new_filter)
- else:
- other_group_filters.append(new_filter)
-
- office_hours_labels = set()
- for a in filtered_assignments:
- suffix = ' office hours'
- if a.session.name.lower().endswith(suffix):
- office_hours_labels.add(a.session.name[:-len(suffix)].strip())
-
- if len(office_hours_labels) > 0:
- # keyword needs to match what's tagged in filter_keywords_for_session()
- extra_filters.append(dict(
- label='Office Hours',
- keyword='officehours',
- children=[
- dict(
- label=label,
- keyword=label.lower().replace(' ', '')+'officehours',
- is_bof=False,
- ) for label in office_hours_labels
- ]
- ))
-
- # Keywords that should appear in 'non-area' column
- non_area_labels = [
- 'BoF', 'EDU', 'Hackathon', 'IEPG', 'IESG', 'IETF', 'Plenary', 'Secretariat', 'Tools',
- ]
- # Remove any unused non-area keywords
- non_area_filters = [
- dict(label=label, keyword=label.lower(), is_bof=False)
- for label in non_area_labels if any([
- label.lower() in assignment.filter_keywords
- for assignment in filtered_assignments
- ])
- ]
- if len(non_area_filters) > 0:
- extra_filters.append(dict(
- label=None,
- keyword=None,
- children=non_area_filters,
- ))
-
- area_group_filters.sort(key=lambda p:p['label'])
- other_group_filters.sort(key=lambda p:p['label'])
- filter_categories = [category
- for category in [area_group_filters, other_group_filters, extra_filters]
- if len(category) > 0]
+ # Now prep the filter UI
+ filter_categories, non_area_labels = prepare_filter_keywords(
+ filtered_assignments,
+ extract_groups_hierarchy(filtered_assignments),
+ )
is_current_meeting = (num is None) or (num == get_current_ietf_meeting_num())
@@ -1756,6 +1781,45 @@ def agenda_by_type_ics(request,num=None,type=None):
updated = meeting.updated()
return render(request,"meeting/agenda.ics",{"schedule":schedule,"updated":updated,"assignments":assignments},content_type="text/calendar")
+
+def agenda_personalize(request, num):
+ meeting = get_ietf_meeting(num) # num may be None, which requests the current meeting
+ if meeting is None or meeting.schedule is None:
+ raise Http404('No such meeting')
+
+ # Select and prepare sessions that should be included
+ filtered_assignments = preprocess_assignments_for_agenda(
+ get_assignments_for_agenda(meeting.schedule),
+ meeting
+ )
+ tag_assignments_with_filter_keywords(filtered_assignments)
+ for assignment in filtered_assignments:
+ # may be None for some sessions
+ assignment.session_keyword = filter_keyword_for_specific_session(assignment.session)
+
+ # Now prep the filter UI
+ filter_categories, non_area_labels = prepare_filter_keywords(
+ filtered_assignments,
+ extract_groups_hierarchy(filtered_assignments),
+ )
+
+ is_current_meeting = (num is None) or (num == get_current_ietf_meeting_num())
+
+ return render(
+ request,
+ "meeting/agenda_personalize.html",
+ {
+ 'schedule': meeting.schedule,
+ 'updated': meeting.updated(),
+ 'filtered_assignments': filtered_assignments,
+ 'filter_categories': filter_categories,
+ 'non_area_labels': non_area_labels,
+ 'timezone': meeting.time_zone,
+ 'is_current_meeting': is_current_meeting,
+ 'cache_time': 150 if is_current_meeting else 3600,
+ }
+ )
+
def session_draft_list(num, acronym):
try:
agendas = Document.objects.filter(type="agenda",
diff --git a/ietf/static/ietf/js/agenda/agenda_filter.js b/ietf/static/ietf/js/agenda/agenda_filter.js
index 58799390b..69d058a63 100644
--- a/ietf/static/ietf/js/agenda/agenda_filter.js
+++ b/ietf/static/ietf/js/agenda/agenda_filter.js
@@ -5,7 +5,14 @@ var agenda_filter_for_testing; // methods to be accessed for automated testing
(function () {
'use strict'
- var update_callback; // function(filter_params)
+ /* n.b., const refers to the opts object itself, not its contents.
+ * Use camelCase for easy translation into element.dataset keys,
+ * which are automatically camel-cased from the data attribute name.
+ * (e.g., data-always-show -> elt.dataset.alwaysShow) */
+ const opts = {
+ alwaysShow: false,
+ updateCallback: null // function(filter_params)
+ };
/* Remove from list, if present */
function remove_list_item (list, item) {
@@ -58,7 +65,7 @@ var agenda_filter_for_testing; // methods to be accessed for automated testing
}
function get_filter_params (qparams) {
- var enabled = !!(qparams.show || qparams.hide);
+ var enabled = opts.alwaysShow || qparams.show || qparams.hide;
return {
enabled: enabled,
show: get_filter_from_qparams(qparams, 'show'),
@@ -114,8 +121,13 @@ var agenda_filter_for_testing; // methods to be accessed for automated testing
return;
}
+ update_href_querystrings(filter_params_as_querystring(filter_params))
+
// show the customizer - it will stay visible even if filtering is disabled
- $('#customize').collapse('show')
+ const customizer = $('#customize');
+ if (customizer.hasClass('collapse')) {
+ customizer.collapse('show')
+ }
// Update button state to match visibility
buttons.each(function (index, elt) {
@@ -140,8 +152,8 @@ var agenda_filter_for_testing; // methods to be accessed for automated testing
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)
+ if (opts.updateCallback) {
+ opts.updateCallback(filter_params)
}
}
@@ -151,20 +163,11 @@ var agenda_filter_for_testing; // methods to be accessed for automated testing
* (if supported) or loads the new URL.
*/
function update_filters (filter_params) {
- var qparams = []
- var search = ''
- if (filter_params.show.length > 0) {
- qparams.push('show=' + filter_params.show.join())
- }
- if (filter_params.hide.length > 0) {
- qparams.push('hide=' + filter_params.hide.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)
+ var new_url = replace_querystring(
+ window.location.href,
+ filter_params_as_querystring(filter_params)
+ )
+ update_href_querystrings(filter_params_as_querystring(filter_params))
if (window.history && window.history.replaceState) {
// Keep current origin, replace search string, no page reload
history.replaceState({}, document.title, new_url)
@@ -175,6 +178,35 @@ var agenda_filter_for_testing; // methods to be accessed for automated testing
}
}
+ /**
+ * Update the querystring in the href filterable agenda links
+ */
+ function update_href_querystrings(querystring) {
+ Array.from(
+ document.getElementsByClassName('agenda-link filterable')
+ ).forEach(
+ (elt) => elt.href = replace_querystring(elt.href, querystring)
+ )
+ }
+
+ function filter_params_as_querystring(filter_params) {
+ var qparams = []
+ if (filter_params.show.length > 0) {
+ qparams.push('show=' + filter_params.show.join())
+ }
+ if (filter_params.hide.length > 0) {
+ qparams.push('hide=' + filter_params.hide.join())
+ }
+ if (qparams.length > 0) {
+ return '?' + qparams.join('&')
+ }
+ return ''
+ }
+
+ function replace_querystring(url, new_querystring) {
+ return url.replace(/(\?.*)?(#.*)?$/, new_querystring + window.location.hash)
+ }
+
/* Helper for pick group/type button handlers - toggles the appropriate parameter entry
* elt - the jquery element that was clicked
*/
@@ -225,7 +257,19 @@ var agenda_filter_for_testing; // methods to be accessed for automated testing
update_filters(fp);
});
}
-
+
+ /**
+ * Read options from the template
+ */
+ function read_template_options() {
+ const opts_elt = document.getElementById('agenda-filter-options');
+ opts.keys().forEach((opt) => {
+ if (opt in opts_elt.dataset) {
+ opts[opt] = opts_elt.dataset[opt];
+ }
+ });
+ }
+
/* Entry point to filtering code when page loads
*
* This must be called if you are using the HTML template to provide a customization
@@ -261,6 +305,6 @@ var agenda_filter_for_testing; // methods to be accessed for automated testing
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}
+ set_update_callback: function (cb) {opts.updateCallback = cb}
};
})();
\ No newline at end of file
diff --git a/ietf/static/ietf/js/agenda/agenda_personalize.js b/ietf/static/ietf/js/agenda/agenda_personalize.js
new file mode 100644
index 000000000..619299e4c
--- /dev/null
+++ b/ietf/static/ietf/js/agenda/agenda_personalize.js
@@ -0,0 +1,82 @@
+// Copyright The IETF Trust 2021, All Rights Reserved
+
+/**
+ * Agenda personalization JS methods
+ *
+ * Requires agenda_timezone.js and timezone.js be included.
+ */
+const agenda_personalize = (
+ function () {
+ 'use strict';
+
+ let meeting_timezone = document.getElementById('initial-data').dataset.timezone;
+ let selection_inputs;
+
+ /**
+ * Update the checkbox state to match the filter parameters
+ */
+ function updateAgendaCheckboxes(filter_params) {
+ selection_inputs.forEach((inp) => {
+ const item_keywords = inp.dataset.filterKeywords.toLowerCase().split(',');
+ if (
+ agenda_filter.keyword_match(item_keywords, filter_params.show)
+ && !agenda_filter.keyword_match(item_keywords, filter_params.hide)
+ ) {
+ inp.checked = true;
+ } else {
+ inp.checked = false;
+ }
+ });
+ }
+
+ function handleFilterParamUpdate(filter_params) {
+ updateAgendaCheckboxes(filter_params);
+ }
+
+ function handleTableClick(event) {
+ if (event.target.name === 'selected-sessions') {
+ // hide the tooltip after clicking on a checkbox
+ const jqElt = jQuery(event.target);
+ if (jqElt.tooltip) {
+ jqElt.tooltip('hide');
+ }
+ }
+ }
+
+ window.addEventListener('load', function () {
+ // Methods/variables here that are not in ietf_timezone or agenda_filter are from agenda_timezone.js
+
+ // First, initialize_moments(). This must be done before calling any of the update methods.
+ // It does not need timezone info, so safe to call before initializing ietf_timezone.
+ initialize_moments(); // fills in moments in the agenda data
+
+ // Now set up callbacks related to ietf_timezone. This must happen before calling initialize().
+ // In particular, set_current_tz_cb() must be called before the update methods are called.
+ set_current_tz_cb(ietf_timezone.get_current_tz); // give agenda_timezone access to this method
+ ietf_timezone.set_tz_change_callback(function (newtz) {
+ update_times(newtz);
+ }
+ );
+
+ // With callbacks in place, call ietf_timezone.initialize(). This will call the tz_change callback
+ // after setting things up.
+ ietf_timezone.initialize(meeting_timezone);
+
+ // Now make other setup calls from agenda_timezone.js
+ add_tooltips();
+ init_timers();
+
+ selection_inputs = document.getElementsByName('selected-sessions');
+
+ agenda_filter.set_update_callback(handleFilterParamUpdate);
+ agenda_filter.enable();
+
+ document.getElementById('agenda-table')
+ .addEventListener('click', handleTableClick);
+ }
+ );
+
+ // export public interface
+ return { meeting_timezone };
+ }
+)();
\ No newline at end of file
diff --git a/ietf/templates/meeting/agenda.html b/ietf/templates/meeting/agenda.html
index 95d93c3d0..70f6fdbd8 100644
--- a/ietf/templates/meeting/agenda.html
+++ b/ietf/templates/meeting/agenda.html
@@ -116,7 +116,7 @@
{% endfor %}
@@ -421,14 +421,7 @@
function update_ical_links(filter_params) {
var ical_link = $("#ical-link");
- if (agenda_filter.filtering_is_enabled(filter_params)) {
- // Replace the query string in the ical link
- var orig_link_href = ical_link.attr("href").split("?")[0];
- ical_link.attr("href", orig_link_href+window.location.search);
- ical_link.removeClass("hidden");
- } else {
- ical_link.addClass("hidden");
- }
+ ical_link.toggleClass("hidden", !agenda_filter.filtering_is_enabled(filter_params));
}
function update_weekview(filter_params) {
diff --git a/ietf/templates/meeting/agenda_filter.html b/ietf/templates/meeting/agenda_filter.html
index b3a9222c2..cd91e7c1f 100644
--- a/ietf/templates/meeting/agenda_filter.html
+++ b/ietf/templates/meeting/agenda_filter.html
@@ -1,14 +1,28 @@
+{% comment %}
+Required parameters:
+ filter_categories - filter description structure (see agenda view for example)
+
+Optional parameters:
+ always_show - if False or absent, menu closes when not in use and "Customize" button is shown
+ customize_button_text - text to show on the "Customize" button (defaults to "Customize...")
+{% endcomment %}
{% load agenda_filter_tags %}
+ Note: IETF agendas are subject to change, up to and during a meeting.
+
+ {% endif %}
+
+ {% include "meeting/agenda_filter.html" with filter_categories=filter_categories always_show=True %}
+
+ {% include "meeting/agenda_personalize_buttonlist.html" with meeting=schedule.meeting only %}
+
+
+ Individual Sessions
+
+
+ Check boxes below to select individual sessions.
+
+
+
+ {% for item in filtered_assignments %}
+
+ {% ifchanged item.timeslot.time|date:"Y-m-d" %}
+
+
+
+
+
+ {# The anchor here needs to be in a div, not in the th, in order for the anchor-target margin hack to work #}
+
+ {{ item.timeslot.time|date:"l, F j, Y" }}
+