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)