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
17 changes: 11 additions & 6 deletions ietf/api/urls.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,30 @@
# Copyright The IETF Trust 2017-2024, All Rights Reserved

from drf_spectacular.views import SpectacularAPIView

from django.conf import settings
from django.urls import include
from django.urls import include, path
from django.views.generic import TemplateView

from ietf import api
from ietf.doc import views_ballot
from ietf.doc import views_ballot, api as doc_api
from ietf.meeting import views as meeting_views
from ietf.submit import views as submit_views
from ietf.utils.urls import url

from . import views as api_views
from .routers import PrefixedSimpleRouter

# DRF API routing - disabled until we plan to use it
# from drf_spectacular.views import SpectacularAPIView
# from django.urls import path
# from ietf.person import api as person_api
# from .routers import PrefixedSimpleRouter
# core_router = PrefixedSimpleRouter(name_prefix="ietf.api.core_api") # core api router
# core_router.register("email", person_api.EmailViewSet)
# core_router.register("person", person_api.PersonViewSet)

# 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)

api.autodiscover()

urlpatterns = [
Expand All @@ -32,7 +36,8 @@
url(r'^v2/person/person', api_views.ApiV2PersonExportView.as_view()),
# --- DRF API ---
# path("core/", include(core_router.urls)),
# path("schema/", SpectacularAPIView.as_view()),
path("red/", include(red_router.urls)),
path("schema/", SpectacularAPIView.as_view()),
#
# --- Custom API endpoints, sorted alphabetically ---
# Email alias information for drafts
Expand Down
88 changes: 88 additions & 0 deletions ietf/doc/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Copyright The IETF Trust 2024, All Rights Reserved
"""Doc API implementations"""
from django.db.models import OuterRef, Subquery, Prefetch
from django.db.models.functions import TruncDate
from django_filters import rest_framework as filters
from rest_framework import filters as drf_filters
from rest_framework.mixins import ListModelMixin, RetrieveModelMixin
from rest_framework.pagination import LimitOffsetPagination
from rest_framework.viewsets import GenericViewSet

from ietf.group.models import Group
from ietf.name.models import StreamName
from ietf.utils.timezone import RPC_TZINFO
from .models import Document, DocEvent, RelatedDocument
from .serializers import RfcMetadataSerializer, RfcStatus


class RfcLimitOffsetPagination(LimitOffsetPagination):
default_limit = 10
max_limit = 500


class RfcFilter(filters.FilterSet):
published = filters.DateFromToRangeFilter()
stream = filters.ModelMultipleChoiceFilter(
queryset=StreamName.objects.filter(used=True)
)
group = filters.ModelMultipleChoiceFilter(
queryset=Group.objects.wgs(),
field_name="group__acronym",
to_field_name="acronym",
)
area = filters.ModelMultipleChoiceFilter(
queryset=Group.objects.areas(),
field_name="group__parent__acronym",
to_field_name="acronym",
)
status = filters.MultipleChoiceFilter(
choices=[(slug, slug) for slug in RfcStatus.status_slugs],
method=RfcStatus.filter,
)
sort = filters.OrderingFilter(
fields=(
("rfc_number", "number"), # ?sort=number / ?sort=-number
("published", "published"), # ?sort=published / ?sort=-published
),
)


class RfcViewSet(ListModelMixin, RetrieveModelMixin, GenericViewSet):
permission_classes = []
queryset = (
Document.objects.filter(type_id="rfc")
.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")
.prefetch_related(
Prefetch(
"targets_related", # relationship to follow
queryset=RelatedDocument.objects.filter(
source__type_id="rfc", relationship_id="obs"
),
to_attr="obsoleted_by", # attr to add to queryset instances
),
Prefetch(
"targets_related", # relationship to follow
queryset=RelatedDocument.objects.filter(
source__type_id="rfc", relationship_id="updates"
),
to_attr="updated_by", # attr to add to queryset instances
),
)
) # default ordering - RfcFilter may override

serializer_class = RfcMetadataSerializer
pagination_class = RfcLimitOffsetPagination
filter_backends = [filters.DjangoFilterBackend, drf_filters.SearchFilter]
filterset_class = RfcFilter
search_fields = ["title", "abstract"]
153 changes: 153 additions & 0 deletions ietf/doc/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
# Copyright The IETF Trust 2024, All Rights Reserved
"""django-rest-framework serializers"""
from dataclasses import dataclass
from typing import Literal, ClassVar

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, RelatedDocument


class RfcAuthorSerializer(serializers.ModelSerializer):
"""Serializer for a DocumentAuthor in a response"""

name = fields.CharField(source="person.plain_name")
email = fields.EmailField(source="email.address", required=False)

class Meta:
model = DocumentAuthor
fields = ["person", "name", "email", "affiliation", "country"]


@dataclass
class DocIdentifier:
type: Literal["doi", "issn"]
value: str


class DocIdentifierSerializer(serializers.Serializer):
type = serializers.ChoiceField(choices=["doi", "issn"])
value = serializers.CharField()


# This should become "type RfcStatusSlugT ..." when we drop pre-py3.12 support
# It should be "RfcStatusSlugT: TypeAlias ..." when we drop py3.9 support
RfcStatusSlugT = Literal[
"standard", "bcp", "informational", "experimental", "historic", "unknown"
]


@dataclass
class RfcStatus:
"""Helper to extract the 'Status' from an RFC document for serialization"""

slug: RfcStatusSlugT

# Names that aren't just the slug itself. ClassVar annotation prevents dataclass from treating this as a field.
fancy_names: ClassVar[dict[RfcStatusSlugT, str]] = {
"standard": "standards track",
"bcp": "best current practice",
}

# ClassVar annotation prevents dataclass from treating this as a field
stdlevelname_slug_map: ClassVar[dict[str, RfcStatusSlugT]] = {
"bcp": "bcp",
"ds": "standard", # ds is obsolete
"exp": "experimental",
"hist": "historic",
"inf": "informational",
"std": "standard",
"ps": "standard",
"unkn": "unknown",
}

# ClassVar annotation prevents dataclass from treating this as a field
status_slugs: ClassVar[list[RfcStatusSlugT]] = sorted(
set(stdlevelname_slug_map.values())
)

@property
def name(self):
return RfcStatus.fancy_names.get(self.slug, self.slug)

@classmethod
def from_document(cls, doc: Document):
"""Decide the status that applies to a document"""
return cls(
slug=(cls.stdlevelname_slug_map.get(doc.std_level.slug, "unknown")),
)

@classmethod
def filter(cls, queryset, name, value: list[RfcStatusSlugT]):
"""Filter a queryset by status

This is basically the inverse of the from_document() method. Given a status name, filter
the queryset to those in that status. The queryset should be a Document queryset.
"""
interesting_slugs = [
stdlevelname_slug
for stdlevelname_slug, status_slug in cls.stdlevelname_slug_map.items()
if status_slug in value
]
if len(interesting_slugs) == 0:
return queryset.none()
return queryset.filter(std_level__slug__in=interesting_slugs)


class RfcStatusSerializer(serializers.Serializer):
"""Status serializer for a Document instance"""

slug = serializers.ChoiceField(choices=RfcStatus.status_slugs)
name = serializers.CharField()

def to_representation(self, instance: Document):
return super().to_representation(instance=RfcStatus.from_document(instance))


class RelatedRfcSerializer(serializers.Serializer):
id = serializers.IntegerField(source="source.id")
number = serializers.IntegerField(source="source.rfc_number")
title = serializers.CharField(source="source.title")


class RfcMetadataSerializer(serializers.ModelSerializer):
number = serializers.IntegerField(source="rfc_number")
published = serializers.DateField()
status = RfcStatusSerializer(source="*")
authors = RfcAuthorSerializer(many=True, source="documentauthor_set")
group = GroupSerializer()
area = GroupSerializer(source="group.area", required=False)
stream = StreamNameSerializer()
identifiers = fields.SerializerMethodField()
obsoleted_by = RelatedRfcSerializer(many=True, read_only=True)
updated_by = RelatedRfcSerializer(many=True, read_only=True)

class Meta:
model = Document
fields = [
"id",
"number",
"title",
"published",
"status",
"pages",
"authors",
"group",
"area",
"stream",
"identifiers",
"obsoleted_by",
"updated_by",
]

@extend_schema_field(DocIdentifierSerializer)
def get_identifiers(self, doc: Document):
identifiers = []
if doc.rfc_number:
identifiers.append(
DocIdentifier(type="doi", value=f"10.17487/RFC{doc.rfc_number:04d}")
)
return DocIdentifierSerializer(instance=identifiers, many=True).data
3 changes: 3 additions & 0 deletions ietf/group/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,9 @@ def active_wgs(self):
def closed_wgs(self):
return self.wgs().exclude(state__in=Group.ACTIVE_STATE_IDS)

def areas(self):
return self.get_queryset().filter(type="area")

def with_meetings(self):
return self.get_queryset().filter(type__features__has_meetings=True)

Expand Down
11 changes: 11 additions & 0 deletions ietf/group/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Copyright The IETF Trust 2024, All Rights Reserved
"""django-rest-framework serializers"""
from rest_framework import serializers

from .models import Group


class GroupSerializer(serializers.ModelSerializer):
class Meta:
model = Group
fields = ["acronym", "name"]
11 changes: 11 additions & 0 deletions ietf/name/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Copyright The IETF Trust 2024, All Rights Reserved
"""django-rest-framework serializers"""
from rest_framework import serializers

from .models import StreamName


class StreamNameSerializer(serializers.ModelSerializer):
class Meta:
model = StreamName
fields = ["slug", "name", "desc"]
2 changes: 2 additions & 0 deletions ietf/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
warnings.filterwarnings("ignore", module="tastypie", message="The django.utils.datetime_safe module is deprecated.")
warnings.filterwarnings("ignore", module="oidc_provider", message="The django.utils.timezone.utc alias is deprecated.")
warnings.filterwarnings("ignore", message="The USE_DEPRECATED_PYTZ setting,") # https://github.com/ietf-tools/datatracker/issues/5635
warnings.filterwarnings("ignore", message="The is_dst argument to make_aware\\(\\)") # caused by django-filters when USE_DEPRECATED_PYTZ is true
warnings.filterwarnings("ignore", message="The USE_L10N setting is deprecated.") # https://github.com/ietf-tools/datatracker/issues/5648
warnings.filterwarnings("ignore", message="django.contrib.auth.hashers.CryptPasswordHasher is deprecated.") # https://github.com/ietf-tools/datatracker/issues/5663
warnings.filterwarnings("ignore", message="'urllib3\\[secure\\]' extra is deprecated")
Expand Down Expand Up @@ -454,6 +455,7 @@ def skip_unreadable_post(record):
'django_celery_beat',
'corsheaders',
'django_markup',
'django_filters',
'oidc_provider',
'drf_spectacular',
'drf_standardized_errors',
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ django-celery-beat>=2.3.0
django-csp>=3.7
django-cors-headers>=3.11.0
django-debug-toolbar>=3.2.4
django-filter>=24.3
django-markup>=1.5 # Limited use - need to reconcile against direct use of markdown
django-oidc-provider>=0.8.1 # 0.8 dropped Django 2 support
django-referrer-policy>=1.0
Expand Down