forked from ungleich-public/dynamicweb
798 lines
31 KiB
Python
798 lines
31 KiB
Python
import contextlib
|
|
import logging
|
|
import sys
|
|
from collections import OrderedDict
|
|
from collections.abc import Generator
|
|
from functools import partial
|
|
from typing import Any
|
|
|
|
from classytags.utils import flatten_context
|
|
from django.conf import settings
|
|
from django.contrib.sites.models import Site
|
|
from django.db import models
|
|
from django.http import HttpRequest
|
|
from django.template import Context
|
|
from django.utils.functional import cached_property
|
|
from django.utils.html import format_html
|
|
from django.utils.module_loading import import_string
|
|
from django.utils.safestring import SafeText, mark_safe
|
|
from django.utils.translation import get_language, override
|
|
from django.views.debug import ExceptionReporter
|
|
|
|
from cms.cache.placeholder import get_placeholder_cache, set_placeholder_cache
|
|
from cms.exceptions import PlaceholderNotFound
|
|
from cms.models import CMSPlugin, Page, PageContent, Placeholder
|
|
from cms.plugin_pool import PluginPool
|
|
from cms.toolbar.utils import (
|
|
get_placeholder_toolbar_js,
|
|
get_plugin_toolbar_js,
|
|
get_toolbar_from_request,
|
|
)
|
|
from cms.utils import get_language_from_request
|
|
from cms.utils.compat.warnings import RemovedInDjangoCMS60Warning
|
|
from cms.utils.conf import get_cms_setting
|
|
from cms.utils.permissions import has_plugin_permission
|
|
from cms.utils.placeholder import (
|
|
get_toolbar_plugin_struct,
|
|
rescan_placeholders_for_obj,
|
|
restore_sekizai_context,
|
|
)
|
|
from cms.utils.plugins import get_plugin_restrictions
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _unpack_plugins(parent_plugin: CMSPlugin) -> Generator[CMSPlugin, None, None]:
|
|
yield parent_plugin
|
|
for plugin in parent_plugin.child_plugin_instances or []:
|
|
yield from _unpack_plugins(plugin)
|
|
|
|
|
|
class RenderedPlaceholder:
|
|
__slots__ = (
|
|
"language",
|
|
"site_id",
|
|
"cached",
|
|
"editable",
|
|
"placeholder",
|
|
"has_content",
|
|
)
|
|
|
|
def __init__(
|
|
self,
|
|
placeholder: Placeholder,
|
|
language: str,
|
|
site_id: int,
|
|
cached: bool = False,
|
|
editable: bool = False,
|
|
has_content: bool = False,
|
|
):
|
|
self.language = language
|
|
self.site_id = site_id
|
|
self.cached = cached
|
|
self.editable = editable
|
|
self.placeholder = placeholder
|
|
self.has_content = has_content
|
|
|
|
def __eq__(self, other):
|
|
# The same placeholder rendered with different
|
|
# parameters is considered the same.
|
|
# This behavior is compatible with previous djangoCMS releases.
|
|
return self.placeholder == other.placeholder
|
|
|
|
def __ne__(self, other):
|
|
return not self.__eq__(other)
|
|
|
|
def __hash__(self):
|
|
return hash(self.placeholder)
|
|
|
|
|
|
class BaseRenderer:
|
|
load_structure: bool = False
|
|
placeholder_edit_template: str = ""
|
|
|
|
def __init__(self, request: HttpRequest):
|
|
self.request = request
|
|
self._cached_plugin_classes = {}
|
|
self._placeholders_content_cache = {}
|
|
self._placeholders_by_page_cache = {}
|
|
self._rendered_placeholders = OrderedDict()
|
|
self._rendered_plugins_by_placeholder = {}
|
|
self._plugins_with_perms = None
|
|
|
|
@cached_property
|
|
def current_page(self) -> Page:
|
|
return self.request.current_page
|
|
|
|
@cached_property
|
|
def current_site(self) -> Site:
|
|
return Site.objects.get_current(self.request)
|
|
|
|
@cached_property
|
|
def toolbar(self):
|
|
return get_toolbar_from_request(self.request)
|
|
|
|
@cached_property
|
|
def templates(self):
|
|
return self.toolbar.templates
|
|
|
|
@cached_property
|
|
def plugin_pool(self) -> PluginPool:
|
|
import cms.plugin_pool
|
|
|
|
return cms.plugin_pool.plugin_pool
|
|
|
|
@cached_property
|
|
def request_language(self) -> str:
|
|
return get_language_from_request(self.request)
|
|
|
|
def get_plugins_with_perms(self) -> list[CMSPlugin]:
|
|
if self._plugins_with_perms is None:
|
|
registered_plugins = self.plugin_pool.registered_plugins
|
|
can_add_plugin = partial(has_plugin_permission, user=self.request.user, permission_type="add")
|
|
self._plugins_with_perms = [
|
|
plugin for plugin in registered_plugins if can_add_plugin(plugin_type=plugin.value)
|
|
]
|
|
|
|
return self._plugins_with_perms
|
|
|
|
def get_placeholder_plugin_menu(self, placeholder: Placeholder, page: Page | None = None):
|
|
plugin_menu = get_toolbar_plugin_struct(
|
|
plugins=self.get_plugins_with_perms(),
|
|
slot=placeholder.slot,
|
|
obj=page,
|
|
)
|
|
plugin_menu_template = self.templates.placeholder_plugin_menu_template
|
|
return plugin_menu_template.render({"plugin_menu": plugin_menu})
|
|
|
|
def get_placeholder_toolbar_js(self, placeholder, page: models.Model | None = None):
|
|
if page:
|
|
import warnings
|
|
|
|
warnings.warn(
|
|
"The 'page' argument of 'get_placeholder_toolbar_js' is deprecated and will be removed in a future "
|
|
"release. It can be safely removed from the call.",
|
|
RemovedInDjangoCMS60Warning,
|
|
stacklevel=2,
|
|
)
|
|
plugins = self.plugin_pool.get_root_plugins(placeholder) # original
|
|
plugin_types = [cls.__name__ for cls in plugins]
|
|
allowed_plugins = plugin_types + self.plugin_pool.get_system_plugins()
|
|
placeholder_toolbar_js = get_placeholder_toolbar_js(
|
|
placeholder=placeholder,
|
|
allowed_plugins=allowed_plugins,
|
|
)
|
|
return placeholder_toolbar_js
|
|
|
|
def get_plugin_toolbar_js(self, plugin: CMSPlugin, obj: models.Model):
|
|
placeholder_cache = self._rendered_plugins_by_placeholder.setdefault(plugin.placeholder_id, {})
|
|
child_classes, _ = get_plugin_restrictions(
|
|
plugin=plugin,
|
|
page=obj,
|
|
restrictions_cache=placeholder_cache, # Store non-global plugin-restriction in placeholder_cache
|
|
)
|
|
# Make the resolved restrictions available to the drag-item template (which renders the
|
|
# same instances) so it can decide whether children may be added without recomputing.
|
|
plugin.child_class_restrictions = child_classes
|
|
content = get_plugin_toolbar_js(
|
|
plugin,
|
|
children=child_classes,
|
|
)
|
|
return content
|
|
|
|
def get_plugin_class(self, plugin: CMSPlugin) -> type:
|
|
plugin_type = plugin.plugin_type
|
|
|
|
if plugin_type not in self._cached_plugin_classes:
|
|
self._cached_plugin_classes[plugin_type] = self.plugin_pool.get_plugin(plugin_type)
|
|
return self._cached_plugin_classes[plugin_type]
|
|
|
|
def get_plugins_to_render(self, placeholder: Placeholder, language: str, template: str | None):
|
|
from cms.utils.plugins import get_plugins
|
|
|
|
plugins = get_plugins(
|
|
request=self.request,
|
|
placeholder=placeholder,
|
|
template=template,
|
|
lang=language,
|
|
)
|
|
return plugins
|
|
|
|
def get_rendered_plugins_cache(self, placeholder: Placeholder):
|
|
blank: dict[str, Any] = {
|
|
"plugins": [],
|
|
"plugin_parents": {},
|
|
"plugin_children": {},
|
|
}
|
|
return self._rendered_plugins_by_placeholder.get(placeholder.pk, blank)
|
|
|
|
def get_rendered_placeholders(self) -> list[Placeholder]:
|
|
rendered = list(self._rendered_placeholders.values())
|
|
return [r.placeholder for r in rendered]
|
|
|
|
def get_rendered_editable_placeholders(self) -> list[Placeholder]:
|
|
rendered = list(self._rendered_placeholders.values())
|
|
return [r.placeholder for r in rendered if r.editable]
|
|
|
|
|
|
class ContentRenderer(BaseRenderer):
|
|
plugin_edit_template = (
|
|
'<template class="cms-plugin '
|
|
'cms-plugin-start cms-plugin-{pk}{disabled}" data-cms-placeholder="{placeholder}"></template>{content}'
|
|
'<template class="cms-plugin cms-plugin-end cms-plugin-{pk}"></template>'
|
|
)
|
|
placeholder_edit_template = (
|
|
'{content} <div class="cms-placeholder cms-placeholder-{placeholder_id}"></div>{plugin_js}{placeholder_js}'
|
|
)
|
|
|
|
def __init__(self, request: HttpRequest):
|
|
super().__init__(request)
|
|
self._placeholders_are_editable = bool(self.toolbar.edit_mode_active)
|
|
|
|
def placeholder_cache_is_enabled(self):
|
|
if not get_cms_setting("PLACEHOLDER_CACHE"):
|
|
return False
|
|
if self.request.user.is_staff:
|
|
return False
|
|
return not self._placeholders_are_editable
|
|
|
|
def render_placeholder(
|
|
self,
|
|
placeholder: Placeholder,
|
|
context: Context,
|
|
language: str | None = None,
|
|
page: Page | None = None,
|
|
editable: bool = False,
|
|
use_cache: bool = False,
|
|
nodelist: Any | None = None,
|
|
width: int | None = None,
|
|
):
|
|
from sekizai.helpers import Watcher
|
|
|
|
language = language or self.request_language
|
|
editable = editable and self._placeholders_are_editable
|
|
|
|
if use_cache and not editable and placeholder.cache_placeholder:
|
|
use_cache = self.placeholder_cache_is_enabled()
|
|
else:
|
|
use_cache = False
|
|
|
|
if use_cache:
|
|
cached_value = self._get_cached_placeholder_content(
|
|
placeholder=placeholder,
|
|
language=language,
|
|
)
|
|
else:
|
|
cached_value = None
|
|
|
|
if cached_value is not None:
|
|
# User has opted to use the cache
|
|
# and there is something in the cache
|
|
restore_sekizai_context(context, cached_value["sekizai"])
|
|
return mark_safe(cached_value["content"])
|
|
|
|
context.push()
|
|
|
|
width = width or placeholder.default_width
|
|
template = page.get_template() if page and hasattr(page, "get_template") else None
|
|
|
|
if width:
|
|
context["width"] = width
|
|
|
|
# Add extra context as defined in settings, but do not overwrite existing context variables,
|
|
# since settings are general and database/template are specific
|
|
# TODO this should actually happen as a plugin context processor, but these currently overwrite
|
|
# existing context -- maybe change this order?
|
|
for key, value in placeholder.get_extra_context(template).items():
|
|
if key not in context:
|
|
context[key] = value
|
|
|
|
if use_cache:
|
|
watcher = Watcher(context)
|
|
|
|
plugin_content = self.render_plugins(
|
|
placeholder,
|
|
language=language,
|
|
context=context,
|
|
editable=editable,
|
|
template=template,
|
|
)
|
|
try:
|
|
placeholder_content = "".join(plugin_content)
|
|
except Exception as e:
|
|
context["exc_info"] = sys.exc_info()
|
|
placeholder_content = self.render_exception("rendering placeholder", context, placeholder, editable)
|
|
if not get_cms_setting("CATCH_PLUGIN_500_EXCEPTION"):
|
|
if not self.toolbar.edit_mode_active and not self.toolbar.preview_mode_active:
|
|
raise e from None
|
|
|
|
if not placeholder_content and nodelist:
|
|
# should be nodelist from a template
|
|
placeholder_content = nodelist.render(context)
|
|
|
|
if use_cache:
|
|
content = {
|
|
"content": placeholder_content,
|
|
"sekizai": watcher.get_changes(),
|
|
}
|
|
set_placeholder_cache(
|
|
placeholder,
|
|
lang=language,
|
|
site_id=self.current_site.pk,
|
|
content=content,
|
|
request=self.request,
|
|
)
|
|
|
|
rendered_placeholder = RenderedPlaceholder(
|
|
placeholder=placeholder,
|
|
language=language,
|
|
site_id=self.current_site.pk,
|
|
cached=use_cache,
|
|
editable=editable,
|
|
has_content=bool(placeholder_content),
|
|
)
|
|
|
|
if placeholder.pk not in self._rendered_placeholders:
|
|
# First time this placeholder is rendered
|
|
if not self.toolbar._cache_disabled:
|
|
# The toolbar middleware needs to know if the response
|
|
# is to be cached.
|
|
# Set the _cache_disabled flag to the value of cache_placeholder
|
|
# only if the flag is False (meaning cache is enabled).
|
|
self.toolbar._cache_disabled = not use_cache
|
|
self._rendered_placeholders[placeholder.pk] = rendered_placeholder
|
|
|
|
if editable:
|
|
request = context.get("request", None)
|
|
with override(request.toolbar.toolbar_language) if request else contextlib.nullcontext():
|
|
data = self.get_editable_placeholder_context(placeholder, page=page)
|
|
data["content"] = placeholder_content
|
|
placeholder_content = self.placeholder_edit_template.format(**data)
|
|
|
|
context.pop()
|
|
return mark_safe(placeholder_content)
|
|
|
|
def get_editable_placeholder_context(self, placeholder: Placeholder, page: Page | None = None) -> dict:
|
|
obj = page or placeholder.source
|
|
placeholder_cache = self.get_rendered_plugins_cache(placeholder)
|
|
placeholder_toolbar_js = self.get_placeholder_toolbar_js(placeholder)
|
|
plugin_toolbar_js_bits = (
|
|
self.get_plugin_toolbar_js(plugin, obj=obj) for plugin in placeholder_cache["plugins"]
|
|
)
|
|
context = {
|
|
"plugin_js": "".join(plugin_toolbar_js_bits),
|
|
"placeholder_js": placeholder_toolbar_js,
|
|
"placeholder_id": placeholder.pk,
|
|
}
|
|
return context
|
|
|
|
def render_obj_placeholder(
|
|
self,
|
|
slot: str,
|
|
context: Context,
|
|
inherit: bool,
|
|
nodelist=None,
|
|
editable: bool = True,
|
|
):
|
|
# Get current object from toolbar
|
|
current_obj = self.toolbar.get_object()
|
|
# Check if page, if so delegate to render_page_placeholder
|
|
if self.current_page and (isinstance(current_obj, (PageContent, type(None)))):
|
|
return self.render_page_placeholder(
|
|
slot,
|
|
context,
|
|
inherit,
|
|
nodelist=nodelist,
|
|
editable=editable,
|
|
)
|
|
# Not page, therefore we will use toolbar object as
|
|
# the current object and render the placeholder
|
|
if current_obj is None:
|
|
raise PlaceholderNotFound(f"No object found for placeholder '{slot}'")
|
|
placeholder = rescan_placeholders_for_obj(current_obj).get(slot)
|
|
content = self.render_placeholder(
|
|
placeholder,
|
|
context=context,
|
|
page=current_obj,
|
|
editable=editable,
|
|
use_cache=True,
|
|
nodelist=None,
|
|
)
|
|
return content
|
|
|
|
def render_page_placeholder(
|
|
self,
|
|
slot: str,
|
|
context: Context,
|
|
inherit: bool,
|
|
page: Page | None = None,
|
|
nodelist=None,
|
|
editable: bool = True,
|
|
):
|
|
if not self.current_page:
|
|
# This method should only be used when rendering a cms page.
|
|
return ""
|
|
|
|
current_page = page or self.current_page
|
|
language = get_language()
|
|
placeholder_cache = self._placeholders_by_page_cache
|
|
|
|
if current_page.pk not in placeholder_cache:
|
|
# Instead of loading plugins for this one placeholder
|
|
# try and load them for all placeholders on the page.
|
|
self._preload_placeholders_for_page(current_page, language=language)
|
|
|
|
try:
|
|
placeholder = placeholder_cache[current_page.pk][slot]
|
|
except KeyError:
|
|
content = ""
|
|
placeholder = None
|
|
else:
|
|
content = self.render_placeholder(
|
|
placeholder,
|
|
context=context,
|
|
language=language,
|
|
page=placeholder.source,
|
|
editable=editable,
|
|
use_cache=True,
|
|
nodelist=None,
|
|
)
|
|
should_inherit = (
|
|
inherit
|
|
and not content
|
|
and current_page.parent
|
|
# The placeholder cache is primed when the first placeholder
|
|
# is loaded. If the current page's parent is not in there,
|
|
# it means its cache was never primed as it wasn't necessary.
|
|
and current_page.parent.pk in placeholder_cache
|
|
# don't display inherited plugins in edit mode, so that the user doesn't
|
|
# mistakenly edit/delete them. This is a fix for issue #1303. See the discussion
|
|
# there for possible enhancements
|
|
and not self.toolbar.edit_mode_active
|
|
)
|
|
|
|
if should_inherit:
|
|
# nodelist is set to None to avoid rendering the nodes inside
|
|
# a {% placeholder or %} block tag.
|
|
content = self.render_page_placeholder(
|
|
slot,
|
|
context,
|
|
inherit=True,
|
|
page=current_page.parent,
|
|
nodelist=None,
|
|
editable=False,
|
|
)
|
|
|
|
if placeholder and (editable and self._placeholders_are_editable):
|
|
# In edit mode, the contents of the placeholder are mixed with our
|
|
# internal toolbar markup, so the content variable will always be True.
|
|
# Use the rendered placeholder has_content flag instead.
|
|
has_content = self._rendered_placeholders[placeholder.pk].has_content
|
|
else:
|
|
# User is not in edit mode or the placeholder doesn't exist.
|
|
# Either way, we can trust the content variable.
|
|
has_content = bool(content)
|
|
|
|
if not has_content and nodelist:
|
|
return content + nodelist.render(context)
|
|
return content
|
|
|
|
def render_plugin(
|
|
self,
|
|
instance: CMSPlugin,
|
|
context: Context,
|
|
placeholder: Placeholder | None = None,
|
|
editable: bool = False,
|
|
):
|
|
context["_last_plugin"] = instance # Used if an exception is rendered
|
|
if placeholder is None:
|
|
placeholder = instance.placeholder
|
|
|
|
instance, plugin = instance.get_plugin_instance()
|
|
|
|
if not instance or not plugin.render_plugin:
|
|
return ""
|
|
|
|
# we'd better pass a flat dict to template.render
|
|
# as plugin.render can return pretty much any kind of context / dictionary
|
|
# we'd better flatten it and force to a Context object
|
|
# flattening the context means that template must be an engine-specific template object
|
|
# which is guaranteed by get_cached_template if the template returned by
|
|
# plugin._get_render_template is either a string or an engine-specific template object
|
|
context = PluginContext(context, instance, placeholder)
|
|
context = plugin.render(context, instance, placeholder.slot)
|
|
context = flatten_context(context)
|
|
template_name = plugin._get_render_template(context, instance, placeholder)
|
|
template = self.templates.get_cached_template(template_name)
|
|
content = template.render(context)
|
|
|
|
for path in get_cms_setting("PLUGIN_PROCESSORS"):
|
|
processor = import_string(path)
|
|
content = processor(instance, placeholder, content, context)
|
|
|
|
if editable:
|
|
is_slot = getattr(plugin, "is_slot", False)
|
|
content = self.plugin_edit_template.format(
|
|
pk=instance.pk,
|
|
placeholder=instance.placeholder_id,
|
|
content=content,
|
|
position=instance.position,
|
|
disabled=' cms-slot' if is_slot else '',
|
|
)
|
|
placeholder_cache = self._rendered_plugins_by_placeholder.setdefault(placeholder.pk, {})
|
|
placeholder_cache.setdefault("plugins", []).append(instance)
|
|
return mark_safe(content)
|
|
|
|
def render_exception(self, action: str, context: Context, placeholder: Placeholder, editable: bool) -> str:
|
|
exc, value, traceback = context["exc_info"]
|
|
placeholder_source_obj = placeholder.source.__class__._meta.verbose_name
|
|
message = (
|
|
f'{exc.__name__}: {value} when {action} "{placeholder}" on '
|
|
f'{placeholder_source_obj} object "{placeholder.source}"'
|
|
)
|
|
if "_last_plugin" in context:
|
|
instance = context["_last_plugin"]
|
|
try:
|
|
description = f"{instance._meta.verbose_name} {instance.get_short_description()}"
|
|
except Exception:
|
|
description = f"{instance._meta.verbose_name}"
|
|
message += f', plugin #{instance.pk} "{description}"'
|
|
logger.error(message, exc_info=(exc, value, traceback))
|
|
|
|
if editable:
|
|
if settings.DEBUG:
|
|
reporter = ExceptionReporter(context["request"], exc, value, traceback)
|
|
html = reporter.get_traceback_html()
|
|
else:
|
|
html = ""
|
|
heading = format_html('<h2 class="cms-rendering-exception-title">{}</h2>', message)
|
|
if "_last_plugin" in context:
|
|
# Make error message editable by double-click to open the editor for the plugin causing the exception
|
|
is_slot = getattr(instance, "is_slot", False)
|
|
heading = self.plugin_edit_template.format(
|
|
pk=instance.pk,
|
|
placeholder=instance.placeholder_id,
|
|
content=heading,
|
|
position=instance.position,
|
|
disabled=' cms-slot' if is_slot else '',
|
|
)
|
|
placeholder_cache = self._rendered_plugins_by_placeholder.setdefault(placeholder.pk, {})
|
|
placeholder_cache.setdefault("plugins", []).append(instance)
|
|
|
|
return f'<div class="cms-rendering-exception">{heading}{html}</div>'
|
|
return ""
|
|
|
|
def render_plugins(
|
|
self,
|
|
placeholder: Placeholder,
|
|
language: str,
|
|
context,
|
|
editable: bool = False,
|
|
template: str | None = None,
|
|
) -> Generator[SafeText | str, None, None]:
|
|
plugins = self.get_plugins_to_render(
|
|
placeholder=placeholder,
|
|
template=template,
|
|
language=language,
|
|
)
|
|
|
|
for plugin in plugins:
|
|
plugin._placeholder_cache = placeholder
|
|
yield self.render_plugin(plugin, context, placeholder, editable)
|
|
|
|
def _get_cached_placeholder_content(self, placeholder, language):
|
|
"""
|
|
Returns a dictionary mapping placeholder content and sekizai data.
|
|
Returns None if no cache is present.
|
|
"""
|
|
# Placeholders can be rendered multiple times under different sites
|
|
# it's important to have a per-site "cache".
|
|
site_id = self.current_site.pk
|
|
site_cache = self._placeholders_content_cache.setdefault(site_id, {})
|
|
# Placeholders can be rendered multiple times under different languages
|
|
# it's important to have a per-language "cache".
|
|
language_cache = site_cache.setdefault(language, {})
|
|
|
|
if placeholder.pk not in language_cache:
|
|
cached_value = get_placeholder_cache(
|
|
placeholder,
|
|
lang=language,
|
|
site_id=site_id,
|
|
request=self.request,
|
|
)
|
|
|
|
if cached_value is not None:
|
|
# None means nothing in the cache
|
|
# Anything else is a valid value
|
|
language_cache[placeholder.pk] = cached_value
|
|
return language_cache.get(placeholder.pk)
|
|
|
|
def _get_content_object(self, page, slots=None):
|
|
toolbar_obj = self.toolbar.get_object()
|
|
if isinstance(toolbar_obj, PageContent) and toolbar_obj.page == page:
|
|
# Current object belongs to the page itself
|
|
return Placeholder.objects.get_for_obj(toolbar_obj)
|
|
elif slots:
|
|
# If looking for inherited placeholders, slots is specified
|
|
if self.toolbar.preview_mode_active or self.toolbar.edit_mode_active:
|
|
page_content = (
|
|
page.pagecontent_set(manager="admin_manager")
|
|
.current_content(language=self.request_language)
|
|
.first()
|
|
)
|
|
else:
|
|
page_content = page.pagecontent_set.filter(language=self.request_language).first()
|
|
return Placeholder.objects.get_for_obj(page_content) if page_content else Placeholder.objects.none()
|
|
elif page_content := page.get_content_obj(self.request_language, fallback=False):
|
|
PageContent.page.field.set_cached_value(page_content, page)
|
|
# Creates any placeholders missing on the page
|
|
return page_content.rescan_placeholders().values()
|
|
else:
|
|
return Placeholder.objects.none()
|
|
|
|
def _preload_placeholders_for_page(self, page, language=None, slots=None, inherit=False):
|
|
"""
|
|
Populates the internal plugin cache of each placeholder
|
|
in the given page if the placeholder has not been
|
|
previously cached.
|
|
"""
|
|
from cms.utils.plugins import assign_plugins
|
|
|
|
placeholders = self._get_content_object(page, slots=slots)
|
|
language = language or self.request_language
|
|
|
|
if inherit:
|
|
# When the inherit flag is True,
|
|
# assume all placeholders found are inherited and thus prefetch them.
|
|
slots_w_inheritance = [pl.slot for pl in placeholders]
|
|
elif not self.toolbar.edit_mode_active:
|
|
# Scan through the page template to find all placeholders
|
|
# that have inheritance turned on.
|
|
slots_w_inheritance = [pl.slot for pl in page.get_declared_placeholders() if pl.inherit]
|
|
else:
|
|
# Inheritance is turned off on edit-mode
|
|
slots_w_inheritance = []
|
|
|
|
if self.placeholder_cache_is_enabled():
|
|
_cached_content = self._get_cached_placeholder_content
|
|
# Only prefetch plugins if the placeholder
|
|
# has not been cached.
|
|
placeholders_to_fetch = [
|
|
placeholder
|
|
for placeholder in placeholders
|
|
if _cached_content(placeholder, language) is None
|
|
]
|
|
else:
|
|
# cache is disabled, prefetch plugins for all
|
|
# placeholders in the page.
|
|
placeholders_to_fetch = placeholders
|
|
|
|
if placeholders_to_fetch:
|
|
assign_plugins(
|
|
request=self.request,
|
|
placeholders=placeholders_to_fetch,
|
|
template=page.get_template(),
|
|
lang=language,
|
|
)
|
|
|
|
# Inherit only placeholders that have no plugins
|
|
# or are not cached.
|
|
placeholders_to_inherit = [
|
|
pl.slot
|
|
for pl in placeholders
|
|
if not getattr(pl, "_plugins_cache", None) and pl.slot in slots_w_inheritance
|
|
]
|
|
|
|
if page.parent and placeholders_to_inherit:
|
|
self._preload_placeholders_for_page(
|
|
page=page.parent,
|
|
language=language,
|
|
slots=placeholders_to_inherit,
|
|
inherit=True,
|
|
)
|
|
|
|
# Internal cache mapping placeholder slots
|
|
# to placeholder instances.
|
|
page_placeholder_cache = {}
|
|
|
|
for placeholder in placeholders:
|
|
# Save a query when the placeholder toolbar is rendered.
|
|
placeholder.page = page
|
|
page_placeholder_cache[placeholder.slot] = placeholder
|
|
|
|
self._placeholders_by_page_cache[page.pk] = page_placeholder_cache
|
|
|
|
|
|
class StructureRenderer(BaseRenderer):
|
|
load_structure = True
|
|
placeholder_edit_template = """
|
|
<template data-cms id="cms-plugin-child-classes-{placeholder_id}" type="text/cms-template">
|
|
{plugin_menu_js}
|
|
</template>
|
|
{plugin_js}{placeholder_js}
|
|
"""
|
|
|
|
def get_plugins_to_render(self, *args, **kwargs):
|
|
plugins = super().get_plugins_to_render(*args, **kwargs)
|
|
|
|
for plugin in plugins:
|
|
yield from _unpack_plugins(plugin)
|
|
|
|
def render_placeholder(self, placeholder, language, obj=None):
|
|
rendered_plugins = self.render_plugins(placeholder, language=language, obj=obj)
|
|
plugin_js_output = "".join(rendered_plugins)
|
|
|
|
placeholder_toolbar_js = self.get_placeholder_toolbar_js(placeholder)
|
|
rendered_placeholder = RenderedPlaceholder(
|
|
placeholder=placeholder,
|
|
language=language,
|
|
site_id=self.current_site.pk,
|
|
cached=False,
|
|
editable=True,
|
|
)
|
|
|
|
if placeholder.pk not in self._rendered_placeholders:
|
|
self._rendered_placeholders[placeholder.pk] = rendered_placeholder
|
|
|
|
placeholder_structure_js = self.placeholder_edit_template.format(
|
|
placeholder_id=placeholder.pk,
|
|
plugin_js=plugin_js_output,
|
|
plugin_menu_js=self.get_placeholder_plugin_menu(placeholder, page=obj),
|
|
placeholder_js=placeholder_toolbar_js,
|
|
)
|
|
return mark_safe(placeholder_structure_js)
|
|
|
|
def render_page_placeholder(self, page, placeholder, language=None):
|
|
return self.render_placeholder(placeholder, language=language, obj=page)
|
|
|
|
def render_plugin(self, instance, obj=None):
|
|
placeholder_cache = self._rendered_plugins_by_placeholder.setdefault(instance.placeholder_id, {})
|
|
placeholder_cache.setdefault("plugins", []).append(instance)
|
|
return self.get_plugin_toolbar_js(instance, obj=obj)
|
|
|
|
def render_plugins(self, placeholder, language, obj=None):
|
|
template = obj.get_template() if obj and hasattr(obj, "get_template") else None
|
|
plugins = self.get_plugins_to_render(placeholder, language, template)
|
|
|
|
for plugin in plugins:
|
|
plugin._placeholder_cache = placeholder
|
|
yield self.render_plugin(plugin, obj=obj)
|
|
|
|
|
|
class LegacyRenderer(ContentRenderer):
|
|
load_structure = True
|
|
placeholder_edit_template = """
|
|
{content}
|
|
<div class="cms-placeholder cms-placeholder-{placeholder_id}"></div>
|
|
<template data-cms id="cms-plugin-child-classes-{placeholder_id}" type="text/cms-template">
|
|
{plugin_menu_js}
|
|
</template>
|
|
{plugin_js}{placeholder_js}
|
|
"""
|
|
|
|
def get_editable_placeholder_context(self, placeholder, page=None):
|
|
context = super().get_editable_placeholder_context(placeholder, page)
|
|
context["plugin_menu_js"] = self.get_placeholder_plugin_menu(placeholder, page=page)
|
|
return context
|
|
|
|
|
|
class PluginContext(Context):
|
|
"""
|
|
This subclass of template.Context automatically populates itself using
|
|
the processors defined in CMS_PLUGIN_CONTEXT_PROCESSORS.
|
|
Additional processors can be specified as a list of callables
|
|
using the "processors" keyword argument.
|
|
"""
|
|
|
|
def __init__(self, dict_, instance, placeholder, processors=None, current_app=None):
|
|
dict_ = flatten_context(dict_)
|
|
super().__init__(dict_)
|
|
|
|
if not processors:
|
|
processors = []
|
|
|
|
for path in get_cms_setting("PLUGIN_CONTEXT_PROCESSORS"):
|
|
processor = import_string(path)
|
|
self.update(processor(instance, placeholder, self))
|
|
for processor in processors:
|
|
self.update(processor(instance, placeholder, self))
|