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:
@@ -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")
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user