feat: upload narrative minutes (#7125)

* feat: upload narrative minutes

* chore: cover other new URL path
This commit is contained in:
Robert Sparks 2024-03-04 16:48:02 -06:00 committed by GitHub
parent aaf402fd1f
commit 7287e98709
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 192 additions and 21 deletions

View file

@ -472,6 +472,9 @@ class ApplyToAllFileUploadForm(FileUploadForm):
class UploadMinutesForm(ApplyToAllFileUploadForm): class UploadMinutesForm(ApplyToAllFileUploadForm):
doc_type = 'minutes' doc_type = 'minutes'
class UploadNarrativeMinutesForm(ApplyToAllFileUploadForm):
doc_type = 'narrativeminutes'
class UploadAgendaForm(ApplyToAllFileUploadForm): class UploadAgendaForm(ApplyToAllFileUploadForm):
doc_type = 'agenda' doc_type = 'agenda'

View file

@ -1147,7 +1147,6 @@ class Session(models.Model):
return can_manage_materials(user,self.group) return can_manage_materials(user,self.group)
def is_material_submission_cutoff(self): def is_material_submission_cutoff(self):
debug.say("is_material_submission_cutoff got called")
return date_today(datetime.timezone.utc) > self.meeting.get_submission_correction_date() return date_today(datetime.timezone.utc) > self.meeting.get_submission_correction_date()
def joint_with_groups_acronyms(self): def joint_with_groups_acronyms(self):

View file

@ -6233,6 +6233,33 @@ class MaterialsTests(TestCase):
self.requests_mock.get(f'{session.notes_url()}/info', text=json.dumps({'title': 'title', 'updatetime': '2021-12-01T17:11:00z'})) self.requests_mock.get(f'{session.notes_url()}/info', text=json.dumps({'title': 'title', 'updatetime': '2021-12-01T17:11:00z'}))
self.crawl_materials(url=url, top=top) self.crawl_materials(url=url, top=top)
@override_settings(MEETING_MATERIALS_SERVE_LOCALLY=True)
def test_upload_narrativeminutes(self):
for type_id in ["interim","ietf"]:
session=SessionFactory(meeting__type_id=type_id,group__acronym='iesg')
doctype='narrativeminutes'
url = urlreverse('ietf.meeting.views.upload_session_narrativeminutes',kwargs={'num':session.meeting.number,'session_id':session.id})
self.client.logout()
login_testing_unauthorized(self,"secretary",url)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertIn('Upload', str(q("title")))
self.assertFalse(session.presentations.filter(document__type_id=doctype))
test_file = BytesIO(b'this is some text for a test')
test_file.name = "not_really.txt"
r = self.client.post(url,dict(submission_method="upload",file=test_file))
self.assertEqual(r.status_code, 302)
doc = session.presentations.filter(document__type_id=doctype).first().document
self.assertEqual(doc.rev,'00')
# Verify that we don't have dead links
url = urlreverse('ietf.meeting.views.session_details', kwargs={'num':session.meeting.number, 'acronym': session.group.acronym})
top = '/meeting/%s/' % session.meeting.number
self.requests_mock.get(f'{session.notes_url()}/download', text='markdown notes')
self.requests_mock.get(f'{session.notes_url()}/info', text=json.dumps({'title': 'title', 'updatetime': '2021-12-01T17:11:00z'}))
self.crawl_materials(url=url, top=top)
def test_enter_agenda(self): def test_enter_agenda(self):
session = SessionFactory(meeting__type_id='ietf') session = SessionFactory(meeting__type_id='ietf')
url = urlreverse('ietf.meeting.views.upload_session_agenda',kwargs={'num':session.meeting.number,'session_id':session.id}) url = urlreverse('ietf.meeting.views.upload_session_agenda',kwargs={'num':session.meeting.number,'session_id':session.id})

View file

@ -18,6 +18,7 @@ safe_for_all_meeting_types = [
url(r'^session/(?P<session_id>\d+)/drafts$', views.add_session_drafts), url(r'^session/(?P<session_id>\d+)/drafts$', views.add_session_drafts),
url(r'^session/(?P<session_id>\d+)/bluesheets$', views.upload_session_bluesheets), url(r'^session/(?P<session_id>\d+)/bluesheets$', views.upload_session_bluesheets),
url(r'^session/(?P<session_id>\d+)/minutes$', views.upload_session_minutes), url(r'^session/(?P<session_id>\d+)/minutes$', views.upload_session_minutes),
url(r'^session/(?P<session_id>\d+)/narrativeminutes$', views.upload_session_narrativeminutes),
url(r'^session/(?P<session_id>\d+)/agenda$', views.upload_session_agenda), url(r'^session/(?P<session_id>\d+)/agenda$', views.upload_session_agenda),
url(r'^session/(?P<session_id>\d+)/import/minutes$', views.import_session_minutes), url(r'^session/(?P<session_id>\d+)/import/minutes$', views.import_session_minutes),
url(r'^session/(?P<session_id>\d+)/propose_slides$', views.propose_session_slides), url(r'^session/(?P<session_id>\d+)/propose_slides$', views.propose_session_slides),

View file

@ -1,4 +1,4 @@
# Copyright The IETF Trust 2016-2020, All Rights Reserved # Copyright The IETF Trust 2016-2024, All Rights Reserved
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import datetime import datetime
import itertools import itertools
@ -555,7 +555,7 @@ class SaveMaterialsError(Exception):
pass pass
def save_session_minutes_revision(session, file, ext, request, encoding=None, apply_to_all=False): def save_session_minutes_revision(session, file, ext, request, encoding=None, apply_to_all=False, narrative=False):
"""Creates or updates session minutes records """Creates or updates session minutes records
This updates the database models to reflect a new version. It does not handle This updates the database models to reflect a new version. It does not handle
@ -568,7 +568,8 @@ def save_session_minutes_revision(session, file, ext, request, encoding=None, ap
Returns (Document, [DocEvents]), which should be passed to doc.save_with_history() Returns (Document, [DocEvents]), which should be passed to doc.save_with_history()
if the file contents are stored successfully. if the file contents are stored successfully.
""" """
minutes_sp = session.presentations.filter(document__type='minutes').first() document_type = DocTypeName.objects.get(slug= 'narrativeminutes' if narrative else 'minutes')
minutes_sp = session.presentations.filter(document__type=document_type).first()
if minutes_sp: if minutes_sp:
doc = minutes_sp.document doc = minutes_sp.document
doc.rev = '%02d' % (int(doc.rev)+1) doc.rev = '%02d' % (int(doc.rev)+1)
@ -580,28 +581,26 @@ def save_session_minutes_revision(session, file, ext, request, encoding=None, ap
if not sess_time: if not sess_time:
raise SessionNotScheduledError raise SessionNotScheduledError
if session.meeting.type_id=='ietf': if session.meeting.type_id=='ietf':
name = 'minutes-%s-%s' % (session.meeting.number, name = f"{document_type.prefix}-{session.meeting.number}-{session.group.acronym}"
session.group.acronym) title = f"{document_type.name} IETF{session.meeting.number}: {session.group.acronym}"
title = 'Minutes IETF%s: %s' % (session.meeting.number,
session.group.acronym)
if not apply_to_all: if not apply_to_all:
name += '-%s' % (sess_time.strftime("%Y%m%d%H%M"),) name += '-%s' % (sess_time.strftime("%Y%m%d%H%M"),)
title += ': %s' % (sess_time.strftime("%a %H:%M"),) title += ': %s' % (sess_time.strftime("%a %H:%M"),)
else: else:
name = 'minutes-%s-%s' % (session.meeting.number, sess_time.strftime("%Y%m%d%H%M")) name =f"{document_type.prefix}-{session.meeting.number}-{sess_time.strftime('%Y%m%d%H%M')}"
title = 'Minutes %s: %s' % (session.meeting.number, sess_time.strftime("%a %H:%M")) title = f"{document_type.name} {session.meeting.number}: {sess_time.strftime('%a %H:%M')}"
if Document.objects.filter(name=name).exists(): if Document.objects.filter(name=name).exists():
doc = Document.objects.get(name=name) doc = Document.objects.get(name=name)
doc.rev = '%02d' % (int(doc.rev)+1) doc.rev = '%02d' % (int(doc.rev)+1)
else: else:
doc = Document.objects.create( doc = Document.objects.create(
name = name, name = name,
type_id = 'minutes', type = document_type,
title = title, title = title,
group = session.group, group = session.group,
rev = '00', rev = '00',
) )
doc.states.add(State.objects.get(type_id='minutes',slug='active')) doc.states.add(State.objects.get(type_id=document_type.slug,slug='active'))
if session.presentations.filter(document=doc).exists(): if session.presentations.filter(document=doc).exists():
sp = session.presentations.get(document=doc) sp = session.presentations.get(document=doc)
sp.rev = doc.rev sp.rev = doc.rev
@ -611,7 +610,7 @@ def save_session_minutes_revision(session, file, ext, request, encoding=None, ap
if apply_to_all: if apply_to_all:
for other_session in get_meeting_sessions(session.meeting.number, session.group.acronym): for other_session in get_meeting_sessions(session.meeting.number, session.group.acronym):
if other_session != session: if other_session != session:
other_session.presentations.filter(document__type='minutes').delete() other_session.presentations.filter(document__type=document_type).delete()
other_session.presentations.create(document=doc,rev=doc.rev) other_session.presentations.create(document=doc,rev=doc.rev)
filename = f'{doc.name}-{doc.rev}{ext}' filename = f'{doc.name}-{doc.rev}{ext}'
doc.uploaded_filename = filename doc.uploaded_filename = filename
@ -628,7 +627,7 @@ def save_session_minutes_revision(session, file, ext, request, encoding=None, ap
file=file, file=file,
filename=doc.uploaded_filename, filename=doc.uploaded_filename,
meeting=session.meeting, meeting=session.meeting,
subdir='minutes', subdir=document_type.slug,
request=request, request=request,
encoding=encoding, encoding=encoding,
) )

View file

@ -102,7 +102,8 @@ from ietf.utils.timezone import datetime_today, date_today
from .forms import (InterimMeetingModelForm, InterimAnnounceForm, InterimSessionModelForm, from .forms import (InterimMeetingModelForm, InterimAnnounceForm, InterimSessionModelForm,
InterimCancelForm, InterimSessionInlineFormSet, RequestMinutesForm, InterimCancelForm, InterimSessionInlineFormSet, RequestMinutesForm,
UploadAgendaForm, UploadBlueSheetForm, UploadMinutesForm, UploadSlidesForm) UploadAgendaForm, UploadBlueSheetForm, UploadMinutesForm, UploadSlidesForm,
UploadNarrativeMinutesForm)
request_summary_exclude_group_types = ['team'] request_summary_exclude_group_types = ['team']
@ -2662,6 +2663,61 @@ def upload_session_minutes(request, session_id, num):
'form': form, 'form': form,
}) })
@role_required("Secretariat")
def upload_session_narrativeminutes(request, session_id, num):
# num is redundant, but we're dragging it along an artifact of where we are in the current URL structure
session = get_object_or_404(Session,pk=session_id)
if session.group.acronym != "iesg":
raise Http404()
session_number = None
sessions = get_sessions(session.meeting.number,session.group.acronym)
show_apply_to_all_checkbox = len(sessions) > 1 if session.type_id == 'regular' else False
if len(sessions) > 1:
session_number = 1 + sessions.index(session)
narrativeminutes_sp = session.presentations.filter(document__type='narrativeminutes').first()
if request.method == 'POST':
form = UploadNarrativeMinutesForm(show_apply_to_all_checkbox,request.POST,request.FILES)
if form.is_valid():
file = request.FILES['file']
_, ext = os.path.splitext(file.name)
apply_to_all = session.type_id == 'regular'
if show_apply_to_all_checkbox:
apply_to_all = form.cleaned_data['apply_to_all']
# Set up the new revision
try:
save_session_minutes_revision(
session=session,
apply_to_all=apply_to_all,
file=file,
ext=ext,
encoding=form.file_encoding[file.name],
request=request,
narrative=True
)
except SessionNotScheduledError:
return HttpResponseGone(
"Cannot receive uploads for an unscheduled session. Please check the session ID.",
content_type="text/plain",
)
except SaveMaterialsError as err:
form.add_error(None, str(err))
else:
# no exception -- success!
messages.success(request, f'Successfully uploaded narrative minutes as revision {session.narrative_minutes().rev}.')
return redirect('ietf.meeting.views.session_details', num=num, acronym=session.group.acronym)
else:
form = UploadMinutesForm(show_apply_to_all_checkbox)
return render(request, "meeting/upload_session_narrativeminutes.html",
{'session': session,
'session_number': session_number,
'minutes_sp' : narrativeminutes_sp,
'form': form,
})
class UploadOrEnterAgendaForm(UploadAgendaForm): class UploadOrEnterAgendaForm(UploadAgendaForm):
ACTIONS = [ ACTIONS = [

View file

@ -2578,6 +2578,32 @@
"model": "doc.state", "model": "doc.state",
"pk": 177 "pk": 177
}, },
{
"fields": {
"desc": "",
"name": "Active",
"next_states": [],
"order": 0,
"slug": "active",
"type": "narrativeminutes",
"used": true
},
"model": "doc.state",
"pk": 178
},
{
"fields": {
"desc": "",
"name": "Deleted",
"next_states": [],
"order": 1,
"slug": "deleted",
"type": "narrativeminutes",
"used": true
},
"model": "doc.state",
"pk": 179
},
{ {
"fields": { "fields": {
"label": "State" "label": "State"
@ -2739,6 +2765,13 @@
"model": "doc.statetype", "model": "doc.statetype",
"pk": "minutes" "pk": "minutes"
}, },
{
"fields": {
"label": "State"
},
"model": "doc.statetype",
"pk": "narrativeminutes"
},
{ {
"fields": { "fields": {
"label": "State" "label": "State"
@ -10763,6 +10796,17 @@
"model": "name.doctypename", "model": "name.doctypename",
"pk": "minutes" "pk": "minutes"
}, },
{
"fields": {
"desc": "",
"name": "Narrative Minutes",
"order": 0,
"prefix": "narrative-minutes",
"used": true
},
"model": "name.doctypename",
"pk": "narrativeminutes"
},
{ {
"fields": { "fields": {
"desc": "", "desc": "",
@ -16734,7 +16778,7 @@
"fields": { "fields": {
"command": "xym", "command": "xym",
"switch": "--version", "switch": "--version",
"time": "2023-11-21T08:09:45.989Z", "time": "2024-02-21T08:06:28.313Z",
"used": true, "used": true,
"version": "xym 0.7.0" "version": "xym 0.7.0"
}, },
@ -16745,7 +16789,7 @@
"fields": { "fields": {
"command": "pyang", "command": "pyang",
"switch": "--version", "switch": "--version",
"time": "2023-11-21T08:09:46.322Z", "time": "2024-02-21T08:06:28.663Z",
"used": true, "used": true,
"version": "pyang 2.6.0" "version": "pyang 2.6.0"
}, },
@ -16756,7 +16800,7 @@
"fields": { "fields": {
"command": "yanglint", "command": "yanglint",
"switch": "--version", "switch": "--version",
"time": "2023-11-21T08:09:46.338Z", "time": "2024-02-21T08:06:28.685Z",
"used": true, "used": true,
"version": "yanglint SO 1.9.2" "version": "yanglint SO 1.9.2"
}, },
@ -16767,9 +16811,9 @@
"fields": { "fields": {
"command": "xml2rfc", "command": "xml2rfc",
"switch": "--version", "switch": "--version",
"time": "2023-11-21T08:09:47.251Z", "time": "2024-02-21T08:06:29.492Z",
"used": true, "used": true,
"version": "xml2rfc 3.18.2" "version": "xml2rfc 3.19.4"
}, },
"model": "utils.versioninfo", "model": "utils.versioninfo",
"pk": 4 "pk": 4

View file

@ -913,7 +913,7 @@ MEETING_VALID_UPLOAD_EXTENSIONS = {
MEETING_VALID_UPLOAD_MIME_TYPES = { MEETING_VALID_UPLOAD_MIME_TYPES = {
'agenda': ['text/plain', 'text/html', 'text/markdown', 'text/x-markdown', ], 'agenda': ['text/plain', 'text/html', 'text/markdown', 'text/x-markdown', ],
'minutes': ['text/plain', 'text/html', 'application/pdf', 'text/markdown', 'text/x-markdown', ], 'minutes': ['text/plain', 'text/html', 'application/pdf', 'text/markdown', 'text/x-markdown', ],
'narrative-minutes': ['text/plain', 'text/html', 'application/pdf', 'text/markdown', 'text/x-markdown', ], 'narrativeminutes': ['text/plain', 'text/html', 'application/pdf', 'text/markdown', 'text/x-markdown', ],
'slides': [], 'slides': [],
'bluesheets': ['application/pdf', 'text/plain', ], 'bluesheets': ['application/pdf', 'text/plain', ],
'procmaterials':['application/pdf', ], 'procmaterials':['application/pdf', ],

View file

@ -73,6 +73,8 @@
{% if user|has_role:"Secretariat" or can_manage_materials %} {% if user|has_role:"Secretariat" or can_manage_materials %}
{% if pres.document.type.slug == 'minutes' %} {% if pres.document.type.slug == 'minutes' %}
{% url 'ietf.meeting.views.upload_session_minutes' session_id=session.pk num=session.meeting.number as upload_url %} {% url 'ietf.meeting.views.upload_session_minutes' session_id=session.pk num=session.meeting.number as upload_url %}
{% elif pres.document.type.slug == 'narrativeminutes' %}
{% url 'ietf.meeting.views.upload_session_narrativeminutes' session_id=session.pk num=session.meeting.number as upload_url %}
{% elif pres.document.type.slug == 'agenda' %} {% elif pres.document.type.slug == 'agenda' %}
{% url 'ietf.meeting.views.upload_session_agenda' session_id=session.pk num=session.meeting.number as upload_url %} {% url 'ietf.meeting.views.upload_session_agenda' session_id=session.pk num=session.meeting.number as upload_url %}
{% else %} {% else %}
@ -106,6 +108,12 @@
Upload minutes Upload minutes
</a> </a>
{% endif %} {% endif %}
{% if not session.type_counter.narrativeminutes and session.group.acronym == "iesg" %}
<a class="btn btn-primary"
href="{% url 'ietf.meeting.views.upload_session_narrativeminutes' session_id=session.pk num=session.meeting.number %}">
Upload narrative minutes
</a>
{% endif %}
{% endif %} {% endif %}
{% if user|has_role:"Secretariat" and not session.type_counter.bluesheets or meeting.type.slug == 'interim' and can_manage_materials and not session.type_counter.bluesheets %} {% if user|has_role:"Secretariat" and not session.type_counter.bluesheets or meeting.type.slug == 'interim' and can_manage_materials and not session.type_counter.bluesheets %}
<a class="btn btn-primary" <a class="btn btn-primary"

View file

@ -0,0 +1,34 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2024, All Rights Reserved #}
{% load origin static django_bootstrap5 tz %}
{% block title %}
{% if narrativeminutes_sp %}
Revise
{% else %}
Upload
{% endif %}
Narrative Minutes for {{ session.meeting }} : {{ session.group.acronym }}
{% endblock %}
{% block content %}
{% origin %}
<h1>
{% if narrativeminutes_sp %}
Revise
{% else %}
Upload
{% endif %}
Narrative Minutes for {{ session.meeting }}
<br>
<small class="text-body-secondary">{{ session.group.acronym }}
{% if session.name %}: {{ session.name }}{% endif %}
</small>
</h1>
{% if session_number %}
<h2>Session {{ session_number }} : {{ session.official_timeslotassignment.timeslot.time|timezone:session.meeting.time_zone|date:"D M-d-Y Hi" }}</h2>
{% endif %}
<form enctype="multipart/form-data" method="post" class="my-3">
{% csrf_token %}
{% bootstrap_form form %}
<button type="submit" class="btn btn-primary">Upload</button>
</form>
{% endblock %}