From 149f82f57804c4d85b213c61124072d59568acc3 Mon Sep 17 00:00:00 2001 From: Jennifer Richards Date: Mon, 18 Dec 2023 11:11:53 -0400 Subject: [PATCH 01/10] fix: Include refs to pre-RFC drafts as refs to the RFC (#6784) * fix: clean up shadowed name in document_referenced_by.html * fix: include refs to rfc's came_from_draft() * fix: include refs to draft's became_rfc() * fix: Count indirect refs by RFCs * refactor: break indirect ref_by counting to its own fn * fix: only count refs to pre-rfc draft, not post-draft rfc (and rename a method) * test: test referenced_by_rfcs methods The test_referenced_by_rfcs_as_rfc_or_draft() test fails because there's a bug! * test: actually, do double-count refs to rfc/draft Let's do include refs to an rfc and its precursor draft as separate refs. This almost surely indicates a data error because it would mean an rfc referenced both an rfc and the draft that it came from. That should never be allowed, so at least let some light fall on it if it happens. * chore: Add docstring to document_referenced_by view --- ietf/doc/models.py | 9 +++- ietf/doc/tests.py | 47 +++++++++++++++++++ ietf/doc/views_doc.py | 18 +++++++ .../templates/doc/document_referenced_by.html | 8 ++-- ietf/templates/person/profile.html | 2 +- 5 files changed, 78 insertions(+), 6 deletions(-) diff --git a/ietf/doc/models.py b/ietf/doc/models.py index fdda23ecd14..9275b541015 100644 --- a/ietf/doc/models.py +++ b/ietf/doc/models.py @@ -651,8 +651,8 @@ def referenced_by(self): | models.Q(source__type__slug="rfc") ) - def referenced_by_rfcs(self): + """Get refs to this doc from RFCs""" return self.relations_that(("refnorm", "refinfo", "refunk", "refold")).filter( source__type__slug="rfc" ) @@ -675,6 +675,13 @@ def contains(self): def part_of(self): return self.related_that("contains") + def referenced_by_rfcs_as_rfc_or_draft(self): + """Get refs to this doc, or a draft/rfc it came from, from an RFC""" + refs_to = self.referenced_by_rfcs() + if self.type_id == "rfc" and self.came_from_draft(): + refs_to |= self.came_from_draft().referenced_by_rfcs() + return refs_to + class Meta: abstract = True diff --git a/ietf/doc/tests.py b/ietf/doc/tests.py index f14b5b1af0f..8ae588ad1dc 100644 --- a/ietf/doc/tests.py +++ b/ietf/doc/tests.py @@ -2951,4 +2951,51 @@ def test_revisions(self): self.assertEqual(draft.revisions_by_dochistory(),[f"{i:02d}" for i in range(8,10)]) self.assertEqual(draft.revisions_by_newrevisionevent(),[f"{i:02d}" for i in [*range(0,5), *range(6,10)]]) + def test_referenced_by_rfcs(self): + # n.b., no significance to the ref* values in this test + referring_draft = WgDraftFactory() + (rfc, referring_rfc) = WgRfcFactory.create_batch(2) + rfc.targets_related.create(relationship_id="refnorm", source=referring_draft) + rfc.targets_related.create(relationship_id="refnorm", source=referring_rfc) + self.assertCountEqual( + rfc.referenced_by_rfcs(), + rfc.targets_related.filter(source=referring_rfc), + ) + def test_referenced_by_rfcs_as_rfc_or_draft(self): + # n.b., no significance to the ref* values in this test + draft = WgDraftFactory() + rfc = WgRfcFactory() + draft.relateddocument_set.create(relationship_id="became_rfc", target=rfc) + + # Draft referring to the rfc and the draft - should not be reported at all + draft_referring_to_both = WgDraftFactory() + draft_referring_to_both.relateddocument_set.create(relationship_id="refnorm", target=draft) + draft_referring_to_both.relateddocument_set.create(relationship_id="refnorm", target=rfc) + + # RFC referring only to the draft - should be reported for either the draft or the rfc + rfc_referring_to_draft = WgRfcFactory() + rfc_referring_to_draft.relateddocument_set.create(relationship_id="refinfo", target=draft) + + # RFC referring only to the rfc - should be reported only for the rfc + rfc_referring_to_rfc = WgRfcFactory() + rfc_referring_to_rfc.relateddocument_set.create(relationship_id="refinfo", target=rfc) + + # RFC referring only to the rfc - should be reported only for the rfc + rfc_referring_to_rfc = WgRfcFactory() + rfc_referring_to_rfc.relateddocument_set.create(relationship_id="refinfo", target=rfc) + + # RFC referring to the rfc and the draft - should be reported for both + rfc_referring_to_both = WgRfcFactory() + rfc_referring_to_both.relateddocument_set.create(relationship_id="refnorm", target=draft) + rfc_referring_to_both.relateddocument_set.create(relationship_id="refnorm", target=rfc) + + self.assertCountEqual( + draft.referenced_by_rfcs_as_rfc_or_draft(), + draft.targets_related.filter(source__type="rfc"), + ) + + self.assertCountEqual( + rfc.referenced_by_rfcs_as_rfc_or_draft(), + draft.targets_related.filter(source__type="rfc") | rfc.targets_related.filter(source__type="rfc"), + ) diff --git a/ietf/doc/views_doc.py b/ietf/doc/views_doc.py index 8408f5a0cd5..665393c24e8 100644 --- a/ietf/doc/views_doc.py +++ b/ietf/doc/views_doc.py @@ -1437,8 +1437,26 @@ def document_references(request, name): return render(request, "doc/document_references.html",dict(doc=doc,refs=sorted(refs,key=lambda x:x.target.name),)) def document_referenced_by(request, name): + """View documents that reference the named document + + The view lists both direct references to a the named document, plus references to + related other documents. For a draft that became an RFC, this will include references + to the RFC. For an RFC, this will include references to the draft it came from, if any. + For a subseries document, this will include references to any of the RFC documents it + contains. + + In the rendered output, a badge is applied to indicate the name of the document the + reference actually targeted. E.g., on the display for a draft that became RFC NNN, + references included because they point to that RFC would be shown with a tag "As RFC NNN". + The intention is to make the "Referenced By" page useful for finding related work while + accurately reflecting the actual reference relationships. + """ doc = get_object_or_404(Document,name=name) refs = doc.referenced_by() + if doc.came_from_draft(): + refs |= doc.came_from_draft().referenced_by() + if doc.became_rfc(): + refs |= doc.became_rfc().referenced_by() if doc.type_id in ["bcp","std","fyi"]: for rfc in doc.contains(): refs |= rfc.referenced_by() diff --git a/ietf/templates/doc/document_referenced_by.html b/ietf/templates/doc/document_referenced_by.html index 2c1729f99c4..e1137768b71 100644 --- a/ietf/templates/doc/document_referenced_by.html +++ b/ietf/templates/doc/document_referenced_by.html @@ -38,10 +38,10 @@

References to {{ name|prettystdname }}

{% for ref in refs %} - {% with ref.source.name as name %} + {% with ref.source.name as src_name %} - {{ name|prettystdname }} + {{ src_name|prettystdname }} {% if ref.target.name != name %}
As {{ ref.target.name }} @@ -51,13 +51,13 @@

References to {{ name|prettystdname }}

{{ ref.source.title }}
References Referenced by diff --git a/ietf/templates/person/profile.html b/ietf/templates/person/profile.html index cc504ebc8da..42e5d2e43ab 100644 --- a/ietf/templates/person/profile.html +++ b/ietf/templates/person/profile.html @@ -109,7 +109,7 @@

{{ doc.pub_date|date:"b Y"|title }} {{ doc.title|urlize_ietf_docs }} - {% with doc.referenced_by_rfcs.count as refbycount %} + {% with doc.referenced_by_rfcs_as_rfc_or_draft.count as refbycount %} {% if refbycount %} Date: Mon, 18 Dec 2023 12:45:51 -0400 Subject: [PATCH 02/10] fix: no dups on stream documents view (#6804) --- ietf/group/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ietf/group/views.py b/ietf/group/views.py index 991a1b8d879..698963678a9 100644 --- a/ietf/group/views.py +++ b/ietf/group/views.py @@ -1296,7 +1296,7 @@ def stream_documents(request, acronym): qs = Document.objects.filter(stream=acronym).filter( Q(type_id="draft", states__type="draft", states__slug="active") | Q(type_id="rfc") - ) + ).distinct() docs, meta = prepare_document_table(request, qs, max_results=1000) return render(request, 'group/stream_documents.html', {'stream':stream, 'docs':docs, 'meta':meta, 'editable':editable } ) From ca664c4162b0a726bc93735e95085ee5dc0ac5be Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Mon, 18 Dec 2023 16:49:27 -0600 Subject: [PATCH 03/10] =?UTF-8?q?fix:=20don't=20create=20a=20volunteer=20o?= =?UTF-8?q?bject=20when=20a=20person=20has=20already=20volunt=E2=80=A6=20(?= =?UTF-8?q?#6809)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: don't create a volunteer object when a person has already volunteered * fix: safer create * refactor: use shorthand from django * fix: also protect volunteer from datatracker from race --- ietf/api/views.py | 9 ++++++--- ietf/nomcom/views.py | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/ietf/api/views.py b/ietf/api/views.py index e587b371211..73b873f5f35 100644 --- a/ietf/api/views.py +++ b/ietf/api/views.py @@ -210,11 +210,14 @@ def err(code, text): except (NomCom.DoesNotExist, NomCom.MultipleObjectsReturned): nomcom = None if nomcom: - Volunteer.objects.create( + Volunteer.objects.get_or_create( nomcom=nomcom, person=object.person, - affiliation=data['affiliation'], - origin='registration') + defaults={ + "affiliation": data["affiliation"], + "origin": "registration" + } + ) return HttpResponse(response, status=202, content_type='text/plain') else: return HttpResponse(status=405) diff --git a/ietf/nomcom/views.py b/ietf/nomcom/views.py index ce7ca9a82d3..7705be5697f 100644 --- a/ietf/nomcom/views.py +++ b/ietf/nomcom/views.py @@ -1373,7 +1373,7 @@ def volunteer(request): form = VolunteerForm(person=person, data=request.POST) if form.is_valid(): for nc in form.cleaned_data['nomcoms']: - nc.volunteer_set.create(person=person, affiliation=form.cleaned_data['affiliation']) + nc.volunteer_set.get_or_create(person=person, defaults={"affiliation": form.cleaned_data["affiliation"], "origin":"datatracker"}) return redirect('ietf.ietfauth.views.profile') else: form = VolunteerForm(person=person,initial=dict(nomcoms=can_volunteer, affiliation=suggest_affiliation(person))) From db8fba486bcad508930545a9279d99f332d8d649 Mon Sep 17 00:00:00 2001 From: Lars Eggert Date: Thu, 21 Dec 2023 00:36:55 +0200 Subject: [PATCH 04/10] 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 9275b541015..434096929a3 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 05/10] 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 47e2334f47c..a57fcf63c12 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 ab392663969..9d07df103ee 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 00000000000..b63d460ed8e --- /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 1856a75bdbb..57cba6b53c6 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 b6df202e7d1..a87d5ac1319 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 06/10] 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 14a0d5ea902..ac7854f2650 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 07/10] 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 8ae588ad1dc..55bd48ef553 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 665393c24e8..f91e25d7830 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 08/10] 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 55bd48ef553..71ec23906ca 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 2e4231c5ace..789f32e3870 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 09/10] 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 f91e25d7830..e0bf328e899 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 10/10] 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 1c3d4662864..a22c746ae26 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}`