From db8fba486bcad508930545a9279d99f332d8d649 Mon Sep 17 00:00:00 2001 From: Lars Eggert Date: Thu, 21 Dec 2023 00:36:55 +0200 Subject: [PATCH 01/14] fix: Deduplicate referenced_by (#6820) Fixes #6808 --- ietf/doc/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ietf/doc/models.py b/ietf/doc/models.py index 9275b54101..434096929a 100644 --- a/ietf/doc/models.py +++ b/ietf/doc/models.py @@ -649,7 +649,7 @@ def referenced_by(self): source__states__slug="active", ) | models.Q(source__type__slug="rfc") - ) + ).distinct() def referenced_by_rfcs(self): """Get refs to this doc from RFCs""" From c1e40ff100078a8ee5eb727ebd224938fdef611d Mon Sep 17 00:00:00 2001 From: Paul Selkirk Date: Wed, 20 Dec 2023 17:40:13 -0500 Subject: [PATCH 02/14] feat: Allow entering agenda text directly (#6792) * feat: Allow entering agenda text directly (#6532) * fix: Hide label as well as file/text input box * refactor: Package javascript for static/dist * fix: Fix test cases broken by view changes * test: Add test case for entering agenda text * refactor: assertRedirects --- ietf/meeting/tests_views.py | 60 ++++++++++++++--- ietf/meeting/views.py | 67 +++++++++++++++++-- ietf/static/js/upload-session-agenda.js | 28 ++++++++ .../meeting/upload_session_agenda.html | 7 +- package.json | 1 + 5 files changed, 145 insertions(+), 18 deletions(-) create mode 100644 ietf/static/js/upload-session-agenda.js 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" From 50aa3995ee2f166ba90bf2fc00fef438d3243946 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Wed, 20 Dec 2023 16:41:33 -0600 Subject: [PATCH 03/14] chore: reconfigure pylint in dev containers (#6817) --- .devcontainer/devcontainer.json | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 14a0d5ea90..ac7854f265 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -33,23 +33,16 @@ "oderwat.indent-rainbow", "redhat.vscode-yaml", "spmeesseman.vscode-taskexplorer", - "visualstudioexptteam.vscodeintellicode" + "visualstudioexptteam.vscodeintellicode", + "ms-python.pylint" ], "settings": { "terminal.integrated.defaultProfile.linux": "zsh", "python.pythonPath": "/usr/local/bin/python", "python.languageServer": "Default", - "python.linting.enabled": true, - "python.linting.pylintEnabled": true, "python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8", "python.formatting.blackPath": "/usr/local/py-utils/bin/black", "python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf", - "python.linting.banditPath": "/usr/local/py-utils/bin/bandit", - "python.linting.flake8Path": "/usr/local/py-utils/bin/flake8", - "python.linting.mypyPath": "/usr/local/py-utils/bin/mypy", - "python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle", - "python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle", - "python.linting.pylintPath": "/usr/local/py-utils/bin/pylint", "python.testing.pytestArgs": [ "ietf" ], From bc74977fcc0d3d13ba9a665b3c370dc6ce56a058 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Thu, 21 Dec 2023 16:27:48 -0600 Subject: [PATCH 04/14] fix: 404 requests for bibtex for none draft or rfc documents (#6834) * fix: 404 requests for bibtex for none draft or rfc documents * test: test rejecting the types with factories already being imported --- ietf/doc/tests.py | 8 +++++++- ietf/doc/views_doc.py | 3 +++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/ietf/doc/tests.py b/ietf/doc/tests.py index 8ae588ad1d..55bd48ef55 100644 --- a/ietf/doc/tests.py +++ b/ietf/doc/tests.py @@ -40,7 +40,7 @@ ConflictReviewFactory, WgDraftFactory, IndividualDraftFactory, WgRfcFactory, IndividualRfcFactory, StateDocEventFactory, BallotPositionDocEventFactory, BallotDocEventFactory, DocumentAuthorFactory, NewRevisionDocEventFactory, - StatusChangeFactory, DocExtResourceFactory, RgDraftFactory) + StatusChangeFactory, DocExtResourceFactory, RgDraftFactory, BcpFactory) from ietf.doc.forms import NotifyForm from ietf.doc.fields import SearchableDocumentsField from ietf.doc.utils import create_ballot_if_not_open, uppercase_std_abbreviated_name @@ -1948,6 +1948,12 @@ def _parse_bibtex_response(self, response) -> dict: @override_settings(RFC_EDITOR_INFO_BASE_URL='https://www.rfc-editor.ietf.org/info/') def test_document_bibtex(self): + + for factory in [CharterFactory, BcpFactory, StatusChangeFactory, ConflictReviewFactory]: # Should be extended to all other doc types + doc = factory() + url = urlreverse("ietf.doc.views_doc.document_bibtex", kwargs=dict(name=doc.name)) + r = self.client.get(url) + self.assertEqual(r.status_code, 404) rfc = WgRfcFactory.create( time=datetime.datetime(2010, 10, 10, tzinfo=ZoneInfo(settings.TIME_ZONE)) ) diff --git a/ietf/doc/views_doc.py b/ietf/doc/views_doc.py index 665393c24e..f91e25d783 100644 --- a/ietf/doc/views_doc.py +++ b/ietf/doc/views_doc.py @@ -1262,6 +1262,9 @@ def document_bibtex(request, name, rev=None): doc = get_object_or_404(Document, name=name) + if doc.type_id not in ["rfc", "draft"]: + raise Http404() + doi = None draft_became_rfc = None replaced_by = None From 359a107fc591c796b26f388a5b02013c5f7a7186 Mon Sep 17 00:00:00 2001 From: Paul Selkirk Date: Thu, 21 Dec 2023 18:27:32 -0500 Subject: [PATCH 05/14] fix: Include RFCs when searching on a draft name (#6787) (#6827) --- ietf/doc/tests.py | 19 ++++++++++++++++++- ietf/doc/views_search.py | 3 +++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/ietf/doc/tests.py b/ietf/doc/tests.py index 55bd48ef55..71ec23906c 100644 --- a/ietf/doc/tests.py +++ b/ietf/doc/tests.py @@ -1,4 +1,4 @@ -# Copyright The IETF Trust 2012-2020, All Rights Reserved +# Copyright The IETF Trust 2012-2023, All Rights Reserved # -*- coding: utf-8 -*- @@ -156,6 +156,23 @@ def test_search(self): self.assertEqual(r.status_code, 200) self.assertContains(r, draft.title) + def test_search_became_rfc(self): + draft = WgDraftFactory() + rfc = WgRfcFactory() + draft.set_state(State.objects.get(type="draft", slug="rfc")) + draft.relateddocument_set.create(relationship_id="became_rfc", target=rfc) + base_url = urlreverse('ietf.doc.views_search.search') + + # find by RFC + r = self.client.get(base_url + f"?rfcs=on&name={rfc.name}") + self.assertEqual(r.status_code, 200) + self.assertContains(r, rfc.title) + + # find by draft + r = self.client.get(base_url + f"?activedrafts=on&rfcs=on&name={draft.name}") + self.assertEqual(r.status_code, 200) + self.assertContains(r, rfc.title) + def test_search_for_name(self): draft = WgDraftFactory(name='draft-ietf-mars-test',group=GroupFactory(acronym='mars',parent=Group.objects.get(acronym='farfut')),authors=[PersonFactory()],ad=PersonFactory()) draft.set_state(State.objects.get(used=True, type="draft-iesg", slug="pub-req")) diff --git a/ietf/doc/views_search.py b/ietf/doc/views_search.py index 2e4231c5ac..789f32e387 100644 --- a/ietf/doc/views_search.py +++ b/ietf/doc/views_search.py @@ -211,6 +211,9 @@ def retrieve_search_results(form, all_types=False): Q(targets_related__source__title__icontains=singlespace, targets_related__relationship_id="contains"), ]) + if query["rfcs"]: + queries.extend([Q(targets_related__source__name__icontains=look_for, targets_related__relationship_id="became_rfc")]) + combined_query = reduce(operator.or_, queries) docs = docs.filter(combined_query).distinct() From c328ff7425bef97249f302e166e3bbc50ec04be8 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Thu, 21 Dec 2023 17:52:55 -0600 Subject: [PATCH 06/14] fix: improve idnits2_state document creation time calculations (#6832) * fix: improve idnits2_state document creation time calculations * chore: apply black to the idnits2_state function --- ietf/doc/views_doc.py | 38 ++++++++++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/ietf/doc/views_doc.py b/ietf/doc/views_doc.py index f91e25d783..e0bf328e89 100644 --- a/ietf/doc/views_doc.py +++ b/ietf/doc/views_doc.py @@ -2188,13 +2188,31 @@ def idnits2_state(request, name, rev=None): if doc.type_id == "rfc": draft = doc.came_from_draft() if draft: - zero_revision = NewRevisionDocEvent.objects.filter(doc=draft,rev='00').first() + zero_revision = NewRevisionDocEvent.objects.filter( + doc=draft, rev="00" + ).first() else: - zero_revision = NewRevisionDocEvent.objects.filter(doc=doc,rev='00').first() + zero_revision = NewRevisionDocEvent.objects.filter(doc=doc, rev="00").first() if zero_revision: doc.created = zero_revision.time else: - doc.created = doc.docevent_set.order_by('-time').first().time + if doc.type_id == "draft": + if doc.became_rfc(): + interesting_event = ( + doc.became_rfc() + .docevent_set.filter(type="published_rfc") + .order_by("-time") + .first() + ) + else: + interesting_event = doc.docevent_set.order_by( + "-time" + ).first() # Is taking the most _recent_ instead of the oldest event correct? + else: # doc.type_id == "rfc" + interesting_event = ( + doc.docevent_set.filter(type="published_rfc").order_by("-time").first() + ) + doc.created = interesting_event.time if doc.std_level: doc.deststatus = doc.std_level.name elif doc.intended_std_level: @@ -2202,8 +2220,16 @@ def idnits2_state(request, name, rev=None): else: text = doc.text() if text: - parsed_draft = PlaintextDraft(text=doc.text(), source=name, name_from_source=False) + parsed_draft = PlaintextDraft( + text=doc.text(), source=name, name_from_source=False + ) doc.deststatus = parsed_draft.get_status() else: - doc.deststatus="Unknown" - return render(request, 'doc/idnits2-state.txt', context={'doc':doc}, content_type='text/plain;charset=utf-8') + doc.deststatus = "Unknown" + return render( + request, + "doc/idnits2-state.txt", + context={"doc": doc}, + content_type="text/plain;charset=utf-8", + ) + From f3f3f51ce2744984c8294ce20c299cac0a9d8b43 Mon Sep 17 00:00:00 2001 From: Nicolas Giard Date: Thu, 21 Dec 2023 23:54:00 -0500 Subject: [PATCH 07/14] ci: disable LETS_ENCRYPT for deploy-to-container CLI --- dev/deploy-to-container/cli.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/deploy-to-container/cli.js b/dev/deploy-to-container/cli.js index 1c3d466286..a22c746ae2 100644 --- a/dev/deploy-to-container/cli.js +++ b/dev/deploy-to-container/cli.js @@ -245,7 +245,7 @@ async function main () { name: `dt-app-${branch}`, Hostname: `dt-app-${branch}`, Env: [ - `LETSENCRYPT_HOST=${hostname}`, + // `LETSENCRYPT_HOST=${hostname}`, `VIRTUAL_HOST=${hostname}`, `VIRTUAL_PORT=8000`, `PGHOST=dt-db-${branch}` From ec90725f12ff1a11a0323286bc0718b0825c7e5a Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Fri, 22 Dec 2023 08:18:43 -0600 Subject: [PATCH 08/14] chore: remove rfc editor sync from sandbox deploy startup --- dev/deploy-to-container/start.sh | 3 --- 1 file changed, 3 deletions(-) diff --git a/dev/deploy-to-container/start.sh b/dev/deploy-to-container/start.sh index 271c54a43e..2c83d6970c 100644 --- a/dev/deploy-to-container/start.sh +++ b/dev/deploy-to-container/start.sh @@ -38,8 +38,5 @@ echo "Running Datatracker checks..." echo "Running Datatracker migrations..." /usr/local/bin/python ./ietf/manage.py migrate --settings=settings_local -echo "Syncing with the rfc-index" -./ietf/bin/rfc-editor-index-updates -d 1969-01-01 - echo "Starting Datatracker..." ./ietf/manage.py runserver 0.0.0.0:8000 --settings=settings_local From 2c11b544f1b552776b88e6680782a43288cac867 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Tue, 26 Dec 2023 08:53:52 -0600 Subject: [PATCH 09/14] fix: show state help for the editorial stream (#6836) --- ietf/doc/views_help.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ietf/doc/views_help.py b/ietf/doc/views_help.py index 73cdcdd20f..e8e63ed8b7 100644 --- a/ietf/doc/views_help.py +++ b/ietf/doc/views_help.py @@ -1,5 +1,7 @@ # Copyright The IETF Trust 2013-2023, All Rights Reserved +import debug # pyflakes: ignore + from django.shortcuts import render, get_object_or_404 from django.http import Http404 @@ -18,6 +20,7 @@ def state_help(request, type=None): "draft-stream-irtf": ("draft-stream-irtf", "IRTF Stream States for Internet-Drafts"), "draft-stream-ise": ("draft-stream-ise", "ISE Stream States for Internet-Drafts"), "draft-stream-iab": ("draft-stream-iab", "IAB Stream States for Internet-Drafts"), + "draft-stream-editorial": ("draft-stream-editorial", "Editorial Stream States for Internet-Drafts"), "charter": ("charter", "Charter States"), "conflict-review": ("conflrev", "Conflict Review States"), "status-change": ("statchg", "RFC Status Change States"), From 1450ecde999e9c454b8eea74811bc44bf7d297eb Mon Sep 17 00:00:00 2001 From: Paul Selkirk Date: Wed, 27 Dec 2023 11:39:13 -0500 Subject: [PATCH 10/14] fix: Retain date of cancelled virtual interim meeting (#6708) (#6852) --- ietf/templates/group/meetings-row.html | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ietf/templates/group/meetings-row.html b/ietf/templates/group/meetings-row.html index fbaf7cd560..65ba435baa 100644 --- a/ietf/templates/group/meetings-row.html +++ b/ietf/templates/group/meetings-row.html @@ -16,6 +16,11 @@ {% endwith %} {% else %}
{{ s.current_status_name }}
+ {% if s.current_status == "canceled" %} + {% with timeslot=s.official_timeslotassignment.timeslot %} + + {% endwith %} + {% endif %} {% endif %} {% if show_request and s.meeting.type_id == 'ietf' %} {% if can_edit %} From 288b69d0f36664d2921d084d0a90c0ef2357ac0b Mon Sep 17 00:00:00 2001 From: Paul Selkirk Date: Wed, 27 Dec 2023 12:09:45 -0500 Subject: [PATCH 11/14] fix: Allow reassignment of withdrawn review assignment (#6723) (#6850) --- ietf/doc/tests_review.py | 19 +++++++++++++++++++ ietf/review/utils.py | 6 ++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/ietf/doc/tests_review.py b/ietf/doc/tests_review.py index 1c2e6c7ace..d9aca94e86 100644 --- a/ietf/doc/tests_review.py +++ b/ietf/doc/tests_review.py @@ -380,6 +380,25 @@ def test_assign_reviewer_after_reject(self): reviewer_label = q("option[value=\"{}\"]".format(reviewer_email.address)).text().lower() self.assertIn("rejected review of document before", reviewer_label) + def test_assign_reviewer_after_withdraw(self): + doc = WgDraftFactory() + review_team = ReviewTeamFactory() + rev_role = RoleFactory(group=review_team,person__user__username='reviewer',person__user__email='reviewer@example.com',name_id='reviewer') + RoleFactory(group=review_team,person__user__username='reviewsecretary',name_id='secr') + review_req = ReviewRequestFactory(team=review_team,doc=doc) + reviewer = rev_role.person.email_set.first() + ReviewAssignmentFactory(review_request=review_req, state_id='withdrawn', reviewer=reviewer) + req_url = urlreverse('ietf.doc.views_review.review_request', kwargs={ "name": doc.name, "request_id": review_req.pk }) + assign_url = urlreverse('ietf.doc.views_review.assign_reviewer', kwargs={ "name": doc.name, "request_id": review_req.pk }) + + login_testing_unauthorized(self, "reviewsecretary", assign_url) + r = self.client.post(assign_url, { "action": "assign", "reviewer": reviewer.pk }) + self.assertRedirects(r, req_url) + review_req = reload_db_objects(review_req) + assignment = review_req.reviewassignment_set.last() + self.assertEqual(assignment.state, ReviewAssignmentStateName.objects.get(slug='assigned')) + self.assertEqual(review_req.state, ReviewRequestStateName.objects.get(slug='assigned')) + def test_previously_reviewed_replaced_doc(self): review_team = ReviewTeamFactory(acronym="reviewteam", name="Review Team", type_id="review", list_email="reviewteam@ietf.org", parent=Group.objects.get(acronym="farfut")) rev_role = RoleFactory(group=review_team,person__user__username='reviewer',person__user__email='reviewer@example.com',person__name='Some Reviewer',name_id='reviewer') diff --git a/ietf/review/utils.py b/ietf/review/utils.py index 8869efaee0..61494738d3 100644 --- a/ietf/review/utils.py +++ b/ietf/review/utils.py @@ -1,4 +1,4 @@ -# Copyright The IETF Trust 2016-2020, All Rights Reserved +# Copyright The IETF Trust 2016-2023, All Rights Reserved # -*- coding: utf-8 -*- @@ -392,7 +392,9 @@ def assign_review_request_to_reviewer(request, review_req, reviewer, add_skip=Fa # cannot reference reviewassignment_set relation until pk exists if review_req.pk is not None: reviewassignment_set = review_req.reviewassignment_set.filter(reviewer=reviewer) - if reviewassignment_set.exists() and not reviewassignment_set.filter(state_id='rejected').exists(): + if (reviewassignment_set.exists() and not + (reviewassignment_set.filter(state_id='rejected').exists() or + reviewassignment_set.filter(state_id='withdrawn').exists())): return # Note that assigning a review no longer unassigns other reviews From 748bcc328fc7eb21715a992d4f52e1de61431bfb Mon Sep 17 00:00:00 2001 From: Paul Selkirk Date: Wed, 27 Dec 2023 12:56:38 -0500 Subject: [PATCH 12/14] fix: Wrap weasyprint to catch exceptions (#6728) * fix: Wrap weasyprint to catch exceptions (#6324) * test: Restore socket function after test * test: Use mock instead of monkeying with sockets * refactor: Log the error * fix: Don't catch non-Exception interruptions --------- Co-authored-by: Robert Sparks --- ietf/doc/models.py | 3 +++ ietf/doc/tests.py | 8 ++++++++ ietf/doc/views_doc.py | 6 ++++-- ietf/templates/doc/weasyprint_failed.html | 17 +++++++++++++++++ 4 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 ietf/templates/doc/weasyprint_failed.html diff --git a/ietf/doc/models.py b/ietf/doc/models.py index 434096929a..e53717cb7d 100644 --- a/ietf/doc/models.py +++ b/ietf/doc/models.py @@ -634,6 +634,9 @@ def pdfized(self): ) except AssertionError: pdf = None + except Exception as e: + log.log('weasyprint failed:'+str(e)) + raise if pdf: cache.set(cache_key, pdf, settings.PDFIZER_CACHE_TIME) return pdf diff --git a/ietf/doc/tests.py b/ietf/doc/tests.py index 71ec23906c..0b5a651060 100644 --- a/ietf/doc/tests.py +++ b/ietf/doc/tests.py @@ -31,6 +31,8 @@ from tastypie.test import ResourceTestCaseMixin +from weasyprint.urls import URLFetchingError + import debug # pyflakes:ignore from ietf.doc.models import ( Document, DocRelationshipName, RelatedDocument, State, @@ -2867,6 +2869,12 @@ def test_pdfized(self): self.should_succeed(dict(name=draft.name,rev=f'{r:02d}',ext=ext)) self.should_404(dict(name=draft.name,rev='02')) + with mock.patch('ietf.doc.models.DocumentInfo.pdfized', side_effect=URLFetchingError): + url = urlreverse(self.view, kwargs=dict(name=rfc.name)) + r = self.client.get(url) + self.assertEqual(r.status_code, 200) + self.assertContains(r, "Error while rendering PDF") + class NotifyValidationTests(TestCase): def test_notify_validation(self): valid_values = [ diff --git a/ietf/doc/views_doc.py b/ietf/doc/views_doc.py index e0bf328e89..293d32daff 100644 --- a/ietf/doc/views_doc.py +++ b/ietf/doc/views_doc.py @@ -51,7 +51,6 @@ from django import forms from django.contrib.staticfiles import finders - import debug # pyflakes:ignore from ietf.doc.models import ( Document, DocHistory, DocEvent, BallotDocEvent, BallotType, @@ -1064,7 +1063,10 @@ def document_pdfized(request, name, rev=None, ext=None): if not os.path.exists(doc.get_file_name()): raise Http404("File not found: %s" % doc.get_file_name()) - pdf = doc.pdfized() + try: + pdf = doc.pdfized() + except Exception: + return render(request, "doc/weasyprint_failed.html") if pdf: return HttpResponse(pdf,content_type='application/pdf') else: diff --git a/ietf/templates/doc/weasyprint_failed.html b/ietf/templates/doc/weasyprint_failed.html new file mode 100644 index 0000000000..985fe12fe1 --- /dev/null +++ b/ietf/templates/doc/weasyprint_failed.html @@ -0,0 +1,17 @@ +{# Copyright The IETF Trust 2023, All Rights Reserved #} +{% extends "base.html" %} +{% load origin %} +{% block title %}Error while rendering PDF{% endblock %} +{% block content %} + {% origin %} +

Error while rendering PDF

+

+ An error was encountered while trying to render your document as PDF. + In case this was a temporary error, you may want to try again in a + little while. +

+

+ A failure report with details about what happened has been sent to the + server administrators. +

+{% endblock %} \ No newline at end of file From bbc64d30fde69b97ed6f15355a580601b1b8cb45 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Wed, 3 Jan 2024 10:37:44 -0600 Subject: [PATCH 13/14] fix: count RFCs correctly for doc/ad (#6833) * fix: count RFCs correctly for doc/ad * chore: improve query legibility --- ietf/doc/views_search.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ietf/doc/views_search.py b/ietf/doc/views_search.py index 789f32e387..964894bff9 100644 --- a/ietf/doc/views_search.py +++ b/ietf/doc/views_search.py @@ -471,11 +471,11 @@ def ad_workload(request): state = doc_state(doc) state_events = doc.docevent_set.filter( - Q(type="started_iesg_process") - | Q(type="changed_state") - | Q(type="published_rfc") - | Q(type="closed_ballot"), - ).order_by("-time") + type__in=["started_iesg_process", "changed_state", "closed_ballot"] + ) + if doc.became_rfc(): + state_events = state_events | doc.became_rfc().docevent_set.filter(type="published_rfc") + state_events = state_events.order_by("-time") # compute state history for drafts last = now From b4451194288ec8a8d1359331a1d98d7d684d1530 Mon Sep 17 00:00:00 2001 From: Paul Selkirk Date: Wed, 3 Jan 2024 17:30:06 -0500 Subject: [PATCH 14/14] fix: Properly set AD as action holder when submitting to IESG for publication (#6854) * fix: Properly set AD as action holder when submitting to IESG for publication (#5227) The clear intent of `to_iesg` is that a) the document AD should be the group AD, if not already set, and b) the document Action Holder should be the document AD; but there was an order-of-operation error, such that the Action Holder remained empty. * refactor: Don't take values out of the parent scope, don't insert values into the parent scope * refactor: Streamline DocEvent creation --- ietf/doc/tests_draft.py | 36 ++++++++++++++++++++++++++++++++++++ ietf/doc/views_draft.py | 34 ++++++++++------------------------ 2 files changed, 46 insertions(+), 24 deletions(-) diff --git a/ietf/doc/tests_draft.py b/ietf/doc/tests_draft.py index e168f685f9..045fdc7f46 100644 --- a/ietf/doc/tests_draft.py +++ b/ietf/doc/tests_draft.py @@ -1483,6 +1483,42 @@ def test_confirm_submission(self): self.assertTrue("aread@" in outbox[-1]['To']) self.assertTrue("iesg-secretary@" in outbox[-1]['Cc']) + def test_confirm_submission_no_doc_ad(self): + url = urlreverse('ietf.doc.views_draft.to_iesg', kwargs=dict(name=self.docname)) + self.client.login(username="marschairman", password="marschairman+password") + + doc = Document.objects.get(name=self.docname) + RoleFactory(name_id='ad', group=doc.group, person=doc.ad) + e = DocEvent(type="changed_document", by=doc.ad, doc=doc, rev=doc.rev, desc="Remove doc AD") + e.save() + doc.ad = None + doc.save_with_history([e]) + + docevents_pre = set(doc.docevent_set.all()) + mailbox_before = len(outbox) + + r = self.client.post(url, dict(confirm="1")) + self.assertEqual(r.status_code, 302) + + doc = Document.objects.get(name=self.docname) + self.assertTrue(doc.get_state('draft-iesg').slug=='pub-req') + self.assertTrue(doc.get_state('draft-stream-ietf').slug=='sub-pub') + + self.assertCountEqual(doc.action_holders.all(), [doc.ad]) + + new_docevents = set(doc.docevent_set.all()) - docevents_pre + self.assertEqual(len(new_docevents), 5) + new_docevent_type_count = Counter([e.type for e in new_docevents]) + self.assertEqual(new_docevent_type_count['changed_state'],2) + self.assertEqual(new_docevent_type_count['started_iesg_process'],1) + self.assertEqual(new_docevent_type_count['changed_action_holders'], 1) + self.assertEqual(new_docevent_type_count['changed_document'], 1) + + self.assertEqual(len(outbox), mailbox_before + 1) + self.assertTrue("Publication has been requested" in outbox[-1]['Subject']) + self.assertTrue("aread@" in outbox[-1]['To']) + self.assertTrue("iesg-secretary@" in outbox[-1]['Cc']) + class RequestPublicationTests(TestCase): diff --git a/ietf/doc/views_draft.py b/ietf/doc/views_draft.py index 4f6659af9f..ea30e7bd2d 100644 --- a/ietf/doc/views_draft.py +++ b/ietf/doc/views_draft.py @@ -560,22 +560,19 @@ def to_iesg(request,name): if request.method == 'POST': if request.POST.get("confirm", ""): - by = request.user.person events = [] - - changes = [] + def doc_event(type, by, doc, desc): + return DocEvent.objects.create(type=type, by=by, doc=doc, rev=doc.rev, desc=desc) if doc.get_state_slug("draft-iesg") == "idexists": - e = DocEvent() - e.type = "started_iesg_process" - e.by = by - e.doc = doc - e.rev = doc.rev - e.desc = "Document is now in IESG state %s" % target_state['iesg'].name - e.save() - events.append(e) + events.append(doc_event("started_iesg_process", by, doc, f"Document is now in IESG state {target_state['iesg'].name}")) + + # do this first, so AD becomes action holder + if not doc.ad == ad : + doc.ad = ad + events.append(doc_event("changed_document", by, doc, f"Responsible AD changed to {doc.ad}")) for state_type in ['draft-iesg','draft-stream-ietf']: prev_state=doc.get_state(state_type) @@ -587,25 +584,14 @@ def to_iesg(request,name): events.append(e) events.append(add_state_change_event(doc=doc,by=by,prev_state=prev_state,new_state=new_state)) - if not doc.ad == ad : - doc.ad = ad - changes.append("Responsible AD changed to %s" % doc.ad) - if not doc.notify == notify : doc.notify = notify - changes.append("State Change Notice email list changed to %s" % doc.notify) + events.append(doc_event("changed_document", by, doc, f"State Change Notice email list changed to {doc.notify}")) # Get the last available writeup previous_writeup = doc.latest_event(WriteupDocEvent,type="changed_protocol_writeup") if previous_writeup != None: - changes.append(previous_writeup.text) - - for c in changes: - e = DocEvent(doc=doc, rev=doc.rev, by=by) - e.desc = c - e.type = "changed_document" - e.save() - events.append(e) + events.append(doc_event("changed_document", by, doc, previous_writeup.text)) doc.save_with_history(events)