Skip to content

feat: Add RESTRenderer #42

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 40 commits into
base: main
Choose a base branch
from
Open

feat: Add RESTRenderer #42

wants to merge 40 commits into from

Conversation

fsbraun
Copy link
Member

@fsbraun fsbraun commented Jul 8, 2025

This PR adds an edit view that show the generated JSON structure by placeholder as opposed to the rendered HTML.

  • Renders all placeholders
  • Double-clicking the rendered JSON of a plugin opens the plugin edit form
  • Object attributes can be collapsed
  • Pretty print for dark and light mode
  • Uses placeholder serializer.
image image

Needs test

Summary by Sourcery

Add a proof-of-concept RESTRenderer that outputs root-level plugin data as pretty-printed JSON and plugs into the CMS toolbar via a custom config

New Features:

  • Introduce RESTRenderer to render CMS plugins as JSON in the edit view
  • Add RESTToolbarMixin and VersioningCMSConfig to integrate RESTRenderer into the CMS toolbar

Enhancements:

  • Implement get_auto_model_serializer to auto-generate serializers excluding core CMS fields

Summary by Sourcery

Add a RESTRenderer and supporting serializer utilities to render and interact with CMS content as JSON in the edit view, integrate it into the CMS toolbar, and back it with comprehensive endpoint and integration tests

New Features:

  • Introduce RESTRenderer to render CMS plugins as pretty-printed, interactive JSON in the edit view
  • Integrate RESTRenderer into the CMS toolbar via RESTToolbarMixin and RESTCMSConfig

Enhancements:

  • Implement serialize_fk and serialize_soft_refs to resolve model references as URLs or identifiers
  • Extend GenericPluginSerializer to handle relational and JSON fields automatically
  • Add get_auto_model_serializer for generating plugin serializers excluding core CMS fields
  • Update PlaceholderSerializer to leverage RESTRenderer and optionally render HTML preview
  • Enhance page serializers to include a details API endpoint in page representations

Tests:

  • Add endpoint tests for placeholder-detail and preview, including error cases
  • Add tests for foreign key and soft reference serialization rules
  • Add integration tests for the REST edit endpoint to verify JSON output matches the API

Copy link

sourcery-ai bot commented Jul 8, 2025

Reviewer's Guide

This PR implements a new RESTRenderer to provide a JSON-based edit view for CMS plugin content, integrates it into the toolbar via a custom CMSConfig, enhances plugin serializers to handle foreign keys and nested JSON fields, refactors placeholder and page serializers for JSON rendering and HTML toggling, and updates tests and settings to validate the new functionality.

Class diagram for RESTRenderer and related plugin rendering classes

classDiagram
    class ContentRenderer {
    }
    class RESTRenderer {
        +render_plugin(instance, context, placeholder, editable)
        +render_plugins(placeholder, language, context, editable, template)
        +serialize_placeholder(placeholder, context, language, use_cache)
        +serialize_plugins(placeholder, language, context)
        +get_plugins_and_placeholder_lot(placeholder, language, context, editable, template)
    }
    class GenericPluginSerializer {
        +to_representation(instance)
    }
    class PlaceholderSerializer {
        +__init__(...)
    }
    ContentRenderer <|-- RESTRenderer
    GenericPluginSerializer <|-- get_auto_model_serializer
    RESTRenderer o-- PlaceholderSerializer
    RESTRenderer o-- GenericPluginSerializer
    RESTRenderer o-- ContentRenderer
Loading

Class diagram for RESTToolbarMixin and RESTCMSConfig integration

classDiagram
    class RESTToolbarMixin {
        +content_renderer
    }
    class RESTCMSConfig {
        +cms_enabled
        +cms_toolbar_mixin
    }
    RESTCMSConfig o-- RESTToolbarMixin
Loading

Class diagram for enhanced plugin serialization (serialize_fk, serialize_soft_refs)

classDiagram
    class serialize_fk {
        +serialize_fk(request, related_model, pk, obj)
    }
    class serialize_soft_refs {
        +serialize_soft_refs(request, data)
    }
    class GenericPluginSerializer {
        +to_representation(instance)
    }
    GenericPluginSerializer o-- serialize_fk
    GenericPluginSerializer o-- serialize_soft_refs
Loading

File-Level Changes

Change Details Files
Introduce RESTRenderer and JSON rendering pipeline in the CMS edit view
  • Implement RESTRenderer subclass with serialize_placeholder/serialize_plugins methods
  • Add helper functions get_auto_model_serializer, serialize_cms_plugin, highlight_json/list in plugin_rendering
  • Replace old render_plugin in serializers/utils with JSON-based rendering logic
  • Create CSS for syntax highlighting and collapsible JSON in the toolbar
