Skip to content

Commit

Permalink
add initial nav rendering
Browse files Browse the repository at this point in the history
  • Loading branch information
joshuadavidthomas authored and jefftriplett committed Dec 5, 2023
1 parent a05d40c commit 0ba5128
Show file tree
Hide file tree
Showing 7 changed files with 187 additions and 0 deletions.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ dev = [
"django-stubs-ext",
"hatch",
"mypy",
"model_bakery",
"nox",
"pytest",
"pytest-cov",
Expand Down
74 changes: 74 additions & 0 deletions src/simple_nav/nav.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
from __future__ import annotations

from dataclasses import dataclass
from dataclasses import field

from django.http import HttpRequest
from django.template.loader import render_to_string
from django.urls import reverse
from django.urls.exceptions import NoReverseMatch


@dataclass
class Nav:
items: list[NavGroup | NavItem]
template_name: str

def _get_url(self, url: str) -> str:
try:
return reverse(url)
except NoReverseMatch:
return url

@classmethod
def render(cls, request: HttpRequest) -> str:
items = [
item for item in cls.items if cls._check_item_visibility(request, item)
]
return render_to_string(
template_name=cls.template_name,
context={"items": items},
)

@classmethod
def _check_item_visibility(
cls, request: HttpRequest, item: NavGroup | NavItem
) -> bool:
if isinstance(item, NavItem):
for idx, perm in enumerate(item.permissions):
user_perm = getattr(request.user, perm, False)
if not user_perm:
return False
if not idx == len(item.permissions) - 1:
continue
elif isinstance(item, NavGroup):
sub_items = [
sub_item
for sub_item in item.items
if cls._check_item_visibility(request, sub_item)
]
if not sub_items:
return False

return True

@staticmethod
def _check_item_active(request: HttpRequest, url: str) -> bool:
return request.path.startswith(url) and url != "/" or request.path == url


@dataclass
class NavGroup:
title: str
items: list[NavGroup | NavItem]
url: str | None = None
permissions: list[str] = field(default_factory=list)
active: bool = False


@dataclass
class NavItem:
title: str
url: str
permissions: list[str] = field(default_factory=list)
active: bool = False
Empty file.
13 changes: 13 additions & 0 deletions src/simple_nav/templatetags/simple_nav_tags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from __future__ import annotations

from django import template
from django.template import Context
from django.utils.module_loading import import_string

register = template.Library()


@register.simple_tag(takes_context=True)
def simple_nav(context: Context, nav_path: str) -> str:
nav = import_string(nav_path)
return nav.render(request=context["request"])
9 changes: 9 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,19 @@ def pytest_configure(config):
},
EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend",
INSTALLED_APPS=[
"django.contrib.auth",
"django.contrib.contenttypes",
"simple_nav",
"tests",
],
LOGGING_CONFIG=None,
PASSWORD_HASHERS=["django.contrib.auth.hashers.MD5PasswordHasher"],
SECRET_KEY="NOTASECRET",
TEMPLATES=[
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"APP_DIRS": True,
}
],
USE_TZ=True,
)
6 changes: 6 additions & 0 deletions tests/templates/tests/test_nav.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{% for item in items %}
{% if item.items %}
{% include 'tests/test_nav.html' with items=item.items %}
{% endif %}
<a href="{{ item.url }}">{{ item.title }}</a>
{% endfor %}
84 changes: 84 additions & 0 deletions tests/test_nav.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
from __future__ import annotations

import pytest
from django.contrib.auth import get_user_model
from django.http import HttpRequest
from django.utils.module_loading import import_string
from model_bakery import baker

from simple_nav.nav import Nav
from simple_nav.nav import NavGroup
from simple_nav.nav import NavItem

pytestmark = pytest.mark.django_db


class TestNav(Nav):
items = [
NavItem(title="Relative URL", url="/relative-url"),
NavItem(title="Absolute URL", url="https://example.com/absolute-url"),
NavGroup(
title="Group",
url="/group",
items=[
NavItem(title="Relative URL", url="/relative-url"),
NavItem(title="Absolute URL", url="https://example.com/absolute-url"),
],
),
NavItem(
title="is_authenticated Item", url="#", permissions=["is_authenticated"]
),
NavItem(title="is_staff Item", url="#", permissions=["is_staff"]),
NavItem(title="is_superuser Item", url="#", permissions=["is_superuser"]),
NavGroup(
title="is_authenticated Group",
permissions=["is_authenticated"],
items=[NavItem(title="Test Item", url="#")],
),
NavGroup(
title="is_staff Group",
permissions=["is_staff"],
items=[NavItem(title="Test Item", url="#")],
),
NavGroup(
title="is_superuser Group",
permissions=["is_superuser"],
items=[NavItem(title="Test Item", url="#")],
),
]
template_name = "tests/test_nav.html"


@pytest.fixture
def req():
return HttpRequest()


@pytest.fixture
def user():
return baker.make(get_user_model())


def test_dotted_path_loading():
nav = import_string("tests.test_nav.TestNav")

assert len(nav.items) == 9
assert nav.template_name == "tests/test_nav.html"


def test_nav_render(req, user):
req.user = user
rendered_template = TestNav.render(req)

assert "Relative URL" in rendered_template
assert "/relative-url" in rendered_template
assert "Absolute URL" in rendered_template
assert "https://example.com/absolute-url" in rendered_template
assert "Group" in rendered_template


def test_dotted_path_rendering(req, user):
req.user = user
nav = import_string("tests.test_nav.TestNav")

assert nav.render(req)

0 comments on commit 0ba5128

Please sign in to comment.