From a2ac0c9094dd8d8e165bf3fb0b73f14270f33a51 Mon Sep 17 00:00:00 2001 From: Jim Fenton Date: Sat, 15 Mar 2025 17:00:21 +0700 Subject: [PATCH 1/6] Warn if uploading minutes before sessionn end --- ietf/meeting/views.py | 2 ++ ietf/templates/meeting/session_details_panel.html | 2 +- ietf/templates/meeting/upload_session_minutes.html | 5 +++++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py index 3fa605ed7e..02b8f2ee5e 100644 --- a/ietf/meeting/views.py +++ b/ietf/meeting/views.py @@ -2526,6 +2526,7 @@ def session_details(request, num, acronym): 'can_manage_materials' : can_manage, 'can_view_request': can_view_request, 'thisweek': datetime_today()-datetime.timedelta(days=7), + 'future': timezone.now() < session.official_timeslotassignment().timeslot.end_time(), }) class SessionDraftsForm(forms.Form): @@ -2822,6 +2823,7 @@ def upload_session_minutes(request, session_id, num): 'session_number': session_number, 'minutes_sp' : minutes_sp, 'form': form, + 'future': timezone.now() < session.official_timeslotassignment().timeslot.end_time(), }) @role_required("Secretariat") diff --git a/ietf/templates/meeting/session_details_panel.html b/ietf/templates/meeting/session_details_panel.html index 9b7a192f05..545222f307 100644 --- a/ietf/templates/meeting/session_details_panel.html +++ b/ietf/templates/meeting/session_details_panel.html @@ -109,7 +109,7 @@

Agenda, Minutes, and Bluesheets

{% endif %} {% if not session.type_counter.minutes %} - Import minutes from notes.ietf.org + Import minutes from notes.ietf.org Upload minutes diff --git a/ietf/templates/meeting/upload_session_minutes.html b/ietf/templates/meeting/upload_session_minutes.html index 30eadda277..324440681f 100644 --- a/ietf/templates/meeting/upload_session_minutes.html +++ b/ietf/templates/meeting/upload_session_minutes.html @@ -26,6 +26,11 @@

{% if session_number %}

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

{% endif %} + {% if future %} +

+ Caution: Session has not ended yet +

+ {% endif %}
{% csrf_token %} {% bootstrap_form form %} From cc9fa4fcec9084ebbbb632a65e94bbda755cdca5 Mon Sep 17 00:00:00 2001 From: Jim Fenton Date: Wed, 19 Mar 2025 13:39:13 +0700 Subject: [PATCH 2/6] Remove extraneous btn-primary for session future Co-authored-by: Robert Sparks --- ietf/templates/meeting/session_details_panel.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ietf/templates/meeting/session_details_panel.html b/ietf/templates/meeting/session_details_panel.html index 545222f307..75bae76376 100644 --- a/ietf/templates/meeting/session_details_panel.html +++ b/ietf/templates/meeting/session_details_panel.html @@ -109,7 +109,7 @@

Agenda, Minutes, and Bluesheets

{% endif %} {% if not session.type_counter.minutes %} - Import minutes from notes.ietf.org + Import minutes from notes.ietf.org Upload minutes From 5b03dba4c8d764002649a2697e3419f3146ac2e6 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Thu, 27 Mar 2025 10:42:23 -0500 Subject: [PATCH 3/6] fix: guard against unscheduled sessions --- ietf/meeting/views.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py index 61ce2e6674..b447c71454 100644 --- a/ietf/meeting/views.py +++ b/ietf/meeting/views.py @@ -2824,12 +2824,14 @@ def upload_session_minutes(request, session_id, num): else: form = UploadMinutesForm(show_apply_to_all_checkbox) + tsa = session.official_timeslotassignment() + future = tsa and timezone.now() < tsa.timeslot.end_time() return render(request, "meeting/upload_session_minutes.html", {'session': session, 'session_number': session_number, 'minutes_sp' : minutes_sp, 'form': form, - 'future': timezone.now() < session.official_timeslotassignment().timeslot.end_time(), + 'future': future, }) @role_required("Secretariat") From 9450398ed8b7daa080b3824d03b7de7474be1be4 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Thu, 27 Mar 2025 11:01:26 -0500 Subject: [PATCH 4/6] fix: test addition of warning --- ietf/meeting/tests_views.py | 272 +++++++++++++++++++----------------- ietf/meeting/views.py | 2 +- 2 files changed, 143 insertions(+), 131 deletions(-) diff --git a/ietf/meeting/tests_views.py b/ietf/meeting/tests_views.py index 0f91986f77..e50f3d5464 100644 --- a/ietf/meeting/tests_views.py +++ b/ietf/meeting/tests_views.py @@ -6543,108 +6543,114 @@ def test_upload_bluesheets_interim_chair_access(self): def test_upload_minutes_agenda(self): for doctype in ('minutes','agenda'): - session = SessionFactory(meeting__type_id='ietf') - if doctype == 'minutes': - url = urlreverse('ietf.meeting.views.upload_session_minutes',kwargs={'num':session.meeting.number,'session_id':session.id}) - else: - url = urlreverse('ietf.meeting.views.upload_session_agenda',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.exists()) - self.assertFalse(q('form input[type="checkbox"]')) - - session2 = SessionFactory(meeting=session.meeting,group=session.group) - r = self.client.get(url) - self.assertEqual(r.status_code, 200) - q = PyQuery(r.content) - self.assertTrue(q('form input[type="checkbox"]')) - - # test not submitting a file - r = self.client.post(url, dict(submission_method="upload")) - 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') - test_file.name = "not_really.json" - 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(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(submission_method="upload",file=test_file)) - self.assertEqual(r.status_code, 200) - q = PyQuery(r.content) - self.assertTrue(q('form .is-invalid')) - - # Test html sanitization - test_file = BytesIO(b'Title

Title

Some text
') - test_file.name = "some.html" - 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') - text = doc.text() - self.assertIn('Some text', text) - self.assertNotIn('
', text) - text = retrieve_str(doctype, f"{doc.name}-{doc.rev}.html") - self.assertIn('Some text', text) - self.assertNotIn('
', text) - - # txt upload - test_bytes = b'This is some text for a test, with the word\nvirtual at the beginning of a line.' - test_file = BytesIO(test_bytes) - test_file.name = "some.txt" - r = self.client.post(url,dict(submission_method="upload",file=test_file,apply_to_all=False)) - self.assertEqual(r.status_code, 302) - doc = session.presentations.filter(document__type_id=doctype).first().document - self.assertEqual(doc.rev,'01') - self.assertFalse(session2.presentations.filter(document__type_id=doctype)) - retrieved_bytes = retrieve_bytes(doctype, f"{doc.name}-{doc.rev}.txt") - self.assertEqual(retrieved_bytes, test_bytes) - - r = self.client.get(url) - self.assertEqual(r.status_code, 200) - q = PyQuery(r.content) - self.assertIn('Revise', str(q("Title"))) - test_bytes = b'this is some different text for a test' - test_file = BytesIO(test_bytes) - test_file.name = "also_some.txt" - 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') - self.assertTrue(session2.presentations.filter(document__type_id=doctype)) - retrieved_bytes = retrieve_bytes(doctype, f"{doc.name}-{doc.rev}.txt") - self.assertEqual(retrieved_bytes, test_bytes) - - # Test bad encoding - test_file = BytesIO('

Title

