From ea3882034a039bd55f4156efd97cd07d4223ee39 Mon Sep 17 00:00:00 2001 From: Jennifer Richards Date: Tue, 11 Aug 2020 13:45:41 +0000 Subject: [PATCH] Use querystring instead of URL hash for agenda filters - Legacy-Id: 18353 --- ietf/meeting/test_data.py | 11 + ietf/meeting/tests_api.py | 3 +- ietf/meeting/tests_js.py | 288 +++++++++++++++++++---- ietf/meeting/tests_views.py | 2 +- ietf/static/ietf/js/toggle-visibility.js | 74 ------ ietf/templates/meeting/agenda.html | 107 ++++++--- ietf/templates/meeting/past.html | 1 - ietf/utils/test_data.py | 1 - ietf/utils/test_runner.py | 2 + 9 files changed, 331 insertions(+), 158 deletions(-) delete mode 100644 ietf/static/ietf/js/toggle-visibility.js diff --git a/ietf/meeting/test_data.py b/ietf/meeting/test_data.py index 3545b9589..37ad1b557 100644 --- a/ietf/meeting/test_data.py +++ b/ietf/meeting/test_data.py @@ -115,6 +115,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, @@ -158,6 +161,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=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() diff --git a/ietf/meeting/tests_api.py b/ietf/meeting/tests_api.py index d176d3749..c7d010dc5 100644 --- a/ietf/meeting/tests_api.py +++ b/ietf/meeting/tests_api.py @@ -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)) diff --git a/ietf/meeting/tests_js.py b/ietf/meeting/tests_js.py index 728854dd8..c66beace9 100644 --- a/ietf/meeting/tests_js.py +++ b/ietf/meeting/tests_js.py @@ -30,6 +30,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 @@ -50,29 +51,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() @@ -206,28 +215,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() @@ -265,27 +253,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): @@ -305,6 +282,223 @@ 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): + # Groups whose display logic is inverted in agenda.html. These have + # toggles with class 'pickviewneg' in the template. + PICKVIEWNEG = ['iepg', 'tools', 'edu', 'ietf', 'iesg', 'iab'] + + 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')) + + # Only 'show' param + result = self.driver.execute_script( + 'return parse_query_params("?show=group1,group2,group3");' + ) + self.assertEqual(result, dict(show='group1,group2,group3')) + + # Only 'hide' param + result = self.driver.execute_script( + 'return parse_query_params("?hide=group4,group5,group6");' + ) + self.assertEqual(result, dict(hide='group4,group5,group6')) + + # Both 'show' and 'hide' + result = self.driver.execute_script( + 'return parse_query_params("?show=group1,group2,group3&hide=group4,group5,group6");' + ) + 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(list0, 'item'); + + // one item, remove it + var list1=['item']; + toggle_list_item(list1, 'item'); + + // one item, add another + var list2=['item1']; + toggle_list_item(list2, 'item2'); + + // multiple items, remove first + var list3=['item1', 'item2', 'item3']; + toggle_list_item(list3, 'item1'); + + // multiple items, remove middle + var list4=['item1', 'item2', 'item3']; + toggle_list_item(list4, 'item2'); + + // multiple items, remove last + var list5=['item1', 'item2', 'item3']; + toggle_list_item(list5, 'item3'); + + return [list0, list1, list2, list3, list4, list5]; + """ + ) + 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 test_agenda_view_filter_show_one(self): + """Filtered agenda view should display only matching rows (one group selected)""" + self.login() + self.driver.get(self.absreverse('ietf.meeting.views.agenda') + '?show=mars') + self.assert_agenda_item_visibility(['mars'] + self.PICKVIEWNEG) # ames and secretariat not selected + + def test_agenda_view_filter_show_two(self): + """Filtered agenda view should display only matching rows (two groups selected)""" + self.login() + self.driver.get(self.absreverse('ietf.meeting.views.agenda') + '?show=mars,ames') + self.assert_agenda_item_visibility(['mars', 'ames'] + self.PICKVIEWNEG) # secretariat not selected + + def test_agenda_view_filter_all(self): + """Filtered agenda view should display only matching rows (all groups selected)""" + self.login() + self.driver.get(self.absreverse('ietf.meeting.views.agenda')) + self.assert_agenda_item_visibility() + + def test_agenda_view_filter_hide(self): + self.login() + self.driver.get(self.absreverse('ietf.meeting.views.agenda') + '?hide=ietf') + self.assert_agenda_item_visibility([g for g in self.PICKVIEWNEG if g != 'ietf']) + + def test_agenda_view_filter_show_and_hide(self): + self.login() + self.driver.get(self.absreverse('ietf.meeting.views.agenda') + '?show=mars&hide=ietf') + self.assert_agenda_item_visibility( + ['mars'] + [g for g in self.PICKVIEWNEG if g != 'ietf'] + ) + + def assert_agenda_item_visibility(self, visible_groups=()): + """Assert that correct items are visible in current browser window + + If visible_groups is empty (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 len(visible_groups) == 0 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 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] + self.PICKVIEWNEG) + + # 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. + # The following are useful debugging tools # If you add this to a LiveServerTestCase and run just this test, you can browse diff --git a/ietf/meeting/tests_views.py b/ietf/meeting/tests_views.py index 84897c660..b887920c2 100644 --- a/ietf/meeting/tests_views.py +++ b/ietf/meeting/tests_views.py @@ -1596,7 +1596,7 @@ class InterimTests(TestCase): 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'), 7) + self.assertEqual(r.content.count(b'UID'), 8) # check filtered output url = url + '?filters=mars' r = self.client.get(url) diff --git a/ietf/static/ietf/js/toggle-visibility.js b/ietf/static/ietf/js/toggle-visibility.js deleted file mode 100644 index 75f6c59a9..000000000 --- a/ietf/static/ietf/js/toggle-visibility.js +++ /dev/null @@ -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(); -}); - diff --git a/ietf/templates/meeting/agenda.html b/ietf/templates/meeting/agenda.html index 135ba35a9..68fa60f8c 100644 --- a/ietf/templates/meeting/agenda.html +++ b/ietf/templates/meeting/agenda.html @@ -359,16 +359,32 @@ {% block js %} - {% endblock %} diff --git a/ietf/utils/test_data.py b/ietf/utils/test_data.py index 9aa7bbdbd..231631b5d 100644 --- a/ietf/utils/test_data.py +++ b/ietf/utils/test_data.py @@ -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")) diff --git a/ietf/utils/test_runner.py b/ietf/utils/test_runner.py index 5826a6f67..a2b2fbc82 100644 --- a/ietf/utils/test_runner.py +++ b/ietf/utils/test_runner.py @@ -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