mirror of
https://github.com/django/django.git
synced 2025-08-21 01:09:13 +00:00
Fixed #36410 -- Added support for Template Partials to the Django Template Language.
Introduced `{% partialdef %}` and `{% partial %}` template tags to define and render reusable named fragments within a template file. Partials can also be accessed using the `template_name#partial_name` syntax via `get_template()`, `render()`, `{% include %}`, and other template-loading tools. Adjusted `get_template()` behavior to support partial resolution, with appropriate error handling for invalid names and edge cases. Introduced `PartialTemplate` to encapsulate partial rendering behavior. Includes tests and internal refactors to support partial context binding, exception reporting, and tag validation. Co-authored-by: Carlton Gibson <carlton@noumenal.es> Co-authored-by: Natalia <124304+nessita@users.noreply.github.com> Co-authored-by: Nick Pope <nick@nickpope.me.uk>
This commit is contained in:
parent
fda3c1712a
commit
5e06b97095
@ -61,6 +61,7 @@ from .base import ( # NOQA isort:skip
|
||||
Node,
|
||||
NodeList,
|
||||
Origin,
|
||||
PartialTemplate,
|
||||
Template,
|
||||
Variable,
|
||||
)
|
||||
|
@ -88,6 +88,11 @@ UNKNOWN_SOURCE = "<unknown source>"
|
||||
# than instantiating SimpleLazyObject with _lazy_re_compile().
|
||||
tag_re = re.compile(r"({%.*?%}|{{.*?}}|{#.*?#})")
|
||||
|
||||
combined_partial_re = re.compile(
|
||||
r"{%\s*partialdef\s+(?P<name>[\w-]+)(?:\s+inline)?\s*%}"
|
||||
r"|{%\s*endpartialdef(?:\s+[\w-]+)?\s*%}"
|
||||
)
|
||||
|
||||
logger = logging.getLogger("django.template")
|
||||
|
||||
|
||||
@ -288,6 +293,57 @@ class Template:
|
||||
}
|
||||
|
||||
|
||||
class PartialTemplate:
|
||||
"""
|
||||
A lightweight Template lookalike used for template partials.
|
||||
|
||||
Wraps nodelist as a partial, in order to be able to bind context.
|
||||
"""
|
||||
|
||||
def __init__(self, nodelist, origin, name):
|
||||
self.nodelist = nodelist
|
||||
self.origin = origin
|
||||
self.name = name
|
||||
|
||||
def get_exception_info(self, exception, token):
|
||||
template = self.origin.loader.get_template(self.origin.template_name)
|
||||
return template.get_exception_info(exception, token)
|
||||
|
||||
def find_partial_source(self, full_source, partial_name):
|
||||
start_match = None
|
||||
nesting = 0
|
||||
|
||||
for match in combined_partial_re.finditer(full_source):
|
||||
if name := match["name"]: # Opening tag.
|
||||
if start_match is None and name == partial_name:
|
||||
start_match = match
|
||||
if start_match is not None:
|
||||
nesting += 1
|
||||
elif start_match is not None:
|
||||
nesting -= 1
|
||||
if nesting == 0:
|
||||
return full_source[start_match.start() : match.end()]
|
||||
|
||||
return ""
|
||||
|
||||
@property
|
||||
def source(self):
|
||||
template = self.origin.loader.get_template(self.origin.template_name)
|
||||
return self.find_partial_source(template.source, self.name)
|
||||
|
||||
def _render(self, context):
|
||||
return self.nodelist.render(context)
|
||||
|
||||
def render(self, context):
|
||||
with context.render_context.push_state(self):
|
||||
if context.template is None:
|
||||
with context.bind_template(self):
|
||||
context.template_name = self.name
|
||||
return self._render(context)
|
||||
else:
|
||||
return self._render(context)
|
||||
|
||||
|
||||
def linebreak_iter(template_source):
|
||||
yield 0
|
||||
p = template_source.find("\n")
|
||||
|
@ -12,6 +12,7 @@ from itertools import groupby
|
||||
from django.conf import settings
|
||||
from django.http import QueryDict
|
||||
from django.utils import timezone
|
||||
from django.utils.datastructures import DeferredSubDict
|
||||
from django.utils.html import conditional_escape, escape, format_html
|
||||
from django.utils.lorem_ipsum import paragraphs, words
|
||||
from django.utils.safestring import mark_safe
|
||||
@ -29,6 +30,7 @@ from .base import (
|
||||
VARIABLE_TAG_START,
|
||||
Node,
|
||||
NodeList,
|
||||
PartialTemplate,
|
||||
TemplateSyntaxError,
|
||||
VariableDoesNotExist,
|
||||
kwarg_re,
|
||||
@ -408,6 +410,31 @@ class NowNode(Node):
|
||||
return formatted
|
||||
|
||||
|
||||
class PartialDefNode(Node):
|
||||
def __init__(self, partial_name, inline, nodelist):
|
||||
self.partial_name = partial_name
|
||||
self.inline = inline
|
||||
self.nodelist = nodelist
|
||||
|
||||
def render(self, context):
|
||||
return self.nodelist.render(context) if self.inline else ""
|
||||
|
||||
|
||||
class PartialNode(Node):
|
||||
def __init__(self, partial_name, partial_mapping):
|
||||
# Defer lookup in `partial_mapping` and nodelist to runtime.
|
||||
self.partial_name = partial_name
|
||||
self.partial_mapping = partial_mapping
|
||||
|
||||
def render(self, context):
|
||||
try:
|
||||
return self.partial_mapping[self.partial_name].render(context)
|
||||
except KeyError:
|
||||
raise TemplateSyntaxError(
|
||||
f"Partial '{self.partial_name}' is not defined in the current template."
|
||||
)
|
||||
|
||||
|
||||
class ResetCycleNode(Node):
|
||||
def __init__(self, node):
|
||||
self.node = node
|
||||
@ -1174,6 +1201,75 @@ def now(parser, token):
|
||||
return NowNode(format_string, asvar)
|
||||
|
||||
|
||||
@register.tag(name="partialdef")
|
||||
def partialdef_func(parser, token):
|
||||
"""
|
||||
Declare a partial that can be used in the template.
|
||||
|
||||
Usage::
|
||||
|
||||
{% partialdef partial_name %}
|
||||
Content goes here.
|
||||
{% endpartialdef %}
|
||||
|
||||
Store the nodelist in the context under the key "partials". It can be
|
||||
retrieved using the ``{% partial %}`` tag.
|
||||
|
||||
The optional ``inline`` argument renders the partial's contents
|
||||
immediately, at the point where it is defined.
|
||||
"""
|
||||
match token.split_contents():
|
||||
case "partialdef", partial_name, "inline":
|
||||
inline = True
|
||||
case "partialdef", partial_name, _:
|
||||
raise TemplateSyntaxError(
|
||||
"The 'inline' argument does not have any parameters; either use "
|
||||
"'inline' or remove it completely."
|
||||
)
|
||||
case "partialdef", partial_name:
|
||||
inline = False
|
||||
case ["partialdef"]:
|
||||
raise TemplateSyntaxError("'partialdef' tag requires a name")
|
||||
case _:
|
||||
raise TemplateSyntaxError("'partialdef' tag takes at most 2 arguments")
|
||||
|
||||
# Parse the content until the end tag.
|
||||
valid_endpartials = ("endpartialdef", f"endpartialdef {partial_name}")
|
||||
nodelist = parser.parse(valid_endpartials)
|
||||
endpartial = parser.next_token()
|
||||
if endpartial.contents not in valid_endpartials:
|
||||
parser.invalid_block_tag(endpartial, "endpartialdef", valid_endpartials)
|
||||
|
||||
# Store the partial nodelist in the parser.extra_data attribute.
|
||||
partials = parser.extra_data.setdefault("partials", {})
|
||||
if partial_name in partials:
|
||||
raise TemplateSyntaxError(
|
||||
f"Partial '{partial_name}' is already defined in the "
|
||||
f"'{parser.origin.name}' template."
|
||||
)
|
||||
partials[partial_name] = PartialTemplate(nodelist, parser.origin, partial_name)
|
||||
|
||||
return PartialDefNode(partial_name, inline, nodelist)
|
||||
|
||||
|
||||
@register.tag(name="partial")
|
||||
def partial_func(parser, token):
|
||||
"""
|
||||
Render a partial previously declared with the ``{% partialdef %}`` tag.
|
||||
|
||||
Usage::
|
||||
|
||||
{% partial partial_name %}
|
||||
"""
|
||||
match token.split_contents():
|
||||
case "partial", partial_name:
|
||||
extra_data = parser.extra_data
|
||||
partial_mapping = DeferredSubDict(extra_data, "partials")
|
||||
return PartialNode(partial_name, partial_mapping=partial_mapping)
|
||||
case _:
|
||||
raise TemplateSyntaxError("'partial' tag requires a single argument")
|
||||
|
||||
|
||||
@register.simple_tag(name="querystring", takes_context=True)
|
||||
def querystring(context, *args, **kwargs):
|
||||
"""
|
||||
|
@ -174,11 +174,31 @@ class Engine:
|
||||
Return a compiled Template object for the given template name,
|
||||
handling template inheritance recursively.
|
||||
"""
|
||||
original_name = template_name
|
||||
try:
|
||||
template_name, _, partial_name = template_name.partition("#")
|
||||
except AttributeError:
|
||||
raise TemplateDoesNotExist(original_name)
|
||||
|
||||
if not template_name:
|
||||
raise TemplateDoesNotExist(original_name)
|
||||
|
||||
template, origin = self.find_template(template_name)
|
||||
if not hasattr(template, "render"):
|
||||
# template needs to be compiled
|
||||
template = Template(template, origin, template_name, engine=self)
|
||||
return template
|
||||
|
||||
if not partial_name:
|
||||
return template
|
||||
|
||||
extra_data = getattr(template, "extra_data", {})
|
||||
try:
|
||||
partial = extra_data["partials"][partial_name]
|
||||
except (KeyError, TypeError):
|
||||
raise TemplateDoesNotExist(partial_name, tried=[template_name])
|
||||
partial.engine = self
|
||||
|
||||
return partial
|
||||
|
||||
def render_to_string(self, template_name, context=None):
|
||||
"""
|
||||
|
@ -24,7 +24,7 @@ from django.core.exceptions import ImproperlyConfigured
|
||||
from django.core.signals import request_started, setting_changed
|
||||
from django.db import DEFAULT_DB_ALIAS, connections, reset_queries
|
||||
from django.db.models.options import Options
|
||||
from django.template import Template
|
||||
from django.template import PartialTemplate, Template
|
||||
from django.test.signals import template_rendered
|
||||
from django.urls import get_script_prefix, set_script_prefix
|
||||
from django.utils.translation import deactivate
|
||||
@ -147,7 +147,9 @@ def setup_test_environment(debug=None):
|
||||
settings.EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
|
||||
|
||||
saved_data.template_render = Template._render
|
||||
saved_data.partial_template_render = PartialTemplate._render
|
||||
Template._render = instrumented_test_render
|
||||
PartialTemplate._render = instrumented_test_render
|
||||
|
||||
mail.outbox = []
|
||||
|
||||
@ -165,6 +167,7 @@ def teardown_test_environment():
|
||||
settings.DEBUG = saved_data.debug
|
||||
settings.EMAIL_BACKEND = saved_data.email_backend
|
||||
Template._render = saved_data.template_render
|
||||
PartialTemplate._render = saved_data.partial_template_render
|
||||
|
||||
del _TestState.saved_data
|
||||
del mail.outbox
|
||||
|
@ -345,3 +345,21 @@ class CaseInsensitiveMapping(Mapping):
|
||||
"Element key %r invalid, only strings are allowed" % elem[0]
|
||||
)
|
||||
yield elem
|
||||
|
||||
|
||||
class DeferredSubDict:
|
||||
"""
|
||||
Wrap a dict, allowing deferred access to a sub-dict under a given key.
|
||||
|
||||
The value at ``deferred_key`` must itself be a dict. Accessing
|
||||
``DeferredSubDict(parent_dict, deferred_key)[key]`` retrieves
|
||||
``parent_dict[deferred_key][key]`` at access time, so updates to
|
||||
the parent dict are reflected.
|
||||
"""
|
||||
|
||||
def __init__(self, parent_dict, deferred_key):
|
||||
self.parent_dict = parent_dict
|
||||
self.deferred_key = deferred_key
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.parent_dict[self.deferred_key][key]
|
||||
|
@ -957,6 +957,80 @@ output (as a string) inside a variable. This is useful if you want to use
|
||||
{% now "Y" as current_year %}
|
||||
{% blocktranslate %}Copyright {{ current_year }}{% endblocktranslate %}
|
||||
|
||||
.. templatetag:: partial
|
||||
|
||||
``partial``
|
||||
-----------
|
||||
|
||||
.. versionadded:: 6.0
|
||||
|
||||
Renders a template fragment that was defined with :ttag:`partialdef`, inserting
|
||||
the matching partial at this location.
|
||||
|
||||
Usage:
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{% partial partial_name %}
|
||||
|
||||
The ``partial_name`` argument is the name of the template fragment to render.
|
||||
|
||||
In the following example, a partial named ``button`` is defined and then
|
||||
rendered three times:
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{% partialdef button %}
|
||||
<button>Submit</button>
|
||||
{% endpartialdef %}
|
||||
|
||||
{% partial button %}
|
||||
{% partial button %}
|
||||
{% partial button %}
|
||||
|
||||
.. templatetag:: partialdef
|
||||
|
||||
``partialdef``
|
||||
--------------
|
||||
|
||||
.. versionadded:: 6.0
|
||||
|
||||
Defines a reusable template fragment that can be rendered multiple times within
|
||||
the same template or accessed directly via :ref:`template loading or inclusion
|
||||
<template-partials-direct-access>`.
|
||||
|
||||
Usage:
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{% partialdef partial_name %}
|
||||
{# Reusable content. #}
|
||||
{% endpartialdef %}
|
||||
|
||||
The ``partial_name`` argument is required and must be a valid template
|
||||
identifier.
|
||||
|
||||
In the following example, a new fragment named ``card`` is defined:
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{% partialdef card %}
|
||||
<div class="card">
|
||||
<h3>{{ title }}</h3>
|
||||
<p>{{ content }}</p>
|
||||
</div>
|
||||
{% endpartialdef %}
|
||||
|
||||
This partial can then be rendered using the :ttag:`partial` tag:
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{% partial card %}
|
||||
{% partial card %}
|
||||
|
||||
To :ref:`render a fragment immediately in place <template-partials-inline>`,
|
||||
use the ``inline`` option. The partial is still stored and can be reused later.
|
||||
|
||||
.. templatetag:: querystring
|
||||
|
||||
``querystring``
|
||||
|
@ -475,6 +475,150 @@ it also defines the content that fills the hole in the *parent*. If there were
|
||||
two similarly-named :ttag:`block` tags in a template, that template's parent
|
||||
wouldn't know which one of the blocks' content to use.
|
||||
|
||||
.. _template-partials:
|
||||
|
||||
Template partials
|
||||
=================
|
||||
|
||||
.. versionadded:: 6.0
|
||||
|
||||
Template partials define reusable template fragments within a template file.
|
||||
They are self-contained components that can be rendered multiple times within
|
||||
the same template, helping to avoid repetition and maintain consistent output.
|
||||
|
||||
Basic syntax
|
||||
------------
|
||||
|
||||
A partial can be defined using the :ttag:`partialdef` tag:
|
||||
|
||||
.. code-block:: html+django
|
||||
:caption: ``authors.html``
|
||||
|
||||
{% partialdef user-info %}
|
||||
<div id="user-info-{{ user.username }}">
|
||||
<h3>{{ user.name }}</h3>
|
||||
<p>{{ user.bio }}</p>
|
||||
</div>
|
||||
{% endpartialdef %}
|
||||
|
||||
For extra readability, the name can be included in the ``{% endpartialdef %}``
|
||||
closing tag:
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{% partialdef user-info %}
|
||||
{# ... #}
|
||||
{% endpartialdef user-info %}
|
||||
|
||||
The template fragment can be rendered using the :ttag:`partial` tag:
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{% partial user-info %}
|
||||
|
||||
Fragment reuse
|
||||
--------------
|
||||
|
||||
A template fragment can be reused multiple times:
|
||||
|
||||
.. code-block:: html+django
|
||||
:caption: ``authors.html``
|
||||
|
||||
{% block content %}
|
||||
<h2>Authors</h2>
|
||||
{% for user in authors %}
|
||||
{% partial user-info %}
|
||||
{% endfor %}
|
||||
|
||||
<h2>Editors</h2>
|
||||
{% for user in editors %}
|
||||
{% partial user-info %}
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
The partial content is rendered each time the named partial is used, with the
|
||||
current template context.
|
||||
|
||||
.. _template-partials-inline:
|
||||
|
||||
Inline partials
|
||||
---------------
|
||||
|
||||
A template fragment can be defined and rendered in-place using the ``inline``
|
||||
argument. This defines the partial for later reuse while also rendering it
|
||||
immediately at its definition:
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{# Define and render immediately. #}
|
||||
{% partialdef user-info inline %}
|
||||
<div id="user-info-{{ user.username }}">
|
||||
<h3>{{ user.name }}</h3>
|
||||
<p>{{ user.bio }}</p>
|
||||
</div>
|
||||
{% endpartialdef %}
|
||||
|
||||
{# Other page content here. #}
|
||||
|
||||
{# Reuse later elsewhere in the template. #}
|
||||
<section class="featured-authors">
|
||||
<h2>Featured Authors</h2>
|
||||
{% for user in featured %}
|
||||
{% partial user-info %}
|
||||
{% endfor %}
|
||||
</section>
|
||||
|
||||
.. _template-partials-direct-access:
|
||||
|
||||
Accessing partials directly
|
||||
---------------------------
|
||||
|
||||
Template fragments defined with :ttag:`partialdef` can be accessed directly via
|
||||
template loading or inclusion using the ``template.html#partial_name`` syntax.
|
||||
|
||||
For example, using the :func:`~django.shortcuts.render` shortcut, the following
|
||||
code renders only the partial named ``user-info`` defined in the
|
||||
``authors.html`` template::
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
|
||||
|
||||
def user_info_partial(request, user_id):
|
||||
user = get_object_or_404(User, id=user_id)
|
||||
return render(request, "authors.html#user-info", {"user": user})
|
||||
|
||||
This approach is particularly useful for AJAX-style requests that update only
|
||||
specific portions of a page with the rendered template fragment.
|
||||
|
||||
Template partials can also be included using the :ttag:`include` template tag
|
||||
with the same ``#`` directive:
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{% include "authors.html#user-info" %}
|
||||
|
||||
Context handling
|
||||
----------------
|
||||
|
||||
Template partials are rendered with the current template context. They work as
|
||||
expected in loops and with context variables:
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{% for user in users %}
|
||||
{% partial user-info %}
|
||||
{% endfor %}
|
||||
|
||||
Context variables can be adjusted using the :ttag:`with` tag:
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{% with user=featured_author %}
|
||||
<h2>Featured Author of the Month</h2>
|
||||
{% partial user-info %}
|
||||
{% endwith %}
|
||||
|
||||
.. _next section: #automatic-html-escaping
|
||||
.. _automatic-html-escaping:
|
||||
|
||||
|
@ -91,6 +91,21 @@ Notably, the return type of the :class:`EmailMessage.message()
|
||||
previous ``SafeMIMEText`` and ``SafeMIMEMultipart`` return types, but is not an
|
||||
instance of those now-deprecated classes.
|
||||
|
||||
Template Partials
|
||||
-----------------
|
||||
|
||||
The :ref:`Django Template Language <template-language-intro>` now supports
|
||||
:ref:`template partials <template-partials>` , making it easier to encapsulate
|
||||
and reuse small named fragments within a template file. The new tags
|
||||
:ttag:`{% partialdef %} <partialdef>` and :ttag:`{% partial %} <partial>`
|
||||
define a partial and render it, respectively.
|
||||
|
||||
Partials can also be referenced using the ``template_name#partial_name`` syntax
|
||||
with :func:`~django.template.Engine.get_template`,
|
||||
:func:`~django.shortcuts.render`, :ttag:`{% include %}<include>`, and other
|
||||
template-loading tools, enabling more modular and maintainable templates
|
||||
without needing to split components into separate files.
|
||||
|
||||
Minor features
|
||||
--------------
|
||||
|
||||
|
@ -306,6 +306,33 @@ The ``django.template.loader`` module defines two functions to load templates.
|
||||
If you want to restrict the search to a particular template engine, pass
|
||||
the engine's :setting:`NAME <TEMPLATES-NAME>` in the ``using`` argument.
|
||||
|
||||
**Partial loading:**
|
||||
|
||||
When using the :class:`~django.template.backends.django.DjangoTemplates`
|
||||
backend, a specific fragment from a template can be loaded by name. This
|
||||
fragment should have been previously defined using the :ttag:`partialdef`
|
||||
template tag::
|
||||
|
||||
from django.template.loader import get_template
|
||||
|
||||
# Load an entire template.
|
||||
template = get_template("template.html")
|
||||
|
||||
# Load a specific fragment from a template.
|
||||
partial = get_template("template.html#partial_name")
|
||||
|
||||
When loading a partial, the returned object behaves like a regular
|
||||
``Template`` but contains only the partial's content.
|
||||
|
||||
See :ref:`template-partials` for more information about defining and using
|
||||
template fragments.
|
||||
|
||||
.. versionchanged:: 6.0
|
||||
|
||||
Support for loading template partials when using the
|
||||
:class:`~django.template.backends.django.DjangoTemplates` backend was
|
||||
added.
|
||||
|
||||
.. function:: select_template(template_name_list, using=None)
|
||||
|
||||
``select_template()`` is just like ``get_template()``, except it takes a
|
||||
|
652
tests/template_tests/syntax_tests/test_partials.py
Normal file
652
tests/template_tests/syntax_tests/test_partials.py
Normal file
@ -0,0 +1,652 @@
|
||||
from django.template import (
|
||||
Context,
|
||||
TemplateDoesNotExist,
|
||||
TemplateSyntaxError,
|
||||
VariableDoesNotExist,
|
||||
)
|
||||
from django.template.base import Token, TokenType
|
||||
from django.test import SimpleTestCase
|
||||
from django.views.debug import ExceptionReporter
|
||||
|
||||
from ..utils import setup
|
||||
|
||||
partial_templates = {
|
||||
"partial_base.html": (
|
||||
"<main>{% block main %}Default main content.{% endblock main %}</main>"
|
||||
),
|
||||
"partial_included.html": (
|
||||
"INCLUDED TEMPLATE START\n"
|
||||
"{% partialdef included-partial %}\n"
|
||||
"THIS IS CONTENT FROM THE INCLUDED PARTIAL\n"
|
||||
"{% endpartialdef %}\n\n"
|
||||
"Now using the partial: {% partial included-partial %}\n"
|
||||
"INCLUDED TEMPLATE END\n"
|
||||
),
|
||||
}
|
||||
|
||||
valid_partialdef_names = (
|
||||
"dot.in.name",
|
||||
"'space in name'",
|
||||
"exclamation!",
|
||||
"@at",
|
||||
"slash/something",
|
||||
"inline",
|
||||
"inline-inline",
|
||||
"INLINE" "with+plus",
|
||||
"with&",
|
||||
"with%percent",
|
||||
"with,comma",
|
||||
"with:colon",
|
||||
"with;semicolon",
|
||||
"[brackets]",
|
||||
"(parens)",
|
||||
"{curly}",
|
||||
)
|
||||
|
||||
|
||||
def gen_partial_template(name, *args, **kwargs):
|
||||
if args or kwargs:
|
||||
extra = " ".join((args, *("{k}={v}" for k, v in kwargs.items()))) + " "
|
||||
else:
|
||||
extra = ""
|
||||
return (
|
||||
f"{{% partialdef {name} {extra}%}}TEST with {name}!{{% endpartialdef %}}"
|
||||
f"{{% partial {name} %}}"
|
||||
)
|
||||
|
||||
|
||||
class PartialTagTests(SimpleTestCase):
|
||||
libraries = {"bad_tag": "template_tests.templatetags.bad_tag"}
|
||||
|
||||
@setup({name: gen_partial_template(name) for name in valid_partialdef_names})
|
||||
def test_valid_partialdef_names(self):
|
||||
for template_name in valid_partialdef_names:
|
||||
with self.subTest(template_name=template_name):
|
||||
output = self.engine.render_to_string(template_name)
|
||||
self.assertEqual(output, f"TEST with {template_name}!")
|
||||
|
||||
@setup(
|
||||
{
|
||||
"basic": (
|
||||
"{% partialdef testing-name %}"
|
||||
"HERE IS THE TEST CONTENT"
|
||||
"{% endpartialdef %}"
|
||||
"{% partial testing-name %}"
|
||||
),
|
||||
"basic_inline": (
|
||||
"{% partialdef testing-name inline %}"
|
||||
"HERE IS THE TEST CONTENT"
|
||||
"{% endpartialdef %}"
|
||||
),
|
||||
"inline_inline": (
|
||||
"{% partialdef inline inline %}"
|
||||
"HERE IS THE TEST CONTENT"
|
||||
"{% endpartialdef %}"
|
||||
),
|
||||
"with_newlines": (
|
||||
"{% partialdef testing-name %}\n"
|
||||
"HERE IS THE TEST CONTENT\n"
|
||||
"{% endpartialdef testing-name %}\n"
|
||||
"{% partial testing-name %}"
|
||||
),
|
||||
}
|
||||
)
|
||||
def test_basic_usage(self):
|
||||
for template_name in (
|
||||
"basic",
|
||||
"basic_inline",
|
||||
"inline_inline",
|
||||
"with_newlines",
|
||||
):
|
||||
with self.subTest(template_name=template_name):
|
||||
output = self.engine.render_to_string(template_name)
|
||||
self.assertEqual(output.strip(), "HERE IS THE TEST CONTENT")
|
||||
|
||||
@setup(
|
||||
{
|
||||
"inline_partial_with_context": (
|
||||
"BEFORE\n"
|
||||
"{% partialdef testing-name inline %}"
|
||||
"HERE IS THE TEST CONTENT"
|
||||
"{% endpartialdef %}\n"
|
||||
"AFTER"
|
||||
)
|
||||
}
|
||||
)
|
||||
def test_partial_inline_only_with_before_and_after_content(self):
|
||||
output = self.engine.render_to_string("inline_partial_with_context")
|
||||
self.assertEqual(output.strip(), "BEFORE\nHERE IS THE TEST CONTENT\nAFTER")
|
||||
|
||||
@setup(
|
||||
{
|
||||
"inline_partial_explicit_end": (
|
||||
"{% partialdef testing-name inline %}"
|
||||
"HERE IS THE TEST CONTENT"
|
||||
"{% endpartialdef testing-name %}\n"
|
||||
"{% partial testing-name %}"
|
||||
)
|
||||
}
|
||||
)
|
||||
def test_partial_inline_and_used_once(self):
|
||||
output = self.engine.render_to_string("inline_partial_explicit_end")
|
||||
self.assertEqual(output, "HERE IS THE TEST CONTENT\nHERE IS THE TEST CONTENT")
|
||||
|
||||
@setup(
|
||||
{
|
||||
"inline_partial_with_usage": (
|
||||
"BEFORE\n"
|
||||
"{% partialdef content_snippet inline %}"
|
||||
"HERE IS THE TEST CONTENT"
|
||||
"{% endpartialdef %}\n"
|
||||
"AFTER\n"
|
||||
"{% partial content_snippet %}"
|
||||
)
|
||||
}
|
||||
)
|
||||
def test_partial_inline_and_used_once_with_before_and_after_content(self):
|
||||
output = self.engine.render_to_string("inline_partial_with_usage")
|
||||
self.assertEqual(
|
||||
output.strip(),
|
||||
"BEFORE\nHERE IS THE TEST CONTENT\nAFTER\nHERE IS THE TEST CONTENT",
|
||||
)
|
||||
|
||||
@setup(
|
||||
{
|
||||
"partial_used_before_definition": (
|
||||
"TEMPLATE START\n"
|
||||
"{% partial testing-name %}\n"
|
||||
"MIDDLE CONTENT\n"
|
||||
"{% partialdef testing-name %}\n"
|
||||
"THIS IS THE PARTIAL CONTENT\n"
|
||||
"{% endpartialdef %}\n"
|
||||
"TEMPLATE END"
|
||||
),
|
||||
}
|
||||
)
|
||||
def test_partial_used_before_definition(self):
|
||||
output = self.engine.render_to_string("partial_used_before_definition")
|
||||
expected = (
|
||||
"TEMPLATE START\n\nTHIS IS THE PARTIAL CONTENT\n\n"
|
||||
"MIDDLE CONTENT\n\nTEMPLATE END"
|
||||
)
|
||||
self.assertEqual(output, expected)
|
||||
|
||||
@setup(
|
||||
{
|
||||
"partial_with_extends": (
|
||||
"{% extends 'partial_base.html' %}"
|
||||
"{% partialdef testing-name %}Inside Content{% endpartialdef %}"
|
||||
"{% block main %}"
|
||||
"Main content with {% partial testing-name %}"
|
||||
"{% endblock %}"
|
||||
),
|
||||
},
|
||||
partial_templates,
|
||||
)
|
||||
def test_partial_defined_outside_main_block(self):
|
||||
output = self.engine.render_to_string("partial_with_extends")
|
||||
self.assertIn("<main>Main content with Inside Content</main>", output)
|
||||
|
||||
@setup(
|
||||
{
|
||||
"partial_with_extends_and_block_super": (
|
||||
"{% extends 'partial_base.html' %}"
|
||||
"{% partialdef testing-name %}Inside Content{% endpartialdef %}"
|
||||
"{% block main %}{{ block.super }} "
|
||||
"Main content with {% partial testing-name %}"
|
||||
"{% endblock %}"
|
||||
),
|
||||
},
|
||||
partial_templates,
|
||||
)
|
||||
def test_partial_used_with_block_super(self):
|
||||
output = self.engine.render_to_string("partial_with_extends_and_block_super")
|
||||
self.assertIn(
|
||||
"<main>Default main content. Main content with Inside Content</main>",
|
||||
output,
|
||||
)
|
||||
|
||||
@setup(
|
||||
{
|
||||
"partial_with_include": (
|
||||
"MAIN TEMPLATE START\n"
|
||||
"{% include 'partial_included.html' %}\n"
|
||||
"MAIN TEMPLATE END"
|
||||
)
|
||||
},
|
||||
partial_templates,
|
||||
)
|
||||
def test_partial_in_included_template(self):
|
||||
output = self.engine.render_to_string("partial_with_include")
|
||||
expected = (
|
||||
"MAIN TEMPLATE START\nINCLUDED TEMPLATE START\n\n\n"
|
||||
"Now using the partial: \n"
|
||||
"THIS IS CONTENT FROM THE INCLUDED PARTIAL\n\n"
|
||||
"INCLUDED TEMPLATE END\n\nMAIN TEMPLATE END"
|
||||
)
|
||||
self.assertEqual(output, expected)
|
||||
|
||||
@setup(
|
||||
{
|
||||
"partial_as_include_in_other_template": (
|
||||
"MAIN TEMPLATE START\n"
|
||||
"{% include 'partial_included.html#included-partial' %}\n"
|
||||
"MAIN TEMPLATE END"
|
||||
)
|
||||
},
|
||||
partial_templates,
|
||||
)
|
||||
def test_partial_as_include_in_template(self):
|
||||
output = self.engine.render_to_string("partial_as_include_in_other_template")
|
||||
expected = (
|
||||
"MAIN TEMPLATE START\n\n"
|
||||
"THIS IS CONTENT FROM THE INCLUDED PARTIAL\n\n"
|
||||
"MAIN TEMPLATE END"
|
||||
)
|
||||
self.assertEqual(output, expected)
|
||||
|
||||
@setup(
|
||||
{
|
||||
"nested_simple": (
|
||||
"{% extends 'base.html' %}"
|
||||
"{% block content %}"
|
||||
"This is my main page."
|
||||
"{% partialdef outer inline %}"
|
||||
" It hosts a couple of partials.\n"
|
||||
" {% partialdef inner inline %}"
|
||||
" And an inner one."
|
||||
" {% endpartialdef inner %}"
|
||||
"{% endpartialdef outer %}"
|
||||
"{% endblock content %}"
|
||||
),
|
||||
"use_outer": "{% include 'nested_simple#outer' %}",
|
||||
"use_inner": "{% include 'nested_simple#inner' %}",
|
||||
}
|
||||
)
|
||||
def test_nested_partials(self):
|
||||
with self.subTest(template_name="use_outer"):
|
||||
output = self.engine.render_to_string("use_outer")
|
||||
self.assertEqual(
|
||||
[line.strip() for line in output.split("\n")],
|
||||
["It hosts a couple of partials.", "And an inner one."],
|
||||
)
|
||||
with self.subTest(template_name="use_inner"):
|
||||
output = self.engine.render_to_string("use_inner")
|
||||
self.assertEqual(output.strip(), "And an inner one.")
|
||||
|
||||
@setup(
|
||||
{
|
||||
"partial_undefined_name": "{% partial undefined %}",
|
||||
"partial_missing_name": "{% partial %}",
|
||||
"partial_closing_tag": (
|
||||
"{% partialdef testing-name %}TEST{% endpartialdef %}"
|
||||
"{% partial testing-name %}{% endpartial %}"
|
||||
),
|
||||
"partialdef_missing_name": "{% partialdef %}{% endpartialdef %}",
|
||||
"partialdef_missing_close_tag": "{% partialdef name %}TEST",
|
||||
"partialdef_opening_closing_name_mismatch": (
|
||||
"{% partialdef testing-name %}TEST{% endpartialdef invalid %}"
|
||||
),
|
||||
"partialdef_invalid_name": gen_partial_template("with\nnewline"),
|
||||
"partialdef_extra_params": (
|
||||
"{% partialdef testing-name inline extra %}TEST{% endpartialdef %}"
|
||||
),
|
||||
"partialdef_duplicated_names": (
|
||||
"{% partialdef testing-name %}TEST{% endpartialdef %}"
|
||||
"{% partialdef testing-name %}TEST{% endpartialdef %}"
|
||||
"{% partial testing-name %}"
|
||||
),
|
||||
"partialdef_duplicated_nested_names": (
|
||||
"{% partialdef testing-name %}"
|
||||
"TEST"
|
||||
"{% partialdef testing-name %}TEST{% endpartialdef %}"
|
||||
"{% endpartialdef %}"
|
||||
"{% partial testing-name %}"
|
||||
),
|
||||
},
|
||||
)
|
||||
def test_basic_parse_errors(self):
|
||||
for template_name, error_msg in (
|
||||
(
|
||||
"partial_undefined_name",
|
||||
"Partial 'undefined' is not defined in the current template.",
|
||||
),
|
||||
("partial_missing_name", "'partial' tag requires a single argument"),
|
||||
("partial_closing_tag", "Invalid block tag on line 1: 'endpartial'"),
|
||||
("partialdef_missing_name", "'partialdef' tag requires a name"),
|
||||
("partialdef_missing_close_tag", "Unclosed tag on line 1: 'partialdef'"),
|
||||
(
|
||||
"partialdef_opening_closing_name_mismatch",
|
||||
"expected 'endpartialdef' or 'endpartialdef testing-name'.",
|
||||
),
|
||||
("partialdef_invalid_name", "Invalid block tag on line 3: 'endpartialdef'"),
|
||||
("partialdef_extra_params", "'partialdef' tag takes at most 2 arguments"),
|
||||
(
|
||||
"partialdef_duplicated_names",
|
||||
"Partial 'testing-name' is already defined in the "
|
||||
"'partialdef_duplicated_names' template.",
|
||||
),
|
||||
(
|
||||
"partialdef_duplicated_nested_names",
|
||||
"Partial 'testing-name' is already defined in the "
|
||||
"'partialdef_duplicated_nested_names' template.",
|
||||
),
|
||||
):
|
||||
with (
|
||||
self.subTest(template_name=template_name),
|
||||
self.assertRaisesMessage(TemplateSyntaxError, error_msg),
|
||||
):
|
||||
self.engine.render_to_string(template_name)
|
||||
|
||||
@setup(
|
||||
{
|
||||
"with_params": (
|
||||
"{% partialdef testing-name inline=true %}TEST{% endpartialdef %}"
|
||||
),
|
||||
"uppercase": "{% partialdef testing-name INLINE %}TEST{% endpartialdef %}",
|
||||
}
|
||||
)
|
||||
def test_partialdef_invalid_inline(self):
|
||||
error_msg = "The 'inline' argument does not have any parameters"
|
||||
for template_name in ("with_params", "uppercase"):
|
||||
with (
|
||||
self.subTest(template_name=template_name),
|
||||
self.assertRaisesMessage(TemplateSyntaxError, error_msg),
|
||||
):
|
||||
self.engine.render_to_string(template_name)
|
||||
|
||||
@setup(
|
||||
{
|
||||
"partial_broken_unclosed": (
|
||||
"<div>Before partial</div>"
|
||||
"{% partialdef unclosed_partial %}"
|
||||
"<p>This partial has no closing tag</p>"
|
||||
"<div>After partial content</div>"
|
||||
)
|
||||
}
|
||||
)
|
||||
def test_broken_partial_unclosed_exception_info(self):
|
||||
with self.assertRaises(TemplateSyntaxError) as cm:
|
||||
self.engine.get_template("partial_broken_unclosed")
|
||||
|
||||
self.assertIn("endpartialdef", str(cm.exception))
|
||||
self.assertIn("Unclosed tag", str(cm.exception))
|
||||
|
||||
reporter = ExceptionReporter(None, cm.exception.__class__, cm.exception, None)
|
||||
traceback_data = reporter.get_traceback_data()
|
||||
|
||||
exception_value = str(traceback_data.get("exception_value", ""))
|
||||
self.assertIn("Unclosed tag", exception_value)
|
||||
|
||||
@setup(
|
||||
{
|
||||
"partial_with_variable_error": (
|
||||
"<h1>Title</h1>\n"
|
||||
"{% partialdef testing-name %}\n"
|
||||
"<p>{{ nonexistent|default:alsonotthere }}</p>\n"
|
||||
"{% endpartialdef %}\n"
|
||||
"<h2>Sub Title</h2>\n"
|
||||
"{% partial testing-name %}\n"
|
||||
),
|
||||
}
|
||||
)
|
||||
def test_partial_runtime_exception_has_debug_info(self):
|
||||
template = self.engine.get_template("partial_with_variable_error")
|
||||
context = Context({})
|
||||
|
||||
if hasattr(self.engine, "string_if_invalid") and self.engine.string_if_invalid:
|
||||
output = template.render(context)
|
||||
# The variable should be replaced with INVALID
|
||||
self.assertIn("INVALID", output)
|
||||
else:
|
||||
with self.assertRaises(VariableDoesNotExist) as cm:
|
||||
template.render(context)
|
||||
|
||||
if self.engine.debug:
|
||||
exc_info = cm.exception.template_debug
|
||||
|
||||
self.assertEqual(
|
||||
exc_info["during"], "{{ nonexistent|default:alsonotthere }}"
|
||||
)
|
||||
self.assertEqual(exc_info["line"], 3)
|
||||
self.assertEqual(exc_info["name"], "partial_with_variable_error")
|
||||
self.assertIn("Failed lookup", exc_info["message"])
|
||||
|
||||
@setup(
|
||||
{
|
||||
"partial_exception_info_test": (
|
||||
"<h1>Title</h1>\n"
|
||||
"{% partialdef testing-name %}\n"
|
||||
"<p>Content</p>\n"
|
||||
"{% endpartialdef %}\n"
|
||||
),
|
||||
}
|
||||
)
|
||||
def test_partial_template_get_exception_info_delegation(self):
|
||||
if self.engine.debug:
|
||||
template = self.engine.get_template("partial_exception_info_test")
|
||||
|
||||
partial_template = template.extra_data["partials"]["testing-name"]
|
||||
|
||||
test_exc = Exception("Test exception")
|
||||
token = Token(
|
||||
token_type=TokenType.VAR,
|
||||
contents="test",
|
||||
position=(0, 4),
|
||||
)
|
||||
|
||||
exc_info = partial_template.get_exception_info(test_exc, token)
|
||||
self.assertIn("message", exc_info)
|
||||
self.assertIn("line", exc_info)
|
||||
self.assertIn("name", exc_info)
|
||||
self.assertEqual(exc_info["name"], "partial_exception_info_test")
|
||||
self.assertEqual(exc_info["message"], "Test exception")
|
||||
|
||||
@setup(
|
||||
{
|
||||
"partial_with_undefined_reference": (
|
||||
"<h1>Header</h1>\n"
|
||||
"{% partial undefined %}\n"
|
||||
"<p>After undefined partial</p>\n"
|
||||
),
|
||||
}
|
||||
)
|
||||
def test_undefined_partial_exception_info(self):
|
||||
template = self.engine.get_template("partial_with_undefined_reference")
|
||||
with self.assertRaises(TemplateSyntaxError) as cm:
|
||||
template.render(Context())
|
||||
|
||||
self.assertIn("undefined", str(cm.exception))
|
||||
self.assertIn("is not defined", str(cm.exception))
|
||||
|
||||
if self.engine.debug:
|
||||
exc_debug = cm.exception.template_debug
|
||||
|
||||
self.assertEqual(exc_debug["during"], "{% partial undefined %}")
|
||||
self.assertEqual(exc_debug["line"], 2)
|
||||
self.assertEqual(exc_debug["name"], "partial_with_undefined_reference")
|
||||
self.assertIn("undefined", exc_debug["message"])
|
||||
|
||||
@setup(
|
||||
{
|
||||
"existing_template": (
|
||||
"<h1>Header</h1><p>This template has no partials defined</p>"
|
||||
),
|
||||
}
|
||||
)
|
||||
def test_undefined_partial_exception_info_template_does_not_exist(self):
|
||||
with self.assertRaises(TemplateDoesNotExist) as cm:
|
||||
self.engine.get_template("existing_template#undefined")
|
||||
|
||||
self.assertIn("undefined", str(cm.exception))
|
||||
|
||||
@setup(
|
||||
{
|
||||
"partial_with_syntax_error": (
|
||||
"<h1>Title</h1>\n"
|
||||
"{% partialdef syntax_error_partial %}\n"
|
||||
" {% if user %}\n"
|
||||
" <p>User: {{ user.name }}</p>\n"
|
||||
" {% endif\n"
|
||||
" <p>Missing closing tag above</p>\n"
|
||||
"{% endpartialdef %}\n"
|
||||
"{% partial syntax_error_partial %}\n"
|
||||
),
|
||||
}
|
||||
)
|
||||
def test_partial_with_syntax_error_exception_info(self):
|
||||
with self.assertRaises(TemplateSyntaxError) as cm:
|
||||
self.engine.get_template("partial_with_syntax_error")
|
||||
|
||||
self.assertIn("endif", str(cm.exception).lower())
|
||||
|
||||
if self.engine.debug:
|
||||
exc_debug = cm.exception.template_debug
|
||||
|
||||
self.assertIn("endpartialdef", exc_debug["during"])
|
||||
self.assertEqual(exc_debug["name"], "partial_with_syntax_error")
|
||||
self.assertIn("endif", exc_debug["message"].lower())
|
||||
|
||||
@setup(
|
||||
{
|
||||
"partial_with_runtime_error": (
|
||||
"<h1>Title</h1>\n"
|
||||
"{% load bad_tag %}\n"
|
||||
"{% partialdef runtime_error_partial %}\n"
|
||||
" <p>This will raise an error:</p>\n"
|
||||
" {% badsimpletag %}\n"
|
||||
"{% endpartialdef %}\n"
|
||||
"{% partial runtime_error_partial %}\n"
|
||||
),
|
||||
}
|
||||
)
|
||||
def test_partial_runtime_error_exception_info(self):
|
||||
template = self.engine.get_template("partial_with_runtime_error")
|
||||
context = Context()
|
||||
|
||||
with self.assertRaises(RuntimeError) as cm:
|
||||
template.render(context)
|
||||
|
||||
if self.engine.debug:
|
||||
exc_debug = cm.exception.template_debug
|
||||
|
||||
self.assertIn("badsimpletag", exc_debug["during"])
|
||||
self.assertEqual(exc_debug["line"], 5) # Line 5 is where badsimpletag is
|
||||
self.assertEqual(exc_debug["name"], "partial_with_runtime_error")
|
||||
self.assertIn("bad simpletag", exc_debug["message"])
|
||||
|
||||
@setup(
|
||||
{
|
||||
"nested_partial_with_undefined_var": (
|
||||
"<h1>Title</h1>\n"
|
||||
"{% partialdef outer_partial %}\n"
|
||||
' <div class="outer">\n'
|
||||
" {% partialdef inner_partial %}\n"
|
||||
" <p>{{ undefined_var }}</p>\n"
|
||||
" {% endpartialdef %}\n"
|
||||
" {% partial inner_partial %}\n"
|
||||
" </div>\n"
|
||||
"{% endpartialdef %}\n"
|
||||
"{% partial outer_partial %}\n"
|
||||
),
|
||||
}
|
||||
)
|
||||
def test_nested_partial_error_exception_info(self):
|
||||
template = self.engine.get_template("nested_partial_with_undefined_var")
|
||||
context = Context()
|
||||
output = template.render(context)
|
||||
|
||||
# When string_if_invalid is set, it will show INVALID
|
||||
# When not set, undefined variables just render as empty string
|
||||
if hasattr(self.engine, "string_if_invalid") and self.engine.string_if_invalid:
|
||||
self.assertIn("INVALID", output)
|
||||
else:
|
||||
self.assertIn("<p>", output)
|
||||
self.assertIn("</p>", output)
|
||||
|
||||
@setup(
|
||||
{
|
||||
"parent.html": (
|
||||
"<!DOCTYPE html>\n"
|
||||
"<html>\n"
|
||||
"<head>{% block title %}Default Title{% endblock %}</head>\n"
|
||||
"<body>\n"
|
||||
" {% block content %}{% endblock %}\n"
|
||||
"</body>\n"
|
||||
"</html>\n"
|
||||
),
|
||||
"child.html": (
|
||||
"{% extends 'parent.html' %}\n"
|
||||
"{% block content %}\n"
|
||||
" {% partialdef content_partial %}\n"
|
||||
" <p>{{ missing_variable|undefined_filter }}</p>\n"
|
||||
" {% endpartialdef %}\n"
|
||||
" {% partial content_partial %}\n"
|
||||
"{% endblock %}\n"
|
||||
),
|
||||
}
|
||||
)
|
||||
def test_partial_in_extended_template_error(self):
|
||||
with self.assertRaises(TemplateSyntaxError) as cm:
|
||||
self.engine.get_template("child.html")
|
||||
|
||||
self.assertIn("undefined_filter", str(cm.exception))
|
||||
|
||||
if self.engine.debug:
|
||||
exc_debug = cm.exception.template_debug
|
||||
|
||||
self.assertIn("undefined_filter", exc_debug["during"])
|
||||
self.assertEqual(exc_debug["name"], "child.html")
|
||||
self.assertIn("undefined_filter", exc_debug["message"])
|
||||
|
||||
@setup(
|
||||
{
|
||||
"partial_broken_nesting": (
|
||||
"<div>Before partial</div>\n"
|
||||
"{% partialdef outer %}\n"
|
||||
"{% partialdef inner %}...{% endpartialdef outer %}\n"
|
||||
"{% endpartialdef inner %}\n"
|
||||
"<div>After partial content</div>"
|
||||
)
|
||||
}
|
||||
)
|
||||
def test_broken_partial_nesting(self):
|
||||
with self.assertRaises(TemplateSyntaxError) as cm:
|
||||
self.engine.get_template("partial_broken_nesting")
|
||||
|
||||
self.assertIn("endpartialdef", str(cm.exception))
|
||||
self.assertIn("Invalid block tag", str(cm.exception))
|
||||
self.assertIn("'endpartialdef inner'", str(cm.exception))
|
||||
|
||||
reporter = ExceptionReporter(None, cm.exception.__class__, cm.exception, None)
|
||||
traceback_data = reporter.get_traceback_data()
|
||||
|
||||
exception_value = str(traceback_data.get("exception_value", ""))
|
||||
self.assertIn("Invalid block tag", exception_value)
|
||||
self.assertIn("'endpartialdef inner'", str(cm.exception))
|
||||
|
||||
@setup(
|
||||
{
|
||||
"partial_broken_nesting_mixed": (
|
||||
"<div>Before partial</div>\n"
|
||||
"{% partialdef outer %}\n"
|
||||
"{% partialdef inner %}...{% endpartialdef %}\n"
|
||||
"{% endpartialdef inner %}\n"
|
||||
"<div>After partial content</div>"
|
||||
)
|
||||
}
|
||||
)
|
||||
def test_broken_partial_nesting_mixed(self):
|
||||
with self.assertRaises(TemplateSyntaxError) as cm:
|
||||
self.engine.get_template("partial_broken_nesting_mixed")
|
||||
|
||||
self.assertIn("endpartialdef", str(cm.exception))
|
||||
self.assertIn("Invalid block tag", str(cm.exception))
|
||||
self.assertIn("'endpartialdef outer'", str(cm.exception))
|
||||
|
||||
reporter = ExceptionReporter(None, cm.exception.__class__, cm.exception, None)
|
||||
traceback_data = reporter.get_traceback_data()
|
||||
|
||||
exception_value = str(traceback_data.get("exception_value", ""))
|
||||
self.assertIn("Invalid block tag", exception_value)
|
||||
self.assertIn("'endpartialdef outer'", str(cm.exception))
|
2
tests/template_tests/templates/partial_base.html
Normal file
2
tests/template_tests/templates/partial_base.html
Normal file
@ -0,0 +1,2 @@
|
||||
{% block main %}
|
||||
{% endblock main %}
|
9
tests/template_tests/templates/partial_child.html
Normal file
9
tests/template_tests/templates/partial_child.html
Normal file
@ -0,0 +1,9 @@
|
||||
{% extends 'partial_base.html' %}
|
||||
|
||||
{% partialdef extra-content %}
|
||||
Extra Content
|
||||
{% endpartialdef %}
|
||||
|
||||
{% block main %}
|
||||
Main Content
|
||||
{% endblock %}
|
15
tests/template_tests/templates/partial_examples.html
Normal file
15
tests/template_tests/templates/partial_examples.html
Normal file
@ -0,0 +1,15 @@
|
||||
{% partialdef test-partial %}
|
||||
TEST-PARTIAL-CONTENT
|
||||
{% endpartialdef %}
|
||||
|
||||
{% block main %}
|
||||
BEGINNING
|
||||
{% partial test-partial %}
|
||||
MIDDLE
|
||||
{% partial test-partial %}
|
||||
END
|
||||
{% endblock main %}
|
||||
|
||||
{% partialdef inline-partial inline %}
|
||||
INLINE-CONTENT
|
||||
{% endpartialdef %}
|
413
tests/template_tests/test_partials.py
Normal file
413
tests/template_tests/test_partials.py
Normal file
@ -0,0 +1,413 @@
|
||||
import os
|
||||
from unittest import mock
|
||||
|
||||
from django.http import HttpResponse
|
||||
from django.template import (
|
||||
Context,
|
||||
Origin,
|
||||
Template,
|
||||
TemplateDoesNotExist,
|
||||
TemplateSyntaxError,
|
||||
engines,
|
||||
)
|
||||
from django.template.backends.django import DjangoTemplates
|
||||
from django.template.loader import render_to_string
|
||||
from django.test import TestCase, override_settings
|
||||
from django.urls import path, reverse
|
||||
|
||||
engine = engines["django"]
|
||||
|
||||
|
||||
class PartialTagsTests(TestCase):
|
||||
|
||||
def test_invalid_template_name_raises_template_does_not_exist(self):
|
||||
for template_name in [123, None, "", "#", "#name"]:
|
||||
with (
|
||||
self.subTest(template_name=template_name),
|
||||
self.assertRaisesMessage(TemplateDoesNotExist, str(template_name)),
|
||||
):
|
||||
engine.get_template(template_name)
|
||||
|
||||
def test_template_source_is_correct(self):
|
||||
partial = engine.get_template("partial_examples.html#test-partial")
|
||||
self.assertEqual(
|
||||
partial.template.source,
|
||||
"{% partialdef test-partial %}\nTEST-PARTIAL-CONTENT\n{% endpartialdef %}",
|
||||
)
|
||||
|
||||
def test_template_source_inline_is_correct(self):
|
||||
partial = engine.get_template("partial_examples.html#inline-partial")
|
||||
self.assertEqual(
|
||||
partial.template.source,
|
||||
"{% partialdef inline-partial inline %}\nINLINE-CONTENT\n"
|
||||
"{% endpartialdef %}",
|
||||
)
|
||||
|
||||
def test_full_template_from_loader(self):
|
||||
template = engine.get_template("partial_examples.html")
|
||||
rendered = template.render({})
|
||||
|
||||
# Check the partial was rendered twice
|
||||
self.assertEqual(2, rendered.count("TEST-PARTIAL-CONTENT"))
|
||||
self.assertEqual(1, rendered.count("INLINE-CONTENT"))
|
||||
|
||||
def test_chained_exception_forwarded(self):
|
||||
with self.assertRaises(TemplateDoesNotExist) as ctx:
|
||||
engine.get_template("not_there.html#not-a-partial")
|
||||
|
||||
exception = ctx.exception
|
||||
self.assertGreater(len(exception.tried), 0)
|
||||
origin, _ = exception.tried[0]
|
||||
self.assertEqual(origin.template_name, "not_there.html")
|
||||
|
||||
def test_partials_use_cached_loader_when_configured(self):
|
||||
template_dir = os.path.join(os.path.dirname(__file__), "templates")
|
||||
backend = DjangoTemplates(
|
||||
{
|
||||
"NAME": "django",
|
||||
"DIRS": [template_dir],
|
||||
"APP_DIRS": False,
|
||||
"OPTIONS": {
|
||||
"loaders": [
|
||||
(
|
||||
"django.template.loaders.cached.Loader",
|
||||
["django.template.loaders.filesystem.Loader"],
|
||||
),
|
||||
],
|
||||
},
|
||||
}
|
||||
)
|
||||
cached_loader = backend.engine.template_loaders[0]
|
||||
filesystem_loader = cached_loader.loaders[0]
|
||||
|
||||
with mock.patch.object(
|
||||
filesystem_loader, "get_contents", wraps=filesystem_loader.get_contents
|
||||
) as mock_get_contents:
|
||||
full_template = backend.get_template("partial_examples.html")
|
||||
self.assertIn("TEST-PARTIAL-CONTENT", full_template.render({}))
|
||||
|
||||
partial_template = backend.get_template(
|
||||
"partial_examples.html#test-partial"
|
||||
)
|
||||
self.assertEqual(
|
||||
"TEST-PARTIAL-CONTENT", partial_template.render({}).strip()
|
||||
)
|
||||
|
||||
mock_get_contents.assert_called_once()
|
||||
|
||||
def test_context_available_in_response_for_partial_template(self):
|
||||
def sample_view(request):
|
||||
return HttpResponse(
|
||||
render_to_string("partial_examples.html#test-partial", {"foo": "bar"})
|
||||
)
|
||||
|
||||
class PartialUrls:
|
||||
urlpatterns = [path("sample/", sample_view, name="sample-view")]
|
||||
|
||||
with override_settings(ROOT_URLCONF=PartialUrls):
|
||||
response = self.client.get(reverse("sample-view"))
|
||||
|
||||
self.assertContains(response, "TEST-PARTIAL-CONTENT")
|
||||
self.assertEqual(response.context.get("foo"), "bar")
|
||||
|
||||
def test_response_with_multiple_parts(self):
|
||||
context = {}
|
||||
template_partials = ["partial_child.html", "partial_child.html#extra-content"]
|
||||
|
||||
response_whole_content_at_once = HttpResponse(
|
||||
"".join(
|
||||
render_to_string(template_name, context)
|
||||
for template_name in template_partials
|
||||
)
|
||||
)
|
||||
|
||||
response_with_multiple_writes = HttpResponse()
|
||||
for template_name in template_partials:
|
||||
response_with_multiple_writes.write(
|
||||
render_to_string(template_name, context)
|
||||
)
|
||||
|
||||
response_with_generator = HttpResponse(
|
||||
render_to_string(template_name, context)
|
||||
for template_name in template_partials
|
||||
)
|
||||
|
||||
for label, response in [
|
||||
("response_whole_content_at_once", response_whole_content_at_once),
|
||||
("response_with_multiple_writes", response_with_multiple_writes),
|
||||
("response_with_generator", response_with_generator),
|
||||
]:
|
||||
with self.subTest(response=label):
|
||||
self.assertIn(b"Main Content", response.content)
|
||||
self.assertIn(b"Extra Content", response.content)
|
||||
|
||||
def test_partial_engine_assignment_with_real_template(self):
|
||||
template_with_partial = engine.get_template(
|
||||
"partial_examples.html#test-partial"
|
||||
)
|
||||
self.assertEqual(template_with_partial.template.engine, engine.engine)
|
||||
rendered_content = template_with_partial.render({})
|
||||
self.assertEqual("TEST-PARTIAL-CONTENT", rendered_content.strip())
|
||||
|
||||
|
||||
class RobustPartialHandlingTests(TestCase):
|
||||
|
||||
def override_get_template(self, **kwargs):
|
||||
class TemplateWithCustomAttrs:
|
||||
def __init__(self, **kwargs):
|
||||
for k, v in kwargs.items():
|
||||
setattr(self, k, v)
|
||||
|
||||
def render(self, context):
|
||||
return "rendered content"
|
||||
|
||||
template = TemplateWithCustomAttrs(**kwargs)
|
||||
origin = self.id()
|
||||
return mock.patch.object(
|
||||
engine.engine,
|
||||
"find_template",
|
||||
return_value=(template, origin),
|
||||
)
|
||||
|
||||
def test_template_without_extra_data_attribute(self):
|
||||
partial_name = "some_partial_name"
|
||||
with (
|
||||
self.override_get_template(),
|
||||
self.assertRaisesMessage(TemplateDoesNotExist, partial_name),
|
||||
):
|
||||
engine.get_template(f"some_template.html#{partial_name}")
|
||||
|
||||
def test_template_extract_extra_data_robust(self):
|
||||
partial_name = "some_partial_name"
|
||||
for extra_data in (
|
||||
None,
|
||||
0,
|
||||
[],
|
||||
{},
|
||||
{"wrong-key": {}},
|
||||
{"partials": None},
|
||||
{"partials": {}},
|
||||
{"partials": []},
|
||||
{"partials": 0},
|
||||
):
|
||||
with (
|
||||
self.subTest(extra_data=extra_data),
|
||||
self.override_get_template(extra_data=extra_data),
|
||||
self.assertRaisesMessage(TemplateDoesNotExist, partial_name),
|
||||
):
|
||||
engine.get_template(f"template.html#{partial_name}")
|
||||
|
||||
def test_nested_partials_rendering_with_context(self):
|
||||
template_source = """
|
||||
{% partialdef outer inline %}
|
||||
Hello {{ name }}!
|
||||
{% partialdef inner inline %}
|
||||
Your age is {{ age }}.
|
||||
{% endpartialdef inner %}
|
||||
Nice to meet you.
|
||||
{% endpartialdef outer %}
|
||||
"""
|
||||
template = Template(template_source, origin=Origin(name="template.html"))
|
||||
|
||||
context = Context({"name": "Alice", "age": 25})
|
||||
rendered = template.render(context)
|
||||
|
||||
self.assertIn("Hello Alice!", rendered)
|
||||
self.assertIn("Your age is 25.", rendered)
|
||||
self.assertIn("Nice to meet you.", rendered)
|
||||
|
||||
|
||||
class FindPartialSourceTests(TestCase):
|
||||
|
||||
def test_find_partial_source_success(self):
|
||||
template = engine.get_template("partial_examples.html").template
|
||||
partial_proxy = template.extra_data["partials"]["test-partial"]
|
||||
|
||||
expected = """{% partialdef test-partial %}
|
||||
TEST-PARTIAL-CONTENT
|
||||
{% endpartialdef %}"""
|
||||
self.assertEqual(partial_proxy.source.strip(), expected.strip())
|
||||
|
||||
def test_find_partial_source_with_inline(self):
|
||||
template = engine.get_template("partial_examples.html").template
|
||||
partial_proxy = template.extra_data["partials"]["inline-partial"]
|
||||
|
||||
expected = """{% partialdef inline-partial inline %}
|
||||
INLINE-CONTENT
|
||||
{% endpartialdef %}"""
|
||||
self.assertEqual(partial_proxy.source.strip(), expected.strip())
|
||||
|
||||
def test_find_partial_source_nonexistent_partial(self):
|
||||
template = engine.get_template("partial_examples.html").template
|
||||
partial_proxy = template.extra_data["partials"]["test-partial"]
|
||||
|
||||
result = partial_proxy.find_partial_source(
|
||||
template.source, "nonexistent-partial"
|
||||
)
|
||||
self.assertEqual(result, "")
|
||||
|
||||
def test_find_partial_source_empty_partial(self):
|
||||
template_source = "{% partialdef empty %}{% endpartialdef %}"
|
||||
template = Template(template_source)
|
||||
partial_proxy = template.extra_data["partials"]["empty"]
|
||||
|
||||
result = partial_proxy.find_partial_source(template_source, "empty")
|
||||
self.assertEqual(result, "{% partialdef empty %}{% endpartialdef %}")
|
||||
|
||||
def test_find_partial_source_multiple_consecutive_partials(self):
|
||||
|
||||
template_source = (
|
||||
"{% partialdef empty %}{% endpartialdef %}"
|
||||
"{% partialdef other %}...{% endpartialdef %}"
|
||||
)
|
||||
template = Template(template_source)
|
||||
|
||||
empty_proxy = template.extra_data["partials"]["empty"]
|
||||
other_proxy = template.extra_data["partials"]["other"]
|
||||
|
||||
empty_result = empty_proxy.find_partial_source(template_source, "empty")
|
||||
self.assertEqual(empty_result, "{% partialdef empty %}{% endpartialdef %}")
|
||||
|
||||
other_result = other_proxy.find_partial_source(template_source, "other")
|
||||
self.assertEqual(other_result, "{% partialdef other %}...{% endpartialdef %}")
|
||||
|
||||
def test_partials_with_duplicate_names(self):
|
||||
test_cases = [
|
||||
(
|
||||
"nested",
|
||||
"""
|
||||
{% partialdef duplicate %}{% partialdef duplicate %}
|
||||
CONTENT
|
||||
{% endpartialdef %}{% endpartialdef %}
|
||||
""",
|
||||
),
|
||||
(
|
||||
"conditional",
|
||||
"""
|
||||
{% if ... %}
|
||||
{% partialdef duplicate %}
|
||||
CONTENT
|
||||
{% endpartialdef %}
|
||||
{% else %}
|
||||
{% partialdef duplicate %}
|
||||
OTHER-CONTENT
|
||||
{% endpartialdef %}
|
||||
{% endif %}
|
||||
""",
|
||||
),
|
||||
]
|
||||
|
||||
for test_name, template_source in test_cases:
|
||||
with self.subTest(test_name=test_name):
|
||||
with self.assertRaisesMessage(
|
||||
TemplateSyntaxError,
|
||||
"Partial 'duplicate' is already defined in the "
|
||||
"'template.html' template.",
|
||||
):
|
||||
Template(template_source, origin=Origin(name="template.html"))
|
||||
|
||||
def test_find_partial_source_supports_named_end_tag(self):
|
||||
template_source = "{% partialdef thing %}CONTENT{% endpartialdef thing %}"
|
||||
template = Template(template_source)
|
||||
partial_proxy = template.extra_data["partials"]["thing"]
|
||||
|
||||
result = partial_proxy.find_partial_source(template_source, "thing")
|
||||
self.assertEqual(
|
||||
result, "{% partialdef thing %}CONTENT{% endpartialdef thing %}"
|
||||
)
|
||||
|
||||
def test_find_partial_source_supports_nested_partials(self):
|
||||
template_source = (
|
||||
"{% partialdef outer %}"
|
||||
"{% partialdef inner %}...{% endpartialdef %}"
|
||||
"{% endpartialdef %}"
|
||||
)
|
||||
template = Template(template_source)
|
||||
|
||||
empty_proxy = template.extra_data["partials"]["outer"]
|
||||
other_proxy = template.extra_data["partials"]["inner"]
|
||||
|
||||
outer_result = empty_proxy.find_partial_source(template_source, "outer")
|
||||
self.assertEqual(
|
||||
outer_result,
|
||||
(
|
||||
"{% partialdef outer %}{% partialdef inner %}"
|
||||
"...{% endpartialdef %}{% endpartialdef %}"
|
||||
),
|
||||
)
|
||||
|
||||
inner_result = other_proxy.find_partial_source(template_source, "inner")
|
||||
self.assertEqual(inner_result, "{% partialdef inner %}...{% endpartialdef %}")
|
||||
|
||||
def test_find_partial_source_supports_nested_partials_and_named_end_tags(self):
|
||||
template_source = (
|
||||
"{% partialdef outer %}"
|
||||
"{% partialdef inner %}...{% endpartialdef inner %}"
|
||||
"{% endpartialdef outer %}"
|
||||
)
|
||||
template = Template(template_source)
|
||||
|
||||
empty_proxy = template.extra_data["partials"]["outer"]
|
||||
other_proxy = template.extra_data["partials"]["inner"]
|
||||
|
||||
outer_result = empty_proxy.find_partial_source(template_source, "outer")
|
||||
self.assertEqual(
|
||||
outer_result,
|
||||
(
|
||||
"{% partialdef outer %}{% partialdef inner %}"
|
||||
"...{% endpartialdef inner %}{% endpartialdef outer %}"
|
||||
),
|
||||
)
|
||||
|
||||
inner_result = other_proxy.find_partial_source(template_source, "inner")
|
||||
self.assertEqual(
|
||||
inner_result, "{% partialdef inner %}...{% endpartialdef inner %}"
|
||||
)
|
||||
|
||||
def test_find_partial_source_supports_nested_partials_and_mixed_end_tags_1(self):
|
||||
template_source = (
|
||||
"{% partialdef outer %}"
|
||||
"{% partialdef inner %}...{% endpartialdef %}"
|
||||
"{% endpartialdef outer %}"
|
||||
)
|
||||
template = Template(template_source)
|
||||
|
||||
empty_proxy = template.extra_data["partials"]["outer"]
|
||||
other_proxy = template.extra_data["partials"]["inner"]
|
||||
|
||||
outer_result = empty_proxy.find_partial_source(template_source, "outer")
|
||||
self.assertEqual(
|
||||
outer_result,
|
||||
(
|
||||
"{% partialdef outer %}{% partialdef inner %}"
|
||||
"...{% endpartialdef %}{% endpartialdef outer %}"
|
||||
),
|
||||
)
|
||||
|
||||
inner_result = other_proxy.find_partial_source(template_source, "inner")
|
||||
self.assertEqual(inner_result, "{% partialdef inner %}...{% endpartialdef %}")
|
||||
|
||||
def test_find_partial_source_supports_nested_partials_and_mixed_end_tags_2(self):
|
||||
template_source = (
|
||||
"{% partialdef outer %}"
|
||||
"{% partialdef inner %}...{% endpartialdef inner %}"
|
||||
"{% endpartialdef %}"
|
||||
)
|
||||
template = Template(template_source)
|
||||
|
||||
empty_proxy = template.extra_data["partials"]["outer"]
|
||||
other_proxy = template.extra_data["partials"]["inner"]
|
||||
|
||||
outer_result = empty_proxy.find_partial_source(template_source, "outer")
|
||||
self.assertEqual(
|
||||
outer_result,
|
||||
(
|
||||
"{% partialdef outer %}{% partialdef inner %}"
|
||||
"...{% endpartialdef inner %}{% endpartialdef %}"
|
||||
),
|
||||
)
|
||||
|
||||
inner_result = other_proxy.find_partial_source(template_source, "inner")
|
||||
self.assertEqual(
|
||||
inner_result, "{% partialdef inner %}...{% endpartialdef inner %}"
|
||||
)
|
@ -9,6 +9,7 @@ import pickle
|
||||
from django.test import SimpleTestCase
|
||||
from django.utils.datastructures import (
|
||||
CaseInsensitiveMapping,
|
||||
DeferredSubDict,
|
||||
DictWrapper,
|
||||
ImmutableList,
|
||||
MultiValueDict,
|
||||
@ -367,3 +368,42 @@ class CaseInsensitiveMappingTests(SimpleTestCase):
|
||||
with self.assertRaisesMessage(TypeError, msg):
|
||||
self.dict1["New Key"] = 1
|
||||
self.assertEqual(len(self.dict1), 2)
|
||||
|
||||
|
||||
class DeferredSubDictTests(SimpleTestCase):
|
||||
def test_basic(self):
|
||||
parent = {
|
||||
"settings": {"theme": "dark", "language": "en"},
|
||||
"config": {"enabled": True, "timeout": 30},
|
||||
}
|
||||
sub = DeferredSubDict(parent, "settings")
|
||||
self.assertEqual(sub["theme"], "dark")
|
||||
self.assertEqual(sub["language"], "en")
|
||||
with self.assertRaises(KeyError):
|
||||
sub["enabled"]
|
||||
|
||||
def test_reflects_changes_in_parent(self):
|
||||
parent = {"settings": {"theme": "dark"}}
|
||||
sub = DeferredSubDict(parent, "settings")
|
||||
parent["settings"]["theme"] = "light"
|
||||
self.assertEqual(sub["theme"], "light")
|
||||
parent["settings"]["mode"] = "tight"
|
||||
self.assertEqual(sub["mode"], "tight")
|
||||
|
||||
def test_missing_deferred_key_raises_keyerror(self):
|
||||
parent = {"settings": {"theme": "dark"}}
|
||||
sub = DeferredSubDict(parent, "nonexistent")
|
||||
with self.assertRaises(KeyError):
|
||||
sub["anything"]
|
||||
|
||||
def test_missing_child_key_raises_keyerror(self):
|
||||
parent = {"settings": {"theme": "dark"}}
|
||||
sub = DeferredSubDict(parent, "settings")
|
||||
with self.assertRaises(KeyError):
|
||||
sub["nonexistent"]
|
||||
|
||||
def test_child_not_a_dict_raises_typeerror(self):
|
||||
parent = {"bad": "not_a_dict"}
|
||||
sub = DeferredSubDict(parent, "bad")
|
||||
with self.assertRaises(TypeError):
|
||||
sub["any_key"]
|
||||
|
Loading…
x
Reference in New Issue
Block a user