Rearrange agenda customization UI and add customization UI to upcoming meetings

- Legacy-Id: 18513
This commit is contained in:
Jennifer Richards 2020-09-23 15:03:40 +00:00
parent 4c709bbfa5
commit 7bee3020fd
9 changed files with 973 additions and 332 deletions

View file

@ -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

View file

@ -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")

View file

@ -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,

View file

@ -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}
}
}();

View file

@ -62,76 +62,7 @@
{% endif %}
<div class="panel-group" id="accordion">
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<a data-toggle="collapse" data-parent="#accordion" href="#customize">
<span class="fa fa-caret-down"></span> Customize the agenda view...
</a>
</h4>
</div>
<div id="customize" class="panel-collapse collapse">
<div class="panel-body">
<p>
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.
</p>
{% if group_parents|length %}
<p>Groups displayed in <b><i>italics</i></b> are BOFs.</p>
<table class="table table-condensed">
<thead>
<tr>
{% for p in group_parents %}
<th style="width:{% widthratio 1 group_parents|length 100 %}%">
<button class="btn btn-default btn-block pickview {{p.acronym|lower}}">{{p.acronym|upper}}</button>
</th>
{% endfor %}
</tr>
</thead>
<tbody>
<tr>
{% for p in group_parents %}
<td class="view {{p.acronym|lower}}">
<div class="btn-group-vertical btn-block">
{% for group in p.group_list %}
<div class="btn-group btn-group-xs btn-group-justified">
<button class="btn btn-default pickview {{group.acronym}}">
{% if group.is_bof %}
<i>{{group.acronym}}</i>
{% else %}
{{group.acronym}}
{% endif %}
</button>
</div>
{% endfor %}
</div>
</td>
{% endfor %}
</tr>
</tbody>
</table>
{% else %}
<blockquote><i>No WG / RG data available -- no WG / RG sessions have been scheduled yet.</i></blockquote>
{% endif %}
<p>Also show special sessions of these groups:</p>
<div class="btn-group btn-group-justified">
<div class="btn-group"><button class="btn btn-default pickview iepg"> IEPG</button></div>
<div class="btn-group"><button class="btn btn-default pickview tools"> Tools</button></div>
<div class="btn-group"><button class="btn btn-default pickview edu"> EDU</button></div>
<div class="btn-group"><button class="btn btn-default pickview ietf"> IETF</button></div>
<div class="btn-group"><button class="btn btn-default pickview iesg"> IESG</button></div>
<div class="btn-group"><button class="btn btn-default pickview iab"> IAB</button></div>
<div class="btn-group"><button class="btn btn-default pickview secretariat"> Secretariat</button></div>
</div>
</div>
</div>
</div>
</div>
{% include "meeting/agenda_filter.html" with group_parents=group_parents non_area_filters=True only %}
<h2>Download as .ics</h2>
<p class="buttonlist">
@ -193,7 +124,10 @@
{% endif %}
{% if item.timeslot.type.slug == 'break' or item.timeslot.type.slug == 'reg' or item.timeslot.type.slug == 'other' %}
<tr id="row-{{ item.slug }}" data-item-group={{ item.session.group.acronym }} data-timeslot-type="{{item.timeslot.type.slug}}">
<tr id="row-{{ item.slug }}"
data-item-group="{% if item.session.historic_group %}{{ item.session.historic_group.acronym }}{% endif %}"
data-item-area="{% if item.session.historic_group and item.session.historic_group.historic_parent %}{{ item.session.historic_group.historic_parent.acronym }}{% endif %}"
data-timeslot-type="{{item.timeslot.type.slug}}">
<td class="text-nowrap text-right">
{% 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 %}
<tr id="row-{{item.slug}}" data-item-group={{ item.session.group.acronym }} data-timeslot-type="{{item.timeslot.type.slug}}" data-ske="row-{{ item.slug }}" {% if item.timeslot.type.slug == 'plenary' %}class="{{item.timeslot.type.slug}}danger"{% endif %}>
<tr id="row-{{item.slug}}"
data-item-group="{{ item.session.historic_group.acronym }}"
data-item-area="{% if item.session.historic_group.historic_parent %}{{ item.session.historic_group.historic_parent.acronym }}{% endif %}"
data-timeslot-type="{{item.timeslot.type.slug}}"
data-ske="row-{{ item.slug }}" {% if item.timeslot.type.slug == 'plenary' %}class="{{item.timeslot.type.slug}}danger"{% endif %}>
{% if item.timeslot.type.slug == 'plenary' %}
<th class="text-nowrap text-right">
{% if "-utc" in request.path %}
@ -359,161 +297,86 @@
{% endblock %}
{% block js %}
<script src="{% static 'ietf/js/agenda/agenda_filter.js' %}"></script>
<script>
function parse_query_params(qs) {
var params = {};
qs = 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;
}
// Update the agenda display with specified filters
function update_agenda_display(filter_params) {
var agenda_rows=$('[id^="row-"]')
if (!agenda_filter.filtering_is_enabled(filter_params)) {
// When filtering is not enabled, show all sessions
agenda_rows.show();
return;
}
return params;
}
/* filt = 'show' or 'hide' */
function get_filter_from_qparams(qparams, filt) {
return qparams[filt] ? qparams[filt].split(',') : [];
// if groups were selected for filtering, hide all rows by default
agenda_rows.hide();
// loop through the has items and change the UI element and row visibilities accordingly
$.each(filter_params['show_groups'], function (i, v) {
// this is a regular item by wg: when present, show these rows
agenda_rows.filter('[data-item-group="'+ v +'"]').show();
agenda_rows.filter('[data-item-area="'+ v +'"]').show();
});
$.each(filter_params['show_types'], function (i, v) {
// this is a regular item by type: when present, show these rows
agenda_rows.filter('[data-timeslot-type*="' + v + '"]').show();
});
$.each(filter_params['hide_groups'], function (i, v) {
// this is a "negative" item by wg: when present, hide these rows
agenda_rows.filter('[data-item-group="'+ v +'"]').hide();
agenda_rows.filter('[data-item-area="'+ v +'"]').hide();
});
$.each(filter_params['hide_types'], function (i, v) {
// this is a "negative" item by type: when present, hide these rows
agenda_rows.filter('[data-timeslot-type*="' + v + '"]').hide();
});
}
function get_filter_params(qparams) {
return {
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 toggle_visibility(filter_params) {
// reset UI elements to default state
$(".pickview").removeClass("active disabled");
$(".pickviewneg").addClass("active");
if (filter_params['show_groups'].length ||
filter_params['hide_groups'].length ||
filter_params['show_types'].length ||
filter_params['hide_types'].length
) {
// if groups were selected for filtering, hide all rows by default
$('[id^="row-"]').hide();
// show the customizer
$("#customize").collapse("show");
// loop through the has items and change the UI element and row visibilities accordingly
$.each(filter_params['show_groups'], function (i, v) {
// this is a regular item by wg: when present, show these rows
$('[id^="row-"]').filter('[data-item-group="'+ v +'"]').show();
$(".view." + v).find("button").addClass("active disabled");
$("button.pickview." + v).addClass("active");
});
$.each(filter_params['show_types'], function (i, v) {
// this is a regular item by type: when present, show these rows
$('[id^="row-"]').filter('[data-timeslot-type*="' + v + '"]').show();
});
$.each(filter_params['hide_groups'], function (i, v) {
// this is a "negative" item by wg: when present, hide these rows
$('[id^="row-"]').filter('[data-item-group="'+ v +'"]').hide();
$(".view." + v).find("button").removeClass("active disabled");
$("button.pickviewneg." + v).removeClass("active");
});
$.each(filter_params['hide_types'], function (i, v) {
// this is a "negative" item by type: when present, hide these rows
$('[id^="row-"]').filter('[data-timeslot-type*="' + v + '"]').hide();
});
// show the week view
update_weekview();
$("#weekview").removeClass("hidden");
// show the custom .ics link
$("#ical-link").attr("href",$("#ical-link").attr("href").split("?")[0]+window.location.search);
$("#ical-link").removeClass("hidden");
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 {
// if the hash is empty, show all and hide weekview / custom ical link
$('[id^="row-"]').show();
$("#ical-link, #weekview").addClass("hidden");
ical_link.addClass("hidden");
}
}
$(".pickview, .pickviewneg").click(function () {
// Get clicked item label
var item = $(this).text().trim().toLowerCase();
var fp = get_filter_params(parse_query_params(window.location.search));
function update_weekview(filter_params) {
var weekview = $("#weekview");
if ($(this).hasClass("pickviewneg")) {
toggle_list_item(fp['hide_groups'], item);
} else {
toggle_list_item(fp['show_groups'], item);
}
update_filters(fp);
});
/* Add to list if not present, remove if present */
function toggle_list_item(list, item) {
var item_index = $.inArray(item, list);
if (item_index === -1) {
list.push(item);
} else {
list.splice(item_index, 1);
}
if (!agenda_filter.filtering_is_enabled(filter_params)) {
weekview.addClass("hidden");
return;
}
// Filtering is enabled
weekview.removeClass("hidden");
var wv_iframe = document.getElementById('weekview');
var wv_window = wv_iframe.contentWindow;
var new_url = 'week-view.html' + window.location.search;
if (wv_iframe.src && wv_window.history && wv_window.history.replaceState) {
wv_window.history.replaceState({}, '', new_url);
wv_window.draw_calendar()
} else {
// ho history.replaceState, page reload required
wv_iframe.src = new_url;
}
}
function update_view(filter_params) {
update_agenda_display(filter_params);
update_weekview(filter_params)
update_ical_links(filter_params)
}
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);
toggle_visibility(filter_params);
} else {
// No window.history.replaceState support, page reload required
window.location = new_url;
}
}
function update_weekview() {
var wv_iframe = document.getElementById('weekview');
var wv_window = wv_iframe.contentWindow;
var new_url = 'week-view.html' + window.location.search;
if (wv_iframe.src && wv_window.history && wv_window.history.replaceState) {
wv_window.history.replaceState({}, '', new_url);
wv_window.draw_calendar()
} else {
// ho history.replaceState, page reload required
wv_iframe.src = new_url;
}
}
$(document).ready(function () {
toggle_visibility(
get_filter_params(
parse_query_params(window.location.search)
)
);
});
agenda_filter.set_update_callback(update_view);
agenda_filter.enable();
$(".modal").on("show.bs.modal", function () {
var i = $(this).find(".frame");

View file

@ -0,0 +1,105 @@
<div class="panel-group" id="accordion">
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<a data-toggle="collapse" data-parent="#accordion" href="#customize">
<span class="fa fa-caret-down"></span> Customize the agenda view...
</a>
</h4>
</div>
<div id="customize" class="panel-collapse collapse">
<div class="panel-body">
<p>
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.
</p>
{% if group_parents|length %}
<p>Groups displayed in <b><i>italics</i></b> are BOFs.</p>
<table class="table table-condensed">
<thead>
<tr>
{% for p in group_parents %}
<th style="width:{% widthratio 1 group_parents|length|add:1 100 %}%">
<button class="btn btn-default btn-block pick-area {{ p.acronym|lower }}">{{ p.acronym|upper }}</button>
</th>
{% endfor %}
{% if non_area_filters %}
<th style="width:{% widthratio 1 group_parents|length|add:1 100 %}">
<button class="btn btn-default btn-block pick-non-area">Non-Area</button>
</th>
{% endif %}
</tr>
</thead>
<tbody>
<tr>
{% for p in group_parents %}
<td class="view {{ p.acronym|lower }}">
<div class="btn-group-vertical btn-block">
{% for group in p.group_list %}
<div class="btn-group btn-group-xs btn-group-justified">
<button class="btn btn-default pickview {{ group.acronym }}"
data-group-area="{{ p.acronym|lower }}">
{% if group.is_bof %}
<i>{{ group.acronym }}</i>
{% else %}
{{ group.acronym }}
{% endif %}
</button>
</div>
{% endfor %}
{% endfor %}
{% if non_area_filters %}
<!-- Non-Area buttons -->
<td class="view non-area">
<div class="btn-group-vertical btn-block">
<div class="btn-group btn-group-xs btn-group-justified">
<button class="btn btn-default picktype plenary">Plenary</button>
</div>
<div class="btn-group btn-group-xs btn-group-justified">
<button class="btn btn-default picktype other">Other</button>
</div>
</div>
<div class="btn-group-vertical btn-block">
<div class="btn-group btn-group-xs btn-group-justified">
<button class="btn btn-default pickviewneg edu"> EDU</button>
</div>
<div class="btn-group btn-group-xs btn-group-justified">
<button class="btn btn-default pickviewneg hackathon"> Hackathon</button>
</div>
<div class="btn-group btn-group-xs btn-group-justified">
<button class="btn btn-default pickviewneg iab"> IAB</button>
</div>
<div class="btn-group btn-group-xs btn-group-justified">
<button class="btn btn-default pickviewneg iepg"> IEPG</button>
</div>
<div class="btn-group btn-group-xs btn-group-justified">
<button class="btn btn-default pickviewneg iesg"> IESG</button>
</div>
<div class="btn-group btn-group-xs btn-group-justified">
<button class="btn btn-default pickviewneg ietf"> IETF</button>
</div>
<div class="btn-group btn-group-xs btn-group-justified">
<button class="btn btn-default pickviewneg secretariat"> Secretariat
</button>
</div>
<div class="btn-group btn-group-xs btn-group-justified">
<button class="btn btn-default pickviewneg tools"> Tools</button>
</div>
</div>
</td>
{% endif %}
</tr>
</tbody>
</table>
{% else %}
<blockquote><i>No WG / RG data available -- no WG / RG sessions have been scheduled yet.</i>
</blockquote>
{% endif %}
</div>
</div>
</div>
</div>

View file

@ -1,5 +1,5 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{# Copyright The IETF Trust 2015, 2020, All Rights Reserved #}
{% load origin %}
{% load cache %}
{% load ietf_filters static classname %}
@ -31,7 +31,9 @@
<p>For more on regular IETF meetings see <a href="https://www.ietf.org/meeting/upcoming.html">here</a></p>
{% if menu_entries %}
{% include 'meeting/agenda_filter.html' with group_parents=group_parents only%}
{% if menu_entries %}
<ul class="nav nav-tabs" role="tablist">
{% for name, url in menu_entries %}
<li {% if selected_menu_entry == name.lower %}class="active"{% endif %}>
@ -42,16 +44,18 @@
{% endif %}
{% if menu_actions %}
<div class="buttonlist">
{% for name, url in menu_actions %}
<a class="btn btn-default" href="{{ url }}">{{ name }}</a>
<div id="menu-actions" class="buttonlist">
{% for action in menu_actions %}
<a class="btn btn-default"
data-append-filter="{{ action.append_filter }}"
href="{{ action.url }}">{{ action.label }}</a>
{% endfor %}
</div>
{% endif %}
{% cache 600 upcoming-meetings entries.count %}
{% if entries %}
<table class="table table-condensed table-striped tablesorter">
<table id="upcoming-meeting-table" class="table table-condensed table-striped tablesorter">
<thead>
<tr>
<th>Date</th>
@ -62,7 +66,9 @@
</thead>
<tbody>
{% for entry in entries %}
<tr>
<tr class="entry"
data-item-group="{% if entry.group %}{{ entry.group.acronym|lower }}{% endif %}"
data-item-area="{% if entry.group and entry.group.parent %}{{ entry.group.parent.acronym|lower }}{% endif %}">
{% if entry|classname == 'Meeting' %}
{% with meeting=entry %}
<td>{{ meeting.date }} - {{ meeting.end }}</td>
@ -110,47 +116,152 @@
<script src="{% static "jquery.tablesorter/js/jquery.tablesorter.combined.min.js" %}"></script>
<script src="{% static 'fullcalendar/core/main.js' %}"></script>
<script src="{% static 'fullcalendar/daygrid/main.js' %}"></script>
<script src="{% static 'ietf/js/agenda/agenda_filter.js' %}"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
var calendarEl = document.getElementById('calendar');
var glue = calendarEl.clientWidth > 720 ? ' ' : '\n';
var calendar = new FullCalendar.Calendar(calendarEl, {
plugins: [ 'dayGrid' ],
displayEventTime: false,
events: [
{% for entry in entries %}
{% if entry|classname == 'Meeting' %}
// List of all events with meta-info needed for filtering
var all_event_list = [{% for entry in entries %}
{% if entry|classname == 'Meeting' %}
{% with meeting=entry %}
{
title: 'IETF {{ meeting.number }}',
start: '{{meeting.date}}',
end: '{{meeting.end}}',
url: '{% url 'ietf.meeting.views.agenda' num=meeting.number %}'
}{% if not forloop.last %}, {% endif %}
{
title: 'IETF {{ meeting.number }}',
start: '{{meeting.date}}',
end: '{{meeting.end}}',
url: '{% url 'ietf.meeting.views.agenda' num=meeting.number %}'
}{% if not forloop.last %}, {% endif %}
{% endwith %}
{% else %} {# if it's not a Meeting, it's a Session #}
{% else %} {# if it's not a Meeting, it's a Session #}
{% with session=entry %}
{
title: '{{session.official_timeslotassignment.timeslot.utc_start_time|date:"H:i"}}-{{session.official_timeslotassignment.timeslot.utc_end_time|date:"H:i"}}'+glue+'{{session.group.acronym}}',
start: '{{session.official_timeslotassignment.timeslot.utc_start_time | date:"Y-m-d H:i"}}',
end: '{{session.official_timeslotassignment.timeslot.utc_end_time | date:"Y-m-d H:i"}}',
url: '{% url 'ietf.meeting.views.session_details' num=session.meeting.number acronym=session.group.acronym %}'
}
{
title: '{{session.official_timeslotassignment.timeslot.utc_start_time|date:"H:i"}}-{{session.official_timeslotassignment.timeslot.utc_end_time|date:"H:i"}}',
group: '{% if session.group %}{{session.group.acronym}}{% endif %}',
area: '{% if session.group and session.group.parent %}{{ session.group.parent.acronym }}{% endif %}',
start: '{{session.official_timeslotassignment.timeslot.utc_start_time | date:"Y-m-d H:i"}}',
end: '{{session.official_timeslotassignment.timeslot.utc_end_time | date:"Y-m-d H:i"}}',
url: '{% url 'ietf.meeting.views.session_details' num=session.meeting.number acronym=session.group.acronym %}'
}
{% endwith %}
{% if not forloop.last %}, {% endif %}
{% endif %}
{% endfor %}
],
eventRender: function(info) {
$(info.el).tooltip({title: info.event.title});
},
timeFormat: 'H:mm',
});
{% endif %}
{% endfor %}];
var filtered_event_list = []; // currently visible list
var event_calendar; // handle on the calendar object
calendar.render();
});
// Test whether an event should be visible given a set of filter parameters
function calendar_event_visible(filter_params, event) {
// Visible if filtering is disabled or event has no group
if (!agenda_filter.filtering_is_enabled(filter_params) || !event.group) {
return true;
}
// Exclude if group or area is in the hide_groups list
if (filter_params['hide_groups'].indexOf(event.group) !== -1) {
return false;
}
if (event.area && (filter_params['hide_groups'].indexOf(event.area) !== -1)) {
return false;
}
// Include if group or area is in the show_groups list
if (filter_params['show_groups'].indexOf(event.group) !== -1) {
return true;
}
if (event.area && (filter_params['show_groups'].indexOf(event.area) !== -1)) {
return true;
}
// Not selected, exclude by default
return false;
}
// Apply filter_params to the event list and format data for the calendar
function filter_calendar_events(filter_params, event_list) {
var calendarEl = document.getElementById('calendar');
var glue = calendarEl.clientWidth > 720 ? ' ' : '\n';
var filtered_output = [];
for (var ii = 0; ii < event_list.length; ii++) {
var this_event = event_list[ii];
if (calendar_event_visible(filter_params, this_event)) {
filtered_output.push({
title: this_event.title + (this_event.group ? (glue + this_event.group) : ''),
start: this_event.start,
end: this_event.end,
url: this_event.url
})
}
}
return filtered_output;
}
// Initialize or update the calendar, updating the filtered event list
function update_calendar(filter_params) {
filtered_event_list = filter_calendar_events(filter_params, all_event_list);
if (event_calendar) {
event_calendar.refetchEvents()
} else {
/* Initialize the calendar object.
* The event source is a function that simply returns the current global list of
* filtered events.
*/
var calendarEl = document.getElementById('calendar')
event_calendar = new FullCalendar.Calendar(calendarEl, {
plugins: ['dayGrid'],
displayEventTime: false,
events: function (fInfo, success) {success(filtered_event_list)},
eventRender: function (info) {
$(info.el).tooltip({ title: info.event.title })
},
timeFormat: 'H:mm',
})
event_calendar.render()
}
}
function update_meeting_display(filter_params) {
var meeting_rows = $("#upcoming-meeting-table tr.entry")
if (!agenda_filter.filtering_is_enabled(filter_params)) {
meeting_rows.show();
return;
}
// hide everything that has a group
meeting_rows.filter("[data-item-group!='']").hide();
$.each(filter_params['show_groups'], function (i, v) {
// this is a regular item by wg: when present, show these rows
meeting_rows.filter('[data-item-group="'+ v +'"]').show();
meeting_rows.filter('[data-item-area="'+ v +'"]').show();
});
$.each(filter_params['hide_groups'], function (i, v) {
// this is a "negative" item by wg: when present, hide these rows
meeting_rows.filter('[data-item-group="'+ v +'"]').hide();
meeting_rows.filter('[data-item-area="'+ v +'"]').hide();
});
}
function update_links(filter_params) {
var filtered_links = $("#menu-actions [data-append-filter='True']");
var filtering_enabled = agenda_filter.filtering_is_enabled(filter_params);
filtered_links.each(function(index, elt) {
var orig_link_href = $(elt).attr("href").split("?")[0];
if (filtering_enabled) {
// append new querystring
$(elt).attr("href", orig_link_href+window.location.search);
} else {
// remove querystring
$(elt).attr("href", orig_link_href);
}
});
}
function update_view(filter_params) {
update_meeting_display(filter_params);
update_links(filter_params);
update_calendar(filter_params);
}
// Set up the filtering - the callback will be called when the page loads and on any filter changes
agenda_filter.set_update_callback(update_view);
agenda_filter.enable();
$(".modal").on("show.bs.modal", function () {
var i = $(this).find(".frame");

View file

@ -153,7 +153,8 @@
var item_area = (item.area || '').toLowerCase();
var item_state = (item.state || '').toLowerCase();
if ((fp['hide_groups'].indexOf(item_group) >= 0) ||
if ((fp['hide_groups'].indexOf(item_group) >= 0) ||
(fp['hide_groups'].indexOf(item_area) >= 0) ||
(fp['hide_types'].indexOf(item_type) >= 0)) {
return false;
}

View file

@ -25,7 +25,7 @@ django-tastypie>=0.14.2 # Django 2.1 will require 0.14.2; Django 3.0 wil
django-webtest>=1.9.7
django-widget-tweaks>=1.4.2
docutils>=0.12,!=0.15
factory-boy>=2.9.0
factory-boy>=2.9.0,<3
Faker>=0.8.8,!=0.8.9,!=0.8.10 # from factory-boy # Faker 0.8.9,0.8.10 sometimes return string names instead of unicode.
hashids>=1.1.0
html2text>=2019.8.11