Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,18 @@ and this project attempts to adhere to [Semantic Versioning](https://semver.org/

## [Unreleased]

### Added

- `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`).

### Changed

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

### Fixed

- `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.

## [0.2.0]

### Added
Expand Down
19 changes: 17 additions & 2 deletions src/django_simple_nav/nav.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from dataclasses import dataclass
from dataclasses import field
from typing import Any

from django.http import HttpRequest
from django.template.loader import render_to_string
Expand Down Expand Up @@ -38,29 +39,43 @@ class NavGroup:
items: list[NavGroup | NavItem]
url: str | None = None
permissions: list[str] = field(default_factory=list)
extra_context: dict[str, Any] = field(default_factory=dict)


@dataclass(frozen=True)
class NavItem:
title: str
url: str
permissions: list[str] = field(default_factory=list)
extra_context: dict[str, Any] = field(default_factory=dict)


@dataclass(frozen=True)
class RenderedNavItem:
item: NavItem | NavGroup
request: HttpRequest

def __getattr__(self, name: str) -> Any:
if name == "extra_context":
return self.item.extra_context
elif hasattr(self.item, name):
return getattr(self.item, name)
else:
try:
return self.item.extra_context[name]
except KeyError as err:
msg = f"{self.item!r} object has no attribute {name!r}"
raise AttributeError(msg) from err

@property
def title(self) -> str:
return mark_safe(self.item.title)

@property
def items(self) -> list[NavGroup | NavItem] | None:
def items(self) -> list[RenderedNavItem] | None:
if not isinstance(self.item, NavGroup):
return None
return self.item.items
return [RenderedNavItem(item, self.request) for item in self.item.items]

@property
def url(self) -> str:
Expand Down
89 changes: 89 additions & 0 deletions tests/test_nav.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
from django.utils.module_loading import import_string
from model_bakery import baker

from django_simple_nav.nav import NavGroup
from django_simple_nav.nav import NavItem
from django_simple_nav.nav import RenderedNavItem
from tests.navs import DummyNav
from tests.utils import count_anchors

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

assert "This is an alternate template." in rendered_template


def test_extra_context(req):
item = NavItem(
title="Test",
url="/test/",
extra_context={"foo": "bar"},
)

rendered_item = RenderedNavItem(item, req)

assert rendered_item.foo == "bar"


def test_extra_context_with_no_extra_context(req):
item = NavItem(
title="Test",
url="/test/",
)

rendered_item = RenderedNavItem(item, req)

with pytest.raises(AttributeError):
assert rendered_item.foo == "bar"


def test_extra_context_shadowing(req):
item = NavItem(
title="Test",
url="/test/",
extra_context={"title": "Shadowed"},
)

rendered_item = RenderedNavItem(item, req)

assert rendered_item.title == "Test"


def test_extra_context_iteration(req):
item = NavItem(
title="Test",
url="/test/",
extra_context={"foo": "bar", "baz": "qux"},
)

rendered_item = RenderedNavItem(item, req)

assert rendered_item.extra_context == {"foo": "bar", "baz": "qux"}
for key, value in rendered_item.extra_context.items():
assert getattr(rendered_item, key) == value


def test_extra_context_builtins(req):
item = NavGroup(
title="Test",
items=[
NavItem(
title="Test",
url="/test/",
permissions=["is_staff"],
extra_context={"foo": "bar"},
),
],
url="/test/",
permissions=["is_staff"],
extra_context={"baz": "qux"},
)

rendered_item = RenderedNavItem(item, req)

assert rendered_item.title == "Test"
assert rendered_item.url == "/test/"
assert rendered_item.permissions == ["is_staff"]
assert rendered_item.extra_context == {"baz": "qux"}
assert rendered_item.baz == "qux"

assert rendered_item.items is not None
assert len(rendered_item.items) == 1

rendered_group_item = rendered_item.items[0]

assert rendered_group_item.title == "Test"
assert rendered_group_item.url == "/test/"
assert rendered_group_item.permissions == ["is_staff"]
assert rendered_group_item.extra_context == {"foo": "bar"}
assert rendered_group_item.foo == "bar"