-
-
Notifications
You must be signed in to change notification settings - Fork 4k
/
Copy pathserializers.py
419 lines (323 loc) · 11.9 KB
/
serializers.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
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)