feat(compte): coquille account_shell (sidebar + offcanvas + garde d'accès) (#73)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Colin Maudry
2026-06-24 18:43:53 +02:00
parent f301a0a336
commit 4602b45f30
2 changed files with 149 additions and 0 deletions
+106
View File
@@ -0,0 +1,106 @@
import dash_bootstrap_components as dbc
from dash import dcc, html
from flask_login import current_user
# Définition centralisée des sections de l'espace compte.
# Ajouter une section future = ajouter une ligne ici (+ créer sa page).
SECTIONS = [
{
"key": "admin",
"label": "Compte",
"href": "/compte/admin",
"require_subscription": False,
},
{
"key": "abonnement",
"label": "Abonnement",
"href": "/compte/abonnement",
"require_subscription": False,
},
{
"key": "archives",
"label": "Mes archives",
"href": "/compte/archives",
"require_subscription": True,
},
{
"key": "filtres",
"label": "Mes filtres",
"href": "/compte/filtres",
"require_subscription": True,
},
{
"key": "siret",
"label": "Mon SIRET",
"href": "/compte/siret",
"require_subscription": True,
},
]
def current_user_has_subscription() -> bool:
"""Stub : à brancher sur la facturation (issue #73)."""
return False
def visible_sections(has_subscription: bool) -> list[dict]:
return [s for s in SECTIONS if has_subscription or not s["require_subscription"]]
def guard_redirect(
is_authenticated: bool,
has_subscription: bool,
require_subscription: bool,
path: str,
) -> str | None:
if not is_authenticated:
return f"/connexion?next={path}"
if require_subscription and not has_subscription:
return "/compte/abonnement"
return None
def account_guard(path: str, require_subscription: bool):
href = guard_redirect(
current_user.is_authenticated,
current_user_has_subscription(),
require_subscription,
path,
)
return dcc.Location(href=href, id="compte-guard-redirect") if href else None
def _nav(active: str):
links = [
dbc.NavLink(s["label"], href=s["href"], active=(s["key"] == active))
for s in visible_sections(current_user_has_subscription())
]
return dbc.Nav(links, vertical=True, pills=True)
def account_shell(active: str, contenu):
sidebar = dbc.Col(
html.Div([html.H5("Mon compte", className="mb-3"), _nav(active)]),
md=3,
className="d-none d-md-block",
)
mobile = html.Div(
[
dbc.Button(
"☰ Sections",
id="compte-offcanvas-open",
color="secondary",
outline=True,
className="mb-3",
),
dbc.Offcanvas(
_nav(active),
id="compte-offcanvas",
title="Mon compte",
is_open=False,
),
],
className="d-md-none",
)
content = dbc.Col([mobile, contenu], md=9)
return dbc.Container(dbc.Row([sidebar, content]), className="py-4")
+43
View File
@@ -0,0 +1,43 @@
from src.pages import _compte_shell as shell
def test_visible_sections_hides_gated_without_subscription():
keys = {s["key"] for s in shell.visible_sections(has_subscription=False)}
assert "admin" in keys
assert "abonnement" in keys
assert "archives" not in keys
def test_visible_sections_shows_all_with_subscription():
keys = {s["key"] for s in shell.visible_sections(has_subscription=True)}
assert "archives" in keys
def test_guard_redirect_anonymous_goes_to_login():
href = shell.guard_redirect(
is_authenticated=False,
has_subscription=False,
require_subscription=False,
path="/compte/admin",
)
assert href == "/connexion?next=/compte/admin"
def test_guard_redirect_unsubscribed_on_gated_goes_to_abonnement():
href = shell.guard_redirect(
is_authenticated=True,
has_subscription=False,
require_subscription=True,
path="/compte/archives",
)
assert href == "/compte/abonnement"
def test_guard_redirect_allowed_returns_none():
href = shell.guard_redirect(
is_authenticated=True,
has_subscription=False,
require_subscription=False,
path="/compte/admin",
)
assert href is None