Skip to content

Commit 1156c6c

Browse files
committed
Add {% plugin %} template tag.
1 parent 3a44667 commit 1156c6c

File tree

14 files changed

+419
-13
lines changed

14 files changed

+419
-13
lines changed

djangocms_frontend/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,4 @@
1919
13. Github actions will publish the new package to pypi
2020
"""
2121

22-
__version__ = "1.3.2"
22+
__version__ = "2.0.0a"

djangocms_frontend/apps.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from django import apps
2+
3+
4+
class DjangocmsFrontendConfig(apps.AppConfig):
5+
name = "djangocms_frontend"
6+
verbose_name = "DjangoCMS Frontend"
7+
8+
def ready(self):
9+
from .pool import setup
10+
11+
setup()

djangocms_frontend/contrib/carousel/templates/djangocms_frontend/bootstrap5/carousel/default/image.html

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
{% load cms_tags easy_thumbnails_tags %}
22
{% if instance.rel_image %}
3-
{% thumbnail instance.rel_image options.size crop=options.crop upscale=options.upscale subject_location=instance.rel_image.subject_location as thumbnail %}
4-
5-
<img class="d-block w-100" src="{{ thumbnail.url }}" alt="{{ instance.rel_image.default_alt_text|default:'' }}" />
3+
<img class="d-block mx-auto" style="height: 40vh;" src="{{ instance.rel_image.url }}" alt="{{ instance.rel_image.default_alt_text|default:'' }}" />
64
{% else %}
75
<div class="d-block w-100"
86
style="aspect-ratio: {{ aspect_ratio }}">

djangocms_frontend/contrib/component/__init__.py

Whitespace-only changes.

djangocms_frontend/contrib/link/helpers.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,14 @@ def get_object_for_value(value):
6060

6161

6262
def unescape(text, nbsp):
63-
return (text.replace("&nbsp;", nbsp)
64-
.replace("&amp;", "&")
65-
.replace("&lt;", "<")
66-
.replace("&gt;", ">")
67-
.replace("&quot;", '"')
68-
.replace("&#x27;", "'"))
63+
return (
64+
text.replace("&nbsp;", nbsp)
65+
.replace("&amp;", "&")
66+
.replace("&lt;", "<")
67+
.replace("&gt;", ">")
68+
.replace("&quot;", '"')
69+
.replace("&#x27;", "'")
70+
)
6971

7072

7173
def get_link_choices(request, term="", lang=None, nbsp=None):

djangocms_frontend/pool.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import copy
2+
import importlib
3+
import warnings
4+
5+
from cms.plugin_pool import plugin_pool
6+
from cms.templatetags.cms_tags import render_plugin
7+
from django.conf import settings
8+
from django.contrib.admin.sites import site as admin_site
9+
from django.template import engines
10+
from django.template.library import SimpleNode
11+
from django.template.loader import get_template
12+
13+
django_engine = engines["django"]
14+
15+
plugin_tag_pool = {}
16+
17+
18+
IGNORED_FIELDS = (
19+
"id",
20+
"cmsplugin_ptr",
21+
"language",
22+
"plugin_type",
23+
"position",
24+
"creation_date",
25+
"ui_item",
26+
)
27+
28+
allowed_plugin_types = tuple(
29+
getattr(importlib.import_module(cls.rsplit(".", 1)[0]), cls.rsplit(".", 1)[-1]) if isinstance(cls, str) else cls
30+
for cls in getattr(settings, "CMS_COMPONENT_PLUGINS", [])
31+
)
32+
33+
34+
def _get_plugindefaults(instance):
35+
defaults = {
36+
field.name: getattr(instance, field.name)
37+
for field in instance._meta.fields
38+
if field.name not in IGNORED_FIELDS and bool(getattr(instance, field.name))
39+
}
40+
defaults["plugin_type"] = instance.__class__.__name__
41+
return defaults
42+
43+
44+
class _DummyUser:
45+
is_superuser = True
46+
is_staff = True
47+
48+
def has_perm(self, perm):
49+
return True
50+
51+
52+
class _DummyRequest:
53+
user = _DummyUser()
54+
55+
56+
def render_dummy_plugin(context, dummy_plugin):
57+
return dummy_plugin.nodelist.render(context)
58+
59+
60+
def patch_template(template):
61+
"""Patches the template to use the dummy plugin renderer instead of the real one."""
62+
copied_template = copy.deepcopy(template)
63+
patch = False
64+
for node in copied_template.template.nodelist.get_nodes_by_type(SimpleNode):
65+
if node.func == render_plugin:
66+
patch = True
67+
node.func = render_dummy_plugin
68+
return copied_template if patch else template
69+
70+
71+
def setup():
72+
global plugin_tag_pool
73+
74+
for plugin in plugin_pool.get_all_plugins():
75+
if not issubclass(plugin, allowed_plugin_types):
76+
continue
77+
tag_name = plugin.__name__.lower()
78+
if tag_name.endswith("plugin"):
79+
tag_name = tag_name[:-6]
80+
try:
81+
instance = plugin.model()
82+
plugin_admin = plugin(admin_site=admin_site)
83+
if hasattr(instance, "initialize_from_form"):
84+
form_class = plugin_admin.get_form(_DummyRequest())
85+
instance.initialize_from_form(form_class)
86+
if tag_name not in plugin_tag_pool:
87+
template = get_template(plugin_admin._get_render_template({}, instance, None))
88+
plugin_tag_pool[tag_name] = {
89+
"defaults": {
90+
**_get_plugindefaults(instance),
91+
**dict(plugin_type=plugin.__name__),
92+
},
93+
"template": patch_template(template),
94+
"class": plugin,
95+
}
96+
else:
97+
warnings.warn(
98+
f"Duplicate candidates for {{% plugin \"{tag_name}\" %}} found. "
99+
f"Only registered {plugin_tag_pool[tag_name]['class'].__name__}.", stacklevel=1)
100+
except Exception as exc:
101+
warnings.warn(str(exc), stacklevel=1)

djangocms_frontend/templatetags/frontend.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import json
22

3+
from classytags.arguments import Argument, MultiKeywordArgument
4+
from classytags.core import Options
5+
from classytags.helpers import AsTag
36
from django import template
7+
from django.conf import settings as django_settings
48
from django.contrib.contenttypes.models import ContentType
59
from django.core.serializers.json import DjangoJSONEncoder
610
from django.template.defaultfilters import safe
@@ -9,8 +13,10 @@
913
from django.utils.html import conditional_escape, mark_safe
1014

1115
from djangocms_frontend import settings
16+
from djangocms_frontend.cms_plugins import CMSUIPlugin
1217
from djangocms_frontend.fields import HTMLsanitized
1318
from djangocms_frontend.helpers import get_related_object as related_object
19+
from djangocms_frontend.pool import plugin_tag_pool
1420

1521
register = template.Library()
1622

@@ -95,3 +101,49 @@ def framework_info(context, item, as_json=True):
95101
if as_json
96102
else framework_info.get(item, "")
97103
)
104+
105+
106+
class DummyPlugin:
107+
def __init__(self, nodelist):
108+
self.nodelist = nodelist
109+
super().__init__()
110+
111+
112+
class Plugin(AsTag):
113+
name = "plugin"
114+
options = Options(
115+
Argument("name", required=True),
116+
MultiKeywordArgument("kwargs", required=False),
117+
"as",
118+
Argument("varname", resolve=False, required=False),
119+
blocks=[("endplugin", "nodelist")],
120+
)
121+
122+
def message(self, message):
123+
return f"<!-- {message} -->" if django_settings.DEBUG else ""
124+
125+
def get_value(self, context, name, kwargs, nodelist):
126+
if name not in plugin_tag_pool:
127+
return self.message(f"Plugin {name} not found in pool for plugins usable with {{% plugin %}}.")
128+
context.push()
129+
instance = (plugin_tag_pool[name]["defaults"])
130+
plugin_class = plugin_tag_pool[name]["class"]
131+
if issubclass(plugin_class, CMSUIPlugin):
132+
#
133+
instance["config"].update(kwargs)
134+
else:
135+
instance.update(kwargs)
136+
# Create context
137+
context["instance"] = plugin_class.model(**instance)
138+
# Call render method of plugin
139+
context = plugin_class().render(context, context["instance"], None)
140+
# Replace inner plugins with the nodelist, i.e. the content within the plugin tag
141+
context["instance"].child_plugin_instances = [DummyPlugin(nodelist)]
142+
# ... and redner
143+
144+
result = plugin_tag_pool[name]["template"].render(context.flatten())
145+
context.pop()
146+
return result
147+
148+
149+
register.tag(Plugin.name, Plugin)

docs/source/components.rst

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,29 @@ each collapsable section.
3535
Also see Bootstrap 5 `Accordion <https://getbootstrap.com/docs/5.3/components/accordion/>`_
3636
documentation.
3737

38+
Re-usable component example
39+
===========================
40+
41+
The accordion component is a good example of a re-usable component. It can be
42+
used in all your project's templates. Here is an example of how to create an
43+
accordion (if key word arguments are skipped they fall back to their defaults):
44+
45+
.. code-block::
46+
47+
{% load frontend %}
48+
{% plugin "accordion" accordion_header_type="h2" accordion_flush=False %}
49+
{% plugin "accordionitem" accordion_item_header="Accordion item 1" accordion_item_open=True %}
50+
Content of accordion item 1
51+
{% endplugin %}
52+
{% plugin "accordionitem" accordion_item_header="Accordion item 2" %}
53+
Content of accordion item 1
54+
{% endplugin %}
55+
{% endplugin %}
56+
3857
.. index::
3958
single: Alert
4059

60+
4161
***************
4262
Alert component
4363
***************
@@ -61,6 +81,21 @@ the right hand side.
6181
Also see Bootstrap 5 `Alerts <https://getbootstrap.com/docs/5.3/components/alerts/>`_
6282
documentation.
6383

84+
Re-usable component example
85+
===========================
86+
87+
**djangocms-frontend** plugins can be used as components. They can be
88+
used in all your project's templates. Example (if key word arguments are
89+
skipped they fall back to their defaults):
90+
91+
.. code-block::
92+
93+
{% load frontend %}
94+
{% plugin "alert" alert_context="primary" alert_dismissible=True %}
95+
Alert text goes here!
96+
{% endplugin %}
97+
98+
6499
.. index::
65100
single: Badge
66101

@@ -79,6 +114,22 @@ plugin, badges are useful, e.g., to mark featured or new headers.
79114
Also see Bootstrap 5 `Badge <https://getbootstrap.com/docs/5.3/components/badge/>`_
80115
documentation.
81116

117+
Re-usable component example
118+
===========================
119+
120+
**djangocms-frontend** plugins can be used as components. They can be
121+
used in all your project's templates. Example (if key word arguments are
122+
skipped they fall back to their defaults):
123+
124+
.. code-block::
125+
126+
{% load frontend %}
127+
{% plugin "badge" badge_text="My badge" badge_context="info" badge_pills=False %}
128+
This content is ignored.
129+
{% endplugin %}
130+
131+
132+
82133
.. index::
83134
single: Card
84135
single: CardInner
@@ -149,6 +200,31 @@ Here is an example of the new card **Image overlay** feature:
149200
Also see Bootstrap 5 `Card <https://getbootstrap.com/docs/5.3/components/card/>`_
150201
documentation.
151202

203+
Re-usable component example
204+
===========================
205+
206+
**djangocms-frontend** plugins can be used as components. They can be
207+
used in all your project's templates. Example (if key word arguments are
208+
skipped they fall back to their defaults):
209+
210+
.. code-block::
211+
212+
{% load frontend %}
213+
{% plugin "card" card_alignment="center" card_outline="info"
214+
card_text_color="primary" card_full_height=True %}
215+
{% plugin "cardinner" inner_type="card-header" text_alignment="start" %}
216+
<h4>Card title</h4>
217+
{% endplugin %}
218+
{% plugin "cardinner" inner_type="card-body" text_alignment="center" %}
219+
Some quick example text to build on the card title and make up the
220+
bulk of the card's content.
221+
{% endplugin %}
222+
{% plugin "listgroupitem" %}An item{% endplugin %}
223+
{% plugin "listgroupitem" %}A second item{% endplugin %}
224+
{% plugin "listgroupitem" %}A third item{% endplugin %}
225+
{% endplugin %}
226+
227+
152228
.. index::
153229
single: Carousel
154230

@@ -239,6 +315,23 @@ For more information, see
239315
is registered and the logged in user has view permissions: A user will only
240316
see a destination if they can view it in the admin site.
241317

318+
Re-usable component example
319+
===========================
320+
321+
**djangocms-frontend** plugins can be used as components. They can be
322+
used in all your project's templates. Example (if key word arguments are
323+
skipped they fall back to their defaults):
324+
325+
.. code-block::
326+
327+
{% load frontend %}
328+
{% url 'some_view' as some_view %}
329+
{% plugin "textlink" external_link=some_view link_type="btn" link_context="primary" link_outline=False %}
330+
Click me!
331+
{% endplugin %}
332+
333+
334+
242335
********************
243336
List group component
244337
********************

docs/source/getting_started.rst

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,3 +439,44 @@ the upper right corner:
439439
.. image:: screenshots/tab-error-indicator.png
440440

441441

442+
.. _components:
443+
444+
Using frontend plugins as components in templates
445+
=================================================
446+
447+
The plugins of **djangocms-frontend** can be used as components in your
448+
templates - even in apps that do not use or integrate with djanog CMS
449+
otherwise. This is useful if you want use exactly the same markup for, say,
450+
buttons, links, the grid both in pages managed with django CMS and in
451+
other parts of your project.
452+
453+
This allows you to keep one set of templates for your django CMS frontend
454+
plugins and any changes to those templates will be reflected in all parts
455+
of your project.
456+
457+
To use a frontend plugin in a template you need to load the ``frontend`` tags
458+
and then use the ``plugin`` template tag to render a frontend plugin.
459+
460+
.. code::
461+
462+
{% load frontend %}
463+
{% plugin "alert" alert_context="secondary" alert_dismissable=True %}
464+
Here goes the content of the alert.
465+
{% endplugin %}
466+
467+
The plugins will be rendered based on their standard attribute settings.
468+
You can override these settings by passing them as keyword arguments to the
469+
``plugin`` template tag.
470+
471+
See the documentation of the djanog CMS plugins for examples of how to use
472+
the ``{% plugin %}`` template tag with each plugin.
473+
474+
.. note::
475+
476+
While this is designed for **djangocms-frontend** plugins primarily, it
477+
will work with most django CMS plugins.
478+
479+
Since no plugins are created in the database, plugins relying on their
480+
instances being available in the database will potentially not work.
481+
This especially is true for plugins that have a foreign key to
482+
other models.

0 commit comments

Comments
 (0)