Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ietf/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
# todo more general name for this API?
red_router = PrefixedSimpleRouter(name_prefix="ietf.api.red_api") # red api router
red_router.register("doc", doc_api.RfcViewSet)
red_router.register("subseries", doc_api.SubseriesViewSet, basename="subseries")

api.autodiscover()

Expand Down
116 changes: 88 additions & 28 deletions ietf/doc/api.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Copyright The IETF Trust 2024, All Rights Reserved
"""Doc API implementations"""
from django.db.models import OuterRef, Subquery, Prefetch, Value, JSONField

from django.db.models import OuterRef, Subquery, Prefetch, Value, JSONField, QuerySet
from django.db.models.functions import TruncDate
from django_filters import rest_framework as filters
from rest_framework import filters as drf_filters
Expand All @@ -10,10 +11,16 @@
from rest_framework.viewsets import GenericViewSet

from ietf.group.models import Group
from ietf.name.models import StreamName
from ietf.name.models import StreamName, DocTypeName
from ietf.utils.timezone import RPC_TZINFO
from .models import Document, DocEvent, RelatedDocument
from .serializers import RfcMetadataSerializer, RfcStatus, RfcSerializer
from .models import Document, DocEvent, RelatedDocument, DocumentAuthor, \
SUBSERIES_DOC_TYPE_IDS
from .serializers import (
RfcMetadataSerializer,
RfcStatus,
RfcSerializer,
SubseriesDocSerializer,
)


