Merged in [18651] from jennifer@painless-security.com:

Retrieve session agenda, slides, and minutes each time agenda modal is opened. Fixes #3050.
 - Legacy-Id: 18658
Note: SVN reference [18651] has been migrated to Git commit bbf04c3fbe
This commit is contained in:
Henrik Levkowetz 2020-11-06 15:53:28 +00:00
commit 35f4f4c8a8
8 changed files with 291 additions and 77 deletions

View file

@ -1,5 +1,7 @@
# -*- conf-mode -*-
/personal/kivinen/7.19.1.dev0@18633
/personal/lars/7.12.1.dev0@18335
/personal/lars/7.12.1.dev0@18330 # Merge attempted but needed re-working; some cherry-picking done
/personal/lars/7.12.1.dev0@18329

View file

@ -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
@ -784,7 +785,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]
@ -798,6 +799,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):

View file

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

View file

@ -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<assignment_id>\d+)/materials.html$', views.assignment_materials),
url(r'^interim/announce/?$', views.interim_announce),
url(r'^interim/announce/(?P<number>[A-Za-z0-9._+-]+)/?$', views.interim_send_announcement),
url(r'^interim/skip_announce/(?P<number>[A-Za-z0-9._+-]+)/?$', views.interim_skip_announcement),

View file

@ -241,7 +241,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()
@ -1300,6 +1299,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=""):

View file

@ -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("<p>Error: missing data-src attribute</p>");
} else {
output_elt.html("<p>Loading " + data_src + "...</p>");
outer_xhr = $.get(data_src)
outer_xhr.done(function(data, status, xhr) {
var t = xhr.getResponseHeader("content-type");
if (!t) {
data = "<p>Error retrieving " + data_src
+ ": Missing content-type in response header</p>";
} else if (t.indexOf("text/plain") > -1) {
data = "<pre class='agenda'>" + data + "</pre>";
} else if (t.indexOf("text/markdown") > -1) {
data = "<pre class='agenda'>" + data + "</pre>";
} else if(t.indexOf("text/html") > -1) {
// nothing to do here
} else {
data = "<p>Unknown type: " + xhr.getResponseHeader("content-type") + "</p>";
}
output_elt.html(data);
}).fail(function() {
output_elt.html("<p>Error retrieving " + data_src
+ ": (" + outer_xhr.status.toString() + ") "
+ outer_xhr.statusText + "</p>");
})
}
}
/**
* 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("<p>Error: missing data-src attribute</p>");
} else {
output_elt.html("<p>Loading...</p>");
$.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 = "<pre class='agenda'>" + data + "</pre>";
} else if (t.indexOf("text/markdown") > -1) {
data = "<pre class='agenda'>" + data + "</pre>";
} else if(t.indexOf("text/html") > -1) {
// nothing to do here
} else {
data = "<p>Unknown type: " + xhr.getResponseHeader("content-type") + "</p>";
}
$(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 = "<pre class='agenda'>" + data + "</pre>";
} else if (t.indexOf("text/markdown") > -1) {
data = "<pre class='agenda'>" + data + "</pre>";
} else if(t.indexOf("text/html") > -1) {
// nothing to do here
} else {
data = "<p>Unknown type: " + xhr.getResponseHeader("content-type") + "</p>";
}
$(j).html(data);
});
}
retrieve_session_modal($(this).find(".assignment-materials"));
});
</script>
{% endblock %}

View file

@ -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" %}
<h4>Agenda</h4>
<div class="agenda-frame" data-src="{{ agenda.get_href }}"></div>
{% else %}
<span class="label label-info">Agenda submitted as {{ agenda.file_extension|upper }}</span>
{% endif %}
{% else %}
<span class="label label-warning">No agenda submitted</span>
{% endif %}
{% endwith %}
{% if item.session.slides %}
<h4>Slides</h4>
<ul class="fa-ul list-unstyled">
{% for slide in item.session.slides %}
<li>
<span class="fa-li fa fa-file-{{ slide.file_extension|lower }}-o"></span>
<a href="{{ slide.get_versionless_href }}">{{ slide.title|clean_whitespace }}</a>
</li>
{% endfor %}
</ul>
{% 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" %}
<h4>Minutes</h4>
<div class="minutes-frame" data-src="{{ minutes.get_href }}"></div>
{% else %}
<span class="label label-info">Minutes submitted as {{ minutes.file_extension|upper }}</span>
{% endif %}
{% else %}
<span class="label label-warning">No minutes submitted</span>
{% endif %}
{% endwith %}

View file

@ -14,43 +14,9 @@
</h4>
</div>
<div class="modal-body">
{% 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" %}
<h4>Agenda</h4>
<div class="frame" data-src="{{agenda.get_href}}"></div>
{% else %}
<span class="label label-info">Agenda submitted as {{agenda.file_extension|upper}}</span>
{% endif %}
{% else %}
<span class="label label-warning">No agenda submitted</span>
{% endif %}
{% endwith %}
{% if item.session.slides %}
<h4>Slides</h4>
<ul class="fa-ul list-unstyled">
{% for slide in item.session.slides %}
<li>
<span class="fa-li fa fa-file-{{slide.file_extension|lower}}-o"></span>
<a href="{{ slide.get_versionless_href }}">{{ slide.title|clean_whitespace }}</a>
</li>
{% endfor %}
</ul>
{% 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" %}
<h4>Minutes</h4>
<div class="frame2" data-src="{{minutes.get_href}}"></div>
{% else %}
<span class="label label-info">Minutes submitted as {{minutes.file_extension|upper}}</span>
{% endif %}
{% else %}
<span class="label label-warning">No minutes submitted</span>
{% endif %}
{% endwith %}
<div class="assignment-materials"
data-src="{% url 'ietf.meeting.views.assignment_materials' assignment_id=item.pk %}">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>