From 1f503deebc798be62aa307a93c836818eaa5a5d5 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Wed, 21 Sep 2022 12:08:39 -0500 Subject: [PATCH 01/18] feat: apis for attaching chatlogs and polls to session materials --- .../0045_docstates_chatlogs_polls.py | 34 +++++++++ ietf/meeting/utils.py | 33 ++++++++- ietf/meeting/views.py | 72 +++++++++++++++++++ .../migrations/0045_polls_and_chatlogs.py | 35 +++++++++ 4 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 ietf/doc/migrations/0045_docstates_chatlogs_polls.py create mode 100644 ietf/name/migrations/0045_polls_and_chatlogs.py diff --git a/ietf/doc/migrations/0045_docstates_chatlogs_polls.py b/ietf/doc/migrations/0045_docstates_chatlogs_polls.py new file mode 100644 index 0000000000..ae8ddaa7c2 --- /dev/null +++ b/ietf/doc/migrations/0045_docstates_chatlogs_polls.py @@ -0,0 +1,34 @@ +# Copyright The IETF Trust 2022, All Rights Reserved +from django.db import migrations + +def forward(apps, shema_editor): + StateType = apps.get_model("doc", "StateType") + State = apps.get_model("doc", "State") + for slug in ("chatlogs", "polls"): + StateType.objects.create(slug=slug, label="State") + for state_slug in ("active", "deleted"): + State.objects.create( + type_id = slug, + slug = state_slug, + name = state_slug.capitalize(), + used = True, + desc = "", + order = 0, + ) + +def reverse(apps, shema_editor): + StateType = apps.get_model("doc", "StateType") + State = apps.get_model("doc", "State") + State.objects.filter(type_id__in=("chatlogs", "polls")).delete() + StateType.objects.filter(slug__in=("chatlogs", "polls")).delete() + +class Migration(migrations.Migration): + + dependencies = [ + ('doc', '0044_procmaterials_states'), + ('name', '0045_polls_and_chatlogs'), + ] + + operations = [ + migrations.RunPython(forward, reverse), + ] diff --git a/ietf/meeting/utils.py b/ietf/meeting/utils.py index 0d1a562702..76486d2c25 100644 --- a/ietf/meeting/utils.py +++ b/ietf/meeting/utils.py @@ -21,7 +21,7 @@ from ietf.doc.models import Document, DocAlias, State, NewRevisionDocEvent from ietf.group.models import Group from ietf.group.utils import can_manage_materials -from ietf.name.models import SessionStatusName, ConstraintName +from ietf.name.models import SessionStatusName, ConstraintName, DocTypeName from ietf.person.models import Person from ietf.secr.proceedings.proc_utils import import_audio_files from ietf.utils.html import sanitize_document @@ -710,3 +710,34 @@ def handle_upload_file(file, filename, meeting, subdir, request=None, encoding=N subprocess.call(['unzip', filename], cwd=path) return None + +def new_doc_for_session(type_id, session): + typename = DocTypeName.objects.get(slug=type_id) + ota = session.official_timeslotassignment() + sess_time = ota and ota.timeslot.time + if session.meeting.type_id == "ietf": + name = f"{typename.prefix}-{session.meeting.number}-{session.group.acronym}-{sess_time.strftime('%Y%m%d%H%M')}" + title = f"{typename.name} IETF{session.meeting.number}: {session.group.acronym}: {sess_time.strftime('%a %H:%M')}" + else: + name = f"{typename.prefix}-{session.meeting.number}-{sess_time.strftime('%Y%m%d%H%M')}" + title = f"{typename.name} {session.meeting.number}: {sess_time.strftime('%a %H:%M')}" + doc = Document.objects.create( + name = name, + type_id = type_id, + title = title, + group = session.group, + rev = '00', + ) + doc.states.add(State.objects.get(type_id=type_id, slug='active')) + DocAlias.objects.create(name=doc.name).docs.add(doc) + session.sessionpresentation_set.create(document=doc,rev='00') + return doc + +def write_doc_for_session(session, type_id, filename, contents): + filename = Path(filename) + path = Path(session.meeting.get_materials_path()) / type_id + path.mkdir(parents=True, exist_ok=True) + with open(path / filename, "wb") as file: + file.write(contents.encode('utf-8')) + return + diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py index 0c054db97d..a814f577f4 100644 --- a/ietf/meeting/views.py +++ b/ietf/meeting/views.py @@ -81,6 +81,7 @@ from ietf.meeting.utils import diff_meeting_schedules, prefetch_schedule_diff_objects from ietf.meeting.utils import swap_meeting_schedule_timeslot_assignments, bulk_create_timeslots from ietf.meeting.utils import preprocess_meeting_important_dates +from ietf.meeting.utils import new_doc_for_session, write_doc_for_session from ietf.message.utils import infer_message from ietf.name.models import SlideSubmissionStatusName, ProceedingsMaterialTypeName, SessionPurposeName from ietf.secr.proceedings.proc_utils import (get_progress_stats, post_process, import_audio_files, @@ -3950,6 +3951,77 @@ def err(code, text): session.attended_set.get_or_create(person=user.person) return HttpResponse("Done", status=200, content_type='text/plain') +@require_api_key +@role_required('Recording Manager', 'Secretariat') +@csrf_exempt +def api_upload_chat(request): + def err(code, text): + return HttpResponse(text, status=code, content_type='text/plain') + api_data_post = request.POST.get('api_data') + if not api_data_post: + return err(400, "Missing api_data parameter") + try: + api_data = json.loads(api_data_post) + except json.decoder.JSONDecodeError: + return err(400, "Malformed post") + if not ( 'session_id' in api_data and type(api_data['session_id']) is int ): + return err(400, "Malformed post") + session_id = api_data['session_id'] + if not ( 'chatlog' in api_data and type(api_data['chatlog']) is list and all([type(el) is dict for el in api_data['chatlog']]) ): + return err(400, "Malformed post") + session = Session.objects.filter(pk=session_id).first() + if not session: + return err(400, "Invalid session") + chatlog_sp = session.sessionpresentation_set.filter(document__type='chatlog').first() + if chatlog_sp: + doc = chatlog_sp.document + doc.rev = f"{(int(doc.rev)+1):02d}" + chatlog_sp.rev = doc.rev + chatlog_sp.save() + else: + doc = new_doc_for_session('chatlog', session) + filename = f"{doc.name}-{doc.rev}.json" + doc.uploaded_filename = filename + write_doc_for_session(session, 'chatlog', filename, api_data['chatlog'] ) + e = NewRevisionDocEvent.objects.create(doc=doc, rev=doc.rev, by=request.user.person, type='new_revision', desc='New revision available: %s'%doc.rev) + doc.save_with_history([e]) + return HttpResponse("Done", status=200, content_type='text/plain') + +@require_api_key +@role_required('Recording Manager', 'Secretariat') +@csrf_exempt +def api_upload_poll(request): + def err(code, text): + return HttpResponse(text, status=code, content_type='text/plain') + api_data_post = request.POST.get('api_data') + if not api_data_post: + return err(400, "Missing api_data parameter") + try: + api_data = json.loads(api_data_post) + except json.decoder.JSONDecodeError: + return err(400, "Malformed post") + if not ( 'session_id' in api_data and type(api_data['session_id']) is int ): + return err(400, "Malformed post") + session_id = api_data['session_id'] + if not ( 'polls' in api_data and type(api_data['poll']) is list and all([type(el) is dict for el in api_data['polls']]) ): + return err(400, "Malformed post") + session = Session.objects.filter(pk=session_id).first() + if not session: + return err(400, "Invalid session") + polls_sp = session.sessionpresentation_set.filter(document__type='polls').first() + if polls_sp: + doc = polls_sp.document + doc.rev = f"{(int(doc.rev)+1):02d}" + polls_sp.rev = doc.rev + polls_sp.save() + else: + doc = new_doc_for_session('polls', session) + filename = f"{doc.name}-{doc.rev}.json" + doc.uploaded_filename = filename + write_doc_for_session(session, 'polls', filename, api_data['polls'] ) + e = NewRevisionDocEvent.objects.create(doc=doc, rev=doc.rev, by=request.user.person, type='new_revision', desc='New revision available: %s'%doc.rev) + doc.save_with_history([e]) + return HttpResponse("Done", status=200, content_type='text/plain') @require_api_key @role_required('Recording Manager', 'Secretariat') diff --git a/ietf/name/migrations/0045_polls_and_chatlogs.py b/ietf/name/migrations/0045_polls_and_chatlogs.py new file mode 100644 index 0000000000..19bd90370f --- /dev/null +++ b/ietf/name/migrations/0045_polls_and_chatlogs.py @@ -0,0 +1,35 @@ +# Copyright The IETF Trust 2022, All Rights Reserved +from django.db import migrations + +def forward(apps, schema_editor): + DocTypeName = apps.get_model("name", "DocTypeName") + DocTypeName.objects.create( + slug = "chatlogs", + name = "Chat Logs", + prefix = "chatlogs", + desc = "", + order = 0, + used = True, + ) + DocTypeName.objects.create( + slug = "polls", + name = "Polls", + prefix = "polls", + desc = "", + order = 0, + used = True, + ) + +def reverse(apps, schema_editor): + DocTypeName = apps.get_model("name", "DocTypeName") + DocTypeName.objects.filter(slug__in=("chatlogs", "polls")).delete() + +class Migration(migrations.Migration): + + dependencies = [ + ('name', '0044_validating_draftsubmissionstatename'), + ] + + operations = [ + migrations.RunPython(forward, reverse) + ] From e16f49d4b167630b6c5558c266baf83eb5b62822 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Thu, 22 Sep 2022 12:08:35 -0500 Subject: [PATCH 02/18] fix: anticipate becoming tzaware, and improve guard against attempts to provide docs for sessions that have no official timeslot assignment. --- ietf/meeting/utils.py | 4 +++- ietf/meeting/views.py | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/ietf/meeting/utils.py b/ietf/meeting/utils.py index 76486d2c25..f761027856 100644 --- a/ietf/meeting/utils.py +++ b/ietf/meeting/utils.py @@ -714,7 +714,9 @@ def handle_upload_file(file, filename, meeting, subdir, request=None, encoding=N def new_doc_for_session(type_id, session): typename = DocTypeName.objects.get(slug=type_id) ota = session.official_timeslotassignment() - sess_time = ota and ota.timeslot.time + if ota is None: + return None + sess_time = ota.timeslot.local_start_time() if session.meeting.type_id == "ietf": name = f"{typename.prefix}-{session.meeting.number}-{session.group.acronym}-{sess_time.strftime('%Y%m%d%H%M')}" title = f"{typename.name} IETF{session.meeting.number}: {session.group.acronym}: {sess_time.strftime('%a %H:%M')}" diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py index a814f577f4..338b0d32c2 100644 --- a/ietf/meeting/views.py +++ b/ietf/meeting/views.py @@ -3980,6 +3980,8 @@ def err(code, text): chatlog_sp.save() else: doc = new_doc_for_session('chatlog', session) + if doc is None: + return err(400, "Could not find official timeslot for session") filename = f"{doc.name}-{doc.rev}.json" doc.uploaded_filename = filename write_doc_for_session(session, 'chatlog', filename, api_data['chatlog'] ) @@ -4016,6 +4018,8 @@ def err(code, text): polls_sp.save() else: doc = new_doc_for_session('polls', session) + if doc is None: + return err(400, "Could not find official timeslot for session") filename = f"{doc.name}-{doc.rev}.json" doc.uploaded_filename = filename write_doc_for_session(session, 'polls', filename, api_data['polls'] ) From 1fbb0c0b1e4f6aa825b59feece7290b8ffcc55fb Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Fri, 23 Sep 2022 12:27:03 -0500 Subject: [PATCH 03/18] fix: get chatlog upload to actually work Modifications to several initial implementation decisions. Updates to the fixtures. --- ietf/api/tests.py | 65 ++++++++ ietf/api/urls.py | 3 + .../0045_docstates_chatlogs_polls.py | 6 +- ietf/meeting/utils.py | 1 - ietf/meeting/views.py | 42 ++--- ietf/name/fixtures/names.json | 152 ++++++++++++++++-- .../migrations/0045_polls_and_chatlogs.py | 8 +- 7 files changed, 240 insertions(+), 37 deletions(-) diff --git a/ietf/api/tests.py b/ietf/api/tests.py index cee760a33b..5355899d15 100644 --- a/ietf/api/tests.py +++ b/ietf/api/tests.py @@ -9,6 +9,7 @@ from importlib import import_module from mock import patch +from pathlib import Path from django.apps import apps from django.conf import settings @@ -21,6 +22,7 @@ import debug # pyflakes:ignore import ietf +from ietf.doc.utils import get_unicode_document_content from ietf.group.factories import RoleFactory from ietf.meeting.factories import MeetingFactory, SessionFactory from ietf.meeting.test_data import make_meeting_test_data @@ -212,6 +214,69 @@ 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_upload_chatlog(self): + url = urlreverse('ietf.meeting.views.api_upload_chatlog') + + recmanrole = RoleFactory(group__type_id='ietf', name_id='recman') + recmanrole.person.user.last_login = timezone.now() + recmanrole.person.user.save() + apikey = PersonalApiKey.objects.create(endpoint=url, person=recmanrole.person) + + badrole = RoleFactory(group__type_id='ietf', name_id='ad') + badrole.person.user.last_login = timezone.now() + badrole.person.user.save() + badapikey = PersonalApiKey.objects.create(endpoint=url, person=badrole.person) + + meeting = MeetingFactory(type_id='ietf') + session = SessionFactory(group__type_id='wg', meeting=meeting) + + 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.get(url, {'apikey': apikey.hash()} ) + self.assertContains(r, "Method not allowed", status_code=405) + + r = self.client.post(url, {'apikey': apikey.hash()} ) + self.assertContains(r, "Missing apidata 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},"chatlog":"not a list;drop table"}}', + f'{{"session_id":{session.pk},"chatlog":"not a list;drop table"}}', + f'{{"session_id":{session.pk},"chatlog":[{{}}, {{}}, "not an int;drop table", {{}}]}}', + ): + r = self.client.post(url, {'apikey': apikey.hash(), 'apidata': 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(), 'apidata': f'{{"session_id":{bad_session_id},"chatlog":[]}}'}) + self.assertContains(r, "Invalid session", status_code=400) + + # Valid post (some extra whitespace in the chatlog argument to improve test readability) + chatlog="""[ + { + "author": "Raymond Lutz", + "text": "