Some\x93text
'.encode('latin1')) - test_file.name = "some.html" - 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') - - # 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) + for future in (True, False): + mtg_date = date_today()+datetime.timedelta(days=180 if future else -180) + session = SessionFactory(meeting__type_id='ietf', meeting__date=mtg_date) + if doctype == 'minutes': + url = urlreverse('ietf.meeting.views.upload_session_minutes',kwargs={'num':session.meeting.number,'session_id':session.id}) + else: + url = urlreverse('ietf.meeting.views.upload_session_agenda',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.exists()) + self.assertFalse(q('form input[type="checkbox"]')) + if future and doctype == "minutes": + self.assertContains(r, "Session has not ended yet") + else: + self.assertNotContains(r, "Session has not ended yet") + + session2 = SessionFactory(meeting=session.meeting,group=session.group) + r = self.client.get(url) + self.assertEqual(r.status_code, 200) + q = PyQuery(r.content) + self.assertTrue(q('form input[type="checkbox"]')) + + # test not submitting a file + r = self.client.post(url, dict(submission_method="upload")) + 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') + test_file.name = "not_really.json" + 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(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(submission_method="upload",file=test_file)) + self.assertEqual(r.status_code, 200) + q = PyQuery(r.content) + self.assertTrue(q('form .is-invalid')) + + # Test html sanitization + test_file = BytesIO(b'Title

Title

Some text
') + test_file.name = "some.html" + 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') + text = doc.text() + self.assertIn('Some text', text) + self.assertNotIn('
', text) + text = retrieve_str(doctype, f"{doc.name}-{doc.rev}.html") + self.assertIn('Some text', text) + self.assertNotIn('
', text) + + # txt upload + test_bytes = b'This is some text for a test, with the word\nvirtual at the beginning of a line.' + test_file = BytesIO(test_bytes) + test_file.name = "some.txt" + r = self.client.post(url,dict(submission_method="upload",file=test_file,apply_to_all=False)) + self.assertEqual(r.status_code, 302) + doc = session.presentations.filter(document__type_id=doctype).first().document + self.assertEqual(doc.rev,'01') + self.assertFalse(session2.presentations.filter(document__type_id=doctype)) + retrieved_bytes = retrieve_bytes(doctype, f"{doc.name}-{doc.rev}.txt") + self.assertEqual(retrieved_bytes, test_bytes) + + r = self.client.get(url) + self.assertEqual(r.status_code, 200) + q = PyQuery(r.content) + self.assertIn('Revise', str(q("Title"))) + test_bytes = b'this is some different text for a test' + test_file = BytesIO(test_bytes) + test_file.name = "also_some.txt" + 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') + self.assertTrue(session2.presentations.filter(document__type_id=doctype)) + retrieved_bytes = retrieve_bytes(doctype, f"{doc.name}-{doc.rev}.txt") + self.assertEqual(retrieved_bytes, test_bytes) + + # Test bad encoding + test_file = BytesIO('

Title

