Skip to content

Commit 35f4f4c

Browse files
committed
Merged in [18651] from jennifer@painless-security.com:
Retrieve session agenda, slides, and minutes each time agenda modal is opened. Fixes ietf-tools#3050. - Legacy-Id: 18658 Note: SVN reference [18651] has been migrated to Git commit bbf04c3
2 parents af70391 + bbf04c3 commit 35f4f4c

8 files changed

Lines changed: 280 additions & 66 deletions

File tree

hold-for-merge

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# -*- conf-mode -*-
22

3+
/personal/kivinen/7.19.1.dev0@18633
4+
35
/personal/lars/7.12.1.dev0@18335
46
/personal/lars/7.12.1.dev0@18330 # Merge attempted but needed re-working; some cherry-picking done
57
/personal/lars/7.12.1.dev0@18329

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
@@ -784,7 +785,7 @@ def assert_agenda_view_filter_matches_ics_filter(self, filter_string):
784785
ics_url = self.absreverse('ietf.meeting.views.agenda_ical')
785786

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

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

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

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

13041314
@ensure_csrf_cookie
13051315
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)