diff --git a/ietf/meeting/tests_views.py b/ietf/meeting/tests_views.py index 47e2334f47..a57fcf63c1 100644 --- a/ietf/meeting/tests_views.py +++ b/ietf/meeting/tests_views.py @@ -1,4 +1,4 @@ -# Copyright The IETF Trust 2009-2020, All Rights Reserved +# Copyright The IETF Trust 2009-2023, All Rights Reserved # -*- coding: utf-8 -*- import datetime import io @@ -6106,21 +6106,21 @@ def test_upload_minutes_agenda(self): test_file = BytesIO(b'this is some text for a test') test_file.name = "not_really.json" - r = self.client.post(url,dict(file=test_file)) + r = self.client.post(url,dict(submission_method="upload",file=test_file)) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertTrue(q('form .is-invalid')) test_file = BytesIO(b'this is some text for a test'*1510000) test_file.name = "not_really.pdf" - r = self.client.post(url,dict(file=test_file)) + r = self.client.post(url,dict(submission_method="upload",file=test_file)) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertTrue(q('form .is-invalid')) test_file = BytesIO(b'') test_file.name = "not_really.html" - r = self.client.post(url,dict(file=test_file)) + r = self.client.post(url,dict(submission_method="upload",file=test_file)) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertTrue(q('form .is-invalid')) @@ -6128,7 +6128,7 @@ def test_upload_minutes_agenda(self): # Test html sanitization test_file = BytesIO(b'Title

Title

Some text
') test_file.name = "some.html" - r = self.client.post(url,dict(file=test_file)) + r = self.client.post(url,dict(submission_method="upload",file=test_file)) self.assertEqual(r.status_code, 302) doc = session.sessionpresentation_set.filter(document__type_id=doctype).first().document self.assertEqual(doc.rev,'00') @@ -6140,7 +6140,7 @@ def test_upload_minutes_agenda(self): # txt upload test_file = BytesIO(b'This is some text for a test, with the word\nvirtual at the beginning of a line.') test_file.name = "some.txt" - r = self.client.post(url,dict(file=test_file,apply_to_all=False)) + r = self.client.post(url,dict(submission_method="upload",file=test_file,apply_to_all=False)) self.assertEqual(r.status_code, 302) doc = session.sessionpresentation_set.filter(document__type_id=doctype).first().document self.assertEqual(doc.rev,'01') @@ -6152,7 +6152,7 @@ def test_upload_minutes_agenda(self): self.assertIn('Revise', str(q("Title"))) test_file = BytesIO(b'this is some different text for a test') test_file.name = "also_some.txt" - r = self.client.post(url,dict(file=test_file,apply_to_all=True)) + r = self.client.post(url,dict(submission_method="upload",file=test_file,apply_to_all=True)) self.assertEqual(r.status_code, 302) doc = Document.objects.get(pk=doc.pk) self.assertEqual(doc.rev,'02') @@ -6161,7 +6161,7 @@ def test_upload_minutes_agenda(self): # Test bad encoding test_file = BytesIO('

Title

Some\x93text
'.encode('latin1')) test_file.name = "some.html" - r = self.client.post(url,dict(file=test_file)) + r = self.client.post(url,dict(submission_method="upload",file=test_file)) self.assertContains(r, 'Could not identify the file encoding') doc = Document.objects.get(pk=doc.pk) self.assertEqual(doc.rev,'02') @@ -6191,7 +6191,7 @@ def test_upload_minutes_agenda_unscheduled(self): test_file = BytesIO(b'this is some text for a test') test_file.name = "not_really.txt" - r = self.client.post(url,dict(file=test_file,apply_to_all=False)) + r = self.client.post(url,dict(submission_method="upload",file=test_file,apply_to_all=False)) self.assertEqual(r.status_code, 410) @override_settings(MEETING_MATERIALS_SERVE_LOCALLY=True) @@ -6211,7 +6211,7 @@ def test_upload_minutes_agenda_interim(self): self.assertFalse(session.sessionpresentation_set.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(file=test_file)) + r = self.client.post(url,dict(submission_method="upload",file=test_file)) self.assertEqual(r.status_code, 302) doc = session.sessionpresentation_set.filter(document__type_id=doctype).first().document self.assertEqual(doc.rev,'00') @@ -6223,6 +6223,46 @@ def test_upload_minutes_agenda_interim(self): 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}) + redirect_url = urlreverse('ietf.meeting.views.session_details', kwargs={'num':session.meeting.number,'acronym':session.group.acronym}) + 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.sessionpresentation_set.exists()) + + test_text = 'Enter agenda from scratch' + r = self.client.post(url,dict(submission_method="enter",content=test_text)) + self.assertRedirects(r, redirect_url) + doc = session.sessionpresentation_set.filter(document__type_id='agenda').first().document + self.assertEqual(doc.rev,'00') + + r = self.client.get(url) + self.assertEqual(r.status_code, 200) + q = PyQuery(r.content) + self.assertIn('Revise', str(q("Title"))) + + test_file = BytesIO(b'Upload after enter') + test_file.name = "some.txt" + r = self.client.post(url,dict(submission_method="upload",file=test_file)) + self.assertRedirects(r, redirect_url) + doc = Document.objects.get(pk=doc.pk) + self.assertEqual(doc.rev,'01') + + r = self.client.get(url) + self.assertEqual(r.status_code, 200) + q = PyQuery(r.content) + self.assertIn('Revise', str(q("Title"))) + + test_text = 'Enter after upload' + r = self.client.post(url,dict(submission_method="enter",content=test_text)) + self.assertRedirects(r, redirect_url) + doc = Document.objects.get(pk=doc.pk) + self.assertEqual(doc.rev,'02') + def test_upload_slides(self): session1 = SessionFactory(meeting__type_id='ietf') diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py index ab39266396..9d07df103e 100644 --- a/ietf/meeting/views.py +++ b/ietf/meeting/views.py @@ -1,4 +1,4 @@ -# Copyright The IETF Trust 2007-2022, All Rights Reserved +# Copyright The IETF Trust 2007-2023, All Rights Reserved # -*- coding: utf-8 -*- @@ -2662,6 +2662,40 @@ def upload_session_minutes(request, session_id, num): }) +class UploadOrEnterAgendaForm(UploadAgendaForm): + ACTIONS = [ + ("upload", "Upload agenda"), + ("enter", "Enter agenda"), + ] + submission_method = forms.ChoiceField(choices=ACTIONS, widget=forms.RadioSelect) + + content = forms.CharField(widget=forms.Textarea, required=False, strip=False, label="Agenda text") + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields["file"].required=False + self.order_fields(["submission_method", "file", "content"]) + + def clean_content(self): + return self.cleaned_data["content"].replace("\r", "") + + def clean_file(self): + submission_method = self.cleaned_data.get("submission_method") + if submission_method == "upload": + return super().clean_file() + return None + + def clean(self): + def require_field(f): + if not self.cleaned_data.get(f): + self.add_error(f, ValidationError("You must fill in this field.")) + + submission_method = self.cleaned_data.get("submission_method") + if submission_method == "upload": + require_field("file") + elif submission_method == "enter": + require_field("content") + def upload_session_agenda(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) @@ -2680,10 +2714,23 @@ def upload_session_agenda(request, session_id, num): agenda_sp = session.sessionpresentation_set.filter(document__type='agenda').first() if request.method == 'POST': - form = UploadAgendaForm(show_apply_to_all_checkbox,request.POST,request.FILES) + form = UploadOrEnterAgendaForm(show_apply_to_all_checkbox,request.POST,request.FILES) if form.is_valid(): - file = request.FILES['file'] - _, ext = os.path.splitext(file.name) + submission_method = form.cleaned_data['submission_method'] + if submission_method == "upload": + file = request.FILES['file'] + _, ext = os.path.splitext(file.name) + else: + if agenda_sp: + doc = agenda_sp.document + _, ext = os.path.splitext(doc.uploaded_filename) + else: + ext = ".md" + fd, name = tempfile.mkstemp(suffix=ext, text=True) + os.close(fd) + with open(name, "w") as file: + file.write(form.cleaned_data['content']) + file = open(name, "rb") apply_to_all = session.type.slug == 'regular' if show_apply_to_all_checkbox: apply_to_all = form.cleaned_data['apply_to_all'] @@ -2738,7 +2785,11 @@ def upload_session_agenda(request, session_id, num): doc.uploaded_filename = filename e = NewRevisionDocEvent.objects.create(doc=doc,by=request.user.person,type='new_revision',desc='New revision available: %s'%doc.rev,rev=doc.rev) # The way this function builds the filename it will never trigger the file delete in handle_file_upload. - save_error = handle_upload_file(file, filename, session.meeting, 'agenda', request=request, encoding=form.file_encoding[file.name]) + try: + encoding=form.file_encoding[file.name] + except AttributeError: + encoding=None + save_error = handle_upload_file(file, filename, session.meeting, 'agenda', request=request, encoding=encoding) if save_error: form.add_error(None, save_error) else: @@ -2746,7 +2797,11 @@ def upload_session_agenda(request, session_id, num): messages.success(request, f'Successfully uploaded agenda as revision {doc.rev}.') return redirect('ietf.meeting.views.session_details',num=num,acronym=session.group.acronym) else: - form = UploadAgendaForm(show_apply_to_all_checkbox, initial={'apply_to_all':session.type_id=='regular'}) + initial={'apply_to_all':session.type_id=='regular', 'submission_method':'upload'} + if agenda_sp: + doc = agenda_sp.document + initial['content'] = doc.text() + form = UploadOrEnterAgendaForm(show_apply_to_all_checkbox, initial=initial) return render(request, "meeting/upload_session_agenda.html", {'session': session, diff --git a/ietf/static/js/upload-session-agenda.js b/ietf/static/js/upload-session-agenda.js new file mode 100644 index 0000000000..b63d460ed8 --- /dev/null +++ b/ietf/static/js/upload-session-agenda.js @@ -0,0 +1,28 @@ +$(document) + .ready(function () { + var form = $("form.my-3"); + + // review submission selection + form.find("[name=submission_method]") + .on("click change", function () { + var val = form.find("[name=submission_method]:checked") + .val(); + + var shouldBeVisible = { + upload: ['[name="file"]'], + enter: ['[name="content"]'] + }; + + for (var v in shouldBeVisible) { + for (var i in shouldBeVisible[v]) { + var selector = shouldBeVisible[v][i]; + var row = form.find(selector).parent(); + if ($.inArray(selector, shouldBeVisible[val]) != -1) + row.show(); + else + row.hide(); + } + } + }) + .trigger("change"); + }); \ No newline at end of file diff --git a/ietf/templates/meeting/upload_session_agenda.html b/ietf/templates/meeting/upload_session_agenda.html index 1856a75bdb..57cba6b53c 100644 --- a/ietf/templates/meeting/upload_session_agenda.html +++ b/ietf/templates/meeting/upload_session_agenda.html @@ -1,5 +1,5 @@ {% extends "base.html" %} -{# Copyright The IETF Trust 2015, All Rights Reserved #} +{# Copyright The IETF Trust 2015-2023, All Rights Reserved #} {% load origin static django_bootstrap5 tz %} {% block title %} {% if agenda_sp %} @@ -29,6 +29,9 @@

Session {{ session_number }} : {{ session.official_timeslotassignment.timesl
{% csrf_token %} {% bootstrap_form form %} - +
+{% endblock %} +{% block js %} + {% endblock %} \ No newline at end of file diff --git a/package.json b/package.json index b6df202e7d..a87d5ac131 100644 --- a/package.json +++ b/package.json @@ -152,6 +152,7 @@ "ietf/static/js/timezone.js", "ietf/static/js/upcoming.js", "ietf/static/js/upload-material.js", + "ietf/static/js/upload-session-agenda.js", "ietf/static/js/upload_bofreq.js", "ietf/static/js/upload_statement.js", "ietf/static/js/zxcvbn.js"