Skip to content

Commit d4bfe19

Browse files
committed
Add slots to components
1 parent e77d34a commit d4bfe19

File tree

5 files changed

+146
-30
lines changed

5 files changed

+146
-30
lines changed

djangocms_frontend/contrib/component/cms_plugins.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
from .models import components
44

5-
for _, plugin in components._registry.values():
5+
for _, plugin, slot_plugins in components._registry.values():
66
globals()[plugin.__name__] = plugin
77
plugin_pool.register_plugin(plugin)
8+
for slot_plugin in slot_plugins:
9+
globals()[slot_plugin.__name__] = slot_plugin
10+
plugin_pool.register_plugin(slot_plugin)

djangocms_frontend/contrib/component/components.py

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import importlib
22

3+
from cms.api import add_plugin
4+
from cms.plugin_base import CMSPluginBase
35
from django import forms
46
from django.utils.module_loading import autodiscover_modules
57
from django.utils.translation import gettext_lazy as _
@@ -22,6 +24,9 @@ def _get_mixin_classes(mixins: list, suffix: str = "") -> list[type]:
2224

2325
class CMSFrontendComponent(forms.Form):
2426
"""Base class for frontend components:"""
27+
28+
slot_template = "djangocms_frontend/slot.html"
29+
2530
@classmethod
2631
def admin_form_factory(cls, **kwargs) -> type:
2732
mixins = getattr(cls._component_meta, "mixins", [])
@@ -48,6 +53,13 @@ def admin_form_factory(cls, **kwargs) -> type:
4853
},
4954
)
5055

56+
@classmethod
57+
def get_slot_plugins(cls) -> dict[str:str]:
58+
slots : list[tuple[str, str]] = getattr(cls._component_meta, "slots", [])
59+
return {
60+
f"{cls.__name__}{slot[0].capitalize()}Plugin": slot[1] for slot in slots
61+
}
62+
5163
@classmethod
5264
def plugin_model_factory(cls) -> type:
5365
model_class = type(
@@ -72,6 +84,7 @@ def plugin_model_factory(cls) -> type:
7284
@classmethod
7385
def plugin_factory(cls) -> type:
7486
mixins = getattr(cls._component_meta, "mixins", [])
87+
slots = cls.get_slot_plugins()
7588
mixins = _get_mixin_classes(mixins)
7689

7790
return type(
@@ -85,14 +98,35 @@ def plugin_factory(cls) -> type:
8598
"module": getattr(cls._component_meta, "module", _("Component")),
8699
"model": cls.plugin_model_factory(),
87100
"form": cls.admin_form_factory(),
88-
"allow_children": getattr(cls._component_meta, "allow_children", False),
89-
"child_classes": getattr(cls._component_meta, "child_classes", []),
101+
"allow_children": getattr(cls._component_meta, "allow_children", False) or slots,
102+
"child_classes": getattr(cls._component_meta, "child_classes", []) + list(slots.keys()),
90103
"render_template": getattr(cls._component_meta, "render_template", CMSUIPlugin.render_template),
91104
"fieldsets": getattr(cls, "fieldsets", cls._generate_fieldset()),
92105
"change_form_template": "djangocms_frontend/admin/base.html",
106+
"slots": slots,
107+
"render": cls.render_slot_context,
108+
"save_model": cls.save_model,
93109
},
94110
)
95111

112+
@classmethod
113+
def slot_plugin_factory(cls) -> list[type]:
114+
slots = cls.get_slot_plugins()
115+
return [
116+
type(
117+
slot,
118+
(CMSPluginBase,),
119+
{
120+
"name": slot_name,
121+
"module": getattr(cls._component_meta, "module", _("Component")),
122+
"allow_children": True,
123+
"parent_classes": cls.__name__ + "Plugin",
124+
"render_template": cls.slot_template,
125+
},
126+
)
127+
for slot, slot_name in slots.items()
128+
]
129+
96130
@classmethod
97131
@property
98132
def _component_meta(cls) -> type | None:
@@ -107,13 +141,27 @@ def _generate_fieldset(cls):
107141
def get_short_description(self) -> str:
108142
return ""
109143

144+
def save_model(self, request, obj, form, change):
145+
super(CMSUIPlugin, self).save_model(request, obj, form, change)
146+
if not change:
147+
for slot in self.slots.keys():
148+
add_plugin(obj.placeholder, slot, obj.language, target=obj)
149+
150+
def render_slot_context(self, context, instance, placeholder):
151+
context["instance"] = instance
152+
return context
153+
110154

111155
class Components:
112156
_registry: dict = {}
113157
_discovered: bool = False
114158

115159
def register(self, component):
116-
self._registry[component.__name__] = (component.plugin_model_factory(), component.plugin_factory())
160+
self._registry[component.__name__] = (
161+
component.plugin_model_factory(),
162+
component.plugin_factory(),
163+
component.slot_plugin_factory(),
164+
)
117165
return component
118166

119167

djangocms_frontend/contrib/component/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@
33
# Register all component models for Django
44
# Component models are unmanaged and do not create migrations
55

6-
for model, _ in components._registry.values():
6+
for model, *_ in components._registry.values():
77
globals()[model.__name__] = model

djangocms_frontend/templatetags/frontend.py

Lines changed: 87 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import json
22

33
from classytags.arguments import Argument, MultiKeywordArgument
4-
from classytags.core import Options
4+
from classytags.core import Options, Tag
55
from classytags.helpers import AsTag
66
from cms.templatetags.cms_tags import render_plugin
77
from django import template
@@ -113,11 +113,35 @@ def user_message(context, message):
113113
return {}
114114

115115

116+
@register.tag
117+
class SlotTag(Tag):
118+
name = "slot"
119+
options = Options(
120+
Argument("slot_name", required=True),
121+
blocks=[("endslot", "nodelist")],
122+
)
123+
124+
def render_tag(self, context, slot_name, nodelist):
125+
return ""
126+
127+
116128
class DummyPlugin:
117-
def __init__(self, nodelist):
129+
def __init__(self, nodelist, plugin_type, slot_name: str | None = None) -> "DummyPlugin":
118130
self.nodelist = nodelist
131+
self.plugin_type = (f"{plugin_type}{slot_name.capitalize()}Plugin") if slot_name else "DummyPlugin"
132+
if slot_name is None:
133+
self.parse_slots(nodelist, plugin_type)
119134
super().__init__()
120135

136+
def parse_slots(self, nodelist, plugin_type):
137+
self.slots = [self]
138+
for node in nodelist:
139+
if isinstance(node, SlotTag):
140+
self.slots.append(DummyPlugin(node.nodelist, plugin_type, node.kwargs.get("slot_name")))
141+
142+
def get_instances(self):
143+
return self.slots
144+
121145

122146
class Plugin(AsTag):
123147
name = "plugin"
@@ -134,9 +158,9 @@ def message(self, message):
134158

135159
def get_value(self, context, name, kwargs, nodelist):
136160
if name not in plugin_tag_pool:
137-
return self.message(f"Plugin \"{name}\" not found in pool for plugins usable with {{% plugin %}}")
161+
return self.message(f'Plugin "{name}" not found in pool for plugins usable with {{% plugin %}}')
138162
context.push()
139-
instance = (plugin_tag_pool[name]["defaults"])
163+
instance = plugin_tag_pool[name]["defaults"]
140164
plugin_class = plugin_tag_pool[name]["class"]
141165
if issubclass(plugin_class, CMSUIPlugin):
142166
#
@@ -148,29 +172,70 @@ def get_value(self, context, name, kwargs, nodelist):
148172
# Call render method of plugin
149173
context = plugin_class().render(context, context["instance"], None)
150174
# Replace inner plugins with the nodelist, i.e. the content within the plugin tag
151-
context["instance"].child_plugin_instances = [DummyPlugin(nodelist)]
175+
context["instance"].child_plugin_instances = DummyPlugin(
176+
nodelist, context["instance"].plugin_type
177+
).get_instances()
152178
# ... and redner
153179
result = plugin_tag_pool[name]["template"].render(context.flatten())
154180
context.pop()
155181
return result
156182

157183

158-
register.tag(Plugin.name, Plugin)
184+
class RenderChildPluginsTag(Tag):
185+
"""
186+
This template node is used to render child plugins of a plugin
187+
instance. It allows for selection of certain plugin types.
159188
189+
e.g.: {% childplugins instance %}
160190
161-
@register.simple_tag(takes_context=True)
162-
def render_child_plugins(context, instance, plugin_type=None):
163-
"""Renders the child plugins of a plugin instance"""
164-
if not instance.child_plugin_instances:
165-
return ""
166-
context.push()
167-
context["parent"] = instance
168-
result = ""
169-
for child in instance.child_plugin_instances:
170-
if isinstance(child, DummyPlugin):
171-
result += child.nodelist.render(context)
172-
else:
173-
if plugin_type and child.plugin_type == plugin_type:
174-
result += render_plugin(context, child)
175-
context.pop()
176-
return result if result else getattr(instance, "simple_content", "")
191+
{% childplugins instance "LinkPlugin" %}
192+
193+
{% placeholder "footer" inherit or %}
194+
<a href="/about/">About us</a>
195+
{% endplaceholder %}
196+
197+
Keyword arguments:
198+
name -- the name of the placeholder
199+
inherit -- optional argument which if given will result in inheriting
200+
the content of the placeholder with the same name on parent pages
201+
or -- optional argument which if given will make the template tag a block
202+
tag whose content is shown if the placeholder is empty
203+
"""
204+
205+
name = "childplugins"
206+
options = Options(
207+
# PlaceholderOptions parses until the "endchildplugins" tag is found if
208+
# the "or" option is given
209+
Argument("instance", required=True),
210+
Argument("plugin_type", required=False),
211+
blocks=[("endchildplugins", "nodelist")],
212+
)
213+
214+
def render_tag(self, context, instance, plugin_type, nodelist):
215+
context.push()
216+
context["parent"] = instance
217+
content = ""
218+
if plugin_type and not plugin_type.endswith("Plugin"):
219+
plugin_type = f"{instance.__class__.__name__}{plugin_type.capitalize()}Plugin"
220+
for node in nodelist:
221+
print(type(node), getattr(node, "args", None), getattr(node, "kwargs", None), plugin_type)
222+
for child in instance.child_plugin_instances:
223+
if not plugin_type or child.plugin_type == plugin_type:
224+
if isinstance(child, DummyPlugin):
225+
content += child.nodelist.render(context)
226+
else:
227+
for grand_child in child.child_plugin_instances:
228+
content += render_plugin(context, grand_child)
229+
content = content or getattr(instance, "simple_content", "")
230+
print(f"{content.strip()=}")
231+
232+
if not content.strip() and nodelist:
233+
# "or" parameter given
234+
return nodelist.render(context)
235+
236+
context.pop()
237+
return content
238+
239+
240+
register.tag(Plugin.name, Plugin)
241+
register.tag(RenderChildPluginsTag.name, RenderChildPluginsTag)

tests/test_plugin_tag.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ def test_complex_tags(self):
7474

7575
self.assertInHTML(expected_result, result)
7676

77-
def test_link_component(self):
77+
def test_link_plugin(self):
7878
template = django_engine.from_string("""{% load frontend %}
7979
{% plugin "link" name="Click" external_link="/" link_type="btn" link_context="primary" link_outline=False %}
8080
Click me!
@@ -88,7 +88,7 @@ def test_link_component(self):
8888
self.assertInHTML(expected_result, result)
8989

9090
@override_settings(DEBUG=True)
91-
def test_non_existing_component(self):
91+
def test_non_existing_plugin(self):
9292
template = django_engine.from_string("""{% load frontend %}
9393
{% plugin "nonexisting" %}
9494
This should not be rendered.
@@ -100,7 +100,7 @@ def test_non_existing_component(self):
100100

101101
self.assertEqual(expected_result.strip(), result.strip())
102102

103-
def test_non_frontend_component(self):
103+
def test_non_frontend_plugin(self):
104104
template = django_engine.from_string("""{% load frontend %}
105105
{% plugin "text" body="<p>my text</p>" %}
106106
This should not be rendered.

0 commit comments

Comments
 (0)