Some\x93text
'.encode('latin1')) + test_file.name = "some.html" + 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') + + # 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_upload_minutes_agenda_unscheduled(self): for doctype in ('minutes','agenda'): @@ -6661,6 +6667,7 @@ def test_upload_minutes_agenda_unscheduled(self): self.assertIn('Upload', str(q("Title"))) self.assertFalse(session.presentations.exists()) self.assertFalse(q('form input[type="checkbox"]')) + self.assertNotContains(r, "Session has not ended yet") test_file = BytesIO(b'this is some text for a test') test_file.name = "not_really.txt" @@ -6669,35 +6676,40 @@ def test_upload_minutes_agenda_unscheduled(self): @override_settings(MEETING_MATERIALS_SERVE_LOCALLY=True) def test_upload_minutes_agenda_interim(self): - session=SessionFactory(meeting__type_id='interim') for doctype in ('minutes','agenda'): - if doctype=='minutes': - url = urlreverse('ietf.meeting.views.upload_session_minutes',kwargs={'num':session.meeting.number,'session_id':session.id}) - else: - url = urlreverse('ietf.meeting.views.upload_session_agenda',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_bytes = b'this is some text for a test' - test_file = BytesIO(test_bytes) - 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') - retrieved_bytes = retrieve_bytes(doctype, f"{doc.name}-{doc.rev}.txt") - self.assertEqual(retrieved_bytes, test_bytes) - - # 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) + for future in (True, False): + session=SessionFactory(meeting__type_id='interim', meeting__date = date_today()+datetime.timedelta(days=180 if future else -180)) + if doctype=='minutes': + url = urlreverse('ietf.meeting.views.upload_session_minutes',kwargs={'num':session.meeting.number,'session_id':session.id}) + else: + url = urlreverse('ietf.meeting.views.upload_session_agenda',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)) + if future and doctype == "minutes": + self.assertContains(r, "Session has not ended yet") + else: + self.assertNotContains(r, "Session has not ended yet") + test_bytes = b'this is some text for a test' + test_file = BytesIO(test_bytes) + 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') + retrieved_bytes = retrieve_bytes(doctype, f"{doc.name}-{doc.rev}.txt") + self.assertEqual(retrieved_bytes, test_bytes) + + # 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) @override_settings(MEETING_MATERIALS_SERVE_LOCALLY=True) def test_upload_narrativeminutes(self): diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py index b447c71454..5b3725fdd0 100644 --- a/ietf/meeting/views.py +++ b/ietf/meeting/views.py @@ -2825,7 +2825,7 @@ def upload_session_minutes(request, session_id, num): form = UploadMinutesForm(show_apply_to_all_checkbox) tsa = session.official_timeslotassignment() - future = tsa and timezone.now() < tsa.timeslot.end_time() + future = tsa is not None and timezone.now() < tsa.timeslot.end_time() return render(request, "meeting/upload_session_minutes.html", {'session': session, 'session_number': session_number, From d6cbf6821a5eda50f092f1493f1d19c44ce3ceec Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Thu, 27 Mar 2025 11:05:38 -0500 Subject: [PATCH 5/6] fix: another guard against unscheduled sessions --- ietf/meeting/views.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py index 5b3725fdd0..722bf829e1 100644 --- a/ietf/meeting/views.py +++ b/ietf/meeting/views.py @@ -2522,6 +2522,8 @@ def session_details(request, num, acronym): else: pending_suggestions = SlideSubmission.objects.none() + tsa = session.official_timeslotassignment() + future = tsa is not None and timezone.now() < tsa.timeslot.end_time() return render(request, "meeting/session_details.html", { 'scheduled_sessions':scheduled_sessions , 'unscheduled_sessions':unscheduled_sessions , @@ -2532,7 +2534,7 @@ def session_details(request, num, acronym): 'can_manage_materials' : can_manage, 'can_view_request': can_view_request, 'thisweek': datetime_today()-datetime.timedelta(days=7), - 'future': timezone.now() < session.official_timeslotassignment().timeslot.end_time(), + 'future': future, }) class SessionDraftsForm(forms.Form): From d385f302d26a2129cea652ceebd7b2d0b476b6d1 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Thu, 27 Mar 2025 11:51:14 -0500 Subject: [PATCH 6/6] feat: test future warning on session details pannel --- ietf/meeting/tests_views.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/ietf/meeting/tests_views.py b/ietf/meeting/tests_views.py index e50f3d5464..a93a26b981 100644 --- a/ietf/meeting/tests_views.py +++ b/ietf/meeting/tests_views.py @@ -6541,6 +6541,20 @@ def test_upload_bluesheets_interim_chair_access(self): self.assertIn('Upload', str(q("title"))) + def test_label_future_sessions(self): + self.client.login(username='secretary', password='secretary+password') + for future in (True, False): + mtg_date = date_today()+datetime.timedelta(days=180 if future else -180) + session = SessionFactory(meeting__type_id='ietf', meeting__date=mtg_date) + # Verify future warning shows on the session details panel + url = urlreverse('ietf.meeting.views.session_details', kwargs={'num':session.meeting.number, 'acronym': session.group.acronym}) + r = self.client.get(url) + self.assertTrue(r.status_code==200) + if future: + self.assertContains(r, "Session has not ended yet") + else: + self.assertNotContains(r, "Session has not ended yet") + def test_upload_minutes_agenda(self): for doctype in ('minutes','agenda'): for future in (True, False):