mirror of
https://github.com/django-components/django-components.git
synced 2025-08-18 13:10:13 +00:00
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:
parent
8e2428ebd0
commit
1e4b556b4d
12 changed files with 399 additions and 186 deletions
10
CHANGELOG.md
10
CHANGELOG.md
|
@ -8,8 +8,10 @@
|
||||||
them to their own files, and link the JS/CSS files with `Component.js_file` and `Component.css_file`.
|
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
|
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
|
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/css` upon first access.
|
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:
|
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()`)
|
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()`
|
- 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:
|
`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
|
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).
|
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
|
## v0.123
|
||||||
|
|
||||||
#### Fix
|
#### Fix
|
||||||
|
|
|
@ -339,7 +339,7 @@ these file paths will be resolved only once you either:
|
||||||
|
|
||||||
1. Access any of the following attributes on the component:
|
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`](../../reference/api.md#django_components.Component.template),
|
||||||
[`template_file`](../../reference/api.md#django_components.Component.template_file),
|
[`template_file`](../../reference/api.md#django_components.Component.template_file),
|
||||||
[`js`](../../reference/api.md#django_components.Component.js),
|
[`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.
|
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
|
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
|
## Accessing component's Media files
|
||||||
|
|
||||||
|
|
|
@ -551,6 +551,22 @@ def gen_reference_templatevars():
|
||||||
f.write(f"::: {ComponentVars.__module__}.{ComponentVars.__name__}.{field}\n\n")
|
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=""):
|
def _list_urls(urlpatterns: Sequence[Union[URLPattern, URLResolver]], prefix=""):
|
||||||
"""Recursively extract all URLs and their associated views from Django's urlpatterns"""
|
"""Recursively extract all URLs and their associated views from Django's urlpatterns"""
|
||||||
urls: List[str] = []
|
urls: List[str] = []
|
||||||
|
@ -732,6 +748,7 @@ def gen_reference():
|
||||||
gen_reference_commands()
|
gen_reference_commands()
|
||||||
gen_reference_templatetags()
|
gen_reference_templatetags()
|
||||||
gen_reference_templatevars()
|
gen_reference_templatevars()
|
||||||
|
gen_reference_signals()
|
||||||
|
|
||||||
|
|
||||||
# This is run when `gen-files` plugin is run in mkdocs.yml
|
# This is run when `gen-files` plugin is run in mkdocs.yml
|
||||||
|
|
37
docs/templates/reference_signals.md
vendored
Normal file
37
docs/templates/reference_signals.md
vendored
Normal 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()
|
||||||
|
```
|
|
@ -31,6 +31,7 @@ from django.template.base import NodeList, Template, TextNode
|
||||||
from django.template.context import Context, RequestContext
|
from django.template.context import Context, RequestContext
|
||||||
from django.template.loader import get_template
|
from django.template.loader import get_template
|
||||||
from django.template.loader_tags import BLOCK_CONTEXT_KEY
|
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.utils.html import conditional_escape
|
||||||
from django.views import View
|
from django.views import View
|
||||||
|
|
||||||
|
@ -227,8 +228,15 @@ class Component(
|
||||||
"""
|
"""
|
||||||
Filepath to the Django template associated with this 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,
|
The filepath must be either:
|
||||||
or one of the roots of `STATIFILES_DIRS`.
|
|
||||||
|
- 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),
|
Only one of [`template_file`](../api#django_components.Component.template_file),
|
||||||
[`get_template_name`](../api#django_components.Component.get_template_name),
|
[`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.
|
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:
|
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,
|
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.
|
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:
|
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,
|
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
|
return ctx.is_filled
|
||||||
|
|
||||||
# NOTE: When the template is taken from a file (AKA specified via `template_file`),
|
# NOTE: We cache the Template instance. When the template is taken from a file
|
||||||
# then we leverage Django's template caching. This means that the same instance
|
# via `get_template_name`, then we leverage Django's template caching with `get_template()`.
|
||||||
# of Template is reused. This is important to keep in mind, because the implication
|
# Otherwise, we use our own `cached_template()` to cache the template.
|
||||||
# is that we should treat Templates AND their nodelists as IMMUTABLE.
|
#
|
||||||
|
# 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:
|
def _get_template(self, context: Context) -> Template:
|
||||||
# Resolve template name
|
template_name = self.get_template_name(context)
|
||||||
template_file = self.template_file
|
# TODO_REMOVE_IN_V1 - Remove `self.get_template_string` in v1
|
||||||
if self.template_file is not None:
|
template_getter = getattr(self, "get_template_string", self.get_template)
|
||||||
if self.get_template_name(context) is not None:
|
template_body = template_getter(context)
|
||||||
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)
|
|
||||||
|
|
||||||
# Resolve template str
|
# `get_template_name()`, `get_template()`, and `template` are mutually exclusive
|
||||||
template_input = self.template
|
#
|
||||||
if self.template is not None:
|
# Note that `template` and `template_name` are also mutually exclusive, but this
|
||||||
if self.get_template(context) is not None:
|
# is checked when lazy-loading the template from `template_name`. So if user specified
|
||||||
raise ImproperlyConfigured(
|
# `template_name`, then `template` will be populated with the content of that file.
|
||||||
"Received non-null value from both 'template' and 'get_template' in"
|
if self.template is not None and template_name is not None:
|
||||||
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:
|
|
||||||
raise ImproperlyConfigured(
|
raise ImproperlyConfigured(
|
||||||
f"Received both 'template_file' and 'template' in Component {type(self).__name__}."
|
"Received non-null value from both 'template/template_name' and 'get_template_name' in"
|
||||||
" Only one of the two must be set."
|
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:
|
if template_name is not None:
|
||||||
return get_template(template_file).template
|
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
|
# We got template string, so we convert it to Template
|
||||||
if isinstance(template_input, str):
|
if isinstance(template_body, str):
|
||||||
template: Template = cached_template(template_input)
|
template: Template = cached_template(
|
||||||
|
template_string=template_body,
|
||||||
|
name=self.template_file,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
template = template_input
|
template = template_body
|
||||||
|
|
||||||
return template
|
return template
|
||||||
|
|
||||||
|
@ -1022,6 +1052,8 @@ class Component(
|
||||||
):
|
):
|
||||||
self.on_render_before(context, template)
|
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
|
# Get the component's HTML
|
||||||
html_content = template.render(context)
|
html_content = template.render(context)
|
||||||
|
|
||||||
|
|
|
@ -2,11 +2,13 @@ import os
|
||||||
import sys
|
import sys
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
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.contrib.staticfiles import finders
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.forms.widgets import Media as MediaCls
|
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.utils.safestring import SafeData
|
||||||
|
|
||||||
from django_components.util.loader import get_component_dirs, resolve_file
|
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.
|
# as relative to the directory where the component class is defined.
|
||||||
_resolve_component_relative_files(comp_cls, comp_media, comp_dirs=comp_dirs)
|
_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.
|
# If the component defined `template_file`, `js_file` or `css_file`, instead of `template`/`js`/`css`,
|
||||||
# Effectively, even if the Component class defined `js_file`, at "runtime" the `js` attribute
|
# 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.
|
# will be set to the content of the file.
|
||||||
comp_media.js = _get_static_asset(
|
# So users can access `Component.js` even if they defined `Component.js_file`.
|
||||||
comp_cls, comp_media, inlined_attr="js", file_attr="js_file", comp_dirs=comp_dirs
|
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_media.js = _get_asset(
|
||||||
comp_cls, comp_media, inlined_attr="css", file_attr="css_file", comp_dirs=comp_dirs
|
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
|
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
|
return comp_dir_path_abs, comp_dir_path_rel
|
||||||
|
|
||||||
|
|
||||||
def _get_static_asset(
|
def _get_asset(
|
||||||
comp_cls: Type["Component"],
|
comp_cls: Type["Component"],
|
||||||
comp_media: ComponentMedia,
|
comp_media: ComponentMedia,
|
||||||
inlined_attr: str,
|
inlined_attr: str,
|
||||||
file_attr: str,
|
file_attr: str,
|
||||||
comp_dirs: List[Path],
|
comp_dirs: List[Path],
|
||||||
|
type: Literal["template", "static"],
|
||||||
) -> Optional[str]:
|
) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
In case of Component's JS or CSS, one can either define that as "inlined" or as a file.
|
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:
|
if asset_file is not None:
|
||||||
# Check if the file is in one of the components' directories
|
# Check if the file is in one of the components' directories
|
||||||
full_path = resolve_file(asset_file, comp_dirs)
|
full_path = resolve_file(asset_file, comp_dirs)
|
||||||
# If not, check if it's in the static files
|
|
||||||
if full_path is None:
|
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:
|
if full_path is None:
|
||||||
# NOTE: The short name, e.g. `js` or `css` is used in the error message for convenience
|
# NOTE: The short name, e.g. `js` or `css` is used in the error message for convenience
|
||||||
|
|
1
tests/test_app/components/test_app_simple_template.html
Normal file
1
tests/test_app/components/test_app_simple_template.html
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Variable: <strong>{{ variable }}</strong>
|
|
@ -20,29 +20,7 @@ setup_test_config({"autodiscover": False})
|
||||||
# "Main media" refer to the HTML, JS, and CSS set on the Component class itself
|
# "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.
|
# (as opposed via the `Media` class). These have special handling in the Component.
|
||||||
class MainMediaTest(BaseTestCase):
|
class MainMediaTest(BaseTestCase):
|
||||||
def test_html_inlined(self):
|
def test_html_js_css_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):
|
|
||||||
class TestComponent(Component):
|
class TestComponent(Component):
|
||||||
template = """
|
template = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
|
@ -77,22 +55,37 @@ class MainMediaTest(BaseTestCase):
|
||||||
rendered,
|
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(
|
@override_settings(
|
||||||
STATICFILES_DIRS=[
|
STATICFILES_DIRS=[
|
||||||
os.path.join(Path(__file__).resolve().parent, "static_root"),
|
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
|
from tests.test_app.components.app_lvl_comp.app_lvl_comp import AppLvlCompComponent
|
||||||
|
|
||||||
class TestComponent(AppLvlCompComponent):
|
class TestComponent(AppLvlCompComponent):
|
||||||
template_file = None
|
pass
|
||||||
template = """
|
|
||||||
{% load component_tags %}
|
registry.register("test", TestComponent)
|
||||||
{% component_js_dependencies %}
|
|
||||||
{% component_css_dependencies %}
|
|
||||||
<div class='html-css-only'>Content</div>
|
|
||||||
"""
|
|
||||||
|
|
||||||
self.assertIn(
|
self.assertIn(
|
||||||
".html-css-only {\n color: blue;\n}",
|
".html-css-only {\n color: blue;\n}",
|
||||||
|
@ -103,10 +96,23 @@ class MainMediaTest(BaseTestCase):
|
||||||
TestComponent.js,
|
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(
|
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,
|
rendered,
|
||||||
)
|
)
|
||||||
self.assertInHTML(
|
self.assertInHTML(
|
||||||
|
@ -118,22 +124,46 @@ class MainMediaTest(BaseTestCase):
|
||||||
rendered,
|
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(
|
@override_settings(
|
||||||
STATICFILES_DIRS=[
|
STATICFILES_DIRS=[
|
||||||
os.path.join(Path(__file__).resolve().parent, "static_root"),
|
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):
|
class TestComponent(Component):
|
||||||
template = """
|
template_file = "test_app_simple_template.html"
|
||||||
{% load component_tags %}
|
|
||||||
{% component_js_dependencies %}
|
|
||||||
{% component_css_dependencies %}
|
|
||||||
<div class='html-css-only'>Content</div>
|
|
||||||
"""
|
|
||||||
css_file = "style.css"
|
css_file = "style.css"
|
||||||
js_file = "script.js"
|
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(
|
self.assertIn(
|
||||||
".html-css-only {\n color: blue;\n}",
|
".html-css-only {\n color: blue;\n}",
|
||||||
TestComponent.css,
|
TestComponent.css,
|
||||||
|
@ -143,10 +173,18 @@ class MainMediaTest(BaseTestCase):
|
||||||
TestComponent.js,
|
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(
|
self.assertIn(
|
||||||
'<div class="html-css-only" data-djc-id-a1bc3e>Content</div>',
|
"Variable: <strong data-djc-id-a1bc41>test</strong>",
|
||||||
rendered,
|
rendered,
|
||||||
)
|
)
|
||||||
self.assertInHTML(
|
self.assertInHTML(
|
||||||
|
@ -158,22 +196,38 @@ class MainMediaTest(BaseTestCase):
|
||||||
rendered,
|
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(
|
@override_settings(
|
||||||
STATICFILES_DIRS=[
|
STATICFILES_DIRS=[
|
||||||
os.path.join(Path(__file__).resolve().parent, "static_root"),
|
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
|
from tests.test_app.components.app_lvl_comp.app_lvl_comp import AppLvlCompComponent
|
||||||
|
|
||||||
class TestComponent(AppLvlCompComponent):
|
class TestComponent(AppLvlCompComponent):
|
||||||
template_file = None
|
pass
|
||||||
template = """
|
|
||||||
{% load component_tags %}
|
|
||||||
{% component_js_dependencies %}
|
|
||||||
{% component_css_dependencies %}
|
|
||||||
<div class='html-css-only'>Content</div>
|
|
||||||
"""
|
|
||||||
|
|
||||||
# NOTE: Since this is a subclass, actual CSS is defined on the parent class, and thus
|
# 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.
|
# the corresponding ComponentMedia instance is also on the parent class.
|
||||||
|
@ -189,15 +243,40 @@ class MainMediaTest(BaseTestCase):
|
||||||
# Access the property to load the CSS
|
# Access the property to load the CSS
|
||||||
_ = TestComponent.css
|
_ = TestComponent.css
|
||||||
|
|
||||||
self.assertHTMLEqual(
|
self.assertEqual(
|
||||||
AppLvlCompComponent._component_media.css, # type: ignore[attr-defined]
|
AppLvlCompComponent._component_media.css, # type: ignore[attr-defined]
|
||||||
".html-css-only { color: blue; }",
|
(".html-css-only {\n" " color: blue;\n" "}\n"),
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
AppLvlCompComponent._component_media.css_file, # type: ignore[attr-defined]
|
AppLvlCompComponent._component_media.css_file, # type: ignore[attr-defined]
|
||||||
"app_lvl_comp/app_lvl_comp.css",
|
"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):
|
def test_html_variable(self):
|
||||||
class VariableHTMLComponent(Component):
|
class VariableHTMLComponent(Component):
|
||||||
def get_template(self, context):
|
def get_template(self, context):
|
||||||
|
|
88
tests/test_signals.py
Normal file
88
tests/test_signals.py
Normal 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)
|
|
@ -1,7 +1,5 @@
|
||||||
"""Catch-all for tests that use template tags and don't fit other files"""
|
"""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.template import Context, Template
|
||||||
|
|
||||||
from django_components import Component, register, registry, types
|
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):
|
class MultilineTagsTests(BaseTestCase):
|
||||||
@parametrize_context_behavior(["django", "isolated"])
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
def test_multiline_tags(self):
|
def test_multiline_tags(self):
|
||||||
|
|
|
@ -581,8 +581,16 @@ class MultiComponentTests(BaseTestCase):
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
|
|
||||||
@parametrize_context_behavior(["django", "isolated"])
|
@parametrize_context_behavior(
|
||||||
def test_both_components_render_correctly_when_only_first_has_slots(self):
|
# 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("first_component", SlottedComponent)
|
||||||
registry.register("second_component", SlottedComponentWithContext)
|
registry.register("second_component", SlottedComponentWithContext)
|
||||||
|
|
||||||
|
@ -599,7 +607,7 @@ class MultiComponentTests(BaseTestCase):
|
||||||
|
|
||||||
self.assertHTMLEqual(
|
self.assertHTMLEqual(
|
||||||
rendered,
|
rendered,
|
||||||
"""
|
f"""
|
||||||
<custom-template data-djc-id-a1bc41>
|
<custom-template data-djc-id-a1bc41>
|
||||||
<header>
|
<header>
|
||||||
<p>Slot #1</p>
|
<p>Slot #1</p>
|
||||||
|
@ -607,7 +615,7 @@ class MultiComponentTests(BaseTestCase):
|
||||||
<main>Default main</main>
|
<main>Default main</main>
|
||||||
<footer>Default footer</footer>
|
<footer>Default footer</footer>
|
||||||
</custom-template>
|
</custom-template>
|
||||||
<custom-template data-djc-id-a1bc45>
|
<custom-template {second_id}>
|
||||||
<header>
|
<header>
|
||||||
Default header
|
Default header
|
||||||
</header>
|
</header>
|
||||||
|
|
|
@ -638,7 +638,7 @@ class ExtendsCompatTests(BaseTestCase):
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html data-djc-id-a1bc40 lang="en">
|
<html data-djc-id-a1bc40 lang="en">
|
||||||
<body>
|
<body>
|
||||||
<custom-template data-djc-id-a1bc45>
|
<custom-template data-djc-id-a1bc49>
|
||||||
<header></header>
|
<header></header>
|
||||||
<main>BODY_FROM_FILL</main>
|
<main>BODY_FROM_FILL</main>
|
||||||
<footer>Default footer</footer>
|
<footer>Default footer</footer>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue