forked from ungleich-public/dynamicweb
370 lines
15 KiB
Python
370 lines
15 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from collections import defaultdict, deque
|
|
from collections.abc import Sequence
|
|
from typing import Any
|
|
|
|
from django.apps import apps
|
|
from django.contrib.contenttypes.models import ContentType
|
|
from django.contrib.sites.models import Site
|
|
from django.db import models
|
|
from django.http import HttpRequest, QueryDict
|
|
from django.urls import NoReverseMatch
|
|
from django.utils.encoding import force_str
|
|
from django.utils.translation import (
|
|
get_language,
|
|
get_language_from_path,
|
|
gettext,
|
|
override as force_language,
|
|
)
|
|
from sekizai.context import SekizaiContext
|
|
from sekizai.helpers import get_varname
|
|
|
|
from cms.constants import PLACEHOLDER_TOOLBAR_JS, PLUGIN_TOOLBAR_JS
|
|
from cms.models import CMSPlugin, PageContent, Placeholder
|
|
from cms.utils.conf import get_cms_setting
|
|
from cms.utils.urlutils import admin_reverse
|
|
|
|
|
|
def get_placeholder_toolbar_js(placeholder: Placeholder, allowed_plugins: list[str] | None = None) -> str:
|
|
label = placeholder.get_label() or ''
|
|
help_text = gettext(
|
|
'Add plugin to placeholder "%(placeholder_label)s"'
|
|
) % {'placeholder_label': label}
|
|
|
|
data = {
|
|
'type': 'placeholder',
|
|
'name': force_str(label),
|
|
'placeholder_id': str(placeholder.pk),
|
|
'plugin_restriction': allowed_plugins or [],
|
|
'addPluginHelpTitle': force_str(help_text),
|
|
'urls': {
|
|
'add_plugin': admin_reverse('cms_placeholder_add_plugin'),
|
|
'copy_plugin': admin_reverse('cms_placeholder_copy_plugins'),
|
|
}
|
|
}
|
|
return PLACEHOLDER_TOOLBAR_JS % {'pk': placeholder.pk, 'config': json.dumps(data)}
|
|
|
|
|
|
def get_plugin_toolbar_info(plugin: CMSPlugin, children: list[str] | None = None, parents: list[str] | None = None) -> dict[str, Any]:
|
|
# ``parents`` is deprecated (see CMSPlugin.get_plugin_info); forwarded for backwards compatibility.
|
|
data = plugin.get_plugin_info(children=children, parents=parents)
|
|
help_text = gettext(
|
|
'Add plugin to %(plugin_name)s'
|
|
) % {'plugin_name': data['plugin_name']}
|
|
data.update({
|
|
"onClose": False,
|
|
"addPluginHelpTitle": force_str(help_text),
|
|
"plugin_order": '',
|
|
"plugin_restriction": children or [],
|
|
})
|
|
return data
|
|
|
|
|
|
def get_plugin_toolbar_js(plugin: CMSPlugin, children: list[str] | None = None, parents: list[str] | None = None) -> str:
|
|
# ``parents`` is deprecated (see CMSPlugin.get_plugin_info); forwarded for backwards compatibility.
|
|
data = get_plugin_toolbar_info(
|
|
plugin,
|
|
children=children,
|
|
parents=parents,
|
|
)
|
|
return PLUGIN_TOOLBAR_JS % {'pk': plugin.pk, 'config': json.dumps(data)}
|
|
|
|
|
|
def create_child_plugin_references(plugins: list[CMSPlugin]) -> deque[CMSPlugin]:
|
|
"""Creates the ``child_plugin_instances`` attribute on each plugin instance after downcasting."""
|
|
plugin_children = defaultdict(deque)
|
|
root_plugins = deque()
|
|
|
|
plugin_ids = frozenset(plugin.pk for plugin in plugins)
|
|
|
|
for plugin in reversed(plugins):
|
|
plugin.child_plugin_instances = plugin_children[plugin.pk]
|
|
|
|
if plugin.parent_id in plugin_ids:
|
|
plugin_children[plugin.parent_id].appendleft(plugin)
|
|
else:
|
|
root_plugins.appendleft(plugin)
|
|
return root_plugins
|
|
|
|
|
|
def get_plugin_tree(
|
|
request: HttpRequest,
|
|
plugins: Sequence[CMSPlugin],
|
|
restrictions: dict | None = None,
|
|
target_plugin: CMSPlugin | None = None,
|
|
) -> dict[str, Any]:
|
|
"""
|
|
Constructs a tree structure of CMS plugins for the toolbar.
|
|
|
|
Args:
|
|
request (HttpRequest): The HTTP request object.
|
|
plugins (list[CMSPlugin]): A list of CMSPlugin instances to be organized into a tree.
|
|
restrictions (Optional[dict], optional): A dictionary of plugin restrictions. Defaults to None.
|
|
target_plugin (Optional[CMSPlugin], optional): The target plugin to render.
|
|
Content will only be rendered if given. Defaults to None.
|
|
|
|
Returns:
|
|
tuple[dict[str, Any]]: A dictionary with up to five keys:
|
|
- 'html': A string of rendered HTML for the plugin tree.
|
|
- 'plugins': A flat list of plugin information dictionaries.
|
|
- 'content': The rendered content of target_plugin (if given) including its children
|
|
- 'target_position': The position of the target_plugin (if given)
|
|
- 'target_placeholder_id': The placeholder id of the target_plugin (if given)
|
|
"""
|
|
from cms.utils.plugins import downcast_plugins, get_plugin_restrictions
|
|
|
|
tree_data = []
|
|
tree_structure = []
|
|
restrictions = restrictions or {}
|
|
toolbar = get_toolbar_from_request(request)
|
|
template = toolbar.templates.drag_item_template
|
|
get_plugin_info = get_plugin_toolbar_info
|
|
placeholder = plugins[0].placeholder
|
|
copy_to_clipboard = toolbar.clipboard and placeholder.pk == toolbar.clipboard.pk
|
|
plugins = list(downcast_plugins(plugins, select_placeholder=True))
|
|
root_plugins = create_child_plugin_references(plugins)
|
|
|
|
def collect_plugin_data(plugin):
|
|
child_classes, _ = get_plugin_restrictions(
|
|
plugin=plugin,
|
|
restrictions_cache=restrictions,
|
|
page=placeholder.source,
|
|
)
|
|
# Make the resolved restrictions available to the drag-item template so it can decide
|
|
# whether children may be added without recomputing them per access.
|
|
plugin.child_class_restrictions = child_classes
|
|
plugin_info = get_plugin_info(
|
|
plugin,
|
|
children=child_classes,
|
|
)
|
|
|
|
tree_data.append(plugin_info)
|
|
|
|
for plugin_instance in plugin.child_plugin_instances:
|
|
collect_plugin_data(plugin_instance)
|
|
|
|
with force_language(toolbar.toolbar_language):
|
|
for root_plugin in root_plugins:
|
|
collect_plugin_data(root_plugin)
|
|
context = {
|
|
'plugin': root_plugin,
|
|
'request': request,
|
|
'clipboard': copy_to_clipboard,
|
|
'cms_toolbar': toolbar,
|
|
}
|
|
tree_structure.append(template.render(context))
|
|
tree_data.reverse()
|
|
|
|
content = {}
|
|
# Render the target plugin if given and all plugins are local
|
|
if target_plugin and all(plugin.plugin_class.is_local for plugin in plugins):
|
|
# Also provide the parent plugin to the context (if available)
|
|
downcasted = next(
|
|
(plugin for plugin in plugins if plugin.pk == target_plugin.pk), None
|
|
)
|
|
parent = next(
|
|
(plugin for plugin in plugins if plugin.pk == target_plugin.parent_id), None
|
|
) if target_plugin.parent_id else None
|
|
content["content"] = get_plugin_content(request, downcasted, {"parent": parent})
|
|
|
|
return {'html': '\n'.join(tree_structure), 'plugins': tree_data, **content}
|
|
|
|
|
|
def get_plugin_content(request: HttpRequest, plugin: CMSPlugin | list[CMSPlugin], context: dict | None = None) -> list[dict[str, Any]]:
|
|
if context is None:
|
|
context = {}
|
|
plugin_list = plugin if isinstance(plugin, list) else [plugin]
|
|
toolbar = get_toolbar_from_request(request)
|
|
renderer = toolbar.content_renderer
|
|
# Switch to edit mode despite the request originally coming from the admin
|
|
toolbar.edit_mode_active = True
|
|
renderer._placeholders_are_editable = True
|
|
sekizai_context = SekizaiContext({'request': request, **context})
|
|
try:
|
|
with force_language(plugin_list[0].language):
|
|
return [{
|
|
"html": renderer.render_plugin(plugin, sekizai_context, placeholder=plugin.placeholder, editable=True),
|
|
"js": "".join(sekizai_context[get_varname()].get("js", [])),
|
|
"css": "".join(sekizai_context[get_varname()].get("css", [])),
|
|
"position": plugin.position,
|
|
"placeholder_id": plugin.placeholder_id,
|
|
"pluginIds": get_plugin_tree_ids(plugin),
|
|
} for plugin in plugin_list]
|
|
except Exception:
|
|
return [] # do not deliver content if rendering fails
|
|
|
|
|
|
def get_plugin_tree_ids(plugin: CMSPlugin) -> list[int]:
|
|
plugin_ids = [plugin.pk]
|
|
for child in plugin.child_plugin_instances or []:
|
|
plugin_ids += get_plugin_tree_ids(child)
|
|
return plugin_ids
|
|
|
|
|
|
def get_toolbar_from_request(request: HttpRequest):
|
|
from .toolbar import EmptyToolbar
|
|
|
|
return getattr(request, 'toolbar', EmptyToolbar(request))
|
|
|
|
|
|
def add_live_url_querystring_param(obj: models.Model, url: str, language: str | None = None) -> str:
|
|
"""
|
|
Append a live url to a given object url using a supplied url parameter configured
|
|
by the setting: CMS_ENDPOINT_LIVE_URL_QUERYSTRING_PARAM
|
|
|
|
:param obj: Placeholder source object
|
|
:param url: Url string
|
|
:param language: The current language code or None
|
|
:returns: A url string
|
|
"""
|
|
url_param = get_cms_setting('ENDPOINT_LIVE_URL_QUERYSTRING_PARAM')
|
|
if not hasattr(obj, "get_absolute_url"):
|
|
return url
|
|
try:
|
|
live_url = obj.get_absolute_url()
|
|
except NoReverseMatch:
|
|
return url
|
|
url_fragments = url.split('?')
|
|
if len(url_fragments) > 1:
|
|
url += f'&{url_param}={live_url}'
|
|
else:
|
|
url += f'?{url_param}={live_url}'
|
|
return url
|
|
|
|
|
|
def _get_object_url(reverse_name: str, obj: models.Model, language: str = None, params: QueryDict | None = None) -> str:
|
|
content_type = ContentType.objects.get_for_model(obj)
|
|
|
|
language = getattr(obj, "language", language) # Object trumps parameter
|
|
if language is None:
|
|
language = get_language()
|
|
|
|
with force_language(language):
|
|
url = admin_reverse(reverse_name, args=[content_type.pk, obj.pk])
|
|
get_params = params.copy() if params else QueryDict(mutable=True)
|
|
if not get_language_from_path(url):
|
|
get_params["language"] = language
|
|
if get_params:
|
|
url = url + "?" + get_params.urlencode()
|
|
if get_cms_setting("ENDPOINT_LIVE_URL_QUERYSTRING_PARAM_ENABLED"):
|
|
url = add_live_url_querystring_param(obj, url, language)
|
|
return url
|
|
|
|
|
|
def get_object_edit_url(obj: models.Model, language: str | None = None, params: QueryDict | None = None) -> str:
|
|
"""
|
|
Returns the url of the edit endpoint for the given object. The object must be frontend-editable
|
|
and registered as such with cms.
|
|
|
|
If the object has a language property, the language parameter is ignored.
|
|
|
|
params is an optional QueryDict to be added to the URL.
|
|
"""
|
|
return _get_object_url("cms_placeholder_render_object_edit", obj, language, params)
|
|
|
|
|
|
def get_object_preview_url(obj: models.Model, language: str | None = None, params: QueryDict | None = None) -> str:
|
|
"""
|
|
Returns the url of the preview endpoint for the given object. The object must be frontend-editable
|
|
and registered as such with cms.
|
|
|
|
If the object has a language property, the language parameter is ignored.
|
|
|
|
params is an optional QueryDict to be added to the URL.
|
|
"""
|
|
return _get_object_url("cms_placeholder_render_object_preview", obj, language, params)
|
|
|
|
|
|
def get_object_structure_url(obj: models.Model, language: str | None = None, params: QueryDict | None = None) -> str:
|
|
"""
|
|
Returns the url of the structure endpoint for the given object. The object must be frontend-editable
|
|
and registered as such with cms.
|
|
|
|
If the object has a language property, the language parameter is ignored.
|
|
|
|
params is an optional QueryDict to be added to the URL.
|
|
|
|
"""
|
|
return _get_object_url("cms_placeholder_render_object_structure", obj, language, params)
|
|
|
|
|
|
def get_object_live_url(obj: models.Model, language: str | None = None, site: Site | None = None, params: QueryDict | None = None) -> str | None:
|
|
"""
|
|
Returns the live url of the given object. The object must be frontend-editable
|
|
and registered as such with cms.
|
|
|
|
If the object has a language property, the language parameter is ignored.
|
|
If the object - or its grouper - has no site property, the site argument is ignored.
|
|
"""
|
|
if not hasattr(obj, "get_absolute_url"):
|
|
return None
|
|
|
|
language = getattr(obj, "language", language) # Object trumps parameter
|
|
if language is None:
|
|
language = get_language()
|
|
|
|
with force_language(language):
|
|
absolute_url = obj.get_absolute_url()
|
|
if not absolute_url:
|
|
return None
|
|
url_param = get_cms_setting("ENDPOINT_LIVE_URL_QUERYSTRING_PARAM")
|
|
if params and url_param in params:
|
|
params = params.copy()
|
|
del params[url_param]
|
|
if params and "?" not in absolute_url:
|
|
absolute_url = absolute_url + "?" + params.urlencode()
|
|
|
|
obj_site = getattr(obj, 'site', None)
|
|
if obj_site is None:
|
|
try:
|
|
grouper_field = apps.get_app_config('cms').cms_extension.model_groupers[obj.__class__]
|
|
obj_site = getattr(getattr(obj, grouper_field, None), 'site', None)
|
|
except KeyError:
|
|
pass
|
|
if obj_site and obj_site != site:
|
|
# Add domain if current and target sites are defined and different
|
|
absolute_url = f"//{obj_site.domain}{absolute_url}"
|
|
|
|
return absolute_url
|
|
|
|
|
|
def get_object_for_language(obj: models.Model, language: str, latest: bool = False) -> models.Model | None:
|
|
"""
|
|
Retrieves the correct content object for the target language. The object must be frontend-editable
|
|
and registered as such with cms.
|
|
|
|
Two cases have to be distinguished:
|
|
|
|
1. **Object has a language property:** If the language of the passed object is different,
|
|
sibling objects are retrieved from the database and cached in the object passed.
|
|
2. **Object has no language property:** The placeholders of the object contain the different
|
|
language content. The object itself is returned
|
|
"""
|
|
if getattr(obj, "language", language) == language:
|
|
# Object does not have language field or language is requested language
|
|
# Return object itself
|
|
return obj
|
|
if isinstance(obj, PageContent):
|
|
return obj.page.get_admin_content(language, fallback=False) or None
|
|
# Does the object have a cache with sister objects
|
|
cached_object = getattr(obj, "_sibling_objects_for_language_cache", {})
|
|
if cached_object:
|
|
return cached_object.get(language)
|
|
|
|
extension = apps.get_app_config('cms').cms_extension
|
|
model = obj.__class__
|
|
field = extension.model_groupers.get(model)
|
|
if not field:
|
|
# Cannot infer sister object if grouper field is unknown
|
|
return None
|
|
# Grouper model not registered or does not have a get_content_obj method,
|
|
# or get_content_obj does not accept language parameter
|
|
# Now query db
|
|
grouper_filter = {field: getattr(obj, field)}
|
|
qs = model.admin_manager.latest_content() if latest and hasattr(model, "admin_manager") else model.objects
|
|
obj._sibling_objects_for_language_cache = {
|
|
result.language: result for result in qs.filter(**grouper_filter)
|
|
}
|
|
return obj._sibling_objects_for_language_cache.get(language)
|