From 94955d438f7b87607b0b9c292c28c21574b4e661 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Thu, 14 May 2026 12:44:26 -0500 Subject: [PATCH 1/3] feat: show rfc titlepage names (#10863) * feat: use titlepage_name for person_links when it exists * test: test using titlepage_name when present, and test html rendering --- ietf/person/templatetags/person_filters.py | 2 + ietf/person/templatetags/tests.py | 65 +++++++++++++++++++++- ietf/templates/doc/document_info.html | 2 +- ietf/templates/person/person_link.html | 2 +- 4 files changed, 68 insertions(+), 3 deletions(-) diff --git a/ietf/person/templatetags/person_filters.py b/ietf/person/templatetags/person_filters.py index 017b29c63ad..a7a6e8193a0 100644 --- a/ietf/person/templatetags/person_filters.py +++ b/ietf/person/templatetags/person_filters.py @@ -50,6 +50,7 @@ def person_link(person, **kwargs): title = kwargs.get("title", "") cls = kwargs.get("class", "") with_email = kwargs.get("with_email", True) + titlepage_name = kwargs.get("titlepage_name", None) if person is not None: plain_name = person.plain_name() name = ( @@ -61,6 +62,7 @@ def person_link(person, **kwargs): return { "name": name, "plain_name": plain_name, + "titlepage_name": titlepage_name, "email": email, "title": title, "class": cls, diff --git a/ietf/person/templatetags/tests.py b/ietf/person/templatetags/tests.py index 327cfad6ce6..7c35fd6b695 100644 --- a/ietf/person/templatetags/tests.py +++ b/ietf/person/templatetags/tests.py @@ -1,4 +1,6 @@ # Copyright The IETF Trust 2022, All Rights Reserved +from django.template.loader import render_to_string + from ietf.person.factories import PersonFactory from ietf.utils.test_utils import TestCase @@ -8,7 +10,6 @@ class PersonLinkTests(TestCase): # Tests of the person_link template tag. These assume it is implemented as an # inclusion tag. - # TODO test that the template actually renders the data in the dict def test_person_link(self): person = PersonFactory() self.assertEqual( @@ -16,6 +17,7 @@ def test_person_link(self): { 'name': person.name, 'plain_name': person.plain_name(), + 'titlepage_name': None, 'email': person.email_address(), 'title': '', 'class': '', @@ -27,6 +29,7 @@ def test_person_link(self): { 'name': person.name, 'plain_name': person.plain_name(), + 'titlepage_name': None, 'email': person.email_address(), 'title': '', 'class': '', @@ -38,6 +41,7 @@ def test_person_link(self): { 'name': person.name, 'plain_name': person.plain_name(), + 'titlepage_name': None, 'email': person.email_address(), 'title': 'Random Title', 'class': '', @@ -50,12 +54,71 @@ def test_person_link(self): { 'name': person.name, 'plain_name': person.plain_name(), + 'titlepage_name': None, 'email': person.email_address(), 'title': '', 'class': 'some-class', 'with_email': True, } ) + self.assertEqual( + person_link(person, titlepage_name='G. Surname'), + { + 'name': person.name, + 'plain_name': person.plain_name(), + 'titlepage_name': 'G. Surname', + 'email': person.email_address(), + 'title': '', + 'class': '', + 'with_email': True, + } + ) + + def test_person_link_renders(self): + """Verifies person/person_link.html renders context dict values correctly.""" + person = PersonFactory() + name = person.name + email = person.email_address() + base_context = { + 'name': name, + 'plain_name': person.plain_name(), + 'titlepage_name': None, + 'email': email, + 'title': '', + 'class': '', + 'with_email': True, + } + + # Default: name is used as link text with default title attribute + html = render_to_string('person/person_link.html', base_context) + self.assertIn(f'>{name}', html) + self.assertIn(f'Datatracker profile of {name}', html) + self.assertIn('bi-envelope', html) + + # titlepage_name overrides name as link text + html = render_to_string('person/person_link.html', {**base_context, 'titlepage_name': 'G. Surname'}) + self.assertIn('>G. Surname', html) + self.assertNotIn(f'>{name}', html) + + # with_email=False suppresses the envelope link + html = render_to_string('person/person_link.html', {**base_context, 'with_email': False}) + self.assertNotIn('bi-envelope', html) + + # Custom title appears in the anchor title attribute + html = render_to_string('person/person_link.html', {**base_context, 'title': 'Special Title'}) + self.assertIn('title="Special Title"', html) + + # Empty context (None person) renders (None) + self.assertInHTML( + '(None)', + render_to_string('person/person_link.html', {}), + ) + + # System email renders (System) + self.assertInHTML( + '(System)', + render_to_string('person/person_link.html', {'email': 'system@datatracker.ietf.org', 'name': ''}), + ) def test_invalid_person(self): """Generates correct context dict when input is invalid/missing""" diff --git a/ietf/templates/doc/document_info.html b/ietf/templates/doc/document_info.html index d6d8d430719..1666c42ae51 100644 --- a/ietf/templates/doc/document_info.html +++ b/ietf/templates/doc/document_info.html @@ -97,7 +97,7 @@ {# Implementation that uses the current primary email for each author #} {% if doc.pk %}{% for author in doc.author_persons_or_names %} - {% if author.person %}{% person_link author.person %}{% else %}{{ author.titlepage_name }}{% endif %}{% if not forloop.last %},{% endif %} + {% if author.person %}{% person_link author.person titlepage_name=author.titlepage_name %}{% else %}{{ author.titlepage_name }}{% endif %}{% if not forloop.last %},{% endif %} {% endfor %}{% endif %} {% if document_html and not snapshot or document_html and doc.rev == latest_rev%}
diff --git a/ietf/templates/person/person_link.html b/ietf/templates/person/person_link.html index f3f7e1a5b7a..b77fe8d6dfd 100644 --- a/ietf/templates/person/person_link.html +++ b/ietf/templates/person/person_link.html @@ -1,7 +1,7 @@ {% if email and email == "system@datatracker.ietf.org" or name and name == "(System)" %}(System){% else %}{% if email or name %}{{ name }}{% if email and with_email %} {% if titlepage_name %}{{ titlepage_name }}{% else %}{{ name }}{% endif %}{% if email and with_email %} From 31dcaeb490f916a5d31ea3299957a3269b0b4727 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Thu, 14 May 2026 13:33:46 -0500 Subject: [PATCH 2/3] feat: disable editing DocumentAuthors when RfcAuthors exist (#10864) * fix: disable editing DocumentAuthors when RfcAuthors exist * test: test disabling edit when rfcauthors exist * chore: consistent use of RFC Editor --- ietf/doc/tests.py | 28 ++++++++++++++++++++++++++++ ietf/doc/views_doc.py | 9 ++++++--- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/ietf/doc/tests.py b/ietf/doc/tests.py index f92c9648e64..b001e01e803 100644 --- a/ietf/doc/tests.py +++ b/ietf/doc/tests.py @@ -1023,6 +1023,34 @@ def test_edit_authors_permissions(self): draft = Document.objects.get(pk=draft.pk) self.assertEqual(draft.author_persons(), orig_authors + [new_auth_person]) + def test_edit_authors_blocked_when_rfcauthors_exist(self): + """edit_authors returns 403 for all users when RfcAuthors exist""" + rfc = WgRfcFactory() + RfcAuthorFactory(document=rfc) + url = urlreverse('ietf.doc.views_doc.edit_authors', kwargs=dict(name=rfc.name)) + + self.client.login(username='secretary', password='secretary+password') + r = self.client.get(url) + self.assertEqual(r.status_code, 403) + r = self.client.post(url, {}) + self.assertEqual(r.status_code, 403) + + def test_document_main_hides_edit_authors_when_rfcauthors_exist(self): + """document_main does not offer edit link for authors when RfcAuthors exist""" + rfc = WgRfcFactory() + edit_authors_url = urlreverse('ietf.doc.views_doc.edit_authors', kwargs=dict(name=rfc.name)) + + self.client.login(username='secretary', password='secretary+password') + + r = self.client.get(urlreverse('ietf.doc.views_doc.document_main', kwargs=dict(name=rfc.name))) + self.assertEqual(r.status_code, 200) + self.assertContains(r, edit_authors_url) + + RfcAuthorFactory(document=rfc) + r = self.client.get(urlreverse('ietf.doc.views_doc.document_main', kwargs=dict(name=rfc.name))) + self.assertEqual(r.status_code, 200) + self.assertNotContains(r, edit_authors_url) + def make_edit_authors_post_data(self, basis, authors): """Helper to generate edit_authors POST data for a set of authors""" def _add_prefix(s): diff --git a/ietf/doc/views_doc.py b/ietf/doc/views_doc.py index 5b57a620743..af056f6a96b 100644 --- a/ietf/doc/views_doc.py +++ b/ietf/doc/views_doc.py @@ -46,7 +46,7 @@ from django.core.files.base import ContentFile from django.core.exceptions import PermissionDenied from django.db.models import Max -from django.http import FileResponse, HttpResponse, Http404, HttpResponseBadRequest, JsonResponse +from django.http import FileResponse, HttpResponse, Http404, HttpResponseBadRequest, HttpResponseForbidden, JsonResponse from django.shortcuts import render, get_object_or_404, redirect from django.template.loader import render_to_string from django.urls import reverse as urlreverse @@ -258,7 +258,7 @@ def document_main(request, name, rev=None, document_html=False): interesting_relations_that, interesting_relations_that_doc = interesting_doc_relations(doc) can_edit = has_role(request.user, ("Area Director", "Secretariat")) - can_edit_authors = has_role(request.user, ("Secretariat")) + can_edit_authors = has_role(request.user, ("Secretariat")) and not doc.rfcauthor_set.exists() stream_slugs = StreamName.objects.values_list("slug", flat=True) # For some reason, AnonymousUser has __iter__, but is not iterable, @@ -1842,12 +1842,15 @@ def add_fields(self, form, index): if fh in form.fields: form.fields[fh].widget = forms.HiddenInput() + doc = get_object_or_404(Document, name=name) + if doc.rfcauthor_set.exists(): + return HttpResponseForbidden("Contact the RFC Editor to change RFC Author information") + AuthorFormSet = forms.formset_factory(DocAuthorForm, formset=_AuthorsBaseFormSet, can_delete=True, can_order=True, extra=0) - doc = get_object_or_404(Document, name=name) if request.method == 'POST': change_basis_form = DocAuthorChangeBasisForm(request.POST) From 1e5f5db9c24db321097dc70849783eff2c3f8057 Mon Sep 17 00:00:00 2001 From: Sai Asish Y Date: Thu, 14 May 2026 12:52:05 -0700 Subject: [PATCH 3/3] fix: avoid PosixPath.endswith() in run_yang_model_checks_task (#10819) Fixes #10818 Signed-off-by: SAY-5 Co-authored-by: SAY-5 --- ietf/submit/utils.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ietf/submit/utils.py b/ietf/submit/utils.py index 7e3106f723c..457462e4f2c 100644 --- a/ietf/submit/utils.py +++ b/ietf/submit/utils.py @@ -1612,6 +1612,4 @@ def active(dirent): log.log(f"Error processing {item.name}: {e}") ftp_moddir = Path(settings.FTP_DIR) / "yang" / "draftmod/" - if not moddir.endswith("/"): - moddir += "/" - subprocess.call(("/usr/bin/rsync", "-aq", "--delete", moddir, ftp_moddir)) + subprocess.call(("/usr/bin/rsync", "-aq", "--delete", f"{moddir}/", str(ftp_moddir)))