djangocms_rest/plugin_rendering.py
djangocms_rest/serializers/utils/render.py
djangocms_rest/static/djangocms_rest/highlight.css
Enhance plugin serializers for foreign keys and JSON fields
  • Add GenericPluginSerializer to auto-serialize relations and JSONField data
  • Implement serialize_fk and serialize_soft_refs utilities for nested FK and soft reference resolution
  • Update plugin definitions generator to use modern type hints
djangocms_rest/serializers/plugins.py
Refactor placeholder and page serializers for REST integration and HTML toggle
  • Extend PlaceholderSerializer to defer to RESTRenderer and support an html query flag
  • Add new "details" field to BasePageSerializer and include API endpoint link
  • Adjust type hints from Dict to dict and standardize representation methods
djangocms_rest/serializers/placeholders.py
djangocms_rest/serializers/pages.py
Hook RESTRenderer into the CMS toolbar via custom CMSConfig
  • Create RESTToolbarMixin to override content_renderer with RESTRenderer
  • Define RESTCMSConfig to register toolbar mixin and set get_api_endpoint methods on Page and File
  • Register RestToolbar in cms_toolbars and disable inline editing on app ready
djangocms_rest/cms_config.py
djangocms_rest/cms_toolbars.py
djangocms_rest/apps.py
Expand and adjust tests and settings for the new REST endpoints
  • Refactor placeholder endpoint tests for compact JSON formatting
  • Add test_edit_endpoint to verify JSON edit view output and sync with API endpoint
  • Add test_fk_serializer to cover serialize_fk and serialize_soft_refs behaviors
  • Update test settings to include djangocms_rest, filer, easy_thumbnails and standardize quote style
tests/endpoints/test_placeholders.py
tests/test_edit_endpoint.py
tests/test_fk_serializer.py
tests/settings.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

codecov bot commented Jul 8, 2025

Codecov Report

❌ Patch coverage is 91.30435% with 28 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.56%. Comparing base (cf5982a) to head (e6a9442).

Files with missing lines Patch % Lines
djangocms_rest/plugin_rendering.py 89.16% 6 Missing and 7 partials ⚠️
djangocms_rest/cms_config.py 79.06% 7 Missing and 2 partials ⚠️
djangocms_rest/serializers/plugins.py 96.07% 3 Missing and 1 partial ⚠️
djangocms_rest/serializers/placeholders.py 90.00% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #42      +/-   ##
==========================================
+ Coverage   91.04%   91.56%   +0.52%     
==========================================
  Files          12       16       +4     
  Lines         480      688     +208     
  Branches       42       77      +35     
==========================================
+ Hits          437      630     +193     
- Misses         26       37      +11     
- Partials       17       21       +4     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@fsbraun fsbraun marked this pull request as ready for review July 16, 2025 14:37
Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey @fsbraun - I've reviewed your changes - here's some feedback:

  • You’re generating PLUGIN_DEFINITIONS twice (once in serializers/plugins.py and again in views.py)—consolidate to a single source to avoid divergence.
  • get_auto_model_serializer creates a new serializer class on every call—add caching (e.g. via functools.lru_cache) to reuse generated classes and reduce overhead.
  • serialize_soft_refs mutates its input in place and handles many cases; consider refactoring into smaller pure functions or returning new data structures to reduce side‐effects and improve readability.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- You’re generating PLUGIN_DEFINITIONS twice (once in serializers/plugins.py and again in views.py)—consolidate to a single source to avoid divergence.
- get_auto_model_serializer creates a new serializer class on every call—add caching (e.g. via functools.lru_cache) to reuse generated classes and reduce overhead.
- serialize_soft_refs mutates its input in place and handles many cases; consider refactoring into smaller pure functions or returning new data structures to reduce side‐effects and improve readability.

## Individual Comments

### Comment 1
<location> `djangocms_rest/serializers/plugins.py:40` </location>
<code_context>
+        dict[str, Any]: A dictionary representing the serialized foreign key, typically as a URL or identifier.
+    """
+    # First choice: Check for get_api_endpoint method
+    if hasattr(related_model, "get_api_endpoint"):
+        if obj is None:
+            obj = related_model.objects.filter(pk=pk).first()
</code_context>

<issue_to_address>
Potential performance issue with database query in serialize_fk.

This approach can cause N+1 query issues if serialize_fk is used in a loop. To improve performance, try to pass the object when possible or prefetch related objects in advance.
</issue_to_address>

### Comment 2
<location> `djangocms_rest/serializers/plugins.py:76` </location>
<code_context>
+    """
+    if isinstance(data, list):
+        return [serialize_soft_refs(request, item) for item in data]
+    for key, value in data.items():
+        if isinstance(value, dict) and set(value.keys()) == {"model", "pk"}:
+            app_name, model_name = value["model"].split(".", 1)
</code_context>

<issue_to_address>
Possible mutation of input data in serialize_soft_refs.

Mutating the input dictionary may cause unintended side effects if it's used elsewhere. Please copy the dictionary before modifying, or clearly document this behavior.
</issue_to_address>

### Comment 3
<location> `djangocms_rest/serializers/plugins.py:123` </location>
<code_context>
+        super().__init__(*args, **kwargs)
+        self.request = self.context.get("request", None)
+
+    def to_representation(self, instance: CMSPlugin):
+        request = getattr(self, "request", None)
+
</code_context>

<issue_to_address>
Potential issue with JSON_FIELDS type checking.

Since 'field' is a model field instance and JSON_FIELDS contains serializer field classes, this type check may always fail. Use the appropriate tuple of model field classes or inspect the serializer field mapping for accurate type checking.
</issue_to_address>

### Comment 4
<location> `djangocms_rest/serializers/pages.py:19` </location>
<code_context>
     redirect = serializers.CharField(max_length=2048, allow_null=True)
     absolute_url = serializers.URLField(max_length=200, allow_blank=True)
     path = serializers.CharField(max_length=200)
+    details = serializers.CharField(max_length=200, allow_blank=True)
     is_home = serializers.BooleanField()
     login_required = serializers.BooleanField()
</code_context>

<issue_to_address>
The 'details' field is limited to 200 characters.

Since API endpoint URLs can be longer than 200 characters, increasing the max_length for 'details' would help prevent data truncation.
</issue_to_address>

<suggested_fix>
<<<<<<< SEARCH
    details = serializers.CharField(max_length=200, allow_blank=True)
=======
    details = serializers.CharField(max_length=2048, allow_blank=True)
>>>>>>> REPLACE

</suggested_fix>

### Comment 5
<location> `djangocms_rest/plugin_rendering.py:23` </location>
<code_context>
+ModelType = TypeVar("ModelType", bound=models.Model)
+
+
+def get_auto_model_serializer(model_class: type[ModelType]) -> type:
+    """
+    Build (once) a generic ModelSerializer subclass that excludes
</code_context>

<issue_to_address>
Potential for serializer class cache growth.

Repeated calls for the same model will create duplicate serializer classes and increase memory usage. Caching serializer classes per model is recommended.

Suggested implementation:

```python

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

# Cache for auto-generated serializer classes per model
_auto_model_serializer_cache: dict[type[ModelType], type] = {}


def get_auto_model_serializer(model_class: type[ModelType]) -> type:

```

```python
def get_auto_model_serializer(model_class: type[ModelType]) -> type:
    """
    Build (once) a generic ModelSerializer subclass that excludes
    common CMS bookkeeping fields.
    Uses a cache to avoid duplicate serializer classes per model.
    """
    if model_class in _auto_model_serializer_cache:
        return _auto_model_serializer_cache[model_class]

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

    meta_class = type(
        "Meta",
        (),

```

```python
    serializer_class = type(
        f"{model_class.__name__}AutoSerializer",
        (serializers.ModelSerializer,),
        {"Meta": meta_class},
    )
    _auto_model_serializer_cache[model_class] = serializer_class
    return serializer_class

```
</issue_to_address>

### Comment 6
<location> `tests/test_fk_serializer.py:44` </location>
<code_context>
+        finally:
+            del Pizza.get_api_endpoint
+
+    def test_serialize_soft_refs(self):
+        request = self.get_request(reverse("page-root", kwargs={"language": "en"}))
+
+        pk = Pizza.objects.create(description="Delicious pizza").pk
+
+        # Serialize a single soft reference
+        fk = serialize_soft_refs(
+            request, dict(ref={"model": "test_app.pizza", "pk": pk})
+        )
+        self.assertEqual(fk, {"ref": f"http://testserver/api/pizza/{pk}/"})
+
+        fk = serialize_soft_refs(
+            request, dict(link={"internal_link": f"test_app.pizza:{pk}"})
+        )
+        self.assertEqual(fk, {"link": f"http://testserver/api/pizza/{pk}/"})
+
+        fk = serialize_soft_refs(
+            request, dict(attrs={"data-cms-href": f"test_app.pizza:{pk}"})
+        )
+        self.assertEqual(
+            fk, {"attrs": {"data-cms-href": f"http://testserver/api/pizza/{pk}/"}}
+        )
</code_context>

<issue_to_address>
Soft reference serialization is well-tested.

Please add a test case for a list of soft references to verify recursive handling.
</issue_to_address>

<suggested_fix>
<<<<<<< SEARCH
    def test_serialize_soft_refs(self):
        request = self.get_request(reverse("page-root", kwargs={"language": "en"}))

        pk = Pizza.objects.create(description="Delicious pizza").pk

        # Serialize a single soft reference
        fk = serialize_soft_refs(
            request, dict(ref={"model": "test_app.pizza", "pk": pk})
        )
        self.assertEqual(fk, {"ref": f"http://testserver/api/pizza/{pk}/"})

        fk = serialize_soft_refs(
            request, dict(link={"internal_link": f"test_app.pizza:{pk}"})
        )
        self.assertEqual(fk, {"link": f"http://testserver/api/pizza/{pk}/"})

        fk = serialize_soft_refs(
            request, dict(attrs={"data-cms-href": f"test_app.pizza:{pk}"})
        )
        self.assertEqual(
            fk, {"attrs": {"data-cms-href": f"http://testserver/api/pizza/{pk}/"}}
        )
=======
    def test_serialize_soft_refs(self):
        request = self.get_request(reverse("page-root", kwargs={"language": "en"}))

        pk = Pizza.objects.create(description="Delicious pizza").pk

        # Serialize a single soft reference
        fk = serialize_soft_refs(
            request, dict(ref={"model": "test_app.pizza", "pk": pk})
        )
        self.assertEqual(fk, {"ref": f"http://testserver/api/pizza/{pk}/"})

        fk = serialize_soft_refs(
            request, dict(link={"internal_link": f"test_app.pizza:{pk}"})
        )
        self.assertEqual(fk, {"link": f"http://testserver/api/pizza/{pk}/"})

        fk = serialize_soft_refs(
            request, dict(attrs={"data-cms-href": f"test_app.pizza:{pk}"})
        )
        self.assertEqual(
            fk, {"attrs": {"data-cms-href": f"http://testserver/api/pizza/{pk}/"}}
        )

    def test_serialize_soft_refs_list(self):
        request = self.get_request(reverse("page-root", kwargs={"language": "en"}))

        pk1 = Pizza.objects.create(description="Pizza 1").pk
        pk2 = Pizza.objects.create(description="Pizza 2").pk

        # List of soft references
        refs = [
            {"ref": {"model": "test_app.pizza", "pk": pk1}},
            {"ref": {"model": "test_app.pizza", "pk": pk2}},
            {"link": {"internal_link": f"test_app.pizza:{pk1}"}},
            {"attrs": {"data-cms-href": f"test_app.pizza:{pk2}"}},
        ]
        serialized = serialize_soft_refs(request, refs)
        self.assertEqual(
            serialized,
            [
                {"ref": f"http://testserver/api/pizza/{pk1}/"},
                {"ref": f"http://testserver/api/pizza/{pk2}/"},
                {"link": f"http://testserver/api/pizza/{pk1}/"},
                {"attrs": {"data-cms-href": f"http://testserver/api/pizza/{pk2}/"}},
            ],
        )
>>>>>>> REPLACE

</suggested_fix>

### Comment 7
<location> `djangocms_rest/serializers/plugins.py:60` </location>
<code_context>
+    return f"{app_name}.{model_name}:{pk}"
+
+
+def serialize_soft_refs(request: HttpRequest, data: Any) -> Any:
+    """
+    Serialize soft references in a dictionary or list.
</code_context>

<issue_to_address>
Consider refactoring the foreign-key reference handling into a single helper function and a simplified loop to avoid repeated parsing logic.

```markdown
You’ve added a lot of special-case logic to walk and rewrite foreign-key references. You can collapse most of it into two small helpers—a single “extract model+pk” function, and a simple loop in your serializer—so you don’t repeat `.split()`, `get_model()`, etc. For example:

```python
# utils.py
from django.apps import apps
from typing import Any, Optional, Tuple, Type
from django.db.models import Model

