Skip to content
Open
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
84 changes: 17 additions & 67 deletions djangocms_rest/plugin_rendering.py
Original file line number Diff line number Diff line change
@@ -1,64 +1,34 @@
import json
from typing import Any, TypeVar
from typing import Any
from collections.abc import Iterable

from django.contrib.sites.shortcuts import get_current_site
from django.core.exceptions import ValidationError
from django.core.validators import URLValidator
from django.db import models
from django.utils.html import escape, mark_safe

from cms.models import Placeholder
from cms.plugin_rendering import ContentRenderer
from cms.utils.plugins import get_plugins

from djangocms_rest.serializers.placeholders import PlaceholderSerializer
from djangocms_rest.serializers.plugins import GenericPluginSerializer, base_exclude
from djangocms_rest.serializers.plugins import (
get_auto_model_serializer,
resolve_plugin_serializer,
)
Comment on lines +15 to +18

Check notice

Code scanning / CodeQL

Cyclic import Note

Import of module
djangocms_rest.serializers.plugins
begins an import cycle.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fsbraun can we ignore this?

from djangocms_rest.serializers.utils.cache import (
get_placeholder_rest_cache,
set_placeholder_rest_cache,
)


ModelType = TypeVar("ModelType", bound=models.Model)


def get_auto_model_serializer(model_class: type[ModelType]) -> type:
"""
Build (once) a generic ModelSerializer subclass that excludes
common CMS bookkeeping fields.
"""

opts = model_class._meta
real_fields = {f.name for f in opts.get_fields()}
exclude = tuple(base_exclude & real_fields)

meta_class = type(
"Meta",
(),
{
"model": model_class,
"exclude": exclude,
},
)
return type(
f"{model_class.__name__}AutoSerializer",
(GenericPluginSerializer,),
{
"Meta": meta_class,
},
)


def serialize_cms_plugin(
instance: Any | None, context: dict[str, Any]
) -> dict[str, Any] | None:
if not instance or not hasattr(instance, "get_plugin_instance"):
def serialize_cms_plugin(instance: Any | None, context: dict[str, Any]) -> dict[str, Any] | None:
if not instance or not hasattr(instance, "get_plugin_instance"): # pragma: no cover
return None
plugin_instance, plugin = instance.get_plugin_instance()

model_cls = plugin_instance.__class__
serializer_cls = getattr(plugin, "serializer_class", None)
serializer_cls = resolve_plugin_serializer(plugin, model_cls)
serializer_cls = serializer_cls or get_auto_model_serializer(model_cls)
plugin.__class__.serializer_class = serializer_cls

Expand All @@ -72,10 +42,7 @@
)

# Template for a collapsable object/list
OBJ_TEMPLATE = (
"<details open><summary>{open}</summary>"
'<div class="indent">{value}</div></details>{close}'
)
OBJ_TEMPLATE = "<details open><summary>{open}</summary>" '<div class="indent">{value}</div></details>{close}'

# Tempalte for a non-collasable object/list
FIXED_TEMPLATE = '{open}<div class="indent">{value}</div>{close}'
Expand Down Expand Up @@ -207,17 +174,13 @@

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

def render_plugin(
self, instance, context, placeholder=None, editable: bool = False
):
def render_plugin(self, instance, context, placeholder=None, editable: bool = False):
"""
Render a CMS plugin instance using the serialize_cms_plugin function.
"""
data = serialize_cms_plugin(instance, context) or {}
children = [
self.render_plugin(
child, context, placeholder=placeholder, editable=editable
)
self.render_plugin(child, context, placeholder=placeholder, editable=editable)
for child in getattr(instance, "child_plugin_instances", [])
] or None
content = OBJ_TEMPLATE.format(**highlight_json(data, children=children))
Expand All @@ -229,15 +192,11 @@
content=content,
position=instance.position,
)
placeholder_cache = self._rendered_plugins_by_placeholder.setdefault(
placeholder.pk, {}
)
placeholder_cache = self._rendered_plugins_by_placeholder.setdefault(placeholder.pk, {})
placeholder_cache.setdefault("plugins", []).append(instance)
return mark_safe(content)

