diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index 5fd9029325..a950cfd565 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -230,6 +230,8 @@ TEMPLATE_CONTEXT_PROCESSORS = ( # Output to use in template system for invalid (e.g. misspelled) variables. TEMPLATE_STRING_IF_INVALID = '' +TEMPLATES = [] + # Default email address to use for various automated correspondence from # the site managers. DEFAULT_FROM_EMAIL = 'webmaster@localhost' diff --git a/django/conf/project_template/project_name/settings.py b/django/conf/project_template/project_name/settings.py index 827822e1ff..33b08fa2a9 100644 --- a/django/conf/project_template/project_name/settings.py +++ b/django/conf/project_template/project_name/settings.py @@ -53,6 +53,25 @@ MIDDLEWARE_CLASSES = ( ROOT_URLCONF = '{{ project_name }}.urls' +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.core.context_processors.debug', + 'django.core.context_processors.i18n', + 'django.core.context_processors.tz', + 'django.core.context_processors.media', + 'django.core.context_processors.static', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + WSGI_APPLICATION = '{{ project_name }}.wsgi.application' @@ -66,6 +85,7 @@ DATABASES = { } } + # Internationalization # https://docs.djangoproject.com/en/{{ docs_version }}/topics/i18n/ diff --git a/django/template/__init__.py b/django/template/__init__.py index 24a51ac20d..31f3078bad 100644 --- a/django/template/__init__.py +++ b/django/template/__init__.py @@ -1,3 +1,15 @@ +### Multiple Template Engines + +from .utils import EngineHandler + + +engines = EngineHandler() + +__all__ = ('engines',) + + +### Django Template Language + # Public exceptions from .base import (TemplateDoesNotExist, TemplateSyntaxError, # NOQA VariableDoesNotExist) @@ -14,4 +26,4 @@ from .base import resolve_variable # NOQA from .base import Library # NOQA -__all__ = ('Template', 'Context', 'RequestContext') +__all__ += ('Template', 'Context', 'RequestContext') diff --git a/django/template/utils.py b/django/template/utils.py index 48e4f9abed..3303c1ccb7 100644 --- a/django/template/utils.py +++ b/django/template/utils.py @@ -1,9 +1,107 @@ +from collections import Counter, OrderedDict import os import sys +import warnings from django.apps import apps +from django.conf import settings +from django.core.exceptions import ImproperlyConfigured from django.utils import lru_cache from django.utils import six +from django.utils.deprecation import RemovedInDjango20Warning +from django.utils.functional import cached_property +from django.utils.module_loading import import_string + + +class InvalidTemplateEngineError(ImproperlyConfigured): + pass + + +class EngineHandler(object): + def __init__(self, templates=None): + """ + templates is an optional list of template engine definitions + (structured like settings.TEMPLATES). + """ + self._templates = templates + self._engines = {} + + @cached_property + def templates(self): + if self._templates is None: + self._templates = settings.TEMPLATES + + if not self._templates: + warnings.warn( + "You haven't defined a TEMPLATES setting. You must do so " + "before upgrading to Django 2.0. Otherwise Django will be " + "unable to load templates.", RemovedInDjango20Warning) + self._templates = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': settings.TEMPLATE_DIRS, + 'OPTIONS': { + 'allowed_include_roots': settings.ALLOWED_INCLUDE_ROOTS, + 'context_processors': settings.TEMPLATE_CONTEXT_PROCESSORS, + 'loaders': settings.TEMPLATE_LOADERS, + 'string_if_invalid': settings.TEMPLATE_STRING_IF_INVALID, + }, + }, + ] + + templates = OrderedDict() + for tpl in self._templates: + tpl = tpl.copy() + try: + # This will raise an exception if 'BACKEND' doesn't exist or + # isn't a string containing at least one dot. + default_name = tpl['BACKEND'].rsplit('.', 2)[-2] + except Exception: + invalid_backend = tpl.get('BACKEND', '') + raise ImproperlyConfigured( + "Invalid BACKEND for a template engine: {}. Check " + "your TEMPLATES setting.".format(invalid_backend)) + + tpl.setdefault('NAME', default_name) + tpl.setdefault('DIRS', []) + tpl.setdefault('APP_DIRS', False) + tpl.setdefault('OPTIONS', {}) + + templates[tpl['NAME']] = tpl + + counts = Counter(list(templates)) + duplicates = [alias for alias, count in counts.most_common() if count > 1] + if duplicates: + raise ImproperlyConfigured( + "Template engine aliases aren't unique, duplicates: {}. " + "Set a unique NAME for each engine in settings.TEMPLATES." + .format(", ".join(duplicates))) + + return templates + + def __getitem__(self, alias): + try: + return self._engines[alias] + except KeyError: + try: + params = self.templates[alias] + except KeyError: + raise InvalidTemplateEngineError( + "Could not find config for '{}' " + "in settings.TEMPLATES".format(alias)) + + backend = params.pop('BACKEND') + engine_cls = import_string(backend) + engine = engine_cls(params) + + self._engines[alias] = engine + return engine + + def __iter__(self): + return iter(self.templates) + + def all(self): + return [self[alias] for alias in self] @lru_cache.lru_cache() diff --git a/django/test/signals.py b/django/test/signals.py index d8a864d1f4..6888753b06 100644 --- a/django/test/signals.py +++ b/django/test/signals.py @@ -84,8 +84,9 @@ def clear_routers_cache(**kwargs): @receiver(setting_changed) -def reset_default_template_engine(**kwargs): +def reset_template_engines(**kwargs): if kwargs['setting'] in { + 'TEMPLATES', 'TEMPLATE_DIRS', 'ALLOWED_INCLUDE_ROOTS', 'TEMPLATE_CONTEXT_PROCESSORS', @@ -93,7 +94,15 @@ def reset_default_template_engine(**kwargs): 'TEMPLATE_LOADERS', 'TEMPLATE_STRING_IF_INVALID', 'FILE_CHARSET', + 'INSTALLED_APPS', }: + from django.template import engines + try: + del engines.templates + except AttributeError: + pass + engines._templates = None + engines._engines = {} from django.template.engine import Engine Engine.get_default.cache_clear() diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index d5dc447d8d..7152b5eed9 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -2288,6 +2288,92 @@ will still be printed, but will not prevent management commands from running. See also the :doc:`/ref/checks` documentation. +.. setting:: TEMPLATES + +TEMPLATES +--------- + +.. versionadded:: 1.8 + +Default:: ``[]`` (Empty list) + +A list containing the settings for all template engines to be used with +Django. Each item of the list is a dictionary containing the options for an +individual engine. + +Here's a simple setup that tells the Django template engine to load templates +from the ``templates`` subdirectories inside installed applications:: + + TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'APP_DIRS': True, + }, + ] + +The following options are available for all backends. + +.. setting:: TEMPLATES-BACKEND + +BACKEND +~~~~~~~ + +Default: not defined + +The template backend to use. The built-in template backends are: + +* ``'django.template.backends.django.DjangoTemplates'`` +* ``'django.template.backends.jinja2.Jinja2'`` + +You can use a template backend that doesn't ship with Django by setting +``BACKEND`` to a fully-qualified path (i.e. ``'mypackage.whatever.Backend'``). + +.. setting:: TEMPLATES-NAME + +NAME +~~~~ + +Default: see below + +The alias for this particular template engine. It's an identifier that allows +selecting an engine for rendering. Aliases must be unique across all +configured template engines. + +It defaults to the name of the module defining the engine class, i.e. the +next to last piece of :setting:`BACKEND `, when it isn't +provided. For example if the backend is ``'mypackage.whatever.Backend'`` then +its default name is ``'whatever'``. + +.. setting:: TEMPLATES-DIRS + +DIRS +~~~~ + +Default:: ``[]`` (Empty list) + +Directories where the engine should look for template source files, in search +order. + +.. setting:: TEMPLATES-APP_DIRS + +APP_DIRS +~~~~~~~~ + +Default:: ``False`` + +Whether the engine should look for template source files inside installed +applications. + +.. setting:: TEMPLATES-OPTIONS + +OPTIONS +~~~~~~~ + +Default:: ``{}`` (Empty dict) + +Extra parameters to pass to the template backend. Available parameters vary +depending on the template backend. + .. setting:: TEMPLATE_CONTEXT_PROCESSORS TEMPLATE_CONTEXT_PROCESSORS @@ -3327,6 +3413,7 @@ Serialization Templates --------- * :setting:`ALLOWED_INCLUDE_ROOTS` +* :setting:`TEMPLATES` * :setting:`TEMPLATE_CONTEXT_PROCESSORS` * :setting:`TEMPLATE_DEBUG` * :setting:`TEMPLATE_DIRS`