Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions django/template/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
Node,
NodeList,
Origin,
PartialTemplate,
Template,
Variable,
)
Expand Down
56 changes: 56 additions & 0 deletions django/template/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@
# 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")


Expand Down Expand Up @@ -288,6 +293,57 @@ def get_exception_info(self, exception, token):
}


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")
Expand Down
96 changes: 96 additions & 0 deletions django/template/defaulttags.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
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
Expand All @@ -29,6 +30,7 @@
VARIABLE_TAG_START,
Node,
NodeList,
PartialTemplate,
TemplateSyntaxError,
VariableDoesNotExist,
kwarg_re,
Expand Down Expand Up @@ -408,6 +410,31 @@ def render(self, context):
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
Expand Down Expand Up @@ -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):
"""
Expand Down
22 changes: 21 additions & 1 deletion django/template/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,11 +174,31 @@ def get_template(self, template_name):
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):
"""
Expand Down
5 changes: 4 additions & 1 deletion django/test/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
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
Expand Down Expand Up @@ -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 = []

Expand All @@ -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
Expand Down
18 changes: 18 additions & 0 deletions django/utils/datastructures.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,3 +345,21 @@ def _unpack_items(data):
"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]
74 changes: 74 additions & 0 deletions docs/ref/templates/builtins.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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``
Expand Down
Loading
Loading