mirror of
https://github.com/django-components/django-components.git
synced 2025-08-26 17:04:06 +00:00
Patch TemplateProxy to restore compatibility with template_partials (#1328)
Co-authored-by: EmilStenstrom <224130+EmilStenstrom@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Juro Oravec <juraj.oravec.josefson@gmail.com>
This commit is contained in:
parent
e9b7470850
commit
aa14e3698d
8 changed files with 159 additions and 13 deletions
|
@ -1,5 +1,6 @@
|
||||||
django
|
django
|
||||||
djc-core-html-parser
|
djc-core-html-parser
|
||||||
|
django-template-partials
|
||||||
tox
|
tox
|
||||||
pytest
|
pytest
|
||||||
pytest-asyncio
|
pytest-asyncio
|
||||||
|
|
|
@ -19,7 +19,11 @@ class ComponentsConfig(AppConfig):
|
||||||
from django_components.component_registry import registry
|
from django_components.component_registry import registry
|
||||||
from django_components.components.dynamic import DynamicComponent
|
from django_components.components.dynamic import DynamicComponent
|
||||||
from django_components.extension import extensions
|
from django_components.extension import extensions
|
||||||
from django_components.util.django_monkeypatch import monkeypatch_include_node, monkeypatch_template_cls
|
from django_components.util.django_monkeypatch import (
|
||||||
|
monkeypatch_include_node,
|
||||||
|
monkeypatch_template_cls,
|
||||||
|
monkeypatch_template_proxy_cls,
|
||||||
|
)
|
||||||
|
|
||||||
# NOTE: This monkeypatch is applied here, before Django processes any requests.
|
# NOTE: This monkeypatch is applied here, before Django processes any requests.
|
||||||
# To make django-components work with django-debug-toolbar-template-profiler
|
# To make django-components work with django-debug-toolbar-template-profiler
|
||||||
|
@ -27,6 +31,10 @@ class ComponentsConfig(AppConfig):
|
||||||
monkeypatch_template_cls(Template)
|
monkeypatch_template_cls(Template)
|
||||||
monkeypatch_include_node(IncludeNode)
|
monkeypatch_include_node(IncludeNode)
|
||||||
|
|
||||||
|
# This makes django-components work with django-template-partials
|
||||||
|
# NOTE: Delete when Django 5.2 reaches end of life
|
||||||
|
monkeypatch_template_proxy_cls()
|
||||||
|
|
||||||
# Import modules set in `COMPONENTS.libraries` setting
|
# Import modules set in `COMPONENTS.libraries` setting
|
||||||
import_libraries()
|
import_libraries()
|
||||||
|
|
||||||
|
|
|
@ -3250,7 +3250,7 @@ class Component(metaclass=ComponentMeta):
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
""" # noqa: 501
|
""" # noqa: E501
|
||||||
|
|
||||||
# TODO_v1 - Remove, superseded by `deps_strategy`
|
# TODO_v1 - Remove, superseded by `deps_strategy`
|
||||||
if type is not None:
|
if type is not None:
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
from typing import Any, Optional, Type
|
from typing import Any, Optional, Type
|
||||||
|
|
||||||
|
from django import VERSION as DJANGO_VERSION
|
||||||
from django.template import Context, NodeList, Template
|
from django.template import Context, NodeList, Template
|
||||||
from django.template.base import Node, Origin, Parser
|
from django.template.base import Node, Origin, Parser
|
||||||
from django.template.loader_tags import IncludeNode
|
from django.template.loader_tags import IncludeNode
|
||||||
|
@ -117,8 +118,12 @@ def monkeypatch_template_compile_nodelist(template_cls: Type[Template]) -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# ---------------- ADDED IN Django v5.1 - See https://github.com/django/django/commit/35bbb2c9c01882b1d77b0b8c737ac646144833d4 # noqa: E501
|
|
||||||
nodelist = parser.parse()
|
nodelist = parser.parse()
|
||||||
|
if DJANGO_VERSION >= (5, 1):
|
||||||
|
# ---------------- ADDED IN Django v5.1 - See https://github.com/django/django/commit/35bbb2c9c01882b1d77b0b8c737ac646144833d4 # noqa: E501
|
||||||
|
# NOTE: This must be enabled ONLY for 5.1 and later. Previously it was also for older
|
||||||
|
# Django versions, and this led to compatibility issue with django-template-partials.
|
||||||
|
# See https://github.com/carltongibson/django-template-partials/pull/85#issuecomment-3187354790
|
||||||
self.extra_data = getattr(parser, "extra_data", {})
|
self.extra_data = getattr(parser, "extra_data", {})
|
||||||
# ---------------- END OF ADDED IN Django v5.1 ----------------
|
# ---------------- END OF ADDED IN Django v5.1 ----------------
|
||||||
return nodelist
|
return nodelist
|
||||||
|
@ -243,5 +248,33 @@ def monkeypatch_include_render(include_node_cls: Type[Node]) -> None:
|
||||||
include_node_cls.render = _include_render
|
include_node_cls.render = _include_render
|
||||||
|
|
||||||
|
|
||||||
|
# NOTE: Remove once Django v5.2 reaches end of life
|
||||||
|
# See https://github.com/django-components/django-components/issues/1323#issuecomment-3163478287
|
||||||
|
def monkeypatch_template_proxy_cls() -> None:
|
||||||
|
# Patch TemplateProxy if template_partials is installed
|
||||||
|
# See https://github.com/django-components/django-components/issues/1323#issuecomment-3164224042
|
||||||
|
try:
|
||||||
|
from template_partials.templatetags.partials import TemplateProxy
|
||||||
|
except ImportError:
|
||||||
|
# template_partials is in INSTALLED_APPS but not actually installed
|
||||||
|
# This is fine, just skip the patching
|
||||||
|
return
|
||||||
|
|
||||||
|
if is_cls_patched(TemplateProxy):
|
||||||
|
return
|
||||||
|
|
||||||
|
monkeypatch_template_proxy_render(TemplateProxy)
|
||||||
|
TemplateProxy._djc_patched = True
|
||||||
|
|
||||||
|
|
||||||
|
def monkeypatch_template_proxy_render(template_proxy_cls: Type[Any]) -> None:
|
||||||
|
# NOTE: TemplateProxy.render() is same logic as Template.render(), just duplicated.
|
||||||
|
# So we can instead reuse Template.render()
|
||||||
|
def _template_proxy_render(self: Any, context: Context, *args: Any, **kwargs: Any) -> str:
|
||||||
|
return Template.render(self, context)
|
||||||
|
|
||||||
|
template_proxy_cls.render = _template_proxy_render
|
||||||
|
|
||||||
|
|
||||||
def is_cls_patched(cls: Type[Any]) -> bool:
|
def is_cls_patched(cls: Type[Any]) -> bool:
|
||||||
return getattr(cls, "_djc_patched", False)
|
return getattr(cls, "_djc_patched", False)
|
||||||
|
|
13
tests/templates/integration_template_partials.html
Normal file
13
tests/templates/integration_template_partials.html
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<!-- TODO: Delete after Django 5.2 reaches end of life -->
|
||||||
|
{% load component_tags %}
|
||||||
|
{% load partials %}
|
||||||
|
|
||||||
|
{% partialdef test___partial inline %}
|
||||||
|
|
||||||
|
{% component_css_dependencies %}
|
||||||
|
|
||||||
|
{% component "calendar" date=date / %}
|
||||||
|
|
||||||
|
{% component_js_dependencies %}
|
||||||
|
|
||||||
|
{% endpartialdef %}
|
84
tests/test_integration_template_partials.py
Normal file
84
tests/test_integration_template_partials.py
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
"""
|
||||||
|
Tests for template_partials integration with django-components.
|
||||||
|
|
||||||
|
See https://github.com/django-components/django-components/issues/1327
|
||||||
|
and https://github.com/django-components/django-components/issues/1323.
|
||||||
|
|
||||||
|
This file can be deleted after Django 5.2 reached end of life.
|
||||||
|
See https://github.com/django-components/django-components/issues/1323#issuecomment-3163478287.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import NamedTuple
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from django.shortcuts import render
|
||||||
|
from django.http import HttpRequest
|
||||||
|
|
||||||
|
from django_components import Component, register
|
||||||
|
from django_components.testing import djc_test
|
||||||
|
from .testutils import setup_test_config
|
||||||
|
|
||||||
|
try:
|
||||||
|
from template_partials.templatetags.partials import TemplateProxy
|
||||||
|
except ImportError:
|
||||||
|
TemplateProxy = None
|
||||||
|
|
||||||
|
|
||||||
|
setup_test_config(components={"autodiscover": False})
|
||||||
|
|
||||||
|
|
||||||
|
# See https://github.com/django-components/django-components/issues/1323#issuecomment-3156654329
|
||||||
|
@djc_test(
|
||||||
|
django_settings={"INSTALLED_APPS": ("template_partials", "django_components", "tests.test_app")}
|
||||||
|
)
|
||||||
|
class TestTemplatePartialsIntegration:
|
||||||
|
@pytest.mark.skipif(TemplateProxy is None, reason="template_partials not available")
|
||||||
|
def test_render_partial(self):
|
||||||
|
@register("calendar")
|
||||||
|
class Calendar(Component):
|
||||||
|
template = """
|
||||||
|
<div class="calendar-component">
|
||||||
|
<div>Today's date is <span>{{ date }}</span></div>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
css = """
|
||||||
|
.calendar-component { width: 200px; background: pink; }
|
||||||
|
.calendar-component span { font-weight: bold; }
|
||||||
|
"""
|
||||||
|
js = """
|
||||||
|
(function(){
|
||||||
|
if (document.querySelector(".calendar-component")) {
|
||||||
|
document.querySelector(".calendar-component").onclick = function(){ alert("Clicked calendar!"); };
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
""" # noqa: E501
|
||||||
|
|
||||||
|
class Kwargs(NamedTuple):
|
||||||
|
date: str
|
||||||
|
|
||||||
|
def get_template_data(self, args, kwargs: Kwargs, slots, context):
|
||||||
|
return {
|
||||||
|
"date": kwargs.date,
|
||||||
|
}
|
||||||
|
|
||||||
|
# NOTE: When a full template is rendered (without the `#` syntax), the output should be as usual.
|
||||||
|
request = HttpRequest()
|
||||||
|
result = render(request, "integration_template_partials.html")
|
||||||
|
content = result.content
|
||||||
|
|
||||||
|
assert b"<!-- _RENDERED" not in content
|
||||||
|
assert b"width: 200px;" in content
|
||||||
|
assert b'alert("Clicked calendar!")' in content
|
||||||
|
|
||||||
|
# NOTE: When a partial is rendered with the `#` syntax, what *actually*
|
||||||
|
# gets rendered is `TemplateProxy` from template_partials, instead of Django's own `Template` class.
|
||||||
|
# Hence, the monkeypatching that we've done on the Template class does NOT apply to TemplateProxy.
|
||||||
|
# So we want to check that the result HAS its CSS/JS processed, which means that the monkeypatching
|
||||||
|
# works as expected.
|
||||||
|
request2 = HttpRequest()
|
||||||
|
result2 = render(request2, "integration_template_partials.html#test___partial")
|
||||||
|
content2 = result2.content
|
||||||
|
|
||||||
|
assert b"<!-- _RENDERED" not in content2
|
||||||
|
assert b"width: 200px;" in content2
|
||||||
|
assert b'alert("Clicked calendar!")' in content2
|
|
@ -581,16 +581,18 @@ class TestExtendsCompat:
|
||||||
# NOTE: It's important that the <script> tags are rendered outside of <div> and <outer> tags,
|
# NOTE: It's important that the <script> tags are rendered outside of <div> and <outer> tags,
|
||||||
# because that tells us that the JS/CSS is rendered by the parent template, not the component
|
# because that tells us that the JS/CSS is rendered by the parent template, not the component
|
||||||
# inside the include.
|
# inside the include.
|
||||||
expected = """
|
# NOTE 2: The IDs differ when rendered as part of whole test suite vs as a single test.
|
||||||
|
comp_id = "ca1bc41" if "ca1bc41" in rendered else "ca1bc40"
|
||||||
|
expected = f"""
|
||||||
<body>
|
<body>
|
||||||
<outer>
|
<outer>
|
||||||
<div data-djc-id-ca1bc41>Hello</div>
|
<div data-djc-id-{comp_id}>Hello</div>
|
||||||
</outer>
|
</outer>
|
||||||
<script src="django_components/django_components.min.js"></script>
|
<script src="django_components/django_components.min.js"></script>
|
||||||
<script type="application/json" data-djc>{"loadedCssUrls": ["c3R5bGUuY3Nz"],
|
<script type="application/json" data-djc>{{"loadedCssUrls": ["c3R5bGUuY3Nz"],
|
||||||
"loadedJsUrls": ["c2NyaXB0Lmpz"],
|
"loadedJsUrls": ["c2NyaXB0Lmpz"],
|
||||||
"toLoadCssTags": [],
|
"toLoadCssTags": [],
|
||||||
"toLoadJsTags": []}</script>
|
"toLoadJsTags": []}}</script>
|
||||||
<script src="script.js"></script>
|
<script src="script.js"></script>
|
||||||
</body>
|
</body>
|
||||||
"""
|
"""
|
||||||
|
@ -636,17 +638,20 @@ class TestExtendsCompat:
|
||||||
template_obj = Template(template)
|
template_obj = Template(template)
|
||||||
context = Context()
|
context = Context()
|
||||||
rendered = template_obj.render(context)
|
rendered = template_obj.render(context)
|
||||||
expected = """
|
|
||||||
|
# NOTE: The IDs differ when rendered as part of whole test suite vs as a single test.
|
||||||
|
comp_id = "ca1bc45" if "ca1bc45" in rendered else "ca1bc44"
|
||||||
|
expected = f"""
|
||||||
<html>
|
<html>
|
||||||
<body data-djc-id-ca1bc43>
|
<body data-djc-id-ca1bc43>
|
||||||
<outer>
|
<outer>
|
||||||
<div data-djc-id-ca1bc45>Hello</div>
|
<div data-djc-id-{comp_id}>Hello</div>
|
||||||
</outer>
|
</outer>
|
||||||
<script src="django_components/django_components.min.js"></script>
|
<script src="django_components/django_components.min.js"></script>
|
||||||
<script type="application/json" data-djc>{"loadedCssUrls": ["c3R5bGUuY3Nz"],
|
<script type="application/json" data-djc>{{"loadedCssUrls": ["c3R5bGUuY3Nz"],
|
||||||
"loadedJsUrls": ["c2NyaXB0Lmpz"],
|
"loadedJsUrls": ["c2NyaXB0Lmpz"],
|
||||||
"toLoadCssTags": [],
|
"toLoadCssTags": [],
|
||||||
"toLoadJsTags": []}</script>
|
"toLoadJsTags": []}}</script>
|
||||||
<script src="script.js"></script>
|
<script src="script.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
2
tox.ini
2
tox.ini
|
@ -44,6 +44,8 @@ deps =
|
||||||
requests
|
requests
|
||||||
types-requests
|
types-requests
|
||||||
whitenoise
|
whitenoise
|
||||||
|
# NOTE: Delete when Django 5.2 reaches end of life
|
||||||
|
django-template-partials
|
||||||
commands = pytest {posargs}
|
commands = pytest {posargs}
|
||||||
|
|
||||||
[testenv:flake8]
|
[testenv:flake8]
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue