From 39d795a5c10a2058263d8eed058a71c573de7c2e Mon Sep 17 00:00:00 2001 From: Josh Thomas Date: Fri, 23 Feb 2024 11:04:42 -0600 Subject: [PATCH] add `extra_context` to `NavGroup` and `NavItem` (#34) --- CHANGELOG.md | 8 ++++ src/django_simple_nav/nav.py | 19 +++++++- tests/test_nav.py | 89 ++++++++++++++++++++++++++++++++++++ 3 files changed, 114 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1481cb..2c727df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/django_simple_nav/nav.py b/src/django_simple_nav/nav.py index 6bab8ed..905efa7 100644 --- a/src/django_simple_nav/nav.py +++ b/src/django_simple_nav/nav.py @@ -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 @@ -38,6 +39,7 @@ 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) @@ -45,6 +47,7 @@ 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) @@ -52,15 +55,27 @@ 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: diff --git a/tests/test_nav.py b/tests/test_nav.py index 2315334..ea97d5c 100644 --- a/tests/test_nav.py +++ b/tests/test_nav.py @@ -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 @@ -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"