refactor: Assign content of file from Component.template_file to Component.template (#880)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
Juro Oravec 2025-01-07 19:34:34 +01:00 committed by GitHub
parent 8e2428ebd0
commit 1e4b556b4d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 399 additions and 186 deletions

View file

@ -8,8 +8,10 @@
them to their own files, and link the JS/CSS files with `Component.js_file` and `Component.css_file`.
Even when you specify the JS/CSS with `Component.js_file` or `Component.css_file`, then you can still
access the content under `Component.js/css` - behind the scenes, the content of the JS/CSS files
will be set to `Component.js/css` upon first access.
access the content under `Component.js` or `Component.css` - behind the scenes, the content of the JS/CSS files
will be set to `Component.js` / `Component.css` upon first access.
The same applies to `Component.template_file`, which will populate `Component.template` upon first access.
With this change, the role of `Component.js/css` and the JS/CSS in `Component.Media` has changed:
@ -32,7 +34,7 @@
every time you render the component (e.g. with `Component.render()`)
- The new `id` is available only during render, so e.g. from within `get_context_data()`
- Component's HTML / CSS / JS are now resolved and loaded lazily. That is, if you specify `template_name`,
- Component's HTML / CSS / JS are now resolved and loaded lazily. That is, if you specify `template_name`/`template_file`,
`js_file`, `css_file`, or `Media.js/css`, the file paths will be resolved only once you:
1. Try to access component's HTML / CSS / JS, or
@ -40,6 +42,8 @@
Read more on [Accessing component's HTML / JS / CSS](https://EmilStenstrom.github.io/django-components/0.124/concepts/fundamentals/defining_js_css_html_files/#customize-how-paths-are-rendered-into-html-tags).
- The [Signals](https://docs.djangoproject.com/en/5.1/topics/signals/) emitted by or during the use of django-components are now documented, together the `template_rendered` signal.
## v0.123
#### Fix

View file

@ -339,7 +339,7 @@ these file paths will be resolved only once you either:
1. Access any of the following attributes on the component:
- [`media`](../../reference/api.md#django_components.Component.media),
- [`media`](../../reference/api.md#django_components.Component.media),
[`template`](../../reference/api.md#django_components.Component.template),
[`template_file`](../../reference/api.md#django_components.Component.template_file),
[`js`](../../reference/api.md#django_components.Component.js),
@ -398,7 +398,7 @@ print(Calendar.css)
If you need to dynamically change these media files, consider instead defining multiple Components.
Modifying these files AFTER the component has been loaded at best does nothing. However, this is
an untested behavior.
an untested behavior, which may lead to unexpected errors.
## Accessing component's Media files

View file

@ -551,6 +551,22 @@ def gen_reference_templatevars():
f.write(f"::: {ComponentVars.__module__}.{ComponentVars.__name__}.{field}\n\n")
# NOTE: Unlike other references, the API of Signals is not yet codified (AKA source of truth defined
# as Python code). Instead, we manually list all signals that are sent by django-components.
def gen_reference_signals():
"""
Generate documentation for all [Django Signals](https://docs.djangoproject.com/en/5.1/ref/signals) that are
send by or during the use of django-components.
"""
preface = "<!-- Autogenerated by reference.py -->\n\n"
preface += (root / "docs/templates/reference_signals.md").read_text()
out_file = root / "docs/reference/signals.md"
out_file.parent.mkdir(parents=True, exist_ok=True)
with out_file.open("w", encoding="utf-8") as f:
f.write(preface + "\n\n")
def _list_urls(urlpatterns: Sequence[Union[URLPattern, URLResolver]], prefix=""):
"""Recursively extract all URLs and their associated views from Django's urlpatterns"""
urls: List[str] = []
@ -732,6 +748,7 @@ def gen_reference():
gen_reference_commands()
gen_reference_templatetags()
gen_reference_templatevars()
gen_reference_signals()
# This is run when `gen-files` plugin is run in mkdocs.yml

37
docs/templates/reference_signals.md vendored Normal file
View file

@ -0,0 +1,37 @@
# Signals
Below are the signals that are sent by or during the use of django-components.
## template_rendered
Django's [`template_rendered`](https://docs.djangoproject.com/en/5.1/ref/signals/#template-rendered) signal.
This signal is sent when a template is rendered.
Django-components triggers this signal when a component is rendered. If there are nested components,
the signal is triggered for each component.
Import from django as `django.test.signals.template_rendered`.
```python
from django.test.signals import template_rendered
# Setup a callback function
def my_callback(sender, **kwargs):
...
template_rendered.connect(my_callback)
class MyTable(Component):
template = """
<table>
<tr>
<th>Header</th>
</tr>
<tr>
<td>Cell</td>
</tr>
"""
# This will trigger the signal
MyTable().render()
```

View file

@ -31,6 +31,7 @@ from django.template.base import NodeList, Template, TextNode
from django.template.context import Context, RequestContext
from django.template.loader import get_template
from django.template.loader_tags import BLOCK_CONTEXT_KEY
from django.test.signals import template_rendered
from django.utils.html import conditional_escape
from django.views import View
@ -227,8 +228,15 @@ class Component(
"""
Filepath to the Django template associated with this component.
The filepath must be relative to either the file where the component class was defined,
or one of the roots of `STATIFILES_DIRS`.
The filepath must be either:
- Relative to the directory where the Component's Python file is defined.
- Relative to one of the component directories, as set by
[`COMPONENTS.dirs`](../settings.md#django_components.app_settings.ComponentsSettings.dirs)
or
[`COMPONENTS.app_dirs`](../settings.md#django_components.app_settings.ComponentsSettings.app_dirs)
(e.g. `<root>/components/`).
- Relative to the template directories, as set by Django's `TEMPLATES` setting (e.g. `<root>/templates/`).
Only one of [`template_file`](../api#django_components.Component.template_file),
[`get_template_name`](../api#django_components.Component.get_template_name),
@ -327,6 +335,16 @@ class Component(
"""
Main JS associated with this component as file path.
The filepath must be either:
- Relative to the directory where the Component's Python file is defined.
- Relative to one of the component directories, as set by
[`COMPONENTS.dirs`](../settings.md#django_components.app_settings.ComponentsSettings.dirs)
or
[`COMPONENTS.app_dirs`](../settings.md#django_components.app_settings.ComponentsSettings.app_dirs)
(e.g. `<root>/components/`).
- Relative to the staticfiles directories, as set by Django's `STATICFILES_DIRS` setting (e.g. `<root>/static/`).
When you create a Component class with `js_file`, these will happen:
1. If the file path is relative to the directory where the component's Python file is,
@ -374,6 +392,16 @@ class Component(
"""
Main CSS associated with this component as file path.
The filepath must be either:
- Relative to the directory where the Component's Python file is defined.
- Relative to one of the component directories, as set by
[`COMPONENTS.dirs`](../settings.md#django_components.app_settings.ComponentsSettings.dirs)
or
[`COMPONENTS.app_dirs`](../settings.md#django_components.app_settings.ComponentsSettings.app_dirs)
(e.g. `<root>/components/`).
- Relative to the staticfiles directories, as set by Django's `STATICFILES_DIRS` setting (e.g. `<root>/static/`).
When you create a Component class with `css_file`, these will happen:
1. If the file path is relative to the directory where the component's Python file is,
@ -628,50 +656,52 @@ class Component(
return ctx.is_filled
# NOTE: When the template is taken from a file (AKA specified via `template_file`),
# then we leverage Django's template caching. This means that the same instance
# of Template is reused. This is important to keep in mind, because the implication
# is that we should treat Templates AND their nodelists as IMMUTABLE.
# NOTE: We cache the Template instance. When the template is taken from a file
# via `get_template_name`, then we leverage Django's template caching with `get_template()`.
# Otherwise, we use our own `cached_template()` to cache the template.
#
# This is important to keep in mind, because the implication is that we should
# treat Templates AND their nodelists as IMMUTABLE.
def _get_template(self, context: Context) -> Template:
# Resolve template name
template_file = self.template_file
if self.template_file is not None:
if self.get_template_name(context) is not None:
raise ImproperlyConfigured(
"Received non-null value from both 'template_file' and 'get_template_name' in"
f" Component {type(self).__name__}. Only one of the two must be set."
)
else:
template_file = self.get_template_name(context)
template_name = self.get_template_name(context)
# TODO_REMOVE_IN_V1 - Remove `self.get_template_string` in v1
template_getter = getattr(self, "get_template_string", self.get_template)
template_body = template_getter(context)
# Resolve template str
template_input = self.template
if self.template is not None:
if self.get_template(context) is not None:
raise ImproperlyConfigured(
"Received non-null value from both 'template' and 'get_template' in"
f" Component {type(self).__name__}. Only one of the two must be set."
)
else:
# TODO_REMOVE_IN_V1 - Remove `self.get_template_string` in v1
template_getter = getattr(self, "get_template_string", self.get_template)
template_input = template_getter(context)
if template_file is not None and template_input is not None:
# `get_template_name()`, `get_template()`, and `template` are mutually exclusive
#
# Note that `template` and `template_name` are also mutually exclusive, but this
# is checked when lazy-loading the template from `template_name`. So if user specified
# `template_name`, then `template` will be populated with the content of that file.
if self.template is not None and template_name is not None:
raise ImproperlyConfigured(
f"Received both 'template_file' and 'template' in Component {type(self).__name__}."
" Only one of the two must be set."
"Received non-null value from both 'template/template_name' and 'get_template_name' in"
f" Component {type(self).__name__}. Only one of the two must be set."
)
if self.template is not None and template_body is not None:
raise ImproperlyConfigured(
"Received non-null value from both 'template/template_name' and 'get_template' in"
f" Component {type(self).__name__}. Only one of the two must be set."
)
if template_name is not None and template_body is not None:
raise ImproperlyConfigured(
"Received non-null value from both 'get_template_name' and 'get_template' in"
f" Component {type(self).__name__}. Only one of the two must be set."
)
if template_file is not None:
return get_template(template_file).template
if template_name is not None:
return get_template(template_name).template
elif template_input is not None:
template_body = template_body if template_body is not None else self.template
if template_body is not None:
# We got template string, so we convert it to Template
if isinstance(template_input, str):
template: Template = cached_template(template_input)
if isinstance(template_body, str):
template: Template = cached_template(
template_string=template_body,
name=self.template_file,
)
else:
template = template_input
template = template_body
return template
@ -1022,6 +1052,8 @@ class Component(
):
self.on_render_before(context, template)
# Emit signal that the template is about to be rendered
template_rendered.send(sender=self, template=self, context=context)
# Get the component's HTML
html_content = template.render(context)

View file

@ -2,11 +2,13 @@ import os
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Protocol, Tuple, Type, Union, cast
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Literal, Optional, Protocol, Tuple, Type, Union, cast
from django.contrib.staticfiles import finders
from django.core.exceptions import ImproperlyConfigured
from django.forms.widgets import Media as MediaCls
from django.template import Template, TemplateDoesNotExist
from django.template.loader import get_template
from django.utils.safestring import SafeData
from django_components.util.loader import get_component_dirs, resolve_file
@ -377,14 +379,24 @@ def _resolve_media(comp_cls: Type["Component"], comp_media: ComponentMedia) -> N
# as relative to the directory where the component class is defined.
_resolve_component_relative_files(comp_cls, comp_media, comp_dirs=comp_dirs)
# If the component defined `js_file` or `css_file`, instead of `js`/`css` resolve them now.
# Effectively, even if the Component class defined `js_file`, at "runtime" the `js` attribute
# If the component defined `template_file`, `js_file` or `css_file`, instead of `template`/`js`/`css`,
# we resolve them now.
# Effectively, even if the Component class defined `js_file` (or others), at "runtime" the `js` attribute
# will be set to the content of the file.
comp_media.js = _get_static_asset(
comp_cls, comp_media, inlined_attr="js", file_attr="js_file", comp_dirs=comp_dirs
# So users can access `Component.js` even if they defined `Component.js_file`.
comp_media.template = _get_asset(
comp_cls,
comp_media,
inlined_attr="template",
file_attr="template_file",
comp_dirs=comp_dirs,
type="template",
)
comp_media.css = _get_static_asset(
comp_cls, comp_media, inlined_attr="css", file_attr="css_file", comp_dirs=comp_dirs
comp_media.js = _get_asset(
comp_cls, comp_media, inlined_attr="js", file_attr="js_file", comp_dirs=comp_dirs, type="static"
)
comp_media.css = _get_asset(
comp_cls, comp_media, inlined_attr="css", file_attr="css_file", comp_dirs=comp_dirs, type="static"
)
media_cls = comp_media.media_class or MediaCls
@ -656,12 +668,13 @@ def _get_dir_path_from_component_path(
return comp_dir_path_abs, comp_dir_path_rel
def _get_static_asset(
def _get_asset(
comp_cls: Type["Component"],
comp_media: ComponentMedia,
inlined_attr: str,
file_attr: str,
comp_dirs: List[Path],
type: Literal["template", "static"],
) -> Optional[str]:
"""
In case of Component's JS or CSS, one can either define that as "inlined" or as a file.
@ -698,9 +711,18 @@ def _get_static_asset(
if asset_file is not None:
# Check if the file is in one of the components' directories
full_path = resolve_file(asset_file, comp_dirs)
# If not, check if it's in the static files
if full_path is None:
full_path = finders.find(asset_file)
# If not, check if it's in the static files
if type == "static":
full_path = finders.find(asset_file)
# Or in the templates
elif type == "template":
try:
template: Template = get_template(asset_file)
full_path = template.origin.name
except TemplateDoesNotExist:
pass
if full_path is None:
# NOTE: The short name, e.g. `js` or `css` is used in the error message for convenience

View file

@ -0,0 +1 @@
Variable: <strong>{{ variable }}</strong>

View file

@ -20,29 +20,7 @@ setup_test_config({"autodiscover": False})
# "Main media" refer to the HTML, JS, and CSS set on the Component class itself
# (as opposed via the `Media` class). These have special handling in the Component.
class MainMediaTest(BaseTestCase):
def test_html_inlined(self):
class InlineHTMLComponent(Component):
template = "<div class='inline'>Hello Inline</div>"
self.assertHTMLEqual(
InlineHTMLComponent.render(),
'<div class="inline" data-djc-id-a1bc3e>Hello Inline</div>',
)
def test_html_filepath(self):
class Test(Component):
template_file = "simple_template.html"
rendered = Test.render(context={"variable": "test"})
self.assertHTMLEqual(
rendered,
"""
Variable: <strong data-djc-id-a1bc3e>test</strong>
""",
)
def test_js_css_inlined(self):
def test_html_js_css_inlined(self):
class TestComponent(Component):
template = """
{% load component_tags %}
@ -77,22 +55,37 @@ class MainMediaTest(BaseTestCase):
rendered,
)
# Check that the HTML / JS / CSS can be accessed on the component class
self.assertEqual(
TestComponent.template,
"""
{% load component_tags %}
{% component_js_dependencies %}
{% component_css_dependencies %}
<div class='html-css-only'>Content</div>
""",
)
self.assertEqual(
TestComponent.css,
".html-css-only { color: blue; }",
)
self.assertEqual(
TestComponent.js,
"console.log('HTML and JS only');",
)
@override_settings(
STATICFILES_DIRS=[
os.path.join(Path(__file__).resolve().parent, "static_root"),
],
)
def test_js_css_filepath_rel_to_component(self):
def test_html_js_css_filepath_rel_to_component(self):
from tests.test_app.components.app_lvl_comp.app_lvl_comp import AppLvlCompComponent
class TestComponent(AppLvlCompComponent):
template_file = None
template = """
{% load component_tags %}
{% component_js_dependencies %}
{% component_css_dependencies %}
<div class='html-css-only'>Content</div>
"""
pass
registry.register("test", TestComponent)
self.assertIn(
".html-css-only {\n color: blue;\n}",
@ -103,10 +96,23 @@ class MainMediaTest(BaseTestCase):
TestComponent.js,
)
rendered = TestComponent.render(kwargs={"variable": "test"})
rendered_raw = Template(
"""
{% load component_tags %}
{% component_js_dependencies %}
{% component_css_dependencies %}
{% component "test" variable="test" / %}
"""
).render(Context())
rendered = render_dependencies(rendered_raw)
self.assertInHTML(
'<div class="html-css-only" data-djc-id-a1bc3e>Content</div>',
"""
<form data-djc-id-a1bc41 method="post">
<input name="variable" type="text" value="test"/>
<input type="submit"/>
</form>
""",
rendered,
)
self.assertInHTML(
@ -118,22 +124,46 @@ class MainMediaTest(BaseTestCase):
rendered,
)
# Check that the HTML / JS / CSS can be accessed on the component class
self.assertEqual(
TestComponent.template,
(
'<form method="post">\n'
" {% csrf_token %}\n"
' <input type="text" name="variable" value="{{ variable }}">\n'
' <input type="submit">\n'
"</form>\n"
),
)
self.assertEqual(TestComponent.css, ".html-css-only {\n" " color: blue;\n" "}\n")
self.assertEqual(
TestComponent.js,
'console.log("JS file");\n',
)
@override_settings(
STATICFILES_DIRS=[
os.path.join(Path(__file__).resolve().parent, "static_root"),
],
)
def test_js_css_filepath_from_static(self):
def test_html_js_css_filepath_from_static(self):
class TestComponent(Component):
template = """
{% load component_tags %}
{% component_js_dependencies %}
{% component_css_dependencies %}
<div class='html-css-only'>Content</div>
"""
template_file = "test_app_simple_template.html"
css_file = "style.css"
js_file = "script.js"
def get_context_data(self, variable):
return {
"variable": variable,
}
registry.register("test", TestComponent)
self.assertIn(
"Variable: <strong>{{ variable }}</strong>",
TestComponent.template,
)
self.assertIn(
".html-css-only {\n color: blue;\n}",
TestComponent.css,
@ -143,10 +173,18 @@ class MainMediaTest(BaseTestCase):
TestComponent.js,
)
rendered = TestComponent.render()
rendered_raw = Template(
"""
{% load component_tags %}
{% component_js_dependencies %}
{% component_css_dependencies %}
{% component "test" variable="test" / %}
"""
).render(Context())
rendered = render_dependencies(rendered_raw)
self.assertInHTML(
'<div class="html-css-only" data-djc-id-a1bc3e>Content</div>',
self.assertIn(
"Variable: <strong data-djc-id-a1bc41>test</strong>",
rendered,
)
self.assertInHTML(
@ -158,22 +196,38 @@ class MainMediaTest(BaseTestCase):
rendered,
)
# Check that the HTML / JS / CSS can be accessed on the component class
self.assertEqual(
TestComponent.template,
"Variable: <strong>{{ variable }}</strong>\n",
)
self.assertEqual(
TestComponent.css,
(
"/* Used in `MainMediaTest` tests in `test_component_media.py` */\n"
".html-css-only {\n"
" color: blue;\n"
"}"
),
)
self.assertEqual(
TestComponent.js,
(
"/* Used in `MainMediaTest` tests in `test_component_media.py` */\n"
'console.log("HTML and JS only");\n'
),
)
@override_settings(
STATICFILES_DIRS=[
os.path.join(Path(__file__).resolve().parent, "static_root"),
],
)
def test_js_css_filepath_lazy_loaded(self):
def test_html_js_css_filepath_lazy_loaded(self):
from tests.test_app.components.app_lvl_comp.app_lvl_comp import AppLvlCompComponent
class TestComponent(AppLvlCompComponent):
template_file = None
template = """
{% load component_tags %}
{% component_js_dependencies %}
{% component_css_dependencies %}
<div class='html-css-only'>Content</div>
"""
pass
# NOTE: Since this is a subclass, actual CSS is defined on the parent class, and thus
# the corresponding ComponentMedia instance is also on the parent class.
@ -189,15 +243,40 @@ class MainMediaTest(BaseTestCase):
# Access the property to load the CSS
_ = TestComponent.css
self.assertHTMLEqual(
self.assertEqual(
AppLvlCompComponent._component_media.css, # type: ignore[attr-defined]
".html-css-only { color: blue; }",
(".html-css-only {\n" " color: blue;\n" "}\n"),
)
self.assertEqual(
AppLvlCompComponent._component_media.css_file, # type: ignore[attr-defined]
"app_lvl_comp/app_lvl_comp.css",
)
# Also check JS and HTML while we're at it
self.assertEqual(
AppLvlCompComponent._component_media.template, # type: ignore[attr-defined]
(
'<form method="post">\n'
" {% csrf_token %}\n"
' <input type="text" name="variable" value="{{ variable }}">\n'
' <input type="submit">\n'
"</form>\n"
),
)
self.assertEqual(
AppLvlCompComponent._component_media.template_file, # type: ignore[attr-defined]
"app_lvl_comp/app_lvl_comp.html",
)
self.assertEqual(
AppLvlCompComponent._component_media.js, # type: ignore[attr-defined]
'console.log("JS file");\n',
)
self.assertEqual(
AppLvlCompComponent._component_media.js_file, # type: ignore[attr-defined]
"app_lvl_comp/app_lvl_comp.js",
)
def test_html_variable(self):
class VariableHTMLComponent(Component):
def get_template(self, context):

88
tests/test_signals.py Normal file
View file

@ -0,0 +1,88 @@
from typing import Callable
from django.template import Context, Template
from django_components import Component, register, registry, types
from .django_test_setup import setup_test_config
from .testutils import BaseTestCase, parametrize_context_behavior
setup_test_config({"autodiscover": False})
class SlottedComponent(Component):
template_file = "slotted_template.html"
def _get_templates_used_to_render(subject_template, render_context=None):
"""Emulate django.test.client.Client (see request method)."""
from django.test.signals import template_rendered
templates_used = []
def receive_template_signal(sender, template, context, **_kwargs):
templates_used.append(template.name)
template_rendered.connect(receive_template_signal, dispatch_uid="test_method")
subject_template.render(render_context or Context({}))
template_rendered.disconnect(dispatch_uid="test_method")
return templates_used
class TemplateSignalTest(BaseTestCase):
saved_render_method: Callable # Assigned during setup.
def tearDown(self):
super().tearDown()
Template._render = self.saved_render_method
def setUp(self):
"""Emulate Django test instrumentation for TestCase (see setup_test_environment)"""
super().setUp()
from django.test.utils import instrumented_test_render
self.saved_render_method = Template._render
Template._render = instrumented_test_render
registry.clear()
registry.register("test_component", SlottedComponent)
@register("inner_component")
class SimpleComponent(Component):
template_file = "simple_template.html"
def get_context_data(self, variable, variable2="default"):
return {
"variable": variable,
"variable2": variable2,
}
class Media:
css = "style.css"
js = "script.js"
@parametrize_context_behavior(["django", "isolated"])
def test_template_rendered(self):
template_str: types.django_html = """
{% load component_tags %}
{% component 'test_component' %}{% endcomponent %}
"""
template = Template(template_str, name="root")
templates_used = _get_templates_used_to_render(template)
self.assertIn("slotted_template.html", templates_used)
@parametrize_context_behavior(["django", "isolated"])
def test_template_rendered_nested_components(self):
template_str: types.django_html = """
{% load component_tags %}
{% component 'test_component' %}
{% fill "header" %}
{% component 'inner_component' variable='foo' %}{% endcomponent %}
{% endfill %}
{% endcomponent %}
"""
template = Template(template_str, name="root")
templates_used = _get_templates_used_to_render(template)
self.assertIn("slotted_template.html", templates_used)
self.assertIn("simple_template.html", templates_used)

View file

@ -1,7 +1,5 @@
"""Catch-all for tests that use template tags and don't fit other files"""
from typing import Callable
from django.template import Context, Template
from django_components import Component, register, registry, types
@ -21,79 +19,6 @@ class SlottedComponent(Component):
#######################
class TemplateInstrumentationTest(BaseTestCase):
saved_render_method: Callable # Assigned during setup.
def tearDown(self):
super().tearDown()
Template._render = self.saved_render_method
def setUp(self):
"""Emulate Django test instrumentation for TestCase (see setup_test_environment)"""
super().setUp()
from django.test.utils import instrumented_test_render
self.saved_render_method = Template._render
Template._render = instrumented_test_render
registry.clear()
registry.register("test_component", SlottedComponent)
@register("inner_component")
class SimpleComponent(Component):
template_file = "simple_template.html"
def get_context_data(self, variable, variable2="default"):
return {
"variable": variable,
"variable2": variable2,
}
class Media:
css = "style.css"
js = "script.js"
def templates_used_to_render(self, subject_template, render_context=None):
"""Emulate django.test.client.Client (see request method)."""
from django.test.signals import template_rendered
templates_used = []
def receive_template_signal(sender, template, context, **_kwargs):
templates_used.append(template.name)
template_rendered.connect(receive_template_signal, dispatch_uid="test_method")
subject_template.render(render_context or Context({}))
template_rendered.disconnect(dispatch_uid="test_method")
return templates_used
@parametrize_context_behavior(["django", "isolated"])
def test_template_shown_as_used(self):
template_str: types.django_html = """
{% load component_tags %}
{% component 'test_component' %}{% endcomponent %}
"""
template = Template(template_str, name="root")
templates_used = self.templates_used_to_render(template)
self.assertIn("slotted_template.html", templates_used)
@parametrize_context_behavior(["django", "isolated"])
def test_nested_component_templates_all_shown_as_used(self):
template_str: types.django_html = """
{% load component_tags %}
{% component 'test_component' %}
{% fill "header" %}
{% component 'inner_component' variable='foo' %}{% endcomponent %}
{% endfill %}
{% endcomponent %}
"""
template = Template(template_str, name="root")
templates_used = self.templates_used_to_render(template)
self.assertIn("slotted_template.html", templates_used)
self.assertIn("simple_template.html", templates_used)
class MultilineTagsTests(BaseTestCase):
@parametrize_context_behavior(["django", "isolated"])
def test_multiline_tags(self):

View file

@ -581,8 +581,16 @@ class MultiComponentTests(BaseTestCase):
""",
)
@parametrize_context_behavior(["django", "isolated"])
def test_both_components_render_correctly_when_only_first_has_slots(self):
@parametrize_context_behavior(
# TODO: Why is this the only place where this needs to be parametrized?
cases=[
("django", "data-djc-id-a1bc48"),
("isolated", "data-djc-id-a1bc45"),
]
)
def test_both_components_render_correctly_when_only_first_has_slots(self, context_behavior_data):
second_id = context_behavior_data
registry.register("first_component", SlottedComponent)
registry.register("second_component", SlottedComponentWithContext)
@ -599,7 +607,7 @@ class MultiComponentTests(BaseTestCase):
self.assertHTMLEqual(
rendered,
"""
f"""
<custom-template data-djc-id-a1bc41>
<header>
<p>Slot #1</p>
@ -607,7 +615,7 @@ class MultiComponentTests(BaseTestCase):
<main>Default main</main>
<footer>Default footer</footer>
</custom-template>
<custom-template data-djc-id-a1bc45>
<custom-template {second_id}>
<header>
Default header
</header>

View file

@ -638,7 +638,7 @@ class ExtendsCompatTests(BaseTestCase):
<!DOCTYPE html>
<html data-djc-id-a1bc40 lang="en">
<body>
<custom-template data-djc-id-a1bc45>
<custom-template data-djc-id-a1bc49>
<header></header>
<main>BODY_FROM_FILL</main>
<footer>Default footer</footer>