def render_plugins(
self, placeholder, language, context, editable=False, template=None
):
def render_plugins(self, placeholder, language, context, editable=False, template=None):
yield "<div class='rest-placeholder' data-placeholder='{placeholder}' data-language='{language}'>".format(
placeholder=placeholder.slot,
language=language,
Expand Down Expand Up @@ -265,9 +224,7 @@
def get_plugins_and_placeholder_lot(
self, placeholder, language, context, editable=False, template=None
) -> Iterable[str]:
yield from super().render_plugins(
placeholder, language, context, editable=editable, template=template
)
yield from super().render_plugins(placeholder, language, context, editable=editable, template=template)

def serialize_placeholder(self, placeholder, context, language, use_cache=True):
context.update({"request": self.request})
Expand Down Expand Up @@ -312,9 +269,7 @@

return plugin_content

def serialize_plugins(
self, placeholder: Placeholder, language: str, context: dict
) -> list:
def serialize_plugins(self, placeholder: Placeholder, language: str, context: dict) -> list:
plugins = get_plugins(
self.request,
placeholder=placeholder,
Expand All @@ -327,9 +282,7 @@
for child_plugin in child_plugins:
child_content = serialize_cms_plugin(child_plugin, context)
if getattr(child_plugin, "child_plugin_instances", None):
child_content["children"] = serialize_children(
child_plugin.child_plugin_instances
)
child_content["children"] = serialize_children(child_plugin.child_plugin_instances)
if child_content:
children_list.append(child_content)
return children_list
Expand All @@ -338,10 +291,7 @@
for plugin in plugins:
plugin_content = serialize_cms_plugin(plugin, context)
if getattr(plugin, "child_plugin_instances", None):
plugin_content["children"] = serialize_children(
plugin.child_plugin_instances
)
plugin_content["children"] = serialize_children(plugin.child_plugin_instances)
if plugin_content:
results.append(plugin_content)
return results

48 changes: 48 additions & 0 deletions djangocms_rest/serializers/alias.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from typing import Any

from djangocms_alias.models import AliasPlugin
from rest_framework import serializers

from djangocms_rest.serializers.plugins import GenericPluginSerializer, base_exclude

Check notice

Code scanning / CodeQL

Cyclic import Note

Import of module
djangocms_rest.serializers.plugins
begins an import cycle.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fsbraun can we ignore this?



class AliasInlineSerializer(GenericPluginSerializer):
content = serializers.SerializerMethodField()

class Meta:
model = AliasPlugin
exclude = tuple(base_exclude)

def get_content(self, instance: AliasPlugin) -> list[dict[str, Any]]:
request = self.request
language = getattr(instance, "language", None)
if not request or not language: # pragma: no cover
return []

alias_stack = getattr(request, "_rest_alias_stack", None)
if alias_stack is None:
alias_stack = []
request._rest_alias_stack = alias_stack

if instance.alias_id in alias_stack:
return []

alias_stack.append(instance.alias_id)
try:
placeholder = instance.alias.get_placeholder(
language=language,
show_draft_content=bool(getattr(request, "_preview_mode", False)),
)
if not placeholder: # pragma: no cover
return []

from djangocms_rest.plugin_rendering import RESTRenderer

Check notice

Code scanning / CodeQL

Cyclic import Note

Import of module
djangocms_rest.plugin_rendering
begins an import cycle.
Copy link
Collaborator Author

@metaforx metaforx Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fsbraun exist to break circular dependencies


renderer = RESTRenderer(request=request)
return renderer.serialize_plugins(
placeholder=placeholder,
language=language,
context=self.context,
)
finally:
alias_stack.pop()
81 changes: 51 additions & 30 deletions djangocms_rest/serializers/plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,7 @@
# Second choice: Use DRF naming conventions to build the default API URL for the related model
model_name = related_model._meta.model_name
try:
return get_absolute_frontend_url(
request, reverse(f"{model_name}-detail", args=(pk,))
)
return get_absolute_frontend_url(request, reverse(f"{model_name}-detail", args=(pk,)))
except NoReverseMatch:
pass

Expand Down Expand Up @@ -132,26 +130,64 @@
request,
field.related_model,
getattr(instance, f"{field.name}_id"),
obj=(
getattr(instance, field.name)
if field.is_cached(instance)
else None
),
obj=(getattr(instance, field.name) if field.is_cached(instance) else None),
)
elif isinstance(field, JSON_FIELDS) and ret.get(field.name):
# If the field is a subclass of JSONField, serialize its value directly
ret[field.name] = serialize_soft_refs(request, ret[field.name])
return ret


def get_plugin_serializer_overrides() -> dict[str, type]:
"""Return built-in serializer overrides keyed by model label."""
overrides: dict[str, type] = {}
if apps.is_installed("djangocms_alias"): # pragma: no cover
from djangocms_rest.serializers.alias import AliasInlineSerializer

Check notice

Code scanning / CodeQL

Cyclic import Note

Import of module
djangocms_rest.serializers.alias
begins an import cycle.
Copy link
Collaborator Author

@metaforx metaforx Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fsbraun exist to break circular dependencies


overrides["djangocms_alias.AliasPlugin"] = AliasInlineSerializer
return overrides


def resolve_plugin_serializer(plugin: Any, model_class: type[Model]) -> type | None:
"""Resolve serializer class using plugin serializer first, then central fallback."""
overrides = get_plugin_serializer_overrides()
model_label = f"{model_class._meta.app_label}.{model_class.__name__}"
return getattr(plugin, "serializer_class", None) or overrides.get(model_label)


def get_auto_model_serializer(model_class: type[Model]) -> type:
"""
Build (once) a generic ModelSerializer subclass that excludes
common CMS bookkeeping fields.
"""

opts = model_class._meta
real_fields = {field.name for field in opts.get_fields()}
exclude = tuple(base_exclude & real_fields)

meta_class = type(
"Meta",
(),
{
"model": model_class,
"exclude": exclude,
},
)
return type(
f"{model_class.__name__}AutoSerializer",
(GenericPluginSerializer,),
{
"Meta": meta_class,
},
)


class PluginDefinitionSerializer(serializers.Serializer):
"""
Serializer for plugin type definitions.
"""

plugin_type = serializers.CharField(
help_text="Unique identifier for the plugin type"
)
plugin_type = serializers.CharField(help_text="Unique identifier for the plugin type")
title = serializers.CharField(help_text="Human readable name of the plugin")
type = serializers.CharField(help_text="Schema type")
properties = serializers.DictField(help_text="Property definitions")
Expand Down Expand Up @@ -186,17 +222,8 @@
definitions = {}

for plugin in plugin_pool.plugins.values():
# Use plugin's serializer_class or create a simple fallback
serializer_cls = getattr(plugin, "serializer_class", None)

if not serializer_cls:

class DynamicModelSerializer(serializers.ModelSerializer):
class Meta:
model = plugin.model
fields = "__all__"

serializer_cls = DynamicModelSerializer
serializer_cls = resolve_plugin_serializer(plugin, plugin.model)
serializer_cls = serializer_cls or get_auto_model_serializer(plugin.model)

try:
serializer_instance = serializer_cls()
Expand All @@ -207,11 +234,7 @@
if field_name in base_exclude:
continue

properties[
field_name
] = PluginDefinitionSerializer.map_field_to_schema(
field, field_name
)
properties[field_name] = PluginDefinitionSerializer.map_field_to_schema(field, field_name)

definitions[plugin.__name__] = {
"name": getattr(plugin, "name", plugin.__name__),
Expand Down Expand Up @@ -270,9 +293,7 @@
# Extract nested properties
properties = {}
for nested_field_name, nested_field in field.fields.items():
properties[
nested_field_name
] = PluginDefinitionSerializer.map_field_to_schema(
properties[nested_field_name] = PluginDefinitionSerializer.map_field_to_schema(
nested_field, nested_field_name
)
if properties:
Expand Down
Loading
Loading