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
diff --git a/ietf/templates/meeting/agenda_personalize.html b/ietf/templates/meeting/agenda_personalize.html new file mode 100644 index 000000000..bee86b1d6 --- /dev/null +++ b/ietf/templates/meeting/agenda_personalize.html @@ -0,0 +1,405 @@ +{% extends "base.html" %} +{# Copyright The IETF Trust 2021, All Rights Reserved #} +{% load origin %} +{% load static %} +{% load ietf_filters %} +{% load textfilters %} +{% load htmlfilters %} + +{% block title %} + IETF {{ schedule.meeting.number }} meeting agenda personalization +{% endblock %} + +{% block morecss %} + tr:not(:first-child) th.gap { + height: 3em !important; + background-color: inherit !important; + border: none !important; + } + tr:first-child th.gap { + height: 0 !important; + background-color: inherit !important; + border: none !important; + } + div.tz-display { + margin-bottom: 0.5em; + margin-top: 1em; + text-align: right; + } + .tz-display a { + cursor: pointer; + } + .tz-display label { + font-weight: normal; + } + .tz-display select { + min-width: 15em; + } + #affix .nav li.tz-display { + padding: 4px 20px; + } + #affix .nav li.tz-display a { + display: inline; + padding: 0; + } +{% endblock %} + +{% block bodyAttrs %}data-spy="scroll" data-target="#affix"{% endblock %} + +{% block content %} + {% origin %} + +
+ 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 %} + ++ Check boxes below to select individual sessions. +
+ ++ | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|
+ {# 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" }} + | +||||||||||
+ | + + {% include "meeting/timeslot_start_end.html" %} + + | ++ | + + {{ item.timeslot.time|date:"l" }} + {{ item.timeslot.name|capfirst_allcaps }} ++ | |||||||
+ {% if item.session_keyword %} + + {% endif %} + | ++ + {% include "meeting/timeslot_start_end.html" %} + + | ++ {{ item.timeslot.get_html_location }} + {% elif item.timeslot.location.floorplan %} + {{ item.timeslot.get_html_location }} + {% else %} + {{ item.timeslot.get_html_location }} + {% endif %} + {% with item.timeslot.location.floorplan as floor %} + {% if item.timeslot.location.floorplan %} + + {{ floor.short }} + + {% endif %} + {% endwith %} + {% endif %} + | + + {% if item.timeslot.show_location and item.timeslot.get_html_location %} + {% if schedule.meeting.number|add:"0" < 96 %} ++ {% if item.session.agenda %} + + {{ item.timeslot.name }} + + {% else %} + {{ item.timeslot.name }} + {% endif %} + + {% if item.session.current_status == 'canceled' %} + CANCELLED + {% endif %} + | ++ | ||||||
+ {% if item.session_keyword %} + + {% endif %} + | + {% if item.timeslot.type.slug == 'plenary' %} ++ + {% include "meeting/timeslot_start_end.html" %} + + | ++ {{ item.timeslot.get_html_location }} + {% elif item.timeslot.location.floorplan %} + {{ item.timeslot.get_html_location }} + {% else %} + {{ item.timeslot.get_html_location }} + {% endif %} + {% endif %} + | + + {% else %} + + {% if item.timeslot.show_location and item.timeslot.get_html_location %} + {% if schedule.meeting.number|add:"0" < 96 %} ++ {% with item.timeslot.location.floorplan as floor %} + {% if item.timeslot.location.floorplan %} + + {{ floor.short }} + + {% endif %} + {% endwith %} + | ++ {% if item.timeslot.show_location and item.timeslot.get_html_location %} + {% if schedule.meeting.number|add:"0" < 96 %} + {{ item.timeslot.get_html_location }} + {% elif item.timeslot.location.floorplan %} + {{ item.timeslot.get_html_location }} + {% else %} + {{ item.timeslot.get_html_location }} + {% endif %} + {% endif %} + | + +{{ item.session.historic_group.historic_parent.acronym }} | + ++ {% if item.session.historic_group %} + {{ item.session.historic_group.acronym }} + {% else %} + {{ item.session.historic_group.acronym }} + {% endif %} + | + {% endif %} + +
+ {% if item.session.agenda %}
+
+ {% endif %}
+ {% if item.timeslot.type.slug == 'plenary' %}
+ {{ item.timeslot.name }}
+ {% else %}
+ {{ item.session.historic_group.name }}
+ {% endif %}
+ {% if item.session.agenda %}
+
+ {% endif %}
+
+ {% if item.session.current_status == 'canceled' %}
+ CANCELLED
+ {% endif %}
+
+ {% if item.session.historic_group.state_id == "bof" %}
+ BOF
+ {% endif %}
+
+ {% if item.session.current_status == 'resched' %}
+
+ RESCHEDULED
+ {% if item.session.rescheduled_to %}
+ TO
+
+ {% if "-utc" in request.path %}
+ {{ item.session.rescheduled_to.utc_start_time|date:"l G:i"|upper }}-
+ {{ item.session.rescheduled_to.utc_end_time|date:"G:i" }}
+ {% else %}
+ {{ item.session.rescheduled_to.time|date:"l G:i"|upper }}-
+ {{ item.session.rescheduled_to.end_time|date:"G:i" }}
+ {% endif %}
+
+ {% endif %}
+
+ {% endif %}
+
+ {% if item.session.agenda_note|first_url|conference_url %}
+ + {{ item.session.agenda_note|slice:":23" }} + {% elif item.session.agenda_note %} + {{ item.session.agenda_note }} + {% endif %} + |
+ + |