mirror of
https://github.com/django-components/django-components.git
synced 2025-07-07 17:34:59 +00:00
refactor: deprecate template caching, get_template_name, get_template, assoc template with Comp cls (#1222)
* refactor: deprecate template caching, get_template_name, get_template, assoc template with Comp cls * refactor: change implementation * refactor: handle cached template loader * refactor: fix tests * refactor: fix test on windows * refactor: try to fix type errors * refactor: Re-cast `context` to fix type errors * refactor: fix linter error * refactor: fix typing * refactor: more linter fixes * refactor: more linter errors * refactor: revert extra node metadata
This commit is contained in:
parent
fa9ae9892f
commit
8677ee7941
28 changed files with 1548 additions and 652 deletions
124
CHANGELOG.md
124
CHANGELOG.md
|
@ -186,6 +186,40 @@ Summary:
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- `Component.template` no longer accepts a Template instance, only plain string.
|
||||||
|
|
||||||
|
Before:
|
||||||
|
|
||||||
|
```py
|
||||||
|
class MyComponent(Component):
|
||||||
|
template = Template("{{ my_var }}")
|
||||||
|
```
|
||||||
|
|
||||||
|
Instead, either:
|
||||||
|
|
||||||
|
1. Set `Component.template` to a plain string.
|
||||||
|
|
||||||
|
```py
|
||||||
|
class MyComponent(Component):
|
||||||
|
template = "{{ my_var }}"
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Move the template to it's own HTML file and set `Component.template_file`.
|
||||||
|
|
||||||
|
```py
|
||||||
|
class MyComponent(Component):
|
||||||
|
template_file = "my_template.html"
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Or, if you dynamically created the template, render the template inside `Component.on_render()`.
|
||||||
|
|
||||||
|
```py
|
||||||
|
class MyComponent(Component):
|
||||||
|
def on_render(self, context, template):
|
||||||
|
dynamic_template = do_something_dynamic()
|
||||||
|
return dynamic_template.render(context)
|
||||||
|
```
|
||||||
|
|
||||||
- The `Component.Url` class was merged with `Component.View`.
|
- The `Component.Url` class was merged with `Component.View`.
|
||||||
|
|
||||||
Instead of `Component.Url.public`, use `Component.View.public`.
|
Instead of `Component.Url.public`, use `Component.View.public`.
|
||||||
|
@ -404,6 +438,51 @@ Summary:
|
||||||
|
|
||||||
Since `get_context_data()` is widely used, it will remain available until v2.
|
Since `get_context_data()` is widely used, it will remain available until v2.
|
||||||
|
|
||||||
|
- `Component.get_template_name()` and `Component.get_template()` are now deprecated. Use `Component.template`,
|
||||||
|
`Component.template_file` or `Component.on_render()` instead.
|
||||||
|
|
||||||
|
`Component.get_template_name()` and `Component.get_template()` will be removed in v1.
|
||||||
|
|
||||||
|
In v1, each Component will have at most one static template.
|
||||||
|
This is needed to enable support for Markdown, Pug, or other pre-processing of templates by extensions.
|
||||||
|
|
||||||
|
If you are using the deprecated methods to point to different templates, there's 2 ways to migrate:
|
||||||
|
|
||||||
|
1. Split the single Component into multiple Components, each with its own template. Then switch between them in `Component.on_render()`:
|
||||||
|
|
||||||
|
```py
|
||||||
|
class MyComponentA(Component):
|
||||||
|
template_file = "a.html"
|
||||||
|
|
||||||
|
class MyComponentB(Component):
|
||||||
|
template_file = "b.html"
|
||||||
|
|
||||||
|
class MyComponent(Component):
|
||||||
|
def on_render(self, context, template):
|
||||||
|
if context["a"]:
|
||||||
|
return MyComponentA.render(context)
|
||||||
|
else:
|
||||||
|
return MyComponentB.render(context)
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Alternatively, use `Component.on_render()` with Django's `get_template()` to dynamically render different templates:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from django.template.loader import get_template
|
||||||
|
|
||||||
|
class MyComponent(Component):
|
||||||
|
def on_render(self, context, template):
|
||||||
|
if context["a"]:
|
||||||
|
template_name = "a.html"
|
||||||
|
else:
|
||||||
|
template_name = "b.html"
|
||||||
|
|
||||||
|
actual_template = get_template(template_name)
|
||||||
|
return actual_template.render(context)
|
||||||
|
```
|
||||||
|
|
||||||
|
Read more in [django-components#1204](https://github.com/django-components/django-components/discussions/1204).
|
||||||
|
|
||||||
- The `type` kwarg in `Component.render()` and `Component.render_to_response()` is now deprecated. Use `deps_strategy` instead. The `type` kwarg will be removed in v1.
|
- The `type` kwarg in `Component.render()` and `Component.render_to_response()` is now deprecated. Use `deps_strategy` instead. The `type` kwarg will be removed in v1.
|
||||||
|
|
||||||
Before:
|
Before:
|
||||||
|
@ -651,6 +730,22 @@ Summary:
|
||||||
|
|
||||||
**Miscellaneous**
|
**Miscellaneous**
|
||||||
|
|
||||||
|
- Template caching with `cached_template()` helper and `template_cache_size` setting is deprecated.
|
||||||
|
These will be removed in v1.
|
||||||
|
|
||||||
|
This feature made sense if you were dynamically generating templates for components using
|
||||||
|
`Component.get_template_string()` and `Component.get_template()`.
|
||||||
|
|
||||||
|
However, in v1, each Component will have at most one static template. This static template
|
||||||
|
is cached internally per component class, and reused across renders.
|
||||||
|
|
||||||
|
This makes the template caching feature obsolete.
|
||||||
|
|
||||||
|
If you relied on `cached_template()`, you should either:
|
||||||
|
|
||||||
|
1. Wrap the templates as Components.
|
||||||
|
2. Manage the cache of Templates yourself.
|
||||||
|
|
||||||
- The `debug_highlight_components` and `debug_highlight_slots` settings are deprecated.
|
- The `debug_highlight_components` and `debug_highlight_slots` settings are deprecated.
|
||||||
These will be removed in v1.
|
These will be removed in v1.
|
||||||
|
|
||||||
|
@ -1004,6 +1099,35 @@ Summary:
|
||||||
can now be accessed also outside of the render call. So now its possible to take the component
|
can now be accessed also outside of the render call. So now its possible to take the component
|
||||||
instance out of `get_template_data()` (although this is not recommended).
|
instance out of `get_template_data()` (although this is not recommended).
|
||||||
|
|
||||||
|
- Components can now be defined without a template.
|
||||||
|
|
||||||
|
Previously, the following would raise an error:
|
||||||
|
|
||||||
|
```py
|
||||||
|
class MyComponent(Component):
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
"Template-less" components can be used together with `Component.on_render()` to dynamically
|
||||||
|
pick what to render:
|
||||||
|
|
||||||
|
```py
|
||||||
|
class TableNew(Component):
|
||||||
|
template_file = "table_new.html"
|
||||||
|
|
||||||
|
class TableOld(Component):
|
||||||
|
template_file = "table_old.html"
|
||||||
|
|
||||||
|
class Table(Component):
|
||||||
|
def on_render(self, context, template):
|
||||||
|
if self.kwargs.get("feat_table_new_ui"):
|
||||||
|
return TableNew.render(args=self.args, kwargs=self.kwargs, slots=self.slots)
|
||||||
|
else:
|
||||||
|
return TableOld.render(args=self.args, kwargs=self.kwargs, slots=self.slots)
|
||||||
|
```
|
||||||
|
|
||||||
|
"Template-less" components can be also used as a base class for other components, or as mixins.
|
||||||
|
|
||||||
- Passing `Slot` instance to `Slot` constructor raises an error.
|
- Passing `Slot` instance to `Slot` constructor raises an error.
|
||||||
|
|
||||||
#### Fix
|
#### Fix
|
||||||
|
|
|
@ -461,8 +461,8 @@ ProjectDashboardAction project.components.dashboard_action.ProjectDashboardAc
|
||||||
## `upgradecomponent`
|
## `upgradecomponent`
|
||||||
|
|
||||||
```txt
|
```txt
|
||||||
usage: upgradecomponent [-h] [--path PATH] [--version] [-v {0,1,2,3}] [--settings SETTINGS] [--pythonpath PYTHONPATH] [--traceback] [--no-color]
|
usage: upgradecomponent [-h] [--path PATH] [--version] [-v {0,1,2,3}] [--settings SETTINGS] [--pythonpath PYTHONPATH] [--traceback] [--no-color] [--force-color]
|
||||||
[--force-color] [--skip-checks]
|
[--skip-checks]
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -67,7 +67,7 @@ If you insert this tag multiple times, ALL JS scripts will be duplicately insert
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L3172" target="_blank">See source code</a>
|
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L3301" target="_blank">See source code</a>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -94,7 +94,6 @@ COMPONENTS = ComponentsSettings(
|
||||||
dirs=[BASE_DIR / "components"],
|
dirs=[BASE_DIR / "components"],
|
||||||
# app_dirs=["components"],
|
# app_dirs=["components"],
|
||||||
# libraries=[],
|
# libraries=[],
|
||||||
# template_cache_size=128,
|
|
||||||
# context_behavior="isolated", # "django" | "isolated"
|
# context_behavior="isolated", # "django" | "isolated"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -620,8 +620,11 @@ class ComponentsSettings(NamedTuple):
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# TODO_V1 - remove
|
||||||
template_cache_size: Optional[int] = None
|
template_cache_size: Optional[int] = None
|
||||||
"""
|
"""
|
||||||
|
DEPRECATED. Template caching will be removed in v1.
|
||||||
|
|
||||||
Configure the maximum amount of Django templates to be cached.
|
Configure the maximum amount of Django templates to be cached.
|
||||||
|
|
||||||
Defaults to `128`.
|
Defaults to `128`.
|
||||||
|
|
|
@ -6,13 +6,10 @@ from django.core.cache.backends.locmem import LocMemCache
|
||||||
from django_components.app_settings import app_settings
|
from django_components.app_settings import app_settings
|
||||||
from django_components.util.cache import LRUCache
|
from django_components.util.cache import LRUCache
|
||||||
|
|
||||||
|
# TODO_V1 - Remove, won't be needed once we remove `get_template_string()`, `get_template_name()`, `get_template()`
|
||||||
|
#
|
||||||
# This stores the parsed Templates. This is strictly local for now, as it stores instances.
|
# This stores the parsed Templates. This is strictly local for now, as it stores instances.
|
||||||
# NOTE: Lazily initialized so it can be configured based on user-defined settings.
|
# NOTE: Lazily initialized so it can be configured based on user-defined settings.
|
||||||
#
|
|
||||||
# TODO: Once we handle whole template parsing ourselves, this could store just
|
|
||||||
# the parsed template AST (+metadata) instead of Template instances. In that case
|
|
||||||
# we could open this up to be stored non-locally and shared across processes.
|
|
||||||
# This would also allow us to remove our custom `LRUCache` implementation.
|
|
||||||
template_cache: Optional[LRUCache] = None
|
template_cache: Optional[LRUCache] = None
|
||||||
|
|
||||||
# This stores the inlined component JS and CSS files (e.g. `Component.js` and `Component.css`).
|
# This stores the inlined component JS and CSS files (e.g. `Component.js` and `Component.css`).
|
||||||
|
@ -20,6 +17,7 @@ template_cache: Optional[LRUCache] = None
|
||||||
component_media_cache: Optional[BaseCache] = None
|
component_media_cache: Optional[BaseCache] = None
|
||||||
|
|
||||||
|
|
||||||
|
# TODO_V1 - Remove, won't be needed once we remove `get_template_string()`, `get_template_name()`, `get_template()`
|
||||||
def get_template_cache() -> LRUCache:
|
def get_template_cache() -> LRUCache:
|
||||||
global template_cache
|
global template_cache
|
||||||
if template_cache is None:
|
if template_cache is None:
|
||||||
|
|
|
@ -6,7 +6,7 @@ from typing import Any
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.template.engine import Engine
|
from django.template.engine import Engine
|
||||||
|
|
||||||
from django_components.template_loader import Loader
|
from django_components.template_loader import DjcLoader
|
||||||
from django_components.util.command import CommandArg, ComponentCommand
|
from django_components.util.command import CommandArg, ComponentCommand
|
||||||
|
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ class UpgradeCommand(ComponentCommand):
|
||||||
|
|
||||||
def handle(self, *args: Any, **options: Any) -> None:
|
def handle(self, *args: Any, **options: Any) -> None:
|
||||||
current_engine = Engine.get_default()
|
current_engine = Engine.get_default()
|
||||||
loader = Loader(current_engine)
|
loader = DjcLoader(current_engine)
|
||||||
dirs = loader.get_dirs(include_apps=False)
|
dirs = loader.get_dirs(include_apps=False)
|
||||||
|
|
||||||
if settings.BASE_DIR:
|
if settings.BASE_DIR:
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import sys
|
import sys
|
||||||
from contextlib import contextmanager
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from types import MethodType
|
from types import MethodType
|
||||||
from typing import (
|
from typing import (
|
||||||
|
@ -7,7 +6,6 @@ from typing import (
|
||||||
Callable,
|
Callable,
|
||||||
ClassVar,
|
ClassVar,
|
||||||
Dict,
|
Dict,
|
||||||
Generator,
|
|
||||||
List,
|
List,
|
||||||
Mapping,
|
Mapping,
|
||||||
NamedTuple,
|
NamedTuple,
|
||||||
|
@ -19,12 +17,10 @@ from typing import (
|
||||||
)
|
)
|
||||||
from weakref import ReferenceType, WeakValueDictionary, finalize
|
from weakref import ReferenceType, WeakValueDictionary, finalize
|
||||||
|
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
|
||||||
from django.forms.widgets import Media as MediaCls
|
from django.forms.widgets import Media as MediaCls
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django.template.base import NodeList, Origin, Parser, Template, Token
|
from django.template.base import NodeList, Parser, Template, Token
|
||||||
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_tags import BLOCK_CONTEXT_KEY, BlockContext
|
from django.template.loader_tags import BLOCK_CONTEXT_KEY, BlockContext
|
||||||
from django.test.signals import template_rendered
|
from django.test.signals import template_rendered
|
||||||
from django.views import View
|
from django.views import View
|
||||||
|
@ -72,12 +68,11 @@ from django_components.slots import (
|
||||||
normalize_slot_fills,
|
normalize_slot_fills,
|
||||||
resolve_fills,
|
resolve_fills,
|
||||||
)
|
)
|
||||||
from django_components.template import cached_template
|
from django_components.template import cache_component_template_file, prepare_component_template
|
||||||
from django_components.util.context import gen_context_processors_data, snapshot_context
|
from django_components.util.context import gen_context_processors_data, snapshot_context
|
||||||
from django_components.util.django_monkeypatch import is_template_cls_patched
|
|
||||||
from django_components.util.exception import component_error_message
|
from django_components.util.exception import component_error_message
|
||||||
from django_components.util.logger import trace_component_msg
|
from django_components.util.logger import trace_component_msg
|
||||||
from django_components.util.misc import default, gen_id, get_import_path, hash_comp_cls, to_dict
|
from django_components.util.misc import default, gen_id, hash_comp_cls, to_dict
|
||||||
from django_components.util.template_tag import TagAttr
|
from django_components.util.template_tag import TagAttr
|
||||||
from django_components.util.weakref import cached_ref
|
from django_components.util.weakref import cached_ref
|
||||||
|
|
||||||
|
@ -412,14 +407,23 @@ class ComponentTemplateNameDescriptor:
|
||||||
|
|
||||||
|
|
||||||
class ComponentMeta(ComponentMediaMeta):
|
class ComponentMeta(ComponentMediaMeta):
|
||||||
def __new__(mcs, name: Any, bases: Tuple, attrs: Dict) -> Any:
|
def __new__(mcs, name: str, bases: Tuple[Type, ...], attrs: Dict) -> Type:
|
||||||
# If user set `template_name` on the class, we instead set it to `template_file`,
|
# If user set `template_name` on the class, we instead set it to `template_file`,
|
||||||
# because we want `template_name` to be the descriptor that proxies to `template_file`.
|
# because we want `template_name` to be the descriptor that proxies to `template_file`.
|
||||||
if "template_name" in attrs:
|
if "template_name" in attrs:
|
||||||
attrs["template_file"] = attrs.pop("template_name")
|
attrs["template_file"] = attrs.pop("template_name")
|
||||||
attrs["template_name"] = ComponentTemplateNameDescriptor()
|
attrs["template_name"] = ComponentTemplateNameDescriptor()
|
||||||
|
|
||||||
return super().__new__(mcs, name, bases, attrs)
|
cls = super().__new__(mcs, name, bases, attrs)
|
||||||
|
|
||||||
|
# If the component defined `template_file`, then associate this Component class
|
||||||
|
# with that template file path.
|
||||||
|
# This way, when we will be instantiating `Template` in order to load the Component's template,
|
||||||
|
# and its template_name matches this path, then we know that the template belongs to this Component class.
|
||||||
|
if "template_file" in attrs and attrs["template_file"]:
|
||||||
|
cache_component_template_file(cls)
|
||||||
|
|
||||||
|
return cls
|
||||||
|
|
||||||
# This runs when a Component class is being deleted
|
# This runs when a Component class is being deleted
|
||||||
def __del__(cls) -> None:
|
def __del__(cls) -> None:
|
||||||
|
@ -646,25 +650,47 @@ class Component(metaclass=ComponentMeta):
|
||||||
|
|
||||||
- Relative to the directory where the Component's Python file is defined.
|
- Relative to the directory where the Component's Python file is defined.
|
||||||
- Relative to one of the component directories, as set by
|
- Relative to one of the component directories, as set by
|
||||||
[`COMPONENTS.dirs`](../settings.md#django_components.app_settings.ComponentsSettings.dirs)
|
[`COMPONENTS.dirs`](../settings#django_components.app_settings.ComponentsSettings.dirs)
|
||||||
or
|
or
|
||||||
[`COMPONENTS.app_dirs`](../settings.md#django_components.app_settings.ComponentsSettings.app_dirs)
|
[`COMPONENTS.app_dirs`](../settings#django_components.app_settings.ComponentsSettings.app_dirs)
|
||||||
(e.g. `<root>/components/`).
|
(e.g. `<root>/components/`).
|
||||||
- Relative to the template directories, as set by Django's `TEMPLATES` setting (e.g. `<root>/templates/`).
|
- 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),
|
!!! warning
|
||||||
[`get_template_name`](../api#django_components.Component.get_template_name),
|
|
||||||
[`template`](../api#django_components.Component.template)
|
Only one of [`template_file`](../api#django_components.Component.template_file),
|
||||||
or [`get_template`](../api#django_components.Component.get_template) must be defined.
|
[`get_template_name`](../api#django_components.Component.get_template_name),
|
||||||
|
[`template`](../api#django_components.Component.template)
|
||||||
|
or [`get_template`](../api#django_components.Component.get_template) must be defined.
|
||||||
|
|
||||||
**Example:**
|
**Example:**
|
||||||
|
|
||||||
```py
|
Assuming this project layout:
|
||||||
class MyComponent(Component):
|
|
||||||
template_file = "path/to/template.html"
|
|
||||||
|
|
||||||
def get_template_data(self, args, kwargs, slots, context):
|
```txt
|
||||||
return {"name": "World"}
|
|- components/
|
||||||
|
|- table/
|
||||||
|
|- table.html
|
||||||
|
|- table.css
|
||||||
|
|- table.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Template name can be either relative to the python file (`components/table/table.py`):
|
||||||
|
|
||||||
|
```python
|
||||||
|
class Table(Component):
|
||||||
|
template_file = "table.html"
|
||||||
|
```
|
||||||
|
|
||||||
|
Or relative to one of the directories in
|
||||||
|
[`COMPONENTS.dirs`](../settings#django_components.app_settings.ComponentsSettings.dirs)
|
||||||
|
or
|
||||||
|
[`COMPONENTS.app_dirs`](../settings#django_components.app_settings.ComponentsSettings.app_dirs)
|
||||||
|
(`components/`):
|
||||||
|
|
||||||
|
```python
|
||||||
|
class Table(Component):
|
||||||
|
template_file = "table/table.html"
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -677,53 +703,142 @@ class Component(metaclass=ComponentMeta):
|
||||||
For historical reasons, django-components used `template_name` to align with Django's
|
For historical reasons, django-components used `template_name` to align with Django's
|
||||||
[TemplateView](https://docs.djangoproject.com/en/5.2/ref/class-based-views/base/#django.views.generic.base.TemplateView).
|
[TemplateView](https://docs.djangoproject.com/en/5.2/ref/class-based-views/base/#django.views.generic.base.TemplateView).
|
||||||
|
|
||||||
`template_file` was introduced to align with `js/js_file` and `css/css_file`.
|
`template_file` was introduced to align with
|
||||||
|
[`js`](../api#django_components.Component.js)/[`js_file`](../api#django_components.Component.js_file)
|
||||||
|
and [`css`](../api#django_components.Component.css)/[`css_file`](../api#django_components.Component.css_file).
|
||||||
|
|
||||||
Setting and accessing this attribute is proxied to `template_file`.
|
Setting and accessing this attribute is proxied to
|
||||||
|
[`template_file`](../api#django_components.Component.template_file).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# TODO_v1 - Remove
|
||||||
def get_template_name(self, context: Context) -> Optional[str]:
|
def get_template_name(self, context: Context) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
Filepath to the Django template associated with this component.
|
DEPRECATED: Use instead [`Component.template_file`](../api#django_components.Component.template_file),
|
||||||
|
[`Component.template`](../api#django_components.Component.template) or
|
||||||
|
[`Component.on_render()`](../api#django_components.Component.on_render).
|
||||||
|
Will be removed in v1.
|
||||||
|
|
||||||
The filepath must be relative to either the file where the component class was defined,
|
Same as [`Component.template_file`](../api#django_components.Component.template_file),
|
||||||
or one of the roots of `STATIFILES_DIRS`.
|
but allows to dynamically resolve the template name at render time.
|
||||||
|
|
||||||
Only one of [`template_file`](../api#django_components.Component.template_file),
|
See [`Component.template_file`](../api#django_components.Component.template_file)
|
||||||
[`get_template_name`](../api#django_components.Component.get_template_name),
|
for more info and examples.
|
||||||
[`template`](../api#django_components.Component.template)
|
|
||||||
or [`get_template`](../api#django_components.Component.get_template) must be defined.
|
!!! warning
|
||||||
|
|
||||||
|
The context is not fully populated at the point when this method is called.
|
||||||
|
|
||||||
|
If you need to access the context, either use
|
||||||
|
[`Component.on_render_before()`](../api#django_components.Component.on_render_before) or
|
||||||
|
[`Component.on_render()`](../api#django_components.Component.on_render).
|
||||||
|
|
||||||
|
!!! warning
|
||||||
|
|
||||||
|
Only one of
|
||||||
|
[`template_file`](../api#django_components.Component.template_file),
|
||||||
|
[`get_template_name()`](../api#django_components.Component.get_template_name),
|
||||||
|
[`template`](../api#django_components.Component.template)
|
||||||
|
or
|
||||||
|
[`get_template()`](../api#django_components.Component.get_template)
|
||||||
|
must be defined.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
context (Context): The Django template\
|
||||||
|
[`Context`](https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.Context)\
|
||||||
|
in which the component is rendered.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Optional[str]: The filepath to the template.
|
||||||
"""
|
"""
|
||||||
return None
|
return None
|
||||||
|
|
||||||
template: Optional[Union[str, Template]] = None
|
template: Optional[str] = None
|
||||||
"""
|
"""
|
||||||
Inlined Django template associated with this component. Can be a plain string or a Template instance.
|
Inlined Django template (as a plain string) associated with this component.
|
||||||
|
|
||||||
Only one of [`template_file`](../api#django_components.Component.template_file),
|
!!! warning
|
||||||
[`get_template_name`](../api#django_components.Component.get_template_name),
|
|
||||||
[`template`](../api#django_components.Component.template)
|
Only one of
|
||||||
or [`get_template`](../api#django_components.Component.get_template) must be defined.
|
[`template_file`](../api#django_components.Component.template_file),
|
||||||
|
[`template`](../api#django_components.Component.template),
|
||||||
|
[`get_template_name()`](../api#django_components.Component.get_template_name),
|
||||||
|
or
|
||||||
|
[`get_template()`](../api#django_components.Component.get_template)
|
||||||
|
must be defined.
|
||||||
|
|
||||||
**Example:**
|
**Example:**
|
||||||
|
|
||||||
```py
|
```python
|
||||||
class MyComponent(Component):
|
class Table(Component):
|
||||||
template = "Hello, {{ name }}!"
|
template = '''
|
||||||
|
<div>
|
||||||
|
{{ my_var }}
|
||||||
|
</div>
|
||||||
|
'''
|
||||||
|
```
|
||||||
|
|
||||||
def get_template_data(self, args, kwargs, slots, context):
|
**Syntax highlighting**
|
||||||
return {"name": "World"}
|
|
||||||
|
When using the inlined template, you can enable syntax highlighting
|
||||||
|
with `django_components.types.django_html`.
|
||||||
|
|
||||||
|
Learn more about [syntax highlighting](../../concepts/fundamentals/single_file_components/#syntax-highlighting).
|
||||||
|
|
||||||
|
```djc_py
|
||||||
|
from django_components import Component, types
|
||||||
|
|
||||||
|
class MyComponent(Component):
|
||||||
|
template: types.django_html = '''
|
||||||
|
<div>
|
||||||
|
{{ my_var }}
|
||||||
|
</div>
|
||||||
|
'''
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# TODO_v1 - Remove
|
||||||
def get_template(self, context: Context) -> Optional[Union[str, Template]]:
|
def get_template(self, context: Context) -> Optional[Union[str, Template]]:
|
||||||
"""
|
"""
|
||||||
Inlined Django template associated with this component. Can be a plain string or a Template instance.
|
DEPRECATED: Use instead [`Component.template_file`](../api#django_components.Component.template_file),
|
||||||
|
[`Component.template`](../api#django_components.Component.template) or
|
||||||
|
[`Component.on_render()`](../api#django_components.Component.on_render).
|
||||||
|
Will be removed in v1.
|
||||||
|
|
||||||
Only one of [`template_file`](../api#django_components.Component.template_file),
|
Same as [`Component.template`](../api#django_components.Component.template),
|
||||||
[`get_template_name`](../api#django_components.Component.get_template_name),
|
but allows to dynamically resolve the template at render time.
|
||||||
[`template`](../api#django_components.Component.template)
|
|
||||||
or [`get_template`](../api#django_components.Component.get_template) must be defined.
|
The template can be either plain string or
|
||||||
|
a [`Template`](https://docs.djangoproject.com/en/5.1/topics/templates/#template) instance.
|
||||||
|
|
||||||
|
See [`Component.template`](../api#django_components.Component.template) for more info and examples.
|
||||||
|
|
||||||
|
!!! warning
|
||||||
|
|
||||||
|
Only one of
|
||||||
|
[`template`](../api#django_components.Component.template)
|
||||||
|
[`template_file`](../api#django_components.Component.template_file),
|
||||||
|
[`get_template_name()`](../api#django_components.Component.get_template_name),
|
||||||
|
or
|
||||||
|
[`get_template()`](../api#django_components.Component.get_template)
|
||||||
|
must be defined.
|
||||||
|
|
||||||
|
!!! warning
|
||||||
|
|
||||||
|
The context is not fully populated at the point when this method is called.
|
||||||
|
|
||||||
|
If you need to access the context, either use
|
||||||
|
[`Component.on_render_before()`](../api#django_components.Component.on_render_before) or
|
||||||
|
[`Component.on_render()`](../api#django_components.Component.on_render).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
context (Context): The Django template\
|
||||||
|
[`Context`](https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.Context)\
|
||||||
|
in which the component is rendered.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Optional[Union[str, Template]]: The inlined Django template string or\
|
||||||
|
a [`Template`](https://docs.djangoproject.com/en/5.1/topics/templates/#template) instance.
|
||||||
"""
|
"""
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -981,8 +1096,10 @@ class Component(metaclass=ComponentMeta):
|
||||||
"""
|
"""
|
||||||
Main JS associated with this component inlined as string.
|
Main JS associated with this component inlined as string.
|
||||||
|
|
||||||
Only one of [`js`](../api#django_components.Component.js) or
|
!!! warning
|
||||||
[`js_file`](../api#django_components.Component.js_file) must be defined.
|
|
||||||
|
Only one of [`js`](../api#django_components.Component.js) or
|
||||||
|
[`js_file`](../api#django_components.Component.js_file) must be defined.
|
||||||
|
|
||||||
**Example:**
|
**Example:**
|
||||||
|
|
||||||
|
@ -990,6 +1107,22 @@ class Component(metaclass=ComponentMeta):
|
||||||
class MyComponent(Component):
|
class MyComponent(Component):
|
||||||
js = "console.log('Hello, World!');"
|
js = "console.log('Hello, World!');"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Syntax highlighting**
|
||||||
|
|
||||||
|
When using the inlined template, you can enable syntax highlighting
|
||||||
|
with `django_components.types.js`.
|
||||||
|
|
||||||
|
Learn more about [syntax highlighting](../../concepts/fundamentals/single_file_components/#syntax-highlighting).
|
||||||
|
|
||||||
|
```djc_py
|
||||||
|
from django_components import Component, types
|
||||||
|
|
||||||
|
class MyComponent(Component):
|
||||||
|
js: types.js = '''
|
||||||
|
console.log('Hello, World!');
|
||||||
|
'''
|
||||||
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
js_file: ClassVar[Optional[str]] = None
|
js_file: ClassVar[Optional[str]] = None
|
||||||
|
@ -1000,9 +1133,9 @@ class Component(metaclass=ComponentMeta):
|
||||||
|
|
||||||
- Relative to the directory where the Component's Python file is defined.
|
- Relative to the directory where the Component's Python file is defined.
|
||||||
- Relative to one of the component directories, as set by
|
- Relative to one of the component directories, as set by
|
||||||
[`COMPONENTS.dirs`](../settings.md#django_components.app_settings.ComponentsSettings.dirs)
|
[`COMPONENTS.dirs`](../settings#django_components.app_settings.ComponentsSettings.dirs)
|
||||||
or
|
or
|
||||||
[`COMPONENTS.app_dirs`](../settings.md#django_components.app_settings.ComponentsSettings.app_dirs)
|
[`COMPONENTS.app_dirs`](../settings#django_components.app_settings.ComponentsSettings.app_dirs)
|
||||||
(e.g. `<root>/components/`).
|
(e.g. `<root>/components/`).
|
||||||
- Relative to the staticfiles directories, as set by Django's `STATICFILES_DIRS` setting (e.g. `<root>/static/`).
|
- Relative to the staticfiles directories, as set by Django's `STATICFILES_DIRS` setting (e.g. `<root>/static/`).
|
||||||
|
|
||||||
|
@ -1012,8 +1145,10 @@ class Component(metaclass=ComponentMeta):
|
||||||
the path is resolved.
|
the path is resolved.
|
||||||
2. The file is read and its contents is set to [`Component.js`](../api#django_components.Component.js).
|
2. The file is read and its contents is set to [`Component.js`](../api#django_components.Component.js).
|
||||||
|
|
||||||
Only one of [`js`](../api#django_components.Component.js) or
|
!!! warning
|
||||||
[`js_file`](../api#django_components.Component.js_file) must be defined.
|
|
||||||
|
Only one of [`js`](../api#django_components.Component.js) or
|
||||||
|
[`js_file`](../api#django_components.Component.js_file) must be defined.
|
||||||
|
|
||||||
**Example:**
|
**Example:**
|
||||||
|
|
||||||
|
@ -1244,19 +1379,39 @@ class Component(metaclass=ComponentMeta):
|
||||||
"""
|
"""
|
||||||
Main CSS associated with this component inlined as string.
|
Main CSS associated with this component inlined as string.
|
||||||
|
|
||||||
Only one of [`css`](../api#django_components.Component.css) or
|
!!! warning
|
||||||
[`css_file`](../api#django_components.Component.css_file) must be defined.
|
|
||||||
|
Only one of [`css`](../api#django_components.Component.css) or
|
||||||
|
[`css_file`](../api#django_components.Component.css_file) must be defined.
|
||||||
|
|
||||||
**Example:**
|
**Example:**
|
||||||
|
|
||||||
```py
|
```py
|
||||||
class MyComponent(Component):
|
class MyComponent(Component):
|
||||||
css = \"\"\"
|
css = \"\"\"
|
||||||
.my-class {
|
.my-class {
|
||||||
color: red;
|
color: red;
|
||||||
}
|
}
|
||||||
\"\"\"
|
\"\"\"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Syntax highlighting**
|
||||||
|
|
||||||
|
When using the inlined template, you can enable syntax highlighting
|
||||||
|
with `django_components.types.css`.
|
||||||
|
|
||||||
|
Learn more about [syntax highlighting](../../concepts/fundamentals/single_file_components/#syntax-highlighting).
|
||||||
|
|
||||||
|
```djc_py
|
||||||
|
from django_components import Component, types
|
||||||
|
|
||||||
|
class MyComponent(Component):
|
||||||
|
css: types.css = '''
|
||||||
|
.my-class {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
'''
|
||||||
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
css_file: ClassVar[Optional[str]] = None
|
css_file: ClassVar[Optional[str]] = None
|
||||||
|
@ -1267,9 +1422,9 @@ class Component(metaclass=ComponentMeta):
|
||||||
|
|
||||||
- Relative to the directory where the Component's Python file is defined.
|
- Relative to the directory where the Component's Python file is defined.
|
||||||
- Relative to one of the component directories, as set by
|
- Relative to one of the component directories, as set by
|
||||||
[`COMPONENTS.dirs`](../settings.md#django_components.app_settings.ComponentsSettings.dirs)
|
[`COMPONENTS.dirs`](../settings#django_components.app_settings.ComponentsSettings.dirs)
|
||||||
or
|
or
|
||||||
[`COMPONENTS.app_dirs`](../settings.md#django_components.app_settings.ComponentsSettings.app_dirs)
|
[`COMPONENTS.app_dirs`](../settings#django_components.app_settings.ComponentsSettings.app_dirs)
|
||||||
(e.g. `<root>/components/`).
|
(e.g. `<root>/components/`).
|
||||||
- Relative to the staticfiles directories, as set by Django's `STATICFILES_DIRS` setting (e.g. `<root>/static/`).
|
- Relative to the staticfiles directories, as set by Django's `STATICFILES_DIRS` setting (e.g. `<root>/static/`).
|
||||||
|
|
||||||
|
@ -1279,8 +1434,10 @@ class Component(metaclass=ComponentMeta):
|
||||||
the path is resolved.
|
the path is resolved.
|
||||||
2. The file is read and its contents is set to [`Component.css`](../api#django_components.Component.css).
|
2. The file is read and its contents is set to [`Component.css`](../api#django_components.Component.css).
|
||||||
|
|
||||||
Only one of [`css`](../api#django_components.Component.css) or
|
!!! warning
|
||||||
[`css_file`](../api#django_components.Component.css_file) must be defined.
|
|
||||||
|
Only one of [`css`](../api#django_components.Component.css) or
|
||||||
|
[`css_file`](../api#django_components.Component.css_file) must be defined.
|
||||||
|
|
||||||
**Example:**
|
**Example:**
|
||||||
|
|
||||||
|
@ -1632,7 +1789,7 @@ class Component(metaclass=ComponentMeta):
|
||||||
# PUBLIC API - HOOKS (Configurable by users)
|
# PUBLIC API - HOOKS (Configurable by users)
|
||||||
# #####################################
|
# #####################################
|
||||||
|
|
||||||
def on_render_before(self, context: Context, template: Template) -> None:
|
def on_render_before(self, context: Context, template: Optional[Template]) -> None:
|
||||||
"""
|
"""
|
||||||
Hook that runs just before the component's template is rendered.
|
Hook that runs just before the component's template is rendered.
|
||||||
|
|
||||||
|
@ -1640,7 +1797,7 @@ class Component(metaclass=ComponentMeta):
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def on_render_after(self, context: Context, template: Template, content: str) -> Optional[SlotResult]:
|
def on_render_after(self, context: Context, template: Optional[Template], content: str) -> Optional[SlotResult]:
|
||||||
"""
|
"""
|
||||||
Hook that runs just after the component's template was rendered.
|
Hook that runs just after the component's template was rendered.
|
||||||
It receives the rendered output as the last argument.
|
It receives the rendered output as the last argument.
|
||||||
|
@ -1753,6 +1910,15 @@ class Component(metaclass=ComponentMeta):
|
||||||
"""Deprecated. Use `Component.class_id` instead."""
|
"""Deprecated. Use `Component.class_id` instead."""
|
||||||
return self.class_id
|
return self.class_id
|
||||||
|
|
||||||
|
_template: Optional[Template] = None
|
||||||
|
"""
|
||||||
|
Cached [`Template`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Template)
|
||||||
|
instance for the [`Component`](../api#django_components.Component),
|
||||||
|
created from
|
||||||
|
[`Component.template`](#django_components.Component.template) or
|
||||||
|
[`Component.template_file`](#django_components.Component.template_file).
|
||||||
|
"""
|
||||||
|
|
||||||
# TODO_v3 - Django-specific property to prevent calling the instance as a function.
|
# TODO_v3 - Django-specific property to prevent calling the instance as a function.
|
||||||
do_not_call_in_templates: ClassVar[bool] = True
|
do_not_call_in_templates: ClassVar[bool] = True
|
||||||
"""
|
"""
|
||||||
|
@ -1850,6 +2016,9 @@ class Component(metaclass=ComponentMeta):
|
||||||
cls.class_id = hash_comp_cls(cls)
|
cls.class_id = hash_comp_cls(cls)
|
||||||
comp_cls_id_mapping[cls.class_id] = cls
|
comp_cls_id_mapping[cls.class_id] = cls
|
||||||
|
|
||||||
|
# Make sure that subclassed component will store it's own template, not the parent's.
|
||||||
|
cls._template = None
|
||||||
|
|
||||||
ALL_COMPONENTS.append(cached_ref(cls)) # type: ignore[arg-type]
|
ALL_COMPONENTS.append(cached_ref(cls)) # type: ignore[arg-type]
|
||||||
extensions._init_component_class(cls)
|
extensions._init_component_class(cls)
|
||||||
extensions.on_component_class_created(OnComponentClassCreatedContext(cls))
|
extensions.on_component_class_created(OnComponentClassCreatedContext(cls))
|
||||||
|
@ -2276,64 +2445,6 @@ class Component(metaclass=ComponentMeta):
|
||||||
# MISC
|
# MISC
|
||||||
# #####################################
|
# #####################################
|
||||||
|
|
||||||
# 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, component_id: str) -> Template:
|
|
||||||
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)
|
|
||||||
|
|
||||||
# `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(
|
|
||||||
"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_name is not None:
|
|
||||||
return get_template(template_name).template
|
|
||||||
|
|
||||||
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_body, str):
|
|
||||||
trace_component_msg("COMP_LOAD", component_name=self.name, component_id=component_id, slot_name=None)
|
|
||||||
template: Template = cached_template(
|
|
||||||
template_string=template_body,
|
|
||||||
name=self.template_file or self.name,
|
|
||||||
origin=Origin(
|
|
||||||
name=self.template_file or get_import_path(self.__class__),
|
|
||||||
template_name=self.template_file or self.name,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
template = template_body
|
|
||||||
|
|
||||||
return template
|
|
||||||
|
|
||||||
raise ImproperlyConfigured(
|
|
||||||
f"Either 'template_file' or 'template' must be set for Component {type(self).__name__}."
|
|
||||||
)
|
|
||||||
|
|
||||||
def inject(self, key: str, default: Optional[Any] = None) -> Any:
|
def inject(self, key: str, default: Optional[Any] = None) -> Any:
|
||||||
"""
|
"""
|
||||||
Use this method to retrieve the data that was passed to a [`{% provide %}`](../template_tags#provide) tag
|
Use this method to retrieve the data that was passed to a [`{% provide %}`](../template_tags#provide) tag
|
||||||
|
@ -2796,12 +2907,12 @@ class Component(metaclass=ComponentMeta):
|
||||||
)
|
)
|
||||||
# Use RequestContext if request is provided, so that child non-component template tags
|
# Use RequestContext if request is provided, so that child non-component template tags
|
||||||
# can access the request object too.
|
# can access the request object too.
|
||||||
context = context or (RequestContext(request) if request else Context())
|
context = context if context is not None else (RequestContext(request) if request else Context())
|
||||||
|
|
||||||
# Allow to provide a dict instead of Context
|
# Allow to provide a dict instead of Context
|
||||||
# NOTE: This if/else is important to avoid nested Contexts,
|
# NOTE: This if/else is important to avoid nested Contexts,
|
||||||
# See https://github.com/django-components/django-components/issues/414
|
# See https://github.com/django-components/django-components/issues/414
|
||||||
if not isinstance(context, Context):
|
if not isinstance(context, (Context, RequestContext)):
|
||||||
context = RequestContext(request, context) if request else Context(context)
|
context = RequestContext(request, context) if request else Context(context)
|
||||||
|
|
||||||
render_id = _gen_component_id()
|
render_id = _gen_component_id()
|
||||||
|
@ -2850,7 +2961,9 @@ class Component(metaclass=ComponentMeta):
|
||||||
|
|
||||||
# Required for compatibility with Django's {% extends %} tag
|
# Required for compatibility with Django's {% extends %} tag
|
||||||
# See https://github.com/django-components/django-components/pull/859
|
# See https://github.com/django-components/django-components/pull/859
|
||||||
context.render_context.push({BLOCK_CONTEXT_KEY: context.render_context.get(BLOCK_CONTEXT_KEY, BlockContext())})
|
context.render_context.push( # type: ignore[union-attr]
|
||||||
|
{BLOCK_CONTEXT_KEY: context.render_context.get(BLOCK_CONTEXT_KEY, BlockContext())} # type: ignore
|
||||||
|
)
|
||||||
|
|
||||||
# We pass down the components the info about the component's parent.
|
# We pass down the components the info about the component's parent.
|
||||||
# This is used for correctly resolving slot fills, correct rendering order,
|
# This is used for correctly resolving slot fills, correct rendering order,
|
||||||
|
@ -2886,7 +2999,7 @@ class Component(metaclass=ComponentMeta):
|
||||||
template_name=None,
|
template_name=None,
|
||||||
# This field will be modified from within `SlotNodes.render()`:
|
# This field will be modified from within `SlotNodes.render()`:
|
||||||
# - The `default_slot` will be set to the first slot that has the `default` attribute set.
|
# - The `default_slot` will be set to the first slot that has the `default` attribute set.
|
||||||
# If multiple slots have the `default` attribute set, yet have different name, then
|
# - If multiple slots have the `default` attribute set, yet have different name, then
|
||||||
# we will raise an error.
|
# we will raise an error.
|
||||||
default_slot=None,
|
default_slot=None,
|
||||||
# NOTE: This is only a SNAPSHOT of the outer context.
|
# NOTE: This is only a SNAPSHOT of the outer context.
|
||||||
|
@ -2937,10 +3050,32 @@ class Component(metaclass=ComponentMeta):
|
||||||
# but instead can render one component at a time.
|
# but instead can render one component at a time.
|
||||||
#############################################################################
|
#############################################################################
|
||||||
|
|
||||||
with _prepare_template(component, template_data) as template:
|
# TODO_v1 - Currently we have to pass `template_data` to `prepare_component_template()`,
|
||||||
component_ctx.template_name = template.name
|
# so that `get_template_string()`, `get_template_name()`, and `get_template()`
|
||||||
|
# have access to the data from `get_template_data()`.
|
||||||
|
#
|
||||||
|
# Because of that there is one layer of `Context.update()` called inside `prepare_component_template()`.
|
||||||
|
#
|
||||||
|
# Once `get_template_string()`, `get_template_name()`, and `get_template()` are removed,
|
||||||
|
# we can remove that layer of `Context.update()`, and NOT pass `template_data`
|
||||||
|
# to `prepare_component_template()`.
|
||||||
|
#
|
||||||
|
# Then we can simply apply `template_data` to the context in the same layer
|
||||||
|
# where we apply `context_processor_data` and `component_vars`.
|
||||||
|
with prepare_component_template(component, template_data) as template:
|
||||||
|
# Set `Template._djc_is_component_nested` based on whether we're currently INSIDE
|
||||||
|
# the `{% extends %}` tag.
|
||||||
|
# Part of fix for https://github.com/django-components/django-components/issues/508
|
||||||
|
# See django_monkeypatch.py
|
||||||
|
if template is not None:
|
||||||
|
template._djc_is_component_nested = bool(
|
||||||
|
context.render_context.get(BLOCK_CONTEXT_KEY) # type: ignore[union-attr]
|
||||||
|
)
|
||||||
|
|
||||||
with context.update(
|
# Capture the template name so we can print better error messages (currently used in slots)
|
||||||
|
component_ctx.template_name = template.name if template else None
|
||||||
|
|
||||||
|
with context.update( # type: ignore[union-attr]
|
||||||
{
|
{
|
||||||
# Make data from context processors available inside templates
|
# Make data from context processors available inside templates
|
||||||
**component.context_processors_data,
|
**component.context_processors_data,
|
||||||
|
@ -2973,7 +3108,7 @@ class Component(metaclass=ComponentMeta):
|
||||||
context_snapshot = snapshot_context(context)
|
context_snapshot = snapshot_context(context)
|
||||||
|
|
||||||
# Cleanup
|
# Cleanup
|
||||||
context.render_context.pop()
|
context.render_context.pop() # type: ignore[union-attr]
|
||||||
|
|
||||||
######################################
|
######################################
|
||||||
# 5. Render component
|
# 5. Render component
|
||||||
|
@ -3089,26 +3224,31 @@ class Component(metaclass=ComponentMeta):
|
||||||
|
|
||||||
# Emit signal that the template is about to be rendered
|
# Emit signal that the template is about to be rendered
|
||||||
template_rendered.send(sender=template, template=template, context=context)
|
template_rendered.send(sender=template, template=template, context=context)
|
||||||
# Get the component's HTML
|
|
||||||
html_content = template.render(context)
|
|
||||||
|
|
||||||
# Add necessary HTML attributes to work with JS and CSS variables
|
if template is not None:
|
||||||
updated_html, child_components = set_component_attrs_for_js_and_css(
|
# Get the component's HTML
|
||||||
html_content=html_content,
|
html_content = template.render(context)
|
||||||
component_id=render_id,
|
|
||||||
css_input_hash=css_input_hash,
|
|
||||||
css_scope_id=css_scope_id,
|
|
||||||
root_attributes=root_attributes,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Prepend an HTML comment to instructs how and what JS and CSS scripts are associated with it.
|
# Add necessary HTML attributes to work with JS and CSS variables
|
||||||
updated_html = insert_component_dependencies_comment(
|
updated_html, child_components = set_component_attrs_for_js_and_css(
|
||||||
updated_html,
|
html_content=html_content,
|
||||||
component_cls=component_cls,
|
component_id=render_id,
|
||||||
component_id=render_id,
|
css_input_hash=css_input_hash,
|
||||||
js_input_hash=js_input_hash,
|
css_scope_id=css_scope_id,
|
||||||
css_input_hash=css_input_hash,
|
root_attributes=root_attributes,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Prepend an HTML comment to instructs how and what JS and CSS scripts are associated with it.
|
||||||
|
updated_html = insert_component_dependencies_comment(
|
||||||
|
updated_html,
|
||||||
|
component_cls=component_cls,
|
||||||
|
component_id=render_id,
|
||||||
|
js_input_hash=js_input_hash,
|
||||||
|
css_input_hash=css_input_hash,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
updated_html = ""
|
||||||
|
child_components = {}
|
||||||
|
|
||||||
trace_component_msg(
|
trace_component_msg(
|
||||||
"COMP_RENDER_END",
|
"COMP_RENDER_END",
|
||||||
|
@ -3278,7 +3418,13 @@ class ComponentNode(BaseNode):
|
||||||
node_id: Optional[str] = None,
|
node_id: Optional[str] = None,
|
||||||
contents: Optional[str] = None,
|
contents: Optional[str] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(params=params, flags=flags, nodelist=nodelist, node_id=node_id, contents=contents)
|
super().__init__(
|
||||||
|
params=params,
|
||||||
|
flags=flags,
|
||||||
|
nodelist=nodelist,
|
||||||
|
node_id=node_id,
|
||||||
|
contents=contents,
|
||||||
|
)
|
||||||
|
|
||||||
self.name = name
|
self.name = name
|
||||||
self.registry = registry
|
self.registry = registry
|
||||||
|
@ -3370,43 +3516,3 @@ def _get_parent_component_context(context: Context) -> Union[Tuple[None, None],
|
||||||
|
|
||||||
parent_comp_ctx = component_context_cache[parent_id]
|
parent_comp_ctx = component_context_cache[parent_id]
|
||||||
return parent_id, parent_comp_ctx
|
return parent_id, parent_comp_ctx
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
|
||||||
def _maybe_bind_template(context: Context, template: Template) -> Generator[None, Any, None]:
|
|
||||||
if context.template is None:
|
|
||||||
with context.bind_template(template):
|
|
||||||
yield
|
|
||||||
else:
|
|
||||||
yield
|
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
|
||||||
def _prepare_template(
|
|
||||||
component: Component,
|
|
||||||
template_data: Any,
|
|
||||||
) -> Generator[Template, Any, None]:
|
|
||||||
context = component.context
|
|
||||||
with context.update(template_data):
|
|
||||||
# Associate the newly-created Context with a Template, otherwise we get
|
|
||||||
# an error when we try to use `{% include %}` tag inside the template?
|
|
||||||
# See https://github.com/django-components/django-components/issues/580
|
|
||||||
# And https://github.com/django-components/django-components/issues/634
|
|
||||||
template = component._get_template(context, component_id=component.id)
|
|
||||||
|
|
||||||
if not is_template_cls_patched(template):
|
|
||||||
raise RuntimeError(
|
|
||||||
"Django-components received a Template instance which was not patched."
|
|
||||||
"If you are using Django's Template class, check if you added django-components"
|
|
||||||
"to INSTALLED_APPS. If you are using a custom template class, then you need to"
|
|
||||||
"manually patch the class."
|
|
||||||
)
|
|
||||||
|
|
||||||
# Set `Template._djc_is_component_nested` based on whether we're currently INSIDE
|
|
||||||
# the `{% extends %}` tag.
|
|
||||||
# Part of fix for https://github.com/django-components/django-components/issues/508
|
|
||||||
# See django_monkeypatch.py
|
|
||||||
template._djc_is_component_nested = bool(context.render_context.get(BLOCK_CONTEXT_KEY))
|
|
||||||
|
|
||||||
with _maybe_bind_template(context, template):
|
|
||||||
yield template
|
|
||||||
|
|
|
@ -26,10 +26,9 @@ from weakref import WeakKeyDictionary
|
||||||
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.template import load_component_template
|
||||||
from django_components.util.loader import get_component_dirs, resolve_file
|
from django_components.util.loader import get_component_dirs, resolve_file
|
||||||
from django_components.util.logger import logger
|
from django_components.util.logger import logger
|
||||||
from django_components.util.misc import flatten, get_import_path, get_module_info, is_glob
|
from django_components.util.misc import flatten, get_import_path, get_module_info, is_glob
|
||||||
|
@ -240,6 +239,7 @@ class ComponentMediaInput(Protocol):
|
||||||
class ComponentMedia:
|
class ComponentMedia:
|
||||||
comp_cls: Type["Component"]
|
comp_cls: Type["Component"]
|
||||||
resolved: bool = False
|
resolved: bool = False
|
||||||
|
resolved_relative_files: bool = False
|
||||||
Media: Optional[Type[ComponentMediaInput]] = None
|
Media: Optional[Type[ComponentMediaInput]] = None
|
||||||
template: Optional[str] = None
|
template: Optional[str] = None
|
||||||
template_file: Optional[str] = None
|
template_file: Optional[str] = None
|
||||||
|
@ -543,9 +543,13 @@ def _resolve_media(comp_cls: Type["Component"], comp_media: ComponentMedia) -> N
|
||||||
assert isinstance(self.media, MyMedia)
|
assert isinstance(self.media, MyMedia)
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
if comp_media.resolved:
|
||||||
|
return
|
||||||
|
|
||||||
|
comp_media.resolved = True
|
||||||
|
|
||||||
# Do not resolve if this is a base class
|
# Do not resolve if this is a base class
|
||||||
if get_import_path(comp_cls) == "django_components.component.Component" or comp_media.resolved:
|
if get_import_path(comp_cls) == "django_components.component.Component":
|
||||||
comp_media.resolved = True
|
|
||||||
return
|
return
|
||||||
|
|
||||||
comp_dirs = get_component_dirs()
|
comp_dirs = get_component_dirs()
|
||||||
|
@ -574,8 +578,6 @@ def _resolve_media(comp_cls: Type["Component"], comp_media: ComponentMedia) -> N
|
||||||
comp_cls, comp_media, inlined_attr="css", file_attr="css_file", comp_dirs=comp_dirs, type="static"
|
comp_cls, comp_media, inlined_attr="css", file_attr="css_file", comp_dirs=comp_dirs, type="static"
|
||||||
)
|
)
|
||||||
|
|
||||||
comp_media.resolved = True
|
|
||||||
|
|
||||||
|
|
||||||
def _normalize_media(media: Type[ComponentMediaInput]) -> None:
|
def _normalize_media(media: Type[ComponentMediaInput]) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -736,6 +738,11 @@ def _resolve_component_relative_files(
|
||||||
as the component class. If so, modify the attributes so the class Django's rendering
|
as the component class. If so, modify the attributes so the class Django's rendering
|
||||||
will pick up these files correctly.
|
will pick up these files correctly.
|
||||||
"""
|
"""
|
||||||
|
if comp_media.resolved_relative_files:
|
||||||
|
return
|
||||||
|
|
||||||
|
comp_media.resolved_relative_files = True
|
||||||
|
|
||||||
# First check if we even need to resolve anything. If the class doesn't define any
|
# First check if we even need to resolve anything. If the class doesn't define any
|
||||||
# HTML/JS/CSS files, just skip.
|
# HTML/JS/CSS files, just skip.
|
||||||
will_resolve_files = False
|
will_resolve_files = False
|
||||||
|
@ -953,27 +960,47 @@ def _get_asset(
|
||||||
asset_content = getattr(comp_media, inlined_attr, None)
|
asset_content = getattr(comp_media, inlined_attr, None)
|
||||||
asset_file = getattr(comp_media, file_attr, None)
|
asset_file = getattr(comp_media, file_attr, None)
|
||||||
|
|
||||||
if asset_file is not None:
|
# No inlined content, nor file name
|
||||||
# Check if the file is in one of the components' directories
|
if asset_content is None and asset_file is None:
|
||||||
full_path = resolve_file(asset_file, comp_dirs)
|
return None
|
||||||
|
|
||||||
if full_path is None:
|
if asset_content is not None and asset_file is not None:
|
||||||
# If not, check if it's in the static files
|
raise ValueError(
|
||||||
if type == "static":
|
f"Received both '{inlined_attr}' and '{file_attr}' in Component {comp_cls.__qualname__}."
|
||||||
full_path = finders.find(asset_file)
|
" Only one of the two must be set."
|
||||||
# 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 the content was inlined into the component (e.g. `Component.template = "..."`)
|
||||||
# NOTE: The short name, e.g. `js` or `css` is used in the error message for convenience
|
# then there's nothing to resolve. Return as is.
|
||||||
raise ValueError(f"Could not find {inlined_attr} file {asset_file}")
|
if asset_content is not None:
|
||||||
|
return asset_content
|
||||||
|
|
||||||
# NOTE: Use explicit encoding for compat with Windows, see #1074
|
# The rest of the code assumes that we were given only a file name
|
||||||
asset_content = Path(full_path).read_text(encoding="utf8")
|
asset_file = cast(str, asset_file)
|
||||||
|
|
||||||
|
if type == "template":
|
||||||
|
# NOTE: While we return on the "source" (plain string) of the template,
|
||||||
|
# by calling `load_component_template()`, we also cache the Template instance.
|
||||||
|
# So later in Component's `render_impl()`, we don't have to re-compile the Template.
|
||||||
|
template = load_component_template(comp_cls, asset_file)
|
||||||
|
return template.source
|
||||||
|
|
||||||
|
# For static files, we have a few options:
|
||||||
|
# 1. Check if the file is in one of the components' directories
|
||||||
|
full_path = resolve_file(asset_file, comp_dirs)
|
||||||
|
|
||||||
|
# 2. If not, check if it's in the static files
|
||||||
|
if full_path is None:
|
||||||
|
full_path = finders.find(asset_file)
|
||||||
|
|
||||||
|
if full_path is None:
|
||||||
|
# NOTE: The short name, e.g. `js` or `css` is used in the error message for convenience
|
||||||
|
raise ValueError(f"Could not find {inlined_attr} file {asset_file}")
|
||||||
|
|
||||||
|
# NOTE: Use explicit encoding for compat with Windows, see #1074
|
||||||
|
asset_content = Path(full_path).read_text(encoding="utf8")
|
||||||
|
|
||||||
|
# TODO: Apply `extensions.on_js_preprocess()` and `extensions.on_css_preprocess()`
|
||||||
|
# NOTE: `on_template_preprocess()` is already applied inside `load_component_template()`
|
||||||
|
|
||||||
return asset_content
|
return asset_content
|
||||||
|
|
|
@ -1605,7 +1605,7 @@ def _nodelist_to_slot(
|
||||||
if index_of_last_component_layer is None:
|
if index_of_last_component_layer is None:
|
||||||
index_of_last_component_layer = 0
|
index_of_last_component_layer = 0
|
||||||
|
|
||||||
# TODO: Currently there's one more layer before the `_COMPONENT_CONTEXT_KEY` layer, which is
|
# TODO_V1: Currently there's one more layer before the `_COMPONENT_CONTEXT_KEY` layer, which is
|
||||||
# pushed in `_prepare_template()` in `component.py`.
|
# pushed in `_prepare_template()` in `component.py`.
|
||||||
# That layer should be removed when `Component.get_template()` is removed, after which
|
# That layer should be removed when `Component.get_template()` is removed, after which
|
||||||
# the following line can be removed.
|
# the following line can be removed.
|
||||||
|
|
|
@ -1,12 +1,24 @@
|
||||||
from typing import Any, Optional, Type
|
import sys
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from typing import TYPE_CHECKING, Any, Dict, Generator, List, Optional, Type, Union, cast
|
||||||
|
from weakref import ReferenceType, ref
|
||||||
|
|
||||||
from django.template import Origin, Template
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
from django.template import Context, Origin, Template
|
||||||
|
from django.template.loader import get_template as django_get_template
|
||||||
|
|
||||||
from django_components.cache import get_template_cache
|
from django_components.cache import get_template_cache
|
||||||
from django_components.util.misc import get_import_path
|
from django_components.util.django_monkeypatch import is_template_cls_patched
|
||||||
|
from django_components.util.loader import get_component_dirs
|
||||||
|
from django_components.util.logger import trace_component_msg
|
||||||
|
from django_components.util.misc import get_import_path, get_module_info
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from django_components.component import Component
|
||||||
|
|
||||||
|
|
||||||
# Central logic for creating Templates from string, so we can cache the results
|
# TODO_V1 - Remove, won't be needed once we remove `get_template_string()`, `get_template_name()`, `get_template()`
|
||||||
|
# Legacy logic for creating Templates from string
|
||||||
def cached_template(
|
def cached_template(
|
||||||
template_string: str,
|
template_string: str,
|
||||||
template_cls: Optional[Type[Template]] = None,
|
template_cls: Optional[Type[Template]] = None,
|
||||||
|
@ -15,6 +27,8 @@ def cached_template(
|
||||||
engine: Optional[Any] = None,
|
engine: Optional[Any] = None,
|
||||||
) -> Template:
|
) -> Template:
|
||||||
"""
|
"""
|
||||||
|
DEPRECATED. Template caching will be removed in v1.
|
||||||
|
|
||||||
Create a Template instance that will be cached as per the
|
Create a Template instance that will be cached as per the
|
||||||
[`COMPONENTS.template_cache_size`](../settings#django_components.app_settings.ComponentsSettings.template_cache_size)
|
[`COMPONENTS.template_cache_size`](../settings#django_components.app_settings.ComponentsSettings.template_cache_size)
|
||||||
setting.
|
setting.
|
||||||
|
@ -62,3 +76,400 @@ def cached_template(
|
||||||
template = maybe_cached_template
|
template = maybe_cached_template
|
||||||
|
|
||||||
return template
|
return template
|
||||||
|
|
||||||
|
|
||||||
|
########################################################
|
||||||
|
# PREPARING COMPONENT TEMPLATES FOR RENDERING
|
||||||
|
########################################################
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def prepare_component_template(
|
||||||
|
component: "Component",
|
||||||
|
template_data: Any,
|
||||||
|
) -> Generator[Optional[Template], Any, None]:
|
||||||
|
context = component.context
|
||||||
|
with context.update(template_data):
|
||||||
|
template = _get_component_template(component)
|
||||||
|
|
||||||
|
if template is None:
|
||||||
|
# If template is None, then the component is "template-less",
|
||||||
|
# and we skip template processing.
|
||||||
|
yield template
|
||||||
|
return
|
||||||
|
|
||||||
|
if not is_template_cls_patched(template):
|
||||||
|
raise RuntimeError(
|
||||||
|
"Django-components received a Template instance which was not patched."
|
||||||
|
"If you are using Django's Template class, check if you added django-components"
|
||||||
|
"to INSTALLED_APPS. If you are using a custom template class, then you need to"
|
||||||
|
"manually patch the class."
|
||||||
|
)
|
||||||
|
|
||||||
|
with _maybe_bind_template(context, template):
|
||||||
|
yield template
|
||||||
|
|
||||||
|
|
||||||
|
# `_maybe_bind_template()` handles two problems:
|
||||||
|
#
|
||||||
|
# 1. Initially, the binding the template was needed for the context processor data
|
||||||
|
# to work when using `RequestContext` (See `RequestContext.bind_template()` in e.g. Django v4.2 or v5.1).
|
||||||
|
# But as of djc v0.140 (possibly earlier) we generate and apply the context processor data
|
||||||
|
# ourselves in `Component._render_impl()`.
|
||||||
|
#
|
||||||
|
# Now, we still want to "bind the template" by setting the `Context.template` attribute.
|
||||||
|
# This is for compatibility with Django, because we don't know if there isn't some code that relies
|
||||||
|
# on the `Context.template` attribute being set.
|
||||||
|
#
|
||||||
|
# But we don't call `context.bind_template()` explicitly. If we did, then we would
|
||||||
|
# be generating and applying the context processor data twice if the context was `RequestContext`.
|
||||||
|
# Instead, we only run the same logic as `Context.bind_template()` but inlined.
|
||||||
|
#
|
||||||
|
# The downstream effect of this is that if the user or some third-party library
|
||||||
|
# uses custom subclass of `Context` with custom logic for `Context.bind_template()`,
|
||||||
|
# then this custom logic will NOT be applied. In such case they should open an issue.
|
||||||
|
#
|
||||||
|
# See https://github.com/django-components/django-components/issues/580
|
||||||
|
# and https://github.com/django-components/django-components/issues/634
|
||||||
|
#
|
||||||
|
# 2. Not sure if I (Juro) remember right, but I think that with the binding of templates
|
||||||
|
# there was also an issue that in *some* cases the template was already bound to the context
|
||||||
|
# by the time we got to rendering the component. This is why we need to check if `context.template`
|
||||||
|
# is already set.
|
||||||
|
#
|
||||||
|
# The cause of this may have been compatibility with Django's `{% extends %}` tag, or
|
||||||
|
# maybe when using the "isolated" context behavior. But not sure.
|
||||||
|
@contextmanager
|
||||||
|
def _maybe_bind_template(context: Context, template: Template) -> Generator[None, Any, None]:
|
||||||
|
if context.template is not None:
|
||||||
|
yield
|
||||||
|
return
|
||||||
|
|
||||||
|
# This code is taken from `Context.bind_template()` from Django v5.1
|
||||||
|
context.template = template
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
context.template = None
|
||||||
|
|
||||||
|
|
||||||
|
########################################################
|
||||||
|
# LOADING TEMPLATES FROM FILEPATH
|
||||||
|
########################################################
|
||||||
|
|
||||||
|
|
||||||
|
# Remember which Component class is currently being loaded
|
||||||
|
# This is important, because multiple Components may define the same `template_file`.
|
||||||
|
# So we need this global state to help us decide which Component class of the list of components
|
||||||
|
# that matched for the given `template_file` should be associated with the template.
|
||||||
|
#
|
||||||
|
# NOTE: Implemented as a list (stack) to handle the case when calling Django's `get_template()`
|
||||||
|
# could lead to more components being loaded at once.
|
||||||
|
# (For this to happen, user would have to define a Django template loader that renders other components
|
||||||
|
# while resolving the template file.)
|
||||||
|
loading_components: List["ComponentRef"] = []
|
||||||
|
|
||||||
|
|
||||||
|
def load_component_template(component_cls: Type["Component"], filepath: str) -> Template:
|
||||||
|
if component_cls._template is not None:
|
||||||
|
return component_cls._template
|
||||||
|
|
||||||
|
loading_components.append(ref(component_cls))
|
||||||
|
|
||||||
|
# Use Django's `get_template()` to load the template
|
||||||
|
template = _load_django_template(filepath)
|
||||||
|
|
||||||
|
# If template.origin.component_cls is already set, then this
|
||||||
|
# Template instance was cached by Django / template loaders.
|
||||||
|
# In that case we want to make a copy of the template which would
|
||||||
|
# be owned by the current Component class.
|
||||||
|
# Thus each Component has it's own Template instance with their own Origins
|
||||||
|
# pointing to the correct Component class.
|
||||||
|
if get_component_from_origin(template.origin) is not None:
|
||||||
|
origin_copy = Origin(template.origin.name, template.origin.template_name, template.origin.loader)
|
||||||
|
set_component_to_origin(origin_copy, component_cls)
|
||||||
|
template = Template(template.source, origin=origin_copy, name=template.name, engine=template.engine)
|
||||||
|
|
||||||
|
component_cls._template = template
|
||||||
|
|
||||||
|
loading_components.pop()
|
||||||
|
|
||||||
|
return template
|
||||||
|
|
||||||
|
|
||||||
|
def _get_component_template(component: "Component") -> Optional[Template]:
|
||||||
|
trace_component_msg("COMP_LOAD", component_name=component.name, component_id=component.id, slot_name=None)
|
||||||
|
|
||||||
|
# TODO_V1 - Remove, not needed once we remove `get_template_string()`, `get_template_name()`, `get_template()`
|
||||||
|
template_sources: Dict[str, Optional[Union[str, Template]]] = {}
|
||||||
|
|
||||||
|
# TODO_V1 - Remove `get_template_name()` in v1
|
||||||
|
template_sources["get_template_name"] = component.get_template_name(component.context)
|
||||||
|
|
||||||
|
# TODO_V1 - Remove `get_template_string()` in v1
|
||||||
|
if hasattr(component, "get_template_string"):
|
||||||
|
template_string_getter = getattr(component, "get_template_string")
|
||||||
|
template_body_from_getter = template_string_getter(component.context)
|
||||||
|
else:
|
||||||
|
template_body_from_getter = None
|
||||||
|
template_sources["get_template_string"] = template_body_from_getter
|
||||||
|
|
||||||
|
# TODO_V1 - Remove `get_template()` in v1
|
||||||
|
template_sources["get_template"] = component.get_template(component.context)
|
||||||
|
|
||||||
|
# NOTE: `component.template` should be populated whether user has set `template` or `template_file`
|
||||||
|
# so we discern between the two cases by checking `component.template_file`
|
||||||
|
if component.template_file is not None:
|
||||||
|
template_sources["template_file"] = component.template_file
|
||||||
|
else:
|
||||||
|
template_sources["template"] = component.template
|
||||||
|
|
||||||
|
# TODO_V1 - Remove this check in v1
|
||||||
|
# Raise if there are multiple sources for the component template
|
||||||
|
sources_with_values = [k for k, v in template_sources.items() if v is not None]
|
||||||
|
if len(sources_with_values) > 1:
|
||||||
|
raise ImproperlyConfigured(
|
||||||
|
f"Component template was set multiple times in Component {component.name}."
|
||||||
|
f"Sources: {sources_with_values}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Load the template based on the source
|
||||||
|
if template_sources["get_template_name"]:
|
||||||
|
template_name = template_sources["get_template_name"]
|
||||||
|
template: Optional[Template] = _load_django_template(template_name)
|
||||||
|
template_string: Optional[str] = None
|
||||||
|
elif template_sources["get_template_string"]:
|
||||||
|
template_string = template_sources["get_template_string"]
|
||||||
|
template = None
|
||||||
|
elif template_sources["get_template"]:
|
||||||
|
# `Component.get_template()` returns either string or Template instance
|
||||||
|
if hasattr(template_sources["get_template"], "render"):
|
||||||
|
template = template_sources["get_template"]
|
||||||
|
template_string = None
|
||||||
|
else:
|
||||||
|
template = None
|
||||||
|
template_string = template_sources["get_template"]
|
||||||
|
elif component.template or component.template_file:
|
||||||
|
# If the template was loaded from `Component.template_file`, then the Template
|
||||||
|
# instance was already created and cached in `Component._template`.
|
||||||
|
#
|
||||||
|
# NOTE: This is important to keep in mind, because the implication is that we should
|
||||||
|
# treat Templates AND their nodelists as IMMUTABLE.
|
||||||
|
if component.__class__._template is not None:
|
||||||
|
template = component.__class__._template
|
||||||
|
template_string = None
|
||||||
|
# Otherwise user have set `Component.template` as string and we still need to
|
||||||
|
# create the instance.
|
||||||
|
else:
|
||||||
|
template = _create_template_from_string(
|
||||||
|
component,
|
||||||
|
# NOTE: We can't reach this branch if `Component.template` is None
|
||||||
|
cast(str, component.template),
|
||||||
|
is_component_template=True,
|
||||||
|
)
|
||||||
|
template_string = None
|
||||||
|
# No template
|
||||||
|
else:
|
||||||
|
template = None
|
||||||
|
template_string = None
|
||||||
|
|
||||||
|
# We already have a template instance, so we can return it
|
||||||
|
if template is not None:
|
||||||
|
return template
|
||||||
|
# Create the template from the string
|
||||||
|
elif template_string is not None:
|
||||||
|
return _create_template_from_string(component, template_string)
|
||||||
|
|
||||||
|
# Otherwise, Component has no template - this is valid, as it may be instead rendered
|
||||||
|
# via `Component.on_render()`
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _create_template_from_string(
|
||||||
|
component: "Component",
|
||||||
|
template_string: str,
|
||||||
|
is_component_template: bool = False,
|
||||||
|
) -> Template:
|
||||||
|
# Generate a valid Origin instance.
|
||||||
|
# When an Origin instance is created by Django when using Django's loaders, it looks like this:
|
||||||
|
# ```
|
||||||
|
# {
|
||||||
|
# 'name': '/path/to/project/django-components/sampleproject/calendarapp/templates/calendarapp/calendar.html',
|
||||||
|
# 'template_name': 'calendarapp/calendar.html',
|
||||||
|
# 'loader': <django.template.loaders.app_directories.Loader object at 0x10b441d90>
|
||||||
|
# }
|
||||||
|
# ```
|
||||||
|
#
|
||||||
|
# Since our template is inlined, we will format as `filepath::ComponentName`
|
||||||
|
#
|
||||||
|
# ```
|
||||||
|
# /path/to/project/django-components/src/calendarapp/calendar.html::Calendar
|
||||||
|
# ```
|
||||||
|
#
|
||||||
|
# See https://docs.djangoproject.com/en/5.2/howto/custom-template-backend/#template-origin-api
|
||||||
|
_, _, module_filepath = get_module_info(component.__class__)
|
||||||
|
origin = Origin(
|
||||||
|
name=f"{module_filepath}::{component.__class__.__name__}",
|
||||||
|
template_name=None,
|
||||||
|
loader=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
set_component_to_origin(origin, component.__class__)
|
||||||
|
|
||||||
|
if is_component_template:
|
||||||
|
template = Template(template_string, name=origin.template_name, origin=origin)
|
||||||
|
component.__class__._template = template
|
||||||
|
else:
|
||||||
|
# TODO_V1 - `cached_template()` won't be needed as there will be only 1 template per component
|
||||||
|
# so we will be able to instead use `template_cache` to store the template
|
||||||
|
template = cached_template(
|
||||||
|
template_string=template_string,
|
||||||
|
name=origin.template_name,
|
||||||
|
origin=origin,
|
||||||
|
)
|
||||||
|
|
||||||
|
return template
|
||||||
|
|
||||||
|
|
||||||
|
# When loading a template, use Django's `get_template()` to ensure it triggers Django template loaders
|
||||||
|
# See https://github.com/django-components/django-components/issues/901
|
||||||
|
#
|
||||||
|
# This may raise `TemplateDoesNotExist` if the template doesn't exist.
|
||||||
|
# See https://docs.djangoproject.com/en/5.2/ref/templates/api/#template-loaders
|
||||||
|
# And https://docs.djangoproject.com/en/5.2/ref/templates/api/#custom-template-loaders
|
||||||
|
#
|
||||||
|
# TODO_v3 - Instead of loading templates with Django's `get_template()`,
|
||||||
|
# we should simply read the files directly (same as we do for JS and CSS).
|
||||||
|
# This has the implications that:
|
||||||
|
# - We would no longer support Django's template loaders
|
||||||
|
# - Instead if users are using template loaders, they should re-create them as djc extensions
|
||||||
|
# - We would no longer need to set `TEMPLATES.OPTIONS.loaders` to include
|
||||||
|
# `django_components.template_loader.Loader`
|
||||||
|
def _load_django_template(template_name: str) -> Template:
|
||||||
|
return django_get_template(template_name).template
|
||||||
|
|
||||||
|
|
||||||
|
########################################################
|
||||||
|
# ASSOCIATING COMPONENT CLASSES WITH TEMPLATES
|
||||||
|
#
|
||||||
|
# See https://github.com/django-components/django-components/pull/1222
|
||||||
|
########################################################
|
||||||
|
|
||||||
|
# NOTE: `ReferenceType` is NOT a generic pre-3.9
|
||||||
|
if sys.version_info >= (3, 9):
|
||||||
|
ComponentRef = ReferenceType[Type["Component"]]
|
||||||
|
else:
|
||||||
|
ComponentRef = ReferenceType
|
||||||
|
|
||||||
|
|
||||||
|
# Remember which Component classes defined `template_file`. Since multiple Components may
|
||||||
|
# define the same `template_file`, we store a list of weak references to the Component classes.
|
||||||
|
component_template_file_cache: Dict[str, List[ComponentRef]] = {}
|
||||||
|
component_template_file_cache_initialized = False
|
||||||
|
|
||||||
|
|
||||||
|
# Remember the mapping of `Component.template_file` -> `Component` class, so that we can associate
|
||||||
|
# the `Template` instances with the correct Component class in our monkepatched `Template.__init__()`.
|
||||||
|
def cache_component_template_file(component_cls: Type["Component"]) -> None:
|
||||||
|
# When a Component class is created before Django is set up,
|
||||||
|
# then `component_template_file_cache_initialized` is False and we leave it for later.
|
||||||
|
# This is necessary because:
|
||||||
|
# 1. We might need to resolve the template_file as relative to the file where the Component class is defined.
|
||||||
|
# 2. To be able to resolve the template_file, Django needs to be set up, because we need to access Django settings.
|
||||||
|
# 3. Django settings may not be available at the time of Component class creation.
|
||||||
|
if not component_template_file_cache_initialized:
|
||||||
|
return
|
||||||
|
|
||||||
|
# NOTE: Avoids circular import
|
||||||
|
from django_components.component_media import ComponentMedia, _resolve_component_relative_files
|
||||||
|
|
||||||
|
# If we access the `Component.template_file` attribute, then this triggers media resolution if it was not done yet.
|
||||||
|
# The problem is that this also causes the loading of the Template, if Component has defined `template_file`.
|
||||||
|
# This triggers `Template.__init__()`, which then triggers another call to `cache_component_template_file()`.
|
||||||
|
#
|
||||||
|
# At the same time, at this point we don't need the media files to be loaded. But we DO need for the relative
|
||||||
|
# file path to be resolved.
|
||||||
|
#
|
||||||
|
# So for this reason, `ComponentMedia.resolved_relative_files` was added to track if the media files were resolved.
|
||||||
|
# Once relative files were resolved, we can safely access the template file from `ComponentMedia` instance
|
||||||
|
# directly, thus avoiding the triggering of the Template loading.
|
||||||
|
comp_media: ComponentMedia = component_cls._component_media # type: ignore[attr-defined]
|
||||||
|
if comp_media.resolved and comp_media.resolved_relative_files:
|
||||||
|
template_file = component_cls.template_file
|
||||||
|
else:
|
||||||
|
# NOTE: This block of code is based on `_resolve_media()` in `component_media.py`
|
||||||
|
if not comp_media.resolved_relative_files:
|
||||||
|
comp_dirs = get_component_dirs()
|
||||||
|
_resolve_component_relative_files(component_cls, comp_media, comp_dirs=comp_dirs)
|
||||||
|
|
||||||
|
template_file = comp_media.template_file
|
||||||
|
|
||||||
|
if template_file is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
if template_file not in component_template_file_cache:
|
||||||
|
component_template_file_cache[template_file] = []
|
||||||
|
|
||||||
|
component_template_file_cache[template_file].append(ref(component_cls))
|
||||||
|
|
||||||
|
|
||||||
|
def get_component_by_template_file(template_file: str) -> Optional[Type["Component"]]:
|
||||||
|
# This function is called from within `Template.__init__()`. At that point, Django MUST be already set up,
|
||||||
|
# because Django's `Template.__init__()` accesses the templating engines.
|
||||||
|
#
|
||||||
|
# So at this point we want to call `cache_component_template_file()` for all Components for which
|
||||||
|
# we skipped it earlier.
|
||||||
|
global component_template_file_cache_initialized
|
||||||
|
if not component_template_file_cache_initialized:
|
||||||
|
component_template_file_cache_initialized = True
|
||||||
|
|
||||||
|
# NOTE: Avoids circular import
|
||||||
|
from django_components.component import all_components
|
||||||
|
|
||||||
|
components = all_components()
|
||||||
|
for component in components:
|
||||||
|
cache_component_template_file(component)
|
||||||
|
|
||||||
|
if template_file not in component_template_file_cache or not len(component_template_file_cache[template_file]):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# There is at least one Component class that has this `template_file`.
|
||||||
|
matched_component_refs = component_template_file_cache[template_file]
|
||||||
|
|
||||||
|
# There may be multiple components that define the same `template_file`.
|
||||||
|
# So to find the correct one, we need to check if the currently loading component
|
||||||
|
# is one of the ones that define the `template_file`.
|
||||||
|
#
|
||||||
|
# If there are NO currently loading components, then `Template.__init__()` was NOT triggered by us,
|
||||||
|
# in which case we don't associate any Component class with this Template.
|
||||||
|
if not len(loading_components):
|
||||||
|
return None
|
||||||
|
|
||||||
|
loading_component = loading_components[-1]()
|
||||||
|
if loading_component is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
for component_ref in matched_component_refs:
|
||||||
|
comp_cls = component_ref()
|
||||||
|
if comp_cls is loading_component:
|
||||||
|
return comp_cls
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# NOTE: Used by `@djc_test` to reset the component template file cache
|
||||||
|
def _reset_component_template_file_cache() -> None:
|
||||||
|
global component_template_file_cache
|
||||||
|
component_template_file_cache = {}
|
||||||
|
|
||||||
|
global component_template_file_cache_initialized
|
||||||
|
component_template_file_cache_initialized = False
|
||||||
|
|
||||||
|
|
||||||
|
# Helpers so we know where in the codebase we set / access the `Origin.component_cls` attribute
|
||||||
|
def set_component_to_origin(origin: Origin, component_cls: Type["Component"]) -> None:
|
||||||
|
origin.component_cls = component_cls
|
||||||
|
|
||||||
|
|
||||||
|
def get_component_from_origin(origin: Origin) -> Optional[Type["Component"]]:
|
||||||
|
return getattr(origin, "component_cls", None)
|
||||||
|
|
|
@ -10,7 +10,7 @@ from django.template.loaders.filesystem import Loader as FilesystemLoader
|
||||||
from django_components.util.loader import get_component_dirs
|
from django_components.util.loader import get_component_dirs
|
||||||
|
|
||||||
|
|
||||||
class Loader(FilesystemLoader):
|
class DjcLoader(FilesystemLoader):
|
||||||
def get_dirs(self, include_apps: bool = True) -> List[Path]:
|
def get_dirs(self, include_apps: bool = True) -> List[Path]:
|
||||||
"""
|
"""
|
||||||
Prepare directories that may contain component files:
|
Prepare directories that may contain component files:
|
||||||
|
@ -26,3 +26,10 @@ class Loader(FilesystemLoader):
|
||||||
`BASE_DIR` setting is required.
|
`BASE_DIR` setting is required.
|
||||||
"""
|
"""
|
||||||
return get_component_dirs(include_apps)
|
return get_component_dirs(include_apps)
|
||||||
|
|
||||||
|
|
||||||
|
# NOTE: Django's template loaders have the pattern of using the `Loader` class name.
|
||||||
|
# However, this then makes it harder to track and distinguish between different loaders.
|
||||||
|
# So internally we use the name `DjcLoader` instead.
|
||||||
|
# But for public API we use the name `Loader` to match Django.
|
||||||
|
Loader = DjcLoader
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
from typing import Any, Type
|
from typing import Any, Optional, Type
|
||||||
|
|
||||||
from django.template import Context, NodeList, Template
|
from django.template import Context, NodeList, Template
|
||||||
from django.template.base import Parser
|
from django.template.base import Origin, Parser
|
||||||
|
|
||||||
from django_components.context import _COMPONENT_CONTEXT_KEY, _STRATEGY_CONTEXT_KEY
|
from django_components.context import _COMPONENT_CONTEXT_KEY, _STRATEGY_CONTEXT_KEY
|
||||||
from django_components.dependencies import COMPONENT_COMMENT_REGEX, render_dependencies
|
from django_components.dependencies import COMPONENT_COMMENT_REGEX, render_dependencies
|
||||||
|
@ -10,11 +10,61 @@ from django_components.util.template_parser import parse_template
|
||||||
|
|
||||||
# In some cases we can't work around Django's design, and need to patch the template class.
|
# In some cases we can't work around Django's design, and need to patch the template class.
|
||||||
def monkeypatch_template_cls(template_cls: Type[Template]) -> None:
|
def monkeypatch_template_cls(template_cls: Type[Template]) -> None:
|
||||||
|
monkeypatch_template_init(template_cls)
|
||||||
monkeypatch_template_compile_nodelist(template_cls)
|
monkeypatch_template_compile_nodelist(template_cls)
|
||||||
monkeypatch_template_render(template_cls)
|
monkeypatch_template_render(template_cls)
|
||||||
template_cls._djc_patched = True
|
template_cls._djc_patched = True
|
||||||
|
|
||||||
|
|
||||||
|
# Patch `Template.__init__` to apply `extensions.on_template_preprocess()` if the template
|
||||||
|
# belongs to a Component.
|
||||||
|
def monkeypatch_template_init(template_cls: Type[Template]) -> None:
|
||||||
|
original_init = template_cls.__init__
|
||||||
|
|
||||||
|
# NOTE: Function signature of Template.__init__ hasn't changed in 11 years, so we can safely patch it.
|
||||||
|
# See https://github.com/django/django/blame/main/django/template/base.py#L139
|
||||||
|
def __init__(
|
||||||
|
self: Template,
|
||||||
|
template_string: Any,
|
||||||
|
origin: Optional[Origin] = None,
|
||||||
|
*args: Any,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> None:
|
||||||
|
# NOTE: Avoids circular import
|
||||||
|
from django_components.template import (
|
||||||
|
get_component_by_template_file,
|
||||||
|
get_component_from_origin,
|
||||||
|
set_component_to_origin,
|
||||||
|
)
|
||||||
|
|
||||||
|
# If this Template instance was created by us when loading a template file for a component
|
||||||
|
# with `load_component_template()`, then we do 2 things:
|
||||||
|
#
|
||||||
|
# 1. Associate the Component class with the template by setting it on the `Origin` instance
|
||||||
|
# (`template.origin.component_cls`). This way the `{% component%}` and `{% slot %}` tags
|
||||||
|
# will know inside which Component class they were defined.
|
||||||
|
#
|
||||||
|
# 2. Apply `extensions.on_template_preprocess()` to the template, so extensions can modify
|
||||||
|
# the template string before it's compiled into a nodelist.
|
||||||
|
if get_component_from_origin(origin) is not None:
|
||||||
|
component_cls = get_component_from_origin(origin)
|
||||||
|
elif origin is not None and origin.template_name is not None:
|
||||||
|
component_cls = get_component_by_template_file(origin.template_name)
|
||||||
|
if component_cls is not None:
|
||||||
|
set_component_to_origin(origin, component_cls)
|
||||||
|
else:
|
||||||
|
component_cls = None
|
||||||
|
|
||||||
|
if component_cls is not None:
|
||||||
|
# TODO - Apply extensions.on_template_preprocess() here.
|
||||||
|
# Then also test both cases when template as `template` or `template_file`.
|
||||||
|
pass
|
||||||
|
|
||||||
|
original_init(self, template_string, origin, *args, **kwargs) # type: ignore[misc]
|
||||||
|
|
||||||
|
template_cls.__init__ = __init__
|
||||||
|
|
||||||
|
|
||||||
# Patch `Template.compile_nodelist` to use our custom parser. Our parser makes it possible
|
# Patch `Template.compile_nodelist` to use our custom parser. Our parser makes it possible
|
||||||
# to use template tags as inputs to the component tag:
|
# to use template tags as inputs to the component tag:
|
||||||
#
|
#
|
||||||
|
@ -94,6 +144,8 @@ def monkeypatch_template_render(template_cls: Type[Template]) -> None:
|
||||||
# and `False` otherwise.
|
# and `False` otherwise.
|
||||||
isolated_context = not self._djc_is_component_nested
|
isolated_context = not self._djc_is_component_nested
|
||||||
|
|
||||||
|
# This is original implementation, except we override `isolated_context`,
|
||||||
|
# and we post-process the result with `render_dependencies()`.
|
||||||
with context.render_context.push_state(self, isolated_context=isolated_context):
|
with context.render_context.push_state(self, isolated_context=isolated_context):
|
||||||
if context.template is None:
|
if context.template is None:
|
||||||
with context.bind_template(self):
|
with context.bind_template(self):
|
||||||
|
|
|
@ -10,12 +10,14 @@ import django
|
||||||
from django.conf import settings as _django_settings
|
from django.conf import settings as _django_settings
|
||||||
from django.core.cache import BaseCache, caches
|
from django.core.cache import BaseCache, caches
|
||||||
from django.template import engines
|
from django.template import engines
|
||||||
|
from django.template.loaders.base import Loader
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
|
|
||||||
from django_components.component import ALL_COMPONENTS, Component, component_node_subclasses_by_name
|
from django_components.component import ALL_COMPONENTS, Component, component_node_subclasses_by_name
|
||||||
from django_components.component_media import ComponentMedia
|
from django_components.component_media import ComponentMedia
|
||||||
from django_components.component_registry import ALL_REGISTRIES, ComponentRegistry
|
from django_components.component_registry import ALL_REGISTRIES, ComponentRegistry
|
||||||
from django_components.extension import extensions
|
from django_components.extension import extensions
|
||||||
|
from django_components.template import _reset_component_template_file_cache, loading_components
|
||||||
|
|
||||||
# NOTE: `ReferenceType` is NOT a generic pre-3.9
|
# NOTE: `ReferenceType` is NOT a generic pre-3.9
|
||||||
if sys.version_info >= (3, 9):
|
if sys.version_info >= (3, 9):
|
||||||
|
@ -457,7 +459,9 @@ def _clear_djc_global_state(
|
||||||
# beause the IDs count will reset to 0, but we won't generate IDs for the Nodes of the cached
|
# beause the IDs count will reset to 0, but we won't generate IDs for the Nodes of the cached
|
||||||
# templates. Thus, the IDs will be out of sync between the tests.
|
# templates. Thus, the IDs will be out of sync between the tests.
|
||||||
for engine in engines.all():
|
for engine in engines.all():
|
||||||
engine.engine.template_loaders[0].reset()
|
for loader in engine.engine.template_loaders:
|
||||||
|
if isinstance(loader, Loader):
|
||||||
|
loader.reset()
|
||||||
|
|
||||||
# NOTE: There are 1-2 tests which check Templates, so we need to clear the cache
|
# NOTE: There are 1-2 tests which check Templates, so we need to clear the cache
|
||||||
from django_components.cache import component_media_cache, template_cache
|
from django_components.cache import component_media_cache, template_cache
|
||||||
|
@ -533,6 +537,10 @@ def _clear_djc_global_state(
|
||||||
# Clear extensions caches
|
# Clear extensions caches
|
||||||
extensions._route_to_url.clear()
|
extensions._route_to_url.clear()
|
||||||
|
|
||||||
|
# Clear other djc state
|
||||||
|
_reset_component_template_file_cache()
|
||||||
|
loading_components.clear()
|
||||||
|
|
||||||
# Clear Django caches
|
# Clear Django caches
|
||||||
all_caches: List[BaseCache] = list(caches.all())
|
all_caches: List[BaseCache] = list(caches.all())
|
||||||
for cache in all_caches:
|
for cache in all_caches:
|
||||||
|
|
|
@ -66,7 +66,6 @@ if not settings.configured:
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
COMPONENTS={
|
COMPONENTS={
|
||||||
"template_cache_size": 128,
|
|
||||||
"autodiscover": False,
|
"autodiscover": False,
|
||||||
"context_behavior": CONTEXT_MODE,
|
"context_behavior": CONTEXT_MODE,
|
||||||
},
|
},
|
||||||
|
|
|
@ -37,7 +37,6 @@ if not settings.configured:
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
COMPONENTS={
|
COMPONENTS={
|
||||||
"template_cache_size": 128,
|
|
||||||
"autodiscover": False,
|
"autodiscover": False,
|
||||||
"context_behavior": CONTEXT_MODE,
|
"context_behavior": CONTEXT_MODE,
|
||||||
},
|
},
|
||||||
|
|
|
@ -66,7 +66,6 @@ if not settings.configured:
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
COMPONENTS={
|
COMPONENTS={
|
||||||
"template_cache_size": 128,
|
|
||||||
"autodiscover": False,
|
"autodiscover": False,
|
||||||
"context_behavior": CONTEXT_MODE,
|
"context_behavior": CONTEXT_MODE,
|
||||||
},
|
},
|
||||||
|
|
|
@ -37,7 +37,6 @@ if not settings.configured:
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
COMPONENTS={
|
COMPONENTS={
|
||||||
"template_cache_size": 128,
|
|
||||||
"autodiscover": False,
|
"autodiscover": False,
|
||||||
"context_behavior": CONTEXT_MODE,
|
"context_behavior": CONTEXT_MODE,
|
||||||
},
|
},
|
||||||
|
|
|
@ -8,7 +8,6 @@ from typing import Any, NamedTuple
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django.template import Context, RequestContext, Template
|
from django.template import Context, RequestContext, Template
|
||||||
from django.template.base import TextNode
|
from django.template.base import TextNode
|
||||||
|
@ -26,6 +25,7 @@ from django_components import (
|
||||||
register,
|
register,
|
||||||
types,
|
types,
|
||||||
)
|
)
|
||||||
|
from django_components.template import _get_component_template
|
||||||
from django_components.urls import urlpatterns as dc_urlpatterns
|
from django_components.urls import urlpatterns as dc_urlpatterns
|
||||||
|
|
||||||
from django_components.testing import djc_test
|
from django_components.testing import djc_test
|
||||||
|
@ -152,23 +152,43 @@ class TestComponentLegacyApi:
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# TODO_v1 - Remove
|
||||||
@djc_test
|
|
||||||
class TestComponent:
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_empty_component(self, components_settings):
|
def test_get_template_name(self, components_settings):
|
||||||
class EmptyComponent(Component):
|
class SvgComponent(Component):
|
||||||
pass
|
def get_template_data(self, args, kwargs, slots, context):
|
||||||
|
return {
|
||||||
|
"name": kwargs.pop("name", None),
|
||||||
|
"css_class": kwargs.pop("css_class", None),
|
||||||
|
"title": kwargs.pop("title", None),
|
||||||
|
**kwargs,
|
||||||
|
}
|
||||||
|
|
||||||
with pytest.raises(ImproperlyConfigured):
|
def get_template_name(self, context):
|
||||||
EmptyComponent.render(args=["123"])
|
return f"dynamic_{context['name']}.svg"
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
assertHTMLEqual(
|
||||||
def test_template_string_static_inlined(self, components_settings):
|
SvgComponent.render(kwargs={"name": "svg1"}),
|
||||||
class SimpleComponent(Component):
|
|
||||||
template: types.django_html = """
|
|
||||||
Variable: <strong>{{ variable }}</strong>
|
|
||||||
"""
|
"""
|
||||||
|
<svg data-djc-id-ca1bc3e>Dynamic1</svg>
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
assertHTMLEqual(
|
||||||
|
SvgComponent.render(kwargs={"name": "svg2"}),
|
||||||
|
"""
|
||||||
|
<svg data-djc-id-ca1bc3f>Dynamic2</svg>
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
# TODO_v1 - Remove
|
||||||
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
|
def test_get_template__string(self, components_settings):
|
||||||
|
class SimpleComponent(Component):
|
||||||
|
def get_template(self, context):
|
||||||
|
content: types.django_html = """
|
||||||
|
Variable: <strong>{{ variable }}</strong>
|
||||||
|
"""
|
||||||
|
return content
|
||||||
|
|
||||||
def get_template_data(self, args, kwargs, slots, context):
|
def get_template_data(self, args, kwargs, slots, context):
|
||||||
return {
|
return {
|
||||||
|
@ -187,8 +207,29 @@ class TestComponent:
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# TODO_v1 - Remove
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_template_string_dynamic(self, components_settings):
|
def test_get_template__template(self, components_settings):
|
||||||
|
class TestComponent(Component):
|
||||||
|
def get_template_data(self, args, kwargs, slots, context):
|
||||||
|
return {
|
||||||
|
"variable": kwargs.pop("variable", None),
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_template(self, context):
|
||||||
|
template_str = "Variable: <strong>{{ variable }}</strong>"
|
||||||
|
return Template(template_str)
|
||||||
|
|
||||||
|
rendered = TestComponent.render(kwargs={"variable": "test"})
|
||||||
|
assertHTMLEqual(
|
||||||
|
rendered,
|
||||||
|
"""
|
||||||
|
Variable: <strong data-djc-id-ca1bc3e>test</strong>
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
# TODO_v1 - Remove
|
||||||
|
def test_get_template_is_cached(self):
|
||||||
class SimpleComponent(Component):
|
class SimpleComponent(Component):
|
||||||
def get_template(self, context):
|
def get_template(self, context):
|
||||||
content: types.django_html = """
|
content: types.django_html = """
|
||||||
|
@ -201,6 +242,35 @@ class TestComponent:
|
||||||
"variable": kwargs.get("variable", None),
|
"variable": kwargs.get("variable", None),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
comp = SimpleComponent()
|
||||||
|
template_1 = _get_component_template(comp)
|
||||||
|
template_1._test_id = "123" # type: ignore[union-attr]
|
||||||
|
|
||||||
|
template_2 = _get_component_template(comp)
|
||||||
|
assert template_2._test_id == "123" # type: ignore[union-attr]
|
||||||
|
|
||||||
|
|
||||||
|
@djc_test
|
||||||
|
class TestComponent:
|
||||||
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
|
def test_empty_component(self, components_settings):
|
||||||
|
class EmptyComponent(Component):
|
||||||
|
pass
|
||||||
|
|
||||||
|
EmptyComponent.render(args=["123"])
|
||||||
|
|
||||||
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
|
def test_template_string_static_inlined(self, components_settings):
|
||||||
|
class SimpleComponent(Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
Variable: <strong>{{ variable }}</strong>
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_template_data(self, args, kwargs, slots, context):
|
||||||
|
return {
|
||||||
|
"variable": kwargs.get("variable", None),
|
||||||
|
}
|
||||||
|
|
||||||
class Media:
|
class Media:
|
||||||
css = "style.css"
|
css = "style.css"
|
||||||
js = "script.js"
|
js = "script.js"
|
||||||
|
@ -235,6 +305,90 @@ class TestComponent:
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Test that even with cached template loaders, each Component has its own `Template`
|
||||||
|
# even when multiple components point to the same template file.
|
||||||
|
@djc_test(
|
||||||
|
parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR,
|
||||||
|
django_settings={
|
||||||
|
"TEMPLATES": [
|
||||||
|
{
|
||||||
|
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||||
|
"DIRS": [
|
||||||
|
"tests/templates/",
|
||||||
|
"tests/components/",
|
||||||
|
],
|
||||||
|
"OPTIONS": {
|
||||||
|
"builtins": [
|
||||||
|
"django_components.templatetags.component_tags",
|
||||||
|
],
|
||||||
|
'loaders': [
|
||||||
|
('django.template.loaders.cached.Loader', [
|
||||||
|
|
||||||
|
# Default Django loader
|
||||||
|
'django.template.loaders.filesystem.Loader',
|
||||||
|
# Including this is the same as APP_DIRS=True
|
||||||
|
'django.template.loaders.app_directories.Loader',
|
||||||
|
# Components loader
|
||||||
|
'django_components.template_loader.Loader',
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
def test_template_file_static__cached(self, components_settings):
|
||||||
|
class SimpleComponent1(Component):
|
||||||
|
template_file = "simple_template.html"
|
||||||
|
|
||||||
|
def get_template_data(self, args, kwargs, slots, context):
|
||||||
|
return {
|
||||||
|
"variable": kwargs.get("variable", None),
|
||||||
|
}
|
||||||
|
|
||||||
|
class SimpleComponent2(Component):
|
||||||
|
template_file = "simple_template.html"
|
||||||
|
|
||||||
|
def get_template_data(self, args, kwargs, slots, context):
|
||||||
|
return {
|
||||||
|
"variable": kwargs.get("variable", None),
|
||||||
|
}
|
||||||
|
|
||||||
|
SimpleComponent1.template # Triggers template loading
|
||||||
|
SimpleComponent2.template # Triggers template loading
|
||||||
|
|
||||||
|
# Both components have their own Template instance, but they point to the same template file.
|
||||||
|
assert isinstance(SimpleComponent1._template, Template)
|
||||||
|
assert isinstance(SimpleComponent2._template, Template)
|
||||||
|
assert SimpleComponent1._template is not SimpleComponent2._template
|
||||||
|
assert SimpleComponent1._template.source == SimpleComponent2._template.source
|
||||||
|
|
||||||
|
# The Template instances have different origins, but they point to the same template file.
|
||||||
|
assert SimpleComponent1._template.origin is not SimpleComponent2._template.origin
|
||||||
|
assert SimpleComponent1._template.origin.template_name == SimpleComponent2._template.origin.template_name
|
||||||
|
assert SimpleComponent1._template.origin.name == SimpleComponent2._template.origin.name
|
||||||
|
assert SimpleComponent1._template.origin.loader == SimpleComponent2._template.origin.loader
|
||||||
|
|
||||||
|
# The origins point to their respective Component classes.
|
||||||
|
assert SimpleComponent1._template.origin.component_cls == SimpleComponent1
|
||||||
|
assert SimpleComponent2._template.origin.component_cls == SimpleComponent2
|
||||||
|
|
||||||
|
rendered = SimpleComponent1.render(kwargs={"variable": "test"})
|
||||||
|
assertHTMLEqual(
|
||||||
|
rendered,
|
||||||
|
"""
|
||||||
|
Variable: <strong data-djc-id-ca1bc3e>test</strong>
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
rendered = SimpleComponent2.render(kwargs={"variable": "test"})
|
||||||
|
assertHTMLEqual(
|
||||||
|
rendered,
|
||||||
|
"""
|
||||||
|
Variable: <strong data-djc-id-ca1bc3f>test</strong>
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_template_file_static__compat(self, components_settings):
|
def test_template_file_static__compat(self, components_settings):
|
||||||
class SimpleComponent(Component):
|
class SimpleComponent(Component):
|
||||||
|
@ -288,53 +442,6 @@ class TestComponent:
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
|
||||||
def test_template_file_dynamic(self, components_settings):
|
|
||||||
class SvgComponent(Component):
|
|
||||||
def get_template_data(self, args, kwargs, slots, context):
|
|
||||||
return {
|
|
||||||
"name": kwargs.pop("name", None),
|
|
||||||
"css_class": kwargs.pop("css_class", None),
|
|
||||||
"title": kwargs.pop("title", None),
|
|
||||||
**kwargs,
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_template_name(self, context):
|
|
||||||
return f"dynamic_{context['name']}.svg"
|
|
||||||
|
|
||||||
assertHTMLEqual(
|
|
||||||
SvgComponent.render(kwargs={"name": "svg1"}),
|
|
||||||
"""
|
|
||||||
<svg data-djc-id-ca1bc3e>Dynamic1</svg>
|
|
||||||
""",
|
|
||||||
)
|
|
||||||
assertHTMLEqual(
|
|
||||||
SvgComponent.render(kwargs={"name": "svg2"}),
|
|
||||||
"""
|
|
||||||
<svg data-djc-id-ca1bc3f>Dynamic2</svg>
|
|
||||||
""",
|
|
||||||
)
|
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
|
||||||
def test_allows_to_return_template(self, components_settings):
|
|
||||||
class TestComponent(Component):
|
|
||||||
def get_template_data(self, args, kwargs, slots, context):
|
|
||||||
return {
|
|
||||||
"variable": kwargs.pop("variable", None),
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_template(self, context):
|
|
||||||
template_str = "Variable: <strong>{{ variable }}</strong>"
|
|
||||||
return Template(template_str)
|
|
||||||
|
|
||||||
rendered = TestComponent.render(kwargs={"variable": "test"})
|
|
||||||
assertHTMLEqual(
|
|
||||||
rendered,
|
|
||||||
"""
|
|
||||||
Variable: <strong data-djc-id-ca1bc3e>test</strong>
|
|
||||||
""",
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_get_component_by_id(self):
|
def test_get_component_by_id(self):
|
||||||
class SimpleComponent(Component):
|
class SimpleComponent(Component):
|
||||||
pass
|
pass
|
||||||
|
@ -369,6 +476,12 @@ class TestComponentRenderAPI:
|
||||||
|
|
||||||
def test_input(self):
|
def test_input(self):
|
||||||
class TestComponent(Component):
|
class TestComponent(Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
Variable: <strong>{{ variable }}</strong>
|
||||||
|
{% slot 'my_slot' / %}
|
||||||
|
"""
|
||||||
|
|
||||||
def get_template_data(self, args, kwargs, slots, context):
|
def get_template_data(self, args, kwargs, slots, context):
|
||||||
assert self.input.args == [123, "str"]
|
assert self.input.args == [123, "str"]
|
||||||
assert self.input.kwargs == {"variable": "test", "another": 1}
|
assert self.input.kwargs == {"variable": "test", "another": 1}
|
||||||
|
@ -381,7 +494,7 @@ class TestComponentRenderAPI:
|
||||||
"variable": kwargs["variable"],
|
"variable": kwargs["variable"],
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_template(self, context):
|
def on_render_before(self, context, template):
|
||||||
assert self.input.args == [123, "str"]
|
assert self.input.args == [123, "str"]
|
||||||
assert self.input.kwargs == {"variable": "test", "another": 1}
|
assert self.input.kwargs == {"variable": "test", "another": 1}
|
||||||
assert isinstance(self.input.context, Context)
|
assert isinstance(self.input.context, Context)
|
||||||
|
@ -389,13 +502,6 @@ class TestComponentRenderAPI:
|
||||||
my_slot = self.input.slots["my_slot"]
|
my_slot = self.input.slots["my_slot"]
|
||||||
assert my_slot() == "MY_SLOT"
|
assert my_slot() == "MY_SLOT"
|
||||||
|
|
||||||
template_str: types.django_html = """
|
|
||||||
{% load component_tags %}
|
|
||||||
Variable: <strong>{{ variable }}</strong>
|
|
||||||
{% slot 'my_slot' / %}
|
|
||||||
"""
|
|
||||||
return Template(template_str)
|
|
||||||
|
|
||||||
rendered = TestComponent.render(
|
rendered = TestComponent.render(
|
||||||
kwargs={"variable": "test", "another": 1},
|
kwargs={"variable": "test", "another": 1},
|
||||||
args=(123, "str"),
|
args=(123, "str"),
|
||||||
|
|
|
@ -210,17 +210,6 @@ class TestMainMedia:
|
||||||
assert AppLvlCompComponent._component_media.js == 'console.log("JS file");\n' # type: ignore[attr-defined]
|
assert AppLvlCompComponent._component_media.js == 'console.log("JS file");\n' # type: ignore[attr-defined]
|
||||||
assert AppLvlCompComponent._component_media.js_file == "app_lvl_comp/app_lvl_comp.js" # type: ignore[attr-defined]
|
assert AppLvlCompComponent._component_media.js_file == "app_lvl_comp/app_lvl_comp.js" # type: ignore[attr-defined]
|
||||||
|
|
||||||
def test_html_variable(self):
|
|
||||||
class VariableHTMLComponent(Component):
|
|
||||||
def get_template(self, context):
|
|
||||||
return Template("<div class='variable-html'>{{ variable }}</div>")
|
|
||||||
|
|
||||||
rendered = VariableHTMLComponent.render(context=Context({"variable": "Dynamic Content"}))
|
|
||||||
assertHTMLEqual(
|
|
||||||
rendered,
|
|
||||||
'<div class="variable-html" data-djc-id-ca1bc3e>Dynamic Content</div>',
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_html_variable_filtered(self):
|
def test_html_variable_filtered(self):
|
||||||
class FilteredComponent(Component):
|
class FilteredComponent(Component):
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
|
@ -851,40 +840,46 @@ class TestMediaStaticfiles:
|
||||||
|
|
||||||
@djc_test
|
@djc_test
|
||||||
class TestMediaRelativePath:
|
class TestMediaRelativePath:
|
||||||
class ParentComponent(Component):
|
def _gen_parent_component(self):
|
||||||
template: types.django_html = """
|
class ParentComponent(Component):
|
||||||
{% load component_tags %}
|
template: types.django_html = """
|
||||||
<div>
|
{% load component_tags %}
|
||||||
<h1>Parent content</h1>
|
<div>
|
||||||
{% component "variable_display" shadowing_variable='override' new_variable='unique_val' %}
|
<h1>Parent content</h1>
|
||||||
{% endcomponent %}
|
{% component "variable_display" shadowing_variable='override' new_variable='unique_val' %}
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{% slot 'content' %}
|
|
||||||
<h2>Slot content</h2>
|
|
||||||
{% component "variable_display" shadowing_variable='slot_default_override' new_variable='slot_default_unique' %}
|
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
{% endslot %}
|
</div>
|
||||||
</div>
|
<div>
|
||||||
""" # noqa
|
{% slot 'content' %}
|
||||||
|
<h2>Slot content</h2>
|
||||||
|
{% component "variable_display" shadowing_variable='slot_default_override' new_variable='slot_default_unique' %}
|
||||||
|
{% endcomponent %}
|
||||||
|
{% endslot %}
|
||||||
|
</div>
|
||||||
|
""" # noqa
|
||||||
|
|
||||||
def get_template_data(self, args, kwargs, slots, context):
|
def get_template_data(self, args, kwargs, slots, context):
|
||||||
return {"shadowing_variable": "NOT SHADOWED"}
|
return {"shadowing_variable": "NOT SHADOWED"}
|
||||||
|
|
||||||
class VariableDisplay(Component):
|
return ParentComponent
|
||||||
template: types.django_html = """
|
|
||||||
{% load component_tags %}
|
|
||||||
<h1>Shadowing variable = {{ shadowing_variable }}</h1>
|
|
||||||
<h1>Uniquely named variable = {{ unique_variable }}</h1>
|
|
||||||
"""
|
|
||||||
|
|
||||||
def get_template_data(self, args, kwargs, slots, context):
|
def _gen_variable_display_component(self):
|
||||||
context = {}
|
class VariableDisplay(Component):
|
||||||
if kwargs["shadowing_variable"] is not None:
|
template: types.django_html = """
|
||||||
context["shadowing_variable"] = kwargs["shadowing_variable"]
|
{% load component_tags %}
|
||||||
if kwargs["new_variable"] is not None:
|
<h1>Shadowing variable = {{ shadowing_variable }}</h1>
|
||||||
context["unique_variable"] = kwargs["new_variable"]
|
<h1>Uniquely named variable = {{ unique_variable }}</h1>
|
||||||
return context
|
"""
|
||||||
|
|
||||||
|
def get_template_data(self, args, kwargs, slots, context):
|
||||||
|
context = {}
|
||||||
|
if kwargs["shadowing_variable"] is not None:
|
||||||
|
context["shadowing_variable"] = kwargs["shadowing_variable"]
|
||||||
|
if kwargs["new_variable"] is not None:
|
||||||
|
context["unique_variable"] = kwargs["new_variable"]
|
||||||
|
return context
|
||||||
|
|
||||||
|
return VariableDisplay
|
||||||
|
|
||||||
# Settings required for autodiscover to work
|
# Settings required for autodiscover to work
|
||||||
@djc_test(
|
@djc_test(
|
||||||
|
@ -896,8 +891,8 @@ class TestMediaRelativePath:
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
def test_component_with_relative_media_paths(self):
|
def test_component_with_relative_media_paths(self):
|
||||||
registry.register(name="parent_component", component=self.ParentComponent)
|
registry.register(name="parent_component", component=self._gen_parent_component())
|
||||||
registry.register(name="variable_display", component=self.VariableDisplay)
|
registry.register(name="variable_display", component=self._gen_variable_display_component())
|
||||||
|
|
||||||
# Ensure that the module is executed again after import in autodiscovery
|
# Ensure that the module is executed again after import in autodiscovery
|
||||||
if "tests.components.relative_file.relative_file" in sys.modules:
|
if "tests.components.relative_file.relative_file" in sys.modules:
|
||||||
|
@ -948,8 +943,8 @@ class TestMediaRelativePath:
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
def test_component_with_relative_media_paths_as_subcomponent(self):
|
def test_component_with_relative_media_paths_as_subcomponent(self):
|
||||||
registry.register(name="parent_component", component=self.ParentComponent)
|
registry.register(name="parent_component", component=self._gen_parent_component())
|
||||||
registry.register(name="variable_display", component=self.VariableDisplay)
|
registry.register(name="variable_display", component=self._gen_variable_display_component())
|
||||||
|
|
||||||
# Ensure that the module is executed again after import in autodiscovery
|
# Ensure that the module is executed again after import in autodiscovery
|
||||||
if "tests.components.relative_file.relative_file" in sys.modules:
|
if "tests.components.relative_file.relative_file" in sys.modules:
|
||||||
|
@ -995,8 +990,8 @@ class TestMediaRelativePath:
|
||||||
|
|
||||||
https://github.com/django-components/django-components/issues/522#issuecomment-2173577094
|
https://github.com/django-components/django-components/issues/522#issuecomment-2173577094
|
||||||
"""
|
"""
|
||||||
registry.register(name="parent_component", component=self.ParentComponent)
|
registry.register(name="parent_component", component=self._gen_parent_component())
|
||||||
registry.register(name="variable_display", component=self.VariableDisplay)
|
registry.register(name="variable_display", component=self._gen_variable_display_component())
|
||||||
|
|
||||||
# Ensure that the module is executed again after import in autodiscovery
|
# Ensure that the module is executed again after import in autodiscovery
|
||||||
if "tests.components.relative_file_pathobj.relative_file_pathobj" in sys.modules:
|
if "tests.components.relative_file_pathobj.relative_file_pathobj" in sys.modules:
|
||||||
|
@ -1066,7 +1061,7 @@ class TestSubclassingMedia:
|
||||||
js = "grandparent.js"
|
js = "grandparent.js"
|
||||||
|
|
||||||
class ParentComponent(GrandParentComponent):
|
class ParentComponent(GrandParentComponent):
|
||||||
Media = None
|
Media = None # type: ignore[assignment]
|
||||||
|
|
||||||
class ChildComponent(ParentComponent):
|
class ChildComponent(ParentComponent):
|
||||||
class Media:
|
class Media:
|
||||||
|
@ -1149,7 +1144,7 @@ class TestSubclassingMedia:
|
||||||
js = "parent1.js"
|
js = "parent1.js"
|
||||||
|
|
||||||
class Parent2Component(GrandParent3Component, GrandParent4Component):
|
class Parent2Component(GrandParent3Component, GrandParent4Component):
|
||||||
Media = None
|
Media = None # type: ignore[assignment]
|
||||||
|
|
||||||
class ChildComponent(Parent1Component, Parent2Component):
|
class ChildComponent(Parent1Component, Parent2Component):
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
|
|
|
@ -24,58 +24,61 @@ def dummy_context_processor(request):
|
||||||
#########################
|
#########################
|
||||||
|
|
||||||
|
|
||||||
class SimpleComponent(Component):
|
def gen_simple_component():
|
||||||
template: types.django_html = """
|
class SimpleComponent(Component):
|
||||||
Variable: <strong>{{ variable }}</strong>
|
template: types.django_html = """
|
||||||
"""
|
Variable: <strong>{{ variable }}</strong>
|
||||||
|
"""
|
||||||
|
|
||||||
def get_template_data(self, args, kwargs, slots, context):
|
def get_template_data(self, args, kwargs, slots, context):
|
||||||
return {"variable": kwargs.get("variable", None)} if "variable" in kwargs else {}
|
return {"variable": kwargs.get("variable", None)} if "variable" in kwargs else {}
|
||||||
|
|
||||||
|
return SimpleComponent
|
||||||
|
|
||||||
|
|
||||||
class VariableDisplay(Component):
|
def gen_variable_display_component():
|
||||||
template: types.django_html = """
|
class VariableDisplay(Component):
|
||||||
{% load component_tags %}
|
template: types.django_html = """
|
||||||
<h1>Shadowing variable = {{ shadowing_variable }}</h1>
|
{% load component_tags %}
|
||||||
<h1>Uniquely named variable = {{ unique_variable }}</h1>
|
<h1>Shadowing variable = {{ shadowing_variable }}</h1>
|
||||||
"""
|
<h1>Uniquely named variable = {{ unique_variable }}</h1>
|
||||||
|
"""
|
||||||
|
|
||||||
def get_template_data(self, args, kwargs, slots, context):
|
def get_template_data(self, args, kwargs, slots, context):
|
||||||
context = {}
|
context = {}
|
||||||
if kwargs["shadowing_variable"] is not None:
|
if kwargs["shadowing_variable"] is not None:
|
||||||
context["shadowing_variable"] = kwargs["shadowing_variable"]
|
context["shadowing_variable"] = kwargs["shadowing_variable"]
|
||||||
if kwargs["new_variable"] is not None:
|
if kwargs["new_variable"] is not None:
|
||||||
context["unique_variable"] = kwargs["new_variable"]
|
context["unique_variable"] = kwargs["new_variable"]
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
return VariableDisplay
|
||||||
|
|
||||||
|
|
||||||
class IncrementerComponent(Component):
|
def gen_incrementer_component():
|
||||||
template: types.django_html = """
|
class IncrementerComponent(Component):
|
||||||
{% load component_tags %}
|
template: types.django_html = """
|
||||||
<p class="incrementer">value={{ value }};calls={{ calls }}</p>
|
{% load component_tags %}
|
||||||
{% slot 'content' %}{% endslot %}
|
<p class="incrementer">value={{ value }};calls={{ calls }}</p>
|
||||||
"""
|
{% slot 'content' %}{% endslot %}
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.call_count = 0
|
self.call_count = 0
|
||||||
|
|
||||||
def get_template_data(self, args, kwargs, slots, context):
|
def get_template_data(self, args, kwargs, slots, context):
|
||||||
value = int(kwargs.get("value", 0))
|
value = int(kwargs.get("value", 0))
|
||||||
if hasattr(self, "call_count"):
|
if hasattr(self, "call_count"):
|
||||||
self.call_count += 1
|
self.call_count += 1
|
||||||
else:
|
else:
|
||||||
self.call_count = 1
|
self.call_count = 1
|
||||||
return {"value": value + 1, "calls": self.call_count}
|
return {"value": value + 1, "calls": self.call_count}
|
||||||
|
|
||||||
|
return IncrementerComponent
|
||||||
|
|
||||||
|
|
||||||
#########################
|
def gen_parent_component():
|
||||||
# TESTS
|
|
||||||
#########################
|
|
||||||
|
|
||||||
|
|
||||||
@djc_test
|
|
||||||
class TestContext:
|
|
||||||
class ParentComponent(Component):
|
class ParentComponent(Component):
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
|
@ -96,129 +99,10 @@ class TestContext:
|
||||||
def get_template_data(self, args, kwargs, slots, context):
|
def get_template_data(self, args, kwargs, slots, context):
|
||||||
return {"shadowing_variable": "NOT SHADOWED"}
|
return {"shadowing_variable": "NOT SHADOWED"}
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
return ParentComponent
|
||||||
def test_nested_component_context_shadows_parent_with_unfilled_slots_and_component_tag(
|
|
||||||
self, components_settings,
|
|
||||||
):
|
|
||||||
registry.register(name="variable_display", component=VariableDisplay)
|
|
||||||
registry.register(name="parent_component", component=self.ParentComponent)
|
|
||||||
|
|
||||||
template_str: types.django_html = """
|
|
||||||
{% load component_tags %}
|
|
||||||
{% component 'parent_component' %}{% endcomponent %}
|
|
||||||
"""
|
|
||||||
template = Template(template_str)
|
|
||||||
rendered = template.render(Context())
|
|
||||||
|
|
||||||
assertInHTML("<h1 data-djc-id-ca1bc43>Shadowing variable = override</h1>", rendered)
|
|
||||||
assertInHTML("<h1 data-djc-id-ca1bc44>Shadowing variable = slot_default_override</h1>", rendered)
|
|
||||||
assert "Shadowing variable = NOT SHADOWED" not in rendered
|
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
|
||||||
def test_nested_component_instances_have_unique_context_with_unfilled_slots_and_component_tag(
|
|
||||||
self, components_settings,
|
|
||||||
):
|
|
||||||
registry.register(name="variable_display", component=VariableDisplay)
|
|
||||||
registry.register(name="parent_component", component=self.ParentComponent)
|
|
||||||
|
|
||||||
template_str: types.django_html = """
|
|
||||||
{% load component_tags %}
|
|
||||||
{% component 'parent_component' %}{% endcomponent %}
|
|
||||||
"""
|
|
||||||
template = Template(template_str)
|
|
||||||
rendered = template.render(Context())
|
|
||||||
|
|
||||||
assertInHTML("<h1 data-djc-id-ca1bc43>Uniquely named variable = unique_val</h1>", rendered)
|
|
||||||
assertInHTML(
|
|
||||||
"<h1 data-djc-id-ca1bc44>Uniquely named variable = slot_default_unique</h1>",
|
|
||||||
rendered,
|
|
||||||
)
|
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
|
||||||
def test_nested_component_context_shadows_parent_with_filled_slots(self, components_settings):
|
|
||||||
registry.register(name="variable_display", component=VariableDisplay)
|
|
||||||
registry.register(name="parent_component", component=self.ParentComponent)
|
|
||||||
|
|
||||||
template_str: types.django_html = """
|
|
||||||
{% load component_tags %}
|
|
||||||
{% component 'parent_component' %}
|
|
||||||
{% fill 'content' %}
|
|
||||||
{% component 'variable_display' shadowing_variable='shadow_from_slot' new_variable='unique_from_slot' %}
|
|
||||||
{% endcomponent %}
|
|
||||||
{% endfill %}
|
|
||||||
{% endcomponent %}
|
|
||||||
""" # NOQA
|
|
||||||
template = Template(template_str)
|
|
||||||
rendered = template.render(Context())
|
|
||||||
|
|
||||||
assertInHTML("<h1 data-djc-id-ca1bc45>Shadowing variable = override</h1>", rendered)
|
|
||||||
assertInHTML("<h1 data-djc-id-ca1bc46>Shadowing variable = shadow_from_slot</h1>", rendered)
|
|
||||||
assert "Shadowing variable = NOT SHADOWED" not in rendered
|
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
|
||||||
def test_nested_component_instances_have_unique_context_with_filled_slots(self, components_settings):
|
|
||||||
registry.register(name="variable_display", component=VariableDisplay)
|
|
||||||
registry.register(name="parent_component", component=self.ParentComponent)
|
|
||||||
|
|
||||||
template_str: types.django_html = """
|
|
||||||
{% load component_tags %}
|
|
||||||
{% component 'parent_component' %}
|
|
||||||
{% fill 'content' %}
|
|
||||||
{% component 'variable_display' shadowing_variable='shadow_from_slot' new_variable='unique_from_slot' %}
|
|
||||||
{% endcomponent %}
|
|
||||||
{% endfill %}
|
|
||||||
{% endcomponent %}
|
|
||||||
""" # NOQA
|
|
||||||
template = Template(template_str)
|
|
||||||
rendered = template.render(Context())
|
|
||||||
|
|
||||||
assertInHTML("<h1 data-djc-id-ca1bc45>Uniquely named variable = unique_val</h1>", rendered)
|
|
||||||
assertInHTML("<h1 data-djc-id-ca1bc46>Uniquely named variable = unique_from_slot</h1>", rendered)
|
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
|
||||||
def test_nested_component_context_shadows_outer_context_with_unfilled_slots_and_component_tag(
|
|
||||||
self, components_settings,
|
|
||||||
):
|
|
||||||
registry.register(name="variable_display", component=VariableDisplay)
|
|
||||||
registry.register(name="parent_component", component=self.ParentComponent)
|
|
||||||
|
|
||||||
template_str: types.django_html = """
|
|
||||||
{% load component_tags %}
|
|
||||||
{% component 'parent_component' %}{% endcomponent %}
|
|
||||||
"""
|
|
||||||
template = Template(template_str)
|
|
||||||
rendered = template.render(Context({"shadowing_variable": "NOT SHADOWED"}))
|
|
||||||
|
|
||||||
assertInHTML("<h1 data-djc-id-ca1bc43>Shadowing variable = override</h1>", rendered)
|
|
||||||
assertInHTML("<h1 data-djc-id-ca1bc44>Shadowing variable = slot_default_override</h1>", rendered)
|
|
||||||
assert "Shadowing variable = NOT SHADOWED" not in rendered
|
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
|
||||||
def test_nested_component_context_shadows_outer_context_with_filled_slots(
|
|
||||||
self, components_settings,
|
|
||||||
):
|
|
||||||
registry.register(name="variable_display", component=VariableDisplay)
|
|
||||||
registry.register(name="parent_component", component=self.ParentComponent)
|
|
||||||
|
|
||||||
template_str: types.django_html = """
|
|
||||||
{% load component_tags %}
|
|
||||||
{% component 'parent_component' %}
|
|
||||||
{% fill 'content' %}
|
|
||||||
{% component 'variable_display' shadowing_variable='shadow_from_slot' new_variable='unique_from_slot' %}
|
|
||||||
{% endcomponent %}
|
|
||||||
{% endfill %}
|
|
||||||
{% endcomponent %}
|
|
||||||
""" # NOQA
|
|
||||||
template = Template(template_str)
|
|
||||||
rendered = template.render(Context({"shadowing_variable": "NOT SHADOWED"}))
|
|
||||||
|
|
||||||
assertInHTML("<h1 data-djc-id-ca1bc45>Shadowing variable = override</h1>", rendered)
|
|
||||||
assertInHTML("<h1 data-djc-id-ca1bc46>Shadowing variable = shadow_from_slot</h1>", rendered)
|
|
||||||
assert "Shadowing variable = NOT SHADOWED" not in rendered
|
|
||||||
|
|
||||||
|
|
||||||
@djc_test
|
def gen_parent_component_with_args():
|
||||||
class TestParentArgs:
|
|
||||||
class ParentComponentWithArgs(Component):
|
class ParentComponentWithArgs(Component):
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
|
@ -239,11 +123,144 @@ class TestParentArgs:
|
||||||
def get_template_data(self, args, kwargs, slots, context):
|
def get_template_data(self, args, kwargs, slots, context):
|
||||||
return {"inner_parent_value": kwargs["parent_value"]}
|
return {"inner_parent_value": kwargs["parent_value"]}
|
||||||
|
|
||||||
|
return ParentComponentWithArgs
|
||||||
|
|
||||||
|
|
||||||
|
#########################
|
||||||
|
# TESTS
|
||||||
|
#########################
|
||||||
|
|
||||||
|
|
||||||
|
@djc_test
|
||||||
|
class TestContext:
|
||||||
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
|
def test_nested_component_context_shadows_parent_with_unfilled_slots_and_component_tag(
|
||||||
|
self, components_settings,
|
||||||
|
):
|
||||||
|
registry.register(name="variable_display", component=gen_variable_display_component())
|
||||||
|
registry.register(name="parent_component", component=gen_parent_component())
|
||||||
|
|
||||||
|
template_str: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% component 'parent_component' %}{% endcomponent %}
|
||||||
|
"""
|
||||||
|
template = Template(template_str)
|
||||||
|
rendered = template.render(Context())
|
||||||
|
|
||||||
|
assertInHTML("<h1 data-djc-id-ca1bc43>Shadowing variable = override</h1>", rendered)
|
||||||
|
assertInHTML("<h1 data-djc-id-ca1bc44>Shadowing variable = slot_default_override</h1>", rendered)
|
||||||
|
assert "Shadowing variable = NOT SHADOWED" not in rendered
|
||||||
|
|
||||||
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
|
def test_nested_component_instances_have_unique_context_with_unfilled_slots_and_component_tag(
|
||||||
|
self, components_settings,
|
||||||
|
):
|
||||||
|
registry.register(name="variable_display", component=gen_variable_display_component())
|
||||||
|
registry.register(name="parent_component", component=gen_parent_component())
|
||||||
|
|
||||||
|
template_str: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% component 'parent_component' %}{% endcomponent %}
|
||||||
|
"""
|
||||||
|
template = Template(template_str)
|
||||||
|
rendered = template.render(Context())
|
||||||
|
|
||||||
|
assertInHTML("<h1 data-djc-id-ca1bc43>Uniquely named variable = unique_val</h1>", rendered)
|
||||||
|
assertInHTML(
|
||||||
|
"<h1 data-djc-id-ca1bc44>Uniquely named variable = slot_default_unique</h1>",
|
||||||
|
rendered,
|
||||||
|
)
|
||||||
|
|
||||||
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
|
def test_nested_component_context_shadows_parent_with_filled_slots(self, components_settings):
|
||||||
|
registry.register(name="variable_display", component=gen_variable_display_component())
|
||||||
|
registry.register(name="parent_component", component=gen_parent_component())
|
||||||
|
|
||||||
|
template_str: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% component 'parent_component' %}
|
||||||
|
{% fill 'content' %}
|
||||||
|
{% component 'variable_display' shadowing_variable='shadow_from_slot' new_variable='unique_from_slot' %}
|
||||||
|
{% endcomponent %}
|
||||||
|
{% endfill %}
|
||||||
|
{% endcomponent %}
|
||||||
|
""" # NOQA
|
||||||
|
template = Template(template_str)
|
||||||
|
rendered = template.render(Context())
|
||||||
|
|
||||||
|
assertInHTML("<h1 data-djc-id-ca1bc45>Shadowing variable = override</h1>", rendered)
|
||||||
|
assertInHTML("<h1 data-djc-id-ca1bc46>Shadowing variable = shadow_from_slot</h1>", rendered)
|
||||||
|
assert "Shadowing variable = NOT SHADOWED" not in rendered
|
||||||
|
|
||||||
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
|
def test_nested_component_instances_have_unique_context_with_filled_slots(self, components_settings):
|
||||||
|
registry.register(name="variable_display", component=gen_variable_display_component())
|
||||||
|
registry.register(name="parent_component", component=gen_parent_component())
|
||||||
|
|
||||||
|
template_str: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% component 'parent_component' %}
|
||||||
|
{% fill 'content' %}
|
||||||
|
{% component 'variable_display' shadowing_variable='shadow_from_slot' new_variable='unique_from_slot' %}
|
||||||
|
{% endcomponent %}
|
||||||
|
{% endfill %}
|
||||||
|
{% endcomponent %}
|
||||||
|
""" # NOQA
|
||||||
|
template = Template(template_str)
|
||||||
|
rendered = template.render(Context())
|
||||||
|
|
||||||
|
assertInHTML("<h1 data-djc-id-ca1bc45>Uniquely named variable = unique_val</h1>", rendered)
|
||||||
|
assertInHTML("<h1 data-djc-id-ca1bc46>Uniquely named variable = unique_from_slot</h1>", rendered)
|
||||||
|
|
||||||
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
|
def test_nested_component_context_shadows_outer_context_with_unfilled_slots_and_component_tag(
|
||||||
|
self, components_settings,
|
||||||
|
):
|
||||||
|
registry.register(name="variable_display", component=gen_variable_display_component())
|
||||||
|
registry.register(name="parent_component", component=gen_parent_component())
|
||||||
|
|
||||||
|
template_str: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% component 'parent_component' %}{% endcomponent %}
|
||||||
|
"""
|
||||||
|
template = Template(template_str)
|
||||||
|
rendered = template.render(Context({"shadowing_variable": "NOT SHADOWED"}))
|
||||||
|
|
||||||
|
assertInHTML("<h1 data-djc-id-ca1bc43>Shadowing variable = override</h1>", rendered)
|
||||||
|
assertInHTML("<h1 data-djc-id-ca1bc44>Shadowing variable = slot_default_override</h1>", rendered)
|
||||||
|
assert "Shadowing variable = NOT SHADOWED" not in rendered
|
||||||
|
|
||||||
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
|
def test_nested_component_context_shadows_outer_context_with_filled_slots(
|
||||||
|
self, components_settings,
|
||||||
|
):
|
||||||
|
registry.register(name="variable_display", component=gen_variable_display_component())
|
||||||
|
registry.register(name="parent_component", component=gen_parent_component())
|
||||||
|
|
||||||
|
template_str: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% component 'parent_component' %}
|
||||||
|
{% fill 'content' %}
|
||||||
|
{% component 'variable_display' shadowing_variable='shadow_from_slot' new_variable='unique_from_slot' %}
|
||||||
|
{% endcomponent %}
|
||||||
|
{% endfill %}
|
||||||
|
{% endcomponent %}
|
||||||
|
""" # NOQA
|
||||||
|
template = Template(template_str)
|
||||||
|
rendered = template.render(Context({"shadowing_variable": "NOT SHADOWED"}))
|
||||||
|
|
||||||
|
assertInHTML("<h1 data-djc-id-ca1bc45>Shadowing variable = override</h1>", rendered)
|
||||||
|
assertInHTML("<h1 data-djc-id-ca1bc46>Shadowing variable = shadow_from_slot</h1>", rendered)
|
||||||
|
assert "Shadowing variable = NOT SHADOWED" not in rendered
|
||||||
|
|
||||||
|
|
||||||
|
@djc_test
|
||||||
|
class TestParentArgs:
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_parent_args_can_be_drawn_from_context(self, components_settings):
|
def test_parent_args_can_be_drawn_from_context(self, components_settings):
|
||||||
registry.register(name="incrementer", component=IncrementerComponent)
|
registry.register(name="incrementer", component=gen_incrementer_component())
|
||||||
registry.register(name="parent_with_args", component=self.ParentComponentWithArgs)
|
registry.register(name="parent_with_args", component=gen_parent_component_with_args())
|
||||||
registry.register(name="variable_display", component=VariableDisplay)
|
registry.register(name="variable_display", component=gen_variable_display_component())
|
||||||
|
|
||||||
template_str: types.django_html = """
|
template_str: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
|
@ -271,9 +288,9 @@ class TestParentArgs:
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_parent_args_available_outside_slots(self, components_settings):
|
def test_parent_args_available_outside_slots(self, components_settings):
|
||||||
registry.register(name="incrementer", component=IncrementerComponent)
|
registry.register(name="incrementer", component=gen_incrementer_component())
|
||||||
registry.register(name="parent_with_args", component=self.ParentComponentWithArgs)
|
registry.register(name="parent_with_args", component=gen_parent_component_with_args())
|
||||||
registry.register(name="variable_display", component=VariableDisplay)
|
registry.register(name="variable_display", component=gen_variable_display_component())
|
||||||
|
|
||||||
template_str: types.django_html = """
|
template_str: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
|
@ -282,8 +299,21 @@ class TestParentArgs:
|
||||||
template = Template(template_str)
|
template = Template(template_str)
|
||||||
rendered = template.render(Context())
|
rendered = template.render(Context())
|
||||||
|
|
||||||
assertInHTML("<h1 data-djc-id-ca1bc43>Shadowing variable = passed_in</h1>", rendered)
|
assertHTMLEqual(
|
||||||
assertInHTML("<h1 data-djc-id-ca1bc44>Uniquely named variable = passed_in</h1>", rendered)
|
rendered,
|
||||||
|
"""
|
||||||
|
<div data-djc-id-ca1bc3f>
|
||||||
|
<h1>Parent content</h1>
|
||||||
|
<h1 data-djc-id-ca1bc43>Shadowing variable = passed_in</h1>
|
||||||
|
<h1 data-djc-id-ca1bc43>Uniquely named variable = unique_val</h1>
|
||||||
|
</div>
|
||||||
|
<div data-djc-id-ca1bc3f>
|
||||||
|
<h2>Slot content</h2>
|
||||||
|
<h1 data-djc-id-ca1bc44>Shadowing variable = slot_default_override</h1>
|
||||||
|
<h1 data-djc-id-ca1bc44>Uniquely named variable = passed_in</h1>
|
||||||
|
</div>
|
||||||
|
""",
|
||||||
|
)
|
||||||
assert "Shadowing variable = NOT SHADOWED" not in rendered
|
assert "Shadowing variable = NOT SHADOWED" not in rendered
|
||||||
|
|
||||||
@djc_test(
|
@djc_test(
|
||||||
|
@ -297,9 +327,9 @@ class TestParentArgs:
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
def test_parent_args_available_in_slots(self, components_settings, first_val, second_val):
|
def test_parent_args_available_in_slots(self, components_settings, first_val, second_val):
|
||||||
registry.register(name="incrementer", component=IncrementerComponent)
|
registry.register(name="incrementer", component=gen_incrementer_component())
|
||||||
registry.register(name="parent_with_args", component=self.ParentComponentWithArgs)
|
registry.register(name="parent_with_args", component=gen_parent_component_with_args())
|
||||||
registry.register(name="variable_display", component=VariableDisplay)
|
registry.register(name="variable_display", component=gen_variable_display_component())
|
||||||
|
|
||||||
template_str: types.django_html = """
|
template_str: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
|
@ -333,7 +363,7 @@ class TestParentArgs:
|
||||||
class TestContextCalledOnce:
|
class TestContextCalledOnce:
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_one_context_call_with_simple_component(self, components_settings):
|
def test_one_context_call_with_simple_component(self, components_settings):
|
||||||
registry.register(name="incrementer", component=IncrementerComponent)
|
registry.register(name="incrementer", component=gen_incrementer_component())
|
||||||
template_str: types.django_html = """
|
template_str: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
{% component 'incrementer' %}{% endcomponent %}
|
{% component 'incrementer' %}{% endcomponent %}
|
||||||
|
@ -347,7 +377,7 @@ class TestContextCalledOnce:
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_one_context_call_with_simple_component_and_arg(self, components_settings):
|
def test_one_context_call_with_simple_component_and_arg(self, components_settings):
|
||||||
registry.register(name="incrementer", component=IncrementerComponent)
|
registry.register(name="incrementer", component=gen_incrementer_component())
|
||||||
template_str: types.django_html = """
|
template_str: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
{% component 'incrementer' value='2' %}{% endcomponent %}
|
{% component 'incrementer' value='2' %}{% endcomponent %}
|
||||||
|
@ -364,7 +394,7 @@ class TestContextCalledOnce:
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_one_context_call_with_component(self, components_settings):
|
def test_one_context_call_with_component(self, components_settings):
|
||||||
registry.register(name="incrementer", component=IncrementerComponent)
|
registry.register(name="incrementer", component=gen_incrementer_component())
|
||||||
template_str: types.django_html = """
|
template_str: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
{% component 'incrementer' %}{% endcomponent %}
|
{% component 'incrementer' %}{% endcomponent %}
|
||||||
|
@ -376,7 +406,7 @@ class TestContextCalledOnce:
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_one_context_call_with_component_and_arg(self, components_settings):
|
def test_one_context_call_with_component_and_arg(self, components_settings):
|
||||||
registry.register(name="incrementer", component=IncrementerComponent)
|
registry.register(name="incrementer", component=gen_incrementer_component())
|
||||||
template_str: types.django_html = """
|
template_str: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
{% component 'incrementer' value='3' %}{% endcomponent %}
|
{% component 'incrementer' value='3' %}{% endcomponent %}
|
||||||
|
@ -388,7 +418,7 @@ class TestContextCalledOnce:
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_one_context_call_with_slot(self, components_settings):
|
def test_one_context_call_with_slot(self, components_settings):
|
||||||
registry.register(name="incrementer", component=IncrementerComponent)
|
registry.register(name="incrementer", component=gen_incrementer_component())
|
||||||
template_str: types.django_html = """
|
template_str: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
{% component 'incrementer' %}
|
{% component 'incrementer' %}
|
||||||
|
@ -411,7 +441,7 @@ class TestContextCalledOnce:
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_one_context_call_with_slot_and_arg(self, components_settings):
|
def test_one_context_call_with_slot_and_arg(self, components_settings):
|
||||||
registry.register(name="incrementer", component=IncrementerComponent)
|
registry.register(name="incrementer", component=gen_incrementer_component())
|
||||||
template_str: types.django_html = """
|
template_str: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
{% component 'incrementer' value='3' %}
|
{% component 'incrementer' value='3' %}
|
||||||
|
@ -446,7 +476,7 @@ class TestComponentsCanAccessOuterContext:
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
def test_simple_component_can_use_outer_context(self, components_settings, expected_value):
|
def test_simple_component_can_use_outer_context(self, components_settings, expected_value):
|
||||||
registry.register(name="simple_component", component=SimpleComponent)
|
registry.register(name="simple_component", component=gen_simple_component())
|
||||||
template_str: types.django_html = """
|
template_str: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
{% component 'simple_component' %}{% endcomponent %}
|
{% component 'simple_component' %}{% endcomponent %}
|
||||||
|
@ -465,7 +495,7 @@ class TestComponentsCanAccessOuterContext:
|
||||||
class TestIsolatedContext:
|
class TestIsolatedContext:
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_simple_component_can_pass_outer_context_in_args(self, components_settings):
|
def test_simple_component_can_pass_outer_context_in_args(self, components_settings):
|
||||||
registry.register(name="simple_component", component=SimpleComponent)
|
registry.register(name="simple_component", component=gen_simple_component())
|
||||||
template_str: types.django_html = """
|
template_str: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
{% component 'simple_component' variable=variable only %}{% endcomponent %}
|
{% component 'simple_component' variable=variable only %}{% endcomponent %}
|
||||||
|
@ -476,7 +506,7 @@ class TestIsolatedContext:
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_simple_component_cannot_use_outer_context(self, components_settings):
|
def test_simple_component_cannot_use_outer_context(self, components_settings):
|
||||||
registry.register(name="simple_component", component=SimpleComponent)
|
registry.register(name="simple_component", component=gen_simple_component())
|
||||||
template_str: types.django_html = """
|
template_str: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
{% component 'simple_component' only %}{% endcomponent %}
|
{% component 'simple_component' only %}{% endcomponent %}
|
||||||
|
@ -492,7 +522,7 @@ class TestIsolatedContextSetting:
|
||||||
def test_component_tag_includes_variable_with_isolated_context_from_settings(
|
def test_component_tag_includes_variable_with_isolated_context_from_settings(
|
||||||
self,
|
self,
|
||||||
):
|
):
|
||||||
registry.register(name="simple_component", component=SimpleComponent)
|
registry.register(name="simple_component", component=gen_simple_component())
|
||||||
template_str: types.django_html = """
|
template_str: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
{% component 'simple_component' variable=variable %}{% endcomponent %}
|
{% component 'simple_component' variable=variable %}{% endcomponent %}
|
||||||
|
@ -505,7 +535,7 @@ class TestIsolatedContextSetting:
|
||||||
def test_component_tag_excludes_variable_with_isolated_context_from_settings(
|
def test_component_tag_excludes_variable_with_isolated_context_from_settings(
|
||||||
self,
|
self,
|
||||||
):
|
):
|
||||||
registry.register(name="simple_component", component=SimpleComponent)
|
registry.register(name="simple_component", component=gen_simple_component())
|
||||||
template_str: types.django_html = """
|
template_str: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
{% component 'simple_component' %}{% endcomponent %}
|
{% component 'simple_component' %}{% endcomponent %}
|
||||||
|
@ -518,7 +548,7 @@ class TestIsolatedContextSetting:
|
||||||
def test_component_includes_variable_with_isolated_context_from_settings(
|
def test_component_includes_variable_with_isolated_context_from_settings(
|
||||||
self,
|
self,
|
||||||
):
|
):
|
||||||
registry.register(name="simple_component", component=SimpleComponent)
|
registry.register(name="simple_component", component=gen_simple_component())
|
||||||
template_str: types.django_html = """
|
template_str: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
{% component 'simple_component' variable=variable %}
|
{% component 'simple_component' variable=variable %}
|
||||||
|
@ -532,7 +562,7 @@ class TestIsolatedContextSetting:
|
||||||
def test_component_excludes_variable_with_isolated_context_from_settings(
|
def test_component_excludes_variable_with_isolated_context_from_settings(
|
||||||
self,
|
self,
|
||||||
):
|
):
|
||||||
registry.register(name="simple_component", component=SimpleComponent)
|
registry.register(name="simple_component", component=gen_simple_component())
|
||||||
template_str: types.django_html = """
|
template_str: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
{% component 'simple_component' %}
|
{% component 'simple_component' %}
|
||||||
|
|
|
@ -919,11 +919,11 @@ class TestSignatureBasedValidation:
|
||||||
template3.render(Context({}))
|
template3.render(Context({}))
|
||||||
|
|
||||||
params3, nodelist3, node_id3, contents3 = captured # type: ignore
|
params3, nodelist3, node_id3, contents3 = captured # type: ignore
|
||||||
assert len(params3) == 1
|
assert len(params3) == 1 # type: ignore
|
||||||
assert isinstance(params3[0], TagAttr)
|
assert isinstance(params3[0], TagAttr) # type: ignore
|
||||||
assert len(nodelist3) == 0
|
assert len(nodelist3) == 0 # type: ignore
|
||||||
assert contents3 is None
|
assert contents3 is None # type: ignore
|
||||||
assert node_id3 == "a1bc40"
|
assert node_id3 == "a1bc40" # type: ignore
|
||||||
|
|
||||||
# Cleanup
|
# Cleanup
|
||||||
TestNodeWithEndTag.unregister(component_tags.register)
|
TestNodeWithEndTag.unregister(component_tags.register)
|
||||||
|
|
|
@ -10,10 +10,6 @@ from .testutils import PARAMETRIZE_CONTEXT_BEHAVIOR, setup_test_config
|
||||||
setup_test_config({"autodiscover": False})
|
setup_test_config({"autodiscover": False})
|
||||||
|
|
||||||
|
|
||||||
class SlottedComponent(Component):
|
|
||||||
template_file = "slotted_template.html"
|
|
||||||
|
|
||||||
|
|
||||||
def _get_templates_used_to_render(subject_template, render_context=None):
|
def _get_templates_used_to_render(subject_template, render_context=None):
|
||||||
"""Emulate django.test.client.Client (see request method)."""
|
"""Emulate django.test.client.Client (see request method)."""
|
||||||
from django.test.signals import template_rendered
|
from django.test.signals import template_rendered
|
||||||
|
@ -48,24 +44,33 @@ def with_template_signal(func):
|
||||||
|
|
||||||
@djc_test
|
@djc_test
|
||||||
class TestTemplateSignal:
|
class TestTemplateSignal:
|
||||||
class InnerComponent(Component):
|
def gen_slotted_component(self):
|
||||||
template_file = "simple_template.html"
|
class SlottedComponent(Component):
|
||||||
|
template_file = "slotted_template.html"
|
||||||
|
|
||||||
def get_template_data(self, args, kwargs, slots, context):
|
return SlottedComponent
|
||||||
return {
|
|
||||||
"variable": kwargs["variable"],
|
|
||||||
"variable2": kwargs.get("variable2", "default"),
|
|
||||||
}
|
|
||||||
|
|
||||||
class Media:
|
def gen_inner_component(self):
|
||||||
css = "style.css"
|
class InnerComponent(Component):
|
||||||
js = "script.js"
|
template_file = "simple_template.html"
|
||||||
|
|
||||||
|
def get_template_data(self, args, kwargs, slots, context):
|
||||||
|
return {
|
||||||
|
"variable": kwargs["variable"],
|
||||||
|
"variable2": kwargs.get("variable2", "default"),
|
||||||
|
}
|
||||||
|
|
||||||
|
class Media:
|
||||||
|
css = "style.css"
|
||||||
|
js = "script.js"
|
||||||
|
|
||||||
|
return InnerComponent
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
@with_template_signal
|
@with_template_signal
|
||||||
def test_template_rendered(self, components_settings):
|
def test_template_rendered(self, components_settings):
|
||||||
registry.register("test_component", SlottedComponent)
|
registry.register("test_component", self.gen_slotted_component())
|
||||||
registry.register("inner_component", self.InnerComponent)
|
registry.register("inner_component", self.gen_inner_component())
|
||||||
template_str: types.django_html = """
|
template_str: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
{% component 'test_component' %}{% endcomponent %}
|
{% component 'test_component' %}{% endcomponent %}
|
||||||
|
@ -77,8 +82,8 @@ class TestTemplateSignal:
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
@with_template_signal
|
@with_template_signal
|
||||||
def test_template_rendered_nested_components(self, components_settings):
|
def test_template_rendered_nested_components(self, components_settings):
|
||||||
registry.register("test_component", SlottedComponent)
|
registry.register("test_component", self.gen_slotted_component())
|
||||||
registry.register("inner_component", self.InnerComponent)
|
registry.register("inner_component", self.gen_inner_component())
|
||||||
template_str: types.django_html = """
|
template_str: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
{% component 'test_component' %}
|
{% component 'test_component' %}
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
from django.template import Context, Template
|
from django.template import Template
|
||||||
|
|
||||||
from django_components import Component, cached_template, types
|
from django_components import Component, cached_template, types
|
||||||
|
|
||||||
|
from django_components.template import _get_component_template
|
||||||
from django_components.testing import djc_test
|
from django_components.testing import djc_test
|
||||||
from .testutils import setup_test_config
|
from .testutils import setup_test_config
|
||||||
|
|
||||||
|
@ -10,6 +11,7 @@ setup_test_config({"autodiscover": False})
|
||||||
|
|
||||||
@djc_test
|
@djc_test
|
||||||
class TestTemplateCache:
|
class TestTemplateCache:
|
||||||
|
# TODO_v1 - Remove
|
||||||
def test_cached_template(self):
|
def test_cached_template(self):
|
||||||
template_1 = cached_template("Variable: <strong>{{ variable }}</strong>")
|
template_1 = cached_template("Variable: <strong>{{ variable }}</strong>")
|
||||||
template_1._test_id = "123"
|
template_1._test_id = "123"
|
||||||
|
@ -18,6 +20,7 @@ class TestTemplateCache:
|
||||||
|
|
||||||
assert template_2._test_id == "123"
|
assert template_2._test_id == "123"
|
||||||
|
|
||||||
|
# TODO_v1 - Remove
|
||||||
def test_cached_template_accepts_class(self):
|
def test_cached_template_accepts_class(self):
|
||||||
class MyTemplate(Template):
|
class MyTemplate(Template):
|
||||||
pass
|
pass
|
||||||
|
@ -25,6 +28,8 @@ class TestTemplateCache:
|
||||||
template = cached_template("Variable: <strong>{{ variable }}</strong>", MyTemplate)
|
template = cached_template("Variable: <strong>{{ variable }}</strong>", MyTemplate)
|
||||||
assert isinstance(template, MyTemplate)
|
assert isinstance(template, MyTemplate)
|
||||||
|
|
||||||
|
# TODO_v1 - Move to `test_component.py`. While `cached_template()` will be removed,
|
||||||
|
# we will internally still cache templates by class, and we will want to test for that.
|
||||||
def test_component_template_is_cached(self):
|
def test_component_template_is_cached(self):
|
||||||
class SimpleComponent(Component):
|
class SimpleComponent(Component):
|
||||||
def get_template(self, context):
|
def get_template(self, context):
|
||||||
|
@ -38,9 +43,11 @@ class TestTemplateCache:
|
||||||
"variable": kwargs.get("variable", None),
|
"variable": kwargs.get("variable", None),
|
||||||
}
|
}
|
||||||
|
|
||||||
comp = SimpleComponent()
|
comp = SimpleComponent(kwargs={"variable": "test"})
|
||||||
template_1 = comp._get_template(Context({}), component_id="123")
|
|
||||||
template_1._test_id = "123"
|
|
||||||
|
|
||||||
template_2 = comp._get_template(Context({}), component_id="123")
|
# Check that we get the same template instance
|
||||||
assert template_2._test_id == "123"
|
template_1 = _get_component_template(comp)
|
||||||
|
template_1._test_id = "123" # type: ignore[union-attr]
|
||||||
|
|
||||||
|
template_2 = _get_component_template(comp)
|
||||||
|
assert template_2._test_id == "123" # type: ignore[union-attr]
|
||||||
|
|
|
@ -10,10 +10,6 @@ from .testutils import PARAMETRIZE_CONTEXT_BEHAVIOR, setup_test_config
|
||||||
setup_test_config({"autodiscover": False})
|
setup_test_config({"autodiscover": False})
|
||||||
|
|
||||||
|
|
||||||
class SlottedComponent(Component):
|
|
||||||
template_file = "slotted_template.html"
|
|
||||||
|
|
||||||
|
|
||||||
#######################
|
#######################
|
||||||
# TESTS
|
# TESTS
|
||||||
#######################
|
#######################
|
||||||
|
|
|
@ -12,22 +12,28 @@ from .testutils import PARAMETRIZE_CONTEXT_BEHAVIOR, setup_test_config
|
||||||
setup_test_config({"autodiscover": False})
|
setup_test_config({"autodiscover": False})
|
||||||
|
|
||||||
|
|
||||||
class SlottedComponent(Component):
|
def gen_slotted_component():
|
||||||
template_file = "slotted_template.html"
|
class SlottedComponent(Component):
|
||||||
|
template_file = "slotted_template.html"
|
||||||
|
|
||||||
|
return SlottedComponent
|
||||||
|
|
||||||
|
|
||||||
class SlottedComponentWithContext(Component):
|
def gen_slotted_component_with_context():
|
||||||
template: types.django_html = """
|
class SlottedComponentWithContext(Component):
|
||||||
{% load component_tags %}
|
template: types.django_html = """
|
||||||
<custom-template>
|
{% load component_tags %}
|
||||||
<header>{% slot "header" %}Default header{% endslot %}</header>
|
<custom-template>
|
||||||
<main>{% slot "main" %}Default main{% endslot %}</main>
|
<header>{% slot "header" %}Default header{% endslot %}</header>
|
||||||
<footer>{% slot "footer" %}Default footer{% endslot %}</footer>
|
<main>{% slot "main" %}Default main{% endslot %}</main>
|
||||||
</custom-template>
|
<footer>{% slot "footer" %}Default footer{% endslot %}</footer>
|
||||||
"""
|
</custom-template>
|
||||||
|
"""
|
||||||
|
|
||||||
def get_template_data(self, args, kwargs, slots, context):
|
def get_template_data(self, args, kwargs, slots, context):
|
||||||
return {"variable": kwargs["variable"]}
|
return {"variable": kwargs["variable"]}
|
||||||
|
|
||||||
|
return SlottedComponentWithContext
|
||||||
|
|
||||||
|
|
||||||
#######################
|
#######################
|
||||||
|
@ -522,8 +528,8 @@ class TestDynamicComponentTemplateTag:
|
||||||
class TestMultiComponent:
|
class TestMultiComponent:
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_both_components_render_correctly_with_no_slots(self, components_settings):
|
def test_both_components_render_correctly_with_no_slots(self, components_settings):
|
||||||
registry.register("first_component", SlottedComponent)
|
registry.register("first_component", gen_slotted_component())
|
||||||
registry.register("second_component", SlottedComponentWithContext)
|
registry.register("second_component", gen_slotted_component_with_context())
|
||||||
|
|
||||||
template_str: types.django_html = """
|
template_str: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
|
@ -557,8 +563,8 @@ class TestMultiComponent:
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_both_components_render_correctly_with_slots(self, components_settings):
|
def test_both_components_render_correctly_with_slots(self, components_settings):
|
||||||
registry.register("first_component", SlottedComponent)
|
registry.register("first_component", gen_slotted_component())
|
||||||
registry.register("second_component", SlottedComponentWithContext)
|
registry.register("second_component", gen_slotted_component_with_context())
|
||||||
|
|
||||||
template_str: types.django_html = """
|
template_str: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
|
@ -594,8 +600,8 @@ class TestMultiComponent:
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_both_components_render_correctly_when_only_first_has_slots(self, components_settings):
|
def test_both_components_render_correctly_when_only_first_has_slots(self, components_settings):
|
||||||
registry.register("first_component", SlottedComponent)
|
registry.register("first_component", gen_slotted_component())
|
||||||
registry.register("second_component", SlottedComponentWithContext)
|
registry.register("second_component", gen_slotted_component_with_context())
|
||||||
|
|
||||||
template_str: types.django_html = """
|
template_str: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
|
@ -630,8 +636,8 @@ class TestMultiComponent:
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_both_components_render_correctly_when_only_second_has_slots(self, components_settings):
|
def test_both_components_render_correctly_when_only_second_has_slots(self, components_settings):
|
||||||
registry.register("first_component", SlottedComponent)
|
registry.register("first_component", gen_slotted_component())
|
||||||
registry.register("second_component", SlottedComponentWithContext)
|
registry.register("second_component", gen_slotted_component_with_context())
|
||||||
|
|
||||||
template_str: types.django_html = """
|
template_str: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
|
@ -667,19 +673,19 @@ class TestMultiComponent:
|
||||||
|
|
||||||
@djc_test
|
@djc_test
|
||||||
class TestComponentIsolation:
|
class TestComponentIsolation:
|
||||||
class SlottedComponent(Component):
|
|
||||||
template: types.django_html = """
|
|
||||||
{% load component_tags %}
|
|
||||||
<custom-template>
|
|
||||||
<header>{% slot "header" %}Default header{% endslot %}</header>
|
|
||||||
<main>{% slot "main" %}Default main{% endslot %}</main>
|
|
||||||
<footer>{% slot "footer" %}Default footer{% endslot %}</footer>
|
|
||||||
</custom-template>
|
|
||||||
"""
|
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_instances_of_component_do_not_share_slots(self, components_settings):
|
def test_instances_of_component_do_not_share_slots(self, components_settings):
|
||||||
registry.register("test", self.SlottedComponent)
|
@register("test")
|
||||||
|
class SlottedComponent(Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
<custom-template>
|
||||||
|
<header>{% slot "header" %}Default header{% endslot %}</header>
|
||||||
|
<main>{% slot "main" %}Default main{% endslot %}</main>
|
||||||
|
<footer>{% slot "footer" %}Default footer{% endslot %}</footer>
|
||||||
|
</custom-template>
|
||||||
|
"""
|
||||||
|
|
||||||
template_str: types.django_html = """
|
template_str: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
{% component "test" %}
|
{% component "test" %}
|
||||||
|
@ -791,7 +797,7 @@ class TestRecursiveComponent:
|
||||||
class TestComponentTemplateSyntaxError:
|
class TestComponentTemplateSyntaxError:
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_variable_outside_fill_tag_compiles_w_out_error(self, components_settings):
|
def test_variable_outside_fill_tag_compiles_w_out_error(self, components_settings):
|
||||||
registry.register("test", SlottedComponent)
|
registry.register("test", gen_slotted_component())
|
||||||
# As of v0.28 this is valid, provided the component registered under "test"
|
# As of v0.28 this is valid, provided the component registered under "test"
|
||||||
# contains a slot tag marked as 'default'. This is verified outside
|
# contains a slot tag marked as 'default'. This is verified outside
|
||||||
# template compilation time.
|
# template compilation time.
|
||||||
|
@ -805,7 +811,7 @@ class TestComponentTemplateSyntaxError:
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_text_outside_fill_tag_is_not_error_when_no_fill_tags(self, components_settings):
|
def test_text_outside_fill_tag_is_not_error_when_no_fill_tags(self, components_settings):
|
||||||
registry.register("test", SlottedComponent)
|
registry.register("test", gen_slotted_component())
|
||||||
# As of v0.28 this is valid, provided the component registered under "test"
|
# As of v0.28 this is valid, provided the component registered under "test"
|
||||||
# contains a slot tag marked as 'default'. This is verified outside
|
# contains a slot tag marked as 'default'. This is verified outside
|
||||||
# template compilation time.
|
# template compilation time.
|
||||||
|
@ -819,7 +825,7 @@ class TestComponentTemplateSyntaxError:
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_text_outside_fill_tag_is_error_when_fill_tags(self, components_settings):
|
def test_text_outside_fill_tag_is_error_when_fill_tags(self, components_settings):
|
||||||
registry.register("test", SlottedComponent)
|
registry.register("test", gen_slotted_component())
|
||||||
template_str: types.django_html = """
|
template_str: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
{% component "test" %}
|
{% component "test" %}
|
||||||
|
@ -837,7 +843,7 @@ class TestComponentTemplateSyntaxError:
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_unclosed_component_is_error(self, components_settings):
|
def test_unclosed_component_is_error(self, components_settings):
|
||||||
registry.register("test", SlottedComponent)
|
registry.register("test", gen_slotted_component())
|
||||||
with pytest.raises(
|
with pytest.raises(
|
||||||
TemplateSyntaxError,
|
TemplateSyntaxError,
|
||||||
match=re.escape("Unclosed tag on line 3: 'component'"),
|
match=re.escape("Unclosed tag on line 3: 'component'"),
|
||||||
|
|
|
@ -11,21 +11,18 @@ from .testutils import PARAMETRIZE_CONTEXT_BEHAVIOR, setup_test_config
|
||||||
setup_test_config({"autodiscover": False})
|
setup_test_config({"autodiscover": False})
|
||||||
|
|
||||||
|
|
||||||
class SlottedComponent(Component):
|
def gen_slotted_component():
|
||||||
template_file = "slotted_template.html"
|
class SlottedComponent(Component):
|
||||||
|
template_file = "slotted_template.html"
|
||||||
|
|
||||||
|
return SlottedComponent
|
||||||
|
|
||||||
|
|
||||||
class BlockedAndSlottedComponent(Component):
|
def gen_blocked_and_slotted_component():
|
||||||
template_file = "blocked_and_slotted_template.html"
|
class BlockedAndSlottedComponent(Component):
|
||||||
|
template_file = "blocked_and_slotted_template.html"
|
||||||
|
|
||||||
|
return BlockedAndSlottedComponent
|
||||||
class RelativeFileComponentUsingTemplateFile(Component):
|
|
||||||
template_file = "relative_extends.html"
|
|
||||||
|
|
||||||
|
|
||||||
class RelativeFileComponentUsingGetTemplateName(Component):
|
|
||||||
def get_template_name(self, context):
|
|
||||||
return "relative_extends.html"
|
|
||||||
|
|
||||||
|
|
||||||
#######################
|
#######################
|
||||||
|
@ -37,7 +34,7 @@ class RelativeFileComponentUsingGetTemplateName(Component):
|
||||||
class TestExtendsCompat:
|
class TestExtendsCompat:
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_double_extends_on_main_template_and_component_one_component(self, components_settings):
|
def test_double_extends_on_main_template_and_component_one_component(self, components_settings):
|
||||||
registry.register("blocked_and_slotted_component", BlockedAndSlottedComponent)
|
registry.register("blocked_and_slotted_component", gen_blocked_and_slotted_component())
|
||||||
|
|
||||||
@register("extended_component")
|
@register("extended_component")
|
||||||
class _ExtendedComponent(Component):
|
class _ExtendedComponent(Component):
|
||||||
|
@ -82,7 +79,7 @@ class TestExtendsCompat:
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_double_extends_on_main_template_and_component_two_identical_components(self, components_settings):
|
def test_double_extends_on_main_template_and_component_two_identical_components(self, components_settings):
|
||||||
registry.register("blocked_and_slotted_component", BlockedAndSlottedComponent)
|
registry.register("blocked_and_slotted_component", gen_blocked_and_slotted_component())
|
||||||
|
|
||||||
@register("extended_component")
|
@register("extended_component")
|
||||||
class _ExtendedComponent(Component):
|
class _ExtendedComponent(Component):
|
||||||
|
@ -134,12 +131,11 @@ class TestExtendsCompat:
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
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): # noqa: E501
|
||||||
registry.register("blocked_and_slotted_component", BlockedAndSlottedComponent)
|
registry.register("blocked_and_slotted_component", gen_blocked_and_slotted_component())
|
||||||
|
|
||||||
@register("extended_component")
|
@register("extended_component")
|
||||||
class _ExtendedComponent(Component):
|
class _ExtendedComponent(Component):
|
||||||
|
@ -201,12 +197,11 @@ class TestExtendsCompat:
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
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): # noqa: E501
|
||||||
registry.register("blocked_and_slotted_component", BlockedAndSlottedComponent)
|
registry.register("blocked_and_slotted_component", gen_blocked_and_slotted_component())
|
||||||
|
|
||||||
@register("extended_component")
|
@register("extended_component")
|
||||||
class _ExtendedComponent(Component):
|
class _ExtendedComponent(Component):
|
||||||
|
@ -271,7 +266,7 @@ class TestExtendsCompat:
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_extends_on_component_one_component(self, components_settings):
|
def test_extends_on_component_one_component(self, components_settings):
|
||||||
registry.register("blocked_and_slotted_component", BlockedAndSlottedComponent)
|
registry.register("blocked_and_slotted_component", gen_blocked_and_slotted_component())
|
||||||
|
|
||||||
@register("extended_component")
|
@register("extended_component")
|
||||||
class _ExtendedComponent(Component):
|
class _ExtendedComponent(Component):
|
||||||
|
@ -314,7 +309,7 @@ class TestExtendsCompat:
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_extends_on_component_two_component(self, components_settings):
|
def test_extends_on_component_two_component(self, components_settings):
|
||||||
registry.register("blocked_and_slotted_component", BlockedAndSlottedComponent)
|
registry.register("blocked_and_slotted_component", gen_blocked_and_slotted_component())
|
||||||
|
|
||||||
@register("extended_component")
|
@register("extended_component")
|
||||||
class _ExtendedComponent(Component):
|
class _ExtendedComponent(Component):
|
||||||
|
@ -368,8 +363,8 @@ class TestExtendsCompat:
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_double_extends_on_main_template_and_nested_component(self, components_settings):
|
def test_double_extends_on_main_template_and_nested_component(self, components_settings):
|
||||||
registry.register("slotted_component", SlottedComponent)
|
registry.register("slotted_component", gen_slotted_component())
|
||||||
registry.register("blocked_and_slotted_component", BlockedAndSlottedComponent)
|
registry.register("blocked_and_slotted_component", gen_blocked_and_slotted_component())
|
||||||
|
|
||||||
@register("extended_component")
|
@register("extended_component")
|
||||||
class _ExtendedComponent(Component):
|
class _ExtendedComponent(Component):
|
||||||
|
@ -425,8 +420,8 @@ class TestExtendsCompat:
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_double_extends_on_main_template_and_nested_component_and_include(self, components_settings):
|
def test_double_extends_on_main_template_and_nested_component_and_include(self, components_settings):
|
||||||
registry.register("slotted_component", SlottedComponent)
|
registry.register("slotted_component", gen_slotted_component())
|
||||||
registry.register("blocked_and_slotted_component", BlockedAndSlottedComponent)
|
registry.register("blocked_and_slotted_component", gen_blocked_and_slotted_component())
|
||||||
|
|
||||||
@register("extended_component")
|
@register("extended_component")
|
||||||
class _ExtendedComponent(Component):
|
class _ExtendedComponent(Component):
|
||||||
|
@ -459,12 +454,25 @@ class TestExtendsCompat:
|
||||||
|
|
||||||
# second rendering after cache built
|
# second rendering after cache built
|
||||||
rendered_2 = Template(template).render(Context({"DJC_DEPS_STRATEGY": "ignore"}))
|
rendered_2 = Template(template).render(Context({"DJC_DEPS_STRATEGY": "ignore"}))
|
||||||
expected_2 = expected.replace("data-djc-id-ca1bc3f", "data-djc-id-ca1bc41")
|
|
||||||
|
expected_2 = """
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<body>
|
||||||
|
<main role="main">
|
||||||
|
<div class='container main-container'>
|
||||||
|
Variable: <strong></strong>
|
||||||
|
Variable: <strong data-djc-id-ca1bc41></strong>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
assertHTMLEqual(rendered_2, expected_2)
|
assertHTMLEqual(rendered_2, expected_2)
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_slots_inside_extends(self, components_settings):
|
def test_slots_inside_extends(self, components_settings):
|
||||||
registry.register("slotted_component", SlottedComponent)
|
registry.register("slotted_component", gen_slotted_component())
|
||||||
|
|
||||||
@register("slot_inside_extends")
|
@register("slot_inside_extends")
|
||||||
class SlotInsideExtendsComponent(Component):
|
class SlotInsideExtendsComponent(Component):
|
||||||
|
@ -497,7 +505,7 @@ class TestExtendsCompat:
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_slots_inside_include(self, components_settings):
|
def test_slots_inside_include(self, components_settings):
|
||||||
registry.register("slotted_component", SlottedComponent)
|
registry.register("slotted_component", gen_slotted_component())
|
||||||
|
|
||||||
@register("slot_inside_include")
|
@register("slot_inside_include")
|
||||||
class SlotInsideIncludeComponent(Component):
|
class SlotInsideIncludeComponent(Component):
|
||||||
|
@ -530,7 +538,7 @@ class TestExtendsCompat:
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_component_inside_block(self, components_settings):
|
def test_component_inside_block(self, components_settings):
|
||||||
registry.register("slotted_component", SlottedComponent)
|
registry.register("slotted_component", gen_slotted_component())
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
{% extends "block.html" %}
|
{% extends "block.html" %}
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
|
@ -565,7 +573,7 @@ class TestExtendsCompat:
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_block_inside_component(self, components_settings):
|
def test_block_inside_component(self, components_settings):
|
||||||
registry.register("slotted_component", SlottedComponent)
|
registry.register("slotted_component", gen_slotted_component())
|
||||||
|
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
{% extends "block_in_component.html" %}
|
{% extends "block_in_component.html" %}
|
||||||
|
@ -594,7 +602,7 @@ class TestExtendsCompat:
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_block_inside_component_parent(self, components_settings):
|
def test_block_inside_component_parent(self, components_settings):
|
||||||
registry.register("slotted_component", SlottedComponent)
|
registry.register("slotted_component", gen_slotted_component())
|
||||||
|
|
||||||
@register("block_in_component_parent")
|
@register("block_in_component_parent")
|
||||||
class BlockInCompParent(Component):
|
class BlockInCompParent(Component):
|
||||||
|
@ -627,7 +635,7 @@ class TestExtendsCompat:
|
||||||
Assert that when we call a component with `{% component %}`, that
|
Assert that when we call a component with `{% component %}`, that
|
||||||
the `{% block %}` will NOT affect the inner component.
|
the `{% block %}` will NOT affect the inner component.
|
||||||
"""
|
"""
|
||||||
registry.register("slotted_component", SlottedComponent)
|
registry.register("slotted_component", gen_slotted_component())
|
||||||
|
|
||||||
@register("block_inside_slot_v1")
|
@register("block_inside_slot_v1")
|
||||||
class BlockInSlotInComponent(Component):
|
class BlockInSlotInComponent(Component):
|
||||||
|
@ -662,7 +670,7 @@ class TestExtendsCompat:
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_slot_inside_block__slot_default_block_default(self, components_settings):
|
def test_slot_inside_block__slot_default_block_default(self, components_settings):
|
||||||
registry.register("slotted_component", SlottedComponent)
|
registry.register("slotted_component", gen_slotted_component())
|
||||||
|
|
||||||
@register("slot_inside_block")
|
@register("slot_inside_block")
|
||||||
class _SlotInsideBlockComponent(Component):
|
class _SlotInsideBlockComponent(Component):
|
||||||
|
@ -695,7 +703,7 @@ class TestExtendsCompat:
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_slot_inside_block__slot_default_block_override(self, components_settings):
|
def test_slot_inside_block__slot_default_block_override(self, components_settings):
|
||||||
registry.clear()
|
registry.clear()
|
||||||
registry.register("slotted_component", SlottedComponent)
|
registry.register("slotted_component", gen_slotted_component())
|
||||||
|
|
||||||
@register("slot_inside_block")
|
@register("slot_inside_block")
|
||||||
class _SlotInsideBlockComponent(Component):
|
class _SlotInsideBlockComponent(Component):
|
||||||
|
@ -730,7 +738,7 @@ class TestExtendsCompat:
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_slot_inside_block__slot_overriden_block_default(self, components_settings):
|
def test_slot_inside_block__slot_overriden_block_default(self, components_settings):
|
||||||
registry.register("slotted_component", SlottedComponent)
|
registry.register("slotted_component", gen_slotted_component())
|
||||||
|
|
||||||
@register("slot_inside_block")
|
@register("slot_inside_block")
|
||||||
class _SlotInsideBlockComponent(Component):
|
class _SlotInsideBlockComponent(Component):
|
||||||
|
@ -766,7 +774,7 @@ class TestExtendsCompat:
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_slot_inside_block__slot_overriden_block_overriden(self, components_settings):
|
def test_slot_inside_block__slot_overriden_block_overriden(self, components_settings):
|
||||||
registry.register("slotted_component", SlottedComponent)
|
registry.register("slotted_component", gen_slotted_component())
|
||||||
|
|
||||||
@register("slot_inside_block")
|
@register("slot_inside_block")
|
||||||
class _SlotInsideBlockComponent(Component):
|
class _SlotInsideBlockComponent(Component):
|
||||||
|
@ -812,7 +820,7 @@ class TestExtendsCompat:
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_inject_inside_block(self, components_settings):
|
def test_inject_inside_block(self, components_settings):
|
||||||
registry.register("slotted_component", SlottedComponent)
|
registry.register("slotted_component", gen_slotted_component())
|
||||||
|
|
||||||
@register("injectee")
|
@register("injectee")
|
||||||
class InjectComponent(Component):
|
class InjectComponent(Component):
|
||||||
|
@ -851,7 +859,9 @@ class TestExtendsCompat:
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_component_using_template_file_extends_relative_file(self, components_settings):
|
def test_component_using_template_file_extends_relative_file(self, components_settings):
|
||||||
registry.register("relative_file_component_using_template_file", RelativeFileComponentUsingTemplateFile)
|
@register("relative_file_component_using_template_file")
|
||||||
|
class RelativeFileComponentUsingTemplateFile(Component):
|
||||||
|
template_file = "relative_extends.html"
|
||||||
|
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
|
@ -874,7 +884,10 @@ class TestExtendsCompat:
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_component_using_get_template_name_extends_relative_file(self, components_settings):
|
def test_component_using_get_template_name_extends_relative_file(self, components_settings):
|
||||||
registry.register("relative_file_component_using_get_template_name", RelativeFileComponentUsingGetTemplateName)
|
@register("relative_file_component_using_get_template_name")
|
||||||
|
class RelativeFileComponentUsingGetTemplateName(Component):
|
||||||
|
def get_template_name(self, context):
|
||||||
|
return "relative_extends.html"
|
||||||
|
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
|
|
|
@ -37,12 +37,19 @@ def setup_test_config(
|
||||||
"OPTIONS": {
|
"OPTIONS": {
|
||||||
"builtins": [
|
"builtins": [
|
||||||
"django_components.templatetags.component_tags",
|
"django_components.templatetags.component_tags",
|
||||||
]
|
],
|
||||||
|
'loaders': [
|
||||||
|
# Default Django loader
|
||||||
|
'django.template.loaders.filesystem.Loader',
|
||||||
|
# Including this is the same as APP_DIRS=True
|
||||||
|
'django.template.loaders.app_directories.Loader',
|
||||||
|
# Components loader
|
||||||
|
'django_components.template_loader.Loader',
|
||||||
|
],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"COMPONENTS": {
|
"COMPONENTS": {
|
||||||
"template_cache_size": 128,
|
|
||||||
**(components or {}),
|
**(components or {}),
|
||||||
},
|
},
|
||||||
"MIDDLEWARE": [],
|
"MIDDLEWARE": [],
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue