Skip to content

Commit bbf04c3

Browse files
Retrieve session agenda, slides, and minutes each time agenda modal is opened. Fixes ietf-tools#3050. Commit ready for merge.
- Legacy-Id: 18651
1 parent d67b298 commit bbf04c3

7 files changed

Lines changed: 278 additions & 66 deletions

File tree

ietf/meeting/tests_js.py

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import debug # pyflakes:ignore
1919

2020
from ietf.doc.factories import DocumentFactory
21+
from ietf.doc.models import State
2122
from ietf.group import colors
2223
from ietf.person.models import Person
2324
from ietf.group.models import Group
@@ -783,7 +784,7 @@ def assert_agenda_view_filter_matches_ics_filter(self, filter_string):
783784
ics_url = self.absreverse('ietf.meeting.views.agenda_ical')
784785

785786
# parse out the events
786-
agenda_rows = self.driver.find_elements_by_css_selector('[id^="row-"')
787+
agenda_rows = self.driver.find_elements_by_css_selector('[id^="row-"]')
787788
visible_rows = [r for r in agenda_rows if r.is_displayed()]
788789
sessions = [self.session_from_agenda_row_id(row.get_attribute("id"))
789790
for row in visible_rows]
@@ -797,6 +798,111 @@ def assert_agenda_view_filter_matches_ics_filter(self, filter_string):
797798
expected_event_uids=expected_uids,
798799
expected_event_count=len(sessions))
799800

801+
def test_session_materials_modal(self):
802+
"""Test opening and re-opening a session materals modal
803+
804+
This currently only tests the slides to ensure that changes to these are picked up
805+
without reloading the main agenda page. This should also test that the agenda and
806+
minutes are displayed and updated correctly, but problems with WebDriver/Selenium/Chromedriver
807+
are blocking this.
808+
"""
809+
session = self.meeting.session_set.filter(group__acronym="mars").first()
810+
assignment = session.official_timeslotassignment()
811+
slug = assignment.slug()
812+
813+
url = self.absreverse('ietf.meeting.views.agenda')
814+
self.driver.get(url)
815+
816+
# modal should start hidden
817+
modal_div = self.driver.find_element_by_css_selector('div#modal-%s' % slug)
818+
self.assertFalse(modal_div.is_displayed())
819+
820+
# Click the 'materials' button
821+
open_modal_button = WebDriverWait(self.driver, 2).until(
822+
expected_conditions.element_to_be_clickable(
823+
(By.CSS_SELECTOR, '[data-target="#modal-%s"]' % slug)
824+
),
825+
'Modal open button not found or not clickable',
826+
)
827+
open_modal_button.click()
828+
WebDriverWait(self.driver, 2).until(
829+
expected_conditions.visibility_of(modal_div),
830+
'Modal did not become visible after clicking open button',
831+
)
832+
833+
# Check that we have the expected slides
834+
not_deleted_slides = session.materials.filter(
835+
type='slides'
836+
).exclude(
837+
states__type__slug='slides',states__slug='deleted'
838+
)
839+
self.assertGreater(not_deleted_slides.count(), 0) # make sure this isn't a pointless test
840+
for slide in not_deleted_slides:
841+
anchor = self.driver.find_element_by_xpath('//a[text()="%s"]' % slide.title)
842+
self.assertIsNotNone(anchor)
843+
844+
deleted_slides = session.materials.filter(
845+
type='slides', states__type__slug='slides', states__slug='deleted'
846+
)
847+
self.assertGreater(deleted_slides.count(), 0) # make sure this isn't a pointless test
848+
for slide in deleted_slides:
849+
with self.assertRaises(NoSuchElementException):
850+
self.driver.find_element_by_xpath('//a[text()="%s"]' % slide.title)
851+
852+
# Now close the modal
853+
close_modal_button = WebDriverWait(self.driver, 2).until(
854+
expected_conditions.element_to_be_clickable(
855+
(By.CSS_SELECTOR, '.modal-footer button[data-dismiss="modal"]')
856+
),
857+
'Modal close button not found or not clickable',
858+
)
859+
close_modal_button.click()
860+
WebDriverWait(self.driver, 2).until(
861+
expected_conditions.invisibility_of_element(modal_div),
862+
'Modal was not hidden after clicking close button',
863+
)
864+
865+
# Modify the session info
866+
newly_deleted_slide = not_deleted_slides.first()
867+
newly_undeleted_slide = deleted_slides.first()
868+
newly_deleted_slide.set_state(State.objects.get(type="slides", slug="deleted"))
869+
newly_undeleted_slide.set_state(State.objects.get(type="slides", slug="active"))
870+
871+
# Click the 'materials' button
872+
open_modal_button = WebDriverWait(self.driver, 2).until(
873+
expected_conditions.element_to_be_clickable(
874+
(By.CSS_SELECTOR, '[data-target="#modal-%s"]' % slug)
875+
),
876+
'Modal open button not found or not clickable for refresh test',
877+
)
878+
open_modal_button.click()
879+
WebDriverWait(self.driver, 2).until(
880+
expected_conditions.visibility_of(modal_div),
881+
'Modal did not become visible after clicking open button for refresh test',
882+
)
883+
884+
# Check that we now see the updated slides
885+
not_deleted_slides = session.materials.filter(
886+
type='slides'
887+
).exclude(
888+
states__type__slug='slides',states__slug='deleted'
889+
)
890+
self.assertNotIn(newly_deleted_slide, not_deleted_slides)
891+
self.assertIn(newly_undeleted_slide, not_deleted_slides)
892+
for slide in not_deleted_slides:
893+
anchor = self.driver.find_element_by_xpath('//a[text()="%s"]' % slide.title)
894+
self.assertIsNotNone(anchor)
895+
896+
deleted_slides = session.materials.filter(
897+
type='slides', states__type__slug='slides', states__slug='deleted'
898+
)
899+
self.assertIn(newly_deleted_slide, deleted_slides)
900+
self.assertNotIn(newly_undeleted_slide, deleted_slides)
901+
for slide in deleted_slides:
902+
with self.assertRaises(NoSuchElementException):
903+
self.driver.find_element_by_xpath('//a[text()="%s"]' % slide.title)
904+
905+
800906
@skipIf(skip_selenium, skip_message)
801907
class InterimTests(MeetingTestCase):
802908
def setUp(self):

