Source code for shuup.xtheme.rendering

# -*- coding: utf-8 -*-
# This file is part of Shuup.
#
# Copyright (c) 2012-2021, Shuup Commerce Inc. All rights reserved.
#
# This source code is licensed under the OSL-3.0 license found in the
# LICENSE file in the root directory of this source tree.
from __future__ import unicode_literals

from django.utils.text import slugify
from django.utils.translation import get_language, ugettext_lazy as _
from markupsafe import Markup

from shuup.core.fields.tagged_json import TaggedJSONEncoder
from shuup.utils.django_compat import force_text
from shuup.xtheme._theme import get_current_theme
from shuup.xtheme.editing import is_edit_mode, may_inject
from shuup.xtheme.layout.utils import get_layout_data_key
from shuup.xtheme.utils import get_html_attrs
from shuup.xtheme.view_config import ViewConfig


[docs]def get_view_config(context, global_type=False): """ Get a view configuration object for a Jinja2 rendering context. :param context: Rendering context :type context: jinja2.runtime.Context :param global_type: Boolean indicating whether this is a global type :type global_type: bool|False :return: View config :rtype: shuup.xtheme.view_config.ViewConfig """ # This uses the Jinja context's technically-immutable vars dict # to cache the view configuration. This is fine in our case, I'd say. request = context.get("request") config_key = "_xtheme_global_view_config" if global_type else "_xtheme_view_config" config = context.vars.get(config_key) if config is None: view_object = context.get("view") if view_object: view_class = view_object.__class__ view_name = view_class.__name__ else: view_name = "UnknownView" config = ViewConfig( theme=getattr(request, "theme", None) or get_current_theme(request.shop), shop=request.shop, view_name=view_name, draft=is_edit_mode(request), global_type=global_type, ) context.vars[config_key] = config return config
[docs]def render_placeholder( context, placeholder_name, default_layout=None, template_name=None, global_type=False ): # doccov: noargs """ Render a placeholder in a given context. See `PlaceholderRenderer` for argument docs. :return: Markup :rtype: Markup """ renderer = PlaceholderRenderer( context, placeholder_name, default_layout=default_layout, template_name=template_name, global_type=global_type, ) return renderer.render()
[docs]class PlaceholderRenderer(object): """ Main class for materializing a placeholder's contents during template render time. """ # TODO: Maybe make this pluggable per-theme? def __init__(self, context, placeholder_name, default_layout=None, template_name=None, global_type=False): """ :param context: Rendering context :type context: jinja2.runtime.Context :param placeholder_name: Placeholder name :type placeholder_name: str :param default_layout: Layout or serialized layout (from template configuration) :type default_layout: Layout|dict|None :param template_name: The actual template this node was in. Used to figure out whether the placeholder lives in an `extends` parent, or in a child. :type template_name: str|None :param global_type: Boolean indicating whether this is a global placeholder :type global_type: bool|False """ self.context = context self.view_config = get_view_config(context, global_type=global_type) self.placeholder_name = placeholder_name self.template_name = "_xtheme_global_template_name" if global_type else template_name self.default_layout = default_layout # Fetch all layouts for this placeholder context combination self.layouts = self.view_config.get_placeholder_layouts(context, placeholder_name, self.default_layout) self.global_type = global_type # For non-global placeholders, editing is only available for placeholders in the "base" template, i.e. # one that is not an `extend` parent. Declaring placeholders in `include`d templates is fine, # but their configuration will not be shared among different uses of the same include. if global_type: self.edit = is_edit_mode(context["request"]) else: is_base = self.template_name == self.context.name self.edit = is_base and is_edit_mode(context["request"])
[docs] def render(self): """ Get this placeholder's rendered contents. :return: Rendered markup. :rtype: markupsafe.Markup """ if not may_inject(self.context): return "" full_content = "" for layout in self.layouts: wrapper_start = "<div%s>" % get_html_attrs(self._get_wrapper_attrs(layout)) buffer = [] write = buffer.append self._render_layout(write, layout) content = "".join(buffer) layout_content = "%(wrapper_start)s%(content)s%(wrapper_end)s" % { "wrapper_start": wrapper_start, "content": content, "wrapper_end": "</div>", } full_content += layout_content return Markup('<div class="placeholder-edit-wrap">%s</div>' % full_content)
def _get_wrapper_attrs(self, layout): layout_data_key = get_layout_data_key(self.placeholder_name, layout, self.context) attrs = { "class": ["xt-ph", "xt-ph-edit" if self.edit else None, "xt-global-ph" if self.global_type else None], "id": "xt-ph-%s" % layout_data_key, } if self.edit: # Pass layout editor to editor so we can fetch # correct layout for editing. attrs["data-xt-layout-identifier"] = layout.identifier # We need to pass layout data key here since this is the last # place whe have the context available before editor. attrs["data-xt-layout-data-key"] = layout_data_key attrs["data-xt-placeholder-name"] = self.placeholder_name attrs["data-xt-global-type"] = "global" if self.global_type else None attrs["title"] = _("Click to edit placeholder: %s") % self.placeholder_name.title() return attrs def _render_layout(self, write, layout): if self.edit: help_text = layout.get_help_text(self.context) if self.global_type: glopal_help_text = _( "This placeholder is global and content of this placeholder is shown on all pages." ) help_text += " " + force_text(glopal_help_text) ph_name = self.placeholder_name.replace("_", " ").title() tmpl = '<p class="placeholder-help-text">%s<span class="layout-identifier">%s</span></p>' write(tmpl % (help_text, ph_name)) if self.edit and self.default_layout: self._render_default_layout_script_tag(write) for y, row in enumerate(layout): self._render_row(write, layout, y, row) def _render_row(self, write, layout, y, row): """ Render a layout row into HTML. :param write: Writer function :type write: callable :param y: Row Y coordinate :type y: int :param row: Row object :type row: shuup.xtheme.view_config.LayoutRow """ row_attrs = {"class": [layout.row_class, "xt-ph-row"]} if self.edit: row_attrs["data-xt-row"] = str(y) write("<div%s>" % get_html_attrs(row_attrs)) language = get_language() saved_view_config = self.view_config.saved_view_config for x, cell in enumerate(row): cache_key_prefix = slugify( "{x}_{y}_{pk}_{status}_{modified_on}_{lang}_{placeholder}_{data_key}".format( x=x, y=y, pk=(saved_view_config.pk if saved_view_config else ""), status=(saved_view_config.status if saved_view_config else ""), modified_on=( saved_view_config.modified_on.isoformat() if saved_view_config and saved_view_config.modified_on else "" ), lang=language, placeholder=self.placeholder_name, data_key=get_layout_data_key(self.placeholder_name, layout, self.context), ) ) self._render_cell(write, layout, x, cell, cache_key_prefix) write("</div>\n") def _render_cell(self, write, layout, x, cell, cache_key_prefix): """ Render a layout cell into HTML. :param write: Writer function :type write: callable :param x: Cell X coordinate :type x: int :param cell: Cell :type cell: shuup.xtheme.view_config.LayoutCell """ classes = ["xt-ph-cell"] for breakpoint, width in cell.sizes.items(): if width is None or width == 0: continue if width < 0: classes.append(layout.hide_cell_class_template % {"breakpoint": breakpoint, "width": width}) else: classes.append(layout.cell_class_template % {"breakpoint": breakpoint, "width": width}) classes.append(cell.align) if cell.extra_classes: classes.append(cell.extra_classes) cell_attrs = {"class": classes} if self.edit: cell_attrs.update({"data-xt-cell": str(x)}) write("<div%s>" % get_html_attrs(cell_attrs)) content = cell.render(self.context, cache_key_prefix=cache_key_prefix) if content is not None: # pragma: no branch write(force_text(content)) write("</div>") def _render_default_layout_script_tag(self, write): # This script tag is read by editor.js write("<script%s>" % get_html_attrs({"class": "xt-ph-default-layout", "type": "text/plain"})) layout = self.default_layout if hasattr(layout, "serialize"): layout = layout.serialize() # TODO: Might have to do something about .. # TODO: .. http://www.w3.org/TR/html5/scripting-1.html#restrictions-for-contents-of-script-elements write(TaggedJSONEncoder(separators=",:").encode(layout)) write("</script>")