Skip to content

Commit ea25fe1

Browse files
committed
feat: add alias default serializer
1 parent 7f76592 commit ea25fe1

File tree

3 files changed

+115
-97
lines changed

3 files changed

+115
-97
lines changed

djangocms_rest/plugin_rendering.py

Lines changed: 16 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,35 @@
11
import json
2-
from typing import Any, TypeVar
2+
from typing import Any
33
from collections.abc import Iterable
44

55
from django.contrib.sites.shortcuts import get_current_site
66
from django.core.exceptions import ValidationError
77
from django.core.validators import URLValidator
8-
from django.db import models
98
from django.utils.html import escape, mark_safe
109

1110
from cms.models import Placeholder
1211
from cms.plugin_rendering import ContentRenderer
1312
from cms.utils.plugins import get_plugins
1413

1514
from djangocms_rest.serializers.placeholders import PlaceholderSerializer
16-
from djangocms_rest.serializers.plugins import GenericPluginSerializer, base_exclude
15+
from djangocms_rest.serializers.plugins import (
16+
get_auto_model_serializer,
17+
resolve_plugin_serializer,
18+
)
1719
from djangocms_rest.serializers.utils.cache import (
1820
get_placeholder_rest_cache,
1921
set_placeholder_rest_cache,
2022
)
2123

2224

23-
ModelType = TypeVar("ModelType", bound=models.Model)
24-
25-
26-
def get_auto_model_serializer(model_class: type[ModelType]) -> type:
27-
"""
28-
Build (once) a generic ModelSerializer subclass that excludes
29-
common CMS bookkeeping fields.
30-
"""
31-
32-
opts = model_class._meta
33-
real_fields = {f.name for f in opts.get_fields()}
34-
exclude = tuple(base_exclude & real_fields)
35-
36-
meta_class = type(
37-
"Meta",
38-
(),
39-
{
40-
"model": model_class,
41-
"exclude": exclude,
42-
},
43-
)
44-
return type(
45-
f"{model_class.__name__}AutoSerializer",
46-
(GenericPluginSerializer,),
47-
{
48-
"Meta": meta_class,
49-
},
50-
)
51-
52-
53-
def serialize_cms_plugin(
54-
instance: Any | None, context: dict[str, Any]
55-
) -> dict[str, Any] | None:
25+
def serialize_cms_plugin(instance: Any | None, context: dict[str, Any]) -> dict[str, Any] | None:
5626
if not instance or not hasattr(instance, "get_plugin_instance"):
5727
return None
5828
plugin_instance, plugin = instance.get_plugin_instance()
5929

6030
model_cls = plugin_instance.__class__
61-
serializer_cls = getattr(plugin, "serializer_class", None)
31+
serializer_cls = resolve_plugin_serializer(plugin, model_cls)
6232
serializer_cls = serializer_cls or get_auto_model_serializer(model_cls)
63-
plugin.__class__.serializer_class = serializer_cls
6433

6534
return serializer_cls(plugin_instance, context=context).data
6635

@@ -72,10 +41,7 @@ def serialize_cms_plugin(
7241
)
7342

7443
# Template for a collapsable object/list
75-
OBJ_TEMPLATE = (
76-
"<details open><summary>{open}</summary>"
77-
'<div class="indent">{value}</div></details>{close}'
78-
)
44+
OBJ_TEMPLATE = "<details open><summary>{open}</summary>" '<div class="indent">{value}</div></details>{close}'
7945

8046
# Tempalte for a non-collasable object/list
8147
FIXED_TEMPLATE = '{open}<div class="indent">{value}</div>{close}'
@@ -207,17 +173,13 @@ class RESTRenderer(ContentRenderer):
207173

208174
placeholder_edit_template = "{content}{plugin_js}{placeholder_js}"
209175

210-
def render_plugin(
211-
self, instance, context, placeholder=None, editable: bool = False
212-
):
176+
def render_plugin(self, instance, context, placeholder=None, editable: bool = False):
213177
"""
214178
Render a CMS plugin instance using the serialize_cms_plugin function.
215179
"""
216180
data = serialize_cms_plugin(instance, context) or {}
217181
children = [
218-
self.render_plugin(
219-
child, context, placeholder=placeholder, editable=editable
220-
)
182+
self.render_plugin(child, context, placeholder=placeholder, editable=editable)
221183
for child in getattr(instance, "child_plugin_instances", [])
222184
] or None
223185
content = OBJ_TEMPLATE.format(**highlight_json(data, children=children))
@@ -229,15 +191,11 @@ def render_plugin(
229191
content=content,
230192
position=instance.position,
231193
)
232-
placeholder_cache = self._rendered_plugins_by_placeholder.setdefault(
233-
placeholder.pk, {}
234-
)
194+
placeholder_cache = self._rendered_plugins_by_placeholder.setdefault(placeholder.pk, {})
235195
placeholder_cache.setdefault("plugins", []).append(instance)
236196
return mark_safe(content)
237197

