Merged in the agenda filtering improvements from jennifer@painless-security.com. This completely reworks the filtering of the IETF agenda, in order to not collide with page navigation and clean up a number of edge cases. It also reinstates the same kind of filtering for the upcoming meetings page.

- Legacy-Id: 18534
This commit is contained in:
Henrik Levkowetz 2020-09-30 11:39:40 +00:00
commit 7d01c661f2
16 changed files with 1927 additions and 422 deletions

View file

@ -116,6 +116,9 @@ def make_meeting_test_data(meeting=None, create_interims=False):
break_slot = TimeSlot.objects.create(meeting=meeting, type_id="break", location=break_room,
duration=datetime.timedelta(minutes=90),
time=datetime.datetime.combine(session_date, datetime.time(7,0)))
plenary_slot = TimeSlot.objects.create(meeting=meeting, type_id="plenary", location=room,
duration=datetime.timedelta(minutes=60),
time=datetime.datetime.combine(session_date, datetime.time(11,0)))
# mars WG
mars = Group.objects.get(acronym='mars')
mars_session = Session.objects.create(meeting=meeting, group=mars,
@ -159,6 +162,14 @@ def make_meeting_test_data(meeting=None, create_interims=False):
SchedulingEvent.objects.create(session=break_session, status_id='schedw', by=system_person)
SchedTimeSessAssignment.objects.create(timeslot=break_slot, session=break_session, schedule=base_schedule)
# IETF Plenary
plenary_session = Session.objects.create(meeting=meeting, group=Group.objects.get(acronym="ietf"),
name="IETF Plenary", attendees=250,
requested_duration=datetime.timedelta(minutes=60),
type_id="plenary")
SchedulingEvent.objects.create(session=plenary_session, status_id='schedw', by=system_person)
SchedTimeSessAssignment.objects.create(timeslot=plenary_slot, session=plenary_session, schedule=schedule)
meeting.schedule = schedule
meeting.save()
@ -209,7 +220,7 @@ def make_interim_test_data():
ad = Person.objects.get(user__username='ad')
RoleFactory(group=area,person=ad,name_id='ad')
mars = GroupFactory(acronym='mars',parent=area,name='Martian Special Interest Group')
ames = GroupFactory(acronym='ames',parent=area)
ames = GroupFactory(acronym='ames',parent=area,name='Asteroid Mining Equipment Standardization Group')
RoleFactory(group=mars,person__user__username='marschairman',name_id='chair')
RoleFactory(group=ames,person__user__username='ameschairman',name_id='chair')

View file

@ -477,8 +477,7 @@ class TimeSlotEditingApiTests(TestCase):
def test_manipulate_timeslot(self):
meeting = make_meeting_test_data()
slot = meeting.timeslot_set.all()[0]
self.assertEqual(TimeSlot.objects.get(pk=slot.pk).type_id,'regular')
slot = meeting.timeslot_set.filter(type_id='regular')[0]
url = urlreverse("ietf.meeting.ajax.timeslot_sloturl",
kwargs=dict(num=meeting.number, slotid=slot.pk))

View file

@ -4,10 +4,14 @@
import time
import datetime
import shutil
import os
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
@ -15,10 +19,13 @@ import debug # pyflakes:ignore
from ietf.doc.factories import DocumentFactory
from ietf.group import colors
from ietf.person.models import Person
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 SchedulingEvent, SessionStatusName
from ietf.meeting.models import (Schedule, SchedTimeSessAssignment, Session,
Room, TimeSlot, Constraint, ConstraintName,
Meeting, SchedulingEvent, SessionStatusName)
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
@ -31,6 +38,7 @@ try:
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions
from selenium.common.exceptions import NoSuchElementException
except ImportError as e:
skip_selenium = True
skip_message = "Skipping selenium tests: %s" % e
@ -51,29 +59,37 @@ def start_web_driver():
options.add_argument("no-sandbox") # docker needs this
return webdriver.Chrome(options=options, service_log_path=settings.TEST_GHOSTDRIVER_LOG_PATH)
@skipIf(skip_selenium, skip_message)
class EditMeetingScheduleTests(IetfLiveServerTestCase):
class MeetingTestCase(IetfLiveServerTestCase):
def __init__(self, *args, **kwargs):
super(MeetingTestCase, self).__init__(*args, **kwargs)
self.login_view = 'ietf.ietfauth.views.login'
def setUp(self):
super(MeetingTestCase, self).setUp()
self.driver = start_web_driver()
self.driver.set_window_size(1024,768)
def tearDown(self):
self.driver.close()
def absreverse(self,*args,**kwargs):
return '%s%s'%(self.live_server_url,urlreverse(*args,**kwargs))
def login(self, username='plain'):
url = self.absreverse(self.login_view)
password = '%s+password' % username
self.driver.get(url)
self.driver.find_element_by_name('username').send_keys(username)
self.driver.find_element_by_name('password').send_keys(password)
self.driver.find_element_by_xpath('//button[@type="submit"]').click()
def debug_snapshot(self,filename='debug_this.png'):
self.driver.execute_script("document.body.bgColor = 'white';")
self.driver.save_screenshot(filename)
def absreverse(self,*args,**kwargs):
return '%s%s'%(self.live_server_url,urlreverse(*args,**kwargs))
def login(self):
url = self.absreverse('ietf.ietfauth.views.login')
self.driver.get(url)
self.driver.find_element_by_name('username').send_keys('plain')
self.driver.find_element_by_name('password').send_keys('plain+password')
self.driver.find_element_by_xpath('//button[@type="submit"]').click()
@skipIf(skip_selenium, skip_message)
class EditMeetingScheduleTests(MeetingTestCase):
def test_edit_meeting_schedule(self):
meeting = make_meeting_test_data()
@ -258,28 +274,7 @@ class EditMeetingScheduleTests(IetfLiveServerTestCase):
@skipIf(skip_selenium, skip_message)
@skipIf(django.VERSION[0]==2, "Skipping test with race conditions under Django 2")
class ScheduleEditTests(IetfLiveServerTestCase):
def setUp(self):
self.driver = start_web_driver()
self.driver.set_window_size(1024,768)
def tearDown(self):
self.driver.close()
def debug_snapshot(self,filename='debug_this.png'):
self.driver.execute_script("document.body.bgColor = 'white';")
self.driver.save_screenshot(filename)
def absreverse(self,*args,**kwargs):
return '%s%s'%(self.live_server_url,urlreverse(*args,**kwargs))
def login(self):
url = self.absreverse('ietf.ietfauth.views.login')
self.driver.get(url)
self.driver.find_element_by_name('username').send_keys('plain')
self.driver.find_element_by_name('password').send_keys('plain+password')
self.driver.find_element_by_xpath('//button[@type="submit"]').click()
class ScheduleEditTests(MeetingTestCase):
def testUnschedule(self):
meeting = make_meeting_test_data()
@ -317,27 +312,16 @@ class ScheduleEditTests(IetfLiveServerTestCase):
self.assertEqual(SchedTimeSessAssignment.objects.filter(session__meeting__number=72,session__group__acronym='mars',schedule__name='test-schedule').count(),0)
@skipIf(skip_selenium, skip_message)
class SlideReorderTests(IetfLiveServerTestCase):
class SlideReorderTests(MeetingTestCase):
def setUp(self):
self.driver = start_web_driver()
self.driver.set_window_size(1024,768)
super(SlideReorderTests, self).setUp()
self.session = SessionFactory(meeting__type_id='ietf', status_id='sched')
self.session.sessionpresentation_set.create(document=DocumentFactory(type_id='slides',name='one'),order=1)
self.session.sessionpresentation_set.create(document=DocumentFactory(type_id='slides',name='two'),order=2)
self.session.sessionpresentation_set.create(document=DocumentFactory(type_id='slides',name='three'),order=3)
def tearDown(self):
self.driver.close()
def absreverse(self,*args,**kwargs):
return '%s%s'%(self.live_server_url,urlreverse(*args,**kwargs))
def secr_login(self):
url = '%s%s'%(self.live_server_url, urlreverse('ietf.ietfauth.views.login'))
self.driver.get(url)
self.driver.find_element_by_name('username').send_keys('secretary')
self.driver.find_element_by_name('password').send_keys('secretary+password')
self.driver.find_element_by_xpath('//button[@type="submit"]').click()
self.login('secretary')
#@override_settings(DEBUG=True)
def testReorderSlides(self):
@ -357,6 +341,477 @@ class SlideReorderTests(IetfLiveServerTestCase):
names=self.session.sessionpresentation_set.values_list('document__name',flat=True)
self.assertEqual(list(names),['one','three','two'])
@skipIf(skip_selenium, skip_message)
class AgendaTests(MeetingTestCase):
def setUp(self):
super(AgendaTests, self).setUp()
self.meeting = make_meeting_test_data()
def row_id_for_item(self, item):
return 'row-%s' % item.slug()
def get_expected_items(self):
expected_items = self.meeting.schedule.assignments.exclude(timeslot__type__in=['lead','offagenda'])
self.assertGreater(len(expected_items), 0, 'Test setup generated an empty schedule')
return expected_items
def test_agenda_view_displays_all_items(self):
"""By default, all agenda items should be displayed"""
self.login()
self.driver.get(self.absreverse('ietf.meeting.views.agenda'))
for item in self.get_expected_items():
row_id = 'row-%s' % item.slug()
try:
item_row = self.driver.find_element_by_id(row_id)
except NoSuchElementException:
item_row = None
self.assertIsNotNone(item_row, 'No row for schedule item "%s"' % row_id)
self.assertTrue(item_row.is_displayed(), 'Row for schedule item "%s" is not displayed' % row_id)
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(
parse_query_params + '("?show=group1,group2,group3");'
)
self.assertEqual(result, dict(show='group1,group2,group3'))
# Only 'hide' param
result = self.driver.execute_script(
parse_query_params + '("?hide=group4,group5,group6");'
)
self.assertEqual(result, dict(hide='group4,group5,group6'))
# Both 'show' and 'hide'
result = self.driver.execute_script(
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'))
result = self.driver.execute_script(
"""
// start empty, add item
var list0=[];
%(toggle_list_item)s(list0, 'item');
// one item, remove it
var list1=['item'];
%(toggle_list_item)s(list1, 'item');
// one item, add another
var list2=['item1'];
%(toggle_list_item)s(list2, 'item2');
// multiple items, remove first
var list3=['item1', 'item2', 'item3'];
%(toggle_list_item)s(list3, 'item1');
// multiple items, remove middle
var list4=['item1', 'item2', 'item3'];
%(toggle_list_item)s(list4, 'item2');
// multiple items, remove last
var list5=['item1', 'item2', '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')
self.assertEqual(result[2], ['item1', 'item2'], 'Adding second item to list failed')
self.assertEqual(result[3], ['item2', 'item3'], 'Removing first item from list failed')
self.assertEqual(result[4], ['item1', 'item3'], 'Removing middle item from list failed')
self.assertEqual(result[5], ['item1', 'item2'], 'Removing last item from list failed')
def do_agenda_view_filter_test(self, querystring, visible_groups=()):
self.login()
self.driver.get(self.absreverse('ietf.meeting.views.agenda') + querystring)
self.assert_agenda_item_visibility(visible_groups)
weekview_iframe = self.driver.find_element_by_id('weekview')
if len(querystring) == 0:
self.assertFalse(weekview_iframe.is_displayed(), 'Weekview should be hidden when filters off')
else:
self.assertTrue(weekview_iframe.is_displayed(), 'Weekview should be visible when filters on')
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'])
def test_agenda_view_filter_all(self):
"""Filtered agenda view should display only matching rows (all groups selected)"""
self.do_agenda_view_filter_test('', None) # None means all should be visible
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'])
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
If visible_groups is None (the default), expects all items to be visible.
"""
for item in self.get_expected_items():
row_id = self.row_id_for_item(item)
try:
item_row = self.driver.find_element_by_id(row_id)
except NoSuchElementException:
item_row = None
self.assertIsNotNone(item_row, 'No row for schedule item "%s"' % row_id)
if visible_groups is None or item.session.group.acronym in visible_groups:
self.assertTrue(item_row.is_displayed(), 'Row for schedule item "%s" is not displayed but should be' % row_id)
else:
self.assertFalse(item_row.is_displayed(), 'Row for schedule item "%s" is displayed but should not be' % row_id)
def assert_weekview_item_visibility(self, visible_groups=None):
for item in self.get_expected_items():
if item.session.name:
label = item.session.name
elif item.timeslot.type_id == 'break':
label = item.timeslot.name
elif item.session.group:
label = item.session.group.name
else:
label = 'Free Slot'
try:
item_div = self.driver.find_element_by_xpath('//div/span[contains(text(),"%s")]/..' % label)
except NoSuchElementException:
item_div = None
if visible_groups is None or item.session.group.acronym in visible_groups:
self.assertIsNotNone(item_div, 'No weekview entry for "%s" (%s)' % (label, item.slug()))
self.assertTrue(item_div.is_displayed(), 'Entry for "%s (%s)" is not displayed but should be' % (label, item.slug()))
else:
self.assertIsNone(item_div, 'Unexpected weekview entry for "%s" (%s)' % (label, item.slug()))
def test_agenda_view_group_filter_toggle(self):
"""Clicking a group toggle enables/disables agenda filtering"""
group_acronym = 'mars'
self.login()
url = self.absreverse('ietf.meeting.views.agenda')
self.driver.get(url)
# Click the 'customize' anchor to reveal the group buttons
customize_anchor = WebDriverWait(self.driver, 2).until(
expected_conditions.element_to_be_clickable(
(By.CSS_SELECTOR, '#accordion a[data-toggle="collapse"]')
)
)
customize_anchor.click()
# Click the group button
group_button = WebDriverWait(self.driver, 2).until(
expected_conditions.element_to_be_clickable(
(By.CSS_SELECTOR, 'button.pickview.%s' % group_acronym)
)
)
group_button.click()
# Check visibility
self.assert_agenda_item_visibility([group_acronym])
# Click the group button again
group_button = WebDriverWait(self.driver, 2).until(
expected_conditions.element_to_be_clickable(
(By.CSS_SELECTOR, 'button.pickview.%s' % group_acronym)
)
)
group_button.click()
# Check visibility
self.assert_agenda_item_visibility()
def test_agenda_view_group_filter_toggle_without_replace_state(self):
"""Toggle should function for browsers without window.history.replaceState"""
group_acronym = 'mars'
self.login()
url = self.absreverse('ietf.meeting.views.agenda')
self.driver.get(url)
# Rather than digging up an ancient browser, simulate absence of history.replaceState
self.driver.execute_script('window.history.replaceState = undefined;')
# Click the 'customize' anchor to reveal the group buttons
customize_anchor = WebDriverWait(self.driver, 2).until(
expected_conditions.element_to_be_clickable(
(By.CSS_SELECTOR, '#accordion a[data-toggle="collapse"]')
)
)
customize_anchor.click()
# Get ready to click the group button
group_button = WebDriverWait(self.driver, 2).until(
expected_conditions.element_to_be_clickable(
(By.CSS_SELECTOR, 'button.pickview.%s' % group_acronym)
)
)
# Be sure we're at the URL we think we're at before we click
self.assertEqual(self.driver.current_url, url)
group_button.click() # click!
expected_url = '%s?show=%s' % (url, group_acronym)
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

@ -22,6 +22,7 @@ from django.conf import settings
from django.contrib.auth.models import User
from django.test import Client, override_settings
from django.db.models import F
from django.http import QueryDict
import debug # pyflakes:ignore
@ -37,7 +38,7 @@ from ietf.meeting.models import Session, TimeSlot, Meeting, SchedTimeSessAssignm
from ietf.meeting.test_data import make_meeting_test_data, make_interim_meeting, make_interim_test_data
from ietf.meeting.utils import finalize, condition_slide_order
from ietf.meeting.utils import add_event_info_to_session_qs
from ietf.meeting.views import session_draft_list
from ietf.meeting.views import session_draft_list, parse_agenda_filter_params
from ietf.name.models import SessionStatusName, ImportantDateName, RoleName
from ietf.utils.decorators import skip_coverage
from ietf.utils.mail import outbox, empty_outbox, get_payload_text
@ -61,6 +62,33 @@ else:
"location indicated in settings.py.")
print(" "+skip_message)
def assert_ical_response_is_valid(test_inst, response, expected_event_summaries=None, expected_event_count=None):
"""Validate an HTTP response containing iCal data
Based on RFC2445, but not exhaustive by any means. Assumes a single iCalendar object. Checks that
expected_event_summaries are found, but other events are allowed to be present. Specify the
expected_event_count if you want to reject additional events.
"""
test_inst.assertEqual(response.get('Content-Type'), "text/calendar")
# Validate iCalendar object
test_inst.assertContains(response, 'BEGIN:VCALENDAR', count=1)
test_inst.assertContains(response, 'END:VCALENDAR', count=1)
test_inst.assertContains(response, 'PRODID:', count=1)
test_inst.assertContains(response, 'VERSION', count=1)
# Validate event objects
if expected_event_summaries is not None:
for summary in expected_event_summaries:
test_inst.assertContains(response, 'SUMMARY:' + summary)
if expected_event_count is not None:
test_inst.assertContains(response, 'BEGIN:VEVENT', count=expected_event_count)
test_inst.assertContains(response, 'END:VEVENT', count=expected_event_count)
test_inst.assertContains(response, 'UID', count=expected_event_count)
class MeetingTests(TestCase):
def setUp(self):
self.materials_dir = self.tempdir('materials')
@ -201,7 +229,7 @@ class MeetingTests(TestCase):
# iCal
r = self.client.get(urlreverse("ietf.meeting.views.agenda_ical", kwargs=dict(num=meeting.number))
+ "?" + session.group.parent.acronym.upper())
+ "?show=" + session.group.parent.acronym.upper())
self.assertContains(r, session.group.acronym)
self.assertContains(r, session.group.name)
self.assertContains(r, slot.location.name)
@ -607,23 +635,282 @@ class MeetingTests(TestCase):
#
url = urlreverse('ietf.meeting.views.agenda_ical', kwargs={'num':meeting.number, 'acronym':s1.group.acronym, })
r = self.client.get(url)
self.assertEqual(r.get('Content-Type'), "text/calendar")
self.assertContains(r, 'BEGIN:VEVENT')
self.assertEqual(r.content.count(b'UID'), 2)
self.assertContains(r, 'SUMMARY:mars - Martian Special Interest Group')
assert_ical_response_is_valid(self,
r,
expected_event_summaries=['mars - Martian Special Interest Group'],
expected_event_count=2)
self.assertContains(r, t1.time.strftime('%Y%m%dT%H%M%S'))
self.assertContains(r, t2.time.strftime('%Y%m%dT%H%M%S'))
self.assertContains(r, 'END:VEVENT')
#
url = urlreverse('ietf.meeting.views.agenda_ical', kwargs={'num':meeting.number, 'session_id':s1.id, })
r = self.client.get(url)
self.assertEqual(r.get('Content-Type'), "text/calendar")
self.assertContains(r, 'BEGIN:VEVENT')
self.assertEqual(r.content.count(b'UID'), 1)
self.assertContains(r, 'SUMMARY:mars - Martian Special Interest Group')
assert_ical_response_is_valid(self, r,
expected_event_summaries=['mars - Martian Special Interest Group'],
expected_event_count=1)
self.assertContains(r, t1.time.strftime('%Y%m%dT%H%M%S'))
self.assertNotContains(r, t2.time.strftime('%Y%m%dT%H%M%S'))
self.assertContains(r, 'END:VEVENT')
def test_meeting_agenda_has_static_ical_links(self):
"""Links to the agenda_ical view must appear on the agenda page
Confirms that these have the correct querystrings. Does not test the JS-based
'Customized schedule' button.
"""
meeting = make_meeting_test_data()
# get the agenda
url = urlreverse('ietf.meeting.views.agenda', kwargs=dict(num=meeting.number))
r = self.client.get(url)
# Check that it has the links we expect
ical_url = urlreverse('ietf.meeting.views.agenda_ical', kwargs=dict(num=meeting.number))
q = PyQuery(r.content)
content = q('#content').html().lower() # don't care about case
# Should be a 'non-area events' link showing appropriate types
self.assertIn('%s?showtypes=plenary,other' % ical_url, content)
assignments = meeting.schedule.assignments.exclude(timeslot__type__in=['lead', 'offagenda'])
# Assume the test meeting is not using historic groups
groups = [a.session.group for a in assignments if a.session is not None]
for g in groups:
if g.parent_id is not None:
self.assertIn('%s?show=%s' % (ical_url, g.parent.acronym.lower()), content)
def test_parse_agenda_filter_params(self):
def _r(show=(), hide=(), showtypes=(), hidetypes=()):
"""Helper to create expected result dict"""
return dict(show=set(show), hide=set(hide), showtypes=set(showtypes), hidetypes=set(hidetypes))
self.assertIsNone(parse_agenda_filter_params(QueryDict('')))
self.assertRaises(ValueError, parse_agenda_filter_params, QueryDict('unknown')) # unknown param
self.assertRaises(ValueError, parse_agenda_filter_params, QueryDict('unknown=x')) # unknown param
# test valid combos (not exhaustive)
for qstr, expected in (
('show=', _r()), ('hide=', _r()), ('showtypes=', _r()), ('hidetypes=', _r()),
('show=x', _r(show=['x'])), ('hide=x', _r(hide=['x'])),
('showtypes=x', _r(showtypes=['x'])), ('hidetypes=x', _r(hidetypes=['x'])),
('show=x,y,z', _r(show=['x','y','z'])),
('hide=x,y,z', _r(hide=['x','y','z'])),
('showtypes=x,y,z', _r(showtypes=['x','y','z'])),
('hidetypes=x,y,z', _r(hidetypes=['x','y','z'])),
('show=a&hide=a', _r(show=['a'], hide=['a'])),
('show=a&hide=b', _r(show=['a'], hide=['b'])),
('show=a&hide=b&showtypes=c&hidetypes=d', _r(show=['a'], hide=['b'], showtypes=['c'], hidetypes=['d'])),
):
self.assertEqual(
parse_agenda_filter_params(QueryDict(qstr)),
expected,
'Parsed "%s" incorrectly' % qstr,
)
def test_ical_filter_invalid_syntaxes(self):
meeting = make_meeting_test_data()
url = urlreverse('ietf.meeting.views.agenda_ical', kwargs={'num':meeting.number})
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 do_ical_filter_test(self, meeting, querystring, expected_session_summaries):
url = urlreverse('ietf.meeting.views.agenda_ical', kwargs={'num':meeting.number})
r = self.client.get(url + querystring)
self.assertEqual(r.status_code, 200)
assert_ical_response_is_valid(self,
r,
expected_event_summaries=expected_session_summaries,
expected_event_count=len(expected_session_summaries))
def test_ical_filter_default(self):
meeting = make_meeting_test_data()
self.do_ical_filter_test(
meeting,
querystring='',
expected_session_summaries=[
'Morning Break',
'Registration',
'IETF Plenary',
'ames - Asteroid Mining Equipment Standardization Group',
'mars - Martian Special Interest Group',
]
)
def test_ical_filter_show(self):
meeting = make_meeting_test_data()
self.do_ical_filter_test(
meeting,
querystring='?show=mars',
expected_session_summaries=[
'mars - Martian Special Interest Group',
]
)
def test_ical_filter_hide(self):
meeting = make_meeting_test_data()
self.do_ical_filter_test(
meeting,
querystring='?hide=ietf',
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(
meeting,
querystring='?show=ames&hide=mars',
expected_session_summaries=[
'ames - Asteroid Mining Equipment Standardization Group',
]
)
def test_ical_filter_show_and_hide_same_group(self):
meeting = make_meeting_test_data()
self.do_ical_filter_test(
meeting,
querystring='?show=ames&hide=ames',
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,
@ -1927,45 +2214,210 @@ class InterimTests(TestCase):
#self.assertIn('CANCELLED', q('[id*="'+id+'"]').text())
self.assertIn('CANCELLED', q('tr>td>a>span').text())
def test_upcoming(self):
make_meeting_test_data(create_interims=True)
def do_upcoming_test(self, querystring=None, create_meeting=True):
if create_meeting:
make_meeting_test_data(create_interims=True)
url = urlreverse("ietf.meeting.views.upcoming")
if querystring is not None:
url += '?' + querystring
today = datetime.date.today()
mars_interim = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', meeting__date__gt=today, group__acronym='mars')).filter(current_status='sched').first().meeting
ames_interim = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', meeting__date__gt=today, group__acronym='ames')).filter(current_status='canceled').first().meeting
r = self.client.get(url)
self.assertContains(r, mars_interim.number)
self.assertContains(r, ames_interim.number)
interims = dict(
mars=add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', meeting__date__gt=today, group__acronym='mars')).filter(current_status='sched').first().meeting,
ames=add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', meeting__date__gt=today, group__acronym='ames')).filter(current_status='canceled').first().meeting,
)
self.check_interim_tabs(url)
return self.client.get(url), interims
def test_upcoming(self):
r, interims = self.do_upcoming_test()
self.assertContains(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())
self.check_interim_tabs(url)
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')
r, interims = self.do_upcoming_test('show=ames', create_meeting=False)
self.assertContains(r, interims['mars'].number)
self.assertContains(r, interims['ames'].number)
self.assertContains(r, 'IETF 72')
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')
sg_interim = make_interim_meeting(somegroup, datetime.date.today() + datetime.timedelta(days=20))
sg_sess = sg_interim.session_set.first()
sg_slot = sg_sess.timeslotassignments.first().timeslot
sg_sess.type_id = 'plenary'
sg_slot.type_id = 'plenary'
sg_sess.save()
sg_slot.save()
url = urlreverse("ietf.meeting.views.upcoming_ical")
if querystring is not None:
url += '?' + querystring
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
return r
def test_upcoming_ical(self):
meeting = make_meeting_test_data(create_interims=True)
populate_important_dates(meeting)
r = self.do_upcoming_ical_test(create_meeting=False)
url = urlreverse("ietf.meeting.views.upcoming_ical")
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
# Expect events for important dates plus 4 - one for each WG and one for the IETF meeting
assert_ical_response_is_valid(self, r,
expected_event_summaries=[
'ames - Asteroid Mining Equipment Standardization Group',
'mars - Martian Special Interest Group',
'sg - Some Group',
'IETF 72',
],
expected_event_count=4 + meeting.importantdate_set.count())
today = datetime.date.today()
mars_interim = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', meeting__date__gt=today, group__acronym='mars')).filter(current_status='sched').first().meeting
ames_interim = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', meeting__date__gt=today, group__acronym='ames')).filter(current_status='canceled').first().meeting
self.assertContains(r, mars_interim.number)
self.assertContains(r, ames_interim.number)
self.assertContains(r, 'IETF 72')
self.assertEqual(r.get('Content-Type'), "text/calendar")
self.assertEqual(r.content.count(b'UID'), 3 + meeting.importantdate_set.count())
def test_upcoming_ical_filter_show(self):
r = self.do_upcoming_ical_test('show=mars,ames')
assert_ical_response_is_valid(self, r,
expected_event_summaries=[
'mars - Martian Special Interest Group',
'ames - Asteroid Mining Equipment Standardization Group',
'IETF 72',
],
expected_event_count=3)
# check filtered output
url = url + '?filters=mars'
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertEqual(r.get('Content-Type'), "text/calendar")
self.assertEqual(r.content.count(b'UID'), 2 + meeting.importantdate_set.count())
def test_upcoming_ical_filter_hide(self):
r = self.do_upcoming_ical_test('hide=mars')
assert_ical_response_is_valid(self, r, expected_event_summaries=['IETF 72'], expected_event_count=1)
def test_upcoming_ical_filter_show_and_hide(self):
r = self.do_upcoming_ical_test('show=mars,ames&hide=mars')
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_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)

View file

@ -25,8 +25,9 @@ from wsgiref.handlers import format_date_time
from django import forms
from django.shortcuts import render, redirect, get_object_or_404
from django.http import ( HttpResponse, HttpResponseRedirect, HttpResponseForbidden, HttpResponseNotFound,
Http404, JsonResponse)
from django.http import (HttpResponse, HttpResponseRedirect, HttpResponseForbidden,
HttpResponseNotFound, Http404, HttpResponseBadRequest,
JsonResponse)
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.decorators import login_required
@ -1773,7 +1774,78 @@ def ical_session_status(assignment):
else:
return "CONFIRMED"
def parse_agenda_filter_params(querydict):
"""Parse agenda filter parameters from a request"""
if len(querydict) == 0:
return None
# Parse group filters from GET parameters. The keys in this dict define the
# allowed querystring parameters.
filt_params = {'show': set(), 'hide': set(), 'showtypes': set(), 'hidetypes': set()}
for key, value in querydict.items():
if key not in filt_params:
raise ValueError('Unrecognized parameter "%s"' % key)
if value is None:
return ValueError(
'Parameter "%s" is not assigned a value (use "key=" for an empty value)' % key
)
vals = unquote(value).lower().split(',')
filt_params[key] = set([v for v in vals if len(v) > 0]) # remove empty strings
return filt_params
def should_include_assignment(filter_params, assignment):
"""Decide whether to include an assignment
When filtering by wg, uses historic_group if available as an attribute
on the session, otherwise falls back to using group.
"""
historic_group = getattr(assignment.session, 'historic_group', None)
if historic_group:
group_acronym = historic_group.acronym
parent = historic_group.historic_parent
parent_acronym = parent.acronym if parent else None
else:
group = assignment.session.group
group_acronym = group.acronym
if group.parent:
parent_acronym = group.parent.acronym
else:
parent_acronym = None
session_type = assignment.timeslot.type_id
# Hide if wg or type hide lists apply
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
return ((group_acronym in filter_params['show']) or
(parent_acronym in filter_params['show']) or
(session_type in filter_params['showtypes']))
def agenda_ical(request, num=None, name=None, acronym=None, session_id=None):
"""Agenda ical view
By default, all agenda items will be shown. A filter can be specified in
the querystring. It has the format
?show=...&hide=...&showtypes=...&hidetypes=...
where any of the parameters can be omitted. The right-hand side of each
'=' is a comma separated list, which can be empty. If none of the filter
parameters are specified, no filtering will be applied, even if the query
string is not empty.
The show and hide parameters each take a list of working group (wg) acronyms.
The showtypes and hidetypes parameters take a list of session types.
Hiding (by wg or type) takes priority over showing.
"""
meeting = get_meeting(num, type_in=None)
schedule = get_schedule(meeting, name)
updated = meeting.updated()
@ -1781,41 +1853,20 @@ def agenda_ical(request, num=None, name=None, acronym=None, session_id=None):
if schedule is None and acronym is None and session_id is None:
raise Http404
q = request.META.get('QUERY_STRING','') or ""
filter = set(unquote(q).lower().split(','))
include = [ i for i in filter if not (i.startswith('-') or i.startswith('~')) ]
include_types = set(["plenary","other"])
exclude = []
# Process the special flags.
# "-wgname" will remove a working group from the output.
# "~Type" will add that type to the output.
# "-~Type" will remove that type from the output
# Current types are:
# Session, Other (default on), Break, Plenary (default on)
# Non-Working Group "wg names" include:
# edu, ietf, tools, iesg, iab
for item in filter:
if len(item) > 2 and item[0] == '-' and item[1] == '~':
include_types -= set([item[2:]])
elif len(item) > 1 and item[0] == '-':
exclude.append(item[1:])
elif len(item) > 1 and item[0] == '~':
include_types |= set([item[1:]])
assignments = SchedTimeSessAssignment.objects.filter(
schedule__in=[schedule, schedule.base],
timeslot__type__private=False,
)
assignments = preprocess_assignments_for_agenda(assignments, meeting)
if q:
assignments = [a for a in assignments if
(a.timeslot.type_id in include_types
or (a.session.historic_group and a.session.historic_group.acronym in include)
or (a.session.historic_group and a.session.historic_group.historic_parent and a.session.historic_group.historic_parent.acronym in include))
and (not a.session.historic_group or a.session.historic_group.acronym not in exclude)]
try:
filt_params = parse_agenda_filter_params(request.GET)
except ValueError as e:
return HttpResponseBadRequest(str(e))
if filt_params is not None:
# Apply the filter
assignments = [a for a in assignments if should_include_assignment(filt_params, a)]
if acronym:
assignments = [ a for a in assignments if a.session.historic_group and a.session.historic_group.acronym == acronym ]
@ -3189,6 +3240,7 @@ def upcoming(request):
ietf_meetings = Meeting.objects.filter(type_id='ietf', date__gte=today-datetime.timedelta(days=7))
for m in ietf_meetings:
m.end = m.date+datetime.timedelta(days=m.days)
interim_sessions = add_event_info_to_session_qs(
Session.objects.filter(
meeting__type_id='interim',
@ -3196,6 +3248,22 @@ def upcoming(request):
timeslotassignments__timeslot__time__gte=today
)
).filter(current_status__in=('sched','canceled'))
# 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
@ -3210,15 +3278,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,
@ -3228,8 +3306,11 @@ def upcoming(request):
def upcoming_ical(request):
'''Return Upcoming meetings in iCalendar file'''
filters = request.GET.getlist('filters')
"""Return Upcoming meetings in iCalendar file
Filters by wg name and session type.
"""
filter_params = parse_agenda_filter_params(request.GET)
today = datetime.date.today()
# get meetings starting 7 days ago -- we'll filter out sessions in the past further down
@ -3246,13 +3327,8 @@ def upcoming_ical(request):
).distinct())
# apply filters
if filters:
assignments = [a for a in assignments if
a.session.group and (
a.session.group.acronym in filters or (
a.session.group.parent and a.session.group.parent.acronym in filters
)
) ]
if filter_params is not None:
assignments = [a for a in assignments if should_include_assignment(filter_params, a)]
# we already collected sessions with current_status, so reuse those
sessions = {s.pk: s for m in meetings for s in m.sessions}

View file

@ -0,0 +1,70 @@
* New filtering behavior
Agenda filtering is defined by four parameters
* show (group/area)
* hide (group/area)
* showtypes (session type)
* hidetypes (session type)
These are specified as querystring parameters to the requested URL. The value
for each parameter is a comma-separated list of groups or types. For example,
the following URL specifies that sessions the babel group or art area (except
for the asdf and dispatch groups) should be shown:
https://datatracker.ietf.org/meeting/agenda/?show=art,babel&hide=asdf,dispatch
Note that, in contrast to the URL fragment previously used, the querystring is
sent in the HTTP request. As a result, manual changes to the filter string will
cause page reloads. On modern browsers, the customization UI avoids these
unnecessary reloads.
If none of the filtering parameters are specified, then filtering is disabled
and all sessions will be shown. If one or more is specified, even if empty,
filtering is enabled. The filtered list of sessions begins empty. Sessions are
added and removed by the following steps, in order.
1. Add any session whose group or area is in the "show" list.
2. Add any session whose type is in the "showtypes" list.
3. Remove any session whose group or area is in the "hide list.
4. Remove any session whose type is in thie "hidetypes" list.
As a result, the "hide" and "hidetypes" list take priority over the "show" and
"showtypes" lists.
Filtering is implemented for the meeting agenda page and its iCal download
links and for the upcoming meetings page and its iCal links. Filtering is
performed server-side for the iCal links and client-side for the other pages.
The upcoming meetings page only filters using group/area filters and ignores
the type filters.
Unrecognized or badly formatted parameter names are treated as errors. Empty
parameter values are allowed. Group, area, and type names are not validated,
so care must be taken to avoid typos.
* New UI: "Non-area" customization buttons
The "special sessions" buttons across the bottom of the customization UI have
been moved to a "Non-area" column on the right side of the UI. This has a
heading button and two sets of buttons. The top set contains session type
buttons ("Plenary" and "Other"). The bottom set contains group and area
buttons.
When no non-area session type is active, only "regular" sessions will appear in
the agenda. In this situation, the non-area group buttons are disabled. A click
on the header button will enable or disable both session types.
If one or both session types is activated, sessions of that type will be shown
and the non-area group buttons will be enabled. These are active by default.
Clicking an active group button will deactivate the button and add the
corresponding group to the "hide" list. If the group was in the "show" list, it
will be removed when added to the "hide" list (and vice versa).
* New UI: Upcoming meeting customization
The customization UI from the agenda page now appears on the upcoming meetings
page. It does not include the "Non-area" buttons.

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

