From 632e9449f42484987099cea292d42fd0184f2d98 Mon Sep 17 00:00:00 2001 From: Josh Date: Thu, 22 Feb 2024 21:25:43 -0600 Subject: [PATCH 1/7] add `extra_context` to `NavGroup` and `NavItem` --- src/django_simple_nav/nav.py | 10 ++++++++++ tests/test_nav.py | 25 +++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/src/django_simple_nav/nav.py b/src/django_simple_nav/nav.py index 6bab8ed..b531b78 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,6 +55,13 @@ class RenderedNavItem: item: NavItem | NavGroup request: HttpRequest + def __getattr__(self, name: str) -> Any: + 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) diff --git a/tests/test_nav.py b/tests/test_nav.py index 2315334..0c35018 100644 --- a/tests/test_nav.py +++ b/tests/test_nav.py @@ -6,6 +6,8 @@ from django.utils.module_loading import import_string from model_bakery import baker +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 +98,26 @@ 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" From e8902c1acdc2f019fd1a93a7ea011735fa85672a Mon Sep 17 00:00:00 2001 From: Josh Date: Thu, 22 Feb 2024 22:56:19 -0600 Subject: [PATCH 2/7] add test for shadowing --- tests/test_nav.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/test_nav.py b/tests/test_nav.py index 0c35018..a99fdf1 100644 --- a/tests/test_nav.py +++ b/tests/test_nav.py @@ -121,3 +121,14 @@ def test_extra_context_with_no_extra_context(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" From ce2c0f0ad7ca07d8ba8867122b9fe7939ecc4a82 Mon Sep 17 00:00:00 2001 From: Josh Date: Fri, 23 Feb 2024 10:47:12 -0600 Subject: [PATCH 3/7] add ability to iterate `extra_context` directly --- src/django_simple_nav/nav.py | 3 +++ tests/test_nav.py | 16 ++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/src/django_simple_nav/nav.py b/src/django_simple_nav/nav.py index b531b78..1ae4129 100644 --- a/src/django_simple_nav/nav.py +++ b/src/django_simple_nav/nav.py @@ -56,6 +56,9 @@ class RenderedNavItem: request: HttpRequest def __getattr__(self, name: str) -> Any: + if name == "extra_context": + return self.item.extra_context + try: return self.item.extra_context[name] except KeyError as err: diff --git a/tests/test_nav.py b/tests/test_nav.py index a99fdf1..9b89272 100644 --- a/tests/test_nav.py +++ b/tests/test_nav.py @@ -117,6 +117,7 @@ def test_extra_context_with_no_extra_context(req): title="Test", url="/test/", ) + rendered_item = RenderedNavItem(item, req) with pytest.raises(AttributeError): @@ -129,6 +130,21 @@ def test_extra_context_shadowing(req): 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 From fecfc3bfecf93212938b56408a68016ed03d79e0 Mon Sep 17 00:00:00 2001 From: Josh Date: Fri, 23 Feb 2024 10:50:01 -0600 Subject: [PATCH 4/7] update CHANGELOG --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1481cb..9ef7d22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,10 @@ and this project attempts to adhere to [Semantic Versioning](https://semver.org/ ## [Unreleased] +### Added + +- A `extra_context` attribute to `NavGroup` and `NavItem` classes. 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`. From 014605cf64347c0a708c8f5335104b35fb982f44 Mon Sep 17 00:00:00 2001 From: Josh Date: Fri, 23 Feb 2024 10:50:28 -0600 Subject: [PATCH 5/7] grammar --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ef7d22..f80f9e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,7 +20,7 @@ and this project attempts to adhere to [Semantic Versioning](https://semver.org/ ### Added -- A `extra_context` attribute to `NavGroup` and `NavItem` classes. 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`). +- An `extra_context` attribute to `NavGroup` and `NavItem` classes. 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 From ce83b5cbf2e3e71f4b06c93ed30c43dfc59fd355 Mon Sep 17 00:00:00 2001 From: Josh Date: Fri, 23 Feb 2024 10:52:05 -0600 Subject: [PATCH 6/7] adjust --- CHANGELOG.md | 2 +- src/django_simple_nav/nav.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f80f9e9..9c79166 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,7 +20,7 @@ and this project attempts to adhere to [Semantic Versioning](https://semver.org/ ### Added -- An `extra_context` attribute to `NavGroup` and `NavItem` classes. 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`). +- `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 diff --git a/src/django_simple_nav/nav.py b/src/django_simple_nav/nav.py index 1ae4129..b8a4fcb 100644 --- a/src/django_simple_nav/nav.py +++ b/src/django_simple_nav/nav.py @@ -58,7 +58,6 @@ class RenderedNavItem: def __getattr__(self, name: str) -> Any: if name == "extra_context": return self.item.extra_context - try: return self.item.extra_context[name] except KeyError as err: From 87c7bbb76db89b88a9edf55f925835ad3efa2b1d Mon Sep 17 00:00:00 2001 From: Josh Date: Fri, 23 Feb 2024 11:02:56 -0600 Subject: [PATCH 7/7] fix a bug with a rendered nav group with items --- CHANGELOG.md | 4 ++++ src/django_simple_nav/nav.py | 17 ++++++++++------- tests/test_nav.py | 37 ++++++++++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c79166..2c727df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,10 @@ and this project attempts to adhere to [Semantic Versioning](https://semver.org/ - 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 b8a4fcb..905efa7 100644 --- a/src/django_simple_nav/nav.py +++ b/src/django_simple_nav/nav.py @@ -58,21 +58,24 @@ class RenderedNavItem: def __getattr__(self, name: str) -> Any: if name == "extra_context": return self.item.extra_context - 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 + 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 9b89272..ea97d5c 100644 --- a/tests/test_nav.py +++ b/tests/test_nav.py @@ -6,6 +6,7 @@ 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 @@ -148,3 +149,39 @@ def test_extra_context_iteration(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"