forked from ungleich-public/dynamicweb
clone cms with menus, make menus standalone app
This commit is contained in:
parent
1ece5ce7c7
commit
afb096b7a3
1153 changed files with 318897 additions and 37 deletions
977
cms/plugin_base.py
Normal file
977
cms/plugin_base.py
Normal file
|
|
@ -0,0 +1,977 @@
|
|||
import fnmatch
|
||||
import json
|
||||
import re
|
||||
from collections.abc import Callable
|
||||
from functools import lru_cache
|
||||
|
||||
from django import forms
|
||||
from django.contrib import admin, messages
|
||||
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist, ValidationError
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.template import Context
|
||||
from django.template.response import TemplateResponse
|
||||
from django.utils.encoding import force_str, smart_str
|
||||
from django.utils.functional import lazy
|
||||
from django.utils.html import conditional_escape, escapejs
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext, gettext_lazy as _
|
||||
|
||||
from cms import operations
|
||||
from cms.exceptions import SubClassNeededError
|
||||
from cms.models import CMSPlugin, Page
|
||||
from cms.models.placeholdermodel import Placeholder
|
||||
from cms.toolbar.utils import get_plugin_tree
|
||||
from cms.utils.compat import DJANGO_5_1
|
||||
from cms.utils.conf import get_cms_setting
|
||||
|
||||
|
||||
class CMSPluginBaseMetaclass(forms.MediaDefiningClass):
|
||||
"""
|
||||
Ensure the CMSPlugin subclasses have sane values and set some defaults if
|
||||
they're not given.
|
||||
"""
|
||||
|
||||
def __new__(cls, name, bases, attrs):
|
||||
super_new = super().__new__
|
||||
parents = [base for base in bases if isinstance(base, CMSPluginBaseMetaclass)]
|
||||
if not parents:
|
||||
# If this is CMSPluginBase itself, and not a subclass, don't do anything
|
||||
return super_new(cls, name, bases, attrs)
|
||||
new_plugin = super_new(cls, name, bases, attrs)
|
||||
# validate model is actually a CMSPlugin subclass.
|
||||
if not issubclass(new_plugin.model, CMSPlugin):
|
||||
raise SubClassNeededError(
|
||||
"The 'model' attribute on CMSPluginBase subclasses must be "
|
||||
"either CMSPlugin or a subclass of CMSPlugin. %r on %r is not." % (new_plugin.model, new_plugin)
|
||||
)
|
||||
# validate the template:
|
||||
if not hasattr(new_plugin, "render_template") and not hasattr(new_plugin, "get_render_template"):
|
||||
raise ImproperlyConfigured(
|
||||
"CMSPluginBase subclasses must have a render_template attribute or get_render_template method"
|
||||
)
|
||||
# Set the default form
|
||||
if not new_plugin.form:
|
||||
form_meta_attrs = {
|
||||
"model": new_plugin.model,
|
||||
"exclude": ("position", "placeholder", "language", "plugin_type", "path", "depth"),
|
||||
}
|
||||
form_attrs = {"Meta": type("Meta", (object,), form_meta_attrs)}
|
||||
new_plugin.form = type("%sForm" % name, (forms.ModelForm,), form_attrs)
|
||||
# Set the default fieldsets
|
||||
if not new_plugin.fieldsets:
|
||||
basic_fields = []
|
||||
advanced_fields = []
|
||||
for f in new_plugin.model._meta.fields:
|
||||
if not f.auto_created and f.editable:
|
||||
if hasattr(f, "advanced"):
|
||||
advanced_fields.append(f.name)
|
||||
else:
|
||||
basic_fields.append(f.name)
|
||||
if advanced_fields:
|
||||
new_plugin.fieldsets = [
|
||||
(None, {"fields": basic_fields}),
|
||||
(_("Advanced options"), {"fields": advanced_fields, "classes": ("collapse",)}),
|
||||
]
|
||||
# Set default name
|
||||
if not new_plugin.name:
|
||||
new_plugin.name = re.sub("([a-z])([A-Z])", "\\g<1> \\g<2>", name)
|
||||
|
||||
# By flagging the plugin class, we avoid having to call these class
|
||||
# methods for every plugin all the time.
|
||||
# Instead, we only call them if they are actually overridden.
|
||||
if "get_extra_placeholder_menu_items" in attrs:
|
||||
new_plugin._has_extra_placeholder_menu_items = True
|
||||
|
||||
if "get_extra_plugin_menu_items" in attrs:
|
||||
new_plugin._has_extra_plugin_menu_items = True
|
||||
|
||||
# Normalize allowed_models entries to lowercase (app_label.model)
|
||||
# to allow case-insensitive configuration such as "cms.PageContent".
|
||||
# ContentType.app_label and .model are lowercase, so we match that.
|
||||
allowed_models = getattr(new_plugin, "allowed_models", None)
|
||||
if allowed_models is not None:
|
||||
# Accept any iterable (list/tuple/set) or a single string
|
||||
if isinstance(allowed_models, (list, tuple, set)):
|
||||
new_plugin.allowed_models = [str(item).lower() for item in allowed_models]
|
||||
else:
|
||||
# Coerce single value into list
|
||||
new_plugin.allowed_models = [str(allowed_models).lower()]
|
||||
return new_plugin
|
||||
|
||||
|
||||
def template_slot_caching(method: Callable) -> Callable:
|
||||
"""
|
||||
Decorator that enables global caching for methods based on placeholder slots and templates.
|
||||
|
||||
This decorator marks methods that should participate in the CMS's template and slot-based
|
||||
caching system. Decorated methods will have their results cached according to the
|
||||
configuration specified in CMS_PLACEHOLDER_CONF.
|
||||
|
||||
Args:
|
||||
method: The method to be decorated.
|
||||
|
||||
Returns:
|
||||
The decorated method with template slot caching enabled.
|
||||
|
||||
Example:
|
||||
@template_slot_caching
|
||||
def get_child_class_overrides(cls, slot: str, page: Optional[Page] = None, instance: Optional[CMSPlugin] = None):
|
||||
# Method implementation...
|
||||
pass
|
||||
"""
|
||||
|
||||
method._template_slot_caching = True
|
||||
return method
|
||||
|
||||
|
||||
class CMSPluginBase(admin.ModelAdmin, metaclass=CMSPluginBaseMetaclass):
|
||||
"""
|
||||
Inherits :class:`django:django.contrib.admin.ModelAdmin` and in most respects behaves like a
|
||||
normal subclass.
|
||||
|
||||
Note however that some attributes of ``ModelAdmin`` simply won't make sense in the
|
||||
context of a Plugin.
|
||||
"""
|
||||
|
||||
#: Name of the plugin needs to be set in child classes
|
||||
name = ""
|
||||
|
||||
#: Modules collect plugins of similar type
|
||||
module = _("Generic") # To be overridden in child classes
|
||||
|
||||
#: Custom form class to be used to edit this plugin.
|
||||
form = None
|
||||
|
||||
change_form_template = "admin/cms/page/plugin/change_form.html"
|
||||
"""
|
||||
The template used to render the form when you edit the plugin.
|
||||
|
||||
Example::
|
||||
|
||||
class MyPlugin(CMSPluginBase):
|
||||
model = MyModel
|
||||
name = _("My Plugin")
|
||||
render_template = "cms/plugins/my_plugin.html"
|
||||
change_form_template = "admin/cms/page/plugin_change_form.html"
|
||||
|
||||
See also: :attr:`frontend_edit_template`.
|
||||
"""
|
||||
# If True, displays a preview in the admin. Not used any more.
|
||||
admin_preview = False
|
||||
|
||||
#: The path to the template used to render the template. If ``render_plugin`` is ``True`` either this or
|
||||
#: ``get_render_template`` **must** be defined. See also: :attr:`render_plugin` , :meth:`get_render_template`.
|
||||
render_template = None
|
||||
|
||||
#: If set to ``False``, this plugin will not be rendered at all. If ``True``, :meth:`render_template` must also
|
||||
#: be defined. See also: :attr:`render_template`, :meth:`get_render_template`.
|
||||
render_plugin = True
|
||||
|
||||
#: If the plugin requires per-instance settings, then this setting must be set to a model that inherits from
|
||||
#: :class:`~cms.models.pluginmodel.CMSPlugin`. See also: :ref:`storing configuration`.
|
||||
model = CMSPlugin
|
||||
|
||||
text_enabled = False
|
||||
"""This attribute controls whether your plugin will be usable (and rendered)
|
||||
in a text plugin. When you edit a text plugin on a page, the plugin will show up in
|
||||
the *CMS Plugins* dropdown and can be configured and inserted. The output will even
|
||||
be previewed in the text editor.
|
||||
|
||||
Of course, not all plugins are usable in text plugins. Therefore the default of this
|
||||
attribute is ``False``. If your plugin *is* usable in a text plugin:
|
||||
|
||||
#. set this to ``True``
|
||||
#. make sure your plugin provides its own :meth:`icon_alt`, this will be used as a tooltip in
|
||||
the text-editor and comes in handy when you use multiple plugins in your text.
|
||||
|
||||
See also: :meth:`icon_alt`, :meth:`icon_src`."""
|
||||
|
||||
#: Set to ``True`` if this plugin should only be used in a placeholder that is attached to a django CMS page,
|
||||
#: and not other models with ``PlaceholderRelationFields``. See also: :attr:`child_classes`, :attr:`parent_classes`,
|
||||
#: :attr:`require_parent`.
|
||||
#:
|
||||
#: Deprecated: Use allowed_models attribute instead (e.g., `allowed_models = ["cms.pagecontent"]`)
|
||||
page_only = False
|
||||
|
||||
allowed_models = None
|
||||
"""Plugin-level restriction: A list of valid models where this plugin can be added.
|
||||
|
||||
Each entry must be the dotted path to the model in the format ``"app_label.modelname"``,
|
||||
e.g., ``["cms.pagecontent", "myapp.mymodel"]``.
|
||||
|
||||
- If ``None`` (default): The plugin can be added to any model that has placeholders.
|
||||
- If a list/tuple is provided: The plugin can only be added to models in this list.
|
||||
- If an empty list ``[]``: The plugin cannot be added to any model.
|
||||
|
||||
Note: This can be combined with the model's ``allowed_plugins`` attribute for fine-grained control.
|
||||
Both filters must pass for a plugin to be available on a model.
|
||||
|
||||
See also: Model's ``allowed_plugins`` attribute for model-level restrictions.
|
||||
"""
|
||||
|
||||
allowed_slots = None
|
||||
"""Plugin-level restriction: A list of placeholder slot names where this plugin can be added.
|
||||
|
||||
Each entry may be an exact slot name or a glob pattern (e.g. ``"content"`` or ``"footer_*"``),
|
||||
matched against the placeholder's ``slot``.
|
||||
|
||||
- If ``None`` (default): The plugin can be added to any slot.
|
||||
- If a list/tuple is provided: The plugin can only be added to slots matching one of the entries.
|
||||
- If an empty list ``[]``: The plugin cannot be added to any slot.
|
||||
|
||||
This is the plugin-side counterpart to the ``plugins``/``excluded_plugins`` keys of
|
||||
``CMS_PLACEHOLDER_CONF``. Both filters must pass for the plugin to be available in a slot.
|
||||
|
||||
See also: :attr:`allowed_models`, ``CMS_PLACEHOLDER_CONF``.
|
||||
"""
|
||||
|
||||
allow_children = False
|
||||
"""Allows this plugin to have child plugins - other plugins placed inside it?
|
||||
|
||||
If ``True`` you need to ensure that your plugin can render its children in the plugin template. For example:
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{% load cms_tags %}
|
||||
<div class="myplugin">
|
||||
{{ instance.my_content }}
|
||||
{% for plugin in instance.child_plugin_instances %}
|
||||
{% render_plugin plugin %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
``instance.child_plugin_instances`` provides access to all the plugin's children.
|
||||
They are pre-filled and ready to use. The child plugins should be rendered using
|
||||
the ``{% render_plugin %}`` template tag.
|
||||
|
||||
See also: :attr:`child_classes`, :attr:`parent_classes`, :attr:`require_parent`.
|
||||
"""
|
||||
|
||||
#: A list of Plugin Class Names. If this is set, only plugins listed here can be added to this plugin.
|
||||
#: Entries may be glob patterns (e.g. ``"Bootstrap*"`` or ``"*Link*"``), which are expanded against the
|
||||
#: names of all registered plugins. An empty list (``[]``), or a list of patterns that match no installed
|
||||
#: plugin, means that no plugins are allowed as children, whereas ``None`` (the default) means there is no
|
||||
#: restriction. As a special case, the string ``"auto"`` allows exactly those plugins that explicitly name
|
||||
#: this plugin in their :attr:`parent_classes` -- i.e. the restriction is driven by the children opting in,
|
||||
#: rather than the parent listing them. See also: :attr:`parent_classes`.
|
||||
child_classes = None
|
||||
|
||||
#: Is it required that this plugin is a child of another plugin? Or can it be added to any placeholder, even one
|
||||
#: attached to a page. See also: :attr:`child_classes`, :attr:`parent_classes`.
|
||||
require_parent = False
|
||||
|
||||
#: A list of the names of permissible parent classes for this plugin. Entries may be glob patterns
|
||||
#: (e.g. ``"Bootstrap*"`` or ``"*Link*"``), which are expanded against the names of all registered plugins.
|
||||
#: An empty list (``[]``), or a list of patterns that match no installed plugin, means that the plugin allows
|
||||
#: no parents and can therefore only be added to a placeholder, whereas ``None`` (the default) means there is
|
||||
#: no restriction. See also: :attr:`child_classes`, :attr:`require_parent`.
|
||||
parent_classes = None
|
||||
|
||||
#: Disables *dragging* of child plugins in structure mode.
|
||||
disable_child_plugins = False
|
||||
|
||||
#: Marks this plugin as a *slot* — a structural container that is not directly editable by the user.
|
||||
#:
|
||||
#: If ``True``, the plugin will be rendered in structure mode normally, but double-clicking on it will
|
||||
#: not open the plugin edit dialog. The user will not have a direct way to change the plugin instance.
|
||||
#:
|
||||
#: Moving or adding child plugins are not affected.
|
||||
is_slot = False
|
||||
|
||||
#: Determines if the add plugin modal is shown for this plugin (default: yes). Useful for plugins which have no
|
||||
#: fields to fill, or which have valid default values for *all* fields.
|
||||
#: If the plugin's form will not validate with the default values the add plugin modal is shown with the form
|
||||
#: errors.
|
||||
show_add_form = True
|
||||
|
||||
#: The plugin does not modify the context or request and its rendering is not influenced by its parent
|
||||
#: plugins. Defaults to ``True`` unless :setting:`CMS_ALWAYS_REFRESH_CONTENT` is set to ``True``.
|
||||
is_local = not get_cms_setting("ALWAYS_REFRESH_CONTENT")
|
||||
|
||||
# Warning: setting these to False, may have a serious performance impact, because their child-parent-relation must
|
||||
# be recomputed each time the plugin tree is rendered. If a parent with cache_child_classes is True but has
|
||||
# children with cache_parent_classes set to False, the child classes will not be cached.
|
||||
cache_child_classes = True
|
||||
cache_parent_classes = True
|
||||
|
||||
_has_extra_placeholder_menu_items = False
|
||||
_has_extra_plugin_menu_items = False
|
||||
|
||||
cache = get_cms_setting("PLUGIN_CACHE")
|
||||
"""Is this plugin cacheable? If your plugin displays content based on the user or
|
||||
request or other dynamic properties set this to ``False``.
|
||||
|
||||
If present and set to ``False``, the plugin will prevent the caching of
|
||||
the resulting page.
|
||||
|
||||
.. important:: Setting this to ``False`` will effectively disable the
|
||||
CMS page cache and all upstream caches for pages where
|
||||
the plugin appears. This may be useful in certain cases
|
||||
but for general cache management, consider using the much
|
||||
more capable :meth:`get_cache_expiration`.
|
||||
|
||||
.. warning::
|
||||
|
||||
If you disable a plugin cache be sure to restart the server and clear the cache afterwards.
|
||||
"""
|
||||
|
||||
#: This flag is used to determine whether a plugin is considered a “system” (internal) plugin — meaning,
|
||||
#: it is used by the cms itself to structure content or placeholders,
|
||||
#: not meant to be added, edited, or deleted directly by end users in the admin or frontend editor.
|
||||
#: When set to ``True``, the cms treats that plugin as infrastructure, not user content (e.g djangocms-transfer).
|
||||
system = False
|
||||
|
||||
opts = {}
|
||||
|
||||
def __init__(self, model=None, admin_site=None):
|
||||
if admin_site:
|
||||
super().__init__(self.model, admin_site)
|
||||
|
||||
self.object_successfully_changed = False
|
||||
self.placeholder = None
|
||||
self.page = None
|
||||
self.cms_plugin_instance = None
|
||||
# The _cms_initial_attributes acts as a hook to set
|
||||
# certain values when the form is saved.
|
||||
# Currently, this only happens on plugin creation.
|
||||
self._cms_initial_attributes = {}
|
||||
self._operation_token = None
|
||||
|
||||
def _get_render_template(self, context, instance, placeholder):
|
||||
if hasattr(self, "get_render_template"):
|
||||
template = self.get_render_template(context, instance, placeholder)
|
||||
elif getattr(self, "render_template", False):
|
||||
template = getattr(self, "render_template", False)
|
||||
else:
|
||||
template = None
|
||||
|
||||
if not template:
|
||||
raise ValidationError("plugin has no render_template: %s" % self.__class__)
|
||||
return template
|
||||
|
||||
@classmethod
|
||||
def get_render_queryset(cls):
|
||||
return cls.model._default_manager.all()
|
||||
|
||||
def render(self, context, instance, placeholder):
|
||||
"""This method returns the context to be used to render the template
|
||||
specified in :attr:`render_template`.
|
||||
|
||||
:param dict context: The context with which the page is rendered.
|
||||
:param instance: The instance of your plugin that is rendered.
|
||||
:type instance: :class:`cms.models.pluginmodel.CMSPlugin` instance
|
||||
:param str placeholder: The name of the placeholder that is rendered.
|
||||
:rtype: `dict` or :class:`django.template.Context`
|
||||
|
||||
This method must return a dictionary or an instance of
|
||||
:class:`django.template.Context`, which will be used as context to render the
|
||||
plugin template.
|
||||
|
||||
By default, this method will add ``instance`` and ``placeholder`` to the
|
||||
context, which means for simple plugins, there is no need to overwrite this
|
||||
method.
|
||||
|
||||
If you overwrite this method it's recommended to always populate the context
|
||||
with default values by calling the render method of the super class::
|
||||
|
||||
def render(self, context, instance, placeholder):
|
||||
context = super().render(context, instance, placeholder)
|
||||
...
|
||||
return context
|
||||
|
||||
"""
|
||||
context["instance"] = instance
|
||||
context["placeholder"] = placeholder
|
||||
return context
|
||||
|
||||
@classmethod
|
||||
def is_allowed_in_slot(cls, slot: str | None) -> bool:
|
||||
"""Return whether this plugin may be added to a placeholder with the given ``slot``.
|
||||
|
||||
Honours the :attr:`allowed_slots` plugin-level restriction. A ``slot`` of ``None``
|
||||
(an unbound query, e.g. the global plugin listing) is always allowed.
|
||||
"""
|
||||
if cls.allowed_slots is None or slot is None:
|
||||
return True
|
||||
# Tolerate a bare string (e.g. ``allowed_slots = "content"``) instead of
|
||||
# iterating over its characters, which would silently misbehave.
|
||||
patterns = [cls.allowed_slots] if isinstance(cls.allowed_slots, str) else cls.allowed_slots
|
||||
return any(fnmatch.fnmatchcase(slot, pattern) for pattern in patterns)
|
||||
|
||||
@classmethod
|
||||
def requires_parent_plugin(cls, slot, page):
|
||||
if cls.get_require_parent(slot, page):
|
||||
return True
|
||||
|
||||
allowed_parents = cls.get_parent_classes(slot, page)
|
||||
return bool(allowed_parents)
|
||||
|
||||
@classmethod
|
||||
@lru_cache
|
||||
def _get_template_for_conf(cls, page: Page | None, instance: CMSPlugin | None = None):
|
||||
"""Cache page template because page.get_template() might have to fetch the page content object from the db
|
||||
since django CMS 4"""
|
||||
if page and hasattr(page, "get_template"):
|
||||
# Make the database access lazy, so that it only happens if needed.
|
||||
return lazy(page.get_template, str)()
|
||||
if (
|
||||
instance is not None
|
||||
and instance.placeholder.source
|
||||
and hasattr(instance.placeholder.source, "get_template")
|
||||
):
|
||||
# If source object has get_template method, use it (lazily)
|
||||
return lazy(instance.placeholder.source.get_template, str)()
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
@template_slot_caching
|
||||
def get_require_parent(cls, slot: str, page: Page | None = None, instance: CMSPlugin | None = None) -> bool:
|
||||
from cms.utils.placeholder import get_placeholder_conf
|
||||
|
||||
template = cls._get_template_for_conf(page, instance)
|
||||
|
||||
# config overrides..
|
||||
require_parent = get_placeholder_conf("require_parent", slot, template, default=cls.require_parent)
|
||||
return require_parent
|
||||
|
||||
def get_cache_expiration(self, request, instance, placeholder):
|
||||
"""
|
||||
Provides hints to the placeholder, and in turn to the page for
|
||||
determining the appropriate Cache-Control headers to add to the
|
||||
HTTPResponse object.
|
||||
|
||||
:param request: Relevant ``HTTPRequest`` instance.
|
||||
:param instance: The ``CMSPlugin`` instance that is being rendered.
|
||||
:rtype: ``None`` or ``datetime`` or ``time_delta`` or ``int``
|
||||
|
||||
Must return one of:
|
||||
|
||||
:``None``:
|
||||
This means the placeholder and the page will not even
|
||||
consider this plugin when calculating the page expiration;
|
||||
|
||||
:``datetime``:
|
||||
A specific date and time (timezone-aware) in the future
|
||||
when this plugin's content expires;
|
||||
|
||||
.. important:: The returned ``datetime`` must be timezone-aware
|
||||
or the plugin will be ignored (with a warning)
|
||||
during expiration calculations.
|
||||
|
||||
|
||||
:``datetime.timedelta``:
|
||||
A timedelta instance indicating how long, relative to
|
||||
the response timestamp that the content can be cached;
|
||||
|
||||
:``int``:
|
||||
An integer number of seconds that this plugin's content can be cached.
|
||||
|
||||
There are constants are defined in ``cms.constants`` that may be
|
||||
useful: :const:`~cms.constants.EXPIRE_NOW` and :const:`~cms.constants.MAX_EXPIRATION_TTL`.
|
||||
|
||||
An integer value of 0 (zero) or :const:`~cms.constants.EXPIRE_NOW` effectively means "do not
|
||||
cache". Negative values will be treated as :const:`~cms.constants.EXPIRE_NOW`. Values exceeding the value
|
||||
:const:`~cms.constants.MAX_EXPIRATION_TTL` will be set to that value.
|
||||
|
||||
Negative `timedelta` values or those greater than ``MAX_EXPIRATION_TTL``
|
||||
will also be ranged in the same manner.
|
||||
|
||||
Similarly, ``datetime`` values earlier than now will be treated as
|
||||
``EXPIRE_NOW``. Values greater than ``MAX_EXPIRATION_TTL`` seconds in the
|
||||
future will be treated as ``MAX_EXPIRATION_TTL`` seconds in the future.
|
||||
"""
|
||||
return None
|
||||
|
||||
def get_vary_cache_on(self, request, instance, placeholder):
|
||||
"""
|
||||
Returns an HTTP VARY header string or a list of them to be considered by the placeholder
|
||||
and in turn by the page to caching behaviour.
|
||||
|
||||
Overriding this method is optional.
|
||||
|
||||
Must return one of:
|
||||
|
||||
:``None``:
|
||||
This means that this plugin declares no headers for the cache
|
||||
to be varied upon. (default)
|
||||
|
||||
:string:
|
||||
The name of a header to vary caching upon.
|
||||
|
||||
:list of strings:
|
||||
A list of strings, each corresponding to a header to vary the
|
||||
cache upon.
|
||||
|
||||
.. note::
|
||||
This only makes sense to use with caching. If this plugin has
|
||||
``cache = False`` or plugin.get_cache_expiration(...) returns 0,
|
||||
get_vary_cache_on() will have no effect.
|
||||
"""
|
||||
return None
|
||||
|
||||
def render_change_form(self, request, context, add=False, change=False, form_url="", obj=None):
|
||||
"""
|
||||
We just need the popup interface here
|
||||
"""
|
||||
context.update(
|
||||
{
|
||||
"preview": "no_preview" not in request.GET,
|
||||
"is_popup": True,
|
||||
"plugin": obj,
|
||||
"CMS_MEDIA_URL": get_cms_setting("MEDIA_URL"),
|
||||
}
|
||||
)
|
||||
|
||||
return super().render_change_form(request, context, add, change, form_url, obj)
|
||||
|
||||
def render_close_frame(
|
||||
self,
|
||||
request: HttpRequest,
|
||||
obj: CMSPlugin | None,
|
||||
action: str | None = "edit",
|
||||
extra_data: Context | None = None,
|
||||
extra_context: Context | None = None,
|
||||
) -> HttpResponse:
|
||||
"""
|
||||
Renders the close frame for a CMS plugin in the Django admin interface.
|
||||
|
||||
This method is used to send information to the javascript frontend
|
||||
after an edit action has taken place. It allows the javascript frontend
|
||||
to update the structure and content on the edit endpoints.
|
||||
|
||||
Args:
|
||||
request (HttpRequest): The HTTP request object.
|
||||
obj (CMSPlugin): The CMS plugin instance being rendered.
|
||||
action (Optional[str]): The action being performed on the plugin.
|
||||
Defaults to "edit". Possible values are "add", "edit", "move", and "delete".
|
||||
extra_data (Optional[Context]): Additional data to include in
|
||||
the data bridge to the frontend. Defaults to None.
|
||||
extra_context (Optional[Context]): Additional context to include in
|
||||
the rendering. Defaults to None.
|
||||
|
||||
Returns:
|
||||
HttpResponse: The rendered confirmation form as an HTTP response.
|
||||
|
||||
Raises:
|
||||
ObjectDoesNotExist: If the parent plugin is a ghost plugin and the
|
||||
plugin tree cannot be fetched.
|
||||
|
||||
Notes:
|
||||
- Handles edge cases where the parent plugin is a ghost plugin.
|
||||
- Constructs the plugin tree structure and gathers plugin-specific
|
||||
data for rendering.
|
||||
- Includes messages from the request in the context.
|
||||
"""
|
||||
|
||||
dj_messages = [
|
||||
{
|
||||
"level": message.level,
|
||||
"message": message.message,
|
||||
"tags": message.tags,
|
||||
}
|
||||
for message in messages.get_messages(request)
|
||||
]
|
||||
|
||||
if not obj:
|
||||
# No object specified - just close the modal frame
|
||||
context = {
|
||||
"is_popup": True,
|
||||
"data_bridge": {
|
||||
"action": action,
|
||||
"messages": dj_messages,
|
||||
**(extra_data or {}),
|
||||
},
|
||||
**(extra_context or {}),
|
||||
}
|
||||
return TemplateResponse(request, "admin/cms/page/plugin/confirm_form.html", context)
|
||||
|
||||
try:
|
||||
root = obj.parent.get_bound_plugin() if obj.parent else obj
|
||||
except ObjectDoesNotExist:
|
||||
# This is a nasty edge-case.
|
||||
# If the parent plugin is a ghost plugin, fetching the plugin tree
|
||||
# will fail because the downcasting function filters out all ghost plugins.
|
||||
# Currently, this case is only present in the djangocms-text app
|
||||
# which uses ghost plugins to create inline plugins on the text.
|
||||
root = obj
|
||||
|
||||
plugins = [root] + list(root.get_descendants())
|
||||
|
||||
target_plugin = (
|
||||
plugins[0]
|
||||
if action in ("add", "delete")
|
||||
else next((plugin for plugin in plugins if plugin.pk == obj.pk), None)
|
||||
)
|
||||
structure = get_plugin_tree(request, plugins, target_plugin=target_plugin)
|
||||
plugin_data = next(
|
||||
(plugin_data for plugin_data in structure["plugins"] if plugin_data["plugin_id"] == obj.pk), {}
|
||||
)
|
||||
|
||||
context = {
|
||||
"plugin": obj,
|
||||
"is_popup": True,
|
||||
"data_bridge": {
|
||||
**plugin_data,
|
||||
"action": action,
|
||||
"plugin_desc": escapejs(force_str(obj.get_short_description())),
|
||||
"structure": structure,
|
||||
"messages": dj_messages,
|
||||
**(extra_data or {}),
|
||||
},
|
||||
**(extra_context or {}),
|
||||
}
|
||||
|
||||
return TemplateResponse(request, "admin/cms/page/plugin/confirm_form.html", context)
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
"""
|
||||
Override original method, and add some attributes to obj
|
||||
This has to be made, because if the object is newly created, it must know
|
||||
where it lives.
|
||||
"""
|
||||
from django.contrib.admin import site
|
||||
|
||||
pl = obj.placeholder
|
||||
pl_admin = site._registry[obj.placeholder.__class__]
|
||||
operation_kwargs = {
|
||||
"request": request,
|
||||
"placeholder": pl,
|
||||
}
|
||||
if change:
|
||||
operation_kwargs["old_plugin"] = self.model.objects.get(pk=obj.pk)
|
||||
operation_kwargs["new_plugin"] = obj
|
||||
operation_kwargs["operation"] = operations.CHANGE_PLUGIN
|
||||
else:
|
||||
parent_id = obj.parent.pk if obj.parent else None
|
||||
tree_order = obj.placeholder.get_plugin_tree_order(obj.language, parent_id)
|
||||
operation_kwargs["plugin"] = obj
|
||||
operation_kwargs["operation"] = operations.ADD_PLUGIN
|
||||
operation_kwargs["tree_order"] = tree_order
|
||||
# Remember the operation token
|
||||
self._operation_token = pl_admin._send_pre_placeholder_operation(**operation_kwargs)
|
||||
# Saves the plugin
|
||||
# remember the saved object
|
||||
self.saved_object = pl.add_plugin(obj)
|
||||
pl.clear_cache(obj.language)
|
||||
|
||||
def save_form(self, request, form, change):
|
||||
obj = super().save_form(request, form, change)
|
||||
|
||||
for field, value in self._cms_initial_attributes.items():
|
||||
# Set the initial attribute hooks (if any)
|
||||
setattr(obj, field, value)
|
||||
return obj
|
||||
|
||||
def response_add(self, request, obj, **kwargs):
|
||||
self.object_successfully_changed = True
|
||||
# Normally we would add the user message to say the object
|
||||
# was added successfully but looks like the CMS has not
|
||||
# supported this and can lead to issues with plugins
|
||||
# like ckeditor.
|
||||
return self.render_close_frame(request, obj, action="add")
|
||||
|
||||
def response_change(self, request, obj):
|
||||
self.object_successfully_changed = True
|
||||
opts = self.model._meta
|
||||
msg_dict = {
|
||||
"name": conditional_escape(force_str(opts.verbose_name)),
|
||||
"obj": conditional_escape(force_str(obj)),
|
||||
}
|
||||
msg = mark_safe(_('The %(name)s "%(obj)s" was changed successfully.') % msg_dict)
|
||||
self.message_user(request, msg, messages.SUCCESS)
|
||||
return self.render_close_frame(request, obj, action="change")
|
||||
|
||||
def log_addition(self, request, obj, bypass=None):
|
||||
pass
|
||||
|
||||
def log_change(self, request, obj, message, bypass=None):
|
||||
pass
|
||||
|
||||
def log_deletion(self, request, obj, object_repr, bypass=None):
|
||||
pass
|
||||
|
||||
def icon_src(self, instance):
|
||||
"""Deprecated: Since djangocms-text-ckeditor introduced inline previews of plugins, the icon will not be
|
||||
rendered in TextPlugins anymore.
|
||||
|
||||
By default, this returns an empty string, which, if left un-overridden would result in no icon
|
||||
rendered at all, which, in turn, would render the plugin un-editable by the operator inside a parent
|
||||
text plugin.
|
||||
|
||||
Therefore, this should be overridden when the plugin has text_enabled set to True to return the path
|
||||
to an icon to display in the text of the text plugin.
|
||||
|
||||
:param instance: The instance of the plugin model.
|
||||
:type instance: :class:`cms.models.pluginmodel.CMSPlugin` instance
|
||||
|
||||
Example::
|
||||
|
||||
def icon_src(self, instance):
|
||||
return static("cms/img/icons/plugins/link.png")
|
||||
|
||||
See also: :attr:`text_enabled`, :meth:`icon_alt`
|
||||
"""
|
||||
return ""
|
||||
|
||||
def icon_alt(self, instance):
|
||||
"""
|
||||
Overwrite this if necessary if ``text_enabled = True``
|
||||
Return the 'alt' text to be used for an icon representing
|
||||
the plugin object in a text editor.
|
||||
|
||||
:param instance: The instance of the plugin model to provide specific information
|
||||
for the 'alt' text.
|
||||
:type instance: :class:`cms.models.pluginmodel.CMSPlugin` instance
|
||||
|
||||
By default, :meth:`icon_alt` will return a string of the form: "[plugin type] -
|
||||
[instance]", but can be modified to return anything you like.
|
||||
|
||||
This function accepts the ``instance`` as a parameter and returns a string to be
|
||||
used as the ``alt`` text for the plugin's preview or icon.
|
||||
|
||||
Authors of text-enabled plugins should consider overriding this function as
|
||||
it will be rendered as a tooltip in most browser. This is useful, because if
|
||||
the same plugin is used multiple times, this tooltip can provide information about
|
||||
its configuration.
|
||||
|
||||
See also: :attr:`text_enabled`, :meth:`icon_src`.
|
||||
|
||||
|
||||
"""
|
||||
return f"{force_str(self.name)} - {force_str(instance)}"
|
||||
|
||||
def get_fieldsets(self, request, obj=None):
|
||||
"""
|
||||
Same as from base class except if there are no fields, show an info message.
|
||||
"""
|
||||
fieldsets = super().get_fieldsets(request, obj)
|
||||
|
||||
for name, data in fieldsets:
|
||||
if data.get("fields"): # if fieldset with non-empty fields is found, return fieldsets
|
||||
return fieldsets
|
||||
|
||||
if self.inlines:
|
||||
return [] # if plugin has inlines but no own fields return empty fieldsets to remove empty white fieldset
|
||||
|
||||
try: # if all fieldsets are empty (assuming there is only one fieldset then) add description
|
||||
fieldsets[0][1]["description"] = self.get_empty_change_form_text(obj=obj)
|
||||
except KeyError:
|
||||
pass
|
||||
return fieldsets
|
||||
|
||||
@classmethod
|
||||
def get_empty_change_form_text(cls, obj=None):
|
||||
"""
|
||||
Returns the text displayed to the user when editing a plugin
|
||||
that requires no configuration.
|
||||
"""
|
||||
return gettext("There are no further settings for this plugin. Please press save.")
|
||||
|
||||
@classmethod
|
||||
@template_slot_caching
|
||||
def get_child_class_overrides(cls, slot: str, page: Page | None = None, instance: CMSPlugin | None = None):
|
||||
"""
|
||||
Returns a list of plugin types that are allowed
|
||||
as children of this plugin.
|
||||
"""
|
||||
from cms.plugin_pool import plugin_pool
|
||||
from cms.utils.placeholder import get_placeholder_conf
|
||||
|
||||
template = cls._get_template_for_conf(page, instance)
|
||||
|
||||
# config overrides..
|
||||
ph_conf = get_placeholder_conf("child_classes", slot, template, default={})
|
||||
child_classes = ph_conf.get(cls.__name__, cls.child_classes)
|
||||
# Expand any glob patterns (e.g. "Bootstrap*") to concrete plugin names.
|
||||
return plugin_pool.resolve_plugin_patterns(child_classes)
|
||||
|
||||
@classmethod
|
||||
def get_child_plugin_candidates(cls, slot: str, page: Page | None = None) -> list:
|
||||
"""
|
||||
Returns a list of all plugin classes
|
||||
that will be considered when fetching
|
||||
all available child classes for this plugin.
|
||||
|
||||
These are the plugins allowed in the placeholder (its ``plugins`` setting) plus the
|
||||
plugins explicitly related to this plugin, i.e., named literally in this plugin's
|
||||
``child_classes`` or naming this plugin literally in their ``parent_classes`` (either
|
||||
on the plugin class or in ``CMS_PLACEHOLDER_CONF``). Glob patterns do not count as
|
||||
explicit relations. ``excluded_plugins`` and ``allowed_slots`` always apply.
|
||||
"""
|
||||
# Adding this as a separate method,
|
||||
# we allow other plugins to affect
|
||||
# the list of child plugin candidates.
|
||||
# Useful in cases like djangocms-text
|
||||
# where only text-only plugins are allowed.
|
||||
from cms.plugin_pool import plugin_pool
|
||||
from cms.utils.placeholder import get_placeholder_conf
|
||||
|
||||
template = cls._get_template_for_conf(page, None)
|
||||
allowed_plugins = get_placeholder_conf("plugins", slot, template)
|
||||
if not allowed_plugins:
|
||||
# No restriction: All plugins are candidates anyway
|
||||
return list(plugin_pool.get_all_plugins(slot, page, root_plugin=False))
|
||||
|
||||
declared_children = get_placeholder_conf("child_classes", slot, template, default={}).get(
|
||||
cls.__name__, cls.child_classes
|
||||
)
|
||||
if isinstance(declared_children, str): # "auto" is covered by the children's parent_classes
|
||||
declared_children = None
|
||||
# Membership tests below are literal: glob patterns never equal a plugin name
|
||||
declared_children = set(declared_children or ())
|
||||
conf_parent_classes = get_placeholder_conf("parent_classes", slot, template, default={})
|
||||
|
||||
def is_candidate(plugin: type[CMSPluginBase]) -> bool:
|
||||
name = plugin.__name__
|
||||
if name in allowed_plugins or name in declared_children:
|
||||
return True
|
||||
return cls.__name__ in (conf_parent_classes.get(name, plugin.parent_classes) or ())
|
||||
|
||||
return [
|
||||
plugin
|
||||
for plugin in plugin_pool.get_all_plugins(slot, page, setting_key=None, root_plugin=False)
|
||||
if is_candidate(plugin)
|
||||
]
|
||||
|
||||
@classmethod
|
||||
@template_slot_caching
|
||||
def get_child_classes(
|
||||
cls, slot, page: Page | None = None, instance: CMSPlugin | None = None, only_uncached: bool = False
|
||||
) -> list[str]:
|
||||
"""
|
||||
Returns a list of plugin types that can be added
|
||||
as children to this plugin.
|
||||
"""
|
||||
# Placeholder overrides are highest in priority
|
||||
child_classes = cls.get_child_class_overrides(slot, page=page, instance=instance)
|
||||
# Get all child plugin candidates - for the same source (and template) as the overrides
|
||||
source = instance.placeholder.source if page is None and instance is not None else page
|
||||
installed_plugins = cls.get_child_plugin_candidates(slot, source)
|
||||
|
||||
if child_classes == "auto":
|
||||
from cms.utils.placeholder import get_placeholder_conf
|
||||
|
||||
# Allow all plugins as children that explicitly declare this plugin as parent,
|
||||
# either on the plugin class or in the placeholder configuration
|
||||
template = cls._get_template_for_conf(page, instance)
|
||||
conf_parent_classes = get_placeholder_conf("parent_classes", slot, template, default={})
|
||||
return [
|
||||
plugin.__name__
|
||||
for plugin in installed_plugins
|
||||
if cls.__name__ in (conf_parent_classes.get(plugin.__name__, plugin.parent_classes) or ())
|
||||
]
|
||||
elif child_classes is not None:
|
||||
# An explicit list of allowed child classes (possibly empty).
|
||||
# An empty list means that no plugins are allowed as children, whereas
|
||||
# ``None`` (handled below) means there is no restriction.
|
||||
# Override skips check if current class is valid parent of child classes
|
||||
return [plugin.__name__ for plugin in installed_plugins if plugin.__name__ in child_classes]
|
||||
|
||||
if only_uncached:
|
||||
installed_plugins = [plugin for plugin in installed_plugins if not plugin.cache_parent_classes]
|
||||
|
||||
child_classes = []
|
||||
plugin_type = cls.__name__
|
||||
|
||||
# The following will go through each
|
||||
# child plugin candidate and check if
|
||||
# has configured parent class restrictions.
|
||||
# If there are restrictions then the plugin
|
||||
# is only a valid child class if the current plugin
|
||||
# matches one of the parent restrictions.
|
||||
# If there are no restrictions then the plugin
|
||||
# is a valid child class.
|
||||
for plugin_class in installed_plugins:
|
||||
allowed_parents = plugin_class.get_parent_classes(slot, page, instance)
|
||||
if allowed_parents is None or plugin_type in allowed_parents:
|
||||
# Plugin has no parent restrictions (``None``) or
|
||||
# current plugin (self) is a configured parent.
|
||||
# An empty ``parent_classes`` list means the plugin allows no
|
||||
# parents and is therefore never a valid child.
|
||||
child_classes.append(plugin_class.__name__)
|
||||
|
||||
return child_classes
|
||||
|
||||
@classmethod
|
||||
@template_slot_caching
|
||||
def get_parent_classes(cls, slot: str, page: Page | None = None, instance: CMSPlugin | None = None) -> list[str]:
|
||||
from cms.plugin_pool import plugin_pool
|
||||
from cms.utils.placeholder import get_placeholder_conf
|
||||
|
||||
template = cls._get_template_for_conf(page, instance)
|
||||
|
||||
# config overrides..
|
||||
ph_conf = get_placeholder_conf("parent_classes", slot, template, default={})
|
||||
parent_classes = ph_conf.get(cls.__name__, cls.parent_classes)
|
||||
# Expand any glob patterns (e.g. "Bootstrap*") to concrete plugin names.
|
||||
return plugin_pool.resolve_plugin_patterns(parent_classes)
|
||||
|
||||
def get_plugin_urls(self) -> list:
|
||||
"""
|
||||
Returns the URL patterns the plugin wants to register views for.
|
||||
They are included under django CMS's page admin URLS in the plugin path
|
||||
(e.g.: ``/admin/cms/page/plugin/<plugin-name>/`` in the default case).
|
||||
|
||||
|
||||
``get_plugin_urls()`` is useful if your plugin needs to talk asynchronously to the admin.
|
||||
"""
|
||||
return []
|
||||
|
||||
def plugin_urls(self):
|
||||
return self.get_plugin_urls()
|
||||
|
||||
plugin_urls = property(plugin_urls)
|
||||
|
||||
@classmethod
|
||||
def get_extra_placeholder_menu_items(cls, request: HttpRequest, placeholder: Placeholder) -> list["PluginMenuItem"] | None:
|
||||
"""Extends the placeholder context menu for all placeholders.
|
||||
|
||||
To add one or more custom context menu items that are displayed in the context menu for all placeholders when
|
||||
in structure mode, override this method in a related plugin to return a list of
|
||||
:class:`cms.plugin_base.PluginMenuItem` instances.
|
||||
"""
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def get_extra_plugin_menu_items(cls, request: HttpRequest, plugin: CMSPlugin) -> list["PluginMenuItem"] | None:
|
||||
"""Extends the plugin context menu for all plugins.
|
||||
|
||||
To add one or more custom context menu items that are displayed in the context menu for all plugins when in
|
||||
structure mode, override this method in a related plugin to return a list of
|
||||
:class:`cms.plugin_base.PluginMenuItem` instances.
|
||||
"""
|
||||
pass
|
||||
|
||||
def __repr__(self):
|
||||
return smart_str(self.name)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class PluginMenuItem:
|
||||
"""
|
||||
Creates an item in the plugin / placeholder menu
|
||||
|
||||
:param name: Item name (label)
|
||||
:param url: URL the item points to. This URL will be called using POST
|
||||
:param data: Data to be POSTed to the above URL
|
||||
:param question: Confirmation text to be shown to the user prior to call the given URL (optional)
|
||||
:param action: Custom action to be called on click; currently supported: 'ajax', 'ajax_add'
|
||||
:param attributes: Dictionary whose content will be added as data-attributes to the menu item
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, name, url, data=None, question=None, action="ajax", attributes=None):
|
||||
if not attributes:
|
||||
attributes = {}
|
||||
|
||||
if data:
|
||||
data = json.dumps(data)
|
||||
|
||||
self.name = name
|
||||
self.url = url
|
||||
self.data = data
|
||||
self.question = question
|
||||
self.action = action
|
||||
self.attributes = attributes
|
||||
Loading…
Add table
Add a link
Reference in a new issue