@ -1,74 +0,0 @@
function toggle_visibility() {
var h = window.location.hash;
h = h.replace(/^#?,?/, '');
// reset UI elements to default state
$(".pickview").removeClass("active disabled");
$(".pickviewneg").addClass("active");
if (h) {
// if there are items in the hash, hide all rows
$('[id^="row-"]').hide();
// show the customizer
$("#customize").collapse("show");
// loop through the has items and change the UI element and row visibilities accordingly
var query_array = [];
$.each(h.split(","), function (i, v) {
if (v.indexOf("-") == 0) {
// this is a "negative" item: when present, hide these rows
v = v.replace(/^-/, '');
$('[id^="row-"]').filter('[id*="-' + v + '"]').hide();
$(".view." + v).find("button").removeClass("active disabled");
$("button.pickviewneg." + v).removeClass("active");
} else {
// this is a regular item: when present, show these rows
$('[id^="row-"]').filter('[id*="-' + v + '"]').show();
$(".view." + v).find("button").addClass("active disabled");
$("button.pickview." + v).addClass("active");
query_array.push("filters=" + v)
}
});
// adjust the custom .ics link
var link = $('a[href*="upcoming.ics"]');
var new_href = link.attr("href").split("?")[0]+"?"+query_array.join("&");
link.attr("href",new_href);
} else {
// if the hash is empty, show all
$('[id^="row-"]').show();
// adjust the custom .ics link
var link = $('a[href*="upcoming.ics"]');
link.attr("href",link.attr("href").split("?")[0]);
}
}
$(".pickview, .pickviewneg").click(function () {
var h = window.location.hash;
var item = $(this).text().trim().toLowerCase();
if ($(this).hasClass("pickviewneg")) {
item = "-" + item;
}
re = new RegExp('(^|#|,)' + item + "(,|$)");
if (h.match(re) == null) {
if (h.replace("#", "").length == 0) {
h = item;
} else {
h += "," + item;
}
h = h.replace(/^#?,/, '');
} else {
h = h.replace(re, "$2").replace(/^#?,/, '');
}
window.location.hash = h.replace(/^#$/, '');
toggle_visibility();
});
$(document).ready(function () {
toggle_visibility();
});

View file

@ -34,7 +34,7 @@
<div class="panel panel-default" id="futuremeets">
<div class="panel-heading">
Future Meetings
<a class="regular pull-right" title="icalendar entry for all scheduled future {{group.acronym}} meetings" href="{% url 'ietf.meeting.views.upcoming_ical' %}?filters={{group.acronym}}"><span class="fa fa-calendar"></span></a>
<a class="regular pull-right" title="icalendar entry for all scheduled future {{group.acronym}} meetings" href="{% url 'ietf.meeting.views.upcoming_ical' %}?show={{group.acronym}}"><span class="fa fa-calendar"></span></a>
</div>
<div class="panel-body">
{% with sessions=future show_request=True show_ical=True can_edit_materials=can_edit %}

View file

@ -62,82 +62,14 @@
{% 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 pickviewneg active iepg"> IEPG</button></div>
<div class="btn-group"><button class="btn btn-default pickviewneg active tools"> Tools</button></div>
<div class="btn-group"><button class="btn btn-default pickviewneg active edu"> EDU</button></div>
<div class="btn-group"><button class="btn btn-default pickviewneg active ietf"> IETF</button></div>
<div class="btn-group"><button class="btn btn-default pickviewneg active iesg"> IESG</button></div>
<div class="btn-group"><button class="btn btn-default pickviewneg active iab"> IAB</button></div>
</div>
</div>
</div>
</div>
</div>
{% include "meeting/agenda_filter.html" with group_parents=group_parents non_area_filters=True customize_button_text="Customize the agenda view..." only %}
<h2>Download as .ics</h2>
<p class="buttonlist">
{% for p in group_parents %}
<a class="btn btn-default" href="{% url "ietf.meeting.views.agenda_ical" num=schedule.meeting.number %}?{{p.acronym|upper}},-~Other,-~Plenary">{{p.acronym|upper}}</a>
<a class="btn btn-default" href="{% url "ietf.meeting.views.agenda_ical" num=schedule.meeting.number %}?show={{p.acronym|upper}}">{{p.acronym|upper}}</a>
{% endfor %}
<a class="btn btn-default" href="{% url "ietf.meeting.views.agenda_ical" num=schedule.meeting.number %}?~Plenary,~Other">Non-area events</a>
<a class="btn btn-default" href="{% url "ietf.meeting.views.agenda_ical" num=schedule.meeting.number %}?showtypes=plenary,other">Non-area events</a>
<a id="ical-link" class="hidden btn btn-primary" href="{% url "ietf.meeting.views.agenda_ical" num=schedule.meeting.number %}">Customized schedule</a>
</p>
@ -193,7 +125,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 }}" 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">
<span class="hidden-xs">
{% include "meeting/timeslot_start_end.html" %}
@ -252,7 +187,11 @@
{% if item.timeslot.type_id == 'regular' or item.timeslot.type.slug == 'plenary' %}
{% if item.session.historic_group %}
<tr id="row-{{item.slug}}" 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">
<span class="hidden-xs">
@ -375,82 +314,86 @@
{% endblock %}
{% block js %}
<script src="{% static 'ietf/js/agenda/agenda_filter.js' %}"></script>
<script>
function toggle_visibility() {
var h = window.location.hash;
h = h.replace(/^#?,?/, '');
// Update the agenda display with specified filters
function update_agenda_display(filter_params) {
var agenda_rows=$('[id^="row-"]')
// reset UI elements to default state
$(".pickview").removeClass("active disabled");
$(".pickviewneg").addClass("active");
if (!agenda_filter.filtering_is_enabled(filter_params)) {
// When filtering is not enabled, show all sessions
agenda_rows.show();
return;
}
if (h) {
// if there are items in the hash, hide all rows that are
// hidden by default, show all rows that are shown by default
$('[id^="row-"]').hide();
$.each($(".pickviewneg").text().trim().split(/ +/), function (i, v) {
v = v.trim().toLowerCase();
$('[id^="row-"]').filter('[id*="-' + v + '"]').show();
});
// if groups were selected for filtering, hide all rows by default
agenda_rows.hide();
// show the customizer
$("#customize").collapse("show");
// loop through the has items and change the UI element and row visibilities accordingly
$.each(h.split(","), function (i, v) {
if (v.indexOf("-") == 0) {
// this is a "negative" item: when present, hide these rows
v = v.replace(/^-/, '');
$('[id^="row-"]').filter('[id*="-' + v + '"]').hide();
$(".view." + v).find("button").removeClass("active disabled");
$("button.pickviewneg." + v).removeClass("active");
} else {
// this is a regular item: when present, show these rows
$('[id^="row-"]').filter('[id*="-' + v + '"]').show();
$(".view." + v).find("button").addClass("active disabled");
$("button.pickview." + v).addClass("active");
}
});
// show the week view
$("#weekview").attr("src", "week-view.html" + window.location.hash).removeClass("hidden");
// show the custom .ics link
$("#ical-link").attr("href",$("#ical-link").attr("href").split("?")[0]+"?"+h);
$("#ical-link").removeClass("hidden");
// 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 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
$('[id^="row-"]').show();
$("#ical-link, #weekview").addClass("hidden");
ical_link.addClass("hidden");
}
}
$(".pickview, .pickviewneg").click(function () {
var h = window.location.hash;
var item = $(this).text().trim().toLowerCase();
if ($(this).hasClass("pickviewneg")) {
item = "-" + item;
}
function update_weekview(filter_params) {
var weekview = $("#weekview");
re = new RegExp('(^|#|,)' + item + "(,|$)");
if (h.match(re) == null) {
if (h.replace("#", "").length == 0) {
h = item;
} else {
h += "," + item;
}
h = h.replace(/^#?,/, '');
} else {
h = h.replace(re, "$2").replace(/^#?,/, '');
}
window.location.hash = h.replace(/^#$/, '');
toggle_visibility();
});
if (!agenda_filter.filtering_is_enabled(filter_params)) {
weekview.addClass("hidden");
return;
}
$(document).ready(function () {
toggle_visibility();
});
// 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)
}
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> {% firstof customize_button_text "Customize..."%}
</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

@ -71,5 +71,4 @@
{% block js %}
<script src="{% static "jquery.tablesorter/js/jquery.tablesorter.combined.min.js" %}"></script>
<script src="{% static 'ietf/js/toggle-visibility.js' %}"></script>
{% endblock %}

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 customize_button_text="Customize the meeting list..." 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

@ -107,33 +107,61 @@
}
}
//===========================================================================
function is_visible(include) {
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) {
// Returns a method to filter objects for visibility
// Accepts show, hide, showtypes, and hidetypes filters. Also 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) {
// "-wgname" will remove a working group from the output.
// "~Type" will add that type to the output.
// "-~Type" will remove that type from the output
// "@bof" will include all BOFs
// Current types are:
// Session, Other, Break, Plenary
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 ("group" in item) {
if (include[(item.group).toLowerCase()]) { return true; }
if (include["-"+(item.group).toLowerCase()]) { return false; }
if ((fp['hide_groups'].indexOf(item_group) >= 0) ||
(fp['hide_groups'].indexOf(item_area) >= 0) ||
(fp['hide_types'].indexOf(item_type) >= 0)) {
return false;
}
if ("state" in item) {
if (include["@"+(item.state).toLowerCase()]) { return true; }
}
if (include["~"+(item.type).toLowerCase()]) { return true; }
if (include["-~"+(item.type).toLowerCase()]) { return false; }
if ("area" in item) {
if (include[(item.area).toLowerCase()]) { return true; }
}
if (item.type === "Plenary") { return true; }
if (item.type === "Other") { return true; }
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]);
}
}
@ -143,16 +171,23 @@
var width = document.body.clientWidth;
var height = document.body.clientHeight;
var include = {};
window.location.hash.replace("#",'').split(',').forEach(function(key){
include[(key + "").toLowerCase()] = true;
});
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 visible_items = all_items.filter(is_visible(include));
var start_day = visible_items[0].day;
var start_day;
var day_start;
if (visible_items.length > 0) {
start_day = visible_items[0].day;
day_start = visible_items[0].start_time;
} else {
// fallback in case all items were filtered
start_day = all_items[0].day;
day_start = all_items[0].start_time;
}
var end_day = start_day;
var day_start = visible_items[0].start_time;
var day_end = 0;
compute_swimlanes(visible_items);
@ -324,6 +359,11 @@
document.body.appendChild(e);
});
// Div to indicate rendering has occurred, for testing purposes.
var elt = document.createElement('div');
elt.id = 'wv-end';
document.body.appendChild(elt);
}
//===========================================================================

View file

@ -428,7 +428,6 @@ def make_test_data():
return draft
return draft
def make_review_data(doc):
team1 = create_group(acronym="reviewteam", name="Review Team", type_id="review", list_email="reviewteam@ietf.org", parent=Group.objects.get(acronym="farfut"))

View file

@ -789,6 +789,8 @@ class IetfLiveServerTestCase(StaticLiveServerTestCase):
set_coverage_checking(False)
super(IetfLiveServerTestCase, cls).setUpClass()
def setUp(self):
super(IetfLiveServerTestCase, self).setUp()
# LiveServerTestCase uses TransactionTestCase which seems to
# somehow interfere with the fixture loading process in
# IetfTestRunner when running multiple tests (the first test