mirror of
https://github.com/django-components/django-components.git
synced 2025-08-04 14:28:18 +00:00
refactor: move Url.public to View.public (#1140)
* refactor: move Url.public to View.public * refactor: fix tests / imports
This commit is contained in:
parent
b49002b545
commit
519529d4e4
17 changed files with 291 additions and 336 deletions
29
CHANGELOG.md
29
CHANGELOG.md
|
@ -95,6 +95,35 @@
|
|||
) -> HttpResponse:
|
||||
```
|
||||
|
||||
- The `Component.Url` class was merged with `Component.View`.
|
||||
|
||||
Instead of `Component.Url.public`, use `Component.View.public`.
|
||||
|
||||
If you imported `ComponentUrl` from `django_components`, you need to update your import to `ComponentView`.
|
||||
|
||||
Before:
|
||||
|
||||
```py
|
||||
class MyComponent(Component):
|
||||
class Url:
|
||||
public = True
|
||||
|
||||
class View:
|
||||
def get(self, request):
|
||||
return self.render_to_response()
|
||||
```
|
||||
|
||||
After:
|
||||
|
||||
```py
|
||||
class MyComponent(Component):
|
||||
class View:
|
||||
public = True
|
||||
|
||||
def get(self, request):
|
||||
return self.render_to_response()
|
||||
```
|
||||
|
||||
#### 🚨📢 Deprecation
|
||||
|
||||
- `get_context_data()` is now deprecated. Use `get_template_data()` instead.
|
||||
|
|
|
@ -325,7 +325,7 @@ Read more about [HTML attributes](https://django-components.github.io/django-com
|
|||
|
||||
- Components can be [exposed as Django Views](https://django-components.github.io/django-components/latest/concepts/fundamentals/component_views_urls/) with `get()`, `post()`, `put()`, `patch()`, `delete()` methods
|
||||
|
||||
- Automatically create an endpoint for a component with [`Component.Url.public`](https://django-components.github.io/django-components/latest/concepts/fundamentals/component_views_urls/#register-urls-automatically)
|
||||
- Automatically create an endpoint for a component with [`Component.View.public`](https://django-components.github.io/django-components/latest/concepts/fundamentals/component_views_urls/#register-urls-automatically)
|
||||
|
||||
```py
|
||||
# components/calendar/calendar.py
|
||||
|
@ -333,12 +333,11 @@ Read more about [HTML attributes](https://django-components.github.io/django-com
|
|||
class Calendar(Component):
|
||||
template_file = "calendar.html"
|
||||
|
||||
# Register Component with `urlpatterns`
|
||||
class Url:
|
||||
class View:
|
||||
# Register Component with `urlpatterns`
|
||||
public = True
|
||||
|
||||
# Define handlers
|
||||
class View:
|
||||
# Define handlers
|
||||
def get(self, request, *args, **kwargs):
|
||||
page = request.GET.get("page", 1)
|
||||
return self.component.render_to_response(
|
||||
|
|
|
@ -13,7 +13,7 @@ django-components has a suite of features that help you write and manage views a
|
|||
|
||||
- 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.1/topics/http/urls/). This works the same way as [`View.as_view()`](https://docs.djangoproject.com/en/5.1/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.Url.public = True`](../../../reference/api#django_components.ComponentUrl.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).
|
||||
|
||||
- In addition, [`Component`](../../../reference/api#django_components.Component) has a [`render_to_response()`](../../../reference/api#django_components.Component.render_to_response) method that renders the component template based on the provided input and returns an `HttpResponse` object.
|
||||
|
||||
|
@ -126,14 +126,13 @@ instance as one of the arguments.
|
|||
|
||||
## Register URLs automatically
|
||||
|
||||
If you don't care about the exact URL of the component, you can let django-components manage the URLs for you by setting the [`Component.Url.public`](../../../reference/api#django_components.ComponentUrl.public) attribute to `True`:
|
||||
If you don't care about the exact URL of the component, you can let django-components manage the URLs for you by setting the [`Component.View.public`](../../../reference/api#django_components.ComponentView.public) attribute to `True`:
|
||||
|
||||
```py
|
||||
class MyComponent(Component):
|
||||
class Url:
|
||||
class View:
|
||||
public = True
|
||||
|
||||
class View:
|
||||
def get(self, request):
|
||||
return self.component.render_to_response(request=request)
|
||||
...
|
||||
|
|
|
@ -47,7 +47,7 @@ Component.render(
|
|||
context: Mapping | django.template.Context | None = None,
|
||||
args: List[Any] | None = None,
|
||||
kwargs: Dict[str, Any] | None = None,
|
||||
slots: Dict[str, str | SafeString | SlotContent] | None = None,
|
||||
slots: Dict[str, str | SafeString | SlotInput] | None = None,
|
||||
escape_slots_content: bool = True
|
||||
) -> str:
|
||||
```
|
||||
|
|
|
@ -257,7 +257,7 @@ Next, you need to set the URL for the component.
|
|||
|
||||
You can either:
|
||||
|
||||
1. Automatically assign the URL by setting the [`Component.Url.public`](../../reference/api#django_components.ComponentUrl.public) attribute to `True`.
|
||||
1. Automatically assign the URL by setting the [`Component.View.public`](../../reference/api#django_components.ComponentView.public) attribute to `True`.
|
||||
|
||||
In this case, use [`get_component_url()`](../../reference/api#django_components.get_component_url) to get the URL for the component view.
|
||||
|
||||
|
@ -265,7 +265,7 @@ You can either:
|
|||
from django_components import Component, get_component_url
|
||||
|
||||
class Calendar(Component):
|
||||
class Url:
|
||||
class View:
|
||||
public = True
|
||||
|
||||
url = get_component_url(Calendar)
|
||||
|
|
|
@ -315,7 +315,7 @@ Read more about [HTML attributes](https://django-components.github.io/django-com
|
|||
|
||||
- Components can be [exposed as Django Views](https://django-components.github.io/django-components/latest/concepts/fundamentals/component_views_urls/) with `get()`, `post()`, `put()`, `patch()`, `delete()` methods
|
||||
|
||||
- Automatically create an endpoint for a component with [`Component.Url.public`](https://django-components.github.io/django-components/latest/concepts/fundamentals/component_views_urls/#register-urls-automatically)
|
||||
- Automatically create an endpoint for a component with [`Component.View.public`](https://django-components.github.io/django-components/latest/concepts/fundamentals/component_views_urls/#register-urls-automatically)
|
||||
|
||||
```py
|
||||
# components/calendar/calendar.py
|
||||
|
@ -323,12 +323,11 @@ Read more about [HTML attributes](https://django-components.github.io/django-com
|
|||
class Calendar(Component):
|
||||
template_file = "calendar.html"
|
||||
|
||||
# Register Component with `urlpatterns`
|
||||
class Url:
|
||||
class View:
|
||||
# Register Component with `urlpatterns`
|
||||
public = True
|
||||
|
||||
# Define handlers
|
||||
class View:
|
||||
# Define handlers
|
||||
def get(self, request, *args, **kwargs):
|
||||
page = request.GET.get("page", 1)
|
||||
return self.component.render_to_response(
|
||||
|
|
|
@ -47,10 +47,6 @@
|
|||
options:
|
||||
show_if_no_docstring: true
|
||||
|
||||
::: django_components.ComponentUrl
|
||||
options:
|
||||
show_if_no_docstring: true
|
||||
|
||||
::: django_components.ComponentVars
|
||||
options:
|
||||
show_if_no_docstring: true
|
||||
|
|
|
@ -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#L2622" target="_blank">See source code</a>
|
||||
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L2619" target="_blank">See source code</a>
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -48,8 +48,7 @@ from django_components.extension import (
|
|||
)
|
||||
from django_components.extensions.cache import ComponentCache
|
||||
from django_components.extensions.defaults import ComponentDefaults, Default
|
||||
from django_components.extensions.view import ComponentView
|
||||
from django_components.extensions.url import ComponentUrl, get_component_url
|
||||
from django_components.extensions.view import ComponentView, get_component_url
|
||||
from django_components.library import TagProtectedError
|
||||
from django_components.node import BaseNode, template_tag
|
||||
from django_components.slots import Slot, SlotContent, SlotFunc, SlotInput, SlotRef, SlotResult
|
||||
|
@ -96,7 +95,6 @@ __all__ = [
|
|||
"ComponentRegistry",
|
||||
"ComponentVars",
|
||||
"ComponentView",
|
||||
"ComponentUrl",
|
||||
"ComponentsSettings",
|
||||
"component_formatter",
|
||||
"component_shorthand_formatter",
|
||||
|
|
|
@ -752,10 +752,9 @@ class InternalSettings:
|
|||
# Prepend built-in extensions
|
||||
from django_components.extensions.cache import CacheExtension
|
||||
from django_components.extensions.defaults import DefaultsExtension
|
||||
from django_components.extensions.url import UrlExtension
|
||||
from django_components.extensions.view import ViewExtension
|
||||
|
||||
extensions = [CacheExtension, DefaultsExtension, ViewExtension, UrlExtension] + list(extensions)
|
||||
extensions = [CacheExtension, DefaultsExtension, ViewExtension] + list(extensions)
|
||||
|
||||
# Extensions may be passed in either as classes or import strings.
|
||||
extension_instances: List["ComponentExtension"] = []
|
||||
|
|
|
@ -61,7 +61,6 @@ from django_components.extension import (
|
|||
)
|
||||
from django_components.extensions.cache import ComponentCache
|
||||
from django_components.extensions.defaults import ComponentDefaults
|
||||
from django_components.extensions.url import ComponentUrl
|
||||
from django_components.extensions.view import ComponentView, ViewFn
|
||||
from django_components.node import BaseNode
|
||||
from django_components.perfutil.component import ComponentRenderer, component_context_cache, component_post_render
|
||||
|
@ -1538,8 +1537,6 @@ class Component(metaclass=ComponentMeta):
|
|||
"""
|
||||
Instance of [`ComponentView`](../api#django_components.ComponentView) available at component render time.
|
||||
"""
|
||||
Url: Type[ComponentUrl]
|
||||
url: ComponentUrl
|
||||
|
||||
# #####################################
|
||||
# MISC
|
||||
|
|
|
@ -1,181 +0,0 @@
|
|||
import sys
|
||||
from typing import TYPE_CHECKING, Optional, Type, Union
|
||||
from weakref import WeakKeyDictionary
|
||||
|
||||
import django.urls
|
||||
|
||||
from django_components.extension import (
|
||||
ComponentExtension,
|
||||
OnComponentClassCreatedContext,
|
||||
OnComponentClassDeletedContext,
|
||||
URLRoute,
|
||||
extensions,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from django_components.component import Component
|
||||
|
||||
|
||||
# NOTE: `WeakKeyDictionary` is NOT a generic pre-3.9
|
||||
if sys.version_info >= (3, 9):
|
||||
ComponentRouteCache = WeakKeyDictionary[Type["Component"], URLRoute]
|
||||
else:
|
||||
ComponentRouteCache = WeakKeyDictionary
|
||||
|
||||
|
||||
def _get_component_route_name(component: Union[Type["Component"], "Component"]) -> str:
|
||||
return f"__component_url__{component.class_id}"
|
||||
|
||||
|
||||
def get_component_url(component: Union[Type["Component"], "Component"]) -> str:
|
||||
"""
|
||||
Get the URL for a [`Component`](../api#django_components.Component).
|
||||
|
||||
Raises `RuntimeError` if the component is not public.
|
||||
|
||||
Read more about [Component views and URLs](../../concepts/fundamentals/component_views_urls).
|
||||
|
||||
**Example:**
|
||||
|
||||
```py
|
||||
from django_components import Component, get_component_url
|
||||
|
||||
class MyComponent(Component):
|
||||
class Url:
|
||||
public = True
|
||||
|
||||
# Get the URL for the component
|
||||
url = get_component_url(MyComponent)
|
||||
```
|
||||
"""
|
||||
url_cls: Optional[Type[ComponentUrl]] = getattr(component, "Url", None)
|
||||
if url_cls is None or not url_cls.public:
|
||||
raise RuntimeError("Component URL is not available - Component is not public")
|
||||
|
||||
route_name = _get_component_route_name(component)
|
||||
return django.urls.reverse(route_name)
|
||||
|
||||
|
||||
class ComponentUrl(ComponentExtension.ExtensionClass): # type: ignore
|
||||
"""
|
||||
The interface for `Component.Url`.
|
||||
|
||||
This class is used to configure whether the component should be available via a URL.
|
||||
|
||||
Read more about [Component views and URLs](../../concepts/fundamentals/component_views_urls).
|
||||
|
||||
**Example:**
|
||||
```python
|
||||
from django_components import Component
|
||||
|
||||
class MyComponent(Component):
|
||||
class Url:
|
||||
public = True
|
||||
|
||||
# Get the URL for the component
|
||||
url = get_component_url(MyComponent)
|
||||
```
|
||||
"""
|
||||
|
||||
public: bool = False
|
||||
"""
|
||||
Whether this [`Component`](../api#django_components.Component) should be available
|
||||
via a URL. Defaults to `False`.
|
||||
|
||||
If `True`, the Component will have its own unique URL path.
|
||||
|
||||
You can use this to write components that will correspond to HTML fragments
|
||||
for HTMX or similar libraries.
|
||||
|
||||
To obtain the component URL, either access the url from
|
||||
[`Component.Url.url`](../api#django_components.ComponentUrl.url) or
|
||||
use the [`get_component_url()`](../api#django_components.get_component_url) function.
|
||||
|
||||
**Example:**
|
||||
|
||||
```py
|
||||
from django_components import Component, get_component_url
|
||||
|
||||
class MyComponent(Component):
|
||||
class Url:
|
||||
public = True
|
||||
|
||||
# Get the URL for the component
|
||||
url = get_component_url(MyComponent)
|
||||
```
|
||||
"""
|
||||
|
||||
@property
|
||||
def url(self) -> str:
|
||||
"""
|
||||
The URL for the component.
|
||||
|
||||
Raises `RuntimeError` if the component is not public.
|
||||
"""
|
||||
return get_component_url(self.component.__class__)
|
||||
|
||||
|
||||
class UrlExtension(ComponentExtension):
|
||||
"""
|
||||
This extension adds a nested `Url` class to each [`Component`](../api#django_components.Component).
|
||||
|
||||
This nested `Url` class configures whether the component should be available via a URL.
|
||||
|
||||
Read more about [Component views and URLs](../../concepts/fundamentals/component_views_urls).
|
||||
|
||||
**Example:**
|
||||
|
||||
```py
|
||||
from django_components import Component
|
||||
|
||||
class MyComponent(Component):
|
||||
class Url:
|
||||
public = True
|
||||
```
|
||||
|
||||
Will create a URL route like `/components/ext/url/components/a1b2c3/`.
|
||||
|
||||
To get the URL for the component, use `get_component_url`:
|
||||
|
||||
```py
|
||||
url = get_component_url(MyComponent)
|
||||
```
|
||||
|
||||
This extension is automatically added to all [`Component`](../api#django_components.Component)
|
||||
classes.
|
||||
"""
|
||||
|
||||
name = "url"
|
||||
|
||||
ExtensionClass = ComponentUrl
|
||||
|
||||
def __init__(self) -> None:
|
||||
# Remember which route belongs to which component
|
||||
self.routes_by_component: ComponentRouteCache = WeakKeyDictionary()
|
||||
|
||||
# Create URL route on creation
|
||||
def on_component_class_created(self, ctx: OnComponentClassCreatedContext) -> None:
|
||||
url_cls: Optional[Type[ComponentUrl]] = getattr(ctx.component_cls, "Url", None)
|
||||
if url_cls is None or not url_cls.public:
|
||||
return
|
||||
|
||||
# Create a URL route like `components/MyTable_a1b2c3/`
|
||||
# And since this is within the `url` extension, the full URL path will then be:
|
||||
# `/components/ext/url/components/MyTable_a1b2c3/`
|
||||
route_path = f"components/{ctx.component_cls.class_id}/"
|
||||
route_name = _get_component_route_name(ctx.component_cls)
|
||||
route = URLRoute(
|
||||
path=route_path,
|
||||
handler=ctx.component_cls.as_view(),
|
||||
name=route_name,
|
||||
)
|
||||
|
||||
self.routes_by_component[ctx.component_cls] = route
|
||||
extensions.add_extension_urls(self.name, [route])
|
||||
|
||||
# Remove URL route on deletion
|
||||
def on_component_class_deleted(self, ctx: OnComponentClassDeletedContext) -> None:
|
||||
route = self.routes_by_component.pop(ctx.component_cls, None)
|
||||
if route is None:
|
||||
return
|
||||
extensions.remove_extension_urls(self.name, [route])
|
|
@ -1,18 +1,66 @@
|
|||
from typing import TYPE_CHECKING, Any, Protocol, cast
|
||||
import sys
|
||||
from typing import TYPE_CHECKING, Any, Optional, Protocol, Type, Union, cast
|
||||
from weakref import WeakKeyDictionary
|
||||
|
||||
import django.urls
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.views.generic import View
|
||||
|
||||
from django_components.extension import ComponentExtension
|
||||
from django_components.extension import (
|
||||
ComponentExtension,
|
||||
OnComponentClassCreatedContext,
|
||||
OnComponentClassDeletedContext,
|
||||
URLRoute,
|
||||
extensions,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from django_components.component import Component
|
||||
|
||||
# NOTE: `WeakKeyDictionary` is NOT a generic pre-3.9
|
||||
if sys.version_info >= (3, 9):
|
||||
ComponentRouteCache = WeakKeyDictionary[Type["Component"], URLRoute]
|
||||
else:
|
||||
ComponentRouteCache = WeakKeyDictionary
|
||||
|
||||
|
||||
class ViewFn(Protocol):
|
||||
def __call__(self, request: HttpRequest, *args: Any, **kwargs: Any) -> Any: ... # noqa: E704
|
||||
|
||||
|
||||
def _get_component_route_name(component: Union[Type["Component"], "Component"]) -> str:
|
||||
return f"__component_url__{component.class_id}"
|
||||
|
||||
|
||||
def get_component_url(component: Union[Type["Component"], "Component"]) -> str:
|
||||
"""
|
||||
Get the URL for a [`Component`](../api#django_components.Component).
|
||||
|
||||
Raises `RuntimeError` if the component is not public.
|
||||
|
||||
Read more about [Component views and URLs](../../concepts/fundamentals/component_views_urls).
|
||||
|
||||
**Example:**
|
||||
|
||||
```py
|
||||
from django_components import Component, get_component_url
|
||||
|
||||
class MyComponent(Component):
|
||||
class View:
|
||||
public = True
|
||||
|
||||
# Get the URL for the component
|
||||
url = get_component_url(MyComponent)
|
||||
```
|
||||
"""
|
||||
view_cls: Optional[Type[ComponentView]] = getattr(component, "View", None)
|
||||
if view_cls is None or not view_cls.public:
|
||||
raise RuntimeError("Component URL is not available - Component is not public")
|
||||
|
||||
route_name = _get_component_route_name(component)
|
||||
return django.urls.reverse(route_name)
|
||||
|
||||
|
||||
class ComponentView(ComponentExtension.ExtensionClass, View): # type: ignore
|
||||
"""
|
||||
The interface for `Component.View`.
|
||||
|
@ -36,9 +84,33 @@ class ComponentView(ComponentExtension.ExtensionClass, View): # type: ignore
|
|||
def get(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
||||
return HttpResponse("Hello, world!")
|
||||
```
|
||||
|
||||
**Component URL:**
|
||||
|
||||
If the `public` attribute is set to `True`, the component will have its own URL
|
||||
that will point to the Component's View.
|
||||
|
||||
```py
|
||||
from django_components import Component
|
||||
|
||||
class MyComponent(Component):
|
||||
class View:
|
||||
public = True
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
return HttpResponse("Hello, world!")
|
||||
```
|
||||
|
||||
Will create a URL route like `/components/ext/view/components/a1b2c3/`.
|
||||
|
||||
To get the URL for the component, use `get_component_url`:
|
||||
|
||||
```py
|
||||
url = get_component_url(MyComponent)
|
||||
```
|
||||
"""
|
||||
|
||||
# NOTE: This attribute must be declared on the class for `View.as_view()` to allow
|
||||
# NOTE: This class attribute must be declared on the class for `View.as_view()` to allow
|
||||
# us to pass `component` kwarg.
|
||||
component = cast("Component", None)
|
||||
"""
|
||||
|
@ -54,10 +126,42 @@ class ComponentView(ComponentExtension.ExtensionClass, View): # type: ignore
|
|||
```
|
||||
"""
|
||||
|
||||
public = False
|
||||
"""
|
||||
Whether the component should be available via a URL.
|
||||
|
||||
**Example:**
|
||||
|
||||
```py
|
||||
from django_components import Component
|
||||
|
||||
class MyComponent(Component):
|
||||
class View:
|
||||
public = True
|
||||
```
|
||||
|
||||
Will create a URL route like `/components/ext/view/components/a1b2c3/`.
|
||||
|
||||
To get the URL for the component, use `get_component_url`:
|
||||
|
||||
```py
|
||||
url = get_component_url(MyComponent)
|
||||
```
|
||||
"""
|
||||
|
||||
def __init__(self, component: "Component", **kwargs: Any) -> None:
|
||||
ComponentExtension.ExtensionClass.__init__(self, component)
|
||||
View.__init__(self, **kwargs)
|
||||
|
||||
@property
|
||||
def url(self) -> str:
|
||||
"""
|
||||
The URL for the component.
|
||||
|
||||
Raises `RuntimeError` if the component is not public.
|
||||
"""
|
||||
return get_component_url(self.component.__class__)
|
||||
|
||||
# NOTE: The methods below are defined to satisfy the `View` class. All supported methods
|
||||
# are defined in `View.http_method_names`.
|
||||
#
|
||||
|
@ -101,9 +205,44 @@ class ViewExtension(ComponentExtension):
|
|||
This nested class is a subclass of `django.views.View`, and allows the component
|
||||
to be used as a view by calling `ComponentView.as_view()`.
|
||||
|
||||
This extension also allows the component to be available via a unique URL.
|
||||
|
||||
This extension is automatically added to all components.
|
||||
"""
|
||||
|
||||
name = "view"
|
||||
|
||||
ExtensionClass = ComponentView
|
||||
|
||||
def __init__(self) -> None:
|
||||
# Remember which route belongs to which component
|
||||
self.routes_by_component: ComponentRouteCache = WeakKeyDictionary()
|
||||
|
||||
# Create URL route on creation
|
||||
def on_component_class_created(self, ctx: OnComponentClassCreatedContext) -> None:
|
||||
comp_cls: Type["Component"] = ctx.component_cls
|
||||
view_cls: Optional[Type[ComponentView]] = getattr(comp_cls, "View", None)
|
||||
if view_cls is None or not view_cls.public:
|
||||
return
|
||||
|
||||
# Create a URL route like `components/MyTable_a1b2c3/`
|
||||
# And since this is within the `view` extension, the full URL path will then be:
|
||||
# `/components/ext/view/components/MyTable_a1b2c3/`
|
||||
route_path = f"components/{comp_cls.class_id}/"
|
||||
route_name = _get_component_route_name(comp_cls)
|
||||
route = URLRoute(
|
||||
path=route_path,
|
||||
handler=comp_cls.as_view(),
|
||||
name=route_name,
|
||||
)
|
||||
|
||||
self.routes_by_component[comp_cls] = route
|
||||
extensions.add_extension_urls(self.name, [route])
|
||||
|
||||
# Remove URL route on deletion
|
||||
def on_component_class_deleted(self, ctx: OnComponentClassDeletedContext) -> None:
|
||||
comp_cls: Type["Component"] = ctx.component_cls
|
||||
route = self.routes_by_component.pop(comp_cls, None)
|
||||
if route is None:
|
||||
return
|
||||
extensions.remove_extension_urls(self.name, [route])
|
||||
|
|
|
@ -97,7 +97,7 @@ class TestExtensionsListCommand:
|
|||
call_command("components", "ext", "list")
|
||||
output = out.getvalue()
|
||||
|
||||
assert output.strip() == "name \n========\ncache \ndefaults\nview \nurl"
|
||||
assert output.strip() == "name \n========\ncache \ndefaults\nview"
|
||||
|
||||
@djc_test(
|
||||
components_settings={"extensions": [EmptyExtension, DummyExtension]},
|
||||
|
@ -108,7 +108,7 @@ class TestExtensionsListCommand:
|
|||
call_command("components", "ext", "list")
|
||||
output = out.getvalue()
|
||||
|
||||
assert output.strip() == "name \n========\ncache \ndefaults\nview \nurl \nempty \ndummy"
|
||||
assert output.strip() == "name \n========\ncache \ndefaults\nview \nempty \ndummy"
|
||||
|
||||
@djc_test(
|
||||
components_settings={"extensions": [EmptyExtension, DummyExtension]},
|
||||
|
@ -119,7 +119,7 @@ class TestExtensionsListCommand:
|
|||
call_command("components", "ext", "list", "--all")
|
||||
output = out.getvalue()
|
||||
|
||||
assert output.strip() == "name \n========\ncache \ndefaults\nview \nurl \nempty \ndummy"
|
||||
assert output.strip() == "name \n========\ncache \ndefaults\nview \nempty \ndummy"
|
||||
|
||||
@djc_test(
|
||||
components_settings={"extensions": [EmptyExtension, DummyExtension]},
|
||||
|
@ -130,7 +130,7 @@ class TestExtensionsListCommand:
|
|||
call_command("components", "ext", "list", "--columns", "name")
|
||||
output = out.getvalue()
|
||||
|
||||
assert output.strip() == "name \n========\ncache \ndefaults\nview \nurl \nempty \ndummy"
|
||||
assert output.strip() == "name \n========\ncache \ndefaults\nview \nempty \ndummy"
|
||||
|
||||
@djc_test(
|
||||
components_settings={"extensions": [EmptyExtension, DummyExtension]},
|
||||
|
@ -141,7 +141,7 @@ class TestExtensionsListCommand:
|
|||
call_command("components", "ext", "list", "--simple")
|
||||
output = out.getvalue()
|
||||
|
||||
assert output.strip() == "cache \ndefaults\nview \nurl \nempty \ndummy"
|
||||
assert output.strip() == "cache \ndefaults\nview \nempty \ndummy"
|
||||
|
||||
|
||||
@djc_test
|
||||
|
@ -159,7 +159,7 @@ class TestExtensionsRunCommand:
|
|||
output
|
||||
== dedent(
|
||||
f"""
|
||||
usage: components ext run [-h] {{cache,defaults,view,url,empty,dummy}} ...
|
||||
usage: components ext run [-h] {{cache,defaults,view,empty,dummy}} ...
|
||||
|
||||
Run a command added by an extension.
|
||||
|
||||
|
@ -167,11 +167,10 @@ class TestExtensionsRunCommand:
|
|||
-h, --help show this help message and exit
|
||||
|
||||
subcommands:
|
||||
{{cache,defaults,view,url,empty,dummy}}
|
||||
{{cache,defaults,view,empty,dummy}}
|
||||
cache Run commands added by the 'cache' extension.
|
||||
defaults Run commands added by the 'defaults' extension.
|
||||
view Run commands added by the 'view' extension.
|
||||
url Run commands added by the 'url' extension.
|
||||
empty Run commands added by the 'empty' extension.
|
||||
dummy Run commands added by the 'dummy' extension.
|
||||
"""
|
||||
|
|
|
@ -1,104 +0,0 @@
|
|||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from django.http import HttpRequest
|
||||
from django.test import Client
|
||||
|
||||
from django_components import Component, get_component_url
|
||||
from django_components.testing import djc_test
|
||||
|
||||
from .testutils import setup_test_config
|
||||
|
||||
# DO NOT REMOVE!
|
||||
#
|
||||
# This is intentionally defined before `setup_test_config()` in order to test that
|
||||
# the URL extension works even before the Django has been set up.
|
||||
#
|
||||
# Because if we define the component before `django.setup()`, then we store it in
|
||||
# event queue, and will register it when `AppConfig.ready()` is finally called.
|
||||
#
|
||||
# This test relies on the "url" extension calling `add_extension_urls()` from within
|
||||
# the `on_component_class_created()` hook.
|
||||
class ComponentBeforeReady(Component):
|
||||
class Url:
|
||||
public = True
|
||||
|
||||
template = "Hello"
|
||||
|
||||
|
||||
setup_test_config({"autodiscover": False})
|
||||
|
||||
|
||||
@djc_test
|
||||
class TestComponentUrl:
|
||||
def test_public_url(self):
|
||||
did_call_get = False
|
||||
did_call_post = False
|
||||
|
||||
class TestComponent(Component):
|
||||
template = "Hello"
|
||||
|
||||
class Url:
|
||||
public = True
|
||||
|
||||
class View:
|
||||
def get(self, request: HttpRequest, **kwargs: Any):
|
||||
nonlocal did_call_get
|
||||
did_call_get = True
|
||||
|
||||
component: Component = self.component # type: ignore[attr-defined]
|
||||
return component.render_to_response()
|
||||
|
||||
def post(self, request: HttpRequest, **kwargs: Any):
|
||||
nonlocal did_call_post
|
||||
did_call_post = True
|
||||
|
||||
component: Component = self.component # type: ignore[attr-defined]
|
||||
return component.render_to_response()
|
||||
|
||||
# Check if the URL is correctly generated
|
||||
component_url = get_component_url(TestComponent)
|
||||
assert component_url == f"/components/ext/url/components/{TestComponent.class_id}/"
|
||||
|
||||
client = Client()
|
||||
response = client.get(component_url)
|
||||
assert response.status_code == 200
|
||||
assert response.content == b"Hello"
|
||||
assert did_call_get
|
||||
|
||||
response = client.post(component_url)
|
||||
assert response.status_code == 200
|
||||
assert response.content == b"Hello"
|
||||
assert did_call_get
|
||||
|
||||
def test_non_public_url(self):
|
||||
did_call_get = False
|
||||
|
||||
class TestComponent(Component):
|
||||
template = "Hi"
|
||||
|
||||
class Url:
|
||||
public = False
|
||||
|
||||
class View:
|
||||
def get(self, request: HttpRequest, **attrs: Any):
|
||||
nonlocal did_call_get
|
||||
did_call_get = True
|
||||
|
||||
component: Component = self.component # type: ignore[attr-defined]
|
||||
return component.render_to_response()
|
||||
|
||||
# Attempt to get the URL should raise RuntimeError
|
||||
with pytest.raises(
|
||||
RuntimeError,
|
||||
match="Component URL is not available - Component is not public",
|
||||
):
|
||||
get_component_url(TestComponent)
|
||||
|
||||
# Even calling the URL directly should raise an error
|
||||
component_url = f"/components/ext/url/components/{TestComponent.class_id}/"
|
||||
|
||||
client = Client()
|
||||
response = client.get(component_url)
|
||||
assert response.status_code == 404
|
||||
assert not did_call_get
|
|
@ -1,17 +1,35 @@
|
|||
from typing import Any, Dict
|
||||
|
||||
import pytest
|
||||
from django.conf import settings
|
||||
from django.http import HttpResponse
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.template import Context, Template
|
||||
from django.test import Client, SimpleTestCase
|
||||
from django.urls import path
|
||||
|
||||
from django_components import Component, ComponentView, register, types
|
||||
from django_components import Component, ComponentView, get_component_url, register, types
|
||||
from django_components.urls import urlpatterns as dc_urlpatterns
|
||||
|
||||
from django_components.testing import djc_test
|
||||
from .testutils import setup_test_config
|
||||
|
||||
# DO NOT REMOVE!
|
||||
#
|
||||
# This is intentionally defined before `setup_test_config()` in order to test that
|
||||
# the URL extension works even before the Django has been set up.
|
||||
#
|
||||
# Because if we define the component before `django.setup()`, then we store it in
|
||||
# event queue, and will register it when `AppConfig.ready()` is finally called.
|
||||
#
|
||||
# This test relies on the "url" extension calling `add_extension_urls()` from within
|
||||
# the `on_component_class_created()` hook.
|
||||
class ComponentBeforeReady(Component):
|
||||
class View:
|
||||
public = True
|
||||
|
||||
template = "Hello"
|
||||
|
||||
|
||||
setup_test_config({"autodiscover": False})
|
||||
|
||||
|
||||
|
@ -278,3 +296,73 @@ class TestComponentAsView(SimpleTestCase):
|
|||
b"<script>",
|
||||
response.content,
|
||||
)
|
||||
|
||||
def test_public_url(self):
|
||||
did_call_get = False
|
||||
did_call_post = False
|
||||
|
||||
class TestComponent(Component):
|
||||
template = "Hello"
|
||||
|
||||
class View:
|
||||
public = True
|
||||
|
||||
def get(self, request: HttpRequest, **kwargs: Any):
|
||||
nonlocal did_call_get
|
||||
did_call_get = True
|
||||
|
||||
component: Component = self.component # type: ignore[attr-defined]
|
||||
return component.render_to_response()
|
||||
|
||||
def post(self, request: HttpRequest, **kwargs: Any):
|
||||
nonlocal did_call_post
|
||||
did_call_post = True
|
||||
|
||||
component: Component = self.component # type: ignore[attr-defined]
|
||||
return component.render_to_response()
|
||||
|
||||
# Check if the URL is correctly generated
|
||||
component_url = get_component_url(TestComponent)
|
||||
assert component_url == f"/components/ext/view/components/{TestComponent.class_id}/"
|
||||
|
||||
client = Client()
|
||||
response = client.get(component_url)
|
||||
assert response.status_code == 200
|
||||
assert response.content == b"Hello"
|
||||
assert did_call_get
|
||||
|
||||
response = client.post(component_url)
|
||||
assert response.status_code == 200
|
||||
assert response.content == b"Hello"
|
||||
assert did_call_get
|
||||
|
||||
def test_non_public_url(self):
|
||||
did_call_get = False
|
||||
|
||||
class TestComponent(Component):
|
||||
template = "Hi"
|
||||
|
||||
class View:
|
||||
public = False
|
||||
|
||||
def get(self, request: HttpRequest, **attrs: Any):
|
||||
nonlocal did_call_get
|
||||
did_call_get = True
|
||||
|
||||
component: Component = self.component # type: ignore[attr-defined]
|
||||
return component.render_to_response()
|
||||
|
||||
# Attempt to get the URL should raise RuntimeError
|
||||
with pytest.raises(
|
||||
RuntimeError,
|
||||
match="Component URL is not available - Component is not public",
|
||||
):
|
||||
get_component_url(TestComponent)
|
||||
|
||||
# Even calling the URL directly should raise an error
|
||||
component_url = f"/components/ext/view/components/{TestComponent.class_id}/"
|
||||
|
||||
client = Client()
|
||||
response = client.get(component_url)
|
||||
assert response.status_code == 404
|
||||
assert not did_call_get
|
|
@ -24,7 +24,6 @@ from django_components.extension import (
|
|||
from django_components.extensions.cache import CacheExtension
|
||||
from django_components.extensions.defaults import DefaultsExtension
|
||||
from django_components.extensions.view import ViewExtension
|
||||
from django_components.extensions.url import UrlExtension
|
||||
|
||||
from django_components.testing import djc_test
|
||||
from .testutils import setup_test_config
|
||||
|
@ -133,12 +132,11 @@ def with_registry(on_created: Callable):
|
|||
class TestExtension:
|
||||
@djc_test(components_settings={"extensions": [DummyExtension]})
|
||||
def test_extensions_setting(self):
|
||||
assert len(app_settings.EXTENSIONS) == 5
|
||||
assert len(app_settings.EXTENSIONS) == 4
|
||||
assert isinstance(app_settings.EXTENSIONS[0], CacheExtension)
|
||||
assert isinstance(app_settings.EXTENSIONS[1], DefaultsExtension)
|
||||
assert isinstance(app_settings.EXTENSIONS[2], ViewExtension)
|
||||
assert isinstance(app_settings.EXTENSIONS[3], UrlExtension)
|
||||
assert isinstance(app_settings.EXTENSIONS[4], DummyExtension)
|
||||
assert isinstance(app_settings.EXTENSIONS[3], DummyExtension)
|
||||
|
||||
@djc_test(components_settings={"extensions": [DummyExtension]})
|
||||
def test_access_component_from_extension(self):
|
||||
|
@ -177,7 +175,7 @@ class TestExtension:
|
|||
class TestExtensionHooks:
|
||||
@djc_test(components_settings={"extensions": [DummyExtension]})
|
||||
def test_component_class_lifecycle_hooks(self):
|
||||
extension = cast(DummyExtension, app_settings.EXTENSIONS[4])
|
||||
extension = cast(DummyExtension, app_settings.EXTENSIONS[3])
|
||||
|
||||
assert len(extension.calls["on_component_class_created"]) == 0
|
||||
assert len(extension.calls["on_component_class_deleted"]) == 0
|
||||
|
@ -209,7 +207,7 @@ class TestExtensionHooks:
|
|||
|
||||
@djc_test(components_settings={"extensions": [DummyExtension]})
|
||||
def test_registry_lifecycle_hooks(self):
|
||||
extension = cast(DummyExtension, app_settings.EXTENSIONS[4])
|
||||
extension = cast(DummyExtension, app_settings.EXTENSIONS[3])
|
||||
|
||||
assert len(extension.calls["on_registry_created"]) == 0
|
||||
assert len(extension.calls["on_registry_deleted"]) == 0
|
||||
|
@ -246,7 +244,7 @@ class TestExtensionHooks:
|
|||
return {"name": name}
|
||||
|
||||
registry.register("test_comp", TestComponent)
|
||||
extension = cast(DummyExtension, app_settings.EXTENSIONS[4])
|
||||
extension = cast(DummyExtension, app_settings.EXTENSIONS[3])
|
||||
|
||||
# Verify on_component_registered was called
|
||||
assert len(extension.calls["on_component_registered"]) == 1
|
||||
|
@ -284,7 +282,7 @@ class TestExtensionHooks:
|
|||
test_slots = {"content": "Some content"}
|
||||
TestComponent.render(context=test_context, args=("arg1", "arg2"), kwargs={"name": "Test"}, slots=test_slots)
|
||||
|
||||
extension = cast(DummyExtension, app_settings.EXTENSIONS[4])
|
||||
extension = cast(DummyExtension, app_settings.EXTENSIONS[3])
|
||||
|
||||
# Verify on_component_input was called with correct args
|
||||
assert len(extension.calls["on_component_input"]) == 1
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue