Skip to content

Commit ebad887

Browse files
committed
Update plugin-list
1 parent 3db89c4 commit ebad887

File tree

4 files changed

+197
-124
lines changed

4 files changed

+197
-124
lines changed

djangocms_rest/serializers/plugins.py

Lines changed: 148 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from typing import Any, Optional
22

33
from django.apps import apps
4-
from django.core.exceptions import FieldDoesNotExist
54
from django.db.models import Field, Model
65
from django.http import HttpRequest
76
from django.urls import NoReverseMatch, reverse
@@ -157,133 +156,164 @@ class PluginDefinitionSerializer(serializers.Serializer):
157156
type = serializers.CharField(help_text="Schema type")
158157
properties = serializers.DictField(help_text="Property definitions")
159158

159+
@staticmethod
160+
def get_field_type(field: Field) -> str:
161+
"""
162+
Convert Django field types to JSON Schema types.
163+
164+
Args:
165+
field (Field): Django model field instance
166+
167+
Returns:
168+
str: JSON Schema type corresponding to the Django field type
169+
"""
170+
field_mapping = {
171+
"CharField": "string",
172+
"TextField": "string",
173+
"URLField": "string",
174+
"EmailField": "string",
175+
"IntegerField": "integer",
176+
"FloatField": "number",
177+
"DecimalField": "number",
178+
"BooleanField": "boolean",
179+
"DateField": "string",
180+
"DateTimeField": "string",
181+
"TimeField": "string",
182+
"FileField": "string",
183+
"ImageField": "string",
184+
"JSONField": "object",
185+
"ForeignKey": "integer",
186+
}
187+
return field_mapping.get(field.__class__.__name__, "string")
188+
189+
@staticmethod
190+
def get_field_format(field: Field) -> Optional[str]:
191+
"""
192+
Get the format for specific field types.
193+
194+
Args:
195+
field (Field): Django model field instance
196+
197+
Returns:
198+
Optional[str]: JSON Schema format string if applicable, None otherwise
199+
"""
200+
format_mapping = {
201+
"URLField": "uri",
202+
"EmailField": "email",
203+
"DateField": "date",
204+
"DateTimeField": "date-time",
205+
"TimeField": "time",
206+
"FileField": "uri",
207+
"ImageField": "uri",
208+
}
209+
return format_mapping.get(field.__class__.__name__)
160210

161-
def get_field_type(field: Field) -> str:
162-
"""
163-
Convert Django field types to JSON Schema types.
211+
@staticmethod
212+
def generate_plugin_definitions() -> dict[str, Any]:
213+
"""
214+
Generate simple plugin definitions for rendering.
215+
"""
216+
definitions = {}
164217

165-
Args:
166-
field (Field): Django model field instance
218+
for plugin in plugin_pool.get_all_plugins():
219+
# Use plugin's serializer_class or create a simple fallback
220+
serializer_cls = getattr(plugin, "serializer_class", None)
167221

168-
Returns:
169-
str: JSON Schema type corresponding to the Django field type
170-
"""
171-
field_mapping = {
172-
"CharField": "string",
173-
"TextField": "string",
174-
"URLField": "string",
175-
"EmailField": "string",
176-
"IntegerField": "integer",
177-
"FloatField": "number",
178-
"DecimalField": "number",
179-
"BooleanField": "boolean",
180-
"DateField": "string",
181-
"DateTimeField": "string",
182-
"TimeField": "string",
183-
"FileField": "string",
184-
"ImageField": "string",
185-
"JSONField": "object",
186-
"ForeignKey": "integer",
187-
}
188-
return field_mapping.get(field.__class__.__name__, "string")
189-
190-
191-
def get_field_format(field: Field) -> Optional[str]:
192-
"""
193-
Get the format for specific field types.
222+
if not serializer_cls:
194223

195-
Args:
196-
field (Field): Django model field instance
224+
class DynamicModelSerializer(serializers.ModelSerializer):
225+
class Meta:
226+
model = plugin.model
227+
fields = "__all__"
197228

198-
Returns:
199-
Optional[str]: JSON Schema format string if applicable, None otherwise
200-
"""
201-
format_mapping = {
202-
"URLField": "uri",
203-
"EmailField": "email",
204-
"DateField": "date",
205-
"DateTimeField": "date-time",
206-
"TimeField": "time",
207-
"FileField": "uri",
208-
"ImageField": "uri",
209-
}
210-
return format_mapping.get(field.__class__.__name__)
211-
212-
213-
def generate_plugin_definitions() -> dict[str, Any]:
214-
"""
215-
Generate plugin definitions from registered plugins.
216-
217-
Returns:
218-
Dict[str, Any]: A dictionary mapping plugin types to their definitions.
219-
Each definition contains:
220-
- title: Human readable name
221-
- type: Schema type (always "object")
222-
- properties: Field definitions following JSON Schema format
223-
- required: List of required field names
224-
"""
225-
definitions = {}
226-
227-
excluded_fields = {
228-
"cmsplugin_ptr",
229-
"id",
230-
"parent",
231-
"creation_date",
232-
"changed_date",
233-
"position",
234-
"language",
235-
"plugin_type",
236-
"placeholder",
237-
}
238-
239-
for plugin in plugin_pool.get_all_plugins():
240-
model = plugin.model
241-
plugin_class = plugin_pool.get_plugin(plugin.__name__)
242-
243-
properties = {}
244-
required = []
245-
246-
# Get fields from the model
247-
for field in model._meta.get_fields():
248-
# Skip excluded and relation fields
249-
if field.name in excluded_fields or field.is_relation:
250-
continue
229+
serializer_cls = DynamicModelSerializer
251230

252231
try:
253-
model_field = model._meta.get_field(field.name)
254-
field_def = {
255-
"type": get_field_type(model_field),
256-
"description": str(getattr(model_field, "help_text", "") or ""),
257-
}
258-
259-
# Add format if applicable
260-
field_format = get_field_format(model_field)
261-
if field_format:
262-
field_def["format"] = field_format
263-
264-
properties[field.name] = field_def
232+
serializer_instance = serializer_cls()
233+
properties = {}
234+
235+
for field_name, field in serializer_instance.fields.items():
236+
# Skip internal CMS fields
237+
if field_name in base_exclude:
238+
continue
239+
240+
properties[
241+
field_name
242+
] = PluginDefinitionSerializer.map_field_to_schema(
243+
field, field_name
244+
)
265245

266-
# Add to required fields if not nullable
267-
if not getattr(model_field, "blank", True):
268-
required.append(field.name)
246+
definitions[plugin.__name__] = {
247+
"name": getattr(plugin, "name", plugin.__name__),
248+
"title": getattr(plugin, "name", plugin.__name__),
249+
"type": "object",
250+
"properties": properties,
251+
}
269252

270-
except FieldDoesNotExist:
253+
except Exception as e:
254+
# Skip plugins that fail to process
255+
input(e)
271256
continue
272257

273-
# Add plugin_type to properties and required
274-
properties["plugin_type"] = {
275-
"type": "string",
276-
"const": plugin.__name__,
277-
"description": "Plugin identifier",
278-
}
279-
required.append("plugin_type")
280-
281-
definitions[plugin.__name__] = {
282-
"title": getattr(plugin_class, "name", plugin.__name__),
283-
"type": "object",
284-
"properties": properties,
285-
"required": required,
286-
"additionalProperties": False,
258+
return definitions
259+
260+
@staticmethod
261+
def map_field_to_schema(field: serializers.Field, field_name: str = "") -> dict:
262+
"""
263+
Map DRF field to simple JSON Schema definition for rendering.
264+
265+
Args:
266+
field: DRF serializer field instance
267+
field_name: Name of the field (unused but kept for compatibility)
268+
269+
Returns:
270+
dict: Basic JSON Schema definition for the field for TypeScript compatibility
271+
"""
272+
273+
# Field type mapping for TypeScript compatibility
274+
field_mapping = {
275+
"CharField": {"type": "string"},
276+
"TextField": {"type": "string"},
277+
"URLField": {"type": "string", "format": "uri"},
278+
"EmailField": {"type": "string", "format": "email"},
279+
"IntegerField": {"type": "integer"},
280+
"FloatField": {"type": "number"},
281+
"DecimalField": {"type": "number"},
282+
"BooleanField": {"type": "boolean"},
283+
"DateField": {"type": "string", "format": "date"},
284+
"DateTimeField": {"type": "string", "format": "date-time"},
285+
"TimeField": {"type": "string", "format": "time"},
286+
"FileField": {"type": "string", "format": "uri"},
287+
"ImageField": {"type": "string", "format": "uri"},
288+
"JSONField": {"type": "object"},
289+
"ForeignKey": {"type": "integer"},
290+
"PrimaryKeyRelatedField": {"type": "integer"},
291+
"ListField": {"type": "array"},
292+
"DictField": {"type": "object"},
293+
"UUIDField": {"type": "string", "format": "uuid"},
287294
}
288295

289-
return definitions
296+
# Handle special cases first
297+
if isinstance(field, serializers.ChoiceField):
298+
schema = {"type": "string", "enum": list(field.choices.keys())}
299+
elif hasattr(field, "fields"): # Nested serializer
300+
schema = {"type": "object"}
301+
# Extract nested properties
302+
properties = {}
303+
for nested_field_name, nested_field in field.fields.items():
304+
properties[
305+
nested_field_name
306+
] = PluginDefinitionSerializer.map_field_to_schema(
307+
nested_field, nested_field_name
308+
)
309+
if properties:
310+
schema["properties"] = properties
311+
else:
312+
# Use mapping or default to string
313+
schema = field_mapping.get(field.__class__.__name__, {"type": "string"})
314+
315+
# Description from help_text
316+
if getattr(field, "help_text", None):
317+
schema["description"] = str(field.help_text)
318+
319+
return schema

djangocms_rest/views.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,10 @@
1919
PreviewPageContentSerializer,
2020
)
2121
from djangocms_rest.serializers.placeholders import PlaceholderSerializer
22-
from djangocms_rest.serializers.plugins import (
23-
PluginDefinitionSerializer,
24-
generate_plugin_definitions,
25-
)
22+
from djangocms_rest.serializers.plugins import PluginDefinitionSerializer
2623
from djangocms_rest.utils import get_object, get_site_filtered_queryset
2724
from djangocms_rest.views_base import BaseAPIView, BaseListAPIView
25+
from django.utils.functional import lazy
2826

