divio/django-cms

View on GitHub
cms/templatetags/cms_tags.py

Summary

Maintainability
F
5 days
Test Coverage
from collections import OrderedDict, namedtuple
from copy import copy
from datetime import datetime

from classytags.arguments import Argument, MultiKeywordArgument, MultiValueArgument
from classytags.core import Options, Tag
from classytags.helpers import AsTag, InclusionTag
from classytags.parser import Parser
from classytags.utils import flatten_context
from classytags.values import ListValue, StringValue
from django import template
from django.conf import settings
from django.contrib.sites.models import Site
from django.core.mail import mail_managers
from django.db.models import Model
from django.template.loader import render_to_string
from django.urls import reverse
from django.utils.encoding import smart_str
from django.utils.html import escape
from django.utils.http import urlencode
from django.utils.translation import get_language, override
from django.utils.translation import gettext_lazy as _
from django.utils.translation import override as force_language
from sekizai.templatetags.sekizai_tags import RenderBlock, SekizaiParser

from cms.cache.page import get_page_url_cache, set_page_url_cache
from cms.exceptions import PlaceholderNotFound
from cms.models import CMSPlugin, Page, StaticPlaceholder
from cms.models import Placeholder as PlaceholderModel
from cms.plugin_pool import plugin_pool
from cms.toolbar.utils import get_toolbar_from_request
from cms.utils import get_current_site, get_language_from_request, get_site_id
from cms.utils.moderator import _use_draft
from cms.utils.page import get_page_queryset
from cms.utils.placeholder import validate_placeholder_name
from cms.utils.urlutils import admin_reverse

NULL = object()
DeclaredPlaceholder = namedtuple('DeclaredPlaceholder', ['slot', 'inherit'])
DeclaredStaticPlaceholder = namedtuple('DeclaredStaticPlaceholder', ['slot', 'site_bound'])


register = template.Library()


def _get_page_by_untyped_arg(page_lookup, request, site_id):
    """
    The `page_lookup` argument can be of any of the following types:
    - Integer: interpreted as `pk` of the desired page
    - String: interpreted as `reverse_id` of the desired page
    - `dict`: a dictionary containing keyword arguments to find the desired page
    (for instance: `{'pk': 1}`)
    - `Page`: you can also pass a Page object directly, in which case there will be no database lookup.
    - `None`: the current page will be used
    """
    if page_lookup is None:
        return request.current_page
    if isinstance(page_lookup, Page):
        if request.current_page and request.current_page.pk == page_lookup.pk:
            return request.current_page
        return page_lookup
    if isinstance(page_lookup, str):
        page_lookup = {'reverse_id': page_lookup}
    elif isinstance(page_lookup, int):
        page_lookup = {'pk': page_lookup}
    elif not isinstance(page_lookup, dict):
        raise TypeError('The page_lookup argument can be either a Dictionary, Integer, Page, or String.')
    site = Site.objects._get_site_by_id(site_id)
    try:
        if 'pk' in page_lookup:
            page = Page.objects.select_related('node').get(**page_lookup)
            if request and _use_draft(request):
                if page.publisher_is_draft:
                    return page
                else:
                    return page.publisher_draft
            else:
                if page.publisher_is_draft:
                    return page.publisher_public
                else:
                    return page
        else:
            pages = get_page_queryset(site, draft=_use_draft(request))
            return pages.select_related('node').get(**page_lookup)
    except Page.DoesNotExist:
        subject = _('Page not found on %(domain)s') % {'domain': site.domain}
        body = _("A template tag couldn't find the page with lookup arguments `%(page_lookup)s\n`. "
                 "The URL of the request was: http://%(host)s%(path)s") \
               % {'page_lookup': repr(page_lookup), 'host': site.domain, 'path': request.path_info}
        if settings.DEBUG:
            raise Page.DoesNotExist(body)
        else:
            mw = settings.MIDDLEWARE
            if getattr(settings, 'SEND_BROKEN_LINK_EMAILS', False):
                mail_managers(subject, body, fail_silently=True)
            elif 'django.middleware.common.BrokenLinkEmailsMiddleware' in mw:
                mail_managers(subject, body, fail_silently=True)
            return None


def _show_placeholder_by_id(context, placeholder_name, reverse_id,
                            lang=None, site=None, use_cache=True):
    validate_placeholder_name(placeholder_name)

    request = context['request']
    toolbar = get_toolbar_from_request(request)
    renderer = toolbar.get_content_renderer()

    if site:
        # Backwards compatibility.
        # Assume user passed in a pk directly.
        site_id = getattr(site, 'pk', site)
    else:
        site_id = renderer.current_site.pk

    page = _get_page_by_untyped_arg(reverse_id, request, site_id)

    if not page:
        return ''

    try:
        placeholder = page.placeholders.get(slot=placeholder_name)
    except PlaceholderModel.DoesNotExist:
        if settings.DEBUG:
            raise
        return ''
    else:
        # save a query. cache the page.
        placeholder.page = page

    content = renderer.render_placeholder(
        placeholder=placeholder,
        context=context,
        language=lang,
        page=page,
        editable=False,
        use_cache=use_cache,
    )
    return content


def _show_uncached_placeholder_by_id(context, *args, **kwargs):
    kwargs['use_cache'] = False
    return _show_placeholder_by_id(context, *args, **kwargs)


@register.simple_tag(takes_context=True)
def render_extra_menu_items(context, obj, template='cms/toolbar/dragitem_extra_menu.html'):
    request = context['request']
    toolbar = get_toolbar_from_request(request)
    template = toolbar.templates.get_cached_template(template)

    if isinstance(obj, CMSPlugin):
        items = []

        for plugin_class in plugin_pool.plugins_with_extra_menu:
            plugin_items = plugin_class.get_extra_plugin_menu_items(request, obj)
            if plugin_items:
                items.extend(plugin_items)
    elif isinstance(obj, PlaceholderModel):
        items = []

        for plugin_class in plugin_pool.plugins_with_extra_placeholder_menu:
            plugin_items = plugin_class.get_extra_placeholder_menu_items(request, obj)

            if plugin_items:
                items.extend(plugin_items)
    else:
        items = []

    if not items:
        return ''
    return template.render({'items': items})


@register.simple_tag(takes_context=True)
def render_plugin(context, plugin):
    request = context['request']
    toolbar = get_toolbar_from_request(request)
    renderer = toolbar.get_content_renderer()
    content = renderer.render_plugin(
        instance=plugin,
        context=context,
        editable=renderer._placeholders_are_editable,
    )
    return content


class PageUrl(AsTag):
    name = 'page_url'

    options = Options(
        Argument('page_lookup'),
        Argument('lang', required=False, default=None),
        Argument('site', required=False, default=None),
        'as',
        Argument('varname', required=False, resolve=False),
    )

    def get_value_for_context(self, context, **kwargs):
        #
        # A design decision with several active members of the django-cms
        # community that using this tag with the 'as' breakpoint should never
        # return Exceptions regardless of the setting of settings.DEBUG.
        #
        # We wish to maintain backwards functionality where the non-as-variant
        # of using this tag will raise DoesNotExist exceptions only when
        # settings.DEBUG=False.
        #
        try:
            return super().get_value_for_context(context, **kwargs)
        except Page.DoesNotExist:
            return ''

    def get_value(self, context, page_lookup, lang, site):

        site_id = get_site_id(site)
        request = context.get('request', False)

        if not request:
            return ''

        if lang is None:
            lang = get_language_from_request(request)

        url = get_page_url_cache(page_lookup, lang, site_id)
        if url is None:
            page = _get_page_by_untyped_arg(page_lookup, request, site_id)
            if page:
                url = page.get_absolute_url(language=lang)
                set_page_url_cache(page_lookup, lang, site_id, url)
        if url:
            return url
        return ''


class PlaceholderParser(Parser):

    def parse_blocks(self):
        for bit in getattr(self.kwargs['extra_bits'], 'value', self.kwargs['extra_bits']):
            if getattr(bit, 'value', bit.var.value) == 'or':
                return super().parse_blocks()
        return


class PlaceholderOptions(Options):

    def get_parser_class(self):
        return PlaceholderParser


class Placeholder(Tag):
    """
    This template node is used to output page content and
    is also used in the admin to dynamically generate input fields.

    eg: {% placeholder "placeholder_name" %}

    {% placeholder "sidebar" inherit %}

    {% placeholder "footer" inherit or %}
        <a href="/about/">About us</a>
    {% endplaceholder %}

    Keyword arguments:
    name -- the name of the placeholder
    inherit -- optional argument which if given will result in inheriting
        the content of the placeholder with the same name on parent pages
    or -- optional argument which if given will make the template tag a block
        tag whose content is shown if the placeholder is empty
    """
    name = 'placeholder'
    options = PlaceholderOptions(
        Argument('name', resolve=False),
        MultiValueArgument('extra_bits', required=False, resolve=False),
        blocks=[
            ('endplaceholder', 'nodelist'),
        ],
    )

    def render_tag(self, context, name, extra_bits, nodelist=None):
        request = context.get('request')

        if not request:
            return ''

        validate_placeholder_name(name)

        toolbar = get_toolbar_from_request(request)
        renderer = toolbar.get_content_renderer()
        inherit = 'inherit' in extra_bits

        try:
            content = renderer.render_page_placeholder(
                slot=name,
                context=context,
                inherit=inherit,
                nodelist=nodelist,
            )
        except PlaceholderNotFound:
            content = ''

        if not content and nodelist:
            return nodelist.render(context)
        return content

    def get_declaration(self):
        flags = self.kwargs['extra_bits']
        slot = self.kwargs['name'].var.value.strip('"').strip("'")

        if isinstance(flags, ListValue):
            inherit = any(extra.var.value.strip() == 'inherit' for extra in flags)
            return DeclaredPlaceholder(slot=slot, inherit=inherit)
        return DeclaredPlaceholder(slot=slot, inherit=False)


class RenderPluginBlock(InclusionTag):
    """
    Acts like the CMS's templatetag 'render_model_block' but with a plugin
    instead of a model. This is used to link from a block of markup to a
    plugin's changeform.

    This is useful for UIs that have some plugins hidden from display in
    preview mode, but the CMS author needs to expose a way to edit them
    anyway. It is also useful for just making duplicate or alternate means of
    triggering the change form for a plugin.
    """

    name = 'render_plugin_block'
    template = "cms/toolbar/plugin.html"
    options = Options(
        Argument('plugin'),
        blocks=[('endrender_plugin_block', 'nodelist')],
    )

    def get_context(self, context, plugin, nodelist):
        context['content'] = nodelist.render(context)
        context['instance'] = plugin
        return context


class PageAttribute(AsTag):
    """
    This template node is used to output an attribute from a page such
    as its title or slug.

    Synopsis
         {% page_attribute "field-name" %}
         {% page_attribute "field-name" as varname %}
         {% page_attribute "field-name" page_lookup %}
         {% page_attribute "field-name" page_lookup as varname %}

    Example
         {# Output current page's page_title attribute: #}
         {% page_attribute "page_title" %}
         {# Output page_title attribute of the page with reverse_id "the_page": #}
         {% page_attribute "page_title" "the_page" %}
         {# Output slug attribute of the page with pk 10: #}
         {% page_attribute "slug" 10 %}
         {# Assign page_title attribute to a variable: #}
         {% page_attribute "page_title" as title %}

    Keyword arguments:
    field-name -- the name of the field to output. Use one of:
    - title
    - menu_title
    - page_title
    - slug
    - meta_description
    - changed_date
    - changed_by

    page_lookup -- lookup argument for Page, if omitted field-name of current page is returned.
    See _get_page_by_untyped_arg() for detailed information on the allowed types and their interpretation
    for the page_lookup argument.

    varname -- context variable name. Output will be added to template context as this variable.
    This argument is required to follow the 'as' keyword.
    """
    name = 'page_attribute'
    options = Options(
        Argument('name', resolve=False),
        Argument('page_lookup', required=False, default=None),
        'as',
        Argument('varname', required=False, resolve=False)
    )

    valid_attributes = [
        "title",
        "slug",
        "meta_description",
        "page_title",
        "menu_title",
        "changed_date",
        "changed_by",
    ]

    def get_value(self, context, name, page_lookup):
        if 'request' not in context:
            return ''
        name = name.lower()
        request = context['request']
        lang = get_language_from_request(request)
        page = _get_page_by_untyped_arg(page_lookup, request, get_site_id(None))
        if page and name in self.valid_attributes:
            func = getattr(page, "get_%s" % name)
            ret_val = func(language=lang, fallback=True)
            if not isinstance(ret_val, datetime):
                ret_val = escape(ret_val)
            return ret_val
        return ''


class CMSToolbar(RenderBlock):
    name = 'cms_toolbar'

    options = Options(
        Argument('name', required=False),  # just here so sekizai thinks this is a RenderBlock
        parser_class=SekizaiParser,
    )

    def render_tag(self, context, name, nodelist):
        request = context.get('request')

        if not request:
            return nodelist.render(context)

        toolbar = get_toolbar_from_request(request)

        if toolbar and toolbar.show_toolbar:
            toolbar.init_toolbar(request)
            return toolbar.render_with_structure(context, nodelist)
        return nodelist.render(context)


class CMSEditableObject(InclusionTag):
    """
    Templatetag that links a content extracted from a generic django model
    to the model admin changeform.
    """
    template = 'cms/toolbar/content.html'
    edit_template = 'cms/toolbar/plugin.html'
    name = 'render_model'
    options = Options(
        Argument('instance'),
        Argument('attribute'),
        Argument('edit_fields', default=None, required=False),
        Argument('language', default=None, required=False),
        Argument('filters', default=None, required=False),
        Argument('view_url', default=None, required=False),
        Argument('view_method', default=None, required=False),
        'as',
        Argument('varname', required=False, resolve=False),
    )

    def __init__(self, parser, tokens):
        self.parser = parser
        super().__init__(parser, tokens)

    def _is_editable(self, request):
        return (request and hasattr(request, 'toolbar') and request.toolbar.edit_mode_active)

    def get_template(self, context, **kwargs):
        if self._is_editable(context.get('request', None)):
            return self.edit_template
        return self.template

    def render_tag(self, context, **kwargs):
        """
        Overridden from InclusionTag to push / pop context to avoid leaks
        """
        context.push()
        template = self.get_template(context, **kwargs)
        data = self.get_context(context, **kwargs)
        output = render_to_string(template, flatten_context(data)).strip()
        context.pop()
        if kwargs.get('varname'):
            context[kwargs['varname']] = output
            return ''
        else:
            return output

    def _get_editable_context(self, context, instance, language, edit_fields,
                              view_method, view_url, querystring, editmode=True):
        """
        Populate the context with the requested attributes to trigger the change form
        """
        request = context['request']
        if hasattr(request, 'toolbar'):
            lang = request.toolbar.toolbar_language
        else:
            lang = get_language()
        opts = instance._meta
        # Django < 1.10 creates dynamic proxy model subclasses when fields are
        # deferred using .only()/.exclude(). Make sure to use the underlying
        # model options when it's the case.
        if getattr(instance, '_deferred', False):
            opts = opts.proxy_for_model._meta
        with force_language(lang):
            extra_context = {}
            if edit_fields == 'changelist':
                instance.get_plugin_name = "{} {} list".format(smart_str(_('Edit')), smart_str(opts.verbose_name))
                extra_context['attribute_name'] = 'changelist'
            elif editmode:
                instance.get_plugin_name = "{} {}".format(smart_str(_('Edit')), smart_str(opts.verbose_name))
                if not context.get('attribute_name', None):
                    # Make sure CMS.Plugin object will not clash in the frontend.
                    extra_context['attribute_name'] = '-'.join(edit_fields) \
                                                        if not isinstance('edit_fields', str) else edit_fields
            else:
                instance.get_plugin_name = "{} {}".format(smart_str(_('Add')), smart_str(opts.verbose_name))
                extra_context['attribute_name'] = 'add'
            extra_context['instance'] = instance
            extra_context['generic'] = opts
            # view_method has the precedence and we retrieve the corresponding
            # attribute in the instance class.
            # If view_method refers to a method it will be called passing the
            # request; if it's an attribute, it's stored for later use
            if view_method:
                method = getattr(instance, view_method)
                if callable(method):
                    url_base = method(context['request'])
                else:
                    url_base = method
            else:
                # The default view_url is the default admin changeform for the
                # current instance
                if not editmode:
                    view_url = 'admin:{}_{}_add'.format(
                        opts.app_label, opts.model_name)
                    url_base = reverse(view_url)
                elif not edit_fields:
                    if not view_url:
                        view_url = 'admin:{}_{}_change'.format(
                            opts.app_label, opts.model_name)
                    if isinstance(instance, Page):
                        url_base = reverse(view_url, args=(instance.pk, language))
                    else:
                        url_base = reverse(view_url, args=(instance.pk,))
                else:
                    if not view_url:
                        view_url = 'admin:{}_{}_edit_field'.format(
                            opts.app_label, opts.model_name)
                    if view_url.endswith('_changelist'):
                        url_base = reverse(view_url)
                    else:
                        url_base = reverse(view_url, args=(instance.pk, language))
                    querystring['edit_fields'] = ",".join(context['edit_fields'])
            if editmode:
                extra_context['edit_url'] = f"{url_base}?{urlencode(querystring)}"
            else:
                extra_context['edit_url'] = "%s" % url_base
            extra_context['refresh_page'] = True
            # We may be outside the CMS (e.g.: an application which is not attached via Apphook)
            # in this case we may only go back to the home page
            if getattr(context['request'], 'current_page', None):
                extra_context['redirect_on_close'] = context['request'].current_page.get_absolute_url(language)
            else:
                extra_context['redirect_on_close'] = ''
        return extra_context

    def _get_content(self, context, instance, attribute, language, filters):
        """
        Renders the requested attribute
        """
        extra_context = copy(context)
        attr_value = None
        if hasattr(instance, 'lazy_translation_getter'):
            attr_value = instance.lazy_translation_getter(attribute, '')
        if not attr_value:
            attr_value = getattr(instance, attribute, '')
        extra_context['content'] = attr_value
        # This allow the requested item to be a method, a property or an
        # attribute
        if callable(extra_context['content']):
            if isinstance(instance, Page):
                extra_context['content'] = extra_context['content'](language)
            else:
                extra_context['content'] = extra_context['content'](context['request'])
        if filters:
            expression = self.parser.compile_filter("content|%s" % (filters))
            extra_context['content'] = expression.resolve(extra_context)
        return extra_context

    def _get_data_context(self, context, instance, attribute, edit_fields,
                          language, filters, view_url, view_method):
        """
        Renders the requested attribute and attach changeform trigger to it

        Uses `_get_empty_context`
        """
        if not attribute:
            return context
        attribute = attribute.strip()
        # ugly-ish
        if isinstance(instance, Page):
            if attribute == 'title':
                attribute = 'get_title'
                if not edit_fields:
                    edit_fields = 'title'
            elif attribute == 'page_title':
                attribute = 'get_page_title'
                if not edit_fields:
                    edit_fields = 'page_title'
            elif attribute == 'menu_title':
                attribute = 'get_menu_title'
                if not edit_fields:
                    edit_fields = 'menu_title'
            elif attribute == 'titles':
                attribute = 'get_title'
                if not edit_fields:
                    edit_fields = 'title,page_title,menu_title'
            view_url = 'admin:cms_page_edit_title_fields'
        extra_context = copy(context)
        extra_context['attribute_name'] = attribute
        extra_context = self._get_empty_context(extra_context, instance,
                                                edit_fields, language, view_url,
                                                view_method)
        extra_context.update(self._get_content(extra_context, instance, attribute,
                                               language, filters))
        # content is for non-edit template content.html
        # rendered_content is for edit template plugin.html
        # in this templatetag both hold the same content
        extra_context['content'] = extra_context['content']
        extra_context['rendered_content'] = extra_context['content']
        return extra_context

    def _get_empty_context(self, context, instance, edit_fields, language,
                           view_url, view_method, editmode=True):
        """
        Inject in a copy of the context the data requested to trigger the edit.

        `content` and `rendered_content` is emptied.
        """
        if not language:
            language = get_language_from_request(context['request'])
        # This allow the requested item to be a method, a property or an
        # attribute
        if not instance and editmode:
            return context
        extra_context = copy(context)
        # ugly-ish
        if instance and isinstance(instance, Page):
            if edit_fields == 'titles':
                edit_fields = 'title,page_title,menu_title'
            view_url = 'admin:cms_page_edit_title_fields'
        if edit_fields == 'changelist':
            view_url = 'admin:{}_{}_changelist'.format(
                instance._meta.app_label, instance._meta.model_name)
        querystring = OrderedDict((('language', language),))
        if edit_fields:
            extra_context['edit_fields'] = edit_fields.strip().split(",")
        # If the toolbar is not enabled the following part is just skipped: it
        # would cause a performance hit for no reason
        if self._is_editable(context.get('request', None)):
            extra_context.update(self._get_editable_context(
                extra_context, instance, language, edit_fields, view_method,
                view_url, querystring, editmode))
        # content is for non-edit template content.html
        # rendered_content is for edit template plugin.html
        # in this templatetag both hold the same content
        extra_context['content'] = ''
        extra_context['rendered_content'] = ''
        return extra_context

    def get_context(self, context, **kwargs):
        """
        Uses _get_data_context to render the requested attributes
        """
        kwargs.pop('varname')
        extra_context = self._get_data_context(context, **kwargs)
        extra_context['render_model'] = True
        return extra_context


class CMSEditableObjectIcon(CMSEditableObject):
    """
    Templatetag that links a content extracted from a generic django model
    to the model admin changeform.

    The output of this templatetag is just an icon to trigger the changeform.
    """
    name = 'render_model_icon'
    options = Options(
        Argument('instance'),
        Argument('edit_fields', default=None, required=False),
        Argument('language', default=None, required=False),
        Argument('view_url', default=None, required=False),
        Argument('view_method', default=None, required=False),
        'as',
        Argument('varname', required=False, resolve=False),
    )

    def get_context(self, context, **kwargs):
        """
        Uses _get_empty_context and adds the `render_model_icon` variable.
        """
        kwargs.pop('varname')
        extra_context = self._get_empty_context(context, **kwargs)
        extra_context['render_model_icon'] = True
        return extra_context


class CMSEditableObjectAdd(CMSEditableObject):
    """
    Templatetag that links a content extracted from a generic django model
    to the model admin changeform.

    The output of this templatetag is just an icon to trigger the changeform.
    """
    name = 'render_model_add'
    options = Options(
        Argument('instance'),
        Argument('language', default=None, required=False),
        Argument('view_url', default=None, required=False),
        Argument('view_method', default=None, required=False),
        'as',
        Argument('varname', required=False, resolve=False),
    )

    def get_context(self, context, instance, language, view_url, view_method,
                    varname):
        """
        Uses _get_empty_context and adds the `render_model_icon` variable.
        """
        if isinstance(instance, Model) and not instance.pk:
            instance.pk = 0
        extra_context = self._get_empty_context(context, instance, None,
                                                language, view_url,
                                                view_method, editmode=False)
        extra_context['render_model_add'] = True
        return extra_context


class CMSEditableObjectAddBlock(CMSEditableObject):
    """
    Templatetag that links arbitrary content to the addform for the specified
    model (based on the provided model instance).
    """
    name = 'render_model_add_block'
    options = Options(
        Argument('instance'),
        Argument('language', default=None, required=False),
        Argument('view_url', default=None, required=False),
        Argument('view_method', default=None, required=False),
        'as',
        Argument('varname', required=False, resolve=False),
        blocks=[('endrender_model_add_block', 'nodelist')],
    )

    def render_tag(self, context, **kwargs):
        """
        Renders the block and then inject the resulting HTML in the template
        context
        """
        context.push()
        template = self.get_template(context, **kwargs)
        data = self.get_context(context, **kwargs)
        data['content'] = kwargs['nodelist'].render(data)
        data['rendered_content'] = data['content']
        output = render_to_string(template, flatten_context(data))
        context.pop()
        if kwargs.get('varname'):
            context[kwargs['varname']] = output
            return ''
        else:
            return output

    def get_context(self, context, **kwargs):
        """
        Uses _get_empty_context and adds the `render_model_icon` variable.
        """
        instance = kwargs.pop('instance')
        if isinstance(instance, Model) and not instance.pk:
            instance.pk = 0
        kwargs.pop('varname')
        kwargs.pop('nodelist')
        extra_context = self._get_empty_context(context, instance, None,
                                                editmode=False, **kwargs)
        extra_context['render_model_add'] = True
        return extra_context


class CMSEditableObjectBlock(CMSEditableObject):
    """
    Templatetag that links a content extracted from a generic django model
    to the model admin changeform.

    The rendered content is to be specified in the enclosed block.
    """
    name = 'render_model_block'
    options = Options(
        Argument('instance'),
        Argument('edit_fields', default=None, required=False),
        Argument('language', default=None, required=False),
        Argument('view_url', default=None, required=False),
        Argument('view_method', default=None, required=False),
        'as',
        Argument('varname', required=False, resolve=False),
        blocks=[('endrender_model_block', 'nodelist')],
    )

    def render_tag(self, context, **kwargs):
        """
        Renders the block and then inject the resulting HTML in the template
        context
        """
        context.push()
        template = self.get_template(context, **kwargs)
        data = self.get_context(context, **kwargs)
        data['content'] = kwargs['nodelist'].render(data)
        data['rendered_content'] = data['content']
        output = render_to_string(template, flatten_context(data))
        context.pop()
        if kwargs.get('varname'):
            context[kwargs['varname']] = output
            return ''
        else:
            return output

    def get_context(self, context, **kwargs):
        """
        Uses _get_empty_context and adds the `instance` object to the local
        context. Context here is to be intended as the context of the nodelist
        in the block.
        """
        kwargs.pop('varname')
        kwargs.pop('nodelist')
        extra_context = self._get_empty_context(context, **kwargs)
        extra_context['instance'] = kwargs.get('instance')
        extra_context['render_model_block'] = True
        return extra_context


class StaticPlaceholderNode(Tag):
    name = 'static_placeholder'
    options = PlaceholderOptions(
        Argument('code', required=True),
        MultiValueArgument('extra_bits', required=False, resolve=False),
        blocks=[
            ('endstatic_placeholder', 'nodelist'),
        ]
    )

    def render_tag(self, context, code, extra_bits, nodelist=None):
        request = context.get('request')

        if not code or not request:
            # an empty string was passed in or the variable is not available in the context
            if nodelist:
                return nodelist.render(context)
            return ''

        toolbar = get_toolbar_from_request(request)
        renderer = toolbar.get_content_renderer()

        if isinstance(code, StaticPlaceholder):
            static_placeholder = code
        else:
            kwargs = {
                'code': code,
                'defaults': {'creation_method': StaticPlaceholder.CREATION_BY_TEMPLATE}
            }

            if 'site' in extra_bits:
                kwargs['site'] = get_current_site()
            else:
                kwargs['site_id__isnull'] = True
            static_placeholder = StaticPlaceholder.objects.get_or_create(**kwargs)[0]

        content = renderer.render_static_placeholder(
            static_placeholder,
            context=context,
            nodelist=nodelist,
        )
        return content

    def get_declaration(self, context):
        flags = self.kwargs['extra_bits']
        slot = self.kwargs['code'].resolve(context)

        if isinstance(flags, ListValue):
            site_bound = any(extra.var.value.strip() == 'site' for extra in flags)
            return DeclaredStaticPlaceholder(slot=slot, site_bound=site_bound)
        return DeclaredStaticPlaceholder(slot=slot, site_bound=False)


class RenderPlaceholder(AsTag):
    """
    Render the content of the plugins contained in a placeholder.
    The result can be assigned to a variable within the template's context by using the `as` keyword.
    It behaves in the same way as the `PageAttribute` class, check its docstring for more details.
    """
    name = 'render_placeholder'
    options = Options(
        Argument('placeholder'),
        Argument('width', default=None, required=False),
        'language',
        Argument('language', default=None, required=False),
        'as',
        Argument('varname', required=False, resolve=False)
    )

    def _get_value(self, context, editable=True, **kwargs):
        request = context['request']
        toolbar = get_toolbar_from_request(request)
        renderer = toolbar.get_content_renderer()
        placeholder = kwargs.get('placeholder')
        nocache = kwargs.get('nocache', False)

        if not placeholder:
            return ''

        if isinstance(placeholder, str):
            placeholder = PlaceholderModel.objects.get(slot=placeholder)

        content = renderer.render_placeholder(
            placeholder=placeholder,
            context=context,
            language=kwargs.get('language'),
            editable=editable,
            use_cache=not nocache,
            width=kwargs.get('width'),
        )
        return content

    def get_value_for_context(self, context, **kwargs):
        return self._get_value(context, editable=False, **kwargs)

    def get_value(self, context, **kwargs):
        return self._get_value(context, **kwargs)


class RenderUncachedPlaceholder(RenderPlaceholder):
    """
    Uncached version of RenderPlaceholder
    This templatetag will neither get the result from cache, nor will update
    the cache value for the given placeholder
    """
    name = 'render_uncached_placeholder'

    def _get_value(self, context, editable=True, **kwargs):
        kwargs['nocache'] = True
        return super()._get_value(context, editable, **kwargs)


class EmptyListValue(list, StringValue):
    """
    A list of template variables for easy resolving
    """
    def __init__(self, value=NULL):
        list.__init__(self)
        if value is not NULL:
            self.append(value)

    def resolve(self, context):
        resolved = [item.resolve(context) for item in self]
        return self.clean(resolved)


class MultiValueArgumentBeforeKeywordArgument(MultiValueArgument):
    sequence_class = EmptyListValue

    def parse(self, parser, token, tagname, kwargs):
        if '=' in token:
            if self.name not in kwargs:
                kwargs[self.name] = self.sequence_class()
            return False
        return super().parse(
            parser,
            token,
            tagname,
            kwargs
        )


class CMSAdminURL(AsTag):
    name = 'cms_admin_url'
    options = Options(
        Argument('viewname'),
        MultiValueArgumentBeforeKeywordArgument('args', required=False),
        MultiKeywordArgument('kwargs', required=False),
        'as',
        Argument('varname', resolve=False, required=False)
    )

    def get_value(self, context, viewname, args, kwargs):
        if hasattr(context.get("request"), "toolbar"):
            with override(context["request"].toolbar.toolbar_language):
                return admin_reverse(viewname, args=args, kwargs=kwargs)
        return admin_reverse(viewname, args=args, kwargs=kwargs)


register.tag('page_attribute', PageAttribute)
register.tag('render_plugin_block', RenderPluginBlock)
register.tag('placeholder', Placeholder)
register.tag('cms_toolbar', CMSToolbar)
register.tag('page_url', PageUrl)
register.tag('page_id_url', PageUrl)
register.tag('render_model_block', CMSEditableObjectBlock)
register.tag('render_model_add_block', CMSEditableObjectAddBlock)
register.tag('render_model_add', CMSEditableObjectAdd)
register.tag('render_model_icon', CMSEditableObjectIcon)
register.tag('render_model', CMSEditableObject)
register.simple_tag(
    _show_placeholder_by_id,
    takes_context=True,
    name='show_placeholder',
)
register.simple_tag(
    _show_placeholder_by_id,
    takes_context=True,
    name='show_placeholder_by_id',
)
register.simple_tag(
    _show_uncached_placeholder_by_id,
    takes_context=True,
    name='show_uncached_placeholder',
)
register.simple_tag(
    _show_uncached_placeholder_by_id,
    takes_context=True,
    name='show_uncached_placeholder_by_id',
)
register.tag('cms_admin_url', CMSAdminURL)
register.tag('render_placeholder', RenderPlaceholder)
register.tag('render_uncached_placeholder', RenderUncachedPlaceholder)
register.tag('static_placeholder', StaticPlaceholderNode)