diff --git a/client/Embedded.vue b/client/Embedded.vue new file mode 100644 index 000000000..a0f0d2831 --- /dev/null +++ b/client/Embedded.vue @@ -0,0 +1,42 @@ + +n-theme + n-message-provider + component(:is='currentComponent', :component-id='props.componentId') + + + diff --git a/client/components/ChatLog.vue b/client/components/ChatLog.vue new file mode 100644 index 000000000..779734f91 --- /dev/null +++ b/client/components/ChatLog.vue @@ -0,0 +1,98 @@ + +.chatlog + n-timeline( + v-if='state.items.length > 0' + :icon-size='18' + size='large' + ) + n-timeline-item( + v-for='item of state.items' + :key='item.id' + type='default' + :color='item.color' + :title='item.author' + :time='item.time' + ) + template(#default) + div(v-html='item.text') + span.text-muted(v-else) + em No chat log available. + + + + + diff --git a/client/components/Polls.vue b/client/components/Polls.vue new file mode 100644 index 000000000..72c8e1c63 --- /dev/null +++ b/client/components/Polls.vue @@ -0,0 +1,79 @@ + +.polls + n-data-table( + v-if='state.items.length > 0' + :data='state.items' + :columns='columns' + striped + ) + span.text-muted(v-else) + em No polls available. + + + + diff --git a/client/embedded.js b/client/embedded.js new file mode 100644 index 000000000..f3b01f68f --- /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/api/tests.py b/ietf/api/tests.py index cee760a33..ab565a917 100644 --- a/ietf/api/tests.py +++ b/ietf/api/tests.py @@ -9,6 +9,7 @@ import sys 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 @@ from tastypie.test import ResourceTestCaseMixin 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,93 @@ class CustomApiTests(TestCase): self.assertTrue(session.attended_set.filter(person=recman).exists()) self.assertTrue(session.attended_set.filter(person=otherperson).exists()) + 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() + + badrole = RoleFactory(group__type_id='ietf', name_id='ad') + badrole.person.user.last_login = timezone.now() + badrole.person.user.save() + + meeting = MeetingFactory(type_id='ietf') + session = SessionFactory(group__type_id='wg', meeting=meeting) + + 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 + } + ]""" + ), + ): + 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') recmanrole = RoleFactory(group__type_id='ietf', name_id='recman') diff --git a/ietf/api/urls.py b/ietf/api/urls.py index ca84870e2..714be8a6a 100644 --- a/ietf/api/urls.py +++ b/ietf/api/urls.py @@ -37,6 +37,10 @@ urlpatterns = [ 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 + 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/doc/migrations/0045_docstates_chatlogs_polls.py b/ietf/doc/migrations/0045_docstates_chatlogs_polls.py new file mode 100644 index 000000000..044cc60de --- /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, schema_editor): + StateType = apps.get_model("doc", "StateType") + State = apps.get_model("doc", "State") + for slug in ("chatlog", "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, schema_editor): + StateType = apps.get_model("doc", "StateType") + State = apps.get_model("doc", "State") + State.objects.filter(type_id__in=("chatlog", "polls")).delete() + StateType.objects.filter(slug__in=("chatlog", "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/doc/models.py b/ietf/doc/models.py index 23a2403bf..6512d4b54 100644 --- a/ietf/doc/models.py +++ b/ietf/doc/models.py @@ -136,7 +136,7 @@ class DocumentInfo(models.Model): 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 @@ class DocumentInfo(models.Model): 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/tests.py b/ietf/doc/tests.py index e01b5e0bd..e7f8ad21c 100644 --- a/ietf/doc/tests.py +++ b/ietf/doc/tests.py @@ -1464,6 +1464,10 @@ Man Expires September 22, 2015 [Page 3] 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') + 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')) @@ -1475,6 +1479,8 @@ Man Expires September 22, 2015 [Page 3] "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 d34ac4a59..7475c6b1f 100644 --- a/ietf/doc/views_doc.py +++ b/ietf/doc/views_doc.py @@ -42,6 +42,7 @@ import os 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,29 @@ def document_main(request, name, rev=None): assignments=assignments, )) + if doc.type_id in ("chatlog", "polls"): + 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( + request, + f"doc/document_{doc.type_id}.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/utils.py b/ietf/meeting/utils.py index 0d1a56270..aae286682 100644 --- a/ietf/meeting/utils.py +++ b/ietf/meeting/utils.py @@ -21,7 +21,7 @@ from ietf.meeting.models import Session, SchedulingEvent, TimeSlot, Constraint, 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,35 @@ 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() + 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')}" + 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 f5a06d6fb..a50ba1ad9 100644 --- a/ietf/meeting/views.py +++ b/ietf/meeting/views.py @@ -81,6 +81,7 @@ from ietf.meeting.utils import preprocess_constraints_for_meeting_schedule_edito 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, @@ -2310,6 +2311,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'] @@ -3820,6 +3822,85 @@ def api_add_session_attendees(request): 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') +@csrf_exempt +def api_upload_chatlog(request): + def err(code, text): + return HttpResponse(text, status=code, content_type='text/plain') + 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: + apidata = json.loads(apidata_post) + except json.decoder.JSONDecodeError: + return err(400, "Malformed post") + 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 ( '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: + 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) + 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, 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') +@csrf_exempt +def api_upload_polls(request): + def err(code, text): + return HttpResponse(text, status=code, content_type='text/plain') + 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: + apidata = json.loads(apidata_post) + except json.decoder.JSONDecodeError: + return err(400, "Malformed post") + 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['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: + 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) + 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, 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') @require_api_key @role_required('Recording Manager', 'Secretariat') diff --git a/ietf/name/fixtures/names.json b/ietf/name/fixtures/names.json index 41f7fec62..f6a3a7d7c 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,58 @@ "model": "doc.state", "pk": 164 }, + { + "fields": { + "desc": "", + "name": "Active", + "next_states": [], + "order": 0, + "slug": "active", + "type": "chatlog", + "used": true + }, + "model": "doc.state", + "pk": 165 + }, + { + "fields": { + "desc": "", + "name": "Deleted", + "next_states": [], + "order": 0, + "slug": "deleted", + "type": "chatlog", + "used": true + }, + "model": "doc.state", + "pk": 166 + }, + { + "fields": { + "desc": "", + "name": "Active", + "next_states": [], + "order": 0, + "slug": "active", + "type": "polls", + "used": true + }, + "model": "doc.state", + "pk": 167 + }, + { + "fields": { + "desc": "", + "name": "Deleted", + "next_states": [], + "order": 0, + "slug": "deleted", + "type": "polls", + "used": true + }, + "model": "doc.state", + "pk": 168 + }, { "fields": { "label": "State" @@ -2433,6 +2485,13 @@ "model": "doc.statetype", "pk": "charter" }, + { + "fields": { + "label": "State" + }, + "model": "doc.statetype", + "pk": "chatlog" + }, { "fields": { "label": "Conflict Review State" @@ -2538,6 +2597,13 @@ "model": "doc.statetype", "pk": "minutes" }, + { + "fields": { + "label": "State" + }, + "model": "doc.statetype", + "pk": "polls" + }, { "fields": { "label": "Proceedings Materials State" @@ -3345,7 +3411,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 +3524,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 +3535,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 +10192,17 @@ "model": "name.doctypename", "pk": "charter" }, + { + "fields": { + "desc": "", + "name": "Chat Log", + "order": 0, + "prefix": "chatlog", + "used": true + }, + "model": "name.doctypename", + "pk": "chatlog" + }, { "fields": { "desc": "", @@ -10181,6 +10258,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 +16086,7 @@ "fields": { "command": "xym", "switch": "--version", - "time": "2022-07-13T00:09:29.108", + "time": "2022-09-22T00:09:27.552", "used": true, "version": "xym 0.5" }, @@ -16009,7 +16097,7 @@ "fields": { "command": "pyang", "switch": "--version", - "time": "2022-07-13T00:09:29.475", + "time": "2022-09-22T00:09:27.867", "used": true, "version": "pyang 2.5.3" }, @@ -16020,7 +16108,7 @@ "fields": { "command": "yanglint", "switch": "--version", - "time": "2022-07-13T00:09:29.497", + "time": "2022-09-22T00:09:27.886", "used": true, "version": "yanglint SO 1.9.2" }, @@ -16031,9 +16119,9 @@ "fields": { "command": "xml2rfc", "switch": "--version", - "time": "2022-07-13T00:09:30.513", + "time": "2022-09-22T00:09:28.809", "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 new file mode 100644 index 000000000..1014a9dce --- /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 = "chatlog", + name = "Chat Log", + prefix = "chatlog", + 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=("chatlog", "polls")).delete() + +class Migration(migrations.Migration): + + dependencies = [ + ('name', '0044_validating_draftsubmissionstatename'), + ] + + operations = [ + migrations.RunPython(forward, reverse) + ] 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 000000000..03afdc599 --- /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), + ), + ] diff --git a/ietf/person/models.py b/ietf/person/models.py index 143b8dd08..c4401b417 100644 --- a/ietf/person/models.py +++ b/ietf/person/models.py @@ -368,7 +368,9 @@ PERSON_API_KEY_VALUES = [ ("/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), ] diff --git a/ietf/settings.py b/ietf/settings.py index aed1f75c7..5e4912d85 100644 --- a/ietf/settings.py +++ b/ietf/settings.py @@ -889,6 +889,8 @@ MEETING_DOC_LOCAL_HREFS = { "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/base.html b/ietf/templates/base.html index ed064a535..10e999112 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 new file mode 100644 index 000000000..1e88810fe --- /dev/null +++ b/ietf/templates/doc/document_chatlog.html @@ -0,0 +1,64 @@ +{% 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 %} ++ {% 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" }} | +
+ {% 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" }} | +
+ {{ pres.document.title }} + ({{ pres.document.name }}) + | +