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'TitleTitle
')
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
'.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
+{% 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"