Yes I like that comment just made

", + "time": "2022-07-28T19:26:16Z" + }, + { + "author": "Carsten Bormann", + "text": "

But software is not a thing.

", + "time": "2022-07-28T19:26:45Z" + } + ]""" + r = self.client.post(url,{'apikey':apikey.hash(),'apidata': f'{{"session_id":{session.pk}, "chatlog":{chatlog}}}'}) + self.assertEqual(r.status_code, 200) + + newdoc = session.sessionpresentation_set.first().document + newdoccontent = get_unicode_document_content(newdoc.name, Path(session.meeting.get_materials_path()) / "chatlog" / newdoc.uploaded_filename) + self.assertIn("Carsten", newdoccontent) + def test_api_upload_bluesheet(self): url = urlreverse('ietf.meeting.views.api_upload_bluesheet') recmanrole = RoleFactory(group__type_id='ietf', name_id='recman') diff --git a/ietf/api/urls.py b/ietf/api/urls.py index ca84870e2d..e9665cf7d5 100644 --- a/ietf/api/urls.py +++ b/ietf/api/urls.py @@ -37,6 +37,9 @@ url(r'^notify/meeting/bluesheet/?$', meeting_views.api_upload_bluesheet), # Let MeetEcho tell us about session attendees url(r'^notify/session/attendees/?$', meeting_views.api_add_session_attendees), + # Let MeetEcho upload session chatlog + url(r'^notify/session/chatlog/?$', meeting_views.api_upload_chatlog), + # Let MeetEcho upload session polls # Let the registration system notify us about registrations url(r'^notify/meeting/registration/?', api_views.api_new_meeting_registration), # OpenID authentication provider diff --git a/ietf/doc/migrations/0045_docstates_chatlogs_polls.py b/ietf/doc/migrations/0045_docstates_chatlogs_polls.py index ae8ddaa7c2..adb3d25237 100644 --- a/ietf/doc/migrations/0045_docstates_chatlogs_polls.py +++ b/ietf/doc/migrations/0045_docstates_chatlogs_polls.py @@ -4,7 +4,7 @@ def forward(apps, shema_editor): StateType = apps.get_model("doc", "StateType") State = apps.get_model("doc", "State") - for slug in ("chatlogs", "polls"): + for slug in ("chatlog", "polls"): StateType.objects.create(slug=slug, label="State") for state_slug in ("active", "deleted"): State.objects.create( @@ -19,8 +19,8 @@ def forward(apps, shema_editor): def reverse(apps, shema_editor): StateType = apps.get_model("doc", "StateType") State = apps.get_model("doc", "State") - State.objects.filter(type_id__in=("chatlogs", "polls")).delete() - StateType.objects.filter(slug__in=("chatlogs", "polls")).delete() + State.objects.filter(type_id__in=("chatlog", "polls")).delete() + StateType.objects.filter(slug__in=("chatlog", "polls")).delete() class Migration(migrations.Migration): diff --git a/ietf/meeting/utils.py b/ietf/meeting/utils.py index f761027856..aae286682d 100644 --- a/ietf/meeting/utils.py +++ b/ietf/meeting/utils.py @@ -742,4 +742,3 @@ def write_doc_for_session(session, type_id, filename, contents): with open(path / filename, "wb") as file: file.write(contents.encode('utf-8')) return - diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py index 338b0d32c2..df5d99c80c 100644 --- a/ietf/meeting/views.py +++ b/ietf/meeting/views.py @@ -3952,22 +3952,24 @@ def err(code, text): return HttpResponse("Done", status=200, content_type='text/plain') @require_api_key -@role_required('Recording Manager', 'Secretariat') +@role_required('Recording Manager') @csrf_exempt -def api_upload_chat(request): +def api_upload_chatlog(request): def err(code, text): return HttpResponse(text, status=code, content_type='text/plain') - api_data_post = request.POST.get('api_data') - if not api_data_post: - return err(400, "Missing api_data parameter") + if request.method != 'POST': + return err(405, "Method not allowed") + apidata_post = request.POST.get('apidata') + if not apidata_post: + return err(400, "Missing apidata parameter") try: - api_data = json.loads(api_data_post) + apidata = json.loads(apidata_post) except json.decoder.JSONDecodeError: return err(400, "Malformed post") - if not ( 'session_id' in api_data and type(api_data['session_id']) is int ): + if not ( 'session_id' in apidata and type(apidata['session_id']) is int ): return err(400, "Malformed post") - session_id = api_data['session_id'] - if not ( 'chatlog' in api_data and type(api_data['chatlog']) is list and all([type(el) is dict for el in api_data['chatlog']]) ): + session_id = apidata['session_id'] + if not ( 'chatlog' in apidata and type(apidata['chatlog']) is list and all([type(el) is dict for el in apidata['chatlog']]) ): return err(400, "Malformed post") session = Session.objects.filter(pk=session_id).first() if not session: @@ -3984,28 +3986,30 @@ def err(code, text): return err(400, "Could not find official timeslot for session") filename = f"{doc.name}-{doc.rev}.json" doc.uploaded_filename = filename - write_doc_for_session(session, 'chatlog', filename, api_data['chatlog'] ) + write_doc_for_session(session, 'chatlog', filename, json.dumps(apidata['chatlog'])) e = NewRevisionDocEvent.objects.create(doc=doc, rev=doc.rev, by=request.user.person, type='new_revision', desc='New revision available: %s'%doc.rev) doc.save_with_history([e]) return HttpResponse("Done", status=200, content_type='text/plain') @require_api_key -@role_required('Recording Manager', 'Secretariat') +@role_required('Recording Manager') @csrf_exempt def api_upload_poll(request): def err(code, text): return HttpResponse(text, status=code, content_type='text/plain') - api_data_post = request.POST.get('api_data') - if not api_data_post: - return err(400, "Missing api_data parameter") + if request.method != 'POST': + return err(405, "Method not allowed") + apidata_post = request.POST.get('apidata') + if not apidata_post: + return err(400, "Missing apidata parameter") try: - api_data = json.loads(api_data_post) + apidata = json.loads(apidata_post) except json.decoder.JSONDecodeError: return err(400, "Malformed post") - if not ( 'session_id' in api_data and type(api_data['session_id']) is int ): + if not ( 'session_id' in apidata and type(apidata['session_id']) is int ): return err(400, "Malformed post") - session_id = api_data['session_id'] - if not ( 'polls' in api_data and type(api_data['poll']) is list and all([type(el) is dict for el in api_data['polls']]) ): + session_id = apidata['session_id'] + if not ( 'polls' in apidata and type(apidata['poll']) is list and all([type(el) is dict for el in apidata['polls']]) ): return err(400, "Malformed post") session = Session.objects.filter(pk=session_id).first() if not session: @@ -4022,7 +4026,7 @@ def err(code, text): return err(400, "Could not find official timeslot for session") filename = f"{doc.name}-{doc.rev}.json" doc.uploaded_filename = filename - write_doc_for_session(session, 'polls', filename, api_data['polls'] ) + write_doc_for_session(session, 'polls', filename, apidata['polls'] ) e = NewRevisionDocEvent.objects.create(doc=doc, rev=doc.rev, by=request.user.person, type='new_revision', desc='New revision available: %s'%doc.rev) doc.save_with_history([e]) return HttpResponse("Done", status=200, content_type='text/plain') diff --git a/ietf/name/fixtures/names.json b/ietf/name/fixtures/names.json index 41f7fec62a..ebce124d2b 100644 --- a/ietf/name/fixtures/names.json +++ b/ietf/name/fixtures/names.json @@ -2200,7 +2200,7 @@ }, { "fields": { - "desc": "The IESG has not started processing this draft, or has stopped processing it without publicastion.", + "desc": "The IESG has not started processing this draft, or has stopped processing it without publication.", "name": "I-D Exists", "next_states": [ 16, @@ -2405,6 +2405,84 @@ "model": "doc.state", "pk": 164 }, + { + "fields": { + "desc": "", + "name": "Active", + "next_states": [], + "order": 0, + "slug": "active", + "type": "chatlogs", + "used": true + }, + "model": "doc.state", + "pk": 165 + }, + { + "fields": { + "desc": "", + "name": "Deleted", + "next_states": [], + "order": 0, + "slug": "deleted", + "type": "chatlogs", + "used": true + }, + "model": "doc.state", + "pk": 166 + }, + { + "fields": { + "desc": "", + "name": "Active", + "next_states": [], + "order": 0, + "slug": "active", + "type": "chatlog", + "used": true + }, + "model": "doc.state", + "pk": 169 + }, + { + "fields": { + "desc": "", + "name": "Deleted", + "next_states": [], + "order": 0, + "slug": "deleted", + "type": "chatlog", + "used": true + }, + "model": "doc.state", + "pk": 170 + }, + { + "fields": { + "desc": "", + "name": "Active", + "next_states": [], + "order": 0, + "slug": "active", + "type": "polls", + "used": true + }, + "model": "doc.state", + "pk": 171 + }, + { + "fields": { + "desc": "", + "name": "Deleted", + "next_states": [], + "order": 0, + "slug": "deleted", + "type": "polls", + "used": true + }, + "model": "doc.state", + "pk": 172 + }, { "fields": { "label": "State" @@ -2433,6 +2511,20 @@ "model": "doc.statetype", "pk": "charter" }, + { + "fields": { + "label": "State" + }, + "model": "doc.statetype", + "pk": "chatlog" + }, + { + "fields": { + "label": "State" + }, + "model": "doc.statetype", + "pk": "chatlogs" + }, { "fields": { "label": "Conflict Review State" @@ -2538,6 +2630,13 @@ "model": "doc.statetype", "pk": "minutes" }, + { + "fields": { + "label": "State" + }, + "model": "doc.statetype", + "pk": "polls" + }, { "fields": { "label": "Proceedings Materials State" @@ -3345,7 +3444,7 @@ "has_session_materials": false, "is_schedulable": false, "material_types": "[\n \"slides\"\n]", - "matman_roles": "[]", + "matman_roles": "[\n \"chair\"\n]", "need_parent": false, "parent_types": [], "req_subm_approval": true, @@ -3458,8 +3557,8 @@ "has_milestones": false, "has_nonsession_materials": true, "has_reviews": false, - "has_session_materials": false, - "is_schedulable": false, + "has_session_materials": true, + "is_schedulable": true, "material_types": "[\n \"slides\"\n]", "matman_roles": "[\n \"chair\",\n \"matman\"\n]", "need_parent": false, @@ -3469,7 +3568,7 @@ "req_subm_approval": false, "role_order": "[\n \"chair\",\n \"member\",\n \"matman\"\n]", "session_purposes": "[\n \"coding\",\n \"presentation\",\n \"social\",\n \"tutorial\"\n]", - "show_on_agenda": false + "show_on_agenda": true }, "model": "group.groupfeatures", "pk": "team" @@ -10126,6 +10225,28 @@ "model": "name.doctypename", "pk": "charter" }, + { + "fields": { + "desc": "", + "name": "Chat Log", + "order": 0, + "prefix": "chatlog", + "used": true + }, + "model": "name.doctypename", + "pk": "chatlog" + }, + { + "fields": { + "desc": "", + "name": "Chat Logs", + "order": 0, + "prefix": "chatlogs", + "used": true + }, + "model": "name.doctypename", + "pk": "chatlogs" + }, { "fields": { "desc": "", @@ -10181,6 +10302,17 @@ "model": "name.doctypename", "pk": "minutes" }, + { + "fields": { + "desc": "", + "name": "Polls", + "order": 0, + "prefix": "polls", + "used": true + }, + "model": "name.doctypename", + "pk": "polls" + }, { "fields": { "desc": "", @@ -15998,7 +16130,7 @@ "fields": { "command": "xym", "switch": "--version", - "time": "2022-07-13T00:09:29.108", + "time": "2022-09-12T00:09:32.787", "used": true, "version": "xym 0.5" }, @@ -16009,7 +16141,7 @@ "fields": { "command": "pyang", "switch": "--version", - "time": "2022-07-13T00:09:29.475", + "time": "2022-09-12T00:09:33.130", "used": true, "version": "pyang 2.5.3" }, @@ -16020,7 +16152,7 @@ "fields": { "command": "yanglint", "switch": "--version", - "time": "2022-07-13T00:09:29.497", + "time": "2022-09-12T00:09:33.147", "used": true, "version": "yanglint SO 1.9.2" }, @@ -16031,9 +16163,9 @@ "fields": { "command": "xml2rfc", "switch": "--version", - "time": "2022-07-13T00:09:30.513", + "time": "2022-09-12T00:09:34.054", "used": true, - "version": "xml2rfc 3.13.0" + "version": "xml2rfc 3.14.2" }, "model": "utils.versioninfo", "pk": 4 diff --git a/ietf/name/migrations/0045_polls_and_chatlogs.py b/ietf/name/migrations/0045_polls_and_chatlogs.py index 19bd90370f..1014a9dcef 100644 --- a/ietf/name/migrations/0045_polls_and_chatlogs.py +++ b/ietf/name/migrations/0045_polls_and_chatlogs.py @@ -4,9 +4,9 @@ def forward(apps, schema_editor): DocTypeName = apps.get_model("name", "DocTypeName") DocTypeName.objects.create( - slug = "chatlogs", - name = "Chat Logs", - prefix = "chatlogs", + slug = "chatlog", + name = "Chat Log", + prefix = "chatlog", desc = "", order = 0, used = True, @@ -22,7 +22,7 @@ def forward(apps, schema_editor): def reverse(apps, schema_editor): DocTypeName = apps.get_model("name", "DocTypeName") - DocTypeName.objects.filter(slug__in=("chatlogs", "polls")).delete() + DocTypeName.objects.filter(slug__in=("chatlog", "polls")).delete() class Migration(migrations.Migration): From 891d8482b196683f366ce9a679a0e59ad01f842f Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Fri, 23 Sep 2022 13:12:26 -0500 Subject: [PATCH 04/18] fix: test polls upload Refactored test to reduce duplicate code --- ietf/api/tests.py | 126 +++++++++++++++++++++++++----------------- ietf/api/urls.py | 1 + ietf/meeting/views.py | 6 +- 3 files changed, 79 insertions(+), 54 deletions(-) diff --git a/ietf/api/tests.py b/ietf/api/tests.py index 5355899d15..ab565a917c 100644 --- a/ietf/api/tests.py +++ b/ietf/api/tests.py @@ -214,68 +214,92 @@ 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_upload_chatlog(self): - url = urlreverse('ietf.meeting.views.api_upload_chatlog') - + def test_api_upload_polls_and_chatlog(self): recmanrole = RoleFactory(group__type_id='ietf', name_id='recman') recmanrole.person.user.last_login = timezone.now() recmanrole.person.user.save() - apikey = PersonalApiKey.objects.create(endpoint=url, person=recmanrole.person) badrole = RoleFactory(group__type_id='ietf', name_id='ad') badrole.person.user.last_login = timezone.now() badrole.person.user.save() - badapikey = PersonalApiKey.objects.create(endpoint=url, person=badrole.person) meeting = MeetingFactory(type_id='ietf') - session = SessionFactory(group__type_id='wg', meeting=meeting) - - 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.get(url, {'apikey': apikey.hash()} ) - self.assertContains(r, "Method not allowed", status_code=405) - - r = self.client.post(url, {'apikey': apikey.hash()} ) - self.assertContains(r, "Missing apidata parameter", status_code=400) + session = SessionFactory(group__type_id='wg', meeting=meeting) - for baddict in ( - '{}', - '{"bogons;drop table":"bogons;drop table"}', - '{"session_id":"Not an integer;drop table"}', - f'{{"session_id":{session.pk},"chatlog":"not a list;drop table"}}', - f'{{"session_id":{session.pk},"chatlog":"not a list;drop table"}}', - f'{{"session_id":{session.pk},"chatlog":[{{}}, {{}}, "not an int;drop table", {{}}]}}', + for type_id, content in ( + ( + "chatlog", + """[ + { + "author": "Raymond Lutz", + "text": "

Yes I like that comment just made

", + "time": "2022-07-28T19:26:16Z" + }, + { + "author": "Carsten Bormann", + "text": "

But software is not a thing.

", + "time": "2022-07-28T19:26:45Z" + } + ]""" + ), + ( + "polls", + """[ + { + "start_time": "2022-07-28T19:19:54Z", + "end_time": "2022-07-28T19:20:23Z", + "text": "Are you willing to review the documents?", + "raise_hand": 57, + "do_not_raise_hand": 11 + }, + { + "start_time": "2022-07-28T19:20:56Z", + "end_time": "2022-07-28T19:21:30Z", + "text": "Would you be willing to edit or coauthor a document?", + "raise_hand": 31, + "do_not_raise_hand": 31 + } + ]""" + ), ): - r = self.client.post(url, {'apikey': apikey.hash(), 'apidata': 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(), 'apidata': f'{{"session_id":{bad_session_id},"chatlog":[]}}'}) - self.assertContains(r, "Invalid session", status_code=400) - - # Valid post (some extra whitespace in the chatlog argument to improve test readability) - chatlog="""[ - { - "author": "Raymond Lutz", - "text": "

Yes I like that comment just made

", - "time": "2022-07-28T19:26:16Z" - }, - { - "author": "Carsten Bormann", - "text": "

But software is not a thing.

", - "time": "2022-07-28T19:26:45Z" - } - ]""" - r = self.client.post(url,{'apikey':apikey.hash(),'apidata': f'{{"session_id":{session.pk}, "chatlog":{chatlog}}}'}) - self.assertEqual(r.status_code, 200) - - newdoc = session.sessionpresentation_set.first().document - newdoccontent = get_unicode_document_content(newdoc.name, Path(session.meeting.get_materials_path()) / "chatlog" / newdoc.uploaded_filename) - self.assertIn("Carsten", newdoccontent) + url = urlreverse(f"ietf.meeting.views.api_upload_{type_id}") + apikey = PersonalApiKey.objects.create(endpoint=url, person=recmanrole.person) + badapikey = PersonalApiKey.objects.create(endpoint=url, person=badrole.person) + + 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.get(url, {'apikey': apikey.hash()} ) + self.assertContains(r, "Method not allowed", status_code=405) + + r = self.client.post(url, {'apikey': apikey.hash()} ) + self.assertContains(r, "Missing apidata 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},"{type_id}":"not a list;drop table"}}', + f'{{"session_id":{session.pk},"{type_id}":"not a list;drop table"}}', + f'{{"session_id":{session.pk},"{type_id}":[{{}}, {{}}, "not an int;drop table", {{}}]}}', + ): + r = self.client.post(url, {'apikey': apikey.hash(), 'apidata': 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(), 'apidata': f'{{"session_id":{bad_session_id},"{type_id}":[]}}'}) + self.assertContains(r, "Invalid session", status_code=400) + + # Valid POST + r = self.client.post(url,{'apikey':apikey.hash(),'apidata': f'{{"session_id":{session.pk}, "{type_id}":{content}}}'}) + self.assertEqual(r.status_code, 200) + + newdoc = session.sessionpresentation_set.get(document__type_id=type_id).document + newdoccontent = get_unicode_document_content(newdoc.name, Path(session.meeting.get_materials_path()) / type_id / newdoc.uploaded_filename) + self.assertEqual(json.loads(content), json.loads(newdoccontent)) def test_api_upload_bluesheet(self): url = urlreverse('ietf.meeting.views.api_upload_bluesheet') diff --git a/ietf/api/urls.py b/ietf/api/urls.py index e9665cf7d5..714be8a6ac 100644 --- a/ietf/api/urls.py +++ b/ietf/api/urls.py @@ -40,6 +40,7 @@ # Let MeetEcho upload session chatlog url(r'^notify/session/chatlog/?$', meeting_views.api_upload_chatlog), # Let MeetEcho upload session polls + url(r'^notify/session/polls/?$', meeting_views.api_upload_polls), # Let the registration system notify us about registrations url(r'^notify/meeting/registration/?', api_views.api_new_meeting_registration), # OpenID authentication provider diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py index df5d99c80c..94b4e72055 100644 --- a/ietf/meeting/views.py +++ b/ietf/meeting/views.py @@ -3994,7 +3994,7 @@ def err(code, text): @require_api_key @role_required('Recording Manager') @csrf_exempt -def api_upload_poll(request): +def api_upload_polls(request): def err(code, text): return HttpResponse(text, status=code, content_type='text/plain') if request.method != 'POST': @@ -4009,7 +4009,7 @@ def err(code, text): if not ( 'session_id' in apidata and type(apidata['session_id']) is int ): return err(400, "Malformed post") session_id = apidata['session_id'] - if not ( 'polls' in apidata and type(apidata['poll']) is list and all([type(el) is dict for el in apidata['polls']]) ): + if not ( 'polls' in apidata and type(apidata['polls']) is list and all([type(el) is dict for el in apidata['polls']]) ): return err(400, "Malformed post") session = Session.objects.filter(pk=session_id).first() if not session: @@ -4026,7 +4026,7 @@ def err(code, text): return err(400, "Could not find official timeslot for session") filename = f"{doc.name}-{doc.rev}.json" doc.uploaded_filename = filename - write_doc_for_session(session, 'polls', filename, apidata['polls'] ) + write_doc_for_session(session, 'polls', filename, json.dumps(apidata['polls'])) e = NewRevisionDocEvent.objects.create(doc=doc, rev=doc.rev, by=request.user.person, type='new_revision', desc='New revision available: %s'%doc.rev) doc.save_with_history([e]) return HttpResponse("Done", status=200, content_type='text/plain') From c99930e1d9139b9345a9ee7844ca2c363172709f Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Fri, 23 Sep 2022 14:20:48 -0500 Subject: [PATCH 05/18] fix: allow api keys to be created for the new endpoints --- ietf/person/models.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ietf/person/models.py b/ietf/person/models.py index 143b8dd088..c4401b4178 100644 --- a/ietf/person/models.py +++ b/ietf/person/models.py @@ -368,7 +368,9 @@ def salt(): ("/api/meeting/session/video/url", "/api/meeting/session/video/url", "Recording Manager"), ("/api/notify/meeting/registration", "/api/notify/meeting/registration", "Robot"), ("/api/notify/meeting/bluesheet", "/api/notify/meeting/bluesheet", "Recording Manager"), - ("/api/notify/session/attendees", "/api/notify/session/attendees", "Recording Manager"), + ("/api/notify/session/attendees", "/api/notify/session/attendees", "Recording Manager"), + ("/api/notify/session/chatlog", "/api/notify/session/chatlog", "Recording Manager"), + ("/api/notify/session/polls", "/api/notify/session/polls", "Recording Manager"), ("/api/appauth/authortools", "/api/appauth/authortools", None), ("/api/appauth/bibxml", "/api/appauth/bibxml", None), ] From 4aae306bf983eb02f4854d389c96c2a38cd3552d Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Mon, 26 Sep 2022 12:14:31 -0500 Subject: [PATCH 06/18] feat: add ability to view chatlog and polls documents. Show links in session materials. --- ietf/doc/models.py | 4 +- ietf/doc/views_doc.py | 25 +++++++- ietf/meeting/views.py | 1 + ietf/name/fixtures/names.json | 60 +++---------------- ietf/settings.py | 2 + .../meeting/session_details_panel.html | 17 ++++++ 6 files changed, 52 insertions(+), 57 deletions(-) diff --git a/ietf/doc/models.py b/ietf/doc/models.py index 23a2403bfe..6512d4b54e 100644 --- a/ietf/doc/models.py +++ b/ietf/doc/models.py @@ -136,7 +136,7 @@ def get_file_path(self): else: self._cached_file_path = settings.INTERNET_ALL_DRAFTS_ARCHIVE_DIR elif self.meeting_related() and self.type_id in ( - "agenda", "minutes", "slides", "bluesheets", "procmaterials" + "agenda", "minutes", "slides", "bluesheets", "procmaterials", "chatlog", "polls" ): meeting = self.get_related_meeting() if meeting is not None: @@ -420,7 +420,7 @@ def has_rfc_editor_note(self): return e != None and (e.text != "") def meeting_related(self): - if self.type_id in ("agenda","minutes","bluesheets","slides","recording","procmaterials"): + if self.type_id in ("agenda","minutes","bluesheets","slides","recording","procmaterials","chatlog","polls"): return self.type_id != "slides" or self.get_state_slug('reuse_policy')=='single' return False diff --git a/ietf/doc/views_doc.py b/ietf/doc/views_doc.py index d34ac4a59b..0e9aab8642 100644 --- a/ietf/doc/views_doc.py +++ b/ietf/doc/views_doc.py @@ -42,6 +42,7 @@ import re from urllib.parse import quote +from pathlib import Path from django.http import HttpResponse, Http404 from django.shortcuts import render, get_object_or_404, redirect @@ -641,9 +642,7 @@ def document_main(request, name, rev=None): sorted_relations=sorted_relations, )) - # TODO : Add "recording", and "bluesheets" here when those documents are appropriately - # created and content is made available on disk - if doc.type_id in ("slides", "agenda", "minutes", "bluesheets","procmaterials",): + if doc.type_id in ("slides", "agenda", "minutes", "bluesheets", "procmaterials",): can_manage_material = can_manage_materials(request.user, doc.group) presentations = doc.future_presentations() if doc.uploaded_filename: @@ -725,6 +724,26 @@ def document_main(request, name, rev=None): assignments=assignments, )) + if doc.type_id == "chatlog": + session = doc.sessionpresentation_set.last().session + pathname = Path(session.meeting.get_materials_path()) / "chatlog" / doc.uploaded_filename + content = get_unicode_document_content(doc.name, str(pathname)) + return render( + request, + "doc/document_chatlog.html", + dict( + doc=doc, + top=top, + content=content, + revisions=revisions, + latest_rev=latest_rev, + snapshot=snapshot, + session=session, + ) + ) + + + raise Http404("Document not found: %s" % (name + ("-%s"%rev if rev else ""))) diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py index 94b4e72055..8dd8478786 100644 --- a/ietf/meeting/views.py +++ b/ietf/meeting/views.py @@ -2423,6 +2423,7 @@ def session_details(request, num, acronym): session.filtered_artifacts.sort(key=lambda d:['agenda','minutes','bluesheets'].index(d.document.type.slug)) session.filtered_slides = session.sessionpresentation_set.filter(document__type__slug='slides').order_by('order') session.filtered_drafts = session.sessionpresentation_set.filter(document__type__slug='draft') + session.filtered_chatlog_and_polls = session.sessionpresentation_set.filter(document__type__slug__in=('chatlog', 'polls')).order_by('document__type__slug') # TODO FIXME Deleted materials shouldn't be in the sessionpresentation_set for qs in [session.filtered_artifacts,session.filtered_slides,session.filtered_drafts]: qs = [p for p in qs if p.document.get_state_slug(p.document.type_id)!='deleted'] diff --git a/ietf/name/fixtures/names.json b/ietf/name/fixtures/names.json index ebce124d2b..f6a3a7d7ca 100644 --- a/ietf/name/fixtures/names.json +++ b/ietf/name/fixtures/names.json @@ -2405,32 +2405,6 @@ "model": "doc.state", "pk": 164 }, - { - "fields": { - "desc": "", - "name": "Active", - "next_states": [], - "order": 0, - "slug": "active", - "type": "chatlogs", - "used": true - }, - "model": "doc.state", - "pk": 165 - }, - { - "fields": { - "desc": "", - "name": "Deleted", - "next_states": [], - "order": 0, - "slug": "deleted", - "type": "chatlogs", - "used": true - }, - "model": "doc.state", - "pk": 166 - }, { "fields": { "desc": "", @@ -2442,7 +2416,7 @@ "used": true }, "model": "doc.state", - "pk": 169 + "pk": 165 }, { "fields": { @@ -2455,7 +2429,7 @@ "used": true }, "model": "doc.state", - "pk": 170 + "pk": 166 }, { "fields": { @@ -2468,7 +2442,7 @@ "used": true }, "model": "doc.state", - "pk": 171 + "pk": 167 }, { "fields": { @@ -2481,7 +2455,7 @@ "used": true }, "model": "doc.state", - "pk": 172 + "pk": 168 }, { "fields": { @@ -2518,13 +2492,6 @@ "model": "doc.statetype", "pk": "chatlog" }, - { - "fields": { - "label": "State" - }, - "model": "doc.statetype", - "pk": "chatlogs" - }, { "fields": { "label": "Conflict Review State" @@ -10236,17 +10203,6 @@ "model": "name.doctypename", "pk": "chatlog" }, - { - "fields": { - "desc": "", - "name": "Chat Logs", - "order": 0, - "prefix": "chatlogs", - "used": true - }, - "model": "name.doctypename", - "pk": "chatlogs" - }, { "fields": { "desc": "", @@ -16130,7 +16086,7 @@ "fields": { "command": "xym", "switch": "--version", - "time": "2022-09-12T00:09:32.787", + "time": "2022-09-22T00:09:27.552", "used": true, "version": "xym 0.5" }, @@ -16141,7 +16097,7 @@ "fields": { "command": "pyang", "switch": "--version", - "time": "2022-09-12T00:09:33.130", + "time": "2022-09-22T00:09:27.867", "used": true, "version": "pyang 2.5.3" }, @@ -16152,7 +16108,7 @@ "fields": { "command": "yanglint", "switch": "--version", - "time": "2022-09-12T00:09:33.147", + "time": "2022-09-22T00:09:27.886", "used": true, "version": "yanglint SO 1.9.2" }, @@ -16163,7 +16119,7 @@ "fields": { "command": "xml2rfc", "switch": "--version", - "time": "2022-09-12T00:09:34.054", + "time": "2022-09-22T00:09:28.809", "used": true, "version": "xml2rfc 3.14.2" }, diff --git a/ietf/settings.py b/ietf/settings.py index aed1f75c7c..5e4912d85c 100644 --- a/ietf/settings.py +++ b/ietf/settings.py @@ -889,6 +889,8 @@ def skip_unreadable_post(record): "agenda": "/meeting/{meeting.number}/materials/{doc.name}-{doc.rev}", "minutes": "/meeting/{meeting.number}/materials/{doc.name}-{doc.rev}", "slides": "/meeting/{meeting.number}/materials/{doc.name}-{doc.rev}", + "chatlog": "/meeting/{meeting.number}/materials/{doc.name}-{doc.rev}", + "polls": "/meeting/{meeting.number}/materials/{doc.name}-{doc.rev}", "recording": "{doc.external_url}", "bluesheets": "https://www.ietf.org/proceedings/{meeting.number}/bluesheets/{doc.uploaded_filename}", "procmaterials": "/meeting/{meeting.number}/materials/{doc.name}-{doc.rev}", diff --git a/ietf/templates/meeting/session_details_panel.html b/ietf/templates/meeting/session_details_panel.html index c3bd70fa6d..eb646e4583 100644 --- a/ietf/templates/meeting/session_details_panel.html +++ b/ietf/templates/meeting/session_details_panel.html @@ -114,6 +114,23 @@

Agenda, Minutes, and Bluesheets

Upload bluesheets {% endif %} + {% if session.filtered_chatlog_and_polls %} +

Chatlog and polls

+ + + {% for pres in session.filtered_chatlog_and_polls %} + + {% url 'ietf.doc.views_doc.document_main' name=pres.document.name as url %} + + + {% endfor %} + +
+ {{ pres.document.title }} + ({{ pres.document.name }}) +
+ {% endif %}

Slides

From 6a1b0219931b71dad265da2581984d9690c591c1 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Wed, 28 Sep 2022 11:27:24 -0500 Subject: [PATCH 07/18] fix: commit new template --- ietf/templates/doc/document_chatlog.html | 63 ++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 ietf/templates/doc/document_chatlog.html diff --git a/ietf/templates/doc/document_chatlog.html b/ietf/templates/doc/document_chatlog.html new file mode 100644 index 0000000000..0f683448e2 --- /dev/null +++ b/ietf/templates/doc/document_chatlog.html @@ -0,0 +1,63 @@ +{% extends "base.html" %} +{# Copyright The IETF Trust 2022, All Rights Reserved #} +{% load origin %} +{% load static %} +{% load ietf_filters textfilters %} +{% block title %}{{ doc.title|default:"Untitled" }}{% endblock %} +{% block content %} + {% origin %} + {{ top|safe }} + {% include "doc/revisions_list.html" %} +
+ {% if doc.rev != latest_rev %} +
The information below is for an old version of the document.
+ {% endif %} +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ {% if doc.meeting_related %}Meeting{% endif %} + {{ doc.type.name }} + + {% if doc.group %} + {{ doc.group.name }} + ({{ doc.group.acronym }}) + {{ doc.group.type.name }} + {% endif %} + {% if snapshot %}Snapshot{% endif %} +
Title{{ doc.title|default:'(None)' }}
Session + + Materials +
Last updated{{ doc.time|date:"Y-m-d" }}
+
+
{{ doc.name }}-{{ doc.rev }}
+
+ {{ content }} {# TODO #} +
+
+{% endblock %} +{% block js %} + + +{% endblock %} \ No newline at end of file From 7bfd3d3bd5fb449ada90235bce98794ad70a1311 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Wed, 28 Sep 2022 18:01:48 -0500 Subject: [PATCH 08/18] fix: typo in migration signatures --- ietf/doc/migrations/0045_docstates_chatlogs_polls.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ietf/doc/migrations/0045_docstates_chatlogs_polls.py b/ietf/doc/migrations/0045_docstates_chatlogs_polls.py index adb3d25237..044cc60de4 100644 --- a/ietf/doc/migrations/0045_docstates_chatlogs_polls.py +++ b/ietf/doc/migrations/0045_docstates_chatlogs_polls.py @@ -1,7 +1,7 @@ # Copyright The IETF Trust 2022, All Rights Reserved from django.db import migrations -def forward(apps, shema_editor): +def forward(apps, schema_editor): StateType = apps.get_model("doc", "StateType") State = apps.get_model("doc", "State") for slug in ("chatlog", "polls"): @@ -16,7 +16,7 @@ def forward(apps, shema_editor): order = 0, ) -def reverse(apps, shema_editor): +def reverse(apps, schema_editor): StateType = apps.get_model("doc", "StateType") State = apps.get_model("doc", "State") State.objects.filter(type_id__in=("chatlog", "polls")).delete() From de01bb53b8ea64e3a389a6ffc8e862597a7eb5e7 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Thu, 29 Sep 2022 09:52:43 -0500 Subject: [PATCH 09/18] feat: add main doc page handling for polls. Improve tests. --- ietf/doc/tests.py | 2 + ietf/doc/views_doc.py | 6 +-- ietf/templates/doc/document_polls.html | 63 ++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 ietf/templates/doc/document_polls.html diff --git a/ietf/doc/tests.py b/ietf/doc/tests.py index e01b5e0bd8..f05a866e35 100644 --- a/ietf/doc/tests.py +++ b/ietf/doc/tests.py @@ -1464,6 +1464,8 @@ def test_document_primary_and_history_views(self): DocumentFactory(type_id='agenda',name='agenda-72-mars') DocumentFactory(type_id='minutes',name='minutes-72-mars') DocumentFactory(type_id='slides',name='slides-72-mars-1-active') + DocumentFactory(type_id="chatlog",name='chatlog-72-mars-197001010000') + DocumentFactory(type_id="polls",name='polls-72-mars-197001010000') statchg = DocumentFactory(type_id='statchg',name='status-change-imaginary-mid-review') statchg.set_state(State.objects.get(type_id='statchg',slug='adrev')) diff --git a/ietf/doc/views_doc.py b/ietf/doc/views_doc.py index 0e9aab8642..89dce2f036 100644 --- a/ietf/doc/views_doc.py +++ b/ietf/doc/views_doc.py @@ -724,13 +724,13 @@ def document_main(request, name, rev=None): assignments=assignments, )) - if doc.type_id == "chatlog": + if doc.type_id in ("chatlog", "polls"): session = doc.sessionpresentation_set.last().session - pathname = Path(session.meeting.get_materials_path()) / "chatlog" / doc.uploaded_filename + pathname = Path(session.meeting.get_materials_path()) / doc.type_id / doc.uploaded_filename content = get_unicode_document_content(doc.name, str(pathname)) return render( request, - "doc/document_chatlog.html", + f"doc/document_{doc.type_id}.html", dict( doc=doc, top=top, diff --git a/ietf/templates/doc/document_polls.html b/ietf/templates/doc/document_polls.html new file mode 100644 index 0000000000..0f683448e2 --- /dev/null +++ b/ietf/templates/doc/document_polls.html @@ -0,0 +1,63 @@ +{% extends "base.html" %} +{# Copyright The IETF Trust 2022, All Rights Reserved #} +{% load origin %} +{% load static %} +{% load ietf_filters textfilters %} +{% block title %}{{ doc.title|default:"Untitled" }}{% endblock %} +{% block content %} + {% origin %} + {{ top|safe }} + {% include "doc/revisions_list.html" %} +
+ {% if doc.rev != latest_rev %} +
The information below is for an old version of the document.
+ {% endif %} + + + + + + + + + + + + + + + + + + + + + + + + +
+ {% if doc.meeting_related %}Meeting{% endif %} + {{ doc.type.name }} + + {% if doc.group %} + {{ doc.group.name }} + ({{ doc.group.acronym }}) + {{ doc.group.type.name }} + {% endif %} + {% if snapshot %}Snapshot{% endif %} +
Title{{ doc.title|default:'(None)' }}
Session + + Materials +
Last updated{{ doc.time|date:"Y-m-d" }}
+
+
{{ doc.name }}-{{ doc.rev }}
+
+ {{ content }} {# TODO #} +
+
+{% endblock %} +{% block js %} + + +{% endblock %} \ No newline at end of file From 85b6485b4a1b044df78a7a3a9b9dcf9f4338d10e Mon Sep 17 00:00:00 2001 From: Nicolas Giard Date: Sat, 8 Oct 2022 02:38:28 +0000 Subject: [PATCH 10/18] feat: chat log vue component + embedded vue loader --- client/Embedded.vue | 41 ++++++++++ client/components/ChatLog.vue | 98 ++++++++++++++++++++++++ client/embedded.js | 13 ++++ ietf/templates/base.html | 1 + ietf/templates/doc/document_chatlog.html | 3 +- vite.config.js | 3 +- 6 files changed, 157 insertions(+), 2 deletions(-) create mode 100644 client/Embedded.vue create mode 100644 client/components/ChatLog.vue create mode 100644 client/embedded.js diff --git a/client/Embedded.vue b/client/Embedded.vue new file mode 100644 index 0000000000..aeaea283d1 --- /dev/null +++ b/client/Embedded.vue @@ -0,0 +1,41 @@ + + + diff --git a/client/components/ChatLog.vue b/client/components/ChatLog.vue new file mode 100644 index 0000000000..779734f919 --- /dev/null +++ b/client/components/ChatLog.vue @@ -0,0 +1,98 @@ + + + + + diff --git a/client/embedded.js b/client/embedded.js new file mode 100644 index 0000000000..f3b01f68f5 --- /dev/null +++ b/client/embedded.js @@ -0,0 +1,13 @@ +import { createApp } from 'vue' +import Embedded from './Embedded.vue' + +// Mount App + +const mountEls = document.querySelectorAll('div.vue-embed') +for (const mnt of mountEls) { + const app = createApp(Embedded, { + componentName: mnt.dataset.component, + componentId: mnt.dataset.componentId + }) + app.mount(mnt) +} diff --git a/ietf/templates/base.html b/ietf/templates/base.html index 1ccef06c9b..537bfc5e05 100644 --- a/ietf/templates/base.html +++ b/ietf/templates/base.html @@ -28,6 +28,7 @@ {% vite_hmr_client %} {% block pagehead %}{% endblock %} + {% vite_asset 'client/embedded.js' %} {% include "base/icons.html" %} {% analytical_head_bottom %} diff --git a/ietf/templates/doc/document_chatlog.html b/ietf/templates/doc/document_chatlog.html index 0f683448e2..1f4d753368 100644 --- a/ietf/templates/doc/document_chatlog.html +++ b/ietf/templates/doc/document_chatlog.html @@ -53,7 +53,8 @@
{{ doc.name }}-{{ doc.rev }}
- {{ content }} {# TODO #} + +
Loading...
{% endblock %} diff --git a/vite.config.js b/vite.config.js index d2c34cfd9c..c7e81bc169 100644 --- a/vite.config.js +++ b/vite.config.js @@ -12,7 +12,8 @@ export default defineConfig(({ command, mode }) => { manifest: true, rollupOptions: { input: { - main: 'client/main.js' + main: 'client/main.js', + embedded: 'client/embedded.js' } } }, From 45ab70913463cfe8777bab93940936de518f342a Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Tue, 11 Oct 2022 15:57:56 -0500 Subject: [PATCH 11/18] feat: render polls using Vue --- client/Embedded.vue | 3 +- client/components/Polls.vue | 79 ++++++++++++++++++++++++++ ietf/templates/doc/document_polls.html | 3 +- 3 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 client/components/Polls.vue diff --git a/client/Embedded.vue b/client/Embedded.vue index aeaea283d1..a0f0d2831e 100644 --- a/client/Embedded.vue +++ b/client/Embedded.vue @@ -13,7 +13,8 @@ import NTheme from './components/n-theme.vue' // COMPONENTS const availableComponents = { - ChatLog: defineAsyncComponent(() => import('./components/ChatLog.vue')) + ChatLog: defineAsyncComponent(() => import('./components/ChatLog.vue')), + Polls: defineAsyncComponent(() => import('./components/Polls.vue')), } // PROPS diff --git a/client/components/Polls.vue b/client/components/Polls.vue new file mode 100644 index 0000000000..c567334564 --- /dev/null +++ b/client/components/Polls.vue @@ -0,0 +1,79 @@ + + + + diff --git a/ietf/templates/doc/document_polls.html b/ietf/templates/doc/document_polls.html index 0f683448e2..0cf13ebc32 100644 --- a/ietf/templates/doc/document_polls.html +++ b/ietf/templates/doc/document_polls.html @@ -53,7 +53,8 @@
{{ doc.name }}-{{ doc.rev }}
- {{ content }} {# TODO #} + +
Loading...
{% endblock %} From a5623a8a741d332585ab1372e5b396efe7f85450 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Tue, 11 Oct 2022 16:11:44 -0500 Subject: [PATCH 12/18] fix: address pug syntax review comments from Nick. --- client/components/Polls.vue | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/components/Polls.vue b/client/components/Polls.vue index c567334564..3fe3140832 100644 --- a/client/components/Polls.vue +++ b/client/components/Polls.vue @@ -2,9 +2,9 @@ .polls n-data-table( v-if='state.items.length > 0' - :data="state.items" - :columns="columns" - striped=true, + :data='state.items' + :columns='columns' + striped ) span.text-muted(v-else) em No chat log available. From e91c82d841b087bc5185f0a03213107d1889e2a0 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Tue, 11 Oct 2022 16:14:00 -0500 Subject: [PATCH 13/18] fix: repair remaining mention of chat log from copymunging --- client/components/Polls.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/components/Polls.vue b/client/components/Polls.vue index 3fe3140832..72c8e1c633 100644 --- a/client/components/Polls.vue +++ b/client/components/Polls.vue @@ -7,7 +7,7 @@ striped ) span.text-muted(v-else) - em No chat log available. + em No polls available. -
Loading...
+
Loading...
{% endblock %} diff --git a/ietf/templates/doc/document_polls.html b/ietf/templates/doc/document_polls.html index 0cf13ebc32..4f5f2531be 100644 --- a/ietf/templates/doc/document_polls.html +++ b/ietf/templates/doc/document_polls.html @@ -54,7 +54,7 @@
{{ doc.name }}-{{ doc.rev }}
-
Loading...
+
Loading...
{% endblock %} From 8704c05e4f6843aa405a67b0d92e0bb593bb41a6 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Wed, 12 Oct 2022 13:35:11 -0500 Subject: [PATCH 15/18] fix: provide missing choices update migration --- .../migrations/0025_chat_and_polls_apikey.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 ietf/person/migrations/0025_chat_and_polls_apikey.py diff --git a/ietf/person/migrations/0025_chat_and_polls_apikey.py b/ietf/person/migrations/0025_chat_and_polls_apikey.py new file mode 100644 index 0000000000..03afdc5999 --- /dev/null +++ b/ietf/person/migrations/0025_chat_and_polls_apikey.py @@ -0,0 +1,17 @@ +# Copyright The IETF Trust 2022, All Rights Reserved + +from django.db import migrations, models + +class Migration(migrations.Migration): + + dependencies = [ + ('person', '0024_pronouns'), + ] + + operations = [ + migrations.AlterField( + model_name='personalapikey', + name='endpoint', + field=models.CharField(choices=[('/api/appauth/authortools', '/api/appauth/authortools'), ('/api/appauth/bibxml', '/api/appauth/bibxml'), ('/api/iesg/position', '/api/iesg/position'), ('/api/meeting/session/video/url', '/api/meeting/session/video/url'), ('/api/notify/meeting/bluesheet', '/api/notify/meeting/bluesheet'), ('/api/notify/meeting/registration', '/api/notify/meeting/registration'), ('/api/notify/session/attendees', '/api/notify/session/attendees'), ('/api/notify/session/chatlog', '/api/notify/session/chatlog'), ('/api/notify/session/polls', '/api/notify/session/polls'), ('/api/v2/person/person', '/api/v2/person/person')], max_length=128), + ), + ] From 313523c9b8f33fb94afaae454e538d8800e72eef Mon Sep 17 00:00:00 2001 From: Nicolas Giard Date: Wed, 12 Oct 2022 20:39:06 +0000 Subject: [PATCH 16/18] test: silence html validator empty attr warnings --- ietf/utils/test_runner.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ietf/utils/test_runner.py b/ietf/utils/test_runner.py index 94c8f770a6..d5ae602b3a 100644 --- a/ietf/utils/test_runner.py +++ b/ietf/utils/test_runner.py @@ -859,9 +859,9 @@ def setup_test_environment(self, **kwargs): # django-bootstrap5 seems to still generate 'checked="checked"', ignore: "attribute-boolean-style": "off", # self-closing style tags are valid in HTML5. Both self-closing and non-self-closing tags are accepted. (vite generates self-closing link tags) - # "void-style": "off", + "void-style": "off", # Both attributes without value and empty strings are equal and valid. (vite generates empty value attributes) - # "attribute-empty-style": "off" + "attribute-empty-style": "off" # For fragments, don't check that elements are in the proper ancestor element "element-required-ancestor": "off", }, From 75f8dd5e9a4dad5e2e18959354e0be341514ea11 Mon Sep 17 00:00:00 2001 From: Nicolas Giard Date: Wed, 12 Oct 2022 20:46:17 +0000 Subject: [PATCH 17/18] test: fix test_runner config --- ietf/utils/test_runner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ietf/utils/test_runner.py b/ietf/utils/test_runner.py index d5ae602b3a..f7790df553 100644 --- a/ietf/utils/test_runner.py +++ b/ietf/utils/test_runner.py @@ -861,7 +861,7 @@ def setup_test_environment(self, **kwargs): # self-closing style tags are valid in HTML5. Both self-closing and non-self-closing tags are accepted. (vite generates self-closing link tags) "void-style": "off", # Both attributes without value and empty strings are equal and valid. (vite generates empty value attributes) - "attribute-empty-style": "off" + "attribute-empty-style": "off", # For fragments, don't check that elements are in the proper ancestor element "element-required-ancestor": "off", }, From 172f68f08a064422e8a5d1be10a15b9181916d48 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Wed, 12 Oct 2022 17:55:14 -0500 Subject: [PATCH 18/18] fix: locate session when looking at a dochistory object for polls or chatlog --- ietf/doc/tests.py | 8 ++++++-- ietf/doc/views_doc.py | 5 ++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/ietf/doc/tests.py b/ietf/doc/tests.py index f05a866e35..e7f8ad21c0 100644 --- a/ietf/doc/tests.py +++ b/ietf/doc/tests.py @@ -1464,8 +1464,10 @@ def test_document_primary_and_history_views(self): DocumentFactory(type_id='agenda',name='agenda-72-mars') DocumentFactory(type_id='minutes',name='minutes-72-mars') DocumentFactory(type_id='slides',name='slides-72-mars-1-active') - DocumentFactory(type_id="chatlog",name='chatlog-72-mars-197001010000') - DocumentFactory(type_id="polls",name='polls-72-mars-197001010000') + chatlog = DocumentFactory(type_id="chatlog",name='chatlog-72-mars-197001010000') + polls = DocumentFactory(type_id="polls",name='polls-72-mars-197001010000') + SessionPresentationFactory(document=chatlog) + SessionPresentationFactory(document=polls) statchg = DocumentFactory(type_id='statchg',name='status-change-imaginary-mid-review') statchg.set_state(State.objects.get(type_id='statchg',slug='adrev')) @@ -1477,6 +1479,8 @@ def test_document_primary_and_history_views(self): "agenda-72-mars", "minutes-72-mars", "slides-72-mars-1-active", + "chatlog-72-mars-197001010000", + "polls-72-mars-197001010000", # TODO: add #"bluesheets-72-mars-1", #"recording-72-mars-1-00", diff --git a/ietf/doc/views_doc.py b/ietf/doc/views_doc.py index 89dce2f036..7475c6b1f7 100644 --- a/ietf/doc/views_doc.py +++ b/ietf/doc/views_doc.py @@ -725,7 +725,10 @@ def document_main(request, name, rev=None): )) if doc.type_id in ("chatlog", "polls"): - session = doc.sessionpresentation_set.last().session + if isinstance(doc,DocHistory): + session = doc.doc.sessionpresentation_set.last().session + else: + session = doc.sessionpresentation_set.last().session pathname = Path(session.meeting.get_materials_path()) / doc.type_id / doc.uploaded_filename content = get_unicode_document_content(doc.name, str(pathname)) return render(