Rework agenda filters with show/hide keywords in place of 'types' and add bof / AD office hours buttons

- Legacy-Id: 18563
This commit is contained in:
Jennifer Richards 2020-10-05 13:55:52 +00:00
parent 97dd600a9b
commit 1b1bc24744
10 changed files with 352 additions and 608 deletions

View file

@ -239,6 +239,29 @@ def preprocess_assignments_for_agenda(assignments_queryset, meeting, extra_prefe
return assignments
def tag_assignments_with_filter_keywords(assignments):
"""Add keywords for agenda filtering
Keywords are all lower case.
"""
for a in assignments:
a.filter_keywords = [a.timeslot.type.slug.lower()]
a.filter_keywords.extend(filter_keywords_for_session(a.session))
def filter_keywords_for_session(session):
keywords = []
group = getattr(session, 'historic_group', session.group)
if group is not None:
if group.state_id == 'bof':
keywords.append('bof')
keywords.append(group.acronym.lower())
area = getattr(group, 'historic_parent', group.parent)
if area is not None:
keywords.append(area.acronym.lower())
if session.name.lower().endswith('office hours'):
keywords.append('adofficehours')
return keywords
def read_session_file(type, num, doc):
# XXXX FIXME: the path fragment in the code below should be moved to
# settings.py. The *_PATH settings should be generalized to format()

View file

@ -0,0 +1,96 @@
# Copyright The IETF Trust 2020, All Rights Reserved
# -*- coding: utf-8 -*-
from ietf.group.factories import GroupFactory
from ietf.meeting.factories import SessionFactory, MeetingFactory
from ietf.meeting.helpers import tag_assignments_with_filter_keywords
from ietf.utils.test_utils import TestCase
class HelpersTests(TestCase):
def do_test_tag_assignments_with_filter_keywords(self, bof=False, historic=None):
"""Assignments should be tagged properly
The historic param can be None, group, or parent, to specify whether to test
with no historic_group, a historic_group but no historic_parent, or both.
"""
meeting_types = ['regular', 'plenary']
group_state_id = 'bof' if bof else 'active'
group = GroupFactory(state_id=group_state_id)
historic_group = GroupFactory(state_id=group_state_id)
historic_parent = GroupFactory(type_id='area')
if historic == 'parent':
historic_group.historic_parent = historic_parent
# Create meeting and sessions
meeting = MeetingFactory()
for meeting_type in meeting_types:
sess = SessionFactory(group=group, meeting=meeting, type_id=meeting_type)
ts = sess.timeslotassignments.first().timeslot
ts.type = sess.type
ts.save()
# Create an office hours session in the group's area (i.e., parent). This is not
# currently really needed, but will protect against areas and groups diverging
# in a way that breaks keywording.
office_hours = SessionFactory(
name='some office hours',
group=group.parent,
meeting=meeting,
type_id='other'
)
ts = office_hours.timeslotassignments.first().timeslot
ts.type = office_hours.type
ts.save()
assignments = meeting.schedule.assignments.all()
orig_num_assignments = len(assignments)
# Set up historic groups if needed
if historic:
for a in assignments:
if a.session != office_hours:
a.session.historic_group = historic_group
# Execute the method under test
tag_assignments_with_filter_keywords(assignments)
# Assert expected results
self.assertEqual(len(assignments), orig_num_assignments, 'Should not change number of assignments')
if historic:
expected_group = historic_group
expected_area = historic_parent if historic == 'parent' else historic_group.parent
else:
expected_group = group
expected_area = group.parent
for assignment in assignments:
expected_filter_keywords = [assignment.timeslot.type.slug]
if assignment.session == office_hours:
expected_filter_keywords.extend([
group.parent.acronym,
'adofficehours',
])
else:
expected_filter_keywords.extend([
expected_group.acronym,
expected_area.acronym
])
if bof:
expected_filter_keywords.append('bof')
self.assertCountEqual(
assignment.filter_keywords,
expected_filter_keywords,
'Assignment has incorrect filter keywords'
)
def test_tag_assignments_with_filter_keywords(self):
self.do_test_tag_assignments_with_filter_keywords()
self.do_test_tag_assignments_with_filter_keywords(historic='group')
self.do_test_tag_assignments_with_filter_keywords(historic='parent')
self.do_test_tag_assignments_with_filter_keywords(bof=True)
self.do_test_tag_assignments_with_filter_keywords(bof=True, historic='group')
self.do_test_tag_assignments_with_filter_keywords(bof=True, historic='parent')

View file

@ -466,6 +466,14 @@ class AgendaTests(MeetingTestCase):
area = mars.parent
self.do_agenda_view_filter_test('?show=%s' % area.acronym, ['ames', 'mars'])
def test_agenda_view_filter_bof(self):
mars = Group.objects.get(acronym='mars')
mars.state_id = 'bof'
mars.save()
self.do_agenda_view_filter_test('?show=bof', ['mars'])
self.do_agenda_view_filter_test('?show=bof,mars', ['mars'])
self.do_agenda_view_filter_test('?show=bof,ames', ['mars','ames'])
def test_agenda_view_filter_show_two(self):
"""Filtered agenda view should display only matching rows (two groups selected)"""
self.do_agenda_view_filter_test('?show=mars,ames', ['mars', 'ames'])
@ -482,43 +490,6 @@ class AgendaTests(MeetingTestCase):
area = mars.parent
self.do_agenda_view_filter_test('?show=mars&hide=%s' % area.acronym, [])
def test_agenda_view_filter_show_and_hide(self):
self.do_agenda_view_filter_test('?show=mars&hide=ietf', ['mars'])
def test_agenda_view_filter_show_and_hide_same_group(self):
self.do_agenda_view_filter_test('?show=mars&hide=mars', [])
def test_agenda_view_filter_showtypes(self):
self.do_agenda_view_filter_test('?showtypes=plenary', ['ietf']) # ietf has a plenary session
def test_agenda_view_filter_hidetypes(self):
self.do_agenda_view_filter_test('?hidetypes=plenary', [])
def test_agenda_view_filter_showtypes_and_hidetypes(self):
self.do_agenda_view_filter_test('?showtypes=plenary&hidetypes=regular', ['ietf']) # ietf has a plenary session
def test_agenda_view_filter_showtypes_and_hidetypes_same_type(self):
self.do_agenda_view_filter_test('?showtypes=plenary&hidetypes=plenary', [])
def test_agenda_view_filter_show_and_showtypes(self):
self.do_agenda_view_filter_test('?show=mars&showtypes=plenary', ['mars', 'ietf']) # ietf has a plenary session
def test_agenda_view_filter_show_and_hidetypes(self):
self.do_agenda_view_filter_test('?show=ietf,mars&hidetypes=plenary', ['mars']) # ietf has a plenary session
def test_agenda_view_filter_hide_and_hidetypes(self):
self.do_agenda_view_filter_test('?hide=ietf,mars&hidetypes=plenary', [])
def test_agenda_view_filter_show_hide_and_showtypes(self):
self.do_agenda_view_filter_test('?show=mars&hide=ames&showtypes=plenary,regular', ['mars', 'ietf']) # ietf has plenary session
def test_agenda_view_filter_show_hide_and_hidetypes(self):
self.do_agenda_view_filter_test('?show=mars,ietf&hide=ames&hidetypes=plenary', ['mars']) # ietf has plenary session
def test_agenda_view_filter_all_params(self):
self.do_agenda_view_filter_test('?show=secretariat,ietf&hide=ames&showtypes=regular&hidetypes=plenary',
['secretariat', 'mars'])
def assert_agenda_item_visibility(self, visible_groups=None):
"""Assert that correct items are visible in current browser window
@ -692,19 +663,20 @@ class InterimTests(MeetingTestCase):
not_visible = set()
unexpected = set()
entries = self.driver.find_elements_by_css_selector(
'table#upcoming-meeting-table > tbody > tr.entry'
'table#upcoming-meeting-table a.ietf-meeting-link, table#upcoming-meeting-table a.interim-meeting-link'
)
for entry in entries:
nums = [n for n in expected if n in entry.text]
entry_text = entry.get_attribute('innerHTML').strip() # gets text, even if element is hidden
nums = [n for n in expected if n in entry_text]
self.assertLessEqual(len(nums), 1, 'Multiple matching meeting numbers')
if len(nums) > 0: # asserted that it's at most 1, so if it's not 0, it's 1.
expected.remove(nums[0])
if not entry.is_displayed():
not_visible.add(nums[0])
continue
# Found an unexpected row - this is ok as long as it's hidden
# Found an unexpected row - this is only a problem if it is visible
if entry.is_displayed():
unexpected.add(entry.text)
unexpected.add(entry_text)
self.assertEqual(expected, set(), "Missing entries for expected iterim meetings.")
self.assertEqual(not_visible, set(), "Hidden rows for expected interim meetings.")

View file

@ -755,29 +755,6 @@ class MeetingTests(TestCase):
expected_session_summaries=[]
)
def test_ical_filter_show_area(self):
meeting = make_meeting_test_data()
mars = Group.objects.get(acronym='mars')
area = mars.parent
self.do_ical_filter_test(
meeting,
querystring='?show=%s' % area.acronym,
expected_session_summaries=[
'ames - Asteroid Mining Equipment Standardization Group',
'mars - Martian Special Interest Group',
]
)
def test_ical_filter_hide_area(self):
meeting = make_meeting_test_data()
mars = Group.objects.get(acronym='mars')
area = mars.parent
self.do_ical_filter_test(
meeting,
querystring='?show=mars&hide=%s' % area.acronym,
expected_session_summaries=[]
)
def test_ical_filter_show_and_hide(self):
meeting = make_meeting_test_data()
self.do_ical_filter_test(
@ -796,122 +773,6 @@ class MeetingTests(TestCase):
expected_session_summaries=[]
)
def test_ical_filter_showtypes(self):
meeting = make_meeting_test_data()
# Show break/plenary types
self.do_ical_filter_test(
meeting,
querystring='?showtypes=break,plenary',
expected_session_summaries=[
'IETF Plenary',
'Morning Break',
]
)
def test_ical_filter_hidetypes(self):
meeting = make_meeting_test_data()
self.do_ical_filter_test(
meeting,
querystring='?hidetypes=plenary',
expected_session_summaries=[]
)
def test_ical_filter_showtypes_and_hidetypes(self):
meeting = make_meeting_test_data()
self.do_ical_filter_test(
meeting,
querystring='?showtypes=break&hidetypes=plenary',
expected_session_summaries=[
'Morning Break',
]
)
def test_ical_filter_showtypes_and_hidetypes_same_type(self):
meeting = make_meeting_test_data()
self.do_ical_filter_test(
meeting,
querystring='?showtypes=plenary&hidetypes=plenary',
expected_session_summaries=[]
)
def test_ical_filter_show_and_showtypes(self):
meeting = make_meeting_test_data()
self.do_ical_filter_test(
meeting,
querystring='?show=mars&showtypes=plenary',
expected_session_summaries=[
'IETF Plenary',
'mars - Martian Special Interest Group',
]
)
def test_ical_filter_hide_and_showtypes(self):
meeting = make_meeting_test_data()
self.do_ical_filter_test(
meeting,
querystring='?hide=ames&showtypes=regular',
expected_session_summaries=[
'mars - Martian Special Interest Group',
]
)
def test_ical_filter_show_and_hidetypes(self):
meeting = make_meeting_test_data()
self.do_ical_filter_test(
meeting,
querystring='?show=ietf,mars&hidetypes=plenary',
expected_session_summaries=[
'mars - Martian Special Interest Group',
]
)
def test_ical_filter_hide_and_hidetypes(self):
meeting = make_meeting_test_data()
self.do_ical_filter_test(
meeting,
querystring='?hide=ietf,mars&hidetypes=plenary',
expected_session_summaries=[]
)
def test_ical_filter_show_hide_and_showtypes(self):
meeting = make_meeting_test_data()
# ames regular session should be suppressed
self.do_ical_filter_test(
meeting,
querystring='?show=ietf&hide=ames&showtypes=regular',
expected_session_summaries=[
'IETF Plenary',
'mars - Martian Special Interest Group',
]
)
def test_ical_filter_show_hide_and_hidetypes(self):
meeting = make_meeting_test_data()
# ietf plenary session should be suppressed
self.do_ical_filter_test(
meeting,
querystring='?show=mars,ietf&hide=ames&hidetypes=plenary',
expected_session_summaries=[
'mars - Martian Special Interest Group',
]
)
def test_ical_filter_all_params(self):
meeting = make_meeting_test_data()
# should include Morning Break / Registration due to secretariat in show list
# should include mars SIG because regular in showtypes list
# should not include IETF plenary because plenary in hidetypes list
# should not show ames SIG because ames in hide list
self.do_ical_filter_test(
meeting,
querystring='?show=secretariat,ietf&hide=ames&showtypes=regular&hidetypes=plenary',
expected_session_summaries=[
'Morning Break',
'Registration',
'mars - Martian Special Interest Group',
]
)
def build_session_setup(self):
# This setup is intentionally unusual - the session has one draft attached as a session presentation,
# but lists a different on in its agenda. The expectation is that the pdf and tgz views will return both.
@ -2291,6 +2152,16 @@ class InterimTests(TestCase):
],
expected_event_count=4 + meeting.importantdate_set.count())
def test_upcoming_ical_filter_invalid_syntaxes(self):
make_meeting_test_data()
url = urlreverse('ietf.meeting.views.upcoming_ical')
r = self.client.get(url + '?unknownparam=mars')
self.assertEqual(r.status_code, 400, 'Unknown parameter should be rejected')
r = self.client.get(url + '?mars')
self.assertEqual(r.status_code, 400, 'Missing parameter name should be rejected')
def test_upcoming_ical_filter_show(self):
r = self.do_upcoming_ical_test('show=mars,ames')
assert_ical_response_is_valid(self, r,
@ -2314,111 +2185,6 @@ class InterimTests(TestCase):
],
expected_event_count=2)
def test_upcoming_ical_filter_showtypes(self):
r = self.do_upcoming_ical_test('showtypes=regular')
assert_ical_response_is_valid(self, r,
expected_event_summaries=[
'ames - Asteroid Mining Equipment Standardization Group',
'mars - Martian Special Interest Group',
'IETF 72',
],
expected_event_count=3)
def test_upcoming_ical_filter_hidetypes(self):
r = self.do_upcoming_ical_test('hidetypes=regular')
assert_ical_response_is_valid(self, r, expected_event_summaries=['IETF 72'])
def test_upcoming_ical_filter_showtypes_and_hidetypes(self):
r = self.do_upcoming_ical_test('showtypes=plenary,regular&hidetypes=regular')
assert_ical_response_is_valid(self, r,
expected_event_summaries=[
'sg - Some Group',
'IETF 72',
],
expected_event_count=2)
def test_upcoming_ical_filter_show_and_showtypes(self):
r = self.do_upcoming_ical_test('show=mars&showtypes=plenary')
assert_ical_response_is_valid(self, r,
expected_event_summaries=[
'mars - Martian Special Interest Group',
'sg - Some Group',
'IETF 72',
],
expected_event_count=3)
def test_upcoming_ical_filter_show_and_hidetypes(self):
r = self.do_upcoming_ical_test('show=mars,sg&hidetypes=regular')
assert_ical_response_is_valid(self, r,
expected_event_summaries=[
'sg - Some Group',
'IETF 72',
],
expected_event_count=2)
def test_upcoming_ical_filter_hide_and_showtypes(self):
r = self.do_upcoming_ical_test('hide=mars&showtypes=regular')
assert_ical_response_is_valid(self, r,
expected_event_summaries=[
'ames - Asteroid Mining Equipment Standardization Group',
'IETF 72',
],
expected_event_count=2)
def test_upcoming_ical_filter_hide_and_hidetypes(self):
r = self.do_upcoming_ical_test('hide=mars&hidetypes=regular')
assert_ical_response_is_valid(self, r, expected_event_summaries=['IETF 72'], expected_event_count=1)
def test_upcoming_ical_filter_show_hide_and_showtypes(self):
r = self.do_upcoming_ical_test('show=ames&hide=mars&showtypes=regular,plenary')
assert_ical_response_is_valid(self, r,
expected_event_summaries=[
'ames - Asteroid Mining Equipment Standardization Group',
'sg - Some Group',
'IETF 72',
],
expected_event_count=3)
def test_upcoming_ical_filter_show_hide_and_hidetypes(self):
r = self.do_upcoming_ical_test('show=ames,sg&hide=mars&hidetypes=regular')
assert_ical_response_is_valid(self, r,
expected_event_summaries=[
'sg - Some Group',
'IETF 72'
],
expected_event_count=2)
def test_upcoming_ical_filter_all_params(self):
r = self.do_upcoming_ical_test('show=sg&hide=ames&showtypes=regular&hidetypes=plenary')
assert_ical_response_is_valid(self, r,
expected_event_summaries=[
'mars - Martian Special Interest Group',
'IETF 72',
],
expected_event_count=2)
def test_upcoming_ical_filter_show_area(self):
make_meeting_test_data(create_interims=True)
mars = Group.objects.get(acronym='mars')
area = mars.parent
r = self.do_upcoming_ical_test('show=%s' % area.acronym,
create_meeting=False)
assert_ical_response_is_valid(self, r,
expected_event_summaries=[
'ames - Asteroid Mining Equipment Standardization Group',
'mars - Martian Special Interest Group',
'IETF 72',
],
expected_event_count=3)
def test_upcoming_ical_filter_hide_area(self):
make_meeting_test_data(create_interims=True)
mars = Group.objects.get(acronym='mars')
area = mars.parent
r = self.do_upcoming_ical_test('show=mars&hide=%s' % area.acronym,
create_meeting=False)
assert_ical_response_is_valid(self, r, expected_event_summaries=['IETF 72'], expected_event_count=1)
def test_upcoming_json(self):
make_meeting_test_data(create_interims=True)
url = urlreverse("ietf.meeting.views.upcoming_json")

View file

@ -66,6 +66,7 @@ from ietf.meeting.helpers import get_wg_list, find_ads_for_meeting
from ietf.meeting.helpers import get_meeting, get_ietf_meeting, get_current_ietf_meeting_num
from ietf.meeting.helpers import get_schedule, schedule_permissions
from ietf.meeting.helpers import preprocess_assignments_for_agenda, read_agenda_file
from ietf.meeting.helpers import filter_keywords_for_session, tag_assignments_with_filter_keywords
from ietf.meeting.helpers import convert_draft_to_pdf, get_earliest_session_date
from ietf.meeting.helpers import can_view_interim_request, can_approve_interim_request
from ietf.meeting.helpers import can_edit_interim_request
@ -1339,6 +1340,7 @@ def agenda(request, num=None, name=None, base=None, ext=None, owner=None, utc=""
timeslot__type__private=False,
)
filtered_assignments = preprocess_assignments_for_agenda(filtered_assignments, meeting)
tag_assignments_with_filter_keywords(filtered_assignments)
if ext == ".csv":
return agenda_csv(schedule, filtered_assignments)
@ -1642,7 +1644,8 @@ def week_view(request, num=None, name=None, owner=None):
# saturday_after = saturday_before + datetime.timedelta(days=7)
# filtered_assignments = filtered_assignments.filter(timeslot__time__gte=saturday_before,timeslot__time__lt=saturday_after)
filtered_assignments = preprocess_assignments_for_agenda(filtered_assignments, meeting)
tag_assignments_with_filter_keywords(filtered_assignments)
items = []
for a in filtered_assignments:
# we don't HTML escape any of these as the week-view code is using createTextNode
@ -1658,7 +1661,8 @@ def week_view(request, num=None, name=None, owner=None):
day_of_month=a.timeslot.time.strftime("%d").lstrip("0"),
year=a.timeslot.time.strftime("%Y"),
),
"type": a.timeslot.type.name
"type": a.timeslot.type.name,
"filter_keywords": ",".join(a.filter_keywords),
}
if a.session:
@ -3267,9 +3271,11 @@ def upcoming(request):
for session in interim_sessions:
session.historic_group = session.group
session.filter_keywords = filter_keywords_for_session(session)
entries.extend(list(interim_sessions))
entries.sort(key = lambda o: pytz.utc.localize(datetime.datetime.combine(o.date, datetime.datetime.min.time())) if isinstance(o,Meeting) else o.official_timeslotassignment().timeslot.utc_start_time())
# add menu entries
menu_entries = get_interim_menu_entries(request)
selected_menu_entry = 'upcoming'
@ -3309,7 +3315,10 @@ def upcoming_ical(request):
Filters by wg name and session type.
"""
filter_params = parse_agenda_filter_params(request.GET)
try:
filter_params = parse_agenda_filter_params(request.GET)
except ValueError as e:
return HttpResponseBadRequest(str(e))
today = datetime.date.today()
# get meetings starting 7 days ago -- we'll filter out sessions in the past further down

View file

@ -1,17 +1,13 @@
var agenda_filter_for_testing = {}; // methods to be accessed for automated testing
var agenda_filter = function () {
var agenda_filter; // public interface
var agenda_filter_for_testing; // methods to be accessed for automated testing
// closure to create private scope
(function () {
'use strict'
var update_callback // function(filter_params)
var enable_non_area = false // if true, show the non-area filters
/* Add to list without duplicates */
function add_list_item (list, item) {
if (list.indexOf(item) === -1) {
list.push(item);
}
}
/* Remove from list, if present */
function remove_list_item (list, item) {
var item_index = list.indexOf(item);
@ -20,13 +16,18 @@ var agenda_filter = function () {
}
}
/* Add to list if not present, remove if present */
/* Add to list if not present, remove if present
*
* Returns true if added to the list, otherwise false.
*/
function toggle_list_item (list, item) {
var item_index = list.indexOf(item);
if (item_index === -1) {
list.push(item)
return true;
} else {
list.splice(item_index, 1)
return false;
}
}
@ -48,101 +49,83 @@ var agenda_filter = function () {
if (!qparams[filt] || (qparams[filt] === true)) {
return [];
}
return $.map(qparams[filt].split(','), function(s){return s.trim();});
var result = [];
var qp = qparams[filt].split(',');
for (var ii = 0; ii < qp.length; ii++) {
result.push(qp[ii].trim());
}
return result;
}
function get_filter_params (qparams) {
var enabled = !!(qparams.show || qparams.hide || qparams.showtypes || qparams.hidetypes);
var enabled = !!(qparams.show || qparams.hide);
return {
enabled: enabled,
show_groups: get_filter_from_qparams(qparams, 'show'),
hide_groups: get_filter_from_qparams(qparams, 'hide'),
show_types: get_filter_from_qparams(qparams, 'showtypes'),
hide_types: get_filter_from_qparams(qparams, 'hidetypes'),
show: get_filter_from_qparams(qparams, 'show'),
hide: get_filter_from_qparams(qparams, 'hide')
}
}
function filtering_is_enabled (filter_params) {
return filter_params['enabled'];
function get_keywords(elt) {
var keywords = $(elt).attr('data-filter-keywords');
if (keywords) {
return keywords.toLowerCase().split(',');
}
return [];
}
function get_area_items (area) {
var types = [];
var groups = [];
var neg_groups = [];
function get_item(elt) {
return elt.text().trim().toLowerCase().replace(/ /g, '');
}
$('.view.' + area).find('button').each(function (index, elt) {
elt = $(elt) // jquerify
var item = elt.text().trim().toLowerCase()
if (elt.hasClass('picktype')) {
types.push(item)
} else if (elt.hasClass('pickview')) {
groups.push(item);
} else if (elt.hasClass('pickviewneg')) {
neg_groups.push(item)
// utility method - is there a match between two lists of keywords?
function keyword_match(list1, list2) {
for (var ii = 0; ii < list1.length; ii++) {
if (list2.indexOf(list1[ii]) !== -1) {
return true;
}
}
return false;
}
// Find the items corresponding to a keyword
function get_items_with_keyword (keyword) {
var items = [];
$('.view button.pickview').filter(function(index, elt) {
return keyword_match(get_keywords(elt), [keyword]);
}).each(function (index, elt) {
items.push(get_item($(elt)));
});
return { 'groups': groups, 'neg_groups': neg_groups, 'types': types };
return items;
}
function filtering_is_enabled (filter_params) {
return filter_params.enabled;
}
// Update the filter / customization UI to match the current filter parameters
function update_filter_ui (filter_params) {
var area_group_buttons = $('.view .pickview, .pick-area');
var non_area_header_button = $('button.pick-non-area');
var non_area_type_buttons = $('.view.non-area .picktype');
var non_area_group_buttons = $('.view.non-area button.pickviewneg');
var buttons = $('.pickview');
if (!filtering_is_enabled(filter_params)) {
// Not filtering - set everything to defaults and exit
area_group_buttons.removeClass('active');
non_area_header_button.removeClass('active');
non_area_type_buttons.removeClass('active');
non_area_group_buttons.removeClass('active');
non_area_group_buttons.addClass('disabled');
// Not filtering - set to default and exit
buttons.removeClass('active');
return;
}
// show the customizer - it will stay visible even if filtering is disabled
$('#customize').collapse('show')
// Group and area buttons - these are all positive selections
area_group_buttons.each(function (index, elt) {
// Update button state to match visibility
buttons.each(function (index, elt) {
elt = $(elt);
var item = elt.text().trim().toLowerCase();
var area = elt.attr('data-group-area');
if ((filter_params['hide_groups'].indexOf(item) === -1) // not hidden...
&& ((filter_params['show_groups'].indexOf(item) !== -1) // AND shown...
|| (area && (filter_params['show_groups'].indexOf(area.trim().toLowerCase()) !== -1))) // OR area shown
) {
elt.addClass('active');
} else {
elt.removeClass('active');
}
});
// Non-area buttons need special handling. Only have positive type and negative group buttons.
// Assume non-area heading is disabled, then enable if one of the types is active
non_area_header_button.removeClass('active');
non_area_group_buttons.addClass('disabled');
non_area_type_buttons.each(function (index, elt) {
// Positive type selection buttons
elt = $(elt);
var item = elt.text().trim().toLowerCase();
if ((filter_params['show_types'].indexOf(item) !== -1)
&& (filter_params['hide_types'].indexOf(item) === -1)){
elt.addClass('active');
non_area_header_button.addClass('active');
non_area_group_buttons.removeClass('disabled');
} else {
elt.removeClass('active');
}
});
non_area_group_buttons.each(function (index, elt) {
// Negative group selection buttons
elt = $(elt);
var item = elt.text().trim().toLowerCase();
if (filter_params['hide_groups'].indexOf(item) === -1) {
var keywords = get_keywords(elt);
keywords.push(get_item(elt)); // treat item as one of its keywords
var hidden = keyword_match(filter_params.hide, keywords);
var shown = keyword_match(filter_params.show, keywords);
if (shown && !hidden) {
elt.addClass('active');
} else {
elt.removeClass('active');
@ -163,7 +146,6 @@ var agenda_filter = function () {
}
}
/* Trigger an update so the user will see the page appropriate for given filter_params
*
* Updates the URL to match filter_params, then updates the history / display to match
@ -172,17 +154,11 @@ var agenda_filter = function () {
function update_filters (filter_params) {
var qparams = []
var search = ''
if (filter_params['show_groups'].length > 0) {
qparams.push('show=' + filter_params['show_groups'].join())
if (filter_params.show.length > 0) {
qparams.push('show=' + filter_params.show.join())
}
if (filter_params['hide_groups'].length > 0) {
qparams.push('hide=' + filter_params['hide_groups'].join())
}
if (filter_params['show_types'].length > 0) {
qparams.push('showtypes=' + filter_params['show_types'].join())
}
if (filter_params['hide_types'].length > 0) {
qparams.push('hidetypes=' + filter_params['hide_types'].join())
if (filter_params.hide.length > 0) {
qparams.push('hide=' + filter_params.hide.join())
}
if (qparams.length > 0) {
search = '?' + qparams.join('&')
@ -202,27 +178,41 @@ var agenda_filter = function () {
/* Helper for pick group/type button handlers - toggles the appropriate parameter entry
* elt - the jquery element that was clicked
* param_type - key of the filter param to update (show_groups, show_types, etc)
* param_type - key of the filter param to update (show, hide)
*/
function handle_pick_button (elt, param_type) {
var area = elt.attr('data-group-area');
var item = elt.text().trim().toLowerCase();
function handle_pick_button (elt) {
var fp = get_filter_params(parse_query_params(window.location.search));
var neg_param_type = {
show_groups: 'hide_groups',
hide_groups: 'show_groups',
show_types: 'hide_types',
hide_types: 'show_types'
}[param_type];
var item = get_item(elt);
if (area && (fp[param_type].indexOf(area.trim().toLowerCase()) !== -1)) {
// Area is shown - toggle hide list
toggle_list_item(fp[neg_param_type], item);
remove_list_item(fp[param_type], item);
/* Normally toggle in and out of the 'show' list. If this item is active because
* one of its keywords is active, invert the sense and toggle in and out of the
* 'hide' list instead. */
var inverted = keyword_match(fp.show, get_keywords(elt));
var just_showed_item = false;
if (inverted) {
toggle_list_item(fp.hide, item);
remove_list_item(fp.show, item);
} else {
toggle_list_item(fp[param_type], item);
remove_list_item(fp[neg_param_type], item);
just_showed_item = toggle_list_item(fp.show, item);
remove_list_item(fp.hide, item);
}
/* If we just showed an item, remove its children from the
* show/hide lists to keep things consistent. This way, selecting
* an area will enable all items in the row as one would expect. */
if (just_showed_item) {
var children = get_items_with_keyword(item);
$.each(children, function(index, child) {
remove_list_item(fp.show, child);
remove_list_item(fp.hide, child);
});
}
// If the show list is empty, clear the hide list because there is nothing to hide
if (fp.show.length === 0) {
fp.hide = [];
}
return fp;
}
@ -230,88 +220,49 @@ var agenda_filter = function () {
return elt.hasClass('disabled');
}
// Various "pick" button handlers
$('.pickview').click(function () {
if (is_disabled($(this))) { return; }
update_filters(handle_pick_button($(this), 'show_groups'))
});
$('.pickviewneg').click(function () {
if (is_disabled($(this))) { return; }
update_filters(handle_pick_button($(this), 'hide_groups'))
});
$('.picktype').click(function () {
if (is_disabled($(this))) { return; }
var fp = handle_pick_button($(this), 'show_types')
// If we just disabled the last non-area type, clear out the hide groups list.
var items = get_area_items('non-area')
var any_left = false
$.each(items.types, function (index, session_type) {
if (fp['show_types'].indexOf(session_type) !== -1) {
any_left = true
}
})
if (!any_left) {
fp['hide_groups'] = []
}
update_filters(fp);
});
// Click handler for an area header button
$('.pick-area').click(function() {
if (is_disabled($(this))) { return; }
var fp = handle_pick_button($(this), 'show_groups');
var items = get_area_items($(this).text().trim().toLowerCase());
// Clear all the individual group show/hide options
$.each(items.groups, function(index, group) {
remove_list_item(fp['show_groups'], group);
remove_list_item(fp['hide_groups'], group);
function register_handlers() {
$('.pickview').click(function () {
if (is_disabled($(this))) { return; }
var fp = handle_pick_button($(this));
update_filters(fp);
});
update_filters(fp);
});
// Click handler for the "Non-Area" header button
$('.pick-non-area').click(function () {
var items = get_area_items('non-area');
var fp = get_filter_params(parse_query_params(window.location.search))
if ($(this).hasClass('active')) {
// Were active - disable or hide everything
$.each(items.types, function (index, session_type) {
remove_list_item(fp['show_types'], session_type)
})
// When no types are shown, no need to hide groups. Empty hide_groups list.
fp['hide_groups'] = []
} else {
// Were not active - enable or stop hiding everything
$.each(items.types, function (index, session_type) {
add_list_item(fp['show_types'], session_type)
})
$.each(items.neg_groups, function (index, group) {
remove_list_item(fp['hide_groups'], group)
})
}
update_filters(fp);
});
// Entry point to filtering code when page loads
}
/* Entry point to filtering code when page loads
*
* This must be called if you are using the HTML template to provide a customization
* button UI. Do not call if you only want to use the parameter parsing routines.
*/
function enable () {
$(document).ready(function () {
update_view()
register_handlers();
update_view();
})
}
// utility method - filter a jquery set to those matching a keyword
function rows_matching_filter_keyword(rows, kw) {
return rows.filter(function(index, element) {
var row_kws = get_keywords(element);
return keyword_match(row_kws, [kw.toLowerCase()]);
});
}
// Make private functions available for unit testing
agenda_filter_for_testing.toggle_list_item = toggle_list_item;
agenda_filter_for_testing.parse_query_params = parse_query_params;
agenda_filter_for_testing = {
parse_query_params: parse_query_params,
toggle_list_item: toggle_list_item
};
// Public interface methods
return {
// Make public interface methods accessible
agenda_filter = {
enable: enable,
filtering_is_enabled: filtering_is_enabled,
get_filter_params: get_filter_params,
include_non_area_selectors: function () {enable_non_area = true},
keyword_match: keyword_match,
parse_query_params: parse_query_params,
rows_matching_filter_keyword: rows_matching_filter_keyword,
set_update_callback: function (cb) {update_callback = cb}
}
}();
};
})();

View file

@ -125,10 +125,7 @@
{% 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="{% 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}}">
<tr id="row-{{ item.slug }}" data-filter-keywords="{{ item.filter_keywords|join:',' }}">
<td class="text-nowrap text-right">
<span class="hidden-xs">
{% include "meeting/timeslot_start_end.html" %}
@ -187,11 +184,9 @@
{% 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.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 %}>
<tr id="row-{{item.slug}}"
{% if item.timeslot.type.slug == 'plenary' %}class="{{item.timeslot.type.slug}}danger"{% endif %}
data-filter-keywords="{{ item.filter_keywords|join:',' }}">
{% if item.timeslot.type.slug == 'plenary' %}
<th class="text-nowrap text-right">
<span class="hidden-xs">
@ -328,26 +323,18 @@
}
// if groups were selected for filtering, hide all rows by default
agenda_rows.hide();
agenda_rows.filter(function(index, row) {
return !!$(row).attr('data-filter-keywords');
}).hide();
// loop through the has items and change the UI element and row visibilities accordingly
$.each(filter_params['show_groups'], function (i, v) {
$.each(filter_params.show, 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();
agenda_filter.rows_matching_filter_keyword(agenda_rows, 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) {
$.each(filter_params.hide, 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();
agenda_filter.rows_matching_filter_keyword(agenda_rows, v).hide();
});
}
@ -381,7 +368,7 @@
wv_window.history.replaceState({}, '', new_url);
wv_window.draw_calendar()
} else {
// ho history.replaceState, page reload required
// either have not yet loaded the iframe or we do not support history replacement
wv_iframe.src = new_url;
}
}

View file

@ -24,13 +24,11 @@
<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>
<button class="btn btn-default btn-block pickview {{ 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>
<th style="width:{% widthratio 1 group_parents|length|add:1 100 %}"></th>
{% endif %}
</tr>
</thead>
@ -42,7 +40,7 @@
{% 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 }}">
data-filter-keywords="{{ p.acronym|lower }}{% if group.is_bof %},bof{% endif %}">
{% if group.is_bof %}
<i>{{ group.acronym }}</i>
{% else %}
@ -57,37 +55,34 @@
<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>
<button class="btn btn-default pickview adofficehours"> AD Office Hours</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>
<button class="btn btn-default pickview bof"> BOF</button>
</div>
<div class="btn-group btn-group-xs btn-group-justified">
<button class="btn btn-default pickviewneg hackathon"> Hackathon</button>
<button class="btn btn-default pickview edu"> EDU</button>
</div>
<div class="btn-group btn-group-xs btn-group-justified">
<button class="btn btn-default pickviewneg iab"> IAB</button>
<button class="btn btn-default pickview hackathon"> Hackathon</button>
</div>
<div class="btn-group btn-group-xs btn-group-justified">
<button class="btn btn-default pickviewneg iepg"> IEPG</button>
<button class="btn btn-default pickview iepg"> IEPG</button>
</div>
<div class="btn-group btn-group-xs btn-group-justified">
<button class="btn btn-default pickviewneg iesg"> IESG</button>
<button class="btn btn-default pickview iesg"> IESG</button>
</div>
<div class="btn-group btn-group-xs btn-group-justified">
<button class="btn btn-default pickviewneg ietf"> IETF</button>
<button class="btn btn-default pickview ietf"> IETF</button>
</div>
<div class="btn-group btn-group-xs btn-group-justified">
<button class="btn btn-default pickviewneg secretariat"> Secretariat
</button>
<button class="btn btn-default pickview plenary"> Plenary</button>
</div>
<div class="btn-group btn-group-xs btn-group-justified">
<button class="btn btn-default pickviewneg tools"> Tools</button>
<button class="btn btn-default pickview secretariat"> Secretariat</button>
</div>
<div class="btn-group btn-group-xs btn-group-justified">
<button class="btn btn-default pickview tools"> Tools</button>
</div>
</div>
</td>

View file

@ -66,9 +66,8 @@
</thead>
<tbody>
{% for entry in entries %}
<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 %}">
<tr class="entry"
{% if entry|classname == 'Session' %}data-filter-keywords="{{ entry.filter_keywords|join:',' }}"{% endif %}>
{% if entry|classname == 'Meeting' %}
{% with meeting=entry %}
<td>{{ meeting.date }} - {{ meeting.end }}</td>
@ -77,11 +76,11 @@
<td></td>
{% endwith %}
{% elif entry|classname == 'Session' %}
{% with session=entry meeting=entry.meeting%}
{% with session=entry group=entry.group meeting=entry.meeting%}
<td>{{ session.official_timeslotassignment.timeslot.utc_start_time | date:"Y-m-d H:i"}} - {{ session.official_timeslotassignment.timeslot.utc_end_time | date:"H:i e" }}</td>
<td><a href="{% url 'ietf.group.views.group_home' acronym=session.group.acronym %}">{{ session.group.acronym }}</a></td>
<td><a href="{% url 'ietf.group.views.group_home' acronym=group.acronym %}">{{ group.acronym }}</a></td>
<td>
<a class="interim-meeting-link" href="{% url 'ietf.meeting.views.session_details' num=session.meeting.number acronym=session.group.acronym %}">{{ session.meeting.number }}</a>
<a class="interim-meeting-link" href="{% url 'ietf.meeting.views.session_details' num=meeting.number acronym=group.acronym %}"> {{ meeting.number }}</a>
</td>
{% if session.current_status == 'canceled' %}
<td class='text-right'>
@ -134,7 +133,7 @@
{
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 %}',
filter_keywords: ["{{ session.filter_keywords|join:'","' }}"],
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 %}'
@ -148,29 +147,14 @@
// 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) {
// Visible if filtering is disabled or event has no keywords
if (!agenda_filter.filtering_is_enabled(filter_params) || !event.filter_keywords) {
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;
// Visible if shown and not hidden
return (!agenda_filter.keyword_match(filter_params.hide, event.filter_keywords)
&& agenda_filter.keyword_match(filter_params.show, event.filter_keywords));
}
// Apply filter_params to the event list and format data for the calendar
@ -217,24 +201,22 @@
}
function update_meeting_display(filter_params) {
var meeting_rows = $("#upcoming-meeting-table tr.entry")
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();
// hide everything that has keywords
meeting_rows.filter(function(index, row){
return !!$(row).attr('data-filter-keywords');
}).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['show'], function (i, v) {
agenda_filter.rows_matching_filter_keyword(meeting_rows, 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();
$.each(filter_params['hide'], function (i, v) {
agenda_filter.rows_matching_filter_keyword(meeting_rows, v).hide();
});
}

View file

@ -2,6 +2,7 @@
{% load origin %}{% origin %}
{% load static %}
<html> <head>
<script src="{% static 'ietf/js/agenda/agenda_filter.js' %}"></script>
<script type="text/javascript">
var all_items = {{ items|safe }};
@ -106,62 +107,17 @@
}
}
}
//===========================================================================
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;
}
}
return params;
}
//===========================================================================
function get_filter_from_qparams(qparams, filt) {
return qparams[filt] ? qparams[filt].split(',') : [];
}
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 is_visible(query_params) {
function is_visible(filter_params) {
// Returns a method to filter objects for visibility
// Accepts show, hide, showtypes, and hidetypes filters. Also accepts
// Accepts show and hide filters. No longer accepts
// '@<state>' to show sessions in a particular state (e.g., @bof).
// Current types are:
// Session, Other, Break, Plenary
var fp = get_filter_params(query_params);
return function (item) {
var item_group = (item.group || '').toLowerCase();
var item_type = (item.type || '').toLowerCase();
var item_area = (item.area || '').toLowerCase();
var item_state = (item.state || '').toLowerCase();
if ((fp['hide_groups'].indexOf(item_group) >= 0) ||
(fp['hide_groups'].indexOf(item_area) >= 0) ||
(fp['hide_types'].indexOf(item_type) >= 0)) {
return false;
}
return ((fp['show_groups'].indexOf(item_group) >= 0) ||
(fp['show_groups'].indexOf(item_area) >= 0) ||
(fp['show_types'].indexOf(item_type) >= 0) ||
query_params['@'+item_state]);
var filter_keywords = item.filter_keywords.split(',');
return (!agenda_filter.keyword_match(filter_keywords, filter_params.hide)
&& agenda_filter.keyword_match(filter_keywords, filter_params.show));
}
}
@ -172,9 +128,12 @@
var height = document.body.clientHeight;
var visible_items = all_items;
var qs = window.location.search;
if (qs.length > 1) {
visible_items = visible_items.filter(is_visible(parse_query_params(qs)));
var filter_params = agenda_filter.get_filter_params(
agenda_filter.parse_query_params(window.location.search)
);
if (agenda_filter.filtering_is_enabled(filter_params)) {
visible_items = visible_items.filter(is_visible(filter_params));
}
var start_day;
@ -539,9 +498,13 @@
//===========================================================================
// Set up events for drawing the calendar
window.addEventListener("resize", draw_calendar, false);
window.addEventListener("load", draw_calendar, false);
window.addEventListener("hashchange", draw_calendar, false);
function redraw_weekview() {
draw_calendar();
}
window.addEventListener("resize", redraw_weekview, false);
window.addEventListener("load", redraw_weekview, false);
window.addEventListener("hashchange", redraw_weekview, false);
</script>
</head>