angrybeanie_wagtail/env/lib/python3.12/site-packages/wagtail/api/v2/serializers.py
2025-07-25 21:32:16 +10:00

419 lines
12 KiB
Python

from collections import OrderedDict
from django.urls.exceptions import NoReverseMatch
from modelcluster.models import get_all_child_relations
from rest_framework import relations, serializers
from rest_framework.fields import Field, SkipField
from taggit.managers import _TaggableManager
from wagtail import fields as wagtailcore_fields
from .utils import get_object_detail_url
class TypeField(Field):
"""
Serializes the "type" field of each object.
Example:
"type": "wagtailimages.Image"
"""
def get_attribute(self, instance):
return instance
def to_representation(self, obj):
name = type(obj)._meta.app_label + "." + type(obj).__name__
self.context["view"].seen_types[name] = type(obj)
return name
class DetailUrlField(Field):
"""
Serializes the "detail_url" field of each object.
Example:
"detail_url": "http://api.example.com/v1/images/1/"
"""
def get_attribute(self, instance):
url = get_object_detail_url(
self.context["router"], self.context["request"], type(instance), instance.pk
)
if url:
return url
else:
# Hide the detail_url field if the object doesn't have an endpoint
raise SkipField
def to_representation(self, url):
return url
class PageHtmlUrlField(Field):
"""
Serializes the "html_url" field for pages.
Example:
"html_url": "http://www.example.com/blog/blog-post/"
"""
def get_attribute(self, instance):
return instance
def to_representation(self, page):
try:
return page.full_url
except NoReverseMatch:
return None
class PageTypeField(Field):
"""
Serializes the "type" field for pages.
This takes into account the fact that we sometimes may not have the "specific"
page object by calling "page.specific_class" instead of looking at the object's
type.
Example:
"type": "blog.BlogPage"
"""
def get_attribute(self, instance):
return instance
def to_representation(self, page):
if page.specific_class is None:
return None
name = page.specific_class._meta.app_label + "." + page.specific_class.__name__
self.context["view"].seen_types[name] = page.specific_class
return name
class PageLocaleField(Field):
"""
Serializes the "locale" field for pages.
"""
def get_attribute(self, instance):
return instance
def to_representation(self, page):
return page.locale.language_code
class RelatedField(relations.RelatedField):
"""
Serializes related objects (eg, foreign keys).
Example:
"feed_image": {
"id": 1,
"meta": {
"type": "wagtailimages.Image",
"detail_url": "http://api.example.com/v1/images/1/"
}
}
"""
def __init__(self, *args, **kwargs):
self.serializer_class = kwargs.pop("serializer_class")
super().__init__(*args, **kwargs)
def to_representation(self, value):
serializer = self.serializer_class(context=self.context)
return serializer.to_representation(value)
class PageParentField(relations.RelatedField):
"""
Serializes the "parent" field on Page objects.
Pages don't have a "parent" field so some extra logic is needed to find the
parent page. That logic is implemented in this class.
The representation is the same as the RelatedField class.
"""
def get_attribute(self, instance):
parent = instance.get_parent()
if self.context["base_queryset"].filter(id=parent.id).exists():
return parent
def to_representation(self, value):
serializer_class = get_serializer_class(
value.__class__,
["id", "type", "detail_url", "html_url", "title"],
meta_fields=["type", "detail_url", "html_url"],
base=PageSerializer,
)
serializer = serializer_class(context=self.context)
return serializer.to_representation(value)
class PageAliasOfField(relations.RelatedField):
"""
Serializes the "alias_of" field on Page objects.
"""
def get_attribute(self, instance):
return instance.alias_of
def to_representation(self, value):
serializer_class = get_serializer_class(
value.__class__,
["id", "type", "detail_url", "html_url", "title"],
meta_fields=["type", "detail_url", "html_url"],
base=PageSerializer,
)
serializer = serializer_class(context=self.context)
return serializer.to_representation(value)
class ChildRelationField(Field):
"""
Serializes child relations.
Child relations are any model that is related to a Page using a ParentalKey.
They are used for repeated fields on a page such as carousel items or related
links.
Child objects are part of the pages content so we nest them. The relation is
represented as a list of objects.
Example:
"carousel_items": [
{
"id": 1,
"meta": {
"type": "demo.MyCarouselItem"
},
"title": "First carousel item",
"image": {
"id": 1,
"meta": {
"type": "wagtailimages.Image",
"detail_url": "http://api.example.com/v1/images/1/"
}
}
},
{
"id": 2,
"meta": {
"type": "demo.MyCarouselItem"
},
"title": "Second carousel item (no image)",
"image": null
}
]
"""
def __init__(self, *args, **kwargs):
self.serializer_class = kwargs.pop("serializer_class")
super().__init__(*args, **kwargs)
def to_representation(self, value):
serializer = self.serializer_class(context=self.context)
return [
serializer.to_representation(child_object) for child_object in value.all()
]
class StreamField(Field):
"""
Serializes StreamField values.
Stream fields are stored in JSON format in the database. We reuse that in
the API.
Example:
"body": [
{
"type": "heading",
"value": {
"text": "Hello world!",
"size": "h1"
}
},
{
"type": "paragraph",
"value": "Some content"
}
{
"type": "image",
"value": 1
}
]
Where "heading" is a struct block containing "text" and "size" fields, and
"paragraph" is a simple text block.
Note that foreign keys are represented slightly differently in stream fields
to other parts of the API. In stream fields, a foreign key is represented
by an integer (the ID of the related object) but elsewhere in the API,
foreign objects are nested objects with id and meta as attributes.
"""
def to_representation(self, value):
return value.stream_block.get_api_representation(value, self.context)
class TagsField(Field):
"""
Serializes django-taggit TaggableManager fields.
These fields are a common way to link tags to objects in Wagtail. The API
serializes these as a list of strings taken from the name attribute of each
tag.
Example:
"tags": ["bird", "wagtail"]
"""
def to_representation(self, value):
return list(value.all().order_by("name").values_list("name", flat=True))
class BaseSerializer(serializers.ModelSerializer):
# Add StreamField to serializer_field_mapping
serializer_field_mapping = (
serializers.ModelSerializer.serializer_field_mapping.copy()
)
serializer_field_mapping.update(
{
wagtailcore_fields.StreamField: StreamField,
}
)
serializer_related_field = RelatedField
# Meta fields
type = TypeField(read_only=True)
detail_url = DetailUrlField(read_only=True)
def to_representation(self, instance):
data = OrderedDict()
fields = [field for field in self.fields.values() if not field.write_only]
# Split meta fields from core fields
meta_fields = [
field for field in fields if field.field_name in self.meta_fields
]
fields = [field for field in fields if field.field_name not in self.meta_fields]
# Make sure id is always first. This will be filled in later
if "id" in [field.field_name for field in fields]:
data["id"] = None
# Serialise meta fields
meta = OrderedDict()
for field in meta_fields:
try:
attribute = field.get_attribute(instance)
except SkipField:
continue
if attribute is None:
# We skip `to_representation` for `None` values so that
# fields do not have to explicitly deal with that case.
meta[field.field_name] = None
else:
meta[field.field_name] = field.to_representation(attribute)
if meta:
data["meta"] = meta
# Serialise core fields
for field in fields:
try:
if field.field_name == "admin_display_title":
instance = instance.specific_deferred
attribute = field.get_attribute(instance)
except SkipField:
continue
if attribute is None:
# We skip `to_representation` for `None` values so that
# fields do not have to explicitly deal with that case.
data[field.field_name] = None
else:
data[field.field_name] = field.to_representation(attribute)
return data
def build_property_field(self, field_name, model_class):
# TaggableManager is not a Django field so it gets treated as a property
field = getattr(model_class, field_name)
if isinstance(field, _TaggableManager):
return TagsField, {}
return super().build_property_field(field_name, model_class)
def build_relational_field(self, field_name, relation_info):
field_class, field_kwargs = super().build_relational_field(
field_name, relation_info
)
field_kwargs["serializer_class"] = self.child_serializer_classes[field_name]
return field_class, field_kwargs
class PageSerializer(BaseSerializer):
type = PageTypeField(read_only=True)
locale = PageLocaleField(read_only=True)
html_url = PageHtmlUrlField(read_only=True)
parent = PageParentField(read_only=True)
alias_of = PageAliasOfField(read_only=True)
def build_relational_field(self, field_name, relation_info):
# Find all relation fields that point to child class and make them use
# the ChildRelationField class.
if relation_info.to_many:
model = getattr(self.Meta, "model")
child_relations = {
child_relation.field.remote_field.related_name: child_relation.related_model
for child_relation in get_all_child_relations(model)
}
if (
field_name in child_relations
and field_name in self.child_serializer_classes
):
return ChildRelationField, {
"serializer_class": self.child_serializer_classes[field_name]
}
return super().build_relational_field(field_name, relation_info)
def get_serializer_class(
model,
field_names,
meta_fields,
field_serializer_overrides=None,
child_serializer_classes=None,
base=BaseSerializer,
):
model_ = model
class Meta:
model = model_
fields = list(field_names)
attrs = {
"Meta": Meta,
"meta_fields": list(meta_fields),
"child_serializer_classes": child_serializer_classes or {},
}
if field_serializer_overrides:
attrs.update(field_serializer_overrides)
return type(str(model_.__name__ + "Serializer"), (base,), attrs)