feat: allow to set query and fragment on get_component_url (#1160)

This commit is contained in:
Juro Oravec 2025-05-03 10:29:38 +02:00 committed by GitHub
parent bf7a204e92
commit c69980493d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 102 additions and 5 deletions

View file

@ -275,6 +275,19 @@
- Ignores placeholders and any `<head>` / `<body>` tags.
- No extra script loaded.
- `get_component_url()` now optionally accepts `query` and `fragment` arguments.
```py
from django_components import get_component_url
url = get_component_url(
MyComponent,
query={"foo": "bar"},
fragment="baz",
)
# /components/ext/view/components/c1ab2c3?foo=bar#baz
```
## v0.139.1
#### Fix

View file

@ -147,3 +147,16 @@ url = get_component_url(MyComponent)
```
This way you don't have to mix your app URLs with component URLs.
!!! info
If you need to pass query parameters or a fragment to the component URL, you can do so by passing the `query` and `fragment` arguments to [`get_component_url()`](../../../reference/api#django_components.get_component_url):
```py
url = get_component_url(
MyComponent,
query={"foo": "bar"},
fragment="baz",
)
# /components/ext/view/components/c1ab2c3?foo=bar#baz
```

View file

@ -1,5 +1,5 @@
import sys
from typing import TYPE_CHECKING, Any, Optional, Protocol, Type, Union, cast
from typing import TYPE_CHECKING, Any, Dict, Optional, Protocol, Type, Union, cast
from weakref import WeakKeyDictionary
import django.urls
@ -13,6 +13,7 @@ from django_components.extension import (
URLRoute,
extensions,
)
from django_components.util.misc import format_url
if TYPE_CHECKING:
from django_components.component import Component
@ -32,7 +33,11 @@ def _get_component_route_name(component: Union[Type["Component"], "Component"])
return f"__component_url__{component.class_id}"
def get_component_url(component: Union[Type["Component"], "Component"]) -> str:
def get_component_url(
component: Union[Type["Component"], "Component"],
query: Optional[Dict] = None,
fragment: Optional[str] = None,
) -> str:
"""
Get the URL for a [`Component`](../api#django_components.Component).
@ -40,6 +45,8 @@ def get_component_url(component: Union[Type["Component"], "Component"]) -> str:
Read more about [Component views and URLs](../../concepts/fundamentals/component_views_urls).
`get_component_url()` optionally accepts `query` and `fragment` arguments.
**Example:**
```py
@ -50,7 +57,12 @@ def get_component_url(component: Union[Type["Component"], "Component"]) -> str:
public = True
# Get the URL for the component
url = get_component_url(MyComponent)
url = get_component_url(
MyComponent,
query={"foo": "bar"},
fragment="baz",
)
# /components/ext/view/components/c1ab2c3?foo=bar#baz
```
"""
view_cls: Optional[Type[ComponentView]] = getattr(component, "View", None)
@ -58,7 +70,8 @@ def get_component_url(component: Union[Type["Component"], "Component"]) -> str:
raise RuntimeError("Component URL is not available - Component is not public")
route_name = _get_component_route_name(component)
return django.urls.reverse(route_name)
url = django.urls.reverse(route_name)
return format_url(url, query=query, fragment=fragment)
class ComponentView(ComponentExtension.ExtensionClass, View): # type: ignore

View file

@ -5,7 +5,8 @@ from hashlib import md5
from importlib import import_module
from itertools import chain
from types import ModuleType
from typing import TYPE_CHECKING, Any, Callable, Iterable, List, Optional, Tuple, Type, TypeVar, Union
from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, List, Optional, Tuple, Type, TypeVar, Union
from urllib import parse
from django_components.constants import UID_LENGTH
from django_components.util.nanoid import generate
@ -147,3 +148,32 @@ def to_dict(data: Any) -> dict:
return asdict(data) # type: ignore[arg-type]
return dict(data)
def format_url(url: str, query: Optional[Dict] = None, fragment: Optional[str] = None) -> str:
"""
Given a URL, add to it query parameters and a fragment, returning an updated URL.
```py
url = format_url(url="https://example.com", query={"foo": "bar"}, fragment="baz")
# https://example.com?foo=bar#baz
```
`query` and `fragment` are optional, and not applied if `None`.
"""
url_parts = parse.urlsplit(url)
escaped_fragment = parse.quote(str(fragment)) if fragment is not None else url_parts.fragment
# NOTE: parse_qsl returns a list of tuples. For simpicity we support only one
# value per key, so we need to unpack the first element.
query_from_url = {key: val[0] for key, val in parse.parse_qsl(url_parts.query)}
all_params = {**query_from_url, **(query or {})}
encoded_params = parse.urlencode(all_params)
updated_parts = url_parts._replace(
query=encoded_params,
fragment=escaped_fragment,
)
new_url = parse.urlunsplit(updated_parts)
return new_url

View file

@ -373,6 +373,16 @@ class TestComponent:
with pytest.raises(KeyError):
get_component_by_class_id("nonexistent")
def test_component_render_id(self):
class SimpleComponent(Component):
template = "render_id: {{ render_id }}"
def get_template_data(self, args, kwargs, slots, context):
return {"render_id": self.id}
rendered = SimpleComponent.render()
assert rendered == "render_id: ca1bc3e"
def test_get_context_data_returns_none(self):
class SimpleComponent(Component):
template = "Hello"

View file

@ -297,6 +297,24 @@ class TestComponentAsView(SimpleTestCase):
response.content,
)
def test_component_url(self):
class TestComponent(Component):
template = "Hello"
class View:
public = True
# Check if the URL is correctly generated
component_url = get_component_url(TestComponent)
assert component_url == f"/components/ext/view/components/{TestComponent.class_id}/"
component_url2 = get_component_url(TestComponent, query={"foo": "bar"}, fragment="baz")
assert component_url2 == f"/components/ext/view/components/{TestComponent.class_id}/?foo=bar#baz"
# Check that the query and fragment are correctly escaped
component_url3 = get_component_url(TestComponent, query={"f'oo": "b ar&ba'z"}, fragment='q u"x')
assert component_url3 == f"/components/ext/view/components/{TestComponent.class_id}/?f%27oo=b+ar%26ba%27z#q%20u%22x" # noqa: E501
def test_public_url(self):
did_call_get = False
did_call_post = False