fix: fix compat with extends and includes (#1344)

This commit is contained in:
Juro Oravec 2025-08-15 10:42:25 +02:00 committed by GitHub
parent a6e840bdca
commit 9be4124339
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 88 additions and 5 deletions

View file

@ -1,5 +1,12 @@
# Release notes # 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 ## v0.141.3
#### Feat #### Feat

View file

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "django_components" name = "django_components"
version = "0.141.3" version = "0.141.4"
requires-python = ">=3.8, <4.0" requires-python = ">=3.8, <4.0"
description = "A way to create simple reusable template components in Django." description = "A way to create simple reusable template components in Django."
keywords = ["django", "components", "css", "js", "html"] keywords = ["django", "components", "css", "js", "html"]

View file

@ -1,11 +1,13 @@
import difflib import difflib
import re import re
from contextlib import contextmanager
from dataclasses import dataclass, field from dataclasses import dataclass, field
from dataclasses import replace as dataclass_replace from dataclasses import replace as dataclass_replace
from typing import ( from typing import (
TYPE_CHECKING, TYPE_CHECKING,
Any, Any,
Dict, Dict,
Generator,
Generic, Generic,
List, List,
Literal, Literal,
@ -1460,8 +1462,10 @@ def _extract_fill_content(
# When, during rendering of this tree, we encounter a {% fill %} node, instead of rendering 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. # it will add itself into captured_fills, because `FILL_GEN_CONTEXT_KEY` is defined.
captured_fills: List[FillWithData] = [] 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 # If we did not encounter any fills (not accounting for those nested in other
# {% componenet %} tags), then we treat the content as default slot. # {% 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: def _is_extracting_fill(context: Context) -> bool:
return context.get(FILL_GEN_CONTEXT_KEY, None) is not None 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

View file

@ -0,0 +1,4 @@
{% extends "extends_compat_d_extended.html" %}
{% block content %}
<p>This template extends another template.</p>
{% endblock %}

View file

@ -0,0 +1,3 @@
<p>This template gets extended.</p>
{% block content %}
{% endblock %}

View file

@ -145,7 +145,9 @@ class TestExtendsCompat:
assertHTMLEqual(rendered, expected) assertHTMLEqual(rendered, expected)
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR) @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()) registry.register("blocked_and_slotted_component", gen_blocked_and_slotted_component())
@register("extended_component") @register("extended_component")
@ -211,7 +213,9 @@ class TestExtendsCompat:
assertHTMLEqual(rendered, expected) assertHTMLEqual(rendered, expected)
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR) @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()) registry.register("blocked_and_slotted_component", gen_blocked_and_slotted_component())
@register("extended_component") @register("extended_component")
@ -1029,3 +1033,43 @@ class TestExtendsCompat:
</html> </html>
""" """
assertHTMLEqual(rendered, expected) 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 %}
<p>This is the outer component.</p>
{% slot "a" default / %}
"""
@register("b_inner")
class BInnerComponent(Component):
template: types.django_html = """
{% load component_tags %}
<p>This is the inner component.</p>
{% 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,
"""
<p data-djc-id-ca1bc40>This is the outer component.</p>
<p data-djc-id-ca1bc40 data-djc-id-ca1bc42>This is the inner component.</p>
<p data-djc-id-ca1bc40 data-djc-id-ca1bc42>This template gets extended.</p>
<p data-djc-id-ca1bc40 data-djc-id-ca1bc42>This template extends another template.</p>
""",
)