-
Couldn't load subscription status.
- Fork 607
feat: RPC modernization APIs #9631
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
0e4a032
e92136c
0f90c4d
0e309d0
3998f34
14471ec
76f043a
ec324e0
75d01b2
f27ff98
9e9a787
fd90d8b
fbc1dde
57f5c9a
603ba8c
084e863
af0ea91
8c29605
272db6d
4bc5ec6
5c0cdec
f2b3138
6b75cff
524f714
3f04ca7
4a129ac
adf4f5a
02bdf4f
5d5459f
633e000
a8087b0
e8fe731
1c0a47f
5863e83
34a0987
93f95be
6ea1e82
2ed6a2e
1d3eba7
ac66c85
a7a7caf
bc8d3f8
9cf8f10
14a76f9
1ab36cd
732c502
ebc2710
eba8175
0545a26
093fc2b
a90cbad
ba3bad5
05e6cb6
501d92a
8eae978
e896928
d245312
d60dba1
c48e6e7
7da3965
151c936
359af60
f88e9f9
114dfa8
968f9cb
389e97a
13a17b4
c2efcfa
76ebc10
cc4ae6e
1f810ef
d37a982
69ef75d
b568be9
28c85ff
95aeccc
6574331
9f718a3
01aa99d
f3b8600
e266247
daf7b62
3d1728c
38fda63
987a460
fc7b1c5
70f093d
84da631
8bca0e1
24d9815
5eae568
b34bb04
9bb80a5
7bc1d63
cb34712
6c6b8d7
983c794
4f5da10
3daf659
18cc41a
8d0bbb2
23960e9
d1aa690
c020f0f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -100,3 +100,7 @@ | |
| bucket_name=f"{storagename}", | ||
| ), | ||
| } | ||
|
|
||
| APP_API_TOKENS = { | ||
| "ietf.api.views_rpc" : ["devtoken"], | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not a real secret? |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,194 @@ | ||
| # Copyright The IETF Trust 2025, All Rights Reserved | ||
| import datetime | ||
| from typing import Literal, Optional | ||
|
|
||
| from django.urls import reverse as urlreverse | ||
| from drf_spectacular.types import OpenApiTypes | ||
| from drf_spectacular.utils import extend_schema_field | ||
| from rest_framework import serializers | ||
|
|
||
| from ietf.doc.models import DocumentAuthor, Document | ||
| from ietf.doc.utils import default_consensus | ||
| from ietf.person.models import Person | ||
|
|
||
|
|
||
| class PersonSerializer(serializers.ModelSerializer): | ||
| email = serializers.EmailField(read_only=True) | ||
| picture = serializers.URLField(source="cdn_photo_url", read_only=True) | ||
| url = serializers.SerializerMethodField( | ||
| help_text="relative URL for datatracker person page" | ||
| ) | ||
|
|
||
| class Meta: | ||
| model = Person | ||
| fields = ["id", "plain_name", "email", "picture", "url"] | ||
| read_only_fields = ["id", "plain_name", "email", "picture", "url"] | ||
|
|
||
| @extend_schema_field(OpenApiTypes.URI) | ||
| def get_url(self, object: Person): | ||
| return urlreverse( | ||
| "ietf.person.views.profile", | ||
| kwargs={"email_or_name": object.email_address() or object.name}, | ||
| ) | ||
|
|
||
|
|
||
| class EmailPersonSerializer(serializers.Serializer): | ||
| email = serializers.EmailField(source="address") | ||
| person_pk = serializers.IntegerField(source="person.pk") | ||
| name = serializers.CharField(source="person.name") | ||
| last_name = serializers.CharField(source="person.last_name") | ||
| initials = serializers.CharField(source="person.initials") | ||
|
|
||
|
|
||
| class LowerCaseEmailField(serializers.EmailField): | ||
| def to_representation(self, value): | ||
| return super().to_representation(value).lower() | ||
|
|
||
|
|
||
| class AuthorPersonSerializer(serializers.ModelSerializer): | ||
| person_pk = serializers.IntegerField(source="pk", read_only=True) | ||
| last_name = serializers.CharField() | ||
| initials = serializers.CharField() | ||
| email_addresses = serializers.ListField( | ||
| source="email_set.all", child=LowerCaseEmailField() | ||
| ) | ||
|
|
||
| class Meta: | ||
| model = Person | ||
| fields = ["person_pk", "name", "last_name", "initials", "email_addresses"] | ||
|
|
||
|
|
||
| class RfcWithAuthorsSerializer(serializers.ModelSerializer): | ||
| authors = AuthorPersonSerializer(many=True) | ||
|
|
||
| class Meta: | ||
| model = Document | ||
| fields = ["rfc_number", "authors"] | ||
|
|
||
|
|
||
| class DraftWithAuthorsSerializer(serializers.ModelSerializer): | ||
| draft_name = serializers.CharField(source="name") | ||
| authors = AuthorPersonSerializer(many=True) | ||
|
|
||
| class Meta: | ||
| model = Document | ||
| fields = ["draft_name", "authors"] | ||
|
|
||
|
|
||
| class DocumentAuthorSerializer(serializers.ModelSerializer): | ||
| """Serializer for a Person in a response""" | ||
|
|
||
| plain_name = serializers.SerializerMethodField() | ||
|
|
||
| class Meta: | ||
| model = DocumentAuthor | ||
| fields = ["person", "plain_name"] | ||
|
|
||
| def get_plain_name(self, document_author: DocumentAuthor) -> str: | ||
| return document_author.person.plain_name() | ||
|
|
||
|
|
||
| class FullDraftSerializer(serializers.ModelSerializer): | ||
| # Redefine these fields so they don't pick up the regex validator patterns. | ||
| # There seem to be some non-compliant drafts in the system! If this serializer | ||
| # is used for a writeable view, the validation will need to be added back. | ||
| name = serializers.CharField(max_length=255) | ||
| title = serializers.CharField(max_length=255) | ||
|
|
||
| # Other fields we need to add / adjust | ||
| source_format = serializers.SerializerMethodField() | ||
| authors = DocumentAuthorSerializer(many=True, source="documentauthor_set") | ||
| shepherd = serializers.SerializerMethodField() | ||
| consensus = serializers.SerializerMethodField() | ||
|
|
||
| class Meta: | ||
| model = Document | ||
| fields = [ | ||
| "id", | ||
| "name", | ||
| "rev", | ||
| "stream", | ||
| "title", | ||
| "pages", | ||
| "source_format", | ||
| "authors", | ||
| "shepherd", | ||
| "intended_std_level", | ||
| "consensus", | ||
| ] | ||
|
|
||
| def get_consensus(self, doc: Document) -> Optional[bool]: | ||
| return default_consensus(doc) | ||
|
|
||
| def get_source_format( | ||
| self, doc: Document | ||
| ) -> Literal["unknown", "xml-v2", "xml-v3", "txt"]: | ||
| submission = doc.submission() | ||
| if submission is None: | ||
| return "unknown" | ||
| if ".xml" in submission.file_types: | ||
| if submission.xml_version == "3": | ||
| return "xml-v3" | ||
| else: | ||
| return "xml-v2" | ||
| elif ".txt" in submission.file_types: | ||
| return "txt" | ||
| return "unknown" | ||
|
|
||
| @extend_schema_field(OpenApiTypes.EMAIL) | ||
| def get_shepherd(self, doc: Document) -> str: | ||
| if doc.shepherd: | ||
| return doc.shepherd.formatted_ascii_email() | ||
| return "" | ||
|
|
||
|
|
||
| class DraftSerializer(FullDraftSerializer): | ||
| class Meta: | ||
| model = Document | ||
| fields = [ | ||
| "id", | ||
| "name", | ||
| "rev", | ||
| "stream", | ||
| "title", | ||
| "pages", | ||
| "source_format", | ||
| "authors", | ||
| ] | ||
|
|
||
|
|
||
| class SubmittedToQueueSerializer(FullDraftSerializer): | ||
| submitted = serializers.SerializerMethodField() | ||
| consensus = serializers.SerializerMethodField() | ||
|
|
||
| class Meta: | ||
| model = Document | ||
| fields = [ | ||
| "id", | ||
| "name", | ||
| "stream", | ||
| "submitted", | ||
| "consensus", | ||
| ] | ||
|
|
||
| def get_submitted(self, doc) -> Optional[datetime.datetime]: | ||
| event = doc.sent_to_rfc_editor_event() | ||
| return None if event is None else event.time | ||
|
|
||
| def get_consensus(self, doc) -> Optional[bool]: | ||
| return default_consensus(doc) | ||
|
|
||
|
|
||
| class OriginalStreamSerializer(serializers.ModelSerializer): | ||
| stream = serializers.CharField(read_only=True, source="orig_stream_id") | ||
|
|
||
| class Meta: | ||
| model = Document | ||
| fields = ["rfc_number", "stream"] | ||
|
|
||
|
|
||
| class ReferenceSerializer(serializers.ModelSerializer): | ||
| class Meta: | ||
| model = Document | ||
| fields = ["id", "name"] | ||
| read_only_fields = ["id", "name"] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| # Copyright The IETF Trust 2025, All Rights Reserved | ||
| # -*- coding: utf-8 -*- | ||
|
|
||
| from django.test.utils import override_settings | ||
| from django.urls import reverse as urlreverse | ||
|
|
||
| from ietf.doc.factories import IndividualDraftFactory | ||
| from ietf.doc.models import RelatedDocument | ||
| from ietf.utils.test_utils import TestCase, reload_db_objects | ||
|
|
||
|
|
||
| class RpcApiTests(TestCase): | ||
| @override_settings(APP_API_TOKENS={"ietf.api.views_rpc": ["valid-token"]}) | ||
| def test_api_refs(self): | ||
| # non-existent draft | ||
| url = urlreverse("ietf.api.views_rpc.rpc_draft_refs", kwargs={"doc_id": 999999}) | ||
| r = self.client.get(url) | ||
| self.assertEqual(r.status_code, 200) | ||
| r = self.client.get(url, headers={"X-Api-Key": "valid-token"}) | ||
| jsondata = r.json() | ||
| refs = jsondata["references"] | ||
| self.assertEqual(refs, []) | ||
|
|
||
| # draft without any normative references | ||
| draft = IndividualDraftFactory() | ||
| draft = reload_db_objects(draft) | ||
| url = urlreverse( | ||
| "ietf.api.views_rpc.rpc_draft_refs", kwargs={"doc_id": draft.id} | ||
| ) | ||
| r = self.client.get(url) | ||
| self.assertEqual(r.status_code, 200) | ||
| r = self.client.get(url, headers={"X-Api-Key": "valid-token"}) | ||
| jsondata = r.json() | ||
| refs = jsondata["references"] | ||
| self.assertEqual(refs, []) | ||
|
|
||
| # draft without any normative references but with an informative reference | ||
| draft_foo = IndividualDraftFactory() | ||
| draft_foo = reload_db_objects(draft_foo) | ||
| RelatedDocument.objects.create( | ||
| source=draft, target=draft_foo, relationship_id="refinfo" | ||
| ) | ||
| url = urlreverse( | ||
| "ietf.api.views_rpc.rpc_draft_refs", kwargs={"doc_id": draft.id} | ||
| ) | ||
| r = self.client.get(url) | ||
| self.assertEqual(r.status_code, 200) | ||
| r = self.client.get(url, headers={"X-Api-Key": "valid-token"}) | ||
| jsondata = r.json() | ||
| refs = jsondata["references"] | ||
| self.assertEqual(refs, []) | ||
|
|
||
| # draft with a normative reference | ||
| draft_bar = IndividualDraftFactory() | ||
| draft_bar = reload_db_objects(draft_bar) | ||
| RelatedDocument.objects.create( | ||
| source=draft, target=draft_bar, relationship_id="refnorm" | ||
| ) | ||
| url = urlreverse( | ||
| "ietf.api.views_rpc.rpc_draft_refs", kwargs={"doc_id": draft.id} | ||
| ) | ||
| r = self.client.get(url) | ||
| self.assertEqual(r.status_code, 200) | ||
| r = self.client.get(url, headers={"X-Api-Key": "valid-token"}) | ||
| jsondata = r.json() | ||
| refs = jsondata["references"] | ||
| self.assertEqual(len(refs), 1) | ||
| self.assertEqual(refs[0]["id"], draft_bar.id) | ||
| self.assertEqual(refs[0]["name"], draft_bar.name) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| # Copyright The IETF Trust 2023-2025, All Rights Reserved | ||
|
|
||
| from rest_framework import routers | ||
|
|
||
| from django.conf import settings | ||
| from django.urls import include, path | ||
|
|
||
| from ietf.api import views_rpc, views_rpc_demo | ||
| from ietf.utils.urls import url | ||
|
|
||
| router = routers.DefaultRouter() | ||
| router.register(r"draft", views_rpc.DraftViewSet, basename="draft") | ||
| router.register(r"person", views_rpc.PersonViewSet) | ||
| router.register(r"rfc", views_rpc.RfcViewSet, basename="rfc") | ||
|
|
||
| if settings.SERVER_MODE not in {"production", "test"}: | ||
| # for non production demos | ||
| router.register(r"demo", views_rpc_demo.DemoViewSet, basename="demo") | ||
|
|
||
|
|
||
| urlpatterns = [ | ||
| url(r"^doc/drafts_by_names/", views_rpc.DraftsByNamesView.as_view()), | ||
| url(r"^persons/search/", views_rpc.RpcPersonSearch.as_view()), | ||
| path(r"subject/<str:subject_id>/person/", views_rpc.SubjectPersonView.as_view()), | ||
| ] | ||
|
|
||
| # add routers at the end so individual routes can steal parts of their address | ||
| # space (specifically, ^person/ routes so far) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. verify that this is current? |
||
| urlpatterns.extend( | ||
| [ | ||
| path("", include(router.urls)), | ||
| ] | ||
| ) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replace this with "not a real secret"?