def extract_ref(data: dict[str, Any]) -> Optional[Tuple[Type[Model], Any]]:
    """
    Recognize any of:
      {"model": "app.Model", "pk": "..."}
      {"internal_link": "app.Model:..."}
      {"file_link": "..."}  (assume filer.File)
    and return (ModelClass, pk) or None.
    """
    if "model" in data and "pk" in data:
        app_label, model_name = data["model"].split(".", 1)
        return apps.get_model(app_label, model_name), data["pk"]

    if "internal_link" in data:
        model_label, pk = data["internal_link"].split(":", 1)
        app_label, model_name = model_label.split(".", 1)
        return apps.get_model(app_label, model_name), pk

    if "file_link" in data:
        # hard-code filer.File once
        return apps.get_model("filer", "file"), data["file_link"]

    return None
```

```python
# in your serializer module
from .utils import extract_ref

def serialize_soft_refs(request, obj):
    if isinstance(obj, list):
        return [serialize_soft_refs(request, item) for item in obj]
    if not isinstance(obj, dict):
        return obj

    for key, val in list(obj.items()):
        # catch attrs.data-cms-href
        if key == "attrs" and isinstance(val, dict) and "data-cms-href" in val:
            obj["attrs"]["data-cms-href"] = serialize_fk(
               request, *extract_ref({"internal_link": val["data-cms-href"]})
            )
            continue

        # generic model/pk or internal/file link
        ref = extract_ref(val) if isinstance(val, dict) else None
        if ref:
            obj[key] = serialize_fk(request, *ref)
        elif isinstance(val, (dict, list)):
            obj[key] = serialize_soft_refs(request, val)

    return obj
```

Benefits:
- One place to adjust your model+pk parsing.
- No more four separate `if isinstance(value, dict)…` blocks.
- Easier to unit‐test `extract_ref()` in isolation.
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

dict[str, Any]: A dictionary representing the serialized foreign key, typically as a URL or identifier.
"""
# First choice: Check for get_api_endpoint method
if hasattr(related_model, "get_api_endpoint"):
Copy link

Choose a reason for hiding this comment

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

issue (performance): Potential performance issue with database query in serialize_fk.

This approach can cause N+1 query issues if serialize_fk is used in a loop. To improve performance, try to pass the object when possible or prefetch related objects in advance.

fsbraun and others added 4 commits July 16, 2025 16:48
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
@metaforx
Copy link
Collaborator

@fsbraun : This looks good to me. I tested this with nested foreign key plugins with a custom serializer for django-imagefield. The output is fine in JSON (CMS) and on the placeholders API endpoint. What is not yet working as expected is the plugins definition API endpoint /api/plugins/

I would expect something like this:

[
 {
   "plugin_type": "TextPlugin",
   "name": "Text",
   "type": "object",
   "properties": {
     "plugin_type": {
       "type": "string"
     },
     "body": {
       "type": "string"
     },
     "json": {
       "type": "object"
     },
     "rte": {
       "type": "string",
       "description": "The rich text editor used to create this text. JSON formats vary between editors."
     }
   }
 },
 {
   "plugin_type": "MediaImageDisplayPlugin",
   "name": "Media Image Display",
   "type": "object",
   "properties": {
     "image": {
       "type": "object",
       "properties": {
         "public_id": {
           "type": "string",
           "format": "uuid"
         },
         "name": {
           "type": "string",
           "description": "Falls nicht gesetzt wird der Dateiname verwendet."
         },
         "description": {
           "type": "string"
         },
         "alt": {
           "type": "string",
           "description": "Alternative Textbeschreibung für das Bild (für Barrierefreiheit)"
         },
         "image_width": {
           "type": "integer"
         },
         "image_height": {
           "type": "integer"
         },
         "image_ppoi": {
           "type": "string"
         },
         "absolute_url": {
           "type": "string"
         }
       }
     },
     "plugin_type": {
       "type": "string"
     }
   }
 },
 {
   "plugin_type": "MediaTeaserPlugin",
   "name": "Teaser",
   "type": "object",
   "properties": {
     "plugin_type": {
       "type": "string"
     }
   }
 }
]

But i get

[
    {
        "plugin_type": "TextPlugin",
        "title": "Text",
        "type": "object",
        "properties": {
            "body": {
                "type": "string",
                "description": ""
            },
            "json": {
                "type": "object",
                "description": ""
            },
            "rte": {
                "type": "string",
                "description": "The rich text editor used to create this text. JSON formats vary between editors."
            },
            "plugin_type": {
                "type": "string",
                "const": "TextPlugin",
                "description": "Plugin identifier"
            }
        }
    },
    {
        "plugin_type": "MediaImageDisplayPlugin",
        "title": "Media Image Display",
        "type": "object",
        "properties": {
            "plugin_type": {
                "type": "string",
                "const": "MediaImageDisplayPlugin",
                "description": "Plugin identifier"
            }
        }
    },
    {
        "plugin_type": "MediaTeaserPlugin",
        "title": "Teaser",
        "type": "object",
        "properties": {
            "plugin_type": {
                "type": "string",
                "const": "MediaTeaserPlugin",
                "description": "Plugin identifier"
            }
        }
    }
]

It only returns the object but not the serialized object fields which are needed to create proper types to set typed frontend rendering.

this might be because of the following code fragments which bind it to DRF serializer and field mapping:

serializer_instance = serializer_cls()
for field_name, field in serializer_instance.fields.items():

if hasattr(field, "fields"):
    for nested_field_name, nested_field in field.fields.items():
    
"UUIDField": {"type": "string", "format": "uuid"},
"ChoiceField": {"enum": ...}

serializer_cls = getattr(plugin, "serializer_class", None)

POC implementation

from rest_framework import serializers
from cms.plugin_pool import plugin_pool
from typing import Dict, Any

# CMS internal fields to exclude from all plugin schemas
EXCLUDE_CMS_PLUGIN_FIELDS = {
    "cmsplugin_ptr",
    "id",
    "parent",
    "creation_date",
    "changed_date",
    "position",
    "language",
    "placeholder",
}



def map_field_to_schema(field: serializers.Field, field_name: str = "") -> dict:
    """
    Map DRF field to simple JSON Schema definition for rendering.

    Args:
        field: DRF serializer field instance
        field_name: Name of the field (unused but kept for compatibility)

    Returns:
        dict: Basic JSON Schema definition for the field for TypeScript compatibility
    """

    # Field type mapping for TypeScript compatibility
    field_mapping = {
        "CharField": {"type": "string"},
        "TextField": {"type": "string"},
        "URLField": {"type": "string", "format": "uri"},
        "EmailField": {"type": "string", "format": "email"},
        "IntegerField": {"type": "integer"},
        "FloatField": {"type": "number"},
        "DecimalField": {"type": "number"},
        "BooleanField": {"type": "boolean"},
        "DateField": {"type": "string", "format": "date"},
        "DateTimeField": {"type": "string", "format": "date-time"},
        "TimeField": {"type": "string", "format": "time"},
        "FileField": {"type": "string", "format": "uri"},
        "ImageField": {"type": "string", "format": "uri"},
        "JSONField": {"type": "object"},
        "ForeignKey": {"type": "integer"},
        "PrimaryKeyRelatedField": {"type": "integer"},
        "ListField": {"type": "array"},
        "DictField": {"type": "object"},
        "UUIDField": {"type": "string", "format": "uuid"},
    }

    # Handle special cases first
    if isinstance(field, serializers.ChoiceField):
        schema = {"type": "string", "enum": list(field.choices.keys())}
    elif hasattr(field, "fields"):  # Nested serializer
        schema = {"type": "object"}
        # Extract nested properties
        properties = {}
        for nested_field_name, nested_field in field.fields.items():
            properties[nested_field_name] = map_field_to_schema(nested_field, nested_field_name)
        if properties:
            schema["properties"] = properties
    else:
        # Use mapping or default to string
        schema = field_mapping.get(field.__class__.__name__, {"type": "string"})

    # Description from help_text
    if getattr(field, "help_text", None):
        schema["description"] = str(field.help_text)

    return schema





def generate_plugin_definitions() -> Dict[str, Any]:
    """
    Generate simple plugin definitions for rendering.
    """
    definitions = {}

    for plugin in plugin_pool.get_all_plugins():
        # 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

        try:
            serializer_instance = serializer_cls()
            properties = {}

            for field_name, field in serializer_instance.fields.items():
                # Skip internal CMS fields
                if field_name in EXCLUDE_CMS_PLUGIN_FIELDS:
                    continue

                properties[field_name] = map_field_to_schema(field, field_name)

            definitions[plugin.__name__] = {
                "name": getattr(plugin, "name", plugin.__name__),
                "type": "object",
                "properties": properties,
            }

        except Exception:
            # Skip plugins that fail to process
            continue

    return definitions

Does this point you to a solution for the problem?

@fsbraun
Copy link
Member Author

fsbraun commented Jul 24, 2025

@metaforx I've just pushed an update. Need to add tests for the special cases (enum etc), though.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants