Skip to content

Commit 2c08514

Browse files
authored
Merge pull request #183 from AAdewunmi/feat/add-role-aware-app-navigation
Feat/add role aware app navigation
2 parents bc43446 + 4bff7d0 commit 2c08514

5 files changed

Lines changed: 154 additions & 8 deletions

File tree

accounts/templatetags/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Custom template tags for the accounts app."""
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
"""Template tags for surface-aware navigation and access checks."""
2+
3+
from django import template
4+
5+
from accounts.constants import (
6+
GROUP_NAMES,
7+
ROLE_ADMIN,
8+
ROLE_CUSTOMER,
9+
ROLE_MERCHANT,
10+
ROLE_OPS,
11+
)
12+
from accounts.mixins import (
13+
is_admin_user,
14+
user_has_surface_access,
15+
user_in_group,
16+
)
17+
18+
register = template.Library()
19+
20+
21+
@register.filter
22+
def has_group(user, role):
23+
"""Return True when the user belongs to the requested role group."""
24+
25+
if role not in GROUP_NAMES:
26+
return False
27+
return user_in_group(user, GROUP_NAMES[role])
28+
29+
30+
@register.filter
31+
def can_access_surface(user, role):
32+
"""Return True when the user may access the requested surface."""
33+
34+
if role not in {ROLE_ADMIN, ROLE_OPS, ROLE_CUSTOMER, ROLE_MERCHANT}:
35+
return False
36+
return user_has_surface_access(user, role)
37+
38+
39+
@register.simple_tag
40+
def is_admin_surface_user(user):
41+
"""Return True when the current user is an admin."""
42+
return is_admin_user(user)

config/settings/base.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@
5858
"django.contrib.messages.context_processors.messages",
5959
"common.context_processors.app_shell",
6060
],
61+
"libraries": {
62+
"surface_access": "accounts.templatetags.surface_access",
63+
},
6164
},
6265
}
6366
]

templates/partials/_app_nav.html

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,48 @@
1-
<!-- path: templates/partials/_app_nav.html -->
2-
<nav class="navbar navbar-expand-lg rh-topbar">
1+
{% load surface_access %}
2+
3+
<nav class="rh-topbar" aria-label="Primary">
34
<div class="container rh-topbar-inner">
45
<a class="rh-brand" href="{% url 'landing' %}">
56
<span class="rh-brand-mark" aria-hidden="true">R</span>
6-
<span>{{ brand_name }}</span>
7+
<span>{{ brand_name|default:"ReturnHub" }}</span>
78
</a>
8-
<div class="rh-topbar-links" aria-label="Primary">
9+
10+
<div class="rh-topbar-links">
911
<a href="{% url 'landing' %}#how-it-works">How it works</a>
1012
<a href="{% url 'landing' %}#workflow">Workflow</a>
11-
<a href="{% url 'admin-login' %}">Admin</a>
12-
<a href="{% url 'ops-login' %}">Ops</a>
13-
<a href="{% url 'customer-login' %}">Customer</a>
14-
<a href="{% url 'merchant-login' %}">Merchant</a>
13+
14+
{% if request.user.is_authenticated %}
15+
{% if request.user|can_access_surface:"admin" %}
16+
<a href="{% url 'accounts:console_admin' %}">Admin</a>
17+
{% endif %}
18+
{% if request.user|can_access_surface:"ops" %}
19+
<a href="{% url 'ops:queue' %}">Ops</a>
20+
{% endif %}
21+
{% if request.user|can_access_surface:"customer" %}
22+
<a href="{% url 'customer_portal:case_list' %}">Customer</a>
23+
{% endif %}
24+
{% if request.user|can_access_surface:"merchant" %}
25+
<a href="{% url 'merchant_portal:case_list' %}">Merchant</a>
26+
{% endif %}
27+
{% else %}
28+
<a href="{% url 'accounts:login_admin' %}">Admin</a>
29+
<a href="{% url 'accounts:login_ops' %}">Ops</a>
30+
<a href="{% url 'accounts:login_customer' %}">Customer</a>
31+
<a href="{% url 'accounts:login_merchant' %}">Merchant</a>
32+
{% endif %}
33+
1534
<a href="{% url 'landing' %}#support">Support</a>
1635
</div>
36+
37+
<div class="rh-topbar-actions">
38+
{% if request.user.is_authenticated %}
39+
<div class="rh-topbar-meta" aria-label="Signed-in user">
40+
<span class="rh-header-note">{{ request.user.get_username }}</span>
41+
</div>
42+
<a class="btn btn-outline-secondary btn-sm rh-topbar-secondary" href="{% url 'accounts:logout' %}">
43+
Sign out
44+
</a>
45+
{% endif %}
46+
</div>
1747
</div>
1848
</nav>
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
"""Tests for surface access template tags."""
2+
3+
from __future__ import annotations
4+
5+
import pytest
6+
from django.contrib.auth.models import AnonymousUser, Group
7+
8+
from accounts.templatetags.surface_access import (
9+
can_access_surface,
10+
has_group,
11+
is_admin_surface_user,
12+
)
13+
from tests.factories import UserFactory
14+
15+
pytestmark = pytest.mark.django_db
16+
17+
18+
def add_group(user, group_name: str) -> None:
19+
"""Attach a Django group to a user for test setup."""
20+
21+
group, _ = Group.objects.get_or_create(name=group_name)
22+
user.groups.add(group)
23+
24+
25+
def test_has_group_returns_true_for_matching_role_group() -> None:
26+
"""The template filter should confirm membership for valid mapped roles."""
27+
28+
merchant_user = UserFactory()
29+
add_group(merchant_user, "merchant")
30+
31+
assert has_group(merchant_user, "merchant") is True
32+
33+
34+
def test_has_group_returns_false_for_unknown_role() -> None:
35+
"""The template filter should reject unknown role keys."""
36+
37+
user = UserFactory()
38+
39+
assert has_group(user, "unknown") is False
40+
41+
42+
def test_can_access_surface_returns_true_for_allowed_surface() -> None:
43+
"""The template filter should mirror the shared surface access helper."""
44+
45+
customer_user = UserFactory()
46+
add_group(customer_user, "customer")
47+
48+
assert can_access_surface(customer_user, "customer") is True
49+
50+
51+
def test_can_access_surface_returns_false_for_unknown_surface() -> None:
52+
"""Unknown surface names should be rejected by the template filter."""
53+
54+
user = UserFactory()
55+
56+
assert can_access_surface(user, "unknown") is False
57+
58+
59+
def test_is_admin_surface_user_returns_true_for_superuser() -> None:
60+
"""The simple tag should identify admin-capable users."""
61+
62+
admin_user = UserFactory(is_superuser=True, is_staff=True)
63+
64+
assert is_admin_surface_user(admin_user) is True
65+
66+
67+
def test_is_admin_surface_user_returns_false_for_anonymous_user() -> None:
68+
"""Anonymous users should never be treated as admin-capable."""
69+
70+
assert is_admin_surface_user(AnonymousUser()) is False

0 commit comments

Comments
 (0)