ietf/meeting/tests_views.py

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -158,12 +158,16 @@ def test_meeting_agenda(self):
158158
self.assertIn(time_interval, agenda_content)
159159
self.assertIn(registration_text, agenda_content)
160160

161-
# Make sure there's a frame for the agenda and it points to the right place
162-
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')]))
163-
164-
# Make sure undeleted slides are present and deleted slides are not
165-
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')]))
166-
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')]))
161+
# Make sure there's a frame for the session agenda and it points to the right place
162+
assignment = session.official_timeslotassignment()
163+
assignment_url = urlreverse('ietf.meeting.views.assignment_materials',
164+
kwargs=dict(assignment_id=assignment.pk))
165+
self.assertTrue(
166+
any(
167+
[assignment_url in x.attrib["data-src"]
168+
for x in q('tr div.modal-body div.assignment-materials')]
169+
)
170+
)
167171

168172
# future meeting, no agenda
169173
r = self.client.get(urlreverse("ietf.meeting.views.agenda", kwargs=dict(num=future_meeting.number)))
@@ -844,6 +848,50 @@ def test_cancelled_ics(self):
844848
self.assertIn('STATUS:CANCELLED',unicontent(r))
845849
self.assertNotIn('STATUS:CONFIRMED',unicontent(r))
846850

851+
def test_assignment_materials(self):
852+
meeting = make_meeting_test_data()
853+
session = Session.objects.filter(meeting=meeting, group__acronym="mars").first()
854+
855+
for assignment in session.timeslotassignments.all():
856+
url = urlreverse('ietf.meeting.views.assignment_materials',
857+
kwargs=dict(assignment_id=assignment.pk))
858+
r = self.client.get(url)
859+
self.assertEqual(r.status_code, 200)
860+
q = PyQuery(r.content)
861+
862+
agenda_div = q('div.agenda-frame')
863+
self.assertIsNotNone(agenda_div)
864+
self.assertEqual(agenda_div.attr('data-src'), session.agenda().get_href())
865+
866+
minutes_div = q('div.minutes-frame')
867+
self.assertIsNotNone(minutes_div)
868+
self.assertEqual(minutes_div.attr('data-src'), session.minutes().get_href())
869+
870+
# Make sure undeleted slides are present and deleted slides are not
871+
not_deleted_slides = session.materials.filter(
872+
type='slides'
873+
).exclude(
874+
states__type__slug='slides',states__slug='deleted'
875+
)
876+
self.assertGreater(not_deleted_slides.count(), 0) # make sure this isn't a pointless test
877+
878+
deleted_slides = session.materials.filter(
879+
type='slides', states__type__slug='slides', states__slug='deleted'
880+
)
881+
self.assertGreater(deleted_slides.count(), 0) # make sure this isn't a pointless test
882+
883+
# live slides should be found
884+
for slide in not_deleted_slides:
885+
self.assertTrue(q('ul li a:contains("%s")' % slide.title))
886+
887+
# deleted slides should not be found
888+
for slide in deleted_slides:
889+
self.assertFalse(q('ul li a:contains("%s")' % slide.title))
890+
891+
892+
for slide in session.slides():
893+
self.assertContains(r, slide.title)
894+
847895
class ReorderSlidesTests(TestCase):
848896

849897
def test_add_slides_to_session(self):

ietf/meeting/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@
111111
# First patterns which start with unique strings
112112
url(r'^$', views.current_materials),
113113
url(r'^ajax/get-utc/?$', views.ajax_get_utc),
114+
url(r'^assignment/(?P<assignment_id>\d+)/materials.html$', views.assignment_materials),
114115
url(r'^interim/announce/?$', views.interim_announce),
115116
url(r'^interim/announce/(?P<number>[A-Za-z0-9._+-]+)/?$', views.interim_send_announcement),
116117
url(r'^interim/skip_announce/(?P<number>[A-Za-z0-9._+-]+)/?$', views.interim_skip_announcement),

ietf/meeting/views.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,6 @@ def materials_document(request, document, num=None, ext=None):
242242
raise Http404("File not found: %s" % filename)
243243

244244
old_proceedings_format = meeting.number.isdigit() and int(meeting.number) <= 96
245-
246245
if settings.MEETING_MATERIALS_SERVE_LOCALLY or old_proceedings_format:
247246
with io.open(filename, 'rb') as file:
248247
bytes = file.read()
@@ -1301,6 +1300,17 @@ def diff_schedules(request, num):
13011300
'to_schedule': to_schedule,
13021301
})
13031302

1303+
@ensure_csrf_cookie
1304+
def assignment_materials(request, assignment_id):
1305+
"""Assignment details for agenda page pop-up"""
1306+
assignments = SchedTimeSessAssignment.objects.filter(pk=int(assignment_id))
1307+
if len(assignments) == 0:
1308+
raise Http404('No such assignment')
1309+
assert len(assignments) == 1
1310+
meeting = assignments[0].timeslot.meeting # timeslot is guaranteed to be non-null
1311+
assignments = preprocess_assignments_for_agenda(assignments, meeting)
1312+
assignment = assignments[0]
1313+
return render(request, 'meeting/assignment_materials.html', dict(item=assignment))
13041314

13051315
@ensure_csrf_cookie
13061316
def agenda(request, num=None, name=None, base=None, ext=None, owner=None, utc=""):

ietf/templates/meeting/agenda.html

Lines changed: 60 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525
background-color: inherit !important;
2626
border: none !important;
2727
}
28+
.assignment-materials .agenda-frame,.minutes-frame {
29+
white-space: normal;
30+
}
2831
{% endblock %}
2932

3033
{% block bodyAttrs %}data-spy="scroll" data-target="#affix"{% endblock %}
@@ -389,12 +392,30 @@ <h2>
389392
agenda_filter.set_update_callback(update_view);
390393
agenda_filter.enable();
391394

