Use querystring instead of URL hash for agenda filters

- Legacy-Id: 18353
This commit is contained in:
Jennifer Richards 2020-08-11 13:45:41 +00:00
parent c5729d5c5d
commit ea3882034a
9 changed files with 331 additions and 158 deletions

View file

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

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

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

View file

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

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

@ -359,16 +359,32 @@
{% block js %}
<script>
function parse_query_params(qs) {
var params = {};
qs = qs.replace(/^\?/, '');
$.each(qs.split('&'), function(i, v) {
var toks = v.split('=', 2)
params[toks[0]] = toks[1];
});
return params;
}
/* filt = 'show' or 'hide' */
function get_filter_from_qparams(qparams, filt) {
return qparams[filt] ? qparams[filt].split(',') : [];
}
function toggle_visibility() {
var h = window.location.hash;
h = h.replace(/^#?,?/, '');
var qparams = parse_query_params(window.location.search);
var show_groups = get_filter_from_qparams(qparams, 'show');
var hide_groups = get_filter_from_qparams(qparams, 'hide');
// 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 that are
if (show_groups.length || hide_groups.length) {
// if groups were selected for filtering, 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) {
@ -380,26 +396,26 @@
$("#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");
}
$.each(hide_groups, 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");
});
$.each(show_groups, function (i, v) {
// 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").attr("href",$("#ical-link").attr("href").split("?")[0]+window.location.search);
$("#ical-link").removeClass("hidden");
} else {
@ -410,26 +426,53 @@
}
$(".pickview, .pickviewneg").click(function () {
var h = window.location.hash;
// Get clicked item label
var item = $(this).text().trim().toLowerCase();
var qparams = parse_query_params(window.location.search);
var show_groups = get_filter_from_qparams(qparams, 'show');
var hide_groups = get_filter_from_qparams(qparams, 'hide');
if ($(this).hasClass("pickviewneg")) {
item = "-" + item;
toggle_list_item(hide_groups, item);
} else {
toggle_list_item(show_groups, item);
}
update_filters(show_groups, hide_groups);
});
/* Add to list if not present, remove if present */
function toggle_list_item(list, item) {
var item_index = $.inArray(item, list);
if (item_index === -1) {
list.push(item);
} else {
list.splice(item_index, 1);
}
}
function update_filters(show, hide) {
var qparams = [];
var search = '';
if (show.length > 0) {
qparams.push('show=' + show.join());
}
if (hide.length > 0) {
qparams.push('hide=' + hide.join());
}
if (qparams.length > 0) {
search = '?' + qparams.join('&');
}
re = new RegExp('(^|#|,)' + item + "(,|$)");
if (h.match(re) == null) {
if (h.replace("#", "").length == 0) {
h = item;
} else {
h += "," + item;
}
h = h.replace(/^#?,/, '');
var new_url = window.location.href.replace(/(\?.*)?$/, search);
if (window.history && window.history.replaceState) {
// Keep current origin, replace search string, no page reload
history.replaceState({}, document.title, new_url);
toggle_visibility();
} else {
h = h.replace(re, "$2").replace(/^#?,/, '');
// No window.history.replaceState support, page reload required
window.location = new_url;
}
window.location.hash = h.replace(/^#$/, '');
toggle_visibility();
});
}
$(document).ready(function () {
toggle_visibility();

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

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