Source code for shuup.xtheme.resources

# -*- 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

import os
import re
import six
from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from jinja2.utils import contextfunction
from logging import getLogger

from shuup.core import cache
from shuup.core.fields import TaggedJSONEncoder
from shuup.core.shop_provider import get_shop
from shuup.utils.django_compat import force_text
from shuup.xtheme.utils import get_html_attrs

LOGGER = getLogger(__name__)

LOCATION_INFO = {
    "head_end": {"name": _("End of head"), "regex": re.compile(r"</head>", re.I), "placement": "pre"},
    "head_start": {"name": _("Start of head"), "regex": re.compile(r"<head[^>]*>", re.I), "placement": "post"},
    "body_end": {"name": _("End of body"), "regex": re.compile(r"</body>", re.I), "placement": "pre"},
    "body_start": {"name": _("Start of body"), "regex": re.compile(r"<body[^>]*>", re.I), "placement": "post"},
    "content_start": {"name": _("Content start"), "regex": re.compile(r"^.*", re.I), "placement": "pre"},
    "content_end": {"name": _("Content end"), "regex": re.compile(r".*$", re.I), "placement": "post"},
}

KNOWN_LOCATIONS = set(LOCATION_INFO.keys())

RESOURCE_CONTAINER_VAR_NAME = "_xtheme_resources"
GLOBAL_SNIPPETS_CACHE_KEY = "global_snippets_{shop_id}"