class RfcLimitOffsetPagination(LimitOffsetPagination):
Expand Down Expand Up @@ -55,38 +62,37 @@ class PrefetchRelatedDocument(Prefetch):
those for which the current RFC is the `source`. If `reverse` is True, includes those
for which it is the `target` instead. Defaults to only "rfc" documents.
"""
@staticmethod
def _get_queryset(relationship_id, reverse, doc_type_id):
"""Get queryset to use for the prefetch"""
return RelatedDocument.objects.filter(
**{
"relationship_id": relationship_id,
f"{'source' if reverse else 'target'}__type_id": doc_type_id,
}
).select_related("source" if reverse else "target")

def __init__(self, to_attr, relationship_id, reverse=False, doc_type_id="rfc"):
super().__init__(
lookup="targets_related" if reverse else "relateddocument_set",
queryset=RelatedDocument.objects.filter(
**{
"relationship_id": relationship_id,
f"{'source' if reverse else 'target'}__type_id": doc_type_id,
}
),
queryset=self._get_queryset(relationship_id, reverse, doc_type_id),
to_attr=to_attr,
)


class RfcViewSet(ListModelMixin, RetrieveModelMixin, GenericViewSet):
permission_classes: list[BasePermission] = []
lookup_field = "rfc_number"
queryset = (
Document.objects.filter(type_id="rfc", rfc_number__isnull=False)
.annotate(
published_datetime=Subquery(
DocEvent.objects.filter(
doc_id=OuterRef("pk"),
type="published_rfc",
)
.order_by("-time")
.values("time")[:1]
),
)
.annotate(published=TruncDate("published_datetime", tzinfo=RPC_TZINFO))
.order_by("-rfc_number")
def augment_rfc_queryset(queryset: QuerySet[Document]):
return (
queryset
.select_related("std_level", "stream")
.prefetch_related(
Prefetch(
"group",
Group.objects.select_related("parent"),
),
Prefetch(
"documentauthor_set",
DocumentAuthor.objects.select_related("email", "person"),
),
PrefetchRelatedDocument(
to_attr="drafts",
relationship_id="became_rfc",
Expand All @@ -102,6 +108,17 @@ class RfcViewSet(ListModelMixin, RetrieveModelMixin, GenericViewSet):
to_attr="updated_by", relationship_id="updates", reverse=True
),
)
.annotate(
published_datetime=Subquery(
DocEvent.objects.filter(
doc_id=OuterRef("pk"),
type="published_rfc",
)
.order_by("-time")
.values("time")[:1]
),
)
.annotate(published=TruncDate("published_datetime", tzinfo=RPC_TZINFO))
.annotate(
# TODO implement these fake fields for real
is_also=Value([], output_field=JSONField()),
Expand All @@ -110,7 +127,16 @@ class RfcViewSet(ListModelMixin, RetrieveModelMixin, GenericViewSet):
keywords=Value(["keyword"], output_field=JSONField()),
errata=Value([], output_field=JSONField()),
)
) # default ordering - RfcFilter may override
)


class RfcViewSet(ListModelMixin, RetrieveModelMixin, GenericViewSet):
permission_classes: list[BasePermission] = []
lookup_field = "rfc_number"
queryset = augment_rfc_queryset(
Document.objects.filter(type_id="rfc", rfc_number__isnull=False)
).order_by("-rfc_number")

pagination_class = RfcLimitOffsetPagination
filter_backends = [filters.DjangoFilterBackend, drf_filters.SearchFilter]
filterset_class = RfcFilter
Expand All @@ -120,3 +146,37 @@ def get_serializer_class(self):
if self.action == "retrieve":
return RfcSerializer
return RfcMetadataSerializer


class PrefetchSubseriesContents(Prefetch):
def __init__(self, to_attr):
super().__init__(
lookup="relateddocument_set",
queryset=RelatedDocument.objects.filter(
relationship_id="contains",
target__type_id="rfc",
).prefetch_related(
Prefetch(
"target",
queryset=augment_rfc_queryset(Document.objects.all()),
)
),
to_attr=to_attr,
)


class SubseriesFilter(filters.FilterSet):
type = filters.ModelMultipleChoiceFilter(
queryset=DocTypeName.objects.filter(pk__in=SUBSERIES_DOC_TYPE_IDS)
)


class SubseriesViewSet(ListModelMixin, RetrieveModelMixin, GenericViewSet):
permission_classes: list[BasePermission] = []
lookup_field = "name"
serializer_class = SubseriesDocSerializer
queryset = Document.objects.subseries_docs().prefetch_related(
PrefetchSubseriesContents(to_attr="contents")
)
filter_backends = [filters.DjangoFilterBackend]
filterset_class = SubseriesFilter
11 changes: 11 additions & 0 deletions ietf/doc/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -937,7 +937,18 @@ def role_for_doc(self):
'invalid'
)


SUBSERIES_DOC_TYPE_IDS = ("bcp", "fyi", "std")


class DocumentQuerySet(models.QuerySet):
def subseries_docs(self):
return self.filter(type_id__in=SUBSERIES_DOC_TYPE_IDS)


class Document(StorableMixin, DocumentInfo):
objects = DocumentQuerySet.as_manager()

name = models.CharField(max_length=255, validators=[validate_docname,], unique=True) # immutable

action_holders = models.ManyToManyField(Person, through=DocumentActionHolder, blank=True)
Expand Down
56 changes: 50 additions & 6 deletions ietf/doc/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
from dataclasses import dataclass
from typing import Literal, ClassVar

from django.db.models.manager import BaseManager
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers, fields

from ietf.group.serializers import GroupSerializer
from ietf.name.serializers import StreamNameSerializer
from .models import Document, DocumentAuthor
from .models import Document, DocumentAuthor, RelatedDocument


class RfcAuthorSerializer(serializers.ModelSerializer):
Expand Down Expand Up @@ -114,10 +115,10 @@ def to_representation(self, instance: Document):
return super().to_representation(instance=RfcStatus.from_document(instance))


class RelatedDraftSerializer(serializers.ModelSerializer):
class Meta:
model = Document
fields = ["id", "name", "title"]
class RelatedDraftSerializer(serializers.Serializer):
id = serializers.IntegerField(source="source.id")
name = serializers.CharField(source="source.name")
title = serializers.CharField(source="source.title")


class RelatedRfcSerializer(serializers.Serializer):
Expand All @@ -144,7 +145,7 @@ class RfcMetadataSerializer(serializers.ModelSerializer):
area = GroupSerializer(source="group.area", required=False)
stream = StreamNameSerializer()
identifiers = fields.SerializerMethodField()
draft = RelatedDraftSerializer(source="came_from_draft", read_only=True) # todo prefetch this
draft = serializers.SerializerMethodField()
obsoletes = RelatedRfcSerializer(many=True, read_only=True)
obsoleted_by = ReverseRelatedRfcSerializer(many=True, read_only=True)
updates = RelatedRfcSerializer(many=True, read_only=True)
Expand Down Expand Up @@ -190,6 +191,14 @@ def get_identifiers(self, doc: Document):
)
return DocIdentifierSerializer(instance=identifiers, many=True).data

@extend_schema_field(RelatedDraftSerializer)
def get_draft(self, object):
try:
related_doc = object.drafts[0]
except IndexError:
return None
return RelatedDraftSerializer(related_doc).data


class RfcSerializer(RfcMetadataSerializer):
"""Serialize an RFC, including its metadata and text content if available"""
Expand All @@ -199,3 +208,38 @@ class RfcSerializer(RfcMetadataSerializer):
class Meta:
model = RfcMetadataSerializer.Meta.model
fields = RfcMetadataSerializer.Meta.fields + ["text"]


class SubseriesContentListSerializer(serializers.ListSerializer):
"""ListSerializer that gets its object from item.target"""

def to_representation(self, data):
"""
List of object instances -> List of dicts of primitive datatypes.
"""
# Dealing with nested relationships, data can be a Manager,
# so, first get a queryset from the Manager if needed
iterable = (
data.all() if isinstance(data, BaseManager) else data
)
# Serialize item.target instead of item itself
return [self.child.to_representation(item.target) for item in iterable]


class SubseriesContentSerializer(RfcMetadataSerializer):
"""Serialize RFC contained in a subseries doc"""
class Meta(RfcMetadataSerializer.Meta):
list_serializer_class = SubseriesContentListSerializer


class SubseriesDocSerializer(serializers.ModelSerializer):
"""Serialize a subseries document (e.g., a BCP or STD)"""
contents = SubseriesContentSerializer(many=True)

class Meta:
model = Document
fields = [
"name",
"type",
"contents",
]