mirror of
https://github.com/django-components/django-components.git
synced 2025-09-26 15:39:08 +00:00
feat: allow extensions to add url views (#1025)
* feat: allow extensions to add url views * refactor: fix linter errors
This commit is contained in:
parent
d3d2d0ab08
commit
12a64f8e41
16 changed files with 443 additions and 76 deletions
|
@ -9,7 +9,8 @@
|
||||||
- Hook into lifecycle events of django-components
|
- Hook into lifecycle events of django-components
|
||||||
- Pre-/post-process component inputs, outputs, and templates
|
- Pre-/post-process component inputs, outputs, and templates
|
||||||
- Add extra methods or attributes to Components
|
- Add extra methods or attributes to Components
|
||||||
- Add custom CLI commands to django-components
|
- Add custom extension-specific CLI commands
|
||||||
|
- Add custom extension-specific URL routes
|
||||||
|
|
||||||
Read more on [Extensions](https://django-components.github.io/django-components/0.131/concepts/advanced/extensions/).
|
Read more on [Extensions](https://django-components.github.io/django-components/0.131/concepts/advanced/extensions/).
|
||||||
|
|
||||||
|
|
|
@ -15,10 +15,10 @@ Extensions can be set by either as an import string or by passing in a class:
|
||||||
```python
|
```python
|
||||||
# settings.py
|
# settings.py
|
||||||
|
|
||||||
class MyExtension(ComponentsExtension):
|
class MyExtension(ComponentExtension):
|
||||||
name = "my_extension"
|
name = "my_extension"
|
||||||
|
|
||||||
class ExtensionClass(BaseExtensionClass):
|
class ExtensionClass(ComponentExtension.ExtensionClass):
|
||||||
...
|
...
|
||||||
|
|
||||||
COMPONENTS = ComponentsSettings(
|
COMPONENTS = ComponentsSettings(
|
||||||
|
@ -188,13 +188,13 @@ This is how extensions define the "default" behavior of their nested extension c
|
||||||
For example, the `View` base extension class defines the handlers for GET, POST, etc:
|
For example, the `View` base extension class defines the handlers for GET, POST, etc:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from django_components.extension import ComponentExtension, BaseExtensionClass
|
from django_components.extension import ComponentExtension
|
||||||
|
|
||||||
class ViewExtension(ComponentExtension):
|
class ViewExtension(ComponentExtension):
|
||||||
name = "view"
|
name = "view"
|
||||||
|
|
||||||
# The default behavior of the `View` extension class.
|
# The default behavior of the `View` extension class.
|
||||||
class ExtensionClass(BaseExtensionClass):
|
class ExtensionClass(ComponentExtension.ExtensionClass):
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
return self.component.get(request)
|
return self.component.get(request)
|
||||||
|
|
||||||
|
@ -229,7 +229,7 @@ class MyTable(Component):
|
||||||
|
|
||||||
!!! warning
|
!!! warning
|
||||||
|
|
||||||
When writing an extension, the `ExtensionClass` MUST subclass the base class [`BaseExtensionClass`](../../../reference/api/#django_components.ComponentExtension.BaseExtensionClass).
|
When writing an extension, the `ExtensionClass` MUST subclass the base class [`ComponentExtension.ExtensionClass`](../../../reference/api/#django_components.ComponentExtension.ExtensionClass).
|
||||||
|
|
||||||
This base class ensures that the extension class will have access to the component instance.
|
This base class ensures that the extension class will have access to the component instance.
|
||||||
|
|
||||||
|
@ -276,7 +276,7 @@ from django_components.extension import (
|
||||||
OnComponentInputContext,
|
OnComponentInputContext,
|
||||||
)
|
)
|
||||||
|
|
||||||
class ColorLoggerExtensionClass(BaseExtensionClass):
|
class ColorLoggerExtensionClass(ComponentExtension.ExtensionClass):
|
||||||
color: str
|
color: str
|
||||||
|
|
||||||
|
|
||||||
|
@ -356,7 +356,7 @@ This subclass should define:
|
||||||
- `handle` - the logic to execute when the command is run
|
- `handle` - the logic to execute when the command is run
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from django_components import ComponentCommand, ComponentsExtension
|
from django_components import ComponentCommand, ComponentExtension
|
||||||
|
|
||||||
class HelloCommand(ComponentCommand):
|
class HelloCommand(ComponentCommand):
|
||||||
name = "hello"
|
name = "hello"
|
||||||
|
@ -365,7 +365,7 @@ class HelloCommand(ComponentCommand):
|
||||||
def handle(self, *args, **kwargs):
|
def handle(self, *args, **kwargs):
|
||||||
print("Hello, world!")
|
print("Hello, world!")
|
||||||
|
|
||||||
class MyExt(ComponentsExtension):
|
class MyExt(ComponentExtension):
|
||||||
name = "my_ext"
|
name = "my_ext"
|
||||||
commands = [HelloCommand]
|
commands = [HelloCommand]
|
||||||
```
|
```
|
||||||
|
@ -382,7 +382,7 @@ as keyword arguments to the [`handle`](../../../reference/api#django_components.
|
||||||
method of the command.
|
method of the command.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from django_components import CommandArg, ComponentCommand, ComponentsExtension
|
from django_components import CommandArg, ComponentCommand, ComponentExtension
|
||||||
|
|
||||||
class HelloCommand(ComponentCommand):
|
class HelloCommand(ComponentCommand):
|
||||||
name = "hello"
|
name = "hello"
|
||||||
|
@ -443,7 +443,7 @@ to provide better organization and help messages.
|
||||||
Read more on [argparse argument groups](https://docs.python.org/3/library/argparse.html#argument-groups).
|
Read more on [argparse argument groups](https://docs.python.org/3/library/argparse.html#argument-groups).
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from django_components import CommandArg, CommandArgGroup, ComponentCommand, ComponentsExtension
|
from django_components import CommandArg, CommandArgGroup, ComponentCommand, ComponentExtension
|
||||||
|
|
||||||
class HelloCommand(ComponentCommand):
|
class HelloCommand(ComponentCommand):
|
||||||
name = "hello"
|
name = "hello"
|
||||||
|
@ -502,7 +502,7 @@ attribute of the extension, you define them in the
|
||||||
attribute of the parent command:
|
attribute of the parent command:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from django_components import CommandArg, CommandArgGroup, ComponentCommand, ComponentsExtension
|
from django_components import CommandArg, CommandArgGroup, ComponentCommand, ComponentExtension
|
||||||
|
|
||||||
class ChildCommand(ComponentCommand):
|
class ChildCommand(ComponentCommand):
|
||||||
name = "child"
|
name = "child"
|
||||||
|
@ -612,3 +612,128 @@ def test_hello_command(self):
|
||||||
output = out.getvalue()
|
output = out.getvalue()
|
||||||
assert output == "Hello, John!\n"
|
assert output == "Hello, John!\n"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Extension URLs
|
||||||
|
|
||||||
|
Extensions can define custom views and endpoints that can be accessed through the Django application.
|
||||||
|
|
||||||
|
To define URLs for an extension, set them in the [`urls`](../../../reference/api#django_components.ComponentExtension.urls) attribute of your [`ComponentExtension`](../../../reference/api#django_components.ComponentExtension) class. Each URL is defined using the [`URLRoute`](../../../reference/api#django_components.URLRoute) class, which specifies the path, handler, and optional name for the route.
|
||||||
|
|
||||||
|
Here's an example of how to define URLs within an extension:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from django_components.extension import ComponentExtension, URLRoute
|
||||||
|
from django.http import HttpResponse
|
||||||
|
|
||||||
|
def my_view(request):
|
||||||
|
return HttpResponse("Hello from my extension!")
|
||||||
|
|
||||||
|
class MyExtension(ComponentExtension):
|
||||||
|
name = "my_extension"
|
||||||
|
|
||||||
|
urls = [
|
||||||
|
URLRoute(path="my-view/", handler=my_view, name="my_view"),
|
||||||
|
URLRoute(path="another-view/<int:id>/", handler=my_view, name="another_view"),
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! warning
|
||||||
|
|
||||||
|
The [`URLRoute`](../../../reference/api#django_components.URLRoute) objects
|
||||||
|
are different from objects created with Django's
|
||||||
|
[`django.urls.path()`](https://docs.djangoproject.com/en/5.1/ref/urls/#path).
|
||||||
|
Do NOT use `URLRoute` objects in Django's [`urlpatterns`](https://docs.djangoproject.com/en/5.1/topics/http/urls/#example)
|
||||||
|
and vice versa!
|
||||||
|
|
||||||
|
django-components uses a custom [`URLRoute`](../../../reference/api#django_components.URLRoute) class to define framework-agnostic routing rules.
|
||||||
|
|
||||||
|
As of v0.131, `URLRoute` objects are directly converted to Django's `URLPattern` and `URLResolver` objects.
|
||||||
|
|
||||||
|
### Accessing Extension URLs
|
||||||
|
|
||||||
|
The URLs defined in an extension are available under the path
|
||||||
|
|
||||||
|
```
|
||||||
|
/components/ext/<extension_name>/
|
||||||
|
```
|
||||||
|
|
||||||
|
For example, if you have defined a URL with the path `my-view/<str:name>/` in an extension named `my_extension`, it can be accessed at:
|
||||||
|
|
||||||
|
```
|
||||||
|
/components/ext/my_extension/my-view/john/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Nested URLs
|
||||||
|
|
||||||
|
Extensions can also define nested URLs to allow for more complex routing structures.
|
||||||
|
|
||||||
|
To define nested URLs, set the [`children`](../../../reference/api#django_components.URLRoute.children)
|
||||||
|
attribute of the [`URLRoute`](../../../reference/api#django_components.URLRoute) object to
|
||||||
|
a list of child [`URLRoute`](../../../reference/api#django_components.URLRoute) objects:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class MyExtension(ComponentExtension):
|
||||||
|
name = "my_extension"
|
||||||
|
|
||||||
|
urls = [
|
||||||
|
URLRoute(
|
||||||
|
path="parent/",
|
||||||
|
name="parent_view",
|
||||||
|
children=[
|
||||||
|
URLRoute(path="child/<str:name>/", handler=my_view, name="child_view"),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
In this example, the URL
|
||||||
|
|
||||||
|
```
|
||||||
|
/components/ext/my_extension/parent/child/john/
|
||||||
|
```
|
||||||
|
|
||||||
|
would call the `my_view` handler with the parameter `name` set to `"John"`.
|
||||||
|
|
||||||
|
### Passing kwargs and other extra fields to URL routes
|
||||||
|
|
||||||
|
The [`URLRoute`](../../../reference/api#django_components.URLRoute) class is framework-agnostic,
|
||||||
|
so that extensions could be used with non-Django frameworks in the future.
|
||||||
|
|
||||||
|
However, that means that there may be some extra fields that Django's
|
||||||
|
[`django.urls.path()`](https://docs.djangoproject.com/en/5.1/ref/urls/#path)
|
||||||
|
accepts, but which are not defined on the `URLRoute` object.
|
||||||
|
|
||||||
|
To address this, the [`URLRoute`](../../../reference/api#django_components.URLRoute) object has
|
||||||
|
an [`extra`](../../../reference/api#django_components.URLRoute.extra) attribute,
|
||||||
|
which is a dictionary that can be used to pass any extra kwargs to `django.urls.path()`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
URLRoute(
|
||||||
|
path="my-view/<str:name>/",
|
||||||
|
handler=my_view,
|
||||||
|
name="my_view",
|
||||||
|
extra={"kwargs": {"foo": "bar"} },
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Is the same as:
|
||||||
|
|
||||||
|
```python
|
||||||
|
django.urls.path(
|
||||||
|
"my-view/<str:name>/",
|
||||||
|
view=my_view,
|
||||||
|
name="my_view",
|
||||||
|
kwargs={"foo": "bar"},
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
because `URLRoute` is converted to Django's route like so:
|
||||||
|
|
||||||
|
```python
|
||||||
|
django.urls.path(
|
||||||
|
route.path,
|
||||||
|
view=route.handler,
|
||||||
|
name=route.name,
|
||||||
|
**route.extra,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
|
@ -35,6 +35,10 @@
|
||||||
options:
|
options:
|
||||||
show_if_no_docstring: true
|
show_if_no_docstring: true
|
||||||
|
|
||||||
|
::: django_components.ComponentCommand
|
||||||
|
options:
|
||||||
|
show_if_no_docstring: true
|
||||||
|
|
||||||
::: django_components.ComponentExtension
|
::: django_components.ComponentExtension
|
||||||
options:
|
options:
|
||||||
show_if_no_docstring: true
|
show_if_no_docstring: true
|
||||||
|
@ -63,10 +67,6 @@
|
||||||
options:
|
options:
|
||||||
show_if_no_docstring: true
|
show_if_no_docstring: true
|
||||||
|
|
||||||
::: django_components.ComponentCommand
|
|
||||||
options:
|
|
||||||
show_if_no_docstring: true
|
|
||||||
|
|
||||||
::: django_components.ComponentsSettings
|
::: django_components.ComponentsSettings
|
||||||
options:
|
options:
|
||||||
show_if_no_docstring: true
|
show_if_no_docstring: true
|
||||||
|
@ -147,6 +147,14 @@
|
||||||
options:
|
options:
|
||||||
show_if_no_docstring: true
|
show_if_no_docstring: true
|
||||||
|
|
||||||
|
::: django_components.URLRoute
|
||||||
|
options:
|
||||||
|
show_if_no_docstring: true
|
||||||
|
|
||||||
|
::: django_components.URLRouteHandler
|
||||||
|
options:
|
||||||
|
show_if_no_docstring: true
|
||||||
|
|
||||||
::: django_components.autodiscover
|
::: django_components.autodiscover
|
||||||
options:
|
options:
|
||||||
show_if_no_docstring: true
|
show_if_no_docstring: true
|
||||||
|
@ -182,4 +190,3 @@
|
||||||
::: django_components.template_tag
|
::: django_components.template_tag
|
||||||
options:
|
options:
|
||||||
show_if_no_docstring: true
|
show_if_no_docstring: true
|
||||||
|
|
||||||
|
|
|
@ -51,7 +51,9 @@ python manage.py components ext run <extension> <command>
|
||||||
## `components create`
|
## `components create`
|
||||||
|
|
||||||
```txt
|
```txt
|
||||||
usage: python manage.py components create [-h] [--path PATH] [--js JS] [--css CSS] [--template TEMPLATE] [--force] [--verbose] [--dry-run] name
|
usage: python manage.py components create [-h] [--path PATH] [--js JS] [--css CSS] [--template TEMPLATE] [--force] [--verbose]
|
||||||
|
[--dry-run]
|
||||||
|
name
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -293,7 +295,7 @@ Each extension can add its own commands, which will be available to run with thi
|
||||||
For example, if you define and install the following extension:
|
For example, if you define and install the following extension:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from django_components ComponentCommand, ComponentsExtension
|
from django_components import ComponentCommand, ComponentExtension
|
||||||
|
|
||||||
class HelloCommand(ComponentCommand):
|
class HelloCommand(ComponentCommand):
|
||||||
name = "hello"
|
name = "hello"
|
||||||
|
@ -301,7 +303,7 @@ class HelloCommand(ComponentCommand):
|
||||||
def handle(self, *args, **kwargs):
|
def handle(self, *args, **kwargs):
|
||||||
print("Hello, world!")
|
print("Hello, world!")
|
||||||
|
|
||||||
class MyExt(ComponentsExtension):
|
class MyExt(ComponentExtension):
|
||||||
name = "my_ext"
|
name = "my_ext"
|
||||||
commands = [HelloCommand]
|
commands = [HelloCommand]
|
||||||
```
|
```
|
||||||
|
@ -315,7 +317,7 @@ python manage.py components ext run my_ext hello
|
||||||
You can also define arguments for the command, which will be passed to the command's `handle` method.
|
You can also define arguments for the command, which will be passed to the command's `handle` method.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from django_components import CommandArg, ComponentCommand, ComponentsExtension
|
from django_components import CommandArg, ComponentCommand, ComponentExtension
|
||||||
|
|
||||||
class HelloCommand(ComponentCommand):
|
class HelloCommand(ComponentCommand):
|
||||||
name = "hello"
|
name = "hello"
|
||||||
|
@ -349,14 +351,15 @@ python manage.py components ext run my_ext hello --name John --shout
|
||||||
## `upgradecomponent`
|
## `upgradecomponent`
|
||||||
|
|
||||||
```txt
|
```txt
|
||||||
usage: upgradecomponent [-h] [--path PATH] [--version] [-v {0,1,2,3}] [--settings SETTINGS] [--pythonpath PYTHONPATH]
|
usage: upgradecomponent [-h] [--path PATH] [--version] [-v {0,1,2,3}] [--settings SETTINGS]
|
||||||
[--traceback] [--no-color] [--force-color] [--skip-checks]
|
[--pythonpath PYTHONPATH] [--traceback] [--no-color] [--force-color]
|
||||||
|
[--skip-checks]
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/management/commands/upgradecomponent.py#L83" target="_blank">See source code</a>
|
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/management/commands/upgradecomponent.py#L89" target="_blank">See source code</a>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -394,16 +397,17 @@ Deprecated. Use `components upgrade` instead.
|
||||||
## `startcomponent`
|
## `startcomponent`
|
||||||
|
|
||||||
```txt
|
```txt
|
||||||
usage: startcomponent [-h] [--path PATH] [--js JS] [--css CSS] [--template TEMPLATE] [--force] [--verbose] [--dry-run]
|
usage: startcomponent [-h] [--path PATH] [--js JS] [--css CSS] [--template TEMPLATE] [--force]
|
||||||
[--version] [-v {0,1,2,3}] [--settings SETTINGS] [--pythonpath PYTHONPATH] [--traceback]
|
[--verbose] [--dry-run] [--version] [-v {0,1,2,3}] [--settings SETTINGS]
|
||||||
[--no-color] [--force-color] [--skip-checks]
|
[--pythonpath PYTHONPATH] [--traceback] [--no-color] [--force-color]
|
||||||
|
[--skip-checks]
|
||||||
name
|
name
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/management/commands/startcomponent.py#L83" target="_blank">See source code</a>
|
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/management/commands/startcomponent.py#L89" target="_blank">See source code</a>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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#L1573" target="_blank">See source code</a>
|
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L1478" target="_blank">See source code</a>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -175,7 +175,7 @@ can access only the data that was explicitly passed to it:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L612" target="_blank">See source code</a>
|
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L614" target="_blank">See source code</a>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -416,7 +416,7 @@ user = self.inject("user_data")["user"]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L153" target="_blank">See source code</a>
|
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L155" target="_blank">See source code</a>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -53,6 +53,7 @@ from django_components.tag_formatter import (
|
||||||
from django_components.template import cached_template
|
from django_components.template import cached_template
|
||||||
import django_components.types as types
|
import django_components.types as types
|
||||||
from django_components.util.loader import ComponentFileEntry, get_component_dirs, get_component_files
|
from django_components.util.loader import ComponentFileEntry, get_component_dirs, get_component_files
|
||||||
|
from django_components.util.routing import URLRoute, URLRouteHandler
|
||||||
from django_components.util.types import EmptyTuple, EmptyDict
|
from django_components.util.types import EmptyTuple, EmptyDict
|
||||||
|
|
||||||
# isort: on
|
# isort: on
|
||||||
|
@ -113,4 +114,6 @@ __all__ = [
|
||||||
"TagResult",
|
"TagResult",
|
||||||
"template_tag",
|
"template_tag",
|
||||||
"types",
|
"types",
|
||||||
|
"URLRoute",
|
||||||
|
"URLRouteHandler",
|
||||||
]
|
]
|
||||||
|
|
|
@ -55,7 +55,7 @@ class ExtRunCommand(ComponentCommand):
|
||||||
For example, if you define and install the following extension:
|
For example, if you define and install the following extension:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from django_components ComponentCommand, ComponentsExtension
|
from django_components import ComponentCommand, ComponentExtension
|
||||||
|
|
||||||
class HelloCommand(ComponentCommand):
|
class HelloCommand(ComponentCommand):
|
||||||
name = "hello"
|
name = "hello"
|
||||||
|
@ -63,7 +63,7 @@ class ExtRunCommand(ComponentCommand):
|
||||||
def handle(self, *args, **kwargs):
|
def handle(self, *args, **kwargs):
|
||||||
print("Hello, world!")
|
print("Hello, world!")
|
||||||
|
|
||||||
class MyExt(ComponentsExtension):
|
class MyExt(ComponentExtension):
|
||||||
name = "my_ext"
|
name = "my_ext"
|
||||||
commands = [HelloCommand]
|
commands = [HelloCommand]
|
||||||
```
|
```
|
||||||
|
@ -77,7 +77,7 @@ class ExtRunCommand(ComponentCommand):
|
||||||
You can also define arguments for the command, which will be passed to the command's `handle` method.
|
You can also define arguments for the command, which will be passed to the command's `handle` method.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from django_components import CommandArg, ComponentCommand, ComponentsExtension
|
from django_components import CommandArg, ComponentCommand, ComponentExtension
|
||||||
|
|
||||||
class HelloCommand(ComponentCommand):
|
class HelloCommand(ComponentCommand):
|
||||||
name = "hello"
|
name = "hello"
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
from argparse import ArgumentParser
|
from argparse import ArgumentParser
|
||||||
from typing import Any, Optional, Type
|
from typing import Any, List, Optional, Type
|
||||||
|
|
||||||
import django
|
import django
|
||||||
|
import django.urls as django_urls
|
||||||
from django.core.management.base import BaseCommand as DjangoCommand
|
from django.core.management.base import BaseCommand as DjangoCommand
|
||||||
|
from django.urls import URLPattern
|
||||||
|
|
||||||
from django_components.util.command import (
|
from django_components.util.command import (
|
||||||
CommandArg,
|
CommandArg,
|
||||||
|
@ -10,6 +12,11 @@ from django_components.util.command import (
|
||||||
_setup_command_arg,
|
_setup_command_arg,
|
||||||
setup_parser_from_command,
|
setup_parser_from_command,
|
||||||
)
|
)
|
||||||
|
from django_components.util.routing import URLRoute
|
||||||
|
|
||||||
|
################################################
|
||||||
|
# COMMANDS
|
||||||
|
################################################
|
||||||
|
|
||||||
# Django command arguments added to all commands
|
# Django command arguments added to all commands
|
||||||
# NOTE: Many of these MUST be present for the command to work with Django
|
# NOTE: Many of these MUST be present for the command to work with Django
|
||||||
|
@ -114,3 +121,44 @@ def load_as_django_command(command: Type[ComponentCommand]) -> Type[DjangoComman
|
||||||
Command.__doc__ = command.__doc__
|
Command.__doc__ = command.__doc__
|
||||||
|
|
||||||
return Command
|
return Command
|
||||||
|
|
||||||
|
|
||||||
|
################################################
|
||||||
|
# ROUTING
|
||||||
|
################################################
|
||||||
|
|
||||||
|
|
||||||
|
def routes_to_django(routes: List[URLRoute]) -> List[URLPattern]:
|
||||||
|
"""
|
||||||
|
Convert a list of `URLRoute` objects to a list of `URLPattern` objects.
|
||||||
|
|
||||||
|
The result is similar to Django's `django.urls.path()` function.
|
||||||
|
|
||||||
|
Nested routes are recursively converted to Django with `django.urls.include()`.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
urls_to_django([
|
||||||
|
URLPattern(
|
||||||
|
"/my/path",
|
||||||
|
handler=my_handler,
|
||||||
|
name="my_name",
|
||||||
|
extra={"kwargs": {"my_extra": "my_value"} },
|
||||||
|
),
|
||||||
|
])
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
django_routes: List[URLPattern] = []
|
||||||
|
for route in routes:
|
||||||
|
# The handler is equivalent to `view` function in Django
|
||||||
|
if route.handler is not None:
|
||||||
|
django_handler = route.handler
|
||||||
|
else:
|
||||||
|
# If the URL has children paths, it's equivalent to "including" another `urlpatterns` in Django
|
||||||
|
subpaths = routes_to_django(route.children)
|
||||||
|
django_handler = django_urls.include(subpaths)
|
||||||
|
|
||||||
|
django_route = django_urls.path(route.path, django_handler, name=route.name, **route.extra)
|
||||||
|
django_routes.append(django_route)
|
||||||
|
return django_routes
|
||||||
|
|
|
@ -111,7 +111,7 @@ CssDataType = TypeVar("CssDataType", bound=Mapping[str, Any])
|
||||||
|
|
||||||
# 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):
|
||||||
AllComponents = List[ReferenceType["ComponentRegistry"]]
|
AllComponents = List[ReferenceType["Component"]]
|
||||||
else:
|
else:
|
||||||
AllComponents = List[ReferenceType]
|
AllComponents = List[ReferenceType]
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from typing import TYPE_CHECKING, Any, Callable, Dict, List, NamedTuple, Tuple, Type, TypeVar
|
from typing import TYPE_CHECKING, Any, Callable, Dict, List, NamedTuple, Tuple, Type, TypeVar
|
||||||
|
|
||||||
|
import django.urls
|
||||||
from django.template import Context
|
from django.template import Context
|
||||||
|
from django.urls import URLResolver
|
||||||
|
|
||||||
from django_components.app_settings import app_settings
|
from django_components.app_settings import app_settings
|
||||||
|
from django_components.compat.django import routes_to_django
|
||||||
from django_components.util.command import ComponentCommand
|
from django_components.util.command import ComponentCommand
|
||||||
from django_components.util.misc import snake_to_pascal
|
from django_components.util.misc import snake_to_pascal
|
||||||
|
from django_components.util.routing import URLRoute
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from django_components import Component
|
from django_components import Component
|
||||||
|
@ -93,6 +97,11 @@ class OnComponentDataContext(NamedTuple):
|
||||||
"""Dictionary of CSS data from `Component.get_css_data()`"""
|
"""Dictionary of CSS data from `Component.get_css_data()`"""
|
||||||
|
|
||||||
|
|
||||||
|
################################################
|
||||||
|
# EXTENSIONS CORE
|
||||||
|
################################################
|
||||||
|
|
||||||
|
|
||||||
class BaseExtensionClass:
|
class BaseExtensionClass:
|
||||||
def __init__(self, component: "Component") -> None:
|
def __init__(self, component: "Component") -> None:
|
||||||
self.component = component
|
self.component = component
|
||||||
|
@ -178,7 +187,7 @@ class ComponentExtension:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
class MyComp(Component):
|
class MyComp(Component):
|
||||||
class MyExtension(BaseExtensionClass):
|
class MyExtension(ComponentExtension.ExtensionClass):
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -244,6 +253,8 @@ class ComponentExtension:
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
urls: List[URLRoute] = []
|
||||||
|
|
||||||
def __init_subclass__(cls) -> None:
|
def __init_subclass__(cls) -> None:
|
||||||
if not cls.name.isidentifier():
|
if not cls.name.isidentifier():
|
||||||
raise ValueError(f"Extension name must be a valid Python identifier, got {cls.name}")
|
raise ValueError(f"Extension name must be a valid Python identifier, got {cls.name}")
|
||||||
|
@ -562,6 +573,12 @@ class ExtensionManager:
|
||||||
extension_instance = used_ext_class(component)
|
extension_instance = used_ext_class(component)
|
||||||
setattr(component, extension.name, extension_instance)
|
setattr(component, extension.name, extension_instance)
|
||||||
|
|
||||||
|
def _init_app(self) -> None:
|
||||||
|
if self._initialized:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._initialized = True
|
||||||
|
|
||||||
# The triggers for following hooks may occur before the `apps.py` `ready()` hook is called.
|
# The triggers for following hooks may occur before the `apps.py` `ready()` hook is called.
|
||||||
# - on_component_class_created
|
# - on_component_class_created
|
||||||
# - on_component_class_deleted
|
# - on_component_class_deleted
|
||||||
|
@ -576,12 +593,6 @@ class ExtensionManager:
|
||||||
# we store these "events" in a list, and then "flush" them all when `ready()` is called.
|
# we store these "events" in a list, and then "flush" them all when `ready()` is called.
|
||||||
#
|
#
|
||||||
# This way, we can ensure that all extensions are present before any hooks are called.
|
# This way, we can ensure that all extensions are present before any hooks are called.
|
||||||
def _init_app(self) -> None:
|
|
||||||
if self._initialized:
|
|
||||||
return
|
|
||||||
|
|
||||||
self._initialized = True
|
|
||||||
|
|
||||||
for hook, data in self._events:
|
for hook, data in self._events:
|
||||||
if hook == "on_component_class_created":
|
if hook == "on_component_class_created":
|
||||||
on_component_created_data: OnComponentClassCreatedContext = data
|
on_component_created_data: OnComponentClassCreatedContext = data
|
||||||
|
@ -589,6 +600,24 @@ class ExtensionManager:
|
||||||
getattr(self, hook)(data)
|
getattr(self, hook)(data)
|
||||||
self._events = []
|
self._events = []
|
||||||
|
|
||||||
|
# Populate the `urlpatterns` with URLs specified by the extensions
|
||||||
|
# TODO_V3 - Django-specific logic - replace with hook
|
||||||
|
urls: List[URLResolver] = []
|
||||||
|
for extension in self.extensions:
|
||||||
|
ext_urls = routes_to_django(extension.urls)
|
||||||
|
ext_url_path = django.urls.path(f"{extension.name}/", django.urls.include(ext_urls))
|
||||||
|
urls.append(ext_url_path)
|
||||||
|
|
||||||
|
# NOTE: `urlconf_name` is the actual source of truth that holds either a list of URLPatterns
|
||||||
|
# or an import string thereof.
|
||||||
|
# However, Django's `URLResolver` caches the resolved value of `urlconf_name`
|
||||||
|
# under the key `url_patterns`.
|
||||||
|
# So we set both:
|
||||||
|
# - `urlconf_name` to update the source of truth
|
||||||
|
# - `url_patterns` to override the caching
|
||||||
|
ext_url_resolver.urlconf_name = urls
|
||||||
|
ext_url_resolver.url_patterns = urls
|
||||||
|
|
||||||
def get_extension(self, name: str) -> ComponentExtension:
|
def get_extension(self, name: str) -> ComponentExtension:
|
||||||
for extension in self.extensions:
|
for extension in self.extensions:
|
||||||
if extension.name == name:
|
if extension.name == name:
|
||||||
|
@ -651,3 +680,25 @@ class ExtensionManager:
|
||||||
|
|
||||||
# NOTE: This is a singleton which is takes the extensions from `app_settings.EXTENSIONS`
|
# NOTE: This is a singleton which is takes the extensions from `app_settings.EXTENSIONS`
|
||||||
extensions = ExtensionManager()
|
extensions = ExtensionManager()
|
||||||
|
|
||||||
|
|
||||||
|
################################
|
||||||
|
# VIEW
|
||||||
|
################################
|
||||||
|
|
||||||
|
# Extensions can define their own URLs, which will be added to the `urlpatterns` list.
|
||||||
|
# These will be available under the `/components/ext/<extension_name>/` path, e.g.:
|
||||||
|
# `/components/ext/my_extension/path/to/route/<str:name>/<int:id>/`
|
||||||
|
urlpatterns = [
|
||||||
|
django.urls.path("ext/", django.urls.include([])),
|
||||||
|
]
|
||||||
|
|
||||||
|
# NOTE: Normally we'd pass all the routes introduced by extensions to `django.urls.include()` and
|
||||||
|
# `django.urls.path()` to construct the `URLResolver` objects that would take care of the rest.
|
||||||
|
#
|
||||||
|
# However, Django's `urlpatterns` are constructed BEFORE the `ready()` hook is called,
|
||||||
|
# and so before the extensions are ready.
|
||||||
|
#
|
||||||
|
# As such, we lazily set the extensions' routes to the `URLResolver` object. And we use the `include()
|
||||||
|
# and `path()` funtions above to ensure that the `URLResolver` object is created correctly.
|
||||||
|
ext_url_resolver: URLResolver = urlpatterns[0]
|
||||||
|
|
|
@ -3,7 +3,7 @@ from typing import TYPE_CHECKING, Any, Protocol, cast
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
|
|
||||||
from django_components.extension import BaseExtensionClass, ComponentExtension
|
from django_components.extension import ComponentExtension
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from django_components.component import Component
|
from django_components.component import Component
|
||||||
|
@ -13,7 +13,7 @@ class ViewFn(Protocol):
|
||||||
def __call__(self, request: HttpRequest, *args: Any, **kwargs: Any) -> Any: ... # noqa: E704
|
def __call__(self, request: HttpRequest, *args: Any, **kwargs: Any) -> Any: ... # noqa: E704
|
||||||
|
|
||||||
|
|
||||||
class ComponentView(BaseExtensionClass, View):
|
class ComponentView(ComponentExtension.ExtensionClass, View): # type: ignore
|
||||||
"""
|
"""
|
||||||
Subclass of `django.views.View` where the `Component` instance is available
|
Subclass of `django.views.View` where the `Component` instance is available
|
||||||
via `self.component`.
|
via `self.component`.
|
||||||
|
@ -24,7 +24,7 @@ class ComponentView(BaseExtensionClass, View):
|
||||||
component = cast("Component", None)
|
component = cast("Component", None)
|
||||||
|
|
||||||
def __init__(self, component: "Component", **kwargs: Any) -> None:
|
def __init__(self, component: "Component", **kwargs: Any) -> None:
|
||||||
BaseExtensionClass.__init__(self, component)
|
ComponentExtension.ExtensionClass.__init__(self, component)
|
||||||
View.__init__(self, **kwargs)
|
View.__init__(self, **kwargs)
|
||||||
|
|
||||||
# NOTE: The methods below are defined to satisfy the `View` class. All supported methods
|
# NOTE: The methods below are defined to satisfy the `View` class. All supported methods
|
||||||
|
|
|
@ -1,7 +1,18 @@
|
||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
|
|
||||||
|
from django_components.dependencies import urlpatterns as dependencies_urlpatterns
|
||||||
|
from django_components.extension import urlpatterns as extension_urlpatterns
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("components/", include("django_components.dependencies")),
|
path(
|
||||||
|
"components/",
|
||||||
|
include(
|
||||||
|
[
|
||||||
|
*dependencies_urlpatterns,
|
||||||
|
*extension_urlpatterns,
|
||||||
|
]
|
||||||
|
),
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
__all__ = ["urlpatterns"]
|
__all__ = ["urlpatterns"]
|
||||||
|
|
|
@ -228,7 +228,7 @@ class ComponentCommand:
|
||||||
For example, if you define and install the following extension:
|
For example, if you define and install the following extension:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from django_components ComponentCommand, ComponentsExtension
|
from django_components ComponentCommand, ComponentExtension
|
||||||
|
|
||||||
class HelloCommand(ComponentCommand):
|
class HelloCommand(ComponentCommand):
|
||||||
name = "hello"
|
name = "hello"
|
||||||
|
@ -236,7 +236,7 @@ class ComponentCommand:
|
||||||
def handle(self, *args, **kwargs):
|
def handle(self, *args, **kwargs):
|
||||||
print("Hello, world!")
|
print("Hello, world!")
|
||||||
|
|
||||||
class MyExt(ComponentsExtension):
|
class MyExt(ComponentExtension):
|
||||||
name = "my_ext"
|
name = "my_ext"
|
||||||
commands = [HelloCommand]
|
commands = [HelloCommand]
|
||||||
```
|
```
|
||||||
|
@ -250,7 +250,7 @@ class ComponentCommand:
|
||||||
You can also define arguments for the command, which will be passed to the command's `handle` method.
|
You can also define arguments for the command, which will be passed to the command's `handle` method.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from django_components import CommandArg, ComponentCommand, ComponentsExtension
|
from django_components import CommandArg, ComponentCommand, ComponentExtension
|
||||||
|
|
||||||
class HelloCommand(ComponentCommand):
|
class HelloCommand(ComponentCommand):
|
||||||
name = "hello"
|
name = "hello"
|
||||||
|
|
62
src/django_components/util/routing.py
Normal file
62
src/django_components/util/routing.py
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Any, Dict, List, Optional, Protocol
|
||||||
|
|
||||||
|
|
||||||
|
class URLRouteHandler(Protocol):
|
||||||
|
"""Framework-agnostic 'view' function for routes"""
|
||||||
|
|
||||||
|
def __call__(self, request: Any, *args: Any, **kwargs: Any) -> Any: ... # noqa: E704
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class URLRoute:
|
||||||
|
"""
|
||||||
|
Framework-agnostic route definition.
|
||||||
|
|
||||||
|
This is similar to Django's `URLPattern` object created with
|
||||||
|
[`django.urls.path()`](https://docs.djangoproject.com/en/5.1/ref/urls/#path).
|
||||||
|
|
||||||
|
The `URLRoute` must either define a `handler` function or have a list of child routes `children`.
|
||||||
|
If both are defined, an error will be raised.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
URLRoute("/my/path", handler=my_handler, name="my_name", extra={"kwargs": {"my_extra": "my_value"}})
|
||||||
|
```
|
||||||
|
|
||||||
|
Is equivalent to:
|
||||||
|
|
||||||
|
```python
|
||||||
|
django.urls.path("/my/path", my_handler, name="my_name", kwargs={"my_extra": "my_value"})
|
||||||
|
```
|
||||||
|
|
||||||
|
With children:
|
||||||
|
|
||||||
|
```python
|
||||||
|
URLRoute(
|
||||||
|
"/my/path",
|
||||||
|
name="my_name",
|
||||||
|
extra={"kwargs": {"my_extra": "my_value"}},
|
||||||
|
children=[
|
||||||
|
URLRoute(
|
||||||
|
"/child/<str:name>/",
|
||||||
|
handler=my_handler,
|
||||||
|
name="my_name",
|
||||||
|
extra={"kwargs": {"my_extra": "my_value"}},
|
||||||
|
),
|
||||||
|
URLRoute("/other/<int:id>/", handler=other_handler),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
path: str
|
||||||
|
handler: Optional[URLRouteHandler] = None
|
||||||
|
children: List["URLRoute"] = field(default_factory=list)
|
||||||
|
name: Optional[str] = None
|
||||||
|
extra: Dict[str, Any] = field(default_factory=dict)
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
if self.handler is not None and self.children:
|
||||||
|
raise ValueError("Cannot have both handler and children")
|
|
@ -438,6 +438,7 @@ def _setup_djc_global_state(
|
||||||
from django_components.app_settings import app_settings
|
from django_components.app_settings import app_settings
|
||||||
|
|
||||||
app_settings._load_settings()
|
app_settings._load_settings()
|
||||||
|
extensions._initialized = False
|
||||||
extensions._init_app()
|
extensions._init_app()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
import gc
|
import gc
|
||||||
from typing import Any, Callable, Dict, List, cast
|
from typing import Any, Callable, Dict, List, cast
|
||||||
|
|
||||||
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django.template import Context
|
from django.template import Context
|
||||||
|
from django.test import Client
|
||||||
|
|
||||||
from django_components import Component, Slot, register, registry
|
from django_components import Component, Slot, register, registry
|
||||||
from django_components.app_settings import app_settings
|
from django_components.app_settings import app_settings
|
||||||
from django_components.component_registry import ComponentRegistry
|
from django_components.component_registry import ComponentRegistry
|
||||||
from django_components.extension import (
|
from django_components.extension import (
|
||||||
|
URLRoute,
|
||||||
ComponentExtension,
|
ComponentExtension,
|
||||||
OnComponentClassCreatedContext,
|
OnComponentClassCreatedContext,
|
||||||
OnComponentClassDeletedContext,
|
OnComponentClassDeletedContext,
|
||||||
|
@ -25,6 +28,16 @@ from .testutils import setup_test_config
|
||||||
setup_test_config({"autodiscover": False})
|
setup_test_config({"autodiscover": False})
|
||||||
|
|
||||||
|
|
||||||
|
def dummy_view(request: HttpRequest):
|
||||||
|
# Test that the request object is passed to the view
|
||||||
|
assert isinstance(request, HttpRequest)
|
||||||
|
return HttpResponse("Hello, world!")
|
||||||
|
|
||||||
|
|
||||||
|
def dummy_view_2(request: HttpRequest, id: int, name: str):
|
||||||
|
return HttpResponse(f"Hello, world! {id} {name}")
|
||||||
|
|
||||||
|
|
||||||
class DummyExtension(ComponentExtension):
|
class DummyExtension(ComponentExtension):
|
||||||
"""
|
"""
|
||||||
Test extension that tracks all hook calls and their arguments.
|
Test extension that tracks all hook calls and their arguments.
|
||||||
|
@ -44,6 +57,11 @@ class DummyExtension(ComponentExtension):
|
||||||
"on_component_data": [],
|
"on_component_data": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
urls = [
|
||||||
|
URLRoute(path="dummy-view/", handler=dummy_view, name="dummy"),
|
||||||
|
URLRoute(path="dummy-view-2/<int:id>/<str:name>/", handler=dummy_view_2, name="dummy-2"),
|
||||||
|
]
|
||||||
|
|
||||||
def on_component_class_created(self, ctx: OnComponentClassCreatedContext) -> None:
|
def on_component_class_created(self, ctx: OnComponentClassCreatedContext) -> None:
|
||||||
# NOTE: Store only component name to avoid strong references
|
# NOTE: Store only component name to avoid strong references
|
||||||
self.calls["on_component_class_created"].append(ctx.component_cls.__name__)
|
self.calls["on_component_class_created"].append(ctx.component_cls.__name__)
|
||||||
|
@ -73,6 +91,20 @@ class DummyExtension(ComponentExtension):
|
||||||
self.calls["on_component_data"].append(ctx)
|
self.calls["on_component_data"].append(ctx)
|
||||||
|
|
||||||
|
|
||||||
|
class DummyNestedExtension(ComponentExtension):
|
||||||
|
name = "test_nested_extension"
|
||||||
|
|
||||||
|
urls = [
|
||||||
|
URLRoute(
|
||||||
|
path="nested-view/",
|
||||||
|
children=[
|
||||||
|
URLRoute(path="<int:id>/<str:name>/", handler=dummy_view_2, name="dummy-2"),
|
||||||
|
],
|
||||||
|
name="dummy",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def with_component_cls(on_created: Callable):
|
def with_component_cls(on_created: Callable):
|
||||||
class TestComponent(Component):
|
class TestComponent(Component):
|
||||||
template = "Hello {{ name }}!"
|
template = "Hello {{ name }}!"
|
||||||
|
@ -91,17 +123,16 @@ def with_registry(on_created: Callable):
|
||||||
|
|
||||||
@djc_test
|
@djc_test
|
||||||
class TestExtension:
|
class TestExtension:
|
||||||
@djc_test(
|
@djc_test(components_settings={"extensions": [DummyExtension]})
|
||||||
components_settings={"extensions": [DummyExtension]}
|
|
||||||
)
|
|
||||||
def test_extensios_setting(self):
|
def test_extensios_setting(self):
|
||||||
assert len(app_settings.EXTENSIONS) == 2
|
assert len(app_settings.EXTENSIONS) == 2
|
||||||
assert isinstance(app_settings.EXTENSIONS[0], ViewExtension)
|
assert isinstance(app_settings.EXTENSIONS[0], ViewExtension)
|
||||||
assert isinstance(app_settings.EXTENSIONS[1], DummyExtension)
|
assert isinstance(app_settings.EXTENSIONS[1], DummyExtension)
|
||||||
|
|
||||||
@djc_test(
|
|
||||||
components_settings={"extensions": [DummyExtension]}
|
@djc_test
|
||||||
)
|
class TestExtensionHooks:
|
||||||
|
@djc_test(components_settings={"extensions": [DummyExtension]})
|
||||||
def test_component_class_lifecycle_hooks(self):
|
def test_component_class_lifecycle_hooks(self):
|
||||||
extension = cast(DummyExtension, app_settings.EXTENSIONS[1])
|
extension = cast(DummyExtension, app_settings.EXTENSIONS[1])
|
||||||
|
|
||||||
|
@ -130,9 +161,7 @@ class TestExtension:
|
||||||
assert len(extension.calls["on_component_class_deleted"]) == 1
|
assert len(extension.calls["on_component_class_deleted"]) == 1
|
||||||
assert extension.calls["on_component_class_deleted"][0] == "TestComponent"
|
assert extension.calls["on_component_class_deleted"][0] == "TestComponent"
|
||||||
|
|
||||||
@djc_test(
|
@djc_test(components_settings={"extensions": [DummyExtension]})
|
||||||
components_settings={"extensions": [DummyExtension]}
|
|
||||||
)
|
|
||||||
def test_registry_lifecycle_hooks(self):
|
def test_registry_lifecycle_hooks(self):
|
||||||
extension = cast(DummyExtension, app_settings.EXTENSIONS[1])
|
extension = cast(DummyExtension, app_settings.EXTENSIONS[1])
|
||||||
|
|
||||||
|
@ -162,9 +191,7 @@ class TestExtension:
|
||||||
assert len(extension.calls["on_registry_deleted"]) == 1
|
assert len(extension.calls["on_registry_deleted"]) == 1
|
||||||
assert extension.calls["on_registry_deleted"][0] == reg_id
|
assert extension.calls["on_registry_deleted"][0] == reg_id
|
||||||
|
|
||||||
@djc_test(
|
@djc_test(components_settings={"extensions": [DummyExtension]})
|
||||||
components_settings={"extensions": [DummyExtension]}
|
|
||||||
)
|
|
||||||
def test_component_registration_hooks(self):
|
def test_component_registration_hooks(self):
|
||||||
class TestComponent(Component):
|
class TestComponent(Component):
|
||||||
template = "Hello {{ name }}!"
|
template = "Hello {{ name }}!"
|
||||||
|
@ -191,9 +218,7 @@ class TestExtension:
|
||||||
assert unreg_call.name == "test_comp"
|
assert unreg_call.name == "test_comp"
|
||||||
assert unreg_call.component_cls == TestComponent
|
assert unreg_call.component_cls == TestComponent
|
||||||
|
|
||||||
@djc_test(
|
@djc_test(components_settings={"extensions": [DummyExtension]})
|
||||||
components_settings={"extensions": [DummyExtension]}
|
|
||||||
)
|
|
||||||
def test_component_render_hooks(self):
|
def test_component_render_hooks(self):
|
||||||
@register("test_comp")
|
@register("test_comp")
|
||||||
class TestComponent(Component):
|
class TestComponent(Component):
|
||||||
|
@ -211,9 +236,7 @@ class TestExtension:
|
||||||
# Render the component with some args and kwargs
|
# Render the component with some args and kwargs
|
||||||
test_context = Context({"foo": "bar"})
|
test_context = Context({"foo": "bar"})
|
||||||
test_slots = {"content": "Some content"}
|
test_slots = {"content": "Some content"}
|
||||||
TestComponent.render(
|
TestComponent.render(context=test_context, args=("arg1", "arg2"), kwargs={"name": "Test"}, slots=test_slots)
|
||||||
context=test_context, args=("arg1", "arg2"), kwargs={"name": "Test"}, slots=test_slots
|
|
||||||
)
|
|
||||||
|
|
||||||
extension = cast(DummyExtension, app_settings.EXTENSIONS[1])
|
extension = cast(DummyExtension, app_settings.EXTENSIONS[1])
|
||||||
|
|
||||||
|
@ -236,3 +259,34 @@ class TestExtension:
|
||||||
assert data_call.context_data == {"name": "Test"}
|
assert data_call.context_data == {"name": "Test"}
|
||||||
assert data_call.js_data == {"script": "console.log('Hello!')"}
|
assert data_call.js_data == {"script": "console.log('Hello!')"}
|
||||||
assert data_call.css_data == {"style": "body { color: blue; }"}
|
assert data_call.css_data == {"style": "body { color: blue; }"}
|
||||||
|
|
||||||
|
|
||||||
|
@djc_test
|
||||||
|
class TestExtensionViews:
|
||||||
|
@djc_test(components_settings={"extensions": [DummyExtension]})
|
||||||
|
def test_views(self):
|
||||||
|
client = Client()
|
||||||
|
|
||||||
|
# Check basic view
|
||||||
|
response = client.get("/components/ext/test_extension/dummy-view/")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.content == b"Hello, world!"
|
||||||
|
|
||||||
|
# Check that URL parameters are passed to the view
|
||||||
|
response2 = client.get("/components/ext/test_extension/dummy-view-2/123/John/")
|
||||||
|
assert response2.status_code == 200
|
||||||
|
assert response2.content == b"Hello, world! 123 John"
|
||||||
|
|
||||||
|
@djc_test(components_settings={"extensions": [DummyNestedExtension]})
|
||||||
|
def test_nested_views(self):
|
||||||
|
client = Client()
|
||||||
|
|
||||||
|
# Check basic view
|
||||||
|
# NOTE: Since the parent route contains child routes, the parent route should not be matched
|
||||||
|
response = client.get("/components/ext/test_nested_extension/nested-view/")
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
# Check that URL parameters are passed to the view
|
||||||
|
response2 = client.get("/components/ext/test_nested_extension/nested-view/123/John/")
|
||||||
|
assert response2.status_code == 200
|
||||||
|
assert response2.content == b"Hello, world! 123 John"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue