Skip to content

Commit 9e1721f

Browse files
allow NavItem.url to be a callable that returns a string (#112)
1 parent 0c7b135 commit 9e1721f

File tree

3 files changed

+39
-7
lines changed

3 files changed

+39
-7
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ and this project attempts to adhere to [Semantic Versioning](https://semver.org/
1818

1919
## [Unreleased]
2020

21+
### Changed
22+
23+
- Updated `NavItem.url` and `NavItem.get_url` to allow for using a callable. This allows `NavItem.url` to support `django.urls.reverse` or `django.urls.reverse_lazy` primarily, but it can be any callable as long as it returns a string.
24+
2125
## [0.7.0]
2226

2327
### Added

src/django_simple_nav/nav.py

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
from dataclasses import field
66
from typing import Callable
77
from typing import cast
8+
from urllib.parse import urlparse
9+
from urllib.parse import urlunparse
810

911
from django.apps import apps
1012
from django.conf import settings
@@ -14,6 +16,7 @@
1416
from django.template.loader import get_template
1517
from django.urls import reverse
1618
from django.urls.exceptions import NoReverseMatch
19+
from django.utils.functional import Promise
1720
from django.utils.safestring import mark_safe
1821

1922
from django_simple_nav._templates import get_template_engine
@@ -63,7 +66,7 @@ def get_template_name(self) -> str:
6366
@dataclass(frozen=True)
6467
class NavItem:
6568
title: str
66-
url: str | None = None
69+
url: str | Callable[..., str] | None = None
6770
permissions: list[str | Callable[[HttpRequest], bool]] = field(default_factory=list)
6871
extra_context: dict[str, object] = field(default_factory=dict)
6972

@@ -92,14 +95,33 @@ def get_title(self) -> str:
9295
def get_url(self) -> str:
9396
url: str | None
9497

95-
try:
96-
url = reverse(self.url)
97-
except NoReverseMatch:
98-
url = self.url
98+
if isinstance(self.url, Promise):
99+
# django.urls.base.reverse_lazy
100+
url = str(self.url)
101+
elif callable(self.url):
102+
# django.urls.base.reverse (or some other basic callable)
103+
url = self.url()
104+
else:
105+
try:
106+
url = reverse(self.url)
107+
except NoReverseMatch:
108+
url = self.url
99109

100110
if url is not None:
101-
if settings.APPEND_SLASH and not url.endswith("/"): # pyright: ignore[reportAny]
102-
url += "/"
111+
parsed_url = urlparse(url)
112+
path = parsed_url.path
113+
if settings.APPEND_SLASH and not path.endswith("/"):
114+
path += "/"
115+
url = urlunparse(
116+
(
117+
parsed_url.scheme,
118+
parsed_url.netloc,
119+
path,
120+
parsed_url.params,
121+
parsed_url.query,
122+
parsed_url.fragment,
123+
)
124+
)
103125
return url
104126

105127
msg = f"{self.__class__!r} must define 'url' or override 'get_url()'"

tests/test_navitem.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
from django.contrib.auth.models import AnonymousUser
77
from django.core.exceptions import ImproperlyConfigured
88
from django.test import override_settings
9+
from django.urls import reverse
10+
from django.urls import reverse_lazy
911
from model_bakery import baker
1012

1113
from django_simple_nav.nav import NavItem
@@ -83,6 +85,10 @@ def get_title(self):
8385
("/test", False, "/test"),
8486
("home", True, "/"),
8587
("home", False, "/"),
88+
(reverse("home"), True, "/"),
89+
(reverse("home"), False, "/"),
90+
(reverse_lazy("home"), True, "/"),
91+
(reverse_lazy("home"), False, "/"),
8692
],
8793
)
8894
def test_get_url(url, append_slash, expected):

0 commit comments

Comments
 (0)