mirror of
https://github.com/django-components/django-components.git
synced 2025-10-09 21:41:59 +00:00
refactor: add missing exports, better error handling, and handle boolean query params (#1422)
Some checks are pending
Docs - build & deploy / docs (push) Waiting to run
Run tests / build (ubuntu-latest, 3.10) (push) Waiting to run
Run tests / build (ubuntu-latest, 3.11) (push) Waiting to run
Run tests / build (ubuntu-latest, 3.12) (push) Waiting to run
Run tests / build (ubuntu-latest, 3.13) (push) Waiting to run
Run tests / build (ubuntu-latest, 3.8) (push) Waiting to run
Run tests / build (ubuntu-latest, 3.9) (push) Waiting to run
Run tests / build (windows-latest, 3.10) (push) Waiting to run
Run tests / build (windows-latest, 3.11) (push) Waiting to run
Run tests / build (windows-latest, 3.12) (push) Waiting to run
Run tests / build (windows-latest, 3.13) (push) Waiting to run
Run tests / build (windows-latest, 3.8) (push) Waiting to run
Run tests / build (windows-latest, 3.9) (push) Waiting to run
Run tests / test_docs (3.13) (push) Waiting to run
Run tests / test_sampleproject (3.13) (push) Waiting to run
Some checks are pending
Docs - build & deploy / docs (push) Waiting to run
Run tests / build (ubuntu-latest, 3.10) (push) Waiting to run
Run tests / build (ubuntu-latest, 3.11) (push) Waiting to run
Run tests / build (ubuntu-latest, 3.12) (push) Waiting to run
Run tests / build (ubuntu-latest, 3.13) (push) Waiting to run
Run tests / build (ubuntu-latest, 3.8) (push) Waiting to run
Run tests / build (ubuntu-latest, 3.9) (push) Waiting to run
Run tests / build (windows-latest, 3.10) (push) Waiting to run
Run tests / build (windows-latest, 3.11) (push) Waiting to run
Run tests / build (windows-latest, 3.12) (push) Waiting to run
Run tests / build (windows-latest, 3.13) (push) Waiting to run
Run tests / build (windows-latest, 3.8) (push) Waiting to run
Run tests / build (windows-latest, 3.9) (push) Waiting to run
Run tests / test_docs (3.13) (push) Waiting to run
Run tests / test_sampleproject (3.13) (push) Waiting to run
This commit is contained in:
parent
e9d1b6c4b2
commit
8957befd1a
14 changed files with 552 additions and 371 deletions
20
CHANGELOG.md
20
CHANGELOG.md
|
@ -29,6 +29,26 @@
|
||||||
|
|
||||||
Each yield operation is independent and returns its own `(html, error)` tuple, allowing you to handle each rendering result separately.
|
Each yield operation is independent and returns its own `(html, error)` tuple, allowing you to handle each rendering result separately.
|
||||||
|
|
||||||
|
#### Fix
|
||||||
|
|
||||||
|
- Improve formatting when an exception is raised while rendering components. Error messages with newlines should now be properly formatted.
|
||||||
|
|
||||||
|
- Add missing exports for `OnComponentRenderedContext`, `OnSlotRenderedContext`, `OnTemplateCompiledContext`, `OnTemplateLoadedContext`.
|
||||||
|
|
||||||
|
#### Refactor
|
||||||
|
|
||||||
|
- Changes to how `get_component_url()` handles query parameters:
|
||||||
|
- `True` values are now converted to boolean flags (e.g. `?enabled` instead of `?enabled=True`).
|
||||||
|
- `False` and `None` values are now filtered out.
|
||||||
|
|
||||||
|
```py
|
||||||
|
url = get_component_url(
|
||||||
|
MyComponent,
|
||||||
|
query={"abc": 123, "enabled": True, "debug": False, "none_key": None},
|
||||||
|
)
|
||||||
|
# /components/ext/view/components/c1ab2c3?abc=123&enabled
|
||||||
|
```
|
||||||
|
|
||||||
## v0.141.6
|
## v0.141.6
|
||||||
|
|
||||||
#### Fix
|
#### Fix
|
||||||
|
|
|
@ -10,8 +10,8 @@ For web applications, it's common to define endpoints that serve HTML content (A
|
||||||
django-components has a suite of features that help you write and manage views and their URLs:
|
django-components has a suite of features that help you write and manage views and their URLs:
|
||||||
|
|
||||||
- For each component, you can define methods for handling HTTP requests (GET, POST, etc.) - `get()`, `post()`, etc.
|
- For each component, you can define methods for handling HTTP requests (GET, POST, etc.) - `get()`, `post()`, etc.
|
||||||
|
|
||||||
- Use [`Component.as_view()`](../../../reference/api#django_components.Component.as_view) to be able to use your Components with Django's [`urlpatterns`](https://docs.djangoproject.com/en/5.2/topics/http/urls/). This works the same way as [`View.as_view()`](https://docs.djangoproject.com/en/5.2/ref/class-based-views/base/#django.views.generic.base.View.as_view).
|
- Use [`Component.as_view()`](../../../reference/api#django_components.Component.as_view) to be able to use your Components with Django's [`urlpatterns`](https://docs.djangoproject.com/en/5.2/topics/http/urls/). This works the same way as [`View.as_view()`](https://docs.djangoproject.com/en/5.2/ref/class-based-views/base/#django.views.generic.base.View.as_view).
|
||||||
|
|
||||||
- To avoid having to manually define the endpoints for each component, you can set the component to be "public" with [`Component.View.public = True`](../../../reference/api#django_components.ComponentView.public). This will automatically create a URL for the component. To retrieve the component URL, use [`get_component_url()`](../../../reference/api#django_components.get_component_url).
|
- To avoid having to manually define the endpoints for each component, you can set the component to be "public" with [`Component.View.public = True`](../../../reference/api#django_components.ComponentView.public). This will automatically create a URL for the component. To retrieve the component URL, use [`get_component_url()`](../../../reference/api#django_components.get_component_url).
|
||||||
|
|
||||||
|
@ -60,7 +60,6 @@ class Calendar(Component):
|
||||||
|
|
||||||
Each of these receive the [`HttpRequest`](https://docs.djangoproject.com/en/5.2/ref/request-response/#django.http.HttpRequest) object as the first argument.
|
Each of these receive the [`HttpRequest`](https://docs.djangoproject.com/en/5.2/ref/request-response/#django.http.HttpRequest) object as the first argument.
|
||||||
|
|
||||||
|
|
||||||
<!-- TODO_V1 REMOVE -->
|
<!-- TODO_V1 REMOVE -->
|
||||||
|
|
||||||
!!! warning
|
!!! warning
|
||||||
|
@ -111,7 +110,7 @@ urlpatterns = [
|
||||||
|
|
||||||
[`Component.as_view()`](../../../reference/api#django_components.Component.as_view)
|
[`Component.as_view()`](../../../reference/api#django_components.Component.as_view)
|
||||||
internally calls [`View.as_view()`](https://docs.djangoproject.com/en/5.2/ref/class-based-views/base/#django.views.generic.base.View.as_view), passing the component
|
internally calls [`View.as_view()`](https://docs.djangoproject.com/en/5.2/ref/class-based-views/base/#django.views.generic.base.View.as_view), passing the component
|
||||||
instance as one of the arguments.
|
class as one of the arguments.
|
||||||
|
|
||||||
## Register URLs automatically
|
## Register URLs automatically
|
||||||
|
|
||||||
|
@ -144,8 +143,14 @@ This way you don't have to mix your app URLs with component URLs.
|
||||||
```py
|
```py
|
||||||
url = get_component_url(
|
url = get_component_url(
|
||||||
MyComponent,
|
MyComponent,
|
||||||
query={"foo": "bar"},
|
query={"foo": "bar", "enabled": True, "debug": False, "unused": None},
|
||||||
fragment="baz",
|
fragment="baz",
|
||||||
)
|
)
|
||||||
# /components/ext/view/components/c1ab2c3?foo=bar#baz
|
# /components/ext/view/components/c1ab2c3?foo=bar&enabled#baz
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Query parameter handling:**
|
||||||
|
|
||||||
|
- `True` values are rendered as flag parameters without values (e.g., `?enabled`)
|
||||||
|
- `False` and `None` values are omitted from the URL
|
||||||
|
- Other values are rendered normally (e.g., `?foo=bar`)
|
||||||
|
|
|
@ -308,6 +308,11 @@ name | type | description
|
||||||
heading_level: 3
|
heading_level: 3
|
||||||
show_if_no_docstring: true
|
show_if_no_docstring: true
|
||||||
|
|
||||||
|
::: django_components.extension.OnComponentRenderedContext
|
||||||
|
options:
|
||||||
|
heading_level: 3
|
||||||
|
show_if_no_docstring: true
|
||||||
|
|
||||||
::: django_components.extension.OnComponentUnregisteredContext
|
::: django_components.extension.OnComponentUnregisteredContext
|
||||||
options:
|
options:
|
||||||
heading_level: 3
|
heading_level: 3
|
||||||
|
@ -323,3 +328,18 @@ name | type | description
|
||||||
heading_level: 3
|
heading_level: 3
|
||||||
show_if_no_docstring: true
|
show_if_no_docstring: true
|
||||||
|
|
||||||
|
::: django_components.extension.OnSlotRenderedContext
|
||||||
|
options:
|
||||||
|
heading_level: 3
|
||||||
|
show_if_no_docstring: true
|
||||||
|
|
||||||
|
::: django_components.extension.OnTemplateCompiledContext
|
||||||
|
options:
|
||||||
|
heading_level: 3
|
||||||
|
show_if_no_docstring: true
|
||||||
|
|
||||||
|
::: django_components.extension.OnTemplateLoadedContext
|
||||||
|
options:
|
||||||
|
heading_level: 3
|
||||||
|
show_if_no_docstring: true
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ playwright
|
||||||
requests
|
requests
|
||||||
types-requests
|
types-requests
|
||||||
whitenoise
|
whitenoise
|
||||||
|
pydantic
|
||||||
pygments
|
pygments
|
||||||
pygments-djc
|
pygments-djc
|
||||||
asv
|
asv
|
||||||
|
|
|
@ -4,17 +4,17 @@
|
||||||
#
|
#
|
||||||
# pip-compile requirements-dev.in
|
# pip-compile requirements-dev.in
|
||||||
#
|
#
|
||||||
asgiref==3.9.1
|
annotated-types==0.7.0
|
||||||
|
# via pydantic
|
||||||
|
asgiref==3.9.2
|
||||||
# via django
|
# via django
|
||||||
asv==0.6.5
|
asv==0.6.5
|
||||||
# via -r requirements-dev.in
|
# via -r requirements-dev.in
|
||||||
asv-runner==0.2.1
|
asv-runner==0.2.1
|
||||||
# via asv
|
# via asv
|
||||||
backports-asyncio-runner==1.2.0
|
|
||||||
# via pytest-asyncio
|
|
||||||
build==1.3.0
|
build==1.3.0
|
||||||
# via asv
|
# via asv
|
||||||
cachetools==6.1.0
|
cachetools==6.2.0
|
||||||
# via tox
|
# via tox
|
||||||
certifi==2025.8.3
|
certifi==2025.8.3
|
||||||
# via requests
|
# via requests
|
||||||
|
@ -28,7 +28,7 @@ colorama==0.4.6
|
||||||
# via tox
|
# via tox
|
||||||
distlib==0.4.0
|
distlib==0.4.0
|
||||||
# via virtualenv
|
# via virtualenv
|
||||||
django==4.2.24
|
django==5.2.6
|
||||||
# via
|
# via
|
||||||
# -r requirements-dev.in
|
# -r requirements-dev.in
|
||||||
# django-template-partials
|
# django-template-partials
|
||||||
|
@ -36,15 +36,13 @@ django-template-partials==25.2
|
||||||
# via -r requirements-dev.in
|
# via -r requirements-dev.in
|
||||||
djc-core-html-parser==1.0.2
|
djc-core-html-parser==1.0.2
|
||||||
# via -r requirements-dev.in
|
# via -r requirements-dev.in
|
||||||
exceptiongroup==1.3.0
|
|
||||||
# via pytest
|
|
||||||
filelock==3.19.1
|
filelock==3.19.1
|
||||||
# via
|
# via
|
||||||
# tox
|
# tox
|
||||||
# virtualenv
|
# virtualenv
|
||||||
greenlet==3.2.4
|
greenlet==3.2.4
|
||||||
# via playwright
|
# via playwright
|
||||||
identify==2.6.13
|
identify==2.6.14
|
||||||
# via pre-commit
|
# via pre-commit
|
||||||
idna==3.10
|
idna==3.10
|
||||||
# via requests
|
# via requests
|
||||||
|
@ -52,7 +50,6 @@ importlib-metadata==8.7.0
|
||||||
# via
|
# via
|
||||||
# asv
|
# asv
|
||||||
# asv-runner
|
# asv-runner
|
||||||
# build
|
|
||||||
iniconfig==2.1.0
|
iniconfig==2.1.0
|
||||||
# via pytest
|
# via pytest
|
||||||
json5==0.12.1
|
json5==0.12.1
|
||||||
|
@ -74,11 +71,11 @@ pathspec==0.12.1
|
||||||
# via
|
# via
|
||||||
# -r requirements-dev.in
|
# -r requirements-dev.in
|
||||||
# mypy
|
# mypy
|
||||||
platformdirs==4.3.8
|
platformdirs==4.4.0
|
||||||
# via
|
# via
|
||||||
# tox
|
# tox
|
||||||
# virtualenv
|
# virtualenv
|
||||||
playwright==1.54.0
|
playwright==1.55.0
|
||||||
# via -r requirements-dev.in
|
# via -r requirements-dev.in
|
||||||
pluggy==1.6.0
|
pluggy==1.6.0
|
||||||
# via
|
# via
|
||||||
|
@ -86,6 +83,10 @@ pluggy==1.6.0
|
||||||
# tox
|
# tox
|
||||||
pre-commit==4.3.0
|
pre-commit==4.3.0
|
||||||
# via -r requirements-dev.in
|
# via -r requirements-dev.in
|
||||||
|
pydantic==2.11.9
|
||||||
|
# via -r requirements-dev.in
|
||||||
|
pydantic-core==2.33.2
|
||||||
|
# via pydantic
|
||||||
pyee==13.0.0
|
pyee==13.0.0
|
||||||
# via playwright
|
# via playwright
|
||||||
pygments==2.19.2
|
pygments==2.19.2
|
||||||
|
@ -101,52 +102,45 @@ pyproject-api==1.9.1
|
||||||
# via tox
|
# via tox
|
||||||
pyproject-hooks==1.2.0
|
pyproject-hooks==1.2.0
|
||||||
# via build
|
# via build
|
||||||
pytest==8.4.1
|
pytest==8.4.2
|
||||||
# via
|
# via
|
||||||
# -r requirements-dev.in
|
# -r requirements-dev.in
|
||||||
# pytest-asyncio
|
# pytest-asyncio
|
||||||
# pytest-django
|
# pytest-django
|
||||||
# syrupy
|
# syrupy
|
||||||
pytest-asyncio==1.1.0
|
pytest-asyncio==1.2.0
|
||||||
# via -r requirements-dev.in
|
# via -r requirements-dev.in
|
||||||
pytest-django==4.11.1
|
pytest-django==4.11.1
|
||||||
# via -r requirements-dev.in
|
# via -r requirements-dev.in
|
||||||
pyyaml==6.0.2
|
pyyaml==6.0.3
|
||||||
# via
|
# via
|
||||||
# asv
|
# asv
|
||||||
# pre-commit
|
# pre-commit
|
||||||
requests==2.32.4
|
requests==2.32.5
|
||||||
# via -r requirements-dev.in
|
# via -r requirements-dev.in
|
||||||
ruff==0.13.2
|
ruff==0.13.2
|
||||||
# via -r requirements-dev.in
|
# via -r requirements-dev.in
|
||||||
sqlparse==0.5.3
|
sqlparse==0.5.3
|
||||||
# via django
|
# via django
|
||||||
syrupy==4.9.1
|
syrupy==5.0.0
|
||||||
# via -r requirements-dev.in
|
# via -r requirements-dev.in
|
||||||
tabulate==0.9.0
|
tabulate==0.9.0
|
||||||
# via asv
|
# via asv
|
||||||
tomli==2.2.1
|
tox==4.30.2
|
||||||
# via
|
|
||||||
# asv
|
|
||||||
# build
|
|
||||||
# mypy
|
|
||||||
# pyproject-api
|
|
||||||
# pytest
|
|
||||||
# tox
|
|
||||||
tox==4.28.4
|
|
||||||
# via -r requirements-dev.in
|
# via -r requirements-dev.in
|
||||||
types-requests==2.32.4.20250809
|
types-requests==2.32.4.20250913
|
||||||
# via -r requirements-dev.in
|
# via -r requirements-dev.in
|
||||||
typing-extensions==4.14.1
|
typing-extensions==4.15.0
|
||||||
# via
|
# via
|
||||||
# -r requirements-dev.in
|
# -r requirements-dev.in
|
||||||
# asgiref
|
|
||||||
# exceptiongroup
|
|
||||||
# mypy
|
# mypy
|
||||||
|
# pydantic
|
||||||
|
# pydantic-core
|
||||||
# pyee
|
# pyee
|
||||||
# pytest-asyncio
|
# pytest-asyncio
|
||||||
# tox
|
# typing-inspection
|
||||||
# virtualenv
|
typing-inspection==0.4.1
|
||||||
|
# via pydantic
|
||||||
urllib3==2.5.0
|
urllib3==2.5.0
|
||||||
# via
|
# via
|
||||||
# requests
|
# requests
|
||||||
|
@ -157,7 +151,7 @@ virtualenv==20.34.0
|
||||||
# asv
|
# asv
|
||||||
# pre-commit
|
# pre-commit
|
||||||
# tox
|
# tox
|
||||||
whitenoise==6.9.0
|
whitenoise==6.11.0
|
||||||
# via -r requirements-dev.in
|
# via -r requirements-dev.in
|
||||||
zipp==3.23.0
|
zipp==3.23.0
|
||||||
# via importlib-metadata
|
# via importlib-metadata
|
||||||
|
|
|
@ -34,7 +34,6 @@ from django_components.component_registry import (
|
||||||
registry,
|
registry,
|
||||||
all_registries,
|
all_registries,
|
||||||
)
|
)
|
||||||
from django_components.components import DynamicComponent
|
|
||||||
from django_components.dependencies import DependenciesStrategy, render_dependencies
|
from django_components.dependencies import DependenciesStrategy, render_dependencies
|
||||||
from django_components.extension import (
|
from django_components.extension import (
|
||||||
ComponentExtension,
|
ComponentExtension,
|
||||||
|
@ -47,6 +46,10 @@ from django_components.extension import (
|
||||||
OnComponentClassDeletedContext,
|
OnComponentClassDeletedContext,
|
||||||
OnComponentInputContext,
|
OnComponentInputContext,
|
||||||
OnComponentDataContext,
|
OnComponentDataContext,
|
||||||
|
OnComponentRenderedContext,
|
||||||
|
OnSlotRenderedContext,
|
||||||
|
OnTemplateCompiledContext,
|
||||||
|
OnTemplateLoadedContext,
|
||||||
)
|
)
|
||||||
from django_components.extensions.cache import ComponentCache
|
from django_components.extensions.cache import ComponentCache
|
||||||
from django_components.extensions.defaults import ComponentDefaults, Default
|
from django_components.extensions.defaults import ComponentDefaults, Default
|
||||||
|
@ -81,6 +84,9 @@ from django_components.util.loader import ComponentFileEntry, get_component_dirs
|
||||||
from django_components.util.routing import URLRoute, URLRouteHandler
|
from django_components.util.routing import URLRoute, URLRouteHandler
|
||||||
from django_components.util.types import Empty
|
from django_components.util.types import Empty
|
||||||
|
|
||||||
|
# NOTE: Import built-in components last to avoid circular imports
|
||||||
|
from django_components.components import DynamicComponent
|
||||||
|
|
||||||
# isort: on
|
# isort: on
|
||||||
|
|
||||||
|
|
||||||
|
@ -122,10 +128,14 @@ __all__ = [
|
||||||
"OnComponentDataContext",
|
"OnComponentDataContext",
|
||||||
"OnComponentInputContext",
|
"OnComponentInputContext",
|
||||||
"OnComponentRegisteredContext",
|
"OnComponentRegisteredContext",
|
||||||
|
"OnComponentRenderedContext",
|
||||||
"OnComponentUnregisteredContext",
|
"OnComponentUnregisteredContext",
|
||||||
"OnRegistryCreatedContext",
|
"OnRegistryCreatedContext",
|
||||||
"OnRegistryDeletedContext",
|
"OnRegistryDeletedContext",
|
||||||
"OnRenderGenerator",
|
"OnRenderGenerator",
|
||||||
|
"OnSlotRenderedContext",
|
||||||
|
"OnTemplateCompiledContext",
|
||||||
|
"OnTemplateLoadedContext",
|
||||||
"ProvideNode",
|
"ProvideNode",
|
||||||
"RegistrySettings",
|
"RegistrySettings",
|
||||||
"ShorthandComponentFormatter",
|
"ShorthandComponentFormatter",
|
||||||
|
|
|
@ -48,6 +48,12 @@ def get_component_url(
|
||||||
|
|
||||||
`get_component_url()` optionally accepts `query` and `fragment` arguments.
|
`get_component_url()` optionally accepts `query` and `fragment` arguments.
|
||||||
|
|
||||||
|
**Query parameter handling:**
|
||||||
|
|
||||||
|
- `True` values are rendered as flag parameters without values (e.g., `?enabled`)
|
||||||
|
- `False` and `None` values are omitted from the URL
|
||||||
|
- Other values are rendered normally (e.g., `?foo=bar`)
|
||||||
|
|
||||||
**Example:**
|
**Example:**
|
||||||
|
|
||||||
```py
|
```py
|
||||||
|
@ -60,10 +66,10 @@ def get_component_url(
|
||||||
# Get the URL for the component
|
# Get the URL for the component
|
||||||
url = get_component_url(
|
url = get_component_url(
|
||||||
MyComponent,
|
MyComponent,
|
||||||
query={"foo": "bar"},
|
query={"foo": "bar", "enabled": True, "debug": False, "unused": None},
|
||||||
fragment="baz",
|
fragment="baz",
|
||||||
)
|
)
|
||||||
# /components/ext/view/components/c1ab2c3?foo=bar#baz
|
# /components/ext/view/components/c1ab2c3?foo=bar&enabled#baz
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
view_cls: Optional[Type[ComponentView]] = getattr(component, "View", None)
|
view_cls: Optional[Type[ComponentView]] = getattr(component, "View", None)
|
||||||
|
|
|
@ -20,20 +20,27 @@ def component_error_message(component_path: List[str]) -> Generator[None, None,
|
||||||
components = getattr(err, "_components", [])
|
components = getattr(err, "_components", [])
|
||||||
components = err._components = [*component_path, *components] # type: ignore[attr-defined]
|
components = err._components = [*component_path, *components] # type: ignore[attr-defined]
|
||||||
|
|
||||||
# Access the exception's message, see https://stackoverflow.com/a/75549200/9788634
|
|
||||||
if len(err.args) and err.args[0] is not None:
|
|
||||||
if not components:
|
|
||||||
orig_msg = str(err.args[0])
|
|
||||||
else:
|
|
||||||
orig_msg = str(err.args[0]).split("\n", 1)[-1]
|
|
||||||
else:
|
|
||||||
orig_msg = str(err)
|
|
||||||
|
|
||||||
# Format component path as
|
# Format component path as
|
||||||
# "MyPage > MyComponent > MyComponent(slot:content) > Base(slot:tab)"
|
# "MyPage > MyComponent > MyComponent(slot:content) > Base(slot:tab)"
|
||||||
comp_path = " > ".join(components)
|
comp_path = " > ".join(components)
|
||||||
prefix = f"An error occured while rendering components {comp_path}:\n"
|
prefix = f"An error occured while rendering components {comp_path}:\n"
|
||||||
|
|
||||||
|
# Access the exception's message, see https://stackoverflow.com/a/75549200/9788634
|
||||||
|
if len(err.args) and err.args[0] is not None:
|
||||||
|
orig_msg = str(err.args[0])
|
||||||
|
if components and "An error occured while rendering components" in orig_msg:
|
||||||
|
orig_msg = str(err.args[0]).split("\n", 1)[-1]
|
||||||
|
else:
|
||||||
|
# When the exception has no message, it may be that the exception
|
||||||
|
# does NOT rely on the `args` attribute. Such case is for example
|
||||||
|
# Pydantic exceptions.
|
||||||
|
#
|
||||||
|
# In this case, we still try to use the `args` attribute, but
|
||||||
|
# it's not guaranteed to work. So we also print out the component
|
||||||
|
# path ourselves.
|
||||||
|
print(prefix) # noqa: T201
|
||||||
|
orig_msg = str(err)
|
||||||
|
|
||||||
err.args = (prefix + orig_msg,) # tuple of one
|
err.args = (prefix + orig_msg,) # tuple of one
|
||||||
|
|
||||||
# `from None` should still raise the original error, but without showing this
|
# `from None` should still raise the original error, but without showing this
|
||||||
|
|
|
@ -162,12 +162,35 @@ def format_url(url: str, query: Optional[Dict] = None, fragment: Optional[str] =
|
||||||
```
|
```
|
||||||
|
|
||||||
`query` and `fragment` are optional, and not applied if `None`.
|
`query` and `fragment` are optional, and not applied if `None`.
|
||||||
|
|
||||||
|
Boolean `True` values in query parameters are rendered as flag parameters without values.
|
||||||
|
|
||||||
|
`False` and `None` values in query parameters are omitted.
|
||||||
|
|
||||||
|
```py
|
||||||
|
url = format_url(
|
||||||
|
url="https://example.com",
|
||||||
|
query={"foo": "bar", "baz": None, "enabled": True, "debug": False},
|
||||||
|
)
|
||||||
|
# https://example.com?foo=bar&enabled
|
||||||
|
```
|
||||||
"""
|
"""
|
||||||
parts = parse.urlsplit(url)
|
parts = parse.urlsplit(url)
|
||||||
fragment_enc = parse.quote(fragment or parts.fragment, safe="")
|
fragment_enc = parse.quote(fragment or parts.fragment, safe="")
|
||||||
base_qs = dict(parse.parse_qsl(parts.query))
|
base_qs = dict(parse.parse_qsl(parts.query))
|
||||||
merged = {**base_qs, **(query or {})}
|
# Filter out `None` and `False` values
|
||||||
encoded_qs = parse.urlencode(merged, safe="")
|
filtered_query = {k: v for k, v in (query or {}).items() if v is not None and v is not False}
|
||||||
|
merged = {**base_qs, **filtered_query}
|
||||||
|
|
||||||
|
# Handle boolean True values as flag parameters (no explicit value)
|
||||||
|
query_parts = []
|
||||||
|
for key, value in merged.items():
|
||||||
|
if value is True:
|
||||||
|
query_parts.append(parse.quote_plus(str(key)))
|
||||||
|
else:
|
||||||
|
query_parts.append(f"{parse.quote_plus(str(key))}={parse.quote_plus(str(value))}")
|
||||||
|
|
||||||
|
encoded_qs = "&".join(query_parts)
|
||||||
|
|
||||||
return parse.urlunsplit(parts._replace(query=encoded_qs, fragment=fragment_enc))
|
return parse.urlunsplit(parts._replace(query=encoded_qs, fragment=fragment_enc))
|
||||||
|
|
||||||
|
|
|
@ -1471,6 +1471,56 @@ class TestComponentRender:
|
||||||
):
|
):
|
||||||
Root.render()
|
Root.render()
|
||||||
|
|
||||||
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
|
def test_pydantic_exception(self, components_settings):
|
||||||
|
from pydantic import BaseModel, ValidationError
|
||||||
|
|
||||||
|
@register("broken")
|
||||||
|
class Broken(Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
<div> injected: {{ data|safe }} </div>
|
||||||
|
<main>
|
||||||
|
{% slot "content" default / %}
|
||||||
|
</main>
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Kwargs(BaseModel):
|
||||||
|
data1: str
|
||||||
|
|
||||||
|
def get_template_data(self, args, kwargs: Kwargs, slots, context):
|
||||||
|
return {"data": kwargs.data1}
|
||||||
|
|
||||||
|
@register("parent")
|
||||||
|
class Parent(Component):
|
||||||
|
def get_template_data(self, args, kwargs, slots, context):
|
||||||
|
return {"data": kwargs["data"]}
|
||||||
|
|
||||||
|
template: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% component "broken" %}
|
||||||
|
{% slot "content" default / %}
|
||||||
|
{% endcomponent %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
@register("root")
|
||||||
|
class Root(Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% component "parent" data=123 %}
|
||||||
|
{% fill "content" %}
|
||||||
|
456
|
||||||
|
{% endfill %}
|
||||||
|
{% endcomponent %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
# NOTE: We're unable to insert the component path in the Pydantic's exception message
|
||||||
|
with pytest.raises(
|
||||||
|
ValidationError,
|
||||||
|
match=re.escape("1 validation error for Kwargs\ndata1\n Field required"),
|
||||||
|
):
|
||||||
|
Root.render()
|
||||||
|
|
||||||
|
|
||||||
@djc_test
|
@djc_test
|
||||||
class TestComponentHook:
|
class TestComponentHook:
|
||||||
|
|
349
tests/test_component_dynamic.py
Normal file
349
tests/test_component_dynamic.py
Normal file
|
@ -0,0 +1,349 @@
|
||||||
|
import re
|
||||||
|
from typing import NamedTuple
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from django.template import Context, Template
|
||||||
|
from pytest_django.asserts import assertHTMLEqual
|
||||||
|
|
||||||
|
from django_components import AlreadyRegistered, Component, DynamicComponent, NotRegistered, registry, types
|
||||||
|
from django_components.testing import djc_test
|
||||||
|
|
||||||
|
from .testutils import PARAMETRIZE_CONTEXT_BEHAVIOR, setup_test_config
|
||||||
|
|
||||||
|
setup_test_config({"autodiscover": False})
|
||||||
|
|
||||||
|
|
||||||
|
@djc_test
|
||||||
|
class TestDynamicComponent:
|
||||||
|
def _gen_simple_component(self):
|
||||||
|
class SimpleComponent(Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
Variable: <strong>{{ variable }}</strong>
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Kwargs(NamedTuple):
|
||||||
|
variable: str
|
||||||
|
variable2: str
|
||||||
|
|
||||||
|
class Defaults:
|
||||||
|
variable2 = "default"
|
||||||
|
|
||||||
|
def get_template_data(self, args, kwargs: Kwargs, slots, context):
|
||||||
|
return {
|
||||||
|
"variable": kwargs.variable,
|
||||||
|
"variable2": kwargs.variable2,
|
||||||
|
}
|
||||||
|
|
||||||
|
class Media:
|
||||||
|
css = "style.css"
|
||||||
|
js = "script.js"
|
||||||
|
|
||||||
|
return SimpleComponent
|
||||||
|
|
||||||
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
|
def test_basic__python(self, components_settings):
|
||||||
|
registry.register(name="test", component=self._gen_simple_component())
|
||||||
|
|
||||||
|
rendered = DynamicComponent.render(
|
||||||
|
kwargs={
|
||||||
|
"is": "test",
|
||||||
|
"variable": "variable",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assertHTMLEqual(
|
||||||
|
rendered,
|
||||||
|
"Variable: <strong data-djc-id-ca1bc3e data-djc-id-ca1bc3f>variable</strong>",
|
||||||
|
)
|
||||||
|
|
||||||
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
|
def test_basic__template(self, components_settings):
|
||||||
|
registry.register(name="test", component=self._gen_simple_component())
|
||||||
|
|
||||||
|
simple_tag_template: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% component "dynamic" is="test" variable="variable" %}{% endcomponent %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
template = Template(simple_tag_template)
|
||||||
|
rendered = template.render(Context({}))
|
||||||
|
assertHTMLEqual(
|
||||||
|
rendered,
|
||||||
|
"Variable: <strong data-djc-id-ca1bc3f data-djc-id-ca1bc40>variable</strong>",
|
||||||
|
)
|
||||||
|
|
||||||
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
|
def test_call_with_invalid_name(self, components_settings):
|
||||||
|
registry.register(name="test", component=self._gen_simple_component())
|
||||||
|
|
||||||
|
simple_tag_template: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% component "dynamic" is="haber_der_baber" variable="variable" %}{% endcomponent %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
template = Template(simple_tag_template)
|
||||||
|
with pytest.raises(NotRegistered, match=re.escape("The component 'haber_der_baber' was not found")):
|
||||||
|
template.render(Context({}))
|
||||||
|
|
||||||
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
|
def test_component_called_with_variable_as_name(self, components_settings):
|
||||||
|
registry.register(name="test", component=self._gen_simple_component())
|
||||||
|
|
||||||
|
simple_tag_template: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% with component_name="test" %}
|
||||||
|
{% component "dynamic" is=component_name variable="variable" %}{% endcomponent %}
|
||||||
|
{% endwith %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
template = Template(simple_tag_template)
|
||||||
|
rendered = template.render(Context({}))
|
||||||
|
assertHTMLEqual(
|
||||||
|
rendered,
|
||||||
|
"Variable: <strong data-djc-id-ca1bc3f data-djc-id-ca1bc40>variable</strong>",
|
||||||
|
)
|
||||||
|
|
||||||
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
|
def test_component_called_with_variable_as_spread(self, components_settings):
|
||||||
|
registry.register(name="test", component=self._gen_simple_component())
|
||||||
|
|
||||||
|
simple_tag_template: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% component "dynamic" ...props %}{% endcomponent %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
template = Template(simple_tag_template)
|
||||||
|
rendered = template.render(
|
||||||
|
Context(
|
||||||
|
{
|
||||||
|
"props": {
|
||||||
|
"is": "test",
|
||||||
|
"variable": "variable",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
assertHTMLEqual(
|
||||||
|
rendered,
|
||||||
|
"Variable: <strong data-djc-id-ca1bc3f data-djc-id-ca1bc40>variable</strong>",
|
||||||
|
)
|
||||||
|
|
||||||
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
|
def test_component_as_class(self, components_settings):
|
||||||
|
SimpleComponent = self._gen_simple_component()
|
||||||
|
registry.register(name="test", component=SimpleComponent)
|
||||||
|
|
||||||
|
simple_tag_template: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% component "dynamic" is=comp_cls variable="variable" %}{% endcomponent %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
template = Template(simple_tag_template)
|
||||||
|
rendered = template.render(
|
||||||
|
Context(
|
||||||
|
{
|
||||||
|
"comp_cls": SimpleComponent,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
assertHTMLEqual(
|
||||||
|
rendered,
|
||||||
|
"Variable: <strong data-djc-id-ca1bc3f data-djc-id-ca1bc40>variable</strong>",
|
||||||
|
)
|
||||||
|
|
||||||
|
@djc_test(
|
||||||
|
parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR,
|
||||||
|
components_settings={
|
||||||
|
"tag_formatter": "django_components.component_shorthand_formatter",
|
||||||
|
"autodiscover": False,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
def test_shorthand_formatter(self, components_settings):
|
||||||
|
from django_components.apps import ComponentsConfig
|
||||||
|
|
||||||
|
ComponentsConfig.ready(None) # type: ignore[arg-type]
|
||||||
|
|
||||||
|
registry.register(name="test", component=self._gen_simple_component())
|
||||||
|
|
||||||
|
simple_tag_template: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% dynamic is="test" variable="variable" %}{% enddynamic %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
template = Template(simple_tag_template)
|
||||||
|
rendered = template.render(Context({}))
|
||||||
|
assertHTMLEqual(rendered, "Variable: <strong data-djc-id-ca1bc3f data-djc-id-ca1bc40>variable</strong>\n")
|
||||||
|
|
||||||
|
@djc_test(
|
||||||
|
parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR,
|
||||||
|
components_settings={
|
||||||
|
"dynamic_component_name": "uno_reverse",
|
||||||
|
"tag_formatter": "django_components.component_shorthand_formatter",
|
||||||
|
"autodiscover": False,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
def test_component_name_is_configurable(self, components_settings):
|
||||||
|
from django_components.apps import ComponentsConfig
|
||||||
|
|
||||||
|
ComponentsConfig.ready(None) # type: ignore[arg-type]
|
||||||
|
|
||||||
|
registry.register(name="test", component=self._gen_simple_component())
|
||||||
|
|
||||||
|
simple_tag_template: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% uno_reverse is="test" variable="variable" %}{% enduno_reverse %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
template = Template(simple_tag_template)
|
||||||
|
rendered = template.render(Context({}))
|
||||||
|
assertHTMLEqual(
|
||||||
|
rendered,
|
||||||
|
"Variable: <strong data-djc-id-ca1bc3f data-djc-id-ca1bc40>variable</strong>",
|
||||||
|
)
|
||||||
|
|
||||||
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
|
def test_raises_already_registered_on_name_conflict(self, components_settings):
|
||||||
|
with pytest.raises(
|
||||||
|
AlreadyRegistered,
|
||||||
|
match=re.escape('The component "dynamic" has already been registered'),
|
||||||
|
):
|
||||||
|
registry.register(name="dynamic", component=self._gen_simple_component())
|
||||||
|
|
||||||
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
|
def test_component_called_with_default_slot(self, components_settings):
|
||||||
|
class SimpleSlottedComponent(Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
Variable: <strong>{{ variable }}</strong>
|
||||||
|
Slot: {% slot "default" default / %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_template_data(self, args, kwargs, slots, context):
|
||||||
|
return {
|
||||||
|
"variable": kwargs["variable"],
|
||||||
|
"variable2": kwargs.get("variable2", "default"),
|
||||||
|
}
|
||||||
|
|
||||||
|
registry.register(name="test", component=SimpleSlottedComponent)
|
||||||
|
|
||||||
|
simple_tag_template: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% with component_name="test" %}
|
||||||
|
{% component "dynamic" is=component_name variable="variable" %}
|
||||||
|
HELLO_FROM_SLOT
|
||||||
|
{% endcomponent %}
|
||||||
|
{% endwith %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
template = Template(simple_tag_template)
|
||||||
|
rendered = template.render(Context({}))
|
||||||
|
assertHTMLEqual(
|
||||||
|
rendered,
|
||||||
|
"""
|
||||||
|
Variable: <strong data-djc-id-ca1bc3f data-djc-id-ca1bc40>variable</strong>
|
||||||
|
Slot: HELLO_FROM_SLOT
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
|
def test_component_called_with_named_slots(self, components_settings):
|
||||||
|
class SimpleSlottedComponent(Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
Variable: <strong>{{ variable }}</strong>
|
||||||
|
Slot 1: {% slot "default" default / %}
|
||||||
|
Slot 2: {% slot "two" / %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_template_data(self, args, kwargs, slots, context):
|
||||||
|
return {
|
||||||
|
"variable": kwargs["variable"],
|
||||||
|
"variable2": kwargs.get("variable2", "default"),
|
||||||
|
}
|
||||||
|
|
||||||
|
registry.register(name="test", component=SimpleSlottedComponent)
|
||||||
|
|
||||||
|
simple_tag_template: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% with component_name="test" %}
|
||||||
|
{% component "dynamic" is=component_name variable="variable" %}
|
||||||
|
{% fill "default" %}
|
||||||
|
HELLO_FROM_SLOT_1
|
||||||
|
{% endfill %}
|
||||||
|
{% fill "two" %}
|
||||||
|
HELLO_FROM_SLOT_2
|
||||||
|
{% endfill %}
|
||||||
|
{% endcomponent %}
|
||||||
|
{% endwith %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
template = Template(simple_tag_template)
|
||||||
|
rendered = template.render(Context({}))
|
||||||
|
assertHTMLEqual(
|
||||||
|
rendered,
|
||||||
|
"""
|
||||||
|
Variable: <strong data-djc-id-ca1bc41 data-djc-id-ca1bc42>variable</strong>
|
||||||
|
Slot 1: HELLO_FROM_SLOT_1
|
||||||
|
Slot 2: HELLO_FROM_SLOT_2
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
|
def test_ignores_invalid_slots(self, components_settings):
|
||||||
|
class SimpleSlottedComponent(Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
Variable: <strong>{{ variable }}</strong>
|
||||||
|
Slot 1: {% slot "default" default / %}
|
||||||
|
Slot 2: {% slot "two" / %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_template_data(self, args, kwargs, slots, context):
|
||||||
|
return {
|
||||||
|
"variable": kwargs["variable"],
|
||||||
|
"variable2": kwargs.get("variable2", "default"),
|
||||||
|
}
|
||||||
|
|
||||||
|
registry.register(name="test", component=SimpleSlottedComponent)
|
||||||
|
|
||||||
|
simple_tag_template: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% with component_name="test" %}
|
||||||
|
{% component "dynamic" is=component_name variable="variable" %}
|
||||||
|
{% fill "default" %}
|
||||||
|
HELLO_FROM_SLOT_1
|
||||||
|
{% endfill %}
|
||||||
|
{% fill "three" %}
|
||||||
|
HELLO_FROM_SLOT_2
|
||||||
|
{% endfill %}
|
||||||
|
{% endcomponent %}
|
||||||
|
{% endwith %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
template = Template(simple_tag_template)
|
||||||
|
rendered = template.render(Context({}))
|
||||||
|
assertHTMLEqual(
|
||||||
|
rendered,
|
||||||
|
"""
|
||||||
|
Variable: <strong data-djc-id-ca1bc41 data-djc-id-ca1bc42>variable</strong>
|
||||||
|
Slot 1: HELLO_FROM_SLOT_1
|
||||||
|
Slot 2:
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
|
def test_raises_on_invalid_input(self, components_settings):
|
||||||
|
registry.register(name="test", component=self._gen_simple_component())
|
||||||
|
|
||||||
|
simple_tag_template: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% with component_name="test" %}
|
||||||
|
{% component "dynamic" is=component_name invalid_variable="variable" %}{% endcomponent %}
|
||||||
|
{% endwith %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
template = Template(simple_tag_template)
|
||||||
|
with pytest.raises(
|
||||||
|
TypeError,
|
||||||
|
match=re.escape("got an unexpected keyword argument 'invalid_variable'"),
|
||||||
|
):
|
||||||
|
template.render(Context({}))
|
|
@ -307,6 +307,18 @@ class TestComponentAsView:
|
||||||
== f"/components/ext/view/components/{TestComponent.class_id}/?f%27oo=b+ar%26ba%27z#q%20u%22x"
|
== f"/components/ext/view/components/{TestComponent.class_id}/?f%27oo=b+ar%26ba%27z#q%20u%22x"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Converts `True` to no flag parameter (no value, e.g. `enabled` instead of `enabled=True`)
|
||||||
|
# And filters out `False` and `None` values
|
||||||
|
component_url3 = get_component_url(
|
||||||
|
TestComponent,
|
||||||
|
query={"f'oo": "b ar&ba'z", "true_key": True, "false_key": False, "none_key": None},
|
||||||
|
fragment='q u"x',
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
component_url3
|
||||||
|
== f"/components/ext/view/components/{TestComponent.class_id}/?f%27oo=b+ar%26ba%27z&true_key#q%20u%22x"
|
||||||
|
)
|
||||||
|
|
||||||
# Merges query params from original URL
|
# Merges query params from original URL
|
||||||
component_url4 = format_url(
|
component_url4 = format_url(
|
||||||
"/components/ext/view/components/123?foo=123&bar=456#abc",
|
"/components/ext/view/components/123?foo=123&bar=456#abc",
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
import re
|
import re
|
||||||
from typing import NamedTuple
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from django.template import Context, Template, TemplateSyntaxError
|
from django.template import Context, Template, TemplateSyntaxError
|
||||||
from pytest_django.asserts import assertHTMLEqual
|
from pytest_django.asserts import assertHTMLEqual
|
||||||
|
|
||||||
from django_components import AlreadyRegistered, Component, NotRegistered, register, registry, types
|
from django_components import Component, NotRegistered, register, registry, types
|
||||||
from django_components.testing import djc_test
|
from django_components.testing import djc_test
|
||||||
|
|
||||||
from .testutils import PARAMETRIZE_CONTEXT_BEHAVIOR, setup_test_config
|
from .testutils import PARAMETRIZE_CONTEXT_BEHAVIOR, setup_test_config
|
||||||
|
@ -208,323 +207,6 @@ class TestComponentTemplateTag:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@djc_test
|
|
||||||
class TestDynamicComponentTemplateTag:
|
|
||||||
class SimpleComponent(Component):
|
|
||||||
template: types.django_html = """
|
|
||||||
Variable: <strong>{{ variable }}</strong>
|
|
||||||
"""
|
|
||||||
|
|
||||||
class Kwargs(NamedTuple):
|
|
||||||
variable: str
|
|
||||||
variable2: str
|
|
||||||
|
|
||||||
class Defaults:
|
|
||||||
variable2 = "default"
|
|
||||||
|
|
||||||
def get_template_data(self, args, kwargs: Kwargs, slots, context):
|
|
||||||
return {
|
|
||||||
"variable": kwargs.variable,
|
|
||||||
"variable2": kwargs.variable2,
|
|
||||||
}
|
|
||||||
|
|
||||||
class Media:
|
|
||||||
css = "style.css"
|
|
||||||
js = "script.js"
|
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
|
||||||
def test_basic(self, components_settings):
|
|
||||||
registry.register(name="test", component=self.SimpleComponent)
|
|
||||||
|
|
||||||
simple_tag_template: types.django_html = """
|
|
||||||
{% load component_tags %}
|
|
||||||
{% component "dynamic" is="test" variable="variable" %}{% endcomponent %}
|
|
||||||
"""
|
|
||||||
|
|
||||||
template = Template(simple_tag_template)
|
|
||||||
rendered = template.render(Context({}))
|
|
||||||
assertHTMLEqual(
|
|
||||||
rendered,
|
|
||||||
"Variable: <strong data-djc-id-ca1bc3f data-djc-id-ca1bc40>variable</strong>",
|
|
||||||
)
|
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
|
||||||
def test_call_with_invalid_name(self, components_settings):
|
|
||||||
registry.register(name="test", component=self.SimpleComponent)
|
|
||||||
|
|
||||||
simple_tag_template: types.django_html = """
|
|
||||||
{% load component_tags %}
|
|
||||||
{% component "dynamic" is="haber_der_baber" variable="variable" %}{% endcomponent %}
|
|
||||||
"""
|
|
||||||
|
|
||||||
template = Template(simple_tag_template)
|
|
||||||
with pytest.raises(NotRegistered, match=re.escape("The component 'haber_der_baber' was not found")):
|
|
||||||
template.render(Context({}))
|
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
|
||||||
def test_component_called_with_variable_as_name(self, components_settings):
|
|
||||||
registry.register(name="test", component=self.SimpleComponent)
|
|
||||||
|
|
||||||
simple_tag_template: types.django_html = """
|
|
||||||
{% load component_tags %}
|
|
||||||
{% with component_name="test" %}
|
|
||||||
{% component "dynamic" is=component_name variable="variable" %}{% endcomponent %}
|
|
||||||
{% endwith %}
|
|
||||||
"""
|
|
||||||
|
|
||||||
template = Template(simple_tag_template)
|
|
||||||
rendered = template.render(Context({}))
|
|
||||||
assertHTMLEqual(
|
|
||||||
rendered,
|
|
||||||
"Variable: <strong data-djc-id-ca1bc3f data-djc-id-ca1bc40>variable</strong>",
|
|
||||||
)
|
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
|
||||||
def test_component_called_with_variable_as_spread(self, components_settings):
|
|
||||||
registry.register(name="test", component=self.SimpleComponent)
|
|
||||||
|
|
||||||
simple_tag_template: types.django_html = """
|
|
||||||
{% load component_tags %}
|
|
||||||
{% component "dynamic" ...props %}{% endcomponent %}
|
|
||||||
"""
|
|
||||||
|
|
||||||
template = Template(simple_tag_template)
|
|
||||||
rendered = template.render(
|
|
||||||
Context(
|
|
||||||
{
|
|
||||||
"props": {
|
|
||||||
"is": "test",
|
|
||||||
"variable": "variable",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
assertHTMLEqual(
|
|
||||||
rendered,
|
|
||||||
"Variable: <strong data-djc-id-ca1bc3f data-djc-id-ca1bc40>variable</strong>",
|
|
||||||
)
|
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
|
||||||
def test_component_as_class(self, components_settings):
|
|
||||||
registry.register(name="test", component=self.SimpleComponent)
|
|
||||||
|
|
||||||
simple_tag_template: types.django_html = """
|
|
||||||
{% load component_tags %}
|
|
||||||
{% component "dynamic" is=comp_cls variable="variable" %}{% endcomponent %}
|
|
||||||
"""
|
|
||||||
|
|
||||||
template = Template(simple_tag_template)
|
|
||||||
rendered = template.render(
|
|
||||||
Context(
|
|
||||||
{
|
|
||||||
"comp_cls": self.SimpleComponent,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
assertHTMLEqual(
|
|
||||||
rendered,
|
|
||||||
"Variable: <strong data-djc-id-ca1bc3f data-djc-id-ca1bc40>variable</strong>",
|
|
||||||
)
|
|
||||||
|
|
||||||
@djc_test(
|
|
||||||
parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR,
|
|
||||||
components_settings={
|
|
||||||
"tag_formatter": "django_components.component_shorthand_formatter",
|
|
||||||
"autodiscover": False,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
def test_shorthand_formatter(self, components_settings):
|
|
||||||
from django_components.apps import ComponentsConfig
|
|
||||||
|
|
||||||
ComponentsConfig.ready(None) # type: ignore[arg-type]
|
|
||||||
|
|
||||||
registry.register(name="test", component=self.SimpleComponent)
|
|
||||||
|
|
||||||
simple_tag_template: types.django_html = """
|
|
||||||
{% load component_tags %}
|
|
||||||
{% dynamic is="test" variable="variable" %}{% enddynamic %}
|
|
||||||
"""
|
|
||||||
|
|
||||||
template = Template(simple_tag_template)
|
|
||||||
rendered = template.render(Context({}))
|
|
||||||
assertHTMLEqual(rendered, "Variable: <strong data-djc-id-ca1bc3f data-djc-id-ca1bc40>variable</strong>\n")
|
|
||||||
|
|
||||||
@djc_test(
|
|
||||||
parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR,
|
|
||||||
components_settings={
|
|
||||||
"dynamic_component_name": "uno_reverse",
|
|
||||||
"tag_formatter": "django_components.component_shorthand_formatter",
|
|
||||||
"autodiscover": False,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
def test_component_name_is_configurable(self, components_settings):
|
|
||||||
from django_components.apps import ComponentsConfig
|
|
||||||
|
|
||||||
ComponentsConfig.ready(None) # type: ignore[arg-type]
|
|
||||||
|
|
||||||
registry.register(name="test", component=self.SimpleComponent)
|
|
||||||
|
|
||||||
simple_tag_template: types.django_html = """
|
|
||||||
{% load component_tags %}
|
|
||||||
{% uno_reverse is="test" variable="variable" %}{% enduno_reverse %}
|
|
||||||
"""
|
|
||||||
|
|
||||||
template = Template(simple_tag_template)
|
|
||||||
rendered = template.render(Context({}))
|
|
||||||
assertHTMLEqual(
|
|
||||||
rendered,
|
|
||||||
"Variable: <strong data-djc-id-ca1bc3f data-djc-id-ca1bc40>variable</strong>",
|
|
||||||
)
|
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
|
||||||
def test_raises_already_registered_on_name_conflict(self, components_settings):
|
|
||||||
with pytest.raises(
|
|
||||||
AlreadyRegistered,
|
|
||||||
match=re.escape('The component "dynamic" has already been registered'),
|
|
||||||
):
|
|
||||||
registry.register(name="dynamic", component=self.SimpleComponent)
|
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
|
||||||
def test_component_called_with_default_slot(self, components_settings):
|
|
||||||
class SimpleSlottedComponent(Component):
|
|
||||||
template: types.django_html = """
|
|
||||||
{% load component_tags %}
|
|
||||||
Variable: <strong>{{ variable }}</strong>
|
|
||||||
Slot: {% slot "default" default / %}
|
|
||||||
"""
|
|
||||||
|
|
||||||
def get_template_data(self, args, kwargs, slots, context):
|
|
||||||
return {
|
|
||||||
"variable": kwargs["variable"],
|
|
||||||
"variable2": kwargs.get("variable2", "default"),
|
|
||||||
}
|
|
||||||
|
|
||||||
registry.register(name="test", component=SimpleSlottedComponent)
|
|
||||||
|
|
||||||
simple_tag_template: types.django_html = """
|
|
||||||
{% load component_tags %}
|
|
||||||
{% with component_name="test" %}
|
|
||||||
{% component "dynamic" is=component_name variable="variable" %}
|
|
||||||
HELLO_FROM_SLOT
|
|
||||||
{% endcomponent %}
|
|
||||||
{% endwith %}
|
|
||||||
"""
|
|
||||||
|
|
||||||
template = Template(simple_tag_template)
|
|
||||||
rendered = template.render(Context({}))
|
|
||||||
assertHTMLEqual(
|
|
||||||
rendered,
|
|
||||||
"""
|
|
||||||
Variable: <strong data-djc-id-ca1bc3f data-djc-id-ca1bc40>variable</strong>
|
|
||||||
Slot: HELLO_FROM_SLOT
|
|
||||||
""",
|
|
||||||
)
|
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
|
||||||
def test_component_called_with_named_slots(self, components_settings):
|
|
||||||
class SimpleSlottedComponent(Component):
|
|
||||||
template: types.django_html = """
|
|
||||||
{% load component_tags %}
|
|
||||||
Variable: <strong>{{ variable }}</strong>
|
|
||||||
Slot 1: {% slot "default" default / %}
|
|
||||||
Slot 2: {% slot "two" / %}
|
|
||||||
"""
|
|
||||||
|
|
||||||
def get_template_data(self, args, kwargs, slots, context):
|
|
||||||
return {
|
|
||||||
"variable": kwargs["variable"],
|
|
||||||
"variable2": kwargs.get("variable2", "default"),
|
|
||||||
}
|
|
||||||
|
|
||||||
registry.register(name="test", component=SimpleSlottedComponent)
|
|
||||||
|
|
||||||
simple_tag_template: types.django_html = """
|
|
||||||
{% load component_tags %}
|
|
||||||
{% with component_name="test" %}
|
|
||||||
{% component "dynamic" is=component_name variable="variable" %}
|
|
||||||
{% fill "default" %}
|
|
||||||
HELLO_FROM_SLOT_1
|
|
||||||
{% endfill %}
|
|
||||||
{% fill "two" %}
|
|
||||||
HELLO_FROM_SLOT_2
|
|
||||||
{% endfill %}
|
|
||||||
{% endcomponent %}
|
|
||||||
{% endwith %}
|
|
||||||
"""
|
|
||||||
|
|
||||||
template = Template(simple_tag_template)
|
|
||||||
rendered = template.render(Context({}))
|
|
||||||
assertHTMLEqual(
|
|
||||||
rendered,
|
|
||||||
"""
|
|
||||||
Variable: <strong data-djc-id-ca1bc41 data-djc-id-ca1bc42>variable</strong>
|
|
||||||
Slot 1: HELLO_FROM_SLOT_1
|
|
||||||
Slot 2: HELLO_FROM_SLOT_2
|
|
||||||
""",
|
|
||||||
)
|
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
|
||||||
def test_ignores_invalid_slots(self, components_settings):
|
|
||||||
class SimpleSlottedComponent(Component):
|
|
||||||
template: types.django_html = """
|
|
||||||
{% load component_tags %}
|
|
||||||
Variable: <strong>{{ variable }}</strong>
|
|
||||||
Slot 1: {% slot "default" default / %}
|
|
||||||
Slot 2: {% slot "two" / %}
|
|
||||||
"""
|
|
||||||
|
|
||||||
def get_template_data(self, args, kwargs, slots, context):
|
|
||||||
return {
|
|
||||||
"variable": kwargs["variable"],
|
|
||||||
"variable2": kwargs.get("variable2", "default"),
|
|
||||||
}
|
|
||||||
|
|
||||||
registry.register(name="test", component=SimpleSlottedComponent)
|
|
||||||
|
|
||||||
simple_tag_template: types.django_html = """
|
|
||||||
{% load component_tags %}
|
|
||||||
{% with component_name="test" %}
|
|
||||||
{% component "dynamic" is=component_name variable="variable" %}
|
|
||||||
{% fill "default" %}
|
|
||||||
HELLO_FROM_SLOT_1
|
|
||||||
{% endfill %}
|
|
||||||
{% fill "three" %}
|
|
||||||
HELLO_FROM_SLOT_2
|
|
||||||
{% endfill %}
|
|
||||||
{% endcomponent %}
|
|
||||||
{% endwith %}
|
|
||||||
"""
|
|
||||||
|
|
||||||
template = Template(simple_tag_template)
|
|
||||||
rendered = template.render(Context({}))
|
|
||||||
assertHTMLEqual(
|
|
||||||
rendered,
|
|
||||||
"""
|
|
||||||
Variable: <strong data-djc-id-ca1bc41 data-djc-id-ca1bc42>variable</strong>
|
|
||||||
Slot 1: HELLO_FROM_SLOT_1
|
|
||||||
Slot 2:
|
|
||||||
""",
|
|
||||||
)
|
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
|
||||||
def test_raises_on_invalid_input(self, components_settings):
|
|
||||||
registry.register(name="test", component=self.SimpleComponent)
|
|
||||||
|
|
||||||
simple_tag_template: types.django_html = """
|
|
||||||
{% load component_tags %}
|
|
||||||
{% with component_name="test" %}
|
|
||||||
{% component "dynamic" is=component_name invalid_variable="variable" %}{% endcomponent %}
|
|
||||||
{% endwith %}
|
|
||||||
"""
|
|
||||||
|
|
||||||
template = Template(simple_tag_template)
|
|
||||||
with pytest.raises(
|
|
||||||
TypeError,
|
|
||||||
match=re.escape("got an unexpected keyword argument 'invalid_variable'"),
|
|
||||||
):
|
|
||||||
template.render(Context({}))
|
|
||||||
|
|
||||||
|
|
||||||
@djc_test
|
@djc_test
|
||||||
class TestMultiComponent:
|
class TestMultiComponent:
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
|
|
2
tox.ini
2
tox.ini
|
@ -39,6 +39,7 @@ deps =
|
||||||
# Othrwise we get error:
|
# Othrwise we get error:
|
||||||
# playwright._impl._errors.Error: BrowserType.launch: Executable doesn't exist at /home/runner/.cache/ms-playwright/chromium-1140/chrome-linux/chrome
|
# playwright._impl._errors.Error: BrowserType.launch: Executable doesn't exist at /home/runner/.cache/ms-playwright/chromium-1140/chrome-linux/chrome
|
||||||
playwright==1.48.0
|
playwright==1.48.0
|
||||||
|
pydantic
|
||||||
requests
|
requests
|
||||||
types-requests
|
types-requests
|
||||||
whitenoise
|
whitenoise
|
||||||
|
@ -60,6 +61,7 @@ deps =
|
||||||
syrupy # snapshot testing
|
syrupy # snapshot testing
|
||||||
# NOTE: Keep playwright in sync with the version in requirements-ci.txt
|
# NOTE: Keep playwright in sync with the version in requirements-ci.txt
|
||||||
playwright==1.48.0
|
playwright==1.48.0
|
||||||
|
pydantic
|
||||||
requests
|
requests
|
||||||
types-requests
|
types-requests
|
||||||
whitenoise
|
whitenoise
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue