From 4602b45f30313e924893191a5848154b1715246a Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Wed, 24 Jun 2026 18:43:53 +0200 Subject: [PATCH] =?UTF-8?q?feat(compte):=20coquille=20account=5Fshell=20(s?= =?UTF-8?q?idebar=20+=20offcanvas=20+=20garde=20d'acc=C3=A8s)=20(#73)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- src/pages/_compte_shell.py | 106 +++++++++++++++++++++++++++++++++++++ tests/test_compte_shell.py | 43 +++++++++++++++ 2 files changed, 149 insertions(+) create mode 100644 src/pages/_compte_shell.py create mode 100644 tests/test_compte_shell.py diff --git a/src/pages/_compte_shell.py b/src/pages/_compte_shell.py new file mode 100644 index 0000000..44af4e4 --- /dev/null +++ b/src/pages/_compte_shell.py @@ -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") diff --git a/tests/test_compte_shell.py b/tests/test_compte_shell.py new file mode 100644 index 0000000..bc8ce4b --- /dev/null +++ b/tests/test_compte_shell.py @@ -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