2927

3028
try:
@@ -49,7 +47,15 @@ def extend_placeholder_schema(func):
4947
return func
5048

5149

52-
PLUGIN_DEFINITIONS = generate_plugin_definitions()
50+
# Generate the plugin definitions once at module load time
51+
# This avoids the need to import the plugin definitions in every view
52+
# and keeps the code cleaner.
53+
# Attn: Dynamic changes to the plugin pool will not be reflected in the
54+
# plugin definitions.
55+
# If you need to update the plugin definitions, you need reassign the variable.
56+
PLUGIN_DEFINITIONS = lazy(
57+
PluginDefinitionSerializer.generate_plugin_definitions, dict
58+
)()
5359

5460

5561
class LanguageListView(BaseAPIView):

tests/endpoints/test_plugin_list.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,22 @@
77

88
class PluginListTestCase(BaseCMSRestTestCase):
99
def test_get(self):
10+
from cms.plugin_pool import plugin_pool
1011

1112
type_checks = PLUGIN_FIELD_TYPES
12-
13+
expected_plugin_types = [
14+
plugin.__name__ for plugin in plugin_pool.get_all_plugins()
15+
]
16+
expected_dummy_plugin_signature = {
17+
"plugin_type": "DummyNumberPlugin",
18+
"title": "Dummy Number Plugin",
19+
"type": "object",
20+
"properties": {
21+
"integer": {"type": "integer"},
22+
"json": {"type": "object"},
23+
"float": {"type": "number"},
24+
},
25+
}
1326
# GET
1427
response = self.client.get(reverse("plugin-list"))
1528
self.assertEqual(response.status_code, 200)
@@ -19,6 +32,14 @@ def test_get(self):
1932
self.assertIsInstance(data, list)
2033
self.assertTrue(len(data) > 0, "Plugin list should not be empty")
2134

35+
# Check completeness
36+
for plugin_type in expected_plugin_types:
37+
self.assertIn(
38+
plugin_type,
39+
[plugin.get("plugin_type") for plugin in data],
40+
f"Plugin type {plugin_type} not found in response",
41+
)
42+
2243
# Check Plugin Types
2344
for plugin in data:
2445
for field, expected_type in type_checks.items():
@@ -29,3 +50,14 @@ def test_get(self):
2950
expected_type,
3051
f"plugin {plugin.get('plugin_type', 'unknown')}",
3152
)
53+
54+
# Check signature of DummyNumberPlugin
55+
dummy_plugin = next(
56+
(
57+
plugin
58+
for plugin in data
59+
if plugin.get("plugin_type") == "DummyNumberPlugin"
60+
),
61+
None,
62+
)
63+
self.assertDictEqual(dummy_plugin, expected_dummy_plugin_signature)

tests/test_app/serializers.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33

44
class CustomSerializer(serializers.Serializer):
5+
id = serializers.IntegerField()
6+
integer = serializers.IntegerField(default=42)
7+
float = serializers.FloatField(default=3.14)
8+
json = serializers.JSONField()
9+
510
def to_representation(self, instance):
611
return {
712
"id": instance.id,

0 commit comments

Comments
 (0)