refactor: move Url.public to View.public (#1140)

* refactor: move Url.public to View.public

* refactor: fix tests / imports
This commit is contained in:
Juro Oravec 2025-04-21 23:12:40 +02:00 committed by GitHub
parent b49002b545
commit 519529d4e4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 291 additions and 336 deletions

View file

@ -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])