1
0
mirror of https://github.com/django/django.git synced 2025-08-23 10:19:13 +00:00
django/tests/template_tests/test_partials.py
farhan 5e06b97095 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>
2025-08-14 21:53:14 -03:00

414 lines
15 KiB
Python

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 %}"
)