Skip to content

Commit e6138ca

Browse files
rjsparksjennifer-richardspselkirk
authored
feat: session apis (ietf-tools#7173)
* feat: Show bluesheets using Attended tables (ietf-tools#7094) * feat: Show bluesheets using Attended tables (ietf-tools#6898) * feat: Allow users to add themselves to session attendance (ietf-tools#6454) * chore: Correct copyright year * fix: Address review comments * fix: Don't try to generate empty bluesheets * refactor: Complete rewrite of bluesheet.html * refactor: Fill in a few gaps, close a few holes - Rename the live "bluesheet" to "attendance", add some explanatory text. - Add attendance links in materials view and pre-finalized proceedings view. - Don't allow users to add themselves after the corrections cutoff date. * fix: Report file-save errors to caller * fix: Address review comments * fix: typo * refactor: if instead of except; refactor gently * refactor: Rearrange logic a little, add comment * style: Black * refactor: auto_now_add->default to allow override * refactor: jsonschema to validate API payload * feat: Handle new API data format Not yet tested except that it falls back when the old format is used. * test: Split test into deprecated/new version Have not yet touched the new version * style: Black * test: Test new add_session_attendees API * fix: Fix bug uncovered by test * refactor: Refactor affiliation lookup a bit * fix: Order bluesheet by Attended.time * refactor: Move helpers from views.py to utils.py * test: Test that finalize calls generate_bluesheets * test: test_bluesheet_data() * fix: Clean up merge * fix: Remove debug statement * chore: comments * refactor: Renumber migrations --------- Co-authored-by: Paul Selkirk <paul@painless-security.com> * chore: Remove unused import * style: Black * feat: Stub session update notify API * feat: Add order & rev to slides JSON * style: Black * feat: Stub actual Meetecho slide deck mgmt API * refactor: Limit reordering to type="slides" * chore: Remove repository from meetecho API (API changed on their end) * feat: update Meetecho on slide reorder * refactor: drop pytz from meetecho.py * chore: Remove more repository refs * refactor: Eliminate more pytz * test: Test add_slide_deck api * fix: Allow 202 status code / absent Content-Type * test: Test delete_slide_deck api * test: Test update_slide_decks api * refactor: sessionpresentation_set -> presentations * test: Test send_update() * fix: Debug send_update() * test: ajax_reorder_slides calls Meetecho API * test: Test SldesManager.add() * feat: Implement SlidesManager.add() * test: Test that ajax_add_slides... calls API * feat: Call Meetecho API when slides added to session * test: Test SlidesManager.delete() * feat: Implement SlidesManager.delete() * test: ajax_remove_slides... calls Meetecho API * feat: Call Meetecho API when slides removed * chore: Update docstring * feat: rudimentary debug mode for Meetecho API * test: remove_sessionpresentation() calls Meetecho API * feat: Call Meetecho API from remove_sessionpresentation() * test: upload_slides() calls Meetecho API * style: Black * fix: Refactor/debug upload_session_slides Avoids double-save of a SessionPresentation for the session being updated and updates other sessions when apply_to_all is set (previously it only created ones that did not exist, so rev would never be updated). * test: Fix test bug * feat: Call Meetecho API when uploading session slides * fix: Only replace slides actually linked to session * fix: Delint Removed some type checking rather than debugging it * fix: Send get_versionless_href() as url for slides * test: TZ-aware timestamps, please * chore: Add comments * feat: Call Meetecho API in edit_sessionpresentation * feat: Call Meetecho API in remove_sessionpresentation * feat: Call Meetecho API from add_sessionpresentation * fix: Set order in add_sessionpresentation * fix: Restrict API calls to "slides" docs * feat: Call Meetecho API on title changes * test: Check meetecho API calls in test_revise() * fix: better Meetecho API "order" management * fix: no PUT if there are no slides after DELETE * feat: Catch exceptions from SlidesManager Don't let errors in the MeetEcho slides API interfere with the ability to modify slides for a session. * feat: Limit which sessions we send notifications for * fix: handle absence of request_timeout in api config * test: always send slide notifications in tests * fix: save slides before sending notification (ietf-tools#7172) * fix: save slides before sending notification * style: fix indentation It's not a bug, it's a flourish! --------- Co-authored-by: Jennifer Richards <jennifer@staff.ietf.org> Co-authored-by: Paul Selkirk <paul@painless-security.com>
1 parent 8166601 commit e6138ca

20 files changed

Lines changed: 2106 additions & 385 deletions

ietf/api/tests.py

Lines changed: 117 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,9 @@ def test_api_set_session_video_url(self):
219219
event = doc.latest_event()
220220
self.assertEqual(event.by, recman)
221221

222-
def test_api_add_session_attendees(self):
222+
def test_api_add_session_attendees_deprecated(self):
223+
# Deprecated test - should be removed when we stop accepting a simple list of user PKs in
224+
# the add_session_attendees() view
223225
url = urlreverse('ietf.meeting.views.api_add_session_attendees')
224226
otherperson = PersonFactory()
225227
recmanrole = RoleFactory(group__type_id='ietf', name_id='recman')
@@ -285,6 +287,120 @@ def test_api_add_session_attendees(self):
285287
self.assertTrue(session.attended_set.filter(person=recman).exists())
286288
self.assertTrue(session.attended_set.filter(person=otherperson).exists())
287289

290+
def test_api_add_session_attendees(self):
291+
url = urlreverse("ietf.meeting.views.api_add_session_attendees")
292+
otherperson = PersonFactory()
293+
recmanrole = RoleFactory(group__type_id="ietf", name_id="recman")
294+
recman = recmanrole.person
295+
meeting = MeetingFactory(type_id="ietf")
296+
session = SessionFactory(group__type_id="wg", meeting=meeting)
297+
apikey = PersonalApiKey.objects.create(endpoint=url, person=recman)
298+
299+
badrole = RoleFactory(group__type_id="ietf", name_id="ad")
300+
badapikey = PersonalApiKey.objects.create(endpoint=url, person=badrole.person)
301+
badrole.person.user.last_login = timezone.now()
302+
badrole.person.user.save()
303+
304+
# Improper credentials, or method
305+
r = self.client.post(url, {})
306+
self.assertContains(r, "Missing apikey parameter", status_code=400)
307+
308+
r = self.client.post(url, {"apikey": badapikey.hash()})
309+
self.assertContains(r, "Restricted to role: Recording Manager", status_code=403)
310+
311+
r = self.client.post(url, {"apikey": apikey.hash()})
312+
self.assertContains(r, "Too long since last regular login", status_code=400)
313+
314+
recman.user.last_login = timezone.now() - datetime.timedelta(days=365)
315+
recman.user.save()
316+
r = self.client.post(url, {"apikey": apikey.hash()})
317+
self.assertContains(r, "Too long since last regular login", status_code=400)
318+
319+
recman.user.last_login = timezone.now()
320+
recman.user.save()
321+
r = self.client.get(url, {"apikey": apikey.hash()})
322+
self.assertContains(r, "Method not allowed", status_code=405)
323+
324+
recman.user.last_login = timezone.now()
325+
recman.user.save()
326+
327+
# Malformed requests
328+
r = self.client.post(url, {"apikey": apikey.hash()})
329+
self.assertContains(r, "Missing attended parameter", status_code=400)
330+
331+
for baddict in (
332+
"{}",
333+
'{"bogons;drop table":"bogons;drop table"}',
334+
'{"session_id":"Not an integer;drop table"}',
335+
f'{{"session_id":{session.pk},"attendees":"not a list;drop table"}}',
336+
f'{{"session_id":{session.pk},"attendees":"not a list;drop table"}}',
337+
f'{{"session_id":{session.pk},"attendees":[1,2,"not an int;drop table",4]}}',
338+
f'{{"session_id":{session.pk},"attendees":["user_id":{recman.user.pk}]}}', # no join_time
339+
f'{{"session_id":{session.pk},"attendees":["user_id":{recman.user.pk},"join_time;drop table":"2024-01-01T00:00:00Z]}}',
340+
f'{{"session_id":{session.pk},"attendees":["user_id":{recman.user.pk},"join_time":"not a time;drop table"]}}',
341+
# next has no time zone indicator
342+
f'{{"session_id":{session.pk},"attendees":["user_id":{recman.user.pk},"join_time":"2024-01-01T00:00:00"]}}',
343+
f'{{"session_id":{session.pk},"attendees":["user_id":"not an int; drop table","join_time":"2024-01-01T00:00:00Z"]}}',
344+
# Uncomment the next one when the _deprecated version of this test is retired
345+
# f'{{"session_id":{session.pk},"attendees":[{recman.user.pk}, {otherperson.user.pk}]}}',
346+
):
347+
r = self.client.post(url, {"apikey": apikey.hash(), "attended": baddict})
348+
self.assertContains(r, "Malformed post", status_code=400)
349+
350+
bad_session_id = Session.objects.order_by("-pk").first().pk + 1
351+
r = self.client.post(
352+
url,
353+
{
354+
"apikey": apikey.hash(),
355+
"attended": f'{{"session_id":{bad_session_id},"attendees":[]}}',
356+
},
357+
)
358+
self.assertContains(r, "Invalid session", status_code=400)
359+
bad_user_id = User.objects.order_by("-pk").first().pk + 1
360+
r = self.client.post(
361+
url,
362+
{
363+
"apikey": apikey.hash(),
364+
"attended": f'{{"session_id":{session.pk},"attendees":[{{"user_id":{bad_user_id}, "join_time":"2024-01-01T00:00:00Z"}}]}}',
365+
},
366+
)
367+
self.assertContains(r, "Invalid attendee", status_code=400)
368+
369+
# Reasonable request
370+
r = self.client.post(
371+
url,
372+
{
373+
"apikey": apikey.hash(),
374+
"attended": json.dumps(
375+
{
376+
"session_id": session.pk,
377+
"attendees": [
378+
{
379+
"user_id": recman.user.pk,
380+
"join_time": "2023-09-03T12:34:56Z",
381+
},
382+
{
383+
"user_id": otherperson.user.pk,
384+
"join_time": "2023-09-03T03:00:19Z",
385+
},
386+
],
387+
}
388+
),
389+
},
390+
)
391+
392+
self.assertEqual(session.attended_set.count(), 2)
393+
self.assertTrue(session.attended_set.filter(person=recman).exists())
394+
self.assertEqual(
395+
session.attended_set.get(person=recman).time,
396+
datetime.datetime(2023, 9, 3, 12, 34, 56, tzinfo=datetime.timezone.utc),
397+
)
398+
self.assertTrue(session.attended_set.filter(person=otherperson).exists())
399+
self.assertEqual(
400+
session.attended_set.get(person=otherperson).time,
401+
datetime.datetime(2023, 9, 3, 3, 0, 19, tzinfo=datetime.timezone.utc),
402+
)
403+
288404
def test_api_upload_polls_and_chatlog(self):
289405
recmanrole = RoleFactory(group__type_id='ietf', name_id='recman')
290406
recmanrole.person.user.last_login = timezone.now()

ietf/doc/tests.py

Lines changed: 83 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2594,37 +2594,68 @@ def test_view_document_meetings(self):
25942594
self.assertFalse(q("#futuremeets a.btn:contains('Remove document')"))
25952595
self.assertFalse(q("#pastmeets a.btn:contains('Remove document')"))
25962596

2597-
def test_edit_document_session(self):
2597+
@override_settings(MEETECHO_API_CONFIG="fake settings")
2598+
@mock.patch("ietf.doc.views_doc.SlidesManager")
2599+
def test_edit_document_session(self, mock_slides_manager_cls):
25982600
doc = IndividualDraftFactory.create()
25992601
sp = doc.presentations.create(session=self.future,rev=None)
26002602

26012603
url = urlreverse('ietf.doc.views_doc.edit_sessionpresentation',kwargs=dict(name='no-such-doc',session_id=sp.session_id))
26022604
response = self.client.get(url)
26032605
self.assertEqual(response.status_code, 404)
2606+
self.assertFalse(mock_slides_manager_cls.called)
26042607

26052608
url = urlreverse('ietf.doc.views_doc.edit_sessionpresentation',kwargs=dict(name=doc.name,session_id=0))
26062609
response = self.client.get(url)
26072610
self.assertEqual(response.status_code, 404)
2611+
self.assertFalse(mock_slides_manager_cls.called)
26082612

26092613
url = urlreverse('ietf.doc.views_doc.edit_sessionpresentation',kwargs=dict(name=doc.name,session_id=sp.session_id))
26102614
response = self.client.get(url)
26112615
self.assertEqual(response.status_code, 404)
2616+
self.assertFalse(mock_slides_manager_cls.called)
26122617

26132618
self.client.login(username=self.other_chair.user.username,password='%s+password'%self.other_chair.user.username)
26142619
response = self.client.get(url)
26152620
self.assertEqual(response.status_code, 404)
2616-
2621+
self.assertFalse(mock_slides_manager_cls.called)
2622+
26172623
self.client.login(username=self.group_chair.user.username,password='%s+password'%self.group_chair.user.username)
26182624
response = self.client.get(url)
26192625
self.assertEqual(response.status_code, 200)
26202626
q = PyQuery(response.content)
26212627
self.assertEqual(2,len(q('select#id_version option')))
2628+
self.assertFalse(mock_slides_manager_cls.called)
26222629

2630+
# edit draft
26232631
self.assertEqual(1,doc.docevent_set.count())
26242632
response = self.client.post(url,{'version':'00','save':''})
26252633
self.assertEqual(response.status_code, 302)
26262634
self.assertEqual(doc.presentations.get(pk=sp.pk).rev,'00')
26272635
self.assertEqual(2,doc.docevent_set.count())
2636+
self.assertFalse(mock_slides_manager_cls.called)
2637+
2638+
# editing slides should call Meetecho API
2639+
slides = SessionPresentationFactory(
2640+
session=self.future,
2641+
document__type_id="slides",
2642+
document__rev="00",
2643+
rev=None,
2644+
order=1,
2645+
).document
2646+
url = urlreverse(
2647+
"ietf.doc.views_doc.edit_sessionpresentation",
2648+
kwargs={"name": slides.name, "session_id": self.future.pk},
2649+
)
2650+
response = self.client.post(url, {"version": "00", "save": ""})
2651+
self.assertEqual(response.status_code, 302)
2652+
self.assertEqual(mock_slides_manager_cls.call_count, 1)
2653+
self.assertEqual(mock_slides_manager_cls.call_args, mock.call(api_config="fake settings"))
2654+
self.assertEqual(mock_slides_manager_cls.return_value.send_update.call_count, 1)
2655+
self.assertEqual(
2656+
mock_slides_manager_cls.return_value.send_update.call_args,
2657+
mock.call(self.future),
2658+
)
26282659

26292660
def test_edit_document_session_after_proceedings_closed(self):
26302661
doc = IndividualDraftFactory.create()
@@ -2641,35 +2672,60 @@ def test_edit_document_session_after_proceedings_closed(self):
26412672
q=PyQuery(response.content)
26422673
self.assertEqual(1,len(q(".alert-warning:contains('may affect published proceedings')")))
26432674

2644-
def test_remove_document_session(self):
2675+
@override_settings(MEETECHO_API_CONFIG="fake settings")
2676+
@mock.patch("ietf.doc.views_doc.SlidesManager")
2677+
def test_remove_document_session(self, mock_slides_manager_cls):
26452678
doc = IndividualDraftFactory.create()
26462679
sp = doc.presentations.create(session=self.future,rev=None)
26472680

26482681
url = urlreverse('ietf.doc.views_doc.remove_sessionpresentation',kwargs=dict(name='no-such-doc',session_id=sp.session_id))
26492682
response = self.client.get(url)
26502683
self.assertEqual(response.status_code, 404)
2684+
self.assertFalse(mock_slides_manager_cls.called)
26512685

26522686
url = urlreverse('ietf.doc.views_doc.remove_sessionpresentation',kwargs=dict(name=doc.name,session_id=0))
26532687
response = self.client.get(url)
26542688
self.assertEqual(response.status_code, 404)
2689+
self.assertFalse(mock_slides_manager_cls.called)
26552690

26562691
url = urlreverse('ietf.doc.views_doc.remove_sessionpresentation',kwargs=dict(name=doc.name,session_id=sp.session_id))
26572692
response = self.client.get(url)
26582693
self.assertEqual(response.status_code, 404)
2694+
self.assertFalse(mock_slides_manager_cls.called)
26592695

26602696
self.client.login(username=self.other_chair.user.username,password='%s+password'%self.other_chair.user.username)
26612697
response = self.client.get(url)
26622698
self.assertEqual(response.status_code, 404)
2663-
2699+
self.assertFalse(mock_slides_manager_cls.called)
2700+
26642701
self.client.login(username=self.group_chair.user.username,password='%s+password'%self.group_chair.user.username)
26652702
response = self.client.get(url)
26662703
self.assertEqual(response.status_code, 200)
2704+
self.assertFalse(mock_slides_manager_cls.called)
26672705

2706+
# removing a draft
26682707
self.assertEqual(1,doc.docevent_set.count())
26692708
response = self.client.post(url,{'remove_session':''})
26702709
self.assertEqual(response.status_code, 302)
26712710
self.assertFalse(doc.presentations.filter(pk=sp.pk).exists())
26722711
self.assertEqual(2,doc.docevent_set.count())
2712+
self.assertFalse(mock_slides_manager_cls.called)
2713+
2714+
# removing slides should call Meetecho API
2715+
slides = SessionPresentationFactory(session=self.future, document__type_id="slides", order=1).document
2716+
url = urlreverse(
2717+
"ietf.doc.views_doc.remove_sessionpresentation",
2718+
kwargs={"name": slides.name, "session_id": self.future.pk},
2719+
)
2720+
response = self.client.post(url, {"remove_session": ""})
2721+
self.assertEqual(response.status_code, 302)
2722+
self.assertEqual(mock_slides_manager_cls.call_count, 1)
2723+
self.assertEqual(mock_slides_manager_cls.call_args, mock.call(api_config="fake settings"))
2724+
self.assertEqual(mock_slides_manager_cls.return_value.delete.call_count, 1)
2725+
self.assertEqual(
2726+
mock_slides_manager_cls.return_value.delete.call_args,
2727+
mock.call(self.future, slides),
2728+
)
26732729

26742730
def test_remove_document_session_after_proceedings_closed(self):
26752731
doc = IndividualDraftFactory.create()
@@ -2686,28 +2742,49 @@ def test_remove_document_session_after_proceedings_closed(self):
26862742
q=PyQuery(response.content)
26872743
self.assertEqual(1,len(q(".alert-warning:contains('may affect published proceedings')")))
26882744

2689-
def test_add_document_session(self):
2745+
@override_settings(MEETECHO_API_CONFIG="fake settings")
2746+
@mock.patch("ietf.doc.views_doc.SlidesManager")
2747+
def test_add_document_session(self, mock_slides_manager_cls):
26902748
doc = IndividualDraftFactory.create()
26912749

26922750
url = urlreverse('ietf.doc.views_doc.add_sessionpresentation',kwargs=dict(name=doc.name))
26932751
login_testing_unauthorized(self,self.group_chair.user.username,url)
26942752
response = self.client.get(url)
26952753
self.assertEqual(response.status_code,200)
2696-
2754+
self.assertFalse(mock_slides_manager_cls.called)
2755+
26972756
response = self.client.post(url,{'session':0,'version':'current'})
26982757
self.assertEqual(response.status_code,200)
26992758
q=PyQuery(response.content)
27002759
self.assertTrue(q('.form-select.is-invalid'))
2760+
self.assertFalse(mock_slides_manager_cls.called)
27012761

27022762
response = self.client.post(url,{'session':self.future.pk,'version':'bogus version'})
27032763
self.assertEqual(response.status_code,200)
27042764
q=PyQuery(response.content)
27052765
self.assertTrue(q('.form-select.is-invalid'))
2766+
self.assertFalse(mock_slides_manager_cls.called)
27062767

2768+
# adding a draft
27072769
self.assertEqual(1,doc.docevent_set.count())
27082770
response = self.client.post(url,{'session':self.future.pk,'version':'current'})
27092771
self.assertEqual(response.status_code,302)
27102772
self.assertEqual(2,doc.docevent_set.count())
2773+
self.assertEqual(doc.presentations.get(session__pk=self.future.pk).order, 0)
2774+
self.assertFalse(mock_slides_manager_cls.called)
2775+
2776+
# adding slides should set order / call Meetecho API
2777+
slides = DocumentFactory(type_id="slides")
2778+
url = urlreverse("ietf.doc.views_doc.add_sessionpresentation", kwargs=dict(name=slides.name))
2779+
response = self.client.post(url, {"session": self.future.pk, "version": "current"})
2780+
self.assertEqual(response.status_code,302)
2781+
self.assertEqual(slides.presentations.get(session__pk=self.future.pk).order, 1)
2782+
self.assertEqual(mock_slides_manager_cls.call_args, mock.call(api_config="fake settings"))
2783+
self.assertEqual(mock_slides_manager_cls.return_value.add.call_count, 1)
2784+
self.assertEqual(
2785+
mock_slides_manager_cls.return_value.add.call_args,
2786+
mock.call(self.future, slides, order=1),
2787+
)
27112788

27122789
def test_get_related_meeting(self):
27132790
"""Should be able to retrieve related meeting"""

0 commit comments

Comments
 (0)