diff options
| author | farhan <farhanalirazaazeemi@gmail.com> | 2025-06-03 10:32:34 +0500 |
|---|---|---|
| committer | nessita <124304+nessita@users.noreply.github.com> | 2025-08-14 21:53:14 -0300 |
| commit | 5e06b970956af4854e970e74990cb971ba31c96b (patch) | |
| tree | 59fb79f5e255298625af1ad072ad4026e64f582d /django/template | |
| parent | fda3c1712a1eb7b20dfc91e6c9abae32bd64d081 (diff) | |
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>
Diffstat (limited to 'django/template')
| -rw-r--r-- | django/template/__init__.py | 1 | ||||
| -rw-r--r-- | django/template/base.py | 56 | ||||
| -rw-r--r-- | django/template/defaulttags.py | 96 | ||||
| -rw-r--r-- | django/template/engine.py | 22 |
4 files changed, 174 insertions, 1 deletions
diff --git a/django/template/__init__.py b/django/template/__init__.py index adb431c00d..92568da793 100644 --- a/django/template/__init__.py +++ b/django/template/__init__.py @@ -61,6 +61,7 @@ from .base import ( # NOQA isort:skip Node, NodeList, Origin, + PartialTemplate, Template, Variable, ) diff --git a/django/template/base.py b/django/template/base.py index 3e8a59fbe7..74b3987410 100644 --- a/django/template/base.py +++ b/django/template/base.py @@ -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") diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py index a20598152c..4cbaf852e1 100644 --- a/django/template/defaulttags.py +++ b/django/template/defaulttags.py @@ -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): """ diff --git a/django/template/engine.py b/django/template/engine.py index 9882d3a16d..df5c8316b9 100644 --- a/django/template/engine.py +++ b/django/template/engine.py @@ -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): """ |
