diff --git a/ietf/meeting/forms.py b/ietf/meeting/forms.py index 164f0fd3b..2cec669db 100644 --- a/ietf/meeting/forms.py +++ b/ietf/meeting/forms.py @@ -472,6 +472,9 @@ class ApplyToAllFileUploadForm(FileUploadForm): class UploadMinutesForm(ApplyToAllFileUploadForm): doc_type = 'minutes' +class UploadNarrativeMinutesForm(ApplyToAllFileUploadForm): + doc_type = 'narrativeminutes' + class UploadAgendaForm(ApplyToAllFileUploadForm): doc_type = 'agenda' diff --git a/ietf/meeting/models.py b/ietf/meeting/models.py index 4689495e0..781ced787 100644 --- a/ietf/meeting/models.py +++ b/ietf/meeting/models.py @@ -1147,7 +1147,6 @@ class Session(models.Model): return can_manage_materials(user,self.group) 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() def joint_with_groups_acronyms(self): diff --git a/ietf/meeting/tests_views.py b/ietf/meeting/tests_views.py index e2abcede8..092f8be89 100644 --- a/ietf/meeting/tests_views.py +++ b/ietf/meeting/tests_views.py @@ -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.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): session = SessionFactory(meeting__type_id='ietf') url = urlreverse('ietf.meeting.views.upload_session_agenda',kwargs={'num':session.meeting.number,'session_id':session.id}) diff --git a/ietf/meeting/urls.py b/ietf/meeting/urls.py index 7c79a8025..1c6e49213 100644 --- a/ietf/meeting/urls.py +++ b/ietf/meeting/urls.py @@ -18,6 +18,7 @@ safe_for_all_meeting_types = [ url(r'^session/(?P\d+)/drafts$', views.add_session_drafts), url(r'^session/(?P\d+)/bluesheets$', views.upload_session_bluesheets), url(r'^session/(?P\d+)/minutes$', views.upload_session_minutes), + url(r'^session/(?P\d+)/narrativeminutes$', views.upload_session_narrativeminutes), url(r'^session/(?P\d+)/agenda$', views.upload_session_agenda), url(r'^session/(?P\d+)/import/minutes$', views.import_session_minutes), url(r'^session/(?P\d+)/propose_slides$', views.propose_session_slides), diff --git a/ietf/meeting/utils.py b/ietf/meeting/utils.py index 9fb062b02..6469dbfbb 100644 --- a/ietf/meeting/utils.py +++ b/ietf/meeting/utils.py @@ -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 -*- import datetime import itertools @@ -555,7 +555,7 @@ class SaveMaterialsError(Exception): 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 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() 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: doc = minutes_sp.document 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: raise SessionNotScheduledError if session.meeting.type_id=='ietf': - name = 'minutes-%s-%s' % (session.meeting.number, - session.group.acronym) - title = 'Minutes IETF%s: %s' % (session.meeting.number, - session.group.acronym) + name = f"{document_type.prefix}-{session.meeting.number}-{session.group.acronym}" + title = f"{document_type.name} IETF{session.meeting.number}: {session.group.acronym}" if not apply_to_all: name += '-%s' % (sess_time.strftime("%Y%m%d%H%M"),) title += ': %s' % (sess_time.strftime("%a %H:%M"),) else: - name = 'minutes-%s-%s' % (session.meeting.number, sess_time.strftime("%Y%m%d%H%M")) - title = 'Minutes %s: %s' % (session.meeting.number, sess_time.strftime("%a %H:%M")) + name =f"{document_type.prefix}-{session.meeting.number}-{sess_time.strftime('%Y%m%d%H%M')}" + title = f"{document_type.name} {session.meeting.number}: {sess_time.strftime('%a %H:%M')}" if Document.objects.filter(name=name).exists(): doc = Document.objects.get(name=name) doc.rev = '%02d' % (int(doc.rev)+1) else: doc = Document.objects.create( name = name, - type_id = 'minutes', + type = document_type, title = title, group = session.group, 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(): sp = session.presentations.get(document=doc) sp.rev = doc.rev @@ -611,7 +610,7 @@ def save_session_minutes_revision(session, file, ext, request, encoding=None, ap if apply_to_all: for other_session in get_meeting_sessions(session.meeting.number, session.group.acronym): 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) filename = f'{doc.name}-{doc.rev}{ext}' doc.uploaded_filename = filename @@ -628,7 +627,7 @@ def save_session_minutes_revision(session, file, ext, request, encoding=None, ap file=file, filename=doc.uploaded_filename, meeting=session.meeting, - subdir='minutes', + subdir=document_type.slug, request=request, encoding=encoding, ) diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py index 1171f7b0b..076e2b544 100644 --- a/ietf/meeting/views.py +++ b/ietf/meeting/views.py @@ -102,7 +102,8 @@ from ietf.utils.timezone import datetime_today, date_today from .forms import (InterimMeetingModelForm, InterimAnnounceForm, InterimSessionModelForm, InterimCancelForm, InterimSessionInlineFormSet, RequestMinutesForm, - UploadAgendaForm, UploadBlueSheetForm, UploadMinutesForm, UploadSlidesForm) + UploadAgendaForm, UploadBlueSheetForm, UploadMinutesForm, UploadSlidesForm, + UploadNarrativeMinutesForm) request_summary_exclude_group_types = ['team'] @@ -2662,6 +2663,61 @@ def upload_session_minutes(request, session_id, num): '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): ACTIONS = [ diff --git a/ietf/name/fixtures/names.json b/ietf/name/fixtures/names.json index fc46970f9..e54233eda 100644 --- a/ietf/name/fixtures/names.json +++ b/ietf/name/fixtures/names.json @@ -2578,6 +2578,32 @@ "model": "doc.state", "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": { "label": "State" @@ -2739,6 +2765,13 @@ "model": "doc.statetype", "pk": "minutes" }, + { + "fields": { + "label": "State" + }, + "model": "doc.statetype", + "pk": "narrativeminutes" + }, { "fields": { "label": "State" @@ -10763,6 +10796,17 @@ "model": "name.doctypename", "pk": "minutes" }, + { + "fields": { + "desc": "", + "name": "Narrative Minutes", + "order": 0, + "prefix": "narrative-minutes", + "used": true + }, + "model": "name.doctypename", + "pk": "narrativeminutes" + }, { "fields": { "desc": "", @@ -16734,7 +16778,7 @@ "fields": { "command": "xym", "switch": "--version", - "time": "2023-11-21T08:09:45.989Z", + "time": "2024-02-21T08:06:28.313Z", "used": true, "version": "xym 0.7.0" }, @@ -16745,7 +16789,7 @@ "fields": { "command": "pyang", "switch": "--version", - "time": "2023-11-21T08:09:46.322Z", + "time": "2024-02-21T08:06:28.663Z", "used": true, "version": "pyang 2.6.0" }, @@ -16756,7 +16800,7 @@ "fields": { "command": "yanglint", "switch": "--version", - "time": "2023-11-21T08:09:46.338Z", + "time": "2024-02-21T08:06:28.685Z", "used": true, "version": "yanglint SO 1.9.2" }, @@ -16767,9 +16811,9 @@ "fields": { "command": "xml2rfc", "switch": "--version", - "time": "2023-11-21T08:09:47.251Z", + "time": "2024-02-21T08:06:29.492Z", "used": true, - "version": "xml2rfc 3.18.2" + "version": "xml2rfc 3.19.4" }, "model": "utils.versioninfo", "pk": 4 diff --git a/ietf/settings.py b/ietf/settings.py index 15302fee6..57e6f20bd 100644 --- a/ietf/settings.py +++ b/ietf/settings.py @@ -913,7 +913,7 @@ MEETING_VALID_UPLOAD_EXTENSIONS = { MEETING_VALID_UPLOAD_MIME_TYPES = { 'agenda': ['text/plain', 'text/html', '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': [], 'bluesheets': ['application/pdf', 'text/plain', ], 'procmaterials':['application/pdf', ], diff --git a/ietf/templates/meeting/session_details_panel.html b/ietf/templates/meeting/session_details_panel.html index 0e3005018..8cad89179 100644 --- a/ietf/templates/meeting/session_details_panel.html +++ b/ietf/templates/meeting/session_details_panel.html @@ -73,6 +73,8 @@ {% if user|has_role:"Secretariat" or can_manage_materials %} {% if pres.document.type.slug == 'minutes' %} {% 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' %} {% url 'ietf.meeting.views.upload_session_agenda' session_id=session.pk num=session.meeting.number as upload_url %} {% else %} @@ -106,6 +108,12 @@ Upload minutes {% endif %} + {% if not session.type_counter.narrativeminutes and session.group.acronym == "iesg" %} + + Upload narrative minutes + + {% 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 narrativeminutes_sp %} + Revise + {% else %} + Upload + {% endif %} + Narrative Minutes for {{ session.meeting }} +
+ {{ session.group.acronym }} + {% if session.name %}: {{ session.name }}{% endif %} + + + {% if session_number %} +

Session {{ session_number }} : {{ session.official_timeslotassignment.timeslot.time|timezone:session.meeting.time_zone|date:"D M-d-Y Hi" }}

+ {% endif %} +
+ {% csrf_token %} + {% bootstrap_form form %} + +
+{% endblock %} \ No newline at end of file