From 1252cd2ac28ac4c6791731df0878f31e497885f1 Mon Sep 17 00:00:00 2001 From: Warren Kumari Date: Thu, 8 Aug 2024 13:10:54 -0400 Subject: [PATCH 1/8] fix: badgeify "Serious Issues", "On the right track" review summaries. (#7809) Addresses #7808 --- ietf/doc/templatetags/ietf_filters.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ietf/doc/templatetags/ietf_filters.py b/ietf/doc/templatetags/ietf_filters.py index 35f9f91b43..5a8afd9956 100644 --- a/ietf/doc/templatetags/ietf_filters.py +++ b/ietf/doc/templatetags/ietf_filters.py @@ -856,10 +856,10 @@ def badgeify(blob): Add an appropriate bootstrap badge around "text", based on its contents. """ config = [ - (r"rejected|not ready", "danger", "x-lg"), + (r"rejected|not ready|serious issues", "danger", "x-lg"), (r"complete|accepted|ready", "success", ""), (r"has nits|almost ready", "info", "info-lg"), - (r"has issues", "warning", "exclamation-lg"), + (r"has issues|on the right track", "warning", "exclamation-lg"), (r"assigned", "info", "person-plus-fill"), (r"will not review|overtaken by events|withdrawn", "secondary", "dash-lg"), (r"no response", "warning", "question-lg"), From ff8898186ba06072d0e226a6c88bb6e371cc3dee Mon Sep 17 00:00:00 2001 From: Matthew Holloway Date: Sat, 10 Aug 2024 02:24:04 +1200 Subject: [PATCH 2/8] fix: Ballot return to url via url params rather than session (#7788) * fix: #7287 ballot return params * fix: Moving Ballot edit position ballot_edit_return_point from session to query param * fix: tests for return_to_path param * chore: removing Playwright tests until we can figure out a plan * feat: return_to path utility refactoring * fix: throw HTTP 400 in view rather than bubbling up a 500 * fix: return http400 rather than raising --- ietf/doc/tests_ballot.py | 30 +++++++++++++ ietf/doc/views_ballot.py | 45 +++++++++++++------ ietf/doc/views_doc.py | 5 --- ietf/iesg/views.py | 3 +- ietf/templates/doc/ballot_popup.html | 2 +- .../doc/document_ballot_content.html | 2 +- ietf/utils/http.py | 26 ++++++++++- playwright/package.json | 1 + 8 files changed, 91 insertions(+), 23 deletions(-) diff --git a/ietf/doc/tests_ballot.py b/ietf/doc/tests_ballot.py index 034ba6f4b2..2403b6ef5c 100644 --- a/ietf/doc/tests_ballot.py +++ b/ietf/doc/tests_ballot.py @@ -20,6 +20,7 @@ BallotPositionDocEventFactory, BallotDocEventFactory, IRSGBallotDocEventFactory) from ietf.doc.templatetags.ietf_filters import can_defer from ietf.doc.utils import create_ballot_if_not_open +from ietf.doc.views_ballot import parse_ballot_edit_return_point from ietf.doc.views_doc import document_ballot_content from ietf.group.models import Group, Role from ietf.group.factories import GroupFactory, RoleFactory, ReviewTeamFactory @@ -1451,3 +1452,32 @@ def test_document_ballot_content_without_send_email_values(self): self._assertBallotMessage(q, balloters[0], 'No discuss send log available') self._assertBallotMessage(q, balloters[1], 'No comment send log available') self._assertBallotMessage(q, old_balloter, 'No ballot position send log available') + +class ReturnToUrlTests(TestCase): + def test_invalid_return_to_url(self): + self.assertRaises( + Exception, + lambda: parse_ballot_edit_return_point('/doc/', 'draft-ietf-opsawg-ipfix-tcpo-v6eh', '998718'), + ) + self.assertRaises( + Exception, + lambda: parse_ballot_edit_return_point('/a-route-that-does-not-exist/', 'draft-ietf-opsawg-ipfix-tcpo-v6eh', '998718'), + ) + self.assertRaises( + Exception, + lambda: parse_ballot_edit_return_point('https://example.com/phishing', 'draft-ietf-opsawg-ipfix-tcpo-v6eh', '998718'), + ) + + def test_valid_default_return_to_url(self): + self.assertEqual(parse_ballot_edit_return_point( + None, + 'draft-ietf-opsawg-ipfix-tcpo-v6eh', + '998718' + ), '/doc/draft-ietf-opsawg-ipfix-tcpo-v6eh/ballot/998718/') + + def test_valid_return_to_url(self): + self.assertEqual(parse_ballot_edit_return_point( + '/doc/draft-ietf-opsawg-ipfix-tcpo-v6eh/ballot/998718/', + 'draft-ietf-opsawg-ipfix-tcpo-v6eh', + '998718' + ), '/doc/draft-ietf-opsawg-ipfix-tcpo-v6eh/ballot/998718/') diff --git a/ietf/doc/views_ballot.py b/ietf/doc/views_ballot.py index 83ccb07be6..357bd2fa2f 100644 --- a/ietf/doc/views_ballot.py +++ b/ietf/doc/views_ballot.py @@ -8,13 +8,14 @@ from django import forms from django.conf import settings -from django.http import HttpResponse, HttpResponseRedirect, Http404 +from django.http import HttpResponse, HttpResponseRedirect, Http404, HttpResponseBadRequest from django.shortcuts import render, get_object_or_404, redirect from django.template.defaultfilters import striptags from django.template.loader import render_to_string from django.urls import reverse as urlreverse from django.views.decorators.csrf import csrf_exempt from django.utils.html import escape +from urllib.parse import urlencode as urllib_urlencode import debug # pyflakes:ignore @@ -39,6 +40,7 @@ from ietf.name.models import BallotPositionName, DocTypeName from ietf.person.models import Person from ietf.utils.fields import ModelMultipleChoiceField +from ietf.utils.http import validate_return_to_path from ietf.utils.mail import send_mail_text, send_mail_preformatted from ietf.utils.decorators import require_api_key from ietf.utils.response import permission_denied @@ -185,11 +187,11 @@ def edit_position(request, name, ballot_id): balloter = login = request.user.person - if 'ballot_edit_return_point' in request.session: - return_to_url = request.session['ballot_edit_return_point'] - else: - return_to_url = urlreverse("ietf.doc.views_doc.document_ballot", kwargs=dict(name=doc.name, ballot_id=ballot_id)) - + try: + return_to_url = parse_ballot_edit_return_point(request.GET.get('ballot_edit_return_point'), doc.name, ballot_id) + except ValueError: + return HttpResponseBadRequest('ballot_edit_return_point is invalid') + # if we're in the Secretariat, we can select a balloter to act as stand-in for if has_role(request.user, "Secretariat"): balloter_id = request.GET.get('balloter') @@ -209,9 +211,14 @@ def edit_position(request, name, ballot_id): save_position(form, doc, ballot, balloter, login, send_mail) if send_mail: - qstr="" + query = {} if request.GET.get('balloter'): - qstr += "?balloter=%s" % request.GET.get('balloter') + query['balloter'] = request.GET.get('balloter') + if request.GET.get('ballot_edit_return_point'): + query['ballot_edit_return_point'] = request.GET.get('ballot_edit_return_point') + qstr = "" + if len(query) > 0: + qstr = "?" + urllib_urlencode(query, safe='/') return HttpResponseRedirect(urlreverse('ietf.doc.views_ballot.send_ballot_comment', kwargs=dict(name=doc.name, ballot_id=ballot_id)) + qstr) elif request.POST.get("Defer") and doc.stream.slug != "irtf": return redirect('ietf.doc.views_ballot.defer_ballot', name=doc) @@ -337,11 +344,11 @@ def send_ballot_comment(request, name, ballot_id): balloter = request.user.person - if 'ballot_edit_return_point' in request.session: - return_to_url = request.session['ballot_edit_return_point'] - else: - return_to_url = urlreverse("ietf.doc.views_doc.document_ballot", kwargs=dict(name=doc.name, ballot_id=ballot_id)) - + try: + return_to_url = parse_ballot_edit_return_point(request.GET.get('ballot_edit_return_point'), doc.name, ballot_id) + except ValueError: + return HttpResponseBadRequest('ballot_edit_return_point is invalid') + if 'HTTP_REFERER' in request.META: back_url = request.META['HTTP_REFERER'] else: @@ -1302,3 +1309,15 @@ def rsab_ballot_status(request): # Possible TODO: add a menu item to show this? Maybe only if you're in rsab or an rswg chair? # There will be so few of these that the general community would follow them from the rswg docs page. # Maybe the view isn't actually needed at all... + + +def parse_ballot_edit_return_point(path, doc_name, ballot_id): + get_default_path = lambda: urlreverse("ietf.doc.views_doc.document_ballot", kwargs=dict(name=doc_name, ballot_id=ballot_id)) + allowed_path_handlers = { + "ietf.doc.views_doc.document_ballot", + "ietf.doc.views_doc.document_irsg_ballot", + "ietf.doc.views_doc.document_rsab_ballot", + "ietf.iesg.views.agenda", + "ietf.iesg.views.agenda_documents", + } + return validate_return_to_path(path, get_default_path, allowed_path_handlers) diff --git a/ietf/doc/views_doc.py b/ietf/doc/views_doc.py index bd49275082..0ef132c215 100644 --- a/ietf/doc/views_doc.py +++ b/ietf/doc/views_doc.py @@ -1538,7 +1538,6 @@ def document_ballot(request, name, ballot_id=None): top = render_document_top(request, doc, ballot_tab, name) c = document_ballot_content(request, doc, ballot.id, editable=True) - request.session['ballot_edit_return_point'] = request.path_info return render(request, "doc/document_ballot.html", dict(doc=doc, @@ -1556,8 +1555,6 @@ def document_irsg_ballot(request, name, ballot_id=None): c = document_ballot_content(request, doc, ballot_id, editable=True) - request.session['ballot_edit_return_point'] = request.path_info - return render(request, "doc/document_ballot.html", dict(doc=doc, top=top, @@ -1575,8 +1572,6 @@ def document_rsab_ballot(request, name, ballot_id=None): c = document_ballot_content(request, doc, ballot_id, editable=True) - request.session['ballot_edit_return_point'] = request.path_info - return render( request, "doc/document_ballot.html", diff --git a/ietf/iesg/views.py b/ietf/iesg/views.py index b67ef04a03..a92d617ac5 100644 --- a/ietf/iesg/views.py +++ b/ietf/iesg/views.py @@ -209,7 +209,6 @@ def agenda(request, date=None): urlreverse("ietf.iesg.views.telechat_agenda_content_view", kwargs={"section": "minutes"}) )) - request.session['ballot_edit_return_point'] = request.path_info return render(request, "iesg/agenda.html", { "date": data["date"], "sections": sorted(data["sections"].items(), key=lambda x:[int(p) for p in x[0].split('.')]), @@ -398,7 +397,7 @@ def agenda_documents(request): "sections": sorted((num, section) for num, section in sections.items() if "2" <= num < "5") }) - request.session['ballot_edit_return_point'] = request.path_info + return render(request, 'iesg/agenda_documents.html', { 'telechats': telechats }) def past_documents(request): diff --git a/ietf/templates/doc/ballot_popup.html b/ietf/templates/doc/ballot_popup.html index 2a04ffab69..2c0863ab6c 100644 --- a/ietf/templates/doc/ballot_popup.html +++ b/ietf/templates/doc/ballot_popup.html @@ -27,7 +27,7 @@ {% if editable and user|has_role:"Area Director,Secretariat,IRSG Member,RSAB Member" %} {% if user|can_ballot:doc %} + href="{% url "ietf.doc.views_ballot.edit_position" name=doc.name ballot_id=ballot_id %}?ballot_edit_return_point={{ request.path|urlencode }}"> Edit position {% endif %} diff --git a/ietf/templates/doc/document_ballot_content.html b/ietf/templates/doc/document_ballot_content.html index 803ed84a36..e0feb78bc7 100644 --- a/ietf/templates/doc/document_ballot_content.html +++ b/ietf/templates/doc/document_ballot_content.html @@ -60,7 +60,7 @@ {% if user|can_ballot:doc %} + href="{% url "ietf.doc.views_ballot.edit_position" name=doc.name ballot_id=ballot.pk %}?ballot_edit_return_point={{ request.path|urlencode }}"> Edit position {% endif %} diff --git a/ietf/utils/http.py b/ietf/utils/http.py index 6e6409e31f..cda51680ab 100644 --- a/ietf/utils/http.py +++ b/ietf/utils/http.py @@ -1,6 +1,8 @@ -# Copyright The IETF Trust 2023, All Rights Reserved +# Copyright The IETF Trust 2023-2024, All Rights Reserved # -*- coding: utf-8 -*- +from django.urls import resolve as urlresolve, Resolver404 + def is_ajax(request): """Checks whether a request was an AJAX call @@ -8,3 +10,25 @@ def is_ajax(request): exact reproduction of the deprecated method suggested there. """ return request.headers.get("x-requested-with") == "XMLHttpRequest" + +def validate_return_to_path(path, get_default_path, allowed_path_handlers): + if path is None: + path = get_default_path() + + # we need to ensure the path isn't used for attacks (eg phishing). + # `path` can be used in HttpResponseRedirect() which could redirect to Datatracker or offsite. + # Eg http://datatracker.ietf.org/...?ballot_edit_return_point=https://example.com/phish + # offsite links could be phishing attempts so let's reject them all, and require valid Datatracker + # routes + try: + # urlresolve will throw if the url doesn't match a route known to Django + match = urlresolve(path) + # further restrict by whether it's in the list of valid routes to prevent + # (eg) redirecting to logout + if match.url_name not in allowed_path_handlers: + raise ValueError("Invalid return to path not among valid matches") + pass + except Resolver404: + raise ValueError("Invalid return to path doesn't match a route") + + return path diff --git a/playwright/package.json b/playwright/package.json index 874faa824e..5e3ba94782 100644 --- a/playwright/package.json +++ b/playwright/package.json @@ -3,6 +3,7 @@ "install-deps": "playwright install --with-deps", "test": "playwright test", "test:legacy": "playwright test -c playwright-legacy.config.js", + "test:legacy:visual": "playwright test -c playwright-legacy.config.js --headed --workers=1", "test:visual": "playwright test --headed --workers=1", "test:debug": "playwright test --debug" }, From 00d61432934a61e1a1c204fadb218874237d382c Mon Sep 17 00:00:00 2001 From: Jennifer Richards Date: Wed, 14 Aug 2024 13:43:21 -0300 Subject: [PATCH 3/8] fix: guard against missing ipr revisions (#7826) * fix: guard against missing "revisions" * test: add tests for DraftForm Only testing the part relevant to this issue. * test: clarify intentions * chore: "document" -> "draft" in comments --- ietf/ipr/forms.py | 2 +- ietf/ipr/tests.py | 56 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/ietf/ipr/forms.py b/ietf/ipr/forms.py index 8ea179789b..62d3f9c216 100644 --- a/ietf/ipr/forms.py +++ b/ietf/ipr/forms.py @@ -112,7 +112,7 @@ def clean(self): if not document: self.add_error("document", "Identifying the Internet-Draft or RFC for this disclosure is required.") elif not document.name.startswith("rfc"): - if revisions.strip() == "": + if revisions is None or revisions.strip() == "": self.add_error("revisions", "Revisions of this Internet-Draft for which this disclosure is relevant must be specified.") return cleaned_data diff --git a/ietf/ipr/tests.py b/ietf/ipr/tests.py index 3a91c9dfb7..5b6a5d5d58 100644 --- a/ietf/ipr/tests.py +++ b/ietf/ipr/tests.py @@ -28,9 +28,11 @@ from ietf.ipr.factories import ( HolderIprDisclosureFactory, GenericIprDisclosureFactory, + IprDisclosureBaseFactory, IprDocRelFactory, IprEventFactory ) +from ietf.ipr.forms import DraftForm from ietf.ipr.mail import (process_response_email, get_reply_to, get_update_submitter_emails, get_pseudo_submitter, get_holders, get_update_cc_addrs) from ietf.ipr.models import (IprDisclosureBase,GenericIprDisclosure,HolderIprDisclosure, @@ -935,3 +937,57 @@ def test_no_revisions_message(self): no_revisions_message(iprdocrel), "No revisions for this Internet-Draft were specified in this disclosure. However, there is only one revision of this Internet-Draft." ) + + +class DraftFormTests(TestCase): + def setUp(self): + super().setUp() + self.disclosure = IprDisclosureBaseFactory() + self.draft = WgDraftFactory() + self.rfc = RfcFactory() + + def test_revisions_valid(self): + post_data = { + "document": str(self.draft.pk), + "disclosure": str(self.disclosure.pk), + } + # The revisions field is just a char field that allows descriptions of the applicable + # document revisions. It's usually just a rev or "00-02", but the form allows anything + # not empty. The secretariat will review the value before the disclosure is posted so + # minimal validation is ok here. + self.assertTrue(DraftForm(post_data | {"revisions": "00"}).is_valid()) + self.assertTrue(DraftForm(post_data | {"revisions": "00-02"}).is_valid()) + self.assertTrue(DraftForm(post_data | {"revisions": "01,03, 05"}).is_valid()) + self.assertTrue(DraftForm(post_data | {"revisions": "all but 01"}).is_valid()) + # RFC instead of draft - allow empty / missing revisions + post_data["document"] = str(self.rfc.pk) + self.assertTrue(DraftForm(post_data).is_valid()) + self.assertTrue(DraftForm(post_data | {"revisions": ""}).is_valid()) + + def test_revisions_invalid(self): + missing_rev_error_msg = ( + "Revisions of this Internet-Draft for which this disclosure is relevant must be specified." + ) + null_char_error_msg = "Null characters are not allowed." + + post_data = { + "document": str(self.draft.pk), + "disclosure": str(self.disclosure.pk), + } + self.assertFormError( + DraftForm(post_data), "revisions", missing_rev_error_msg + ) + self.assertFormError( + DraftForm(post_data | {"revisions": ""}), "revisions", missing_rev_error_msg + ) + self.assertFormError( + DraftForm(post_data | {"revisions": "1\x00"}), + "revisions", + [null_char_error_msg, missing_rev_error_msg], + ) + # RFC instead of draft still validates the revisions field + self.assertFormError( + DraftForm(post_data | {"document": str(self.rfc.pk), "revisions": "1\x00"}), + "revisions", + null_char_error_msg, + ) From bb0b3427167b91d434a2b751718df7ad7fa976f8 Mon Sep 17 00:00:00 2001 From: Jennifer Richards Date: Thu, 15 Aug 2024 18:33:14 -0300 Subject: [PATCH 4/8] test: fix DraftFormTest setup (#7828) * test: fix DraftFormTest setup Tests were failing when the doc factories started producing pks >= 10 because of an incorrect test data structure. * chore: improve comments --- ietf/ipr/tests.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/ietf/ipr/tests.py b/ietf/ipr/tests.py index 5b6a5d5d58..922ae272a9 100644 --- a/ietf/ipr/tests.py +++ b/ietf/ipr/tests.py @@ -943,12 +943,14 @@ class DraftFormTests(TestCase): def setUp(self): super().setUp() self.disclosure = IprDisclosureBaseFactory() - self.draft = WgDraftFactory() + self.draft = WgDraftFactory.create_batch(10)[-1] self.rfc = RfcFactory() def test_revisions_valid(self): post_data = { - "document": str(self.draft.pk), + # n.b., "document" is a SearchableDocumentField, which is a multiple choice field limited + # to a single choice. Its value must be an array of pks with one element. + "document": [str(self.draft.pk)], "disclosure": str(self.disclosure.pk), } # The revisions field is just a char field that allows descriptions of the applicable @@ -960,7 +962,7 @@ def test_revisions_valid(self): self.assertTrue(DraftForm(post_data | {"revisions": "01,03, 05"}).is_valid()) self.assertTrue(DraftForm(post_data | {"revisions": "all but 01"}).is_valid()) # RFC instead of draft - allow empty / missing revisions - post_data["document"] = str(self.rfc.pk) + post_data["document"] = [str(self.rfc.pk)] self.assertTrue(DraftForm(post_data).is_valid()) self.assertTrue(DraftForm(post_data | {"revisions": ""}).is_valid()) @@ -971,7 +973,9 @@ def test_revisions_invalid(self): null_char_error_msg = "Null characters are not allowed." post_data = { - "document": str(self.draft.pk), + # n.b., "document" is a SearchableDocumentField, which is a multiple choice field limited + # to a single choice. Its value must be an array of pks with one element. + "document": [str(self.draft.pk)], "disclosure": str(self.disclosure.pk), } self.assertFormError( @@ -987,7 +991,7 @@ def test_revisions_invalid(self): ) # RFC instead of draft still validates the revisions field self.assertFormError( - DraftForm(post_data | {"document": str(self.rfc.pk), "revisions": "1\x00"}), + DraftForm(post_data | {"document": [str(self.rfc.pk)], "revisions": "1\x00"}), "revisions", null_char_error_msg, ) From f53d1cae65e3462af485ac10a55e0ba7cf0a6828 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Wed, 21 Aug 2024 08:29:09 -0500 Subject: [PATCH 5/8] fix: remove unused notes field from SchedSessTimeAssignment model (#7832) --- ...0008_remove_schedtimesessassignment_notes.py | 17 +++++++++++++++++ ietf/meeting/models.py | 3 +-- ietf/meeting/resources.py | 1 - 3 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 ietf/meeting/migrations/0008_remove_schedtimesessassignment_notes.py diff --git a/ietf/meeting/migrations/0008_remove_schedtimesessassignment_notes.py b/ietf/meeting/migrations/0008_remove_schedtimesessassignment_notes.py new file mode 100644 index 0000000000..3c0b85fc22 --- /dev/null +++ b/ietf/meeting/migrations/0008_remove_schedtimesessassignment_notes.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.15 on 2024-08-16 13:49 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("meeting", "0007_attended_origin_attended_time"), + ] + + operations = [ + migrations.RemoveField( + model_name="schedtimesessassignment", + name="notes", + ), + ] diff --git a/ietf/meeting/models.py b/ietf/meeting/models.py index d8a069ef3c..693cb99dfd 100644 --- a/ietf/meeting/models.py +++ b/ietf/meeting/models.py @@ -788,7 +788,6 @@ class SchedTimeSessAssignment(models.Model): schedule = ForeignKey('Schedule', null=False, blank=False, related_name='assignments') extendedfrom = ForeignKey('self', null=True, default=None, help_text="Timeslot this session is an extension of.") modified = models.DateTimeField(auto_now=True) - notes = models.TextField(blank=True) badness = models.IntegerField(default=0, blank=True, null=True) pinned = models.BooleanField(default=False, help_text="Do not move session during automatic placement.") @@ -1423,7 +1422,7 @@ class MeetingHost(models.Model): validate_file_extension, settings.MEETING_VALID_UPLOAD_EXTENSIONS['meetinghostlogo'], ), - WrappedValidator( + WrappedValidator( validate_mime_type, settings.MEETING_VALID_UPLOAD_MIME_TYPES['meetinghostlogo'], True, diff --git a/ietf/meeting/resources.py b/ietf/meeting/resources.py index dc273c04cf..de9ca01476 100644 --- a/ietf/meeting/resources.py +++ b/ietf/meeting/resources.py @@ -269,7 +269,6 @@ class Meta: filtering = { "id": ALL, "modified": ALL, - "notes": ALL, "badness": ALL, "pinned": ALL, "timeslot": ALL_WITH_RELATIONS, From c323cdde036593244ddcec218c0ffcedda56e792 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Wed, 21 Aug 2024 08:32:06 -0500 Subject: [PATCH 6/8] fix: remove unused internal_comments from the DocumentInfo abstract model (#7833) --- ...e_dochistory_internal_comments_and_more.py | 21 +++++++++++++++++++ ietf/doc/models.py | 1 - ietf/doc/resources.py | 2 -- ietf/secr/static/css/custom.css | 7 +------ 4 files changed, 22 insertions(+), 9 deletions(-) create mode 100644 ietf/doc/migrations/0022_remove_dochistory_internal_comments_and_more.py diff --git a/ietf/doc/migrations/0022_remove_dochistory_internal_comments_and_more.py b/ietf/doc/migrations/0022_remove_dochistory_internal_comments_and_more.py new file mode 100644 index 0000000000..ad27793a83 --- /dev/null +++ b/ietf/doc/migrations/0022_remove_dochistory_internal_comments_and_more.py @@ -0,0 +1,21 @@ +# Generated by Django 4.2.15 on 2024-08-16 16:43 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("doc", "0021_narrativeminutes"), + ] + + operations = [ + migrations.RemoveField( + model_name="dochistory", + name="internal_comments", + ), + migrations.RemoveField( + model_name="document", + name="internal_comments", + ), + ] diff --git a/ietf/doc/models.py b/ietf/doc/models.py index a103fca645..639e6ca857 100644 --- a/ietf/doc/models.py +++ b/ietf/doc/models.py @@ -122,7 +122,6 @@ class DocumentInfo(models.Model): external_url = models.URLField(blank=True) uploaded_filename = models.TextField(blank=True) note = models.TextField(blank=True) - internal_comments = models.TextField(blank=True) rfc_number = models.PositiveIntegerField(blank=True, null=True) # only valid for type="rfc" def file_extension(self): diff --git a/ietf/doc/resources.py b/ietf/doc/resources.py index 6bb6ffa281..bba57013b9 100644 --- a/ietf/doc/resources.py +++ b/ietf/doc/resources.py @@ -130,7 +130,6 @@ class Meta: "external_url": ALL, "uploaded_filename": ALL, "note": ALL, - "internal_comments": ALL, "name": ALL, "type": ALL_WITH_RELATIONS, "stream": ALL_WITH_RELATIONS, @@ -247,7 +246,6 @@ class Meta: "external_url": ALL, "uploaded_filename": ALL, "note": ALL, - "internal_comments": ALL, "name": ALL, "type": ALL_WITH_RELATIONS, "stream": ALL_WITH_RELATIONS, diff --git a/ietf/secr/static/css/custom.css b/ietf/secr/static/css/custom.css index 8816b3f13d..8a622cba5d 100644 --- a/ietf/secr/static/css/custom.css +++ b/ietf/secr/static/css/custom.css @@ -319,11 +319,6 @@ input.draft-file-input { width: 4em; } -.draft-container #id_internal_comments { - height: 4em; - width: 40em; -} - .draft-container #id_abstract { height: 15em; width: 40em; @@ -842,4 +837,4 @@ td, th, li, h2 { thead th { font-size: 12px; -} \ No newline at end of file +} From 9b4671c076d5393117abda5faf6256dd8dcdc62a Mon Sep 17 00:00:00 2001 From: Kesara Rathnayake Date: Fri, 23 Aug 2024 02:25:03 +1200 Subject: [PATCH 7/8] fix: Install weasyprint as xml2rfc extra (#7845) Since xml2rfc v3.23.0, WeasyPrint version is pinned. --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 8187c1cebf..88b78e665b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -71,5 +71,5 @@ tqdm>=4.64.0 Unidecode>=1.3.4 urllib3>=2 weasyprint>=59 -xml2rfc>=3.12.4 +xml2rfc[pdf]>=3.23.0 xym>=0.6,<1.0 From 2230242b9b7c6f3c64a0e32d96ce6ac004dffe54 Mon Sep 17 00:00:00 2001 From: Jennifer Richards Date: Thu, 22 Aug 2024 12:34:52 -0300 Subject: [PATCH 8/8] refactor: speed up finalizing proceedings (#7846) * chore: increase nginx proxy_read_timeout * refactor: speed up bluesheet_data The affiliation helper was extremely slow. Using queryset annotation speeds it up by almost an order of magnitude. * chore: delint --- ietf/meeting/utils.py | 31 ++++++++++++++++++++----------- k8s/nginx-auth.conf | 4 ++++ k8s/nginx-datatracker.conf | 4 ++++ 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/ietf/meeting/utils.py b/ietf/meeting/utils.py index e3d8c830ee..b68a311f5d 100644 --- a/ietf/meeting/utils.py +++ b/ietf/meeting/utils.py @@ -12,7 +12,8 @@ from django.conf import settings from django.contrib import messages -from django.db.models import Q +from django.db.models import OuterRef, Subquery, TextField, Q, Value +from django.db.models.functions import Coalesce from django.template.loader import render_to_string from django.utils import timezone from django.utils.encoding import smart_str @@ -149,19 +150,27 @@ def create_proceedings_templates(meeting): def bluesheet_data(session): - def affiliation(meeting, person): - # from OidcExtraScopeClaims.scope_registration() - email_list = person.email_set.values_list("address") - q = Q(person=person, meeting=meeting) | Q(email__in=email_list, meeting=meeting) - reg = MeetingRegistration.objects.filter(q).exclude(affiliation="").first() - return reg.affiliation if reg else "" - - attendance = Attended.objects.filter(session=session).order_by("time") - meeting = session.meeting + attendance = ( + Attended.objects.filter(session=session) + .annotate( + affiliation=Coalesce( + Subquery( + MeetingRegistration.objects.filter( + Q(meeting=session.meeting), + Q(person=OuterRef("person")) | Q(email=OuterRef("person__email")), + ).values("affiliation")[:1] + ), + Value(""), + output_field=TextField(), + ) + ).distinct() + .order_by("time") + ) + return [ { "name": attended.person.plain_name(), - "affiliation": affiliation(meeting, attended.person), + "affiliation": attended.affiliation, } for attended in attendance ] diff --git a/k8s/nginx-auth.conf b/k8s/nginx-auth.conf index 6dd5d6ed56..a38b8f50c7 100644 --- a/k8s/nginx-auth.conf +++ b/k8s/nginx-auth.conf @@ -34,5 +34,9 @@ server { proxy_set_header X-Forwarded-For $${keepempty}proxy_add_x_forwarded_for; proxy_set_header X-Real-IP $${keepempty}remote_addr; proxy_pass http://localhost:8000; + # Set timeouts longer than Cloudflare proxy limits + proxy_connect_timeout 60; # nginx default (Cf = 15) + proxy_read_timeout 120; # nginx default = 60 (Cf = 100) + proxy_send_timeout 60; # nginx default = 60 (Cf = 30) } } diff --git a/k8s/nginx-datatracker.conf b/k8s/nginx-datatracker.conf index 5cbc22e6c7..7c0dc85fd0 100644 --- a/k8s/nginx-datatracker.conf +++ b/k8s/nginx-datatracker.conf @@ -23,6 +23,10 @@ server { proxy_set_header X-Forwarded-For $${keepempty}proxy_add_x_forwarded_for; proxy_set_header X-Real-IP $${keepempty}remote_addr; proxy_pass http://localhost:8000; + # Set timeouts longer than Cloudflare proxy limits + proxy_connect_timeout 60; # nginx default (Cf = 15) + proxy_read_timeout 120; # nginx default = 60 (Cf = 100) + proxy_send_timeout 60; # nginx default = 60 (Cf = 30) client_max_body_size 0; # disable size check } }