[docs]class InlineScriptResource(six.text_type): """ An inline script resource (a subclass of string). The contents are rendered inside a ``<script>`` tag. """ @classmethod
[docs] def from_vars(cls, var_name, *args, **kwargs): """ Create an InlineScriptResource assigning an object of variables into a name in the `window` scope. Aside from ``var_name`` the signature of this function is similar to that of ``dict``. Useful for configuration options, etc. :param var_name: The variable to add into global scope. :type var_name: str :return: An `InlineScriptResource` object. :rtype: InlineScriptResource """ ns = dict(*args, **kwargs) return cls("window.%s = %s;" % (var_name, TaggedJSONEncoder().encode(ns)))
[docs]class JinjaMarkupResource(object): """ A Jinja markup resource. """ def __init__(self, template, context): self.template = template self.context = context def __str__(self): return self.template
[docs] def render(self): template = force_text(self.template) if not template: return template from django.template import engines for engine_name in engines: engine = engines[engine_name] try: return engine.env.from_string(template).render(self.context) except Exception: LOGGER.exception("Error! Failed to render Jinja string in Snippet plugin.") return force_text(_("(Error while rendering.)"))
def __eq__(self, other): return self.render() == other
[docs]class InlineMarkupResource(six.text_type): """ An inline markup resource (a subclass of string). The contents are rendered as-is. """
[docs]class InlineStyleResource(six.text_type): """ An inline style resource (a subclass of string). """
[docs]class ResourceContainer(object): """ ResourceContainers deal with storing and rendering injected resources. A ResourceContainer is injected into rendering contexts by `~shuup.xtheme.engine.XthemeTemplate` (akin to how `django-jinja`'s Template injects `request` and `csrf_token`). """ def __init__(self): self.resources = {}
[docs] def add_resource(self, location, resource): """ Add a resource into the given location. Duplicate resources are ignored (and false is returned). Resource injection order is retained. :param location: The name of the location. See KNOWN_LOCATIONS. :type location: str :param resource: The actual resource. Either an URL string or one of the inline resource classes. :type resource: str|InlineMarkupResource|InlineScriptResource :return: Success flag. :rtype: bool """ if not resource: return False if location not in KNOWN_LOCATIONS: raise ValueError("Error! `%r` is not a known xtheme resource location." % location) lst = self.resources.setdefault(location, []) if resource not in lst: lst.append(resource) return True return False
[docs] def render_resources(self, location, clean=True): """ Render the resources for the given location, then (by default) clean that list of resources. :param location: The name of the location. See `KNOWN_LOCATIONS`. :type location: str :param clean: Whether or not to clean up the list of resources. :type clean: bool :return: String of HTML. """ lst = self.resources.get(location) if not lst: return "" content = "".join(self._render_resource(resource) for resource in lst) if clean: # pragma: no branch self.resources.pop(location, None) return mark_safe(content)
def _render_resource(self, resource): """ Render a single resource. :param resource: The resource. :type resource: str|InlineMarkupResource|InlineScriptResource :return: String of HTML. """ if not resource: # pragma: no cover return "" if isinstance(resource, JinjaMarkupResource): return resource.render() if isinstance(resource, InlineMarkupResource): return force_text(resource) if isinstance(resource, InlineStyleResource): return '<style type="text/css">%s</style>' % resource if isinstance(resource, InlineScriptResource): return "<script>%s</script>" % resource resource = force_text(resource) from six.moves.urllib.parse import urlparse file_path = urlparse(resource) file_name = os.path.basename(file_path.path) if file_name.endswith(".js"): return "<script%s></script>" % get_html_attrs({"src": resource}) if file_name.endswith(".css"): return "<link%s>" % get_html_attrs({"href": resource, "rel": "stylesheet"}) return "<!-- (unknown resource type: %s) -->" % escape(resource)
@contextfunction
[docs]def inject_resources(context, content, clean=True): """ Inject all the resources in the context's `ResourceContainer` into appropriate places in the content given. :param context: Rendering context. :type context: jinja2.runtime.Context :param content: HTML content. :type content: str :param clean: Clean the resource container as we go? :type clean: bool :return: Possibly modified HTML content. :rtype: str """ rc = get_resource_container(context) if not rc: # No resource container? Well, whatever. return content for location_name, location in LOCATION_INFO.items(): if not rc.resources.get(location_name): continue injection = rc.render_resources(location_name, clean=clean) if not injection: continue match = location["regex"].search(content) if not match: continue start = match.start() end = match.end() placement = location["placement"] if placement == "pre": content = content[:start] + injection + content[start:] elif placement == "post": content = content[:end] + injection + content[end:] else: # pragma: no cover raise ValueError("Error! Unknown placement `%s`." % placement) return content
[docs]def get_resource_container(context): """ Get a `ResourceContainer` from a rendering context. :param context: Context. :type context: jinja2.runtime.Context :return: Resource Container. :rtype: shuup.xtheme.resources.ResourceContainer|None """ return context.get(RESOURCE_CONTAINER_VAR_NAME)
@contextfunction
[docs]def add_resource(context, location, resource): """ Add an Xtheme resource into the given context. :param context: Context. :type context: jinja2.runtime.Context :param location: Location string (see `KNOWN_LOCATIONS`). :type location: str :param resource: Resource descriptor (URL or inline markup object). :type resource: str|InlineMarkupResource|InlineScriptResource :return: Success flag. :rtype: bool """ rc = get_resource_container(context) if rc: return bool(rc.add_resource(location, resource)) return False
[docs]def valid_view(context): """ Prevent adding the global snippet in admin views and in editor view. """ view_class = getattr(context["view"], "__class__", None) if context.get("view") else None request = context.get("request") if not (view_class and request): return False match = request.resolver_match if not (match and match.app_name != "shuup_admin"): return False from shuup.xtheme.views.editor import EditorView if issubclass(view_class, EditorView): return False return True
[docs]def inject_global_snippet(context, content): if not valid_view(context): return from shuup.xtheme import get_current_theme from shuup.xtheme.models import Snippet, SnippetType request = context["request"] shop = getattr(request, "shop", None) or get_shop(context["request"]) cache_key = GLOBAL_SNIPPETS_CACHE_KEY.format(shop_id=shop.id) snippets = cache.get(cache_key) if snippets is None: snippets = Snippet.objects.filter(shop=shop) cache.set(cache_key, snippets) for snippet in snippets: if snippet.themes: current_theme = getattr(request, "theme", None) or get_current_theme(shop) if current_theme and current_theme.identifier not in snippet.themes: continue content = snippet.snippet if snippet.snippet_type == SnippetType.InlineJS: content = InlineScriptResource(content) elif snippet.snippet_type == SnippetType.InlineCSS: content = InlineStyleResource(content) elif snippet.snippet_type == SnippetType.InlineHTMLMarkup: content = InlineMarkupResource(content) elif snippet.snippet_type == SnippetType.InlineJinjaHTMLMarkup: context = dict(context.items()) # prevent recursive injection context["allow_resource_injection"] = False content = JinjaMarkupResource(content, context) add_resource(context, snippet.location, content)