392-
$(".modal").on("show.bs.modal", function () {
393-
var i = $(this).find(".frame");
394-
if ($(i).data("src")) {
395-
$.get($(i).data("src"), function (data, status, xhr) {
395+
/**
396+
* Retrieve and display materials for a session
397+
*
398+
* If output_elt exists and has a "data-src" attribute, retrieves the document
399+
* from that URL and displays under output_elt. Handles text/plain, text/markdown,
400+
* and text/html.
401+
*
402+
* @param output_elt Element, probably a div, to hold the output
403+
*/
404+
function retrieve_session_materials(output_elt) {
405+
if (!output_elt) {return;}
406+
output_elt = $(output_elt);
407+
var data_src = output_elt.attr("data-src");
408+
if (!data_src) {
409+
output_elt.html("<p>Error: missing data-src attribute</p>");
410+
} else {
411+
output_elt.html("<p>Loading " + data_src + "...</p>");
412+
outer_xhr = $.get(data_src)
413+
outer_xhr.done(function(data, status, xhr) {
396414
var t = xhr.getResponseHeader("content-type");
397-
if (t.indexOf("text/plain") > -1) {
415+
if (!t) {
416+
data = "<p>Error retrieving " + data_src
417+
+ ": Missing content-type in response header</p>";
418+
} else if (t.indexOf("text/plain") > -1) {
398419
data = "<pre class='agenda'>" + data + "</pre>";
399420
} else if (t.indexOf("text/markdown") > -1) {
400421
data = "<pre class='agenda'>" + data + "</pre>";
@@ -403,25 +424,43 @@ <h2>
403424
} else {
404425
data = "<p>Unknown type: " + xhr.getResponseHeader("content-type") + "</p>";
405426
}
406-
$(i).html(data);
407-
});
427+
output_elt.html(data);
428+
}).fail(function() {
429+
output_elt.html("<p>Error retrieving " + data_src
430+
+ ": (" + outer_xhr.status.toString() + ") "
431+
+ outer_xhr.statusText + "</p>");
432+
})
408433
}
409-
var j = $(this).find(".frame2");
410-
if ($(j).data("src")) {
411-
$.get($(j).data("src"), function (data, status, xhr) {
412-
var t = xhr.getResponseHeader("content-type");
413-
if (t.indexOf("text/plain") > -1) {
414-
data = "<pre class='agenda'>" + data + "</pre>";
415-
} else if (t.indexOf("text/markdown") > -1) {
416-
data = "<pre class='agenda'>" + data + "</pre>";
417-
} else if(t.indexOf("text/html") > -1) {
418-
// nothing to do here
419-
} else {
420-
data = "<p>Unknown type: " + xhr.getResponseHeader("content-type") + "</p>";
421-
}
422-
$(j).html(data);
434+
}
435+
436+
/**
437+
* Retrieve contents of a session materials modal
438+
*
439+
* Expects output_elt to exist and have a "data-src" attribute. Retrieves the
440+
* contents of that URL, then attempts to populate the .agenda-frame and
441+
* .minutes-frame elements.
442+
*
443+
* @param output_elt Element, probably a div, to hold the output
444+
*/
445+
function retrieve_session_modal(output_elt) {
446+
if (!output_elt) {return;}
447+
output_elt = $(output_elt);
448+
var data_src = output_elt.attr("data-src");
449+
if (!data_src) {
450+
output_elt.html("<p>Error: missing data-src attribute</p>");
451+
} else {
452+
output_elt.html("<p>Loading...</p>");
453+
$.get(data_src).done(function(data) {
454+
output_elt.html(data);
455+
retrieve_session_materials(output_elt.find(".agenda-frame"));
456+
retrieve_session_materials(output_elt.find(".minutes-frame"));
423457
});
424458
}
459+
}
460+
461+
$(".modal").on("show.bs.modal", function () {
462+
retrieve_session_modal($(this).find(".assignment-materials"));
425463
});
464+
426465
</script>
427466
{% endblock %}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
{# Copyright The IETF Trust 2015-2020, All Rights Reserved #}
2+
{% load origin %}{% origin %}
3+
{% load static %}
4+
{% load textfilters %}
5+
{% load ietf_filters %}
6+
{% with item.session.agenda as agenda %}
7+
{% if agenda %}
8+
{% if agenda.file_extension == "txt" or agenda.file_extension == "md" or agenda.file_extension == "html" or agenda.file_extension == "htm" %}
9+
<h4>Agenda</h4>
10+
<div class="agenda-frame" data-src="{{ agenda.get_href }}"></div>
11+
{% else %}
12+
<span class="label label-info">Agenda submitted as {{ agenda.file_extension|upper }}</span>
13+
{% endif %}
14+
{% else %}
15+
<span class="label label-warning">No agenda submitted</span>
16+
{% endif %}
17+
{% endwith %}
18+
19+
{% if item.session.slides %}
20+
<h4>Slides</h4>
21+
<ul class="fa-ul list-unstyled">
22+
{% for slide in item.session.slides %}
23+
<li>
24+
<span class="fa-li fa fa-file-{{ slide.file_extension|lower }}-o"></span>
25+
<a href="{{ slide.get_versionless_href }}">{{ slide.title|clean_whitespace }}</a>
26+
</li>
27+
{% endfor %}
28+
</ul>
29+
{% endif %}
30+
31+
{% with item.session.minutes as minutes %}
32+
{% if minutes %}
33+
{% if minutes.file_extension == "txt" or minutes.file_extension == "md" or minutes.file_extension == "html" or minutes.file_extension == "htm" %}
34+
<h4>Minutes</h4>
35+
<div class="minutes-frame" data-src="{{ minutes.get_href }}"></div>
36+
{% else %}
37+
<span class="label label-info">Minutes submitted as {{ minutes.file_extension|upper }}</span>
38+
{% endif %}
39+
{% else %}
40+
<span class="label label-warning">No minutes submitted</span>
41+
{% endif %}
42+
{% endwith %}

0 commit comments

Comments
 (0)