From 9be4124339e8147cd4342b0c6163cf18e4aba313 Mon Sep 17 00:00:00 2001 From: Juro Oravec Date: Fri, 15 Aug 2025 10:42:25 +0200 Subject: [PATCH] fix: fix compat with extends and includes (#1344) --- CHANGELOG.md | 7 +++ pyproject.toml | 2 +- src/django_components/slots.py | 29 ++++++++++- tests/templates/extends_compat_c_include.html | 4 ++ .../templates/extends_compat_d_extended.html | 3 ++ tests/test_templatetags_extends.py | 48 ++++++++++++++++++- 6 files changed, 88 insertions(+), 5 deletions(-) create mode 100644 tests/templates/extends_compat_c_include.html create mode 100644 tests/templates/extends_compat_d_extended.html diff --git a/CHANGELOG.md b/CHANGELOG.md index 83aebc5b..b60d0b64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Release notes +## v0.141.4 + +#### Fix + +- Fix compatibility with Django's `{% include %}` and `{% extends %}` tags. + See https://github.com/django-components/django-components/issues/1325 + ## v0.141.3 #### Feat diff --git a/pyproject.toml b/pyproject.toml index d47f0240..17f10a24 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "django_components" -version = "0.141.3" +version = "0.141.4" requires-python = ">=3.8, <4.0" description = "A way to create simple reusable template components in Django." keywords = ["django", "components", "css", "js", "html"] diff --git a/src/django_components/slots.py b/src/django_components/slots.py index 69c60b10..060ce866 100644 --- a/src/django_components/slots.py +++ b/src/django_components/slots.py @@ -1,11 +1,13 @@ import difflib import re +from contextlib import contextmanager from dataclasses import dataclass, field from dataclasses import replace as dataclass_replace from typing import ( TYPE_CHECKING, Any, Dict, + Generator, Generic, List, Literal, @@ -1460,8 +1462,10 @@ def _extract_fill_content( # When, during rendering of this tree, we encounter a {% fill %} node, instead of rendering content, # it will add itself into captured_fills, because `FILL_GEN_CONTEXT_KEY` is defined. captured_fills: List[FillWithData] = [] - with context.update({FILL_GEN_CONTEXT_KEY: captured_fills}): - content = mark_safe(nodes.render(context).strip()) + + with _extends_context_reset(context): + with context.update({FILL_GEN_CONTEXT_KEY: captured_fills}): + content = mark_safe(nodes.render(context).strip()) # If we did not encounter any fills (not accounting for those nested in other # {% componenet %} tags), then we treat the content as default slot. @@ -1667,3 +1671,24 @@ def _nodelist_to_slot( def _is_extracting_fill(context: Context) -> bool: return context.get(FILL_GEN_CONTEXT_KEY, None) is not None + + +# Fix for compatibility with Django's `{% include %}` and `{% extends %}` tags. +# See https://github.com/django-components/django-components/issues/1325 +# +# When we search for `{% fill %}` tags, we also evaluate `{% include %}` and `{% extends %}` +# tags if they are within component body (between `{% component %}` / `{% endcomponent %}` tags). +# But by doing so, we trigger Django's block/extends logic to remember that this extended file +# was already walked. +# (See https://github.com/django/django/blob/0bff53b4138d8c6009e9040dbb8916a1271a68d7/django/template/loader_tags.py#L114) # noqa: E501 +# +# We need to clear that state, otherwise Django won't render the extended template the second time +# (when we actually render it). +@contextmanager +def _extends_context_reset(context: Context) -> Generator[None, None, None]: + b4_ctx_extends = context.render_context.setdefault("extends_context", []).copy() + + yield + + # Reset the state of what extends have been seen. + context.render_context["extends_context"] = b4_ctx_extends diff --git a/tests/templates/extends_compat_c_include.html b/tests/templates/extends_compat_c_include.html new file mode 100644 index 00000000..c56c1ed8 --- /dev/null +++ b/tests/templates/extends_compat_c_include.html @@ -0,0 +1,4 @@ +{% extends "extends_compat_d_extended.html" %} +{% block content %} +

This template extends another template.

+{% endblock %} \ No newline at end of file diff --git a/tests/templates/extends_compat_d_extended.html b/tests/templates/extends_compat_d_extended.html new file mode 100644 index 00000000..11d3b390 --- /dev/null +++ b/tests/templates/extends_compat_d_extended.html @@ -0,0 +1,3 @@ +

This template gets extended.

+{% block content %} +{% endblock %} diff --git a/tests/test_templatetags_extends.py b/tests/test_templatetags_extends.py index b6f99948..a935f221 100644 --- a/tests/test_templatetags_extends.py +++ b/tests/test_templatetags_extends.py @@ -145,7 +145,9 @@ class TestExtendsCompat: assertHTMLEqual(rendered, expected) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR) - def test_double_extends_on_main_template_and_component_two_different_components_same_parent(self, components_settings): # noqa: E501 + def test_double_extends_on_main_template_and_component_two_different_components_same_parent( + self, components_settings + ): registry.register("blocked_and_slotted_component", gen_blocked_and_slotted_component()) @register("extended_component") @@ -211,7 +213,9 @@ class TestExtendsCompat: assertHTMLEqual(rendered, expected) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR) - def test_double_extends_on_main_template_and_component_two_different_components_different_parent(self, components_settings): # noqa: E501 + def test_double_extends_on_main_template_and_component_two_different_components_different_parent( + self, components_settings + ): registry.register("blocked_and_slotted_component", gen_blocked_and_slotted_component()) @register("extended_component") @@ -1029,3 +1033,43 @@ class TestExtendsCompat: """ assertHTMLEqual(rendered, expected) + + # Fix for compatibility with Django's `{% include %}` and `{% extends %}` tags. + # See https://github.com/django-components/django-components/issues/1325 + @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR) + def test_nested_component_with_include_and_extends_in_slot(self, components_settings): + @register("a_outer") + class AOuterComponent(Component): + template: types.django_html = """ + {% load component_tags %} +

This is the outer component.

+ {% slot "a" default / %} + """ + + @register("b_inner") + class BInnerComponent(Component): + template: types.django_html = """ + {% load component_tags %} +

This is the inner component.

+ {% slot "b" default / %} + """ + + template: types.django_html = """ + {% load component_tags %} + {% component "a_outer" %} + {% component "b_inner" %} + {% include "extends_compat_c_include.html" %} + {% endcomponent %} + {% endcomponent %} + """ + rendered = Template(template).render(Context({})) + + assertHTMLEqual( + rendered, + """ +

This is the outer component.

+

This is the inner component.

+

This template gets extended.

+

This template extends another template.

+ """, + )