Skip to content

Commit

Permalink
feat: session apis (#7173)
Browse files Browse the repository at this point in the history
* feat: Show bluesheets using Attended tables (#7094)

* feat: Show bluesheets using Attended tables (#6898)

* feat: Allow users to add themselves to session attendance (#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 <[email protected]>

* 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 (#7172)

* fix: save slides before sending notification

* style: fix indentation

It's not a bug, it's a flourish!

---------

Co-authored-by: Jennifer Richards <[email protected]>
Co-authored-by: Paul Selkirk <[email protected]>
  • Loading branch information
3 people authored Mar 12, 2024
1 parent 8166601 commit e6138ca
Show file tree
Hide file tree
Showing 20 changed files with 2,106 additions and 385 deletions.
118 changes: 117 additions & 1 deletion ietf/api/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,9 @@ def test_api_set_session_video_url(self):
event = doc.latest_event()
self.assertEqual(event.by, recman)

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

def test_api_add_session_attendees(self):
url = urlreverse("ietf.meeting.views.api_add_session_attendees")
otherperson = PersonFactory()
recmanrole = RoleFactory(group__type_id="ietf", name_id="recman")
recman = recmanrole.person
meeting = MeetingFactory(type_id="ietf")
session = SessionFactory(group__type_id="wg", meeting=meeting)
apikey = PersonalApiKey.objects.create(endpoint=url, person=recman)

badrole = RoleFactory(group__type_id="ietf", name_id="ad")
badapikey = PersonalApiKey.objects.create(endpoint=url, person=badrole.person)
badrole.person.user.last_login = timezone.now()
badrole.person.user.save()

# Improper credentials, or method
r = self.client.post(url, {})
self.assertContains(r, "Missing apikey parameter", status_code=400)

r = self.client.post(url, {"apikey": badapikey.hash()})
self.assertContains(r, "Restricted to role: Recording Manager", status_code=403)

r = self.client.post(url, {"apikey": apikey.hash()})
self.assertContains(r, "Too long since last regular login", status_code=400)

recman.user.last_login = timezone.now() - datetime.timedelta(days=365)
recman.user.save()
r = self.client.post(url, {"apikey": apikey.hash()})
self.assertContains(r, "Too long since last regular login", status_code=400)

recman.user.last_login = timezone.now()
recman.user.save()
r = self.client.get(url, {"apikey": apikey.hash()})
self.assertContains(r, "Method not allowed", status_code=405)

recman.user.last_login = timezone.now()
recman.user.save()

# Malformed requests
r = self.client.post(url, {"apikey": apikey.hash()})
self.assertContains(r, "Missing attended parameter", status_code=400)

for baddict in (
"{}",
'{"bogons;drop table":"bogons;drop table"}',
'{"session_id":"Not an integer;drop table"}',
f'{{"session_id":{session.pk},"attendees":"not a list;drop table"}}',
f'{{"session_id":{session.pk},"attendees":"not a list;drop table"}}',
f'{{"session_id":{session.pk},"attendees":[1,2,"not an int;drop table",4]}}',
f'{{"session_id":{session.pk},"attendees":["user_id":{recman.user.pk}]}}', # no join_time
f'{{"session_id":{session.pk},"attendees":["user_id":{recman.user.pk},"join_time;drop table":"2024-01-01T00:00:00Z]}}',
f'{{"session_id":{session.pk},"attendees":["user_id":{recman.user.pk},"join_time":"not a time;drop table"]}}',
# next has no time zone indicator
f'{{"session_id":{session.pk},"attendees":["user_id":{recman.user.pk},"join_time":"2024-01-01T00:00:00"]}}',
f'{{"session_id":{session.pk},"attendees":["user_id":"not an int; drop table","join_time":"2024-01-01T00:00:00Z"]}}',
# Uncomment the next one when the _deprecated version of this test is retired
# f'{{"session_id":{session.pk},"attendees":[{recman.user.pk}, {otherperson.user.pk}]}}',
):
r = self.client.post(url, {"apikey": apikey.hash(), "attended": baddict})
self.assertContains(r, "Malformed post", status_code=400)

bad_session_id = Session.objects.order_by("-pk").first().pk + 1
r = self.client.post(
url,
{
"apikey": apikey.hash(),
"attended": f'{{"session_id":{bad_session_id},"attendees":[]}}',
},
)
self.assertContains(r, "Invalid session", status_code=400)
bad_user_id = User.objects.order_by("-pk").first().pk + 1
r = self.client.post(
url,
{
"apikey": apikey.hash(),
"attended": f'{{"session_id":{session.pk},"attendees":[{{"user_id":{bad_user_id}, "join_time":"2024-01-01T00:00:00Z"}}]}}',
},
)
self.assertContains(r, "Invalid attendee", status_code=400)

# Reasonable request
r = self.client.post(
url,
{
"apikey": apikey.hash(),
"attended": json.dumps(
{
"session_id": session.pk,
"attendees": [
{
"user_id": recman.user.pk,
"join_time": "2023-09-03T12:34:56Z",
},
{
"user_id": otherperson.user.pk,
"join_time": "2023-09-03T03:00:19Z",
},
],
}
),
},
)

self.assertEqual(session.attended_set.count(), 2)
self.assertTrue(session.attended_set.filter(person=recman).exists())
self.assertEqual(
session.attended_set.get(person=recman).time,
datetime.datetime(2023, 9, 3, 12, 34, 56, tzinfo=datetime.timezone.utc),
)
self.assertTrue(session.attended_set.filter(person=otherperson).exists())
self.assertEqual(
session.attended_set.get(person=otherperson).time,
datetime.datetime(2023, 9, 3, 3, 0, 19, tzinfo=datetime.timezone.utc),
)

def test_api_upload_polls_and_chatlog(self):
recmanrole = RoleFactory(group__type_id='ietf', name_id='recman')
recmanrole.person.user.last_login = timezone.now()
Expand Down
89 changes: 83 additions & 6 deletions ietf/doc/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -2594,37 +2594,68 @@ def test_view_document_meetings(self):
self.assertFalse(q("#futuremeets a.btn:contains('Remove document')"))
self.assertFalse(q("#pastmeets a.btn:contains('Remove document')"))

def test_edit_document_session(self):
@override_settings(MEETECHO_API_CONFIG="fake settings")
@mock.patch("ietf.doc.views_doc.SlidesManager")
def test_edit_document_session(self, mock_slides_manager_cls):
doc = IndividualDraftFactory.create()
sp = doc.presentations.create(session=self.future,rev=None)

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

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

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

self.client.login(username=self.other_chair.user.username,password='%s+password'%self.other_chair.user.username)
response = self.client.get(url)
self.assertEqual(response.status_code, 404)

self.assertFalse(mock_slides_manager_cls.called)

self.client.login(username=self.group_chair.user.username,password='%s+password'%self.group_chair.user.username)
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
q = PyQuery(response.content)
self.assertEqual(2,len(q('select#id_version option')))
self.assertFalse(mock_slides_manager_cls.called)

# edit draft
self.assertEqual(1,doc.docevent_set.count())
response = self.client.post(url,{'version':'00','save':''})
self.assertEqual(response.status_code, 302)
self.assertEqual(doc.presentations.get(pk=sp.pk).rev,'00')
self.assertEqual(2,doc.docevent_set.count())
self.assertFalse(mock_slides_manager_cls.called)

# editing slides should call Meetecho API
slides = SessionPresentationFactory(
session=self.future,
document__type_id="slides",
document__rev="00",
rev=None,
order=1,
).document
url = urlreverse(
"ietf.doc.views_doc.edit_sessionpresentation",
kwargs={"name": slides.name, "session_id": self.future.pk},
)
response = self.client.post(url, {"version": "00", "save": ""})
self.assertEqual(response.status_code, 302)
self.assertEqual(mock_slides_manager_cls.call_count, 1)
self.assertEqual(mock_slides_manager_cls.call_args, mock.call(api_config="fake settings"))
self.assertEqual(mock_slides_manager_cls.return_value.send_update.call_count, 1)
self.assertEqual(
mock_slides_manager_cls.return_value.send_update.call_args,
mock.call(self.future),
)

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

def test_remove_document_session(self):
@override_settings(MEETECHO_API_CONFIG="fake settings")
@mock.patch("ietf.doc.views_doc.SlidesManager")
def test_remove_document_session(self, mock_slides_manager_cls):
doc = IndividualDraftFactory.create()
sp = doc.presentations.create(session=self.future,rev=None)

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

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

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

self.client.login(username=self.other_chair.user.username,password='%s+password'%self.other_chair.user.username)
response = self.client.get(url)
self.assertEqual(response.status_code, 404)

self.assertFalse(mock_slides_manager_cls.called)

self.client.login(username=self.group_chair.user.username,password='%s+password'%self.group_chair.user.username)
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertFalse(mock_slides_manager_cls.called)

# removing a draft
self.assertEqual(1,doc.docevent_set.count())
response = self.client.post(url,{'remove_session':''})
self.assertEqual(response.status_code, 302)
self.assertFalse(doc.presentations.filter(pk=sp.pk).exists())
self.assertEqual(2,doc.docevent_set.count())
self.assertFalse(mock_slides_manager_cls.called)

# removing slides should call Meetecho API
slides = SessionPresentationFactory(session=self.future, document__type_id="slides", order=1).document
url = urlreverse(
"ietf.doc.views_doc.remove_sessionpresentation",
kwargs={"name": slides.name, "session_id": self.future.pk},
)
response = self.client.post(url, {"remove_session": ""})
self.assertEqual(response.status_code, 302)
self.assertEqual(mock_slides_manager_cls.call_count, 1)
self.assertEqual(mock_slides_manager_cls.call_args, mock.call(api_config="fake settings"))
self.assertEqual(mock_slides_manager_cls.return_value.delete.call_count, 1)
self.assertEqual(
mock_slides_manager_cls.return_value.delete.call_args,
mock.call(self.future, slides),
)

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

def test_add_document_session(self):
@override_settings(MEETECHO_API_CONFIG="fake settings")
@mock.patch("ietf.doc.views_doc.SlidesManager")
def test_add_document_session(self, mock_slides_manager_cls):
doc = IndividualDraftFactory.create()

url = urlreverse('ietf.doc.views_doc.add_sessionpresentation',kwargs=dict(name=doc.name))
login_testing_unauthorized(self,self.group_chair.user.username,url)
response = self.client.get(url)
self.assertEqual(response.status_code,200)

self.assertFalse(mock_slides_manager_cls.called)

response = self.client.post(url,{'session':0,'version':'current'})
self.assertEqual(response.status_code,200)
q=PyQuery(response.content)
self.assertTrue(q('.form-select.is-invalid'))
self.assertFalse(mock_slides_manager_cls.called)

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

# adding a draft
self.assertEqual(1,doc.docevent_set.count())
response = self.client.post(url,{'session':self.future.pk,'version':'current'})
self.assertEqual(response.status_code,302)
self.assertEqual(2,doc.docevent_set.count())
self.assertEqual(doc.presentations.get(session__pk=self.future.pk).order, 0)
self.assertFalse(mock_slides_manager_cls.called)

# adding slides should set order / call Meetecho API
slides = DocumentFactory(type_id="slides")
url = urlreverse("ietf.doc.views_doc.add_sessionpresentation", kwargs=dict(name=slides.name))
response = self.client.post(url, {"session": self.future.pk, "version": "current"})
self.assertEqual(response.status_code,302)
self.assertEqual(slides.presentations.get(session__pk=self.future.pk).order, 1)
self.assertEqual(mock_slides_manager_cls.call_args, mock.call(api_config="fake settings"))
self.assertEqual(mock_slides_manager_cls.return_value.add.call_count, 1)
self.assertEqual(
mock_slides_manager_cls.return_value.add.call_args,
mock.call(self.future, slides, order=1),
)

def test_get_related_meeting(self):
"""Should be able to retrieve related meeting"""
Expand Down
Loading

0 comments on commit e6138ca

Please sign in to comment.