238-
def render_plugins(
239-
self, placeholder, language, context, editable=False, template=None
240-
):
198+
def render_plugins(self, placeholder, language, context, editable=False, template=None):
241199
yield "<div class='rest-placeholder' data-placeholder='{placeholder}' data-language='{language}'>".format(
242200
placeholder=placeholder.slot,
243201
language=language,
@@ -265,9 +223,7 @@ def render_plugins(
265223
def get_plugins_and_placeholder_lot(
266224
self, placeholder, language, context, editable=False, template=None
267225
) -> Iterable[str]:
268-
yield from super().render_plugins(
269-
placeholder, language, context, editable=editable, template=template
270-
)
226+
yield from super().render_plugins(placeholder, language, context, editable=editable, template=template)
271227

272228
def serialize_placeholder(self, placeholder, context, language, use_cache=True):
273229
context.update({"request": self.request})
@@ -312,9 +268,7 @@ def serialize_placeholder(self, placeholder, context, language, use_cache=True):
312268

313269
return plugin_content
314270

315-
def serialize_plugins(
316-
self, placeholder: Placeholder, language: str, context: dict
317-
) -> list:
271+
def serialize_plugins(self, placeholder: Placeholder, language: str, context: dict) -> list:
318272
plugins = get_plugins(
319273
self.request,
320274
placeholder=placeholder,
@@ -327,9 +281,7 @@ def serialize_children(child_plugins):
327281
for child_plugin in child_plugins:
328282
child_content = serialize_cms_plugin(child_plugin, context)
329283
if getattr(child_plugin, "child_plugin_instances", None):
330-
child_content["children"] = serialize_children(
331-
child_plugin.child_plugin_instances
332-
)
284+
child_content["children"] = serialize_children(child_plugin.child_plugin_instances)
333285
if child_content:
334286
children_list.append(child_content)
335287
return children_list
@@ -338,10 +290,7 @@ def serialize_children(child_plugins):
338290
for plugin in plugins:
339291
plugin_content = serialize_cms_plugin(plugin, context)
340292
if getattr(plugin, "child_plugin_instances", None):
341-
plugin_content["children"] = serialize_children(
342-
plugin.child_plugin_instances
343-
)
293+
plugin_content["children"] = serialize_children(plugin.child_plugin_instances)
344294
if plugin_content:
345295
results.append(plugin_content)
346296
return results
347-
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from typing import Any
2+
3+
from djangocms_alias.models import AliasPlugin
4+
from rest_framework import serializers
5+
6+
from djangocms_rest.serializers.plugins import GenericPluginSerializer, base_exclude
7+
8+
9+
class AliasInlineSerializer(GenericPluginSerializer):
10+
content = serializers.SerializerMethodField()
11+
12+
class Meta:
13+
model = AliasPlugin
14+
exclude = tuple(base_exclude)
15+
16+
def get_content(self, instance: AliasPlugin) -> list[dict[str, Any]]:
17+
request = self.request
18+
language = getattr(instance, "language", None)
19+
if not request or not language:
20+
return []
21+
22+
alias_stack = getattr(request, "_rest_alias_stack", None)
23+
if alias_stack is None:
24+
alias_stack = []
25+
request._rest_alias_stack = alias_stack
26+
27+
if instance.alias_id in alias_stack:
28+
return []
29+
30+
alias_stack.append(instance.alias_id)
31+
try:
32+
placeholder = instance.alias.get_placeholder(
33+
language=language,
34+
show_draft_content=bool(getattr(request, "_preview_mode", False)),
35+
)
36+
if not placeholder:
37+
return []
38+
39+
from djangocms_rest.plugin_rendering import RESTRenderer
40+
41+
renderer = RESTRenderer(request=request)
42+
return renderer.serialize_plugins(
43+
placeholder=placeholder,
44+
language=language,
45+
context=self.context,
46+
)
47+
finally:
48+
alias_stack.pop()

djangocms_rest/serializers/plugins.py

Lines changed: 51 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,7 @@ def serialize_fk(
4545
# Second choice: Use DRF naming conventions to build the default API URL for the related model
4646
model_name = related_model._meta.model_name
4747
try:
48-
return get_absolute_frontend_url(
49-
request, reverse(f"{model_name}-detail", args=(pk,))
50-
)
48+
return get_absolute_frontend_url(request, reverse(f"{model_name}-detail", args=(pk,)))
5149
except NoReverseMatch:
5250
pass
5351

@@ -132,26 +130,64 @@ def to_representation(self, instance: CMSPlugin):
132130
request,
133131
field.related_model,
134132
getattr(instance, f"{field.name}_id"),
135-
obj=(
136-
getattr(instance, field.name)
137-
if field.is_cached(instance)
138-
else None
139-
),
133+
obj=(getattr(instance, field.name) if field.is_cached(instance) else None),
140134
)
141135
elif isinstance(field, JSON_FIELDS) and ret.get(field.name):
142136
# If the field is a subclass of JSONField, serialize its value directly
143137
ret[field.name] = serialize_soft_refs(request, ret[field.name])
144138
return ret
145139

146140

141+
def get_plugin_serializer_overrides() -> dict[str, type]:
142+
"""Return built-in serializer overrides keyed by model label."""
143+
overrides: dict[str, type] = {}
144+
if apps.is_installed("djangocms_alias"):
145+
from djangocms_rest.serializers.alias import AliasInlineSerializer
146+
147+
overrides["djangocms_alias.AliasPlugin"] = AliasInlineSerializer
148+
return overrides
149+
150+
151+
def resolve_plugin_serializer(plugin: Any, model_class: type[Model]) -> type | None:
152+
"""Resolve serializer class using plugin serializer first, then central fallback."""
153+
overrides = get_plugin_serializer_overrides()
154+
model_label = f"{model_class._meta.app_label}.{model_class.__name__}"
155+
return getattr(plugin, "serializer_class", None) or overrides.get(model_label)
156+
157+
158+
def get_auto_model_serializer(model_class: type[Model]) -> type:
159+
"""
160+
Build (once) a generic ModelSerializer subclass that excludes
161+
common CMS bookkeeping fields.
162+
"""
163+
164+
opts = model_class._meta
165+
real_fields = {field.name for field in opts.get_fields()}
166+
exclude = tuple(base_exclude & real_fields)
167+
168+
meta_class = type(
169+
"Meta",
170+
(),
171+
{
172+
"model": model_class,
173+
"exclude": exclude,
174+
},
175+
)
176+
return type(
177+
f"{model_class.__name__}AutoSerializer",
178+
(GenericPluginSerializer,),
179+
{
180+
"Meta": meta_class,
181+
},
182+
)
183+
184+
147185
class PluginDefinitionSerializer(serializers.Serializer):
148186
"""
149187
Serializer for plugin type definitions.
150188
"""
151189

152-
plugin_type = serializers.CharField(
153-
help_text="Unique identifier for the plugin type"
154-
)
190+
plugin_type = serializers.CharField(help_text="Unique identifier for the plugin type")
155191
title = serializers.CharField(help_text="Human readable name of the plugin")
156192
type = serializers.CharField(help_text="Schema type")
157193
properties = serializers.DictField(help_text="Property definitions")
@@ -186,17 +222,8 @@ def generate_plugin_definitions() -> dict[str, Any]:
186222
definitions = {}
187223

188224
for plugin in plugin_pool.plugins.values():
189-
# Use plugin's serializer_class or create a simple fallback
190-
serializer_cls = getattr(plugin, "serializer_class", None)
191-
192-
if not serializer_cls:
193-
194-
class DynamicModelSerializer(serializers.ModelSerializer):
195-
class Meta:
196-
model = plugin.model
197-
fields = "__all__"
198-
199-
serializer_cls = DynamicModelSerializer
225+
serializer_cls = resolve_plugin_serializer(plugin, plugin.model)
226+
serializer_cls = serializer_cls or get_auto_model_serializer(plugin.model)
200227

201228
try:
202229
serializer_instance = serializer_cls()
@@ -207,11 +234,7 @@ class Meta:
207234
if field_name in base_exclude:
208235
continue
209236

210-
properties[
211-
field_name
212-
] = PluginDefinitionSerializer.map_field_to_schema(
213-
field, field_name
214-
)
237+
properties[field_name] = PluginDefinitionSerializer.map_field_to_schema(field, field_name)
215238

216239
definitions[plugin.__name__] = {
217240
"name": getattr(plugin, "name", plugin.__name__),
@@ -270,9 +293,7 @@ def map_field_to_schema(field: serializers.Field, field_name: str = "") -> dict:
270293
# Extract nested properties
271294
properties = {}
272295
for nested_field_name, nested_field in field.fields.items():
273-
properties[
274-
nested_field_name
275-
] = PluginDefinitionSerializer.map_field_to_schema(
296+
properties[nested_field_name] = PluginDefinitionSerializer.map_field_to_schema(
276297
nested_field, nested_field_name
277298
)
278299
if properties:

0 commit comments

Comments
 (0)