Skip to content

Commit

Permalink
add extra_context to NavGroup and NavItem (#34)
Browse files Browse the repository at this point in the history
  • Loading branch information
joshuadavidthomas authored Feb 23, 2024
1 parent 1a40cff commit 39d795a
Show file tree
Hide file tree
Showing 3 changed files with 114 additions and 2 deletions.
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"

0 comments on commit 39d795a

Please sign in to comment.