From bbf04c3fbecb6a49d55d75c95a77fe2acbfe1419 Mon Sep 17 00:00:00 2001 From: Jennifer Richards Date: Fri, 30 Oct 2020 17:17:49 +0000 Subject: [PATCH] Retrieve session agenda, slides, and minutes each time agenda modal is opened. Fixes #3050. Commit ready for merge. - Legacy-Id: 18651 --- ietf/meeting/tests_js.py | 108 +++++++++++++++++- ietf/meeting/tests_views.py | 60 +++++++++- ietf/meeting/urls.py | 1 + ietf/meeting/views.py | 12 +- ietf/templates/meeting/agenda.html | 103 +++++++++++------ .../meeting/assignment_materials.html | 42 +++++++ .../meeting/session_agenda_include.html | 40 +------ 7 files changed, 289 insertions(+), 77 deletions(-) create mode 100644 ietf/templates/meeting/assignment_materials.html diff --git a/ietf/meeting/tests_js.py b/ietf/meeting/tests_js.py index 4951cb2dc..06acaa0cb 100644 --- a/ietf/meeting/tests_js.py +++ b/ietf/meeting/tests_js.py @@ -18,6 +18,7 @@ from django.db.models import F import debug # pyflakes:ignore from ietf.doc.factories import DocumentFactory +from ietf.doc.models import State from ietf.group import colors from ietf.person.models import Person from ietf.group.models import Group @@ -783,7 +784,7 @@ class AgendaTests(MeetingTestCase): ics_url = self.absreverse('ietf.meeting.views.agenda_ical') # parse out the events - agenda_rows = self.driver.find_elements_by_css_selector('[id^="row-"') + agenda_rows = self.driver.find_elements_by_css_selector('[id^="row-"]') visible_rows = [r for r in agenda_rows if r.is_displayed()] sessions = [self.session_from_agenda_row_id(row.get_attribute("id")) for row in visible_rows] @@ -797,6 +798,111 @@ class AgendaTests(MeetingTestCase): expected_event_uids=expected_uids, expected_event_count=len(sessions)) + def test_session_materials_modal(self): + """Test opening and re-opening a session materals modal + + This currently only tests the slides to ensure that changes to these are picked up + without reloading the main agenda page. This should also test that the agenda and + minutes are displayed and updated correctly, but problems with WebDriver/Selenium/Chromedriver + are blocking this. + """ + session = self.meeting.session_set.filter(group__acronym="mars").first() + assignment = session.official_timeslotassignment() + slug = assignment.slug() + + url = self.absreverse('ietf.meeting.views.agenda') + self.driver.get(url) + + # modal should start hidden + modal_div = self.driver.find_element_by_css_selector('div#modal-%s' % slug) + self.assertFalse(modal_div.is_displayed()) + + # Click the 'materials' button + open_modal_button = WebDriverWait(self.driver, 2).until( + expected_conditions.element_to_be_clickable( + (By.CSS_SELECTOR, '[data-target="#modal-%s"]' % slug) + ), + 'Modal open button not found or not clickable', + ) + open_modal_button.click() + WebDriverWait(self.driver, 2).until( + expected_conditions.visibility_of(modal_div), + 'Modal did not become visible after clicking open button', + ) + + # Check that we have the expected slides + not_deleted_slides = session.materials.filter( + type='slides' + ).exclude( + states__type__slug='slides',states__slug='deleted' + ) + self.assertGreater(not_deleted_slides.count(), 0) # make sure this isn't a pointless test + for slide in not_deleted_slides: + anchor = self.driver.find_element_by_xpath('//a[text()="%s"]' % slide.title) + self.assertIsNotNone(anchor) + + deleted_slides = session.materials.filter( + type='slides', states__type__slug='slides', states__slug='deleted' + ) + self.assertGreater(deleted_slides.count(), 0) # make sure this isn't a pointless test + for slide in deleted_slides: + with self.assertRaises(NoSuchElementException): + self.driver.find_element_by_xpath('//a[text()="%s"]' % slide.title) + + # Now close the modal + close_modal_button = WebDriverWait(self.driver, 2).until( + expected_conditions.element_to_be_clickable( + (By.CSS_SELECTOR, '.modal-footer button[data-dismiss="modal"]') + ), + 'Modal close button not found or not clickable', + ) + close_modal_button.click() + WebDriverWait(self.driver, 2).until( + expected_conditions.invisibility_of_element(modal_div), + 'Modal was not hidden after clicking close button', + ) + + # Modify the session info + newly_deleted_slide = not_deleted_slides.first() + newly_undeleted_slide = deleted_slides.first() + newly_deleted_slide.set_state(State.objects.get(type="slides", slug="deleted")) + newly_undeleted_slide.set_state(State.objects.get(type="slides", slug="active")) + + # Click the 'materials' button + open_modal_button = WebDriverWait(self.driver, 2).until( + expected_conditions.element_to_be_clickable( + (By.CSS_SELECTOR, '[data-target="#modal-%s"]' % slug) + ), + 'Modal open button not found or not clickable for refresh test', + ) + open_modal_button.click() + WebDriverWait(self.driver, 2).until( + expected_conditions.visibility_of(modal_div), + 'Modal did not become visible after clicking open button for refresh test', + ) + + # Check that we now see the updated slides + not_deleted_slides = session.materials.filter( + type='slides' + ).exclude( + states__type__slug='slides',states__slug='deleted' + ) + self.assertNotIn(newly_deleted_slide, not_deleted_slides) + self.assertIn(newly_undeleted_slide, not_deleted_slides) + for slide in not_deleted_slides: + anchor = self.driver.find_element_by_xpath('//a[text()="%s"]' % slide.title) + self.assertIsNotNone(anchor) + + deleted_slides = session.materials.filter( + type='slides', states__type__slug='slides', states__slug='deleted' + ) + self.assertIn(newly_deleted_slide, deleted_slides) + self.assertNotIn(newly_undeleted_slide, deleted_slides) + for slide in deleted_slides: + with self.assertRaises(NoSuchElementException): + self.driver.find_element_by_xpath('//a[text()="%s"]' % slide.title) + + @skipIf(skip_selenium, skip_message) class InterimTests(MeetingTestCase): def setUp(self): diff --git a/ietf/meeting/tests_views.py b/ietf/meeting/tests_views.py index 55706a653..f7406c6b3 100644 --- a/ietf/meeting/tests_views.py +++ b/ietf/meeting/tests_views.py @@ -158,12 +158,16 @@ class MeetingTests(TestCase): self.assertIn(time_interval, agenda_content) self.assertIn(registration_text, agenda_content) - # Make sure there's a frame for the agenda and it points to the right place - self.assertTrue(any([session.materials.get(type='agenda').get_href() in x.attrib["data-src"] for x in q('tr div.modal-body div.frame')])) - - # Make sure undeleted slides are present and deleted slides are not - self.assertTrue(any([session.materials.filter(type='slides').exclude(states__type__slug='slides',states__slug='deleted').first().title in x.text for x in q('tr div.modal-body ul a')])) - self.assertFalse(any([session.materials.filter(type='slides',states__type__slug='slides',states__slug='deleted').first().title in x.text for x in q('tr div.modal-body ul a')])) + # Make sure there's a frame for the session agenda and it points to the right place + assignment = session.official_timeslotassignment() + assignment_url = urlreverse('ietf.meeting.views.assignment_materials', + kwargs=dict(assignment_id=assignment.pk)) + self.assertTrue( + any( + [assignment_url in x.attrib["data-src"] + for x in q('tr div.modal-body div.assignment-materials')] + ) + ) # future meeting, no agenda r = self.client.get(urlreverse("ietf.meeting.views.agenda", kwargs=dict(num=future_meeting.number))) @@ -844,6 +848,50 @@ class MeetingTests(TestCase): self.assertIn('STATUS:CANCELLED',unicontent(r)) self.assertNotIn('STATUS:CONFIRMED',unicontent(r)) + def test_assignment_materials(self): + meeting = make_meeting_test_data() + session = Session.objects.filter(meeting=meeting, group__acronym="mars").first() + + for assignment in session.timeslotassignments.all(): + url = urlreverse('ietf.meeting.views.assignment_materials', + kwargs=dict(assignment_id=assignment.pk)) + r = self.client.get(url) + self.assertEqual(r.status_code, 200) + q = PyQuery(r.content) + + agenda_div = q('div.agenda-frame') + self.assertIsNotNone(agenda_div) + self.assertEqual(agenda_div.attr('data-src'), session.agenda().get_href()) + + minutes_div = q('div.minutes-frame') + self.assertIsNotNone(minutes_div) + self.assertEqual(minutes_div.attr('data-src'), session.minutes().get_href()) + + # Make sure undeleted slides are present and deleted slides are not + not_deleted_slides = session.materials.filter( + type='slides' + ).exclude( + states__type__slug='slides',states__slug='deleted' + ) + self.assertGreater(not_deleted_slides.count(), 0) # make sure this isn't a pointless test + + deleted_slides = session.materials.filter( + type='slides', states__type__slug='slides', states__slug='deleted' + ) + self.assertGreater(deleted_slides.count(), 0) # make sure this isn't a pointless test + + # live slides should be found + for slide in not_deleted_slides: + self.assertTrue(q('ul li a:contains("%s")' % slide.title)) + + # deleted slides should not be found + for slide in deleted_slides: + self.assertFalse(q('ul li a:contains("%s")' % slide.title)) + + + for slide in session.slides(): + self.assertContains(r, slide.title) + class ReorderSlidesTests(TestCase): def test_add_slides_to_session(self): diff --git a/ietf/meeting/urls.py b/ietf/meeting/urls.py index c914a5ceb..a0f8690c6 100644 --- a/ietf/meeting/urls.py +++ b/ietf/meeting/urls.py @@ -111,6 +111,7 @@ urlpatterns = [ # First patterns which start with unique strings url(r'^$', views.current_materials), url(r'^ajax/get-utc/?$', views.ajax_get_utc), + url(r'^assignment/(?P\d+)/materials.html$', views.assignment_materials), url(r'^interim/announce/?$', views.interim_announce), url(r'^interim/announce/(?P[A-Za-z0-9._+-]+)/?$', views.interim_send_announcement), url(r'^interim/skip_announce/(?P[A-Za-z0-9._+-]+)/?$', views.interim_skip_announcement), diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py index 0a7a0966c..4360eea79 100644 --- a/ietf/meeting/views.py +++ b/ietf/meeting/views.py @@ -242,7 +242,6 @@ def materials_document(request, document, num=None, ext=None): raise Http404("File not found: %s" % filename) old_proceedings_format = meeting.number.isdigit() and int(meeting.number) <= 96 - if settings.MEETING_MATERIALS_SERVE_LOCALLY or old_proceedings_format: with io.open(filename, 'rb') as file: bytes = file.read() @@ -1301,6 +1300,17 @@ def diff_schedules(request, num): 'to_schedule': to_schedule, }) +@ensure_csrf_cookie +def assignment_materials(request, assignment_id): + """Assignment details for agenda page pop-up""" + assignments = SchedTimeSessAssignment.objects.filter(pk=int(assignment_id)) + if len(assignments) == 0: + raise Http404('No such assignment') + assert len(assignments) == 1 + meeting = assignments[0].timeslot.meeting # timeslot is guaranteed to be non-null + assignments = preprocess_assignments_for_agenda(assignments, meeting) + assignment = assignments[0] + return render(request, 'meeting/assignment_materials.html', dict(item=assignment)) @ensure_csrf_cookie def agenda(request, num=None, name=None, base=None, ext=None, owner=None, utc=""): diff --git a/ietf/templates/meeting/agenda.html b/ietf/templates/meeting/agenda.html index 1e5c20cbf..ba929bbbb 100644 --- a/ietf/templates/meeting/agenda.html +++ b/ietf/templates/meeting/agenda.html @@ -25,6 +25,9 @@ background-color: inherit !important; border: none !important; } + .assignment-materials .agenda-frame,.minutes-frame { + white-space: normal; + } {% endblock %} {% block bodyAttrs %}data-spy="scroll" data-target="#affix"{% endblock %} @@ -389,39 +392,75 @@ agenda_filter.set_update_callback(update_view); agenda_filter.enable(); + /** + * Retrieve and display materials for a session + * + * If output_elt exists and has a "data-src" attribute, retrieves the document + * from that URL and displays under output_elt. Handles text/plain, text/markdown, + * and text/html. + * + * @param output_elt Element, probably a div, to hold the output + */ + function retrieve_session_materials(output_elt) { + if (!output_elt) {return;} + output_elt = $(output_elt); + var data_src = output_elt.attr("data-src"); + if (!data_src) { + output_elt.html("

Error: missing data-src attribute

"); + } else { + output_elt.html("

Loading " + data_src + "...

"); + outer_xhr = $.get(data_src) + outer_xhr.done(function(data, status, xhr) { + var t = xhr.getResponseHeader("content-type"); + if (!t) { + data = "

Error retrieving " + data_src + + ": Missing content-type in response header

"; + } else if (t.indexOf("text/plain") > -1) { + data = "
" + data + "
"; + } else if (t.indexOf("text/markdown") > -1) { + data = "
" + data + "
"; + } else if(t.indexOf("text/html") > -1) { + // nothing to do here + } else { + data = "

Unknown type: " + xhr.getResponseHeader("content-type") + "

"; + } + output_elt.html(data); + }).fail(function() { + output_elt.html("

Error retrieving " + data_src + + ": (" + outer_xhr.status.toString() + ") " + + outer_xhr.statusText + "

"); + }) + } + } + + /** + * Retrieve contents of a session materials modal + * + * Expects output_elt to exist and have a "data-src" attribute. Retrieves the + * contents of that URL, then attempts to populate the .agenda-frame and + * .minutes-frame elements. + * + * @param output_elt Element, probably a div, to hold the output + */ + function retrieve_session_modal(output_elt) { + if (!output_elt) {return;} + output_elt = $(output_elt); + var data_src = output_elt.attr("data-src"); + if (!data_src) { + output_elt.html("

Error: missing data-src attribute

"); + } else { + output_elt.html("

Loading...

"); + $.get(data_src).done(function(data) { + output_elt.html(data); + retrieve_session_materials(output_elt.find(".agenda-frame")); + retrieve_session_materials(output_elt.find(".minutes-frame")); + }); + } + } + $(".modal").on("show.bs.modal", function () { - var i = $(this).find(".frame"); - if ($(i).data("src")) { - $.get($(i).data("src"), function (data, status, xhr) { - var t = xhr.getResponseHeader("content-type"); - if (t.indexOf("text/plain") > -1) { - data = "
" + data + "
"; - } else if (t.indexOf("text/markdown") > -1) { - data = "
" + data + "
"; - } else if(t.indexOf("text/html") > -1) { - // nothing to do here - } else { - data = "

Unknown type: " + xhr.getResponseHeader("content-type") + "

"; - } - $(i).html(data); - }); - } - var j = $(this).find(".frame2"); - if ($(j).data("src")) { - $.get($(j).data("src"), function (data, status, xhr) { - var t = xhr.getResponseHeader("content-type"); - if (t.indexOf("text/plain") > -1) { - data = "
" + data + "
"; - } else if (t.indexOf("text/markdown") > -1) { - data = "
" + data + "
"; - } else if(t.indexOf("text/html") > -1) { - // nothing to do here - } else { - data = "

Unknown type: " + xhr.getResponseHeader("content-type") + "

"; - } - $(j).html(data); - }); - } + retrieve_session_modal($(this).find(".assignment-materials")); }); + {% endblock %} diff --git a/ietf/templates/meeting/assignment_materials.html b/ietf/templates/meeting/assignment_materials.html new file mode 100644 index 000000000..b065ffb68 --- /dev/null +++ b/ietf/templates/meeting/assignment_materials.html @@ -0,0 +1,42 @@ +{# Copyright The IETF Trust 2015-2020, All Rights Reserved #} +{% load origin %}{% origin %} +{% load static %} +{% load textfilters %} +{% load ietf_filters %} +{% with item.session.agenda as agenda %} + {% if agenda %} + {% if agenda.file_extension == "txt" or agenda.file_extension == "md" or agenda.file_extension == "html" or agenda.file_extension == "htm" %} +

Agenda

+
+ {% else %} + Agenda submitted as {{ agenda.file_extension|upper }} + {% endif %} + {% else %} + No agenda submitted + {% endif %} +{% endwith %} + +{% if item.session.slides %} +

Slides

+ +{% endif %} + +{% with item.session.minutes as minutes %} + {% if minutes %} + {% if minutes.file_extension == "txt" or minutes.file_extension == "md" or minutes.file_extension == "html" or minutes.file_extension == "htm" %} +

Minutes

+
+ {% else %} + Minutes submitted as {{ minutes.file_extension|upper }} + {% endif %} + {% else %} + No minutes submitted + {% endif %} +{% endwith %} diff --git a/ietf/templates/meeting/session_agenda_include.html b/ietf/templates/meeting/session_agenda_include.html index 308092886..eeedc16ea 100644 --- a/ietf/templates/meeting/session_agenda_include.html +++ b/ietf/templates/meeting/session_agenda_include.html @@ -14,43 +14,9 @@