From 314b8a7d8cf73dae3d915662fc39118864159c90 Mon Sep 17 00:00:00 2001 From: Jennifer Richards Date: Wed, 25 Feb 2026 21:55:02 -0400 Subject: [PATCH 01/14] feat: more editable RFC fields for API (WIP) Checkpoint commit! --- ietf/api/serializers_rpc.py | 193 ++++++++++++++++++++++++++++++++++-- ietf/api/views_rpc.py | 27 ++++- 2 files changed, 209 insertions(+), 11 deletions(-) diff --git a/ietf/api/serializers_rpc.py b/ietf/api/serializers_rpc.py index 34e2c791c0..ee9ee86a9c 100644 --- a/ietf/api/serializers_rpc.py +++ b/ietf/api/serializers_rpc.py @@ -216,6 +216,16 @@ class Meta: read_only_fields = ["id", "name"] +def _update_authors(rfc, authors_data): + # Construct unsaved instances from validated author data + new_authors = [RfcAuthor(**ad) for ad in authors_data] + # Update the RFC with the new author set + with transaction.atomic(): + change_events = update_rfcauthors(rfc, new_authors) + for event in change_events: + event.save() + return change_events + class EditableRfcSerializer(serializers.ModelSerializer): # Would be nice to reconcile this with ietf.doc.serializers.RfcSerializer. # The purposes of that serializer (representing data for Red) and this one @@ -232,15 +242,10 @@ class Meta: def update(self, instance, validated_data): assert isinstance(instance, Document) + assert instance.type_id == "rfc" authors_data = validated_data.pop("rfcauthor_set", None) if authors_data is not None: - # Construct unsaved instances from validated author data - new_authors = [RfcAuthor(**ad) for ad in authors_data] - # Update the RFC with the new author set - with transaction.atomic(): - change_events = update_rfcauthors(instance, new_authors) - for event in change_events: - event.save() + _update_authors(instance, authors_data) return instance @@ -327,6 +332,9 @@ def validate(self, data): ) return data + def update(self, instance, validated_data): + raise RuntimeError("Cannot update with this serializer") + def create(self, validated_data): """Publish an RFC""" published = validated_data.pop("published") @@ -515,6 +523,177 @@ def _create_rfc(self, validated_data): return rfc +class RfcAmendMetadataSerializer(serializers.ModelSerializer): + published = serializers.DateTimeField(default_timezone=datetime.timezone.utc) + authors = RfcAuthorSerializer(source="rfcauthor_set", many=True) + subseries = serializers.ListField( + child=serializers.RegexField( + required=False, + # pattern: no leading 0, finite length (arbitrarily set to 5 digits) + regex=r"^(bcp|std|fyi)[1-9][0-9]{0,4}$", + ) + ) + + class Meta: + model = Document + fields = [ + "published", + "title", + "authors", + "stream", + "abstract", + "pages", + "std_level", + "subseries", + ] + + def create(self, validated_data): + raise RuntimeError("Cannot create with this serializer") + + def update(self, instance, validated_data): + assert isinstance(instance, Document) + assert instance.type_id == "rfc" + rfc = instance # get better name + breakpoint() + + system_person = Person.objects.get(name="(System)") + + # Remove data that needs special handling. Use a singleton object to detect + # missing values in case we ever support a value that needs None as an option. + omitted = object() + published = validated_data.pop("published", omitted) + subseries = validated_data.pop("subseries", omitted) + authors_data = validated_data.pop("authors", omitted) + + # Transaction to clean up if something fails + with transaction.atomic(): + # update the rfc Document itself + rfc_changes = [] + + for attr, new_value in validated_data.items(): + old_value = getattr(instance, attr) + if new_value != old_value: + rfc_changes.append( + f"changed {attr} to '{new_value}' from '{old_value}'" + ) + setattr(instance, attr, new_value) + if len(rfc_changes) > 0: + rfc_change_summary = f" ({', '.join(rfc_changes)})" + else: + rfc_change_summary = "" + rfc_events = [ + ( + DocEvent.objects.create( + doc=rfc, + rev=rfc.rev, + by=system_person, + type="sync_from_rfc_editor", + desc=( + "Metadata changes received from RFC Editor" + + rfc_change_summary + ), + ) + ) + ] + if authors_data is not omitted: + rfc_events.extend(_update_authors(instance, authors_data)) + + if published is not omitted: + published_event = rfc.latest_event(type="published_rfc") + if published_event is None: + # unexpected, but possible in theory + rfc_events.append( + DocEvent.objects.create( + doc=rfc, + rev=rfc.rev, + type="published_rfc", + time=published, + by=system_person, + desc="RFC published", + ) + ) + rfc_events.append( + DocEvent.objects.create( + doc=rfc, + rev=rfc.rev, + type="sync_from_rfc_editor", + by=system_person, + desc=( + f"Set publication timestamp to {published.isoformat()}" + ), + ) + ) + else: + original_pub_time = published_event.time + if published != original_pub_time: + published_event.time = published + published_event.save() + rfc_events.append( + DocEvent.objects.create( + doc=rfc, + rev=rfc.rev, + type="sync_from_rfc_editor", + by=system_person, + desc=( + f"Changed publication time to " + f"{published.isoformat()} from " + f"{original_pub_time.isoformat()}" + ) + ) + ) + + # update subseries relations + if subseries is not omitted: + for subseries_doc_name in subseries: + ss_slug = subseries_doc_name[:3] + subseries_doc, ss_doc_created = Document.objects.get_or_create( + type_id=ss_slug, name=subseries_doc_name + ) + if ss_doc_created: + subseries_doc.docevent_set.create( + type=f"{ss_slug}_doc_created", + by=system_person, + desc=f"Created {subseries_doc_name} via update of {rfc.name}", + ) + _, ss_rel_created = subseries_doc.relateddocument_set.get_or_create( + relationship_id="contains", target=rfc + ) + if ss_rel_created: + subseries_doc.docevent_set.create( + type="sync_from_rfc_editor", + by=system_person, + desc=f"Added {rfc.name} to {subseries_doc.name}", + ) + rfc_events.append( + rfc.docevent_set.create( + type="sync_from_rfc_editor", + by=system_person, + desc=f"Added {rfc.name} to {subseries_doc.name}", + ) + ) + # Delete subseries relations that are no longer current + stale_subseries_relations = rfc.relations_that("contains").exclude( + source__name__in=subseries + ) + for stale_relation in stale_subseries_relations: + stale_subseries_doc = stale_relation.source + rfc_events.append( + rfc.docevent_set.create( + type="sync_from_rfc_editor", + by=system_person, + desc=f"Removed {rfc.name} from {stale_subseries_doc.name}", + ) + ) + stale_subseries_doc.docevent_set.create( + type="sync_from_rfc_editor", + by=system_person, + desc=f"Removed {rfc.name} from {stale_subseries_doc.name}", + ) + stale_subseries_relations.delete() + rfc.save_with_history(rfc_events) + return rfc + + class RfcFileSerializer(serializers.Serializer): # The structure of this serializer is constrained by what openapi-generator-cli's # python generator can correctly serialize as multipart/form-data. It does not diff --git a/ietf/api/views_rpc.py b/ietf/api/views_rpc.py index 2bf16480f2..a344d690da 100644 --- a/ietf/api/views_rpc.py +++ b/ietf/api/views_rpc.py @@ -33,9 +33,11 @@ RfcWithAuthorsSerializer, DraftWithAuthorsSerializer, NotificationAckSerializer, RfcPubSerializer, RfcFileSerializer, - EditableRfcSerializer, + EditableRfcSerializer, RfcAmendMetadataSerializer, ) -from ietf.doc.models import Document, DocHistory, RfcAuthor +from ietf.doc.api import PrefetchRelatedDocument +from ietf.doc.models import Document, DocHistory, RfcAuthor, SUBSERIES_DOC_TYPE_IDS, \ + DocEvent from ietf.doc.serializers import RfcAuthorSerializer from ietf.doc.storage_utils import remove_from_storage, store_file, exists_in_storage from ietf.person.models import Email, Person @@ -273,10 +275,27 @@ def bulk_authors(self, request): ) ) class RfcViewSet(mixins.UpdateModelMixin, viewsets.GenericViewSet): - queryset = Document.objects.filter(type_id="rfc") + queryset = Document.objects.filter(type_id="rfc").prefetch_related( + PrefetchRelatedDocument( + to_attr="subseries", + relationship_id="contains", + reverse=True, + doc_type_ids=SUBSERIES_DOC_TYPE_IDS, + ) + ).annotate( + published=Subquery( + DocEvent.objects.filter( + doc_id=OuterRef("pk"), + type="published_rfc", + ) + .order_by("-time") + .values("time")[:1] + ), + ) + api_key_endpoint = "ietf.api.views_rpc" lookup_field = "rfc_number" - serializer_class = EditableRfcSerializer + serializer_class = RfcAmendMetadataSerializer @action(detail=False, serializer_class=OriginalStreamSerializer) def rfc_original_stream(self, request): From 6c676f00d7f368cd962fe464ec33e0a2fc8e5c0f Mon Sep 17 00:00:00 2001 From: Jennifer Richards Date: Thu, 26 Feb 2026 16:59:46 -0400 Subject: [PATCH 02/14] chore: avoid requiring prefetch Makes some fields write-only to achieve this. --- ietf/api/serializers_rpc.py | 11 +++++++---- ietf/api/views_rpc.py | 19 +------------------ 2 files changed, 8 insertions(+), 22 deletions(-) diff --git a/ietf/api/serializers_rpc.py b/ietf/api/serializers_rpc.py index ee9ee86a9c..eb60ac5e66 100644 --- a/ietf/api/serializers_rpc.py +++ b/ietf/api/serializers_rpc.py @@ -524,14 +524,18 @@ def _create_rfc(self, validated_data): class RfcAmendMetadataSerializer(serializers.ModelSerializer): - published = serializers.DateTimeField(default_timezone=datetime.timezone.utc) + published = serializers.DateTimeField( + default_timezone=datetime.timezone.utc, + write_only=True, + ) authors = RfcAuthorSerializer(source="rfcauthor_set", many=True) subseries = serializers.ListField( child=serializers.RegexField( required=False, # pattern: no leading 0, finite length (arbitrarily set to 5 digits) regex=r"^(bcp|std|fyi)[1-9][0-9]{0,4}$", - ) + ), + write_only=True, ) class Meta: @@ -554,7 +558,6 @@ def update(self, instance, validated_data): assert isinstance(instance, Document) assert instance.type_id == "rfc" rfc = instance # get better name - breakpoint() system_person = Person.objects.get(name="(System)") @@ -563,7 +566,7 @@ def update(self, instance, validated_data): omitted = object() published = validated_data.pop("published", omitted) subseries = validated_data.pop("subseries", omitted) - authors_data = validated_data.pop("authors", omitted) + authors_data = validated_data.pop("rfcauthor_set", omitted) # Transaction to clean up if something fails with transaction.atomic(): diff --git a/ietf/api/views_rpc.py b/ietf/api/views_rpc.py index a344d690da..8754053659 100644 --- a/ietf/api/views_rpc.py +++ b/ietf/api/views_rpc.py @@ -275,24 +275,7 @@ def bulk_authors(self, request): ) ) class RfcViewSet(mixins.UpdateModelMixin, viewsets.GenericViewSet): - queryset = Document.objects.filter(type_id="rfc").prefetch_related( - PrefetchRelatedDocument( - to_attr="subseries", - relationship_id="contains", - reverse=True, - doc_type_ids=SUBSERIES_DOC_TYPE_IDS, - ) - ).annotate( - published=Subquery( - DocEvent.objects.filter( - doc_id=OuterRef("pk"), - type="published_rfc", - ) - .order_by("-time") - .values("time")[:1] - ), - ) - + queryset = Document.objects.filter(type_id="rfc") api_key_endpoint = "ietf.api.views_rpc" lookup_field = "rfc_number" serializer_class = RfcAmendMetadataSerializer From 94e77967112ecb69af2245cabcdfc775465ac0a8 Mon Sep 17 00:00:00 2001 From: Jennifer Richards Date: Thu, 26 Feb 2026 17:38:33 -0400 Subject: [PATCH 03/14] refactor: replace EditableRfcSerializer --- ietf/api/serializers_rpc.py | 35 +++++++++++------------------------ ietf/api/views_rpc.py | 4 ++-- 2 files changed, 13 insertions(+), 26 deletions(-) diff --git a/ietf/api/serializers_rpc.py b/ietf/api/serializers_rpc.py index eb60ac5e66..1f3d238f8c 100644 --- a/ietf/api/serializers_rpc.py +++ b/ietf/api/serializers_rpc.py @@ -226,28 +226,6 @@ def _update_authors(rfc, authors_data): event.save() return change_events -class EditableRfcSerializer(serializers.ModelSerializer): - # Would be nice to reconcile this with ietf.doc.serializers.RfcSerializer. - # The purposes of that serializer (representing data for Red) and this one - # (accepting updates from Purple) are different enough that separate formats - # may be needed, but if not it'd be nice to have a single RfcSerializer that - # can serve both. - # - # For now, only handles authors - authors = RfcAuthorSerializer(many=True, min_length=1, source="rfcauthor_set") - - class Meta: - model = Document - fields = ["id", "authors"] - - def update(self, instance, validated_data): - assert isinstance(instance, Document) - assert instance.type_id == "rfc" - authors_data = validated_data.pop("rfcauthor_set", None) - if authors_data is not None: - _update_authors(instance, authors_data) - return instance - class RfcPubSerializer(serializers.ModelSerializer): """Write-only serializer for RFC publication""" @@ -523,12 +501,21 @@ def _create_rfc(self, validated_data): return rfc -class RfcAmendMetadataSerializer(serializers.ModelSerializer): +class EditableRfcSerializer(serializers.ModelSerializer): + # Would be nice to reconcile this with ietf.doc.serializers.RfcSerializer. + # The purposes of that serializer (representing data for Red) and this one + # (accepting updates from Purple) are different enough that separate formats + # may be needed, but if not it'd be nice to have a single RfcSerializer that + # can serve both. + # + # Treats published and subseries fields as write-only. This isn't quite correct, + # but makes it easier and we don't currently use the serialized value except for + # debugging. published = serializers.DateTimeField( default_timezone=datetime.timezone.utc, write_only=True, ) - authors = RfcAuthorSerializer(source="rfcauthor_set", many=True) + authors = RfcAuthorSerializer(many=True, min_length=1, source="rfcauthor_set") subseries = serializers.ListField( child=serializers.RegexField( required=False, diff --git a/ietf/api/views_rpc.py b/ietf/api/views_rpc.py index 8754053659..13a0908f04 100644 --- a/ietf/api/views_rpc.py +++ b/ietf/api/views_rpc.py @@ -33,7 +33,7 @@ RfcWithAuthorsSerializer, DraftWithAuthorsSerializer, NotificationAckSerializer, RfcPubSerializer, RfcFileSerializer, - EditableRfcSerializer, RfcAmendMetadataSerializer, + EditableRfcSerializer, ) from ietf.doc.api import PrefetchRelatedDocument from ietf.doc.models import Document, DocHistory, RfcAuthor, SUBSERIES_DOC_TYPE_IDS, \ @@ -278,7 +278,7 @@ class RfcViewSet(mixins.UpdateModelMixin, viewsets.GenericViewSet): queryset = Document.objects.filter(type_id="rfc") api_key_endpoint = "ietf.api.views_rpc" lookup_field = "rfc_number" - serializer_class = RfcAmendMetadataSerializer + serializer_class = EditableRfcSerializer @action(detail=False, serializer_class=OriginalStreamSerializer) def rfc_original_stream(self, request): From 2bab330c48da2b521a52d058827afe5c78248cd9 Mon Sep 17 00:00:00 2001 From: Jennifer Richards Date: Thu, 26 Feb 2026 18:16:30 -0400 Subject: [PATCH 04/14] fix: mark read-only field properly --- ietf/doc/serializers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ietf/doc/serializers.py b/ietf/doc/serializers.py index 36076c30be..a7ea640be8 100644 --- a/ietf/doc/serializers.py +++ b/ietf/doc/serializers.py @@ -27,6 +27,7 @@ class RfcAuthorSerializer(serializers.ModelSerializer): source="person.get_absolute_url", required=False, help_text="URL for person link (relative to datatracker base URL)", + read_only=True, ) class Meta: From d43a5c6b6678bca744aac5b34384ee2de495fb6f Mon Sep 17 00:00:00 2001 From: Jennifer Richards Date: Fri, 27 Feb 2026 10:03:28 -0400 Subject: [PATCH 05/14] refactor: SubseriesNameField --- ietf/api/serializers_rpc.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/ietf/api/serializers_rpc.py b/ietf/api/serializers_rpc.py index 1f3d238f8c..5e7ba1f5e7 100644 --- a/ietf/api/serializers_rpc.py +++ b/ietf/api/serializers_rpc.py @@ -227,6 +227,15 @@ def _update_authors(rfc, authors_data): return change_events +class SubseriesNameField(serializers.RegexField): + + def __init__(self, **kwargs): + # pattern: no leading 0, finite length (arbitrarily set to 5 digits) + regex = r"^(bcp|std|fyi)[1-9][0-9]{0,4}$" + super().__init__(regex, **kwargs) + + + class RfcPubSerializer(serializers.ModelSerializer): """Write-only serializer for RFC publication""" # publication-related fields @@ -266,13 +275,7 @@ class RfcPubSerializer(serializers.ModelSerializer): slug_field="rfc_number", queryset=Document.objects.filter(type_id="rfc"), ) - subseries = serializers.ListField( - child=serializers.RegexField( - required=False, - # pattern: no leading 0, finite length (arbitrarily set to 5 digits) - regex=r"^(bcp|std|fyi)[1-9][0-9]{0,4}$", - ) - ) + subseries = serializers.ListField(child=SubseriesNameField(required=False)) # N.b., authors is _not_ a field on Document! authors = RfcAuthorSerializer(many=True) @@ -508,6 +511,8 @@ class EditableRfcSerializer(serializers.ModelSerializer): # may be needed, but if not it'd be nice to have a single RfcSerializer that # can serve both. # + # Should also consider whether this and RfcPubSerializer should merge. + # # Treats published and subseries fields as write-only. This isn't quite correct, # but makes it easier and we don't currently use the serialized value except for # debugging. @@ -517,11 +522,7 @@ class EditableRfcSerializer(serializers.ModelSerializer): ) authors = RfcAuthorSerializer(many=True, min_length=1, source="rfcauthor_set") subseries = serializers.ListField( - child=serializers.RegexField( - required=False, - # pattern: no leading 0, finite length (arbitrarily set to 5 digits) - regex=r"^(bcp|std|fyi)[1-9][0-9]{0,4}$", - ), + child=SubseriesNameField(required=False), write_only=True, ) From a69de2531834e043586459d95a0b6db3dae7b5fb Mon Sep 17 00:00:00 2001 From: Jennifer Richards Date: Fri, 27 Feb 2026 11:22:42 -0400 Subject: [PATCH 06/14] test: EditableRfcSerializer --- ietf/api/tests_serializers_rpc.py | 126 ++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 ietf/api/tests_serializers_rpc.py diff --git a/ietf/api/tests_serializers_rpc.py b/ietf/api/tests_serializers_rpc.py new file mode 100644 index 0000000000..8a2cc43787 --- /dev/null +++ b/ietf/api/tests_serializers_rpc.py @@ -0,0 +1,126 @@ +# Copyright The IETF Trust 2026, All Rights Reserved +from django.utils import timezone + +from ietf.utils.test_utils import TestCase +from ietf.doc.models import Document +from ietf.doc.factories import WgRfcFactory +from .serializers_rpc import EditableRfcSerializer + + +class EditableRfcSerializerTests(TestCase): + def test_create(self): + serializer = EditableRfcSerializer( + data={ + "published": timezone.now(), + "title": "Yadda yadda yadda", + "authors": [ + { + "titlepage_name": "B. Fett", + "is_editor": False, + "affiliation": "DBA Galactic Empire", + "country": "", + }, + ], + "stream": "ietf", + "abstract": "A long time ago in a galaxy far, far away...", + "pages": 3, + "std_level": "inf", + "subseries": ["fyi999"], + } + ) + self.assertTrue(serializer.is_valid()) + with self.assertRaises(RuntimeError, msg="serializer does not allow create()"): + serializer.save() + + def test_update(self): + rfc = WgRfcFactory(pages=10) + serializer = EditableRfcSerializer( + instance=rfc, + data={ + "published": timezone.now(), + "title": "Yadda yadda yadda", + "authors": [ + { + "titlepage_name": "B. Fett", + "is_editor": False, + "affiliation": "DBA Galactic Empire", + "country": "", + }, + ], + "stream": "ise", + "abstract": "A long time ago in a galaxy far, far away...", + "pages": 3, + "std_level": "inf", + "subseries": ["fyi999"], + }, + ) + self.assertTrue(serializer.is_valid()) + result = serializer.save() + self.assertEqual(result.title, "Yadda yadda yadda") + self.assertEqual( + list( + result.rfcauthor_set.values( + "titlepage_name", "is_editor", "affiliation", "country" + ) + ), + [ + { + "titlepage_name": "B. Fett", + "is_editor": False, + "affiliation": "DBA Galactic Empire", + "country": "", + }, + ], + ) + self.assertEqual(result.stream_id, "ise") + self.assertEqual( + result.abstract, "A long time ago in a galaxy far, far away..." + ) + self.assertEqual(result.pages, 3) + self.assertEqual(result.std_level_id, "inf") + self.assertEqual( + result.part_of(), + [Document.objects.get(name="fyi999")], + ) + + def test_partial_update(self): + # We could test other permutations of fields, but authors is a partial update + # we know we are going to use, so verifying that one in particular. + rfc = WgRfcFactory(pages=10, abstract="do or do not", title="jedi master") + serializer = EditableRfcSerializer( + partial=True, + instance=rfc, + data={ + "authors": [ + { + "titlepage_name": "B. Fett", + "is_editor": False, + "affiliation": "DBA Galactic Empire", + "country": "", + }, + ], + }, + ) + self.assertTrue(serializer.is_valid()) + result = serializer.save() + self.assertEqual(rfc.title, "jedi master") + self.assertEqual( + list( + result.rfcauthor_set.values( + "titlepage_name", "is_editor", "affiliation", "country" + ) + ), + [ + { + "titlepage_name": "B. Fett", + "is_editor": False, + "affiliation": "DBA Galactic Empire", + "country": "", + }, + ], + ) + self.assertEqual(result.stream_id, "ietf") + self.assertEqual(result.abstract, "do or do not") + self.assertEqual(result.pages, 10) + self.assertEqual(result.std_level_id, "ps") + self.assertEqual(result.part_of(), []) From 8cd05de4bbc233ae7fda2667aa52c5247919d543 Mon Sep 17 00:00:00 2001 From: Jennifer Richards Date: Fri, 27 Feb 2026 11:28:35 -0400 Subject: [PATCH 07/14] refactor: DocEvent adjustment --- ietf/api/serializers_rpc.py | 12 +++--------- ietf/api/views_rpc.py | 10 ++++++++++ 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/ietf/api/serializers_rpc.py b/ietf/api/serializers_rpc.py index 5e7ba1f5e7..bf3143bf0f 100644 --- a/ietf/api/serializers_rpc.py +++ b/ietf/api/serializers_rpc.py @@ -560,6 +560,7 @@ def update(self, instance, validated_data): with transaction.atomic(): # update the rfc Document itself rfc_changes = [] + rfc_events = [] for attr, new_value in validated_data.items(): old_value = getattr(instance, attr) @@ -570,22 +571,15 @@ def update(self, instance, validated_data): setattr(instance, attr, new_value) if len(rfc_changes) > 0: rfc_change_summary = f" ({', '.join(rfc_changes)})" - else: - rfc_change_summary = "" - rfc_events = [ - ( + rfc_events.append( DocEvent.objects.create( doc=rfc, rev=rfc.rev, by=system_person, type="sync_from_rfc_editor", - desc=( - "Metadata changes received from RFC Editor" - + rfc_change_summary - ), + desc=f"Changed metadata: {rfc_change_summary}", ) ) - ] if authors_data is not omitted: rfc_events.extend(_update_authors(instance, authors_data)) diff --git a/ietf/api/views_rpc.py b/ietf/api/views_rpc.py index 13a0908f04..ff87fde49d 100644 --- a/ietf/api/views_rpc.py +++ b/ietf/api/views_rpc.py @@ -280,6 +280,16 @@ class RfcViewSet(mixins.UpdateModelMixin, viewsets.GenericViewSet): lookup_field = "rfc_number" serializer_class = EditableRfcSerializer + def perform_update(self, serializer): + DocEvent.objects.create( + doc=serializer.instance, + rev=serializer.instance.rev, + by=Person.objects.get(name="(System)"), + type="sync_from_rfc_editor", + desc="Metadata sync from RFC Editor", + ) + super().perform_update(serializer) + @action(detail=False, serializer_class=OriginalStreamSerializer) def rfc_original_stream(self, request): rfcs = self.get_queryset().annotate( From 0ccc4f992c286de841a878b9592ea3bfbe995e18 Mon Sep 17 00:00:00 2001 From: Jennifer Richards Date: Fri, 27 Feb 2026 11:40:14 -0400 Subject: [PATCH 08/14] feat: record person ids for authors --- ietf/doc/utils.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/ietf/doc/utils.py b/ietf/doc/utils.py index 42fab7d472..396b3fcfa4 100644 --- a/ietf/doc/utils.py +++ b/ietf/doc/utils.py @@ -740,14 +740,26 @@ def _rfcauthor_from_documentauthor(docauthor: DocumentAuthor) -> RfcAuthor: new_author.document = rfc new_author.order = order + 1 new_author.save() - changes.append(f'Added "{new_author.titlepage_name}" as author') + if new_author.person_id is not None: + person_desc = f"Person {new_author.person_id}" + else: + person_desc = "no Person linked" + changes.append( + f'Added "{new_author.titlepage_name}" ({person_desc}) as author' + ) # Any authors left in original_authors are no longer in the list, so remove them for removed_author in original_authors: # Skip actual removal of old authors if we are converting from the # DocumentAuthor models - the original_authors were just stand-ins anyway. if not converting_from_docauthors: removed_author.delete() - changes.append(f'Removed "{removed_author.titlepage_name}" as author') + if removed_author.person_id is not None: + person_desc = f"Person {removed_author.person_id}" + else: + person_desc = "no Person linked" + changes.append( + f'Removed "{removed_author.titlepage_name}" ({person_desc}) as author' + ) # Create DocEvents, but leave it up to caller to save if by is None: by = Person.objects.get(name="(System)") From a91822b62e81550107ddc654c77e0926543ef2ea Mon Sep 17 00:00:00 2001 From: Jennifer Richards Date: Fri, 27 Feb 2026 11:43:28 -0400 Subject: [PATCH 09/14] chore: adjust history message --- ietf/api/views_rpc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ietf/api/views_rpc.py b/ietf/api/views_rpc.py index ff87fde49d..bb2c60f6c1 100644 --- a/ietf/api/views_rpc.py +++ b/ietf/api/views_rpc.py @@ -286,7 +286,7 @@ def perform_update(self, serializer): rev=serializer.instance.rev, by=Person.objects.get(name="(System)"), type="sync_from_rfc_editor", - desc="Metadata sync from RFC Editor", + desc="Metadata update from RFC Editor", ) super().perform_update(serializer) From 31d96bb2f689ba05ea16a3fe8dc35e26c3a57c4e Mon Sep 17 00:00:00 2001 From: Jennifer Richards Date: Fri, 27 Feb 2026 11:56:45 -0400 Subject: [PATCH 10/14] fix: always save!! --- ietf/api/serializers_rpc.py | 6 +++--- ietf/api/tests_serializers_rpc.py | 17 +++++++++++++++-- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/ietf/api/serializers_rpc.py b/ietf/api/serializers_rpc.py index bf3143bf0f..db73a158d6 100644 --- a/ietf/api/serializers_rpc.py +++ b/ietf/api/serializers_rpc.py @@ -563,12 +563,12 @@ def update(self, instance, validated_data): rfc_events = [] for attr, new_value in validated_data.items(): - old_value = getattr(instance, attr) + old_value = getattr(rfc, attr) if new_value != old_value: rfc_changes.append( f"changed {attr} to '{new_value}' from '{old_value}'" ) - setattr(instance, attr, new_value) + setattr(rfc, attr, new_value) if len(rfc_changes) > 0: rfc_change_summary = f" ({', '.join(rfc_changes)})" rfc_events.append( @@ -675,7 +675,7 @@ def update(self, instance, validated_data): desc=f"Removed {rfc.name} from {stale_subseries_doc.name}", ) stale_subseries_relations.delete() - rfc.save_with_history(rfc_events) + rfc.save_with_history(rfc_events) return rfc diff --git a/ietf/api/tests_serializers_rpc.py b/ietf/api/tests_serializers_rpc.py index 8a2cc43787..1babb4c30f 100644 --- a/ietf/api/tests_serializers_rpc.py +++ b/ietf/api/tests_serializers_rpc.py @@ -56,6 +56,7 @@ def test_update(self): ) self.assertTrue(serializer.is_valid()) result = serializer.save() + result.refresh_from_db() self.assertEqual(result.title, "Yadda yadda yadda") self.assertEqual( list( @@ -86,7 +87,7 @@ def test_update(self): def test_partial_update(self): # We could test other permutations of fields, but authors is a partial update # we know we are going to use, so verifying that one in particular. - rfc = WgRfcFactory(pages=10, abstract="do or do not", title="jedi master") + rfc = WgRfcFactory(pages=10, abstract="do or do not", title="padawan") serializer = EditableRfcSerializer( partial=True, instance=rfc, @@ -103,7 +104,8 @@ def test_partial_update(self): ) self.assertTrue(serializer.is_valid()) result = serializer.save() - self.assertEqual(rfc.title, "jedi master") + result.refresh_from_db() + self.assertEqual(rfc.title, "padawan") self.assertEqual( list( result.rfcauthor_set.values( @@ -124,3 +126,14 @@ def test_partial_update(self): self.assertEqual(result.pages, 10) self.assertEqual(result.std_level_id, "ps") self.assertEqual(result.part_of(), []) + + # Test only a field on the Document itself to be sure that it works + serializer = EditableRfcSerializer( + partial=True, + instance=rfc, + data={"title": "jedi master"}, + ) + self.assertTrue(serializer.is_valid()) + result = serializer.save() + result.refresh_from_db() + self.assertEqual(rfc.title, "jedi master") From c3d29f63b32277866aadf2567dc0fa7fa11de8c0 Mon Sep 17 00:00:00 2001 From: Jennifer Richards Date: Fri, 27 Feb 2026 11:56:59 -0400 Subject: [PATCH 11/14] fix: better msg formatting --- ietf/api/serializers_rpc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ietf/api/serializers_rpc.py b/ietf/api/serializers_rpc.py index db73a158d6..6991ff54fe 100644 --- a/ietf/api/serializers_rpc.py +++ b/ietf/api/serializers_rpc.py @@ -570,7 +570,7 @@ def update(self, instance, validated_data): ) setattr(rfc, attr, new_value) if len(rfc_changes) > 0: - rfc_change_summary = f" ({', '.join(rfc_changes)})" + rfc_change_summary = f"{', '.join(rfc_changes)}" rfc_events.append( DocEvent.objects.create( doc=rfc, From 00aa8d1498beeaadc2557067ee0c577c54e6787f Mon Sep 17 00:00:00 2001 From: Jennifer Richards Date: Fri, 27 Feb 2026 11:58:24 -0400 Subject: [PATCH 12/14] fix: _almost_ always save!! --- ietf/api/serializers_rpc.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ietf/api/serializers_rpc.py b/ietf/api/serializers_rpc.py index 6991ff54fe..af02536fa3 100644 --- a/ietf/api/serializers_rpc.py +++ b/ietf/api/serializers_rpc.py @@ -675,7 +675,8 @@ def update(self, instance, validated_data): desc=f"Removed {rfc.name} from {stale_subseries_doc.name}", ) stale_subseries_relations.delete() - rfc.save_with_history(rfc_events) + if len(rfc_events) > 0: + rfc.save_with_history(rfc_events) return rfc From 9b7c6efd046922a0b6526317f7f39697ba1a841f Mon Sep 17 00:00:00 2001 From: Jennifer Richards Date: Fri, 27 Feb 2026 12:20:04 -0400 Subject: [PATCH 13/14] fix: lint --- ietf/api/views_rpc.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ietf/api/views_rpc.py b/ietf/api/views_rpc.py index bb2c60f6c1..0819c04215 100644 --- a/ietf/api/views_rpc.py +++ b/ietf/api/views_rpc.py @@ -35,9 +35,7 @@ NotificationAckSerializer, RfcPubSerializer, RfcFileSerializer, EditableRfcSerializer, ) -from ietf.doc.api import PrefetchRelatedDocument -from ietf.doc.models import Document, DocHistory, RfcAuthor, SUBSERIES_DOC_TYPE_IDS, \ - DocEvent +from ietf.doc.models import Document, DocHistory, RfcAuthor, DocEvent from ietf.doc.serializers import RfcAuthorSerializer from ietf.doc.storage_utils import remove_from_storage, store_file, exists_in_storage from ietf.person.models import Email, Person From 1c1127ee659a1c28362a559f6217e9d535149075 Mon Sep 17 00:00:00 2001 From: Jennifer Richards Date: Fri, 27 Feb 2026 12:30:55 -0400 Subject: [PATCH 14/14] refactor: rename var --- ietf/api/serializers_rpc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ietf/api/serializers_rpc.py b/ietf/api/serializers_rpc.py index af02536fa3..d5f5363990 100644 --- a/ietf/api/serializers_rpc.py +++ b/ietf/api/serializers_rpc.py @@ -218,7 +218,7 @@ class Meta: def _update_authors(rfc, authors_data): # Construct unsaved instances from validated author data - new_authors = [RfcAuthor(**ad) for ad in authors_data] + new_authors = [RfcAuthor(**authdata) for authdata in authors_data] # Update the RFC with the new author set with transaction.atomic(): change_events = update_rfcauthors(rfc, new_authors)