Skip to content

Commit

Permalink
chore: deprecate loader_tags
Browse files Browse the repository at this point in the history
BREAKING CHANGE: mezzanine.template.loader_tags has been deprecated. If you're still using the {% overextends %} tag provided by it switch to Django's {% extend %} tag for identical results. Fixes #1974.
  • Loading branch information
jerivas committed Jul 21, 2021
1 parent c2ee5b4 commit 6cec67c
Show file tree
Hide file tree
Showing 4 changed files with 20 additions and 304 deletions.
171 changes: 15 additions & 156 deletions mezzanine/core/checks.py
@@ -1,180 +1,39 @@
import pprint

from django import VERSION as DJANGO_VERSION
from django.conf import global_settings
from django.core.checks import Warning, register

from mezzanine.conf import settings
from mezzanine.utils.conf import middlewares_or_subclasses_installed
from mezzanine.utils.sites import SITE_PERMISSION_MIDDLEWARE

LOADER_TAGS_WARNING = (
"You have included 'mezzanine.template.loader_tags' as a builtin in your template "
"configuration. 'loader_tags' no longer exists and should be removed. If you're "
"still using the {% overextends %} tag please replace it with Django's "
"{% extend %} for identical results."
)


@register()
def check_template_settings(app_configs, **kwargs):

issues = []

if not settings.TEMPLATES:

suggested_config = _build_suggested_template_config(settings)

declaration = "TEMPLATES = "
config_formatted = pprint.pformat(suggested_config)
config_formatted = "\n".join(
" " * len(declaration) + line for line in config_formatted.splitlines()
)
config_formatted = declaration + config_formatted[len(declaration) :]

issues.append(
Warning(
"Please update your settings to use the TEMPLATES setting rather "
"than the deprecated individual TEMPLATE_ settings. The latter "
"are unsupported and correct behaviour is not guaranteed. Here's "
"a suggestion based on on your existing configuration:\n\n%s\n"
% config_formatted,
id="mezzanine.core.W01",
)
)

if settings.DEBUG != settings.TEMPLATE_DEBUG:
issues.append(
Warning(
"TEMPLATE_DEBUG and DEBUG settings have different values, "
"which may not be what you want. Mezzanine used to fix this "
"for you, but doesn't any more. Update your settings.py to "
"use the TEMPLATES setting to have template debugging "
"controlled by the DEBUG setting.",
id="mezzanine.core.W02",
)
)

else:
loader_tags_built_in = any(
"mezzanine.template.loader_tags"
in config.get("OPTIONS", {}).get("builtins", {})
for config in settings.TEMPLATES
)
if not DJANGO_VERSION < (1, 9) and not loader_tags_built_in:
issues.append(
Warning(
"You haven't included 'mezzanine.template.loader_tags' as a "
"builtin in any of your template configurations. Mezzanine's "
"'overextends' tag will not be available in your templates.",
id="mezzanine.core.W03",
)
)
if any(
"mezzanine.template.loader_tags"
in config.get("OPTIONS", {}).get("builtins", {})
for config in settings.TEMPLATES
):
issues.append(Warning(LOADER_TAGS_WARNING, id="mezzanine.core.W05"))

return issues


def _build_suggested_template_config(settings):

suggested_templates_config = {
"BACKEND": "django.template.backends.django.DjangoTemplates",
"OPTIONS": {
"builtins": [
"mezzanine.template.loader_tags",
],
},
}

def set_setting(name, value, unconditional=False):
if value or unconditional:
suggested_templates_config[name] = value

def set_option(name, value):
if value:
suggested_templates_config["OPTIONS"][name.lower()] = value

def get_debug(_):
if settings.TEMPLATE_DEBUG != settings.DEBUG:
return settings.TEMPLATE_DEBUG

def get_default(default):
def getter(name):
value = getattr(settings, name)
if value == getattr(global_settings, name):
value = default
return value

return getter

default_context_processors = [
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
"django.core.context_processors.debug",
"django.core.context_processors.i18n",
"django.core.context_processors.static",
"django.core.context_processors.media",
"django.core.context_processors.request",
"django.core.context_processors.tz",
"mezzanine.conf.context_processors.settings",
"mezzanine.pages.context_processors.page",
]

def get_loaders(_):
"""
Django's default TEMPLATES setting doesn't specify loaders, instead
dynamically sets a default based on whether or not APP_DIRS is True.
We check here if the existing TEMPLATE_LOADERS setting matches one
of those default cases, and omit the 'loaders' option if so.
"""
template_loaders = list(settings.TEMPLATE_LOADERS)
default_loaders = list(global_settings.TEMPLATE_LOADERS)

if template_loaders == default_loaders:
# Equivalent to Django's default with APP_DIRS True
template_loaders = None
app_dirs = True
elif template_loaders == default_loaders[:1]:
# Equivalent to Django's default with APP_DIRS False
template_loaders = None
app_dirs = False
else:
# This project has a custom loaders setting, which we'll use.
# Custom loaders are incompatible with APP_DIRS.
app_dirs = False

return template_loaders, app_dirs

def set_loaders(name, value):
template_loaders, app_dirs = value
set_option(name, template_loaders)
set_setting("APP_DIRS", app_dirs, unconditional=True)

old_settings = [
("ALLOWED_INCLUDE_ROOTS", settings.__getattr__, set_option),
("TEMPLATE_STRING_IF_INVALID", settings.__getattr__, set_option),
("TEMPLATE_DIRS", settings.__getattr__, set_setting),
(
"TEMPLATE_CONTEXT_PROCESSORS",
get_default(default_context_processors),
set_option,
),
("TEMPLATE_DEBUG", get_debug, set_option),
("TEMPLATE_LOADERS", get_loaders, set_loaders),
]

def convert_setting_name(old_name):
return old_name.rpartition("TEMPLATE_")[2]

for setting_name, getter, setter in old_settings:
value = getter(setting_name)
new_setting_name = convert_setting_name(setting_name)
setter(new_setting_name, value)

return [suggested_templates_config]


@register()
def check_sites_middleware(app_configs, **kwargs):

if not middlewares_or_subclasses_installed([SITE_PERMISSION_MIDDLEWARE]):
return [
Warning(
SITE_PERMISSION_MIDDLEWARE
+ " missing from settings.MIDDLEWARE - per site"
" permissions not applied",
f"{SITE_PERMISSION_MIDDLEWARE} missing from settings.MIDDLEWARE - "
"per site permissions not applied",
id="mezzanine.core.W04",
)
]
Expand Down
3 changes: 0 additions & 3 deletions mezzanine/project_template/project_name/settings.py
Expand Up @@ -207,9 +207,6 @@
"mezzanine.conf.context_processors.settings",
"mezzanine.pages.context_processors.page",
],
"builtins": [
"mezzanine.template.loader_tags",
],
"loaders": [
"mezzanine.template.loaders.host_themes.Loader",
"django.template.loaders.filesystem.Loader",
Expand Down
148 changes: 4 additions & 144 deletions mezzanine/template/loader_tags.py
@@ -1,147 +1,7 @@
import os
import warnings
from itertools import chain

from django import VERSION as DJANGO_VERSION
from django.template import Template, TemplateDoesNotExist, TemplateSyntaxError
from django.template.loader_tags import ExtendsNode

from mezzanine import template
from django import template

register = template.Library()


class OverExtendsNode(ExtendsNode):
"""
Allows the template ``foo/bar.html`` to extend ``foo/bar.html``,
given that there is another version of it that can be loaded. This
allows templates to be created in a project that extend their app
template counterparts, or even app templates that extend other app
templates with the same relative name/path.
We use our own version of ``find_template``, that uses an explict
list of template directories to search for the template, based on
the directories that the known template loaders
(``app_directories`` and ``filesystem``) use. This list gets stored
in the template context, and each time a template is found, its
absolute path gets removed from the list, so that subsequent
searches for the same relative name/path can find parent templates
in other directories, which allows circular inheritance to occur.
Django's ``app_directories``, ``filesystem``, and ``cached``
loaders are supported. The ``eggs`` loader, and any loader that
implements ``load_template_source`` with a source string returned,
should also theoretically work.
"""

def find_template(self, name, context, peeking=False):
"""
Replacement for Django's ``find_template`` that uses the current
template context to keep track of which template directories it
has used when finding a template. This allows multiple templates
with the same relative name/path to be discovered, so that
circular template inheritance can occur.
"""

# These imports want settings, which aren't available when this
# module is imported to ``add_to_builtins``, so do them here.
import django.template.loaders.app_directories as app_directories

from mezzanine.conf import settings

# Store a dictionary in the template context mapping template
# names to the lists of template directories available to
# search for that template. Each time a template is loaded, its
# origin directory is removed from its directories list.
context_name = "OVEREXTENDS_DIRS"
if context_name not in context:
context[context_name] = {}
if name not in context[context_name]:
all_dirs = list(
chain.from_iterable(
[
template_engine.get("DIRS", [])
for template_engine in settings.TEMPLATES
]
)
) + list(app_directories.get_app_template_dirs("templates"))
# os.path.abspath is needed under uWSGI, and also ensures we
# have consistent path separators across different OSes.
context[context_name][name] = list(map(os.path.abspath, all_dirs))

# Build a list of template loaders to use. For loaders that wrap
# other loaders like the ``cached`` template loader, unwind its
# internal loaders and add those instead.
loaders = []
for loader in context.template.engine.template_loaders:
loaders.extend(getattr(loader, "loaders", [loader]))

# Go through the loaders and try to find the template. When
# found, removed its absolute path from the context dict so
# that it won't be used again when the same relative name/path
# is requested.
for loader in loaders:
dirs = context[context_name][name]
try:
source, path = loader.load_template_source(name, dirs)
except TemplateDoesNotExist:
pass
else:
# Only remove the absolute path for the initial call in
# get_parent, and not when we're peeking during the
# second call.
if not peeking:
remove_path = os.path.abspath(path[: -len(name) - 1])
context[context_name][name].remove(remove_path)
return Template(source)
raise TemplateDoesNotExist(name)

def get_parent(self, context):
"""
Load the parent template using our own ``find_template``, which
will cause its absolute path to not be used again. Then peek at
the first node, and if its parent arg is the same as the
current parent arg, we know circular inheritance is going to
occur, in which case we try and find the template again, with
the absolute directory removed from the search list.
"""
parent = self.parent_name.resolve(context)
# If parent is a template object, just return it.
if hasattr(parent, "render"):
return parent
template = self.find_template(parent, context)
for node in template.nodelist:
if (
isinstance(node, ExtendsNode)
and node.parent_name.resolve(context) == parent
):
return self.find_template(parent, context, peeking=True)
return template


@register.tag
def overextends(parser, token):
"""
Extended version of Django's ``extends`` tag that allows circular
inheritance to occur, eg a template can both be overridden and
extended at once.
"""
if DJANGO_VERSION >= (1, 9):
warnings.warn(
"The `overextends` template tag is deprecated in favour of "
"Django's built-in `extends` tag, which supports recursive "
"extension in Django 1.9 and above.",
DeprecationWarning,
stacklevel=2,
)

bits = token.split_contents()
if len(bits) != 2:
raise TemplateSyntaxError("'%s' takes one argument" % bits[0])
parent_name = parser.compile_filter(bits[1])
nodelist = parser.parse()
if nodelist.get_nodes_by_type(ExtendsNode):
raise TemplateSyntaxError(
"'%s' cannot appear more than once " "in the same template" % bits[0]
)
return OverExtendsNode(nodelist, parent_name, None)
# TODO: Remove this file a couple releases after Mezzanine 5
# We've kept this file because users upgrading to Mezzanine 5 might still refer to it.
# However, mezzanine.core.checks should warn them about it being deprecated.
2 changes: 1 addition & 1 deletion pytest.ini
Expand Up @@ -5,4 +5,4 @@ addopts =
--cov-report html
--cov-report term:skip-covered
# Original coverage was 54% (not great), but at least ensure we don't go below
--cov-fail-under 54
--cov-fail-under 55

0 comments on commit 6cec67c

Please sign in to comment.