Skip to content

Commit 39d795a

Browse files
add extra_context to NavGroup and NavItem (#34)
1 parent 1a40cff commit 39d795a

File tree

3 files changed

+114
-2
lines changed

3 files changed

+114
-2
lines changed

CHANGELOG.md

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

1919
## [Unreleased]
2020

21+
### Added
22+
23+
- `NavGroup` and `NavItem` now has a new `extra_context` attribute. This allows for passing additional context to the template when rendering the navigation, either via the extra attribute (`item.foo`) or the `extra_context` attribute itself (`item.extra_context.foo`).
24+
2125
### Changed
2226

2327
- Now using v2024.13 of `django-twc-package`.
2428

29+
### Fixed
30+
31+
- `RenderedNavItem.items` property now correctly returns a list of `RenderedNavItem` objects, rather than a list of `NavItem` objects. This fixes a bug where the properties that should be available (e.g. `active`, `url`, etc.) were not available when iterating over the `RenderedNavItem.items` list if the item was a `NavGroup` object with child items.
32+
2533
## [0.2.0]
2634

2735
### Added

src/django_simple_nav/nav.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from dataclasses import dataclass
44
from dataclasses import field
5+
from typing import Any
56

67
from django.http import HttpRequest
78
from django.template.loader import render_to_string
@@ -38,29 +39,43 @@ class NavGroup:
3839
items: list[NavGroup | NavItem]
3940
url: str | None = None
4041
permissions: list[str] = field(default_factory=list)
42+
extra_context: dict[str, Any] = field(default_factory=dict)
4143

4244

4345
@dataclass(frozen=True)
4446
class NavItem:
4547
title: str
4648
url: str
4749
permissions: list[str] = field(default_factory=list)
50+
extra_context: dict[str, Any] = field(default_factory=dict)
4851

4952

5053
@dataclass(frozen=True)
5154
class RenderedNavItem:
5255
item: NavItem | NavGroup
5356
request: HttpRequest
5457

58+
def __getattr__(self, name: str) -> Any:
59+
if name == "extra_context":
60+
return self.item.extra_context
61+
elif hasattr(self.item, name):
62+
return getattr(self.item, name)
63+
else:
64+
try:
65+
return self.item.extra_context[name]
66+
except KeyError as err:
67+
msg = f"{self.item!r} object has no attribute {name!r}"
68+
raise AttributeError(msg) from err
69+
5570
@property
5671
def title(self) -> str:
5772
return mark_safe(self.item.title)
5873

5974
@property
60-
def items(self) -> list[NavGroup | NavItem] | None:
75+
def items(self) -> list[RenderedNavItem] | None:
6176
if not isinstance(self.item, NavGroup):
6277
return None
63-
return self.item.items
78+
return [RenderedNavItem(item, self.request) for item in self.item.items]
6479

6580
@property
6681
def url(self) -> str:

tests/test_nav.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
from django.utils.module_loading import import_string
77
from model_bakery import baker
88

9+
from django_simple_nav.nav import NavGroup
10+
from django_simple_nav.nav import NavItem
11+
from django_simple_nav.nav import RenderedNavItem
912
from tests.navs import DummyNav
1013
from tests.utils import count_anchors
1114

@@ -96,3 +99,89 @@ def test_nav_render_from_request_with_template_name(req):
9699
rendered_template = DummyNav.render_from_request(req, "tests/alternate.html")
97100

98101
assert "This is an alternate template." in rendered_template
102+
103+
104+
def test_extra_context(req):
105+
item = NavItem(
106+
title="Test",
107+
url="/test/",
108+
extra_context={"foo": "bar"},
109+
)
110+
111+
rendered_item = RenderedNavItem(item, req)
112+
113+
assert rendered_item.foo == "bar"
114+
115+
116+
def test_extra_context_with_no_extra_context(req):
117+
item = NavItem(
118+
title="Test",
119+
url="/test/",
120+
)
121+
122+
rendered_item = RenderedNavItem(item, req)
123+
124+
with pytest.raises(AttributeError):
125+
assert rendered_item.foo == "bar"
126+
127+
128+
def test_extra_context_shadowing(req):
129+
item = NavItem(
130+
title="Test",
131+
url="/test/",
132+
extra_context={"title": "Shadowed"},
133+
)
134+
135+
rendered_item = RenderedNavItem(item, req)
136+
137+
assert rendered_item.title == "Test"
138+
139+
140+
def test_extra_context_iteration(req):
141+
item = NavItem(
142+
title="Test",
143+
url="/test/",
144+
extra_context={"foo": "bar", "baz": "qux"},
145+
)
146+
147+
rendered_item = RenderedNavItem(item, req)
148+
149+
assert rendered_item.extra_context == {"foo": "bar", "baz": "qux"}
150+
for key, value in rendered_item.extra_context.items():
151+
assert getattr(rendered_item, key) == value
152+
153+
154+
def test_extra_context_builtins(req):
155+
item = NavGroup(
156+
title="Test",
157+
items=[
158+
NavItem(
159+
title="Test",
160+
url="/test/",
161+
permissions=["is_staff"],
162+
extra_context={"foo": "bar"},
163+
),
164+
],
165+
url="/test/",
166+
permissions=["is_staff"],
167+
extra_context={"baz": "qux"},
168+
)
169+
170+
rendered_item = RenderedNavItem(item, req)
171+
172+
assert rendered_item.title == "Test"
173+
assert rendered_item.url == "/test/"
174+
assert rendered_item.permissions == ["is_staff"]
175+
assert rendered_item.extra_context == {"baz": "qux"}
176+
assert rendered_item.baz == "qux"
177+
178+
assert rendered_item.items is not None
179+
assert len(rendered_item.items) == 1
180+
181+
rendered_group_item = rendered_item.items[0]
182+
183+
assert rendered_group_item.title == "Test"
184+
assert rendered_group_item.url == "/test/"
185+
assert rendered_group_item.permissions == ["is_staff"]
186+
assert rendered_group_item.extra_context == {"foo": "bar"}
187+
assert rendered_group_item.foo == "bar"

0 commit comments

Comments
 (0)