From 70ca65e962a332c4a1797289cf8288d5bfc73bac Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Tue, 14 Jul 2026 12:53:01 +0200 Subject: [PATCH] Adaptation de l'effet de TOUS_ABONNES --- src/auth/routes.py | 7 ++- src/pages/a_propos/abonnement.py | 7 +-- src/pages/compte/abonnement.py | 43 ++++++++++++++++--- src/pages/compte/abonnement_mes_infos.py | 9 ++++ src/pages/inscription.py | 9 +++- tests/auth/test_verify_email.py | 15 +++++++ tests/subscriptions/test_compte_abonnement.py | 33 ++++++++++++++ tests/subscriptions/test_mes_infos_plan.py | 12 ++++++ tests/test_abonnement_public.py | 15 ++++++- tests/test_boutons.py | 25 ----------- tests/test_linkedin_button.py | 9 ++++ 11 files changed, 145 insertions(+), 39 deletions(-) delete mode 100644 tests/test_boutons.py diff --git a/src/auth/routes.py b/src/auth/routes.py index ef1ce9e..fcdbe20 100644 --- a/src/auth/routes.py +++ b/src/auth/routes.py @@ -99,7 +99,12 @@ def verify_email(): return redirect("/verification-email?error=invalid_token") db.set_email_verified(user_id) login_user(User(db.get_user_by_id(user_id)), remember=True) - return redirect("/compte/abonnement/mes-infos") + from src.utils import TOUS_ABONNES + + # Sous TOUS_ABONNES, la page carte bancaire (mes-infos) est un cul-de-sac + # (pas de prestataire de paiement) : on renvoie vers la page abonnement. + dest = "/compte/abonnement" if TOUS_ABONNES else "/compte/abonnement/mes-infos" + return redirect(dest) @auth_bp.route("/login", methods=["POST"]) diff --git a/src/pages/a_propos/abonnement.py b/src/pages/a_propos/abonnement.py index 63d91fb..5ead13e 100644 --- a/src/pages/a_propos/abonnement.py +++ b/src/pages/a_propos/abonnement.py @@ -95,6 +95,7 @@ def _subscribe_button( authenticated: bool, has_active_subscription: bool, tous_abonnes: bool ): if tous_abonnes: + href = "/compte/abonnement" if authenticated else "/inscription" return html.Div( [ dbc.Alert( @@ -104,9 +105,9 @@ def _subscribe_button( color="info", ), html.A( - "Je m'abonne", - href="#", - className="btn btn-secondary disabled", + "Je m'abonne gratuitement", + href=href, + className="btn btn-secondary", ), ], className="text-center my-4", diff --git a/src/pages/compte/abonnement.py b/src/pages/compte/abonnement.py index 02dd0e0..6622e1f 100644 --- a/src/pages/compte/abonnement.py +++ b/src/pages/compte/abonnement.py @@ -41,6 +41,39 @@ def _reabo_button(): ) +def _free_access_view(): + from src.pages.a_propos.abonnement import abonnement_features + + return html.Div( + [ + html.H3( + "Vous avez temporairement accès à toutes les fonctionnalités", + className="mb-3", + ), + html.P("Votre accès gratuit débloque :"), + abonnement_features, + dcc.Markdown( + "Pensez à copier les liens vers vos vues pour les conserver si vous " + "ne comptez pas souscrire à un abonnement payant.", + className="text-muted mt-3", + ), + ], + className="mb-4", + ) + + +def _no_sub_view(tous_abonnes: bool, row): + if tous_abonnes: + return _free_access_view() + blocks = [] + if row is not None and row["status"] == "expired": + blocks.append( + dbc.Alert("Votre abonnement a expiré.", color="warning", className="mb-4") + ) + blocks.append(_reabo_button()) + return html.Div(blocks) + + def _active_view(row): meta = plans.plan_meta(row["plan"]) or {"label": row["plan"]} end = format_date_french(row["current_period_end"]) @@ -262,13 +295,9 @@ def layout(**query): body.append(_active_view(row)) body.append(_resiliation_modal(row["current_period_end"])) else: - if row is not None and row["status"] == "expired": - body.append( - dbc.Alert( - "Votre abonnement a expiré.", color="warning", className="mb-4" - ) - ) - body.append(_reabo_button()) + from src.utils import TOUS_ABONNES + + body.append(_no_sub_view(TOUS_ABONNES, row)) body.append(_salaire_modal) return account_shell("abonnement", html.Div(body)) diff --git a/src/pages/compte/abonnement_mes_infos.py b/src/pages/compte/abonnement_mes_infos.py index 05539bb..d0a3d97 100644 --- a/src/pages/compte/abonnement_mes_infos.py +++ b/src/pages/compte/abonnement_mes_infos.py @@ -191,6 +191,15 @@ def _cgu_modal(): def layout(**query): + from src.utils import TOUS_ABONNES + + # Sous TOUS_ABONNES, la souscription payante est désactivée : cette page + # carte bancaire n'a plus de sens, on renvoie vers la page abonnement. + if TOUS_ABONNES: + return dcc.Location( + href="/compte/abonnement", id="mes-infos-tous-abonnes-redirect" + ) + guard = account_guard("/compte/abonnement/mes-infos", require_subscription=False) if guard is not None: return guard diff --git a/src/pages/inscription.py b/src/pages/inscription.py index 585ffc2..426dd4e 100644 --- a/src/pages/inscription.py +++ b/src/pages/inscription.py @@ -23,10 +23,17 @@ ERROR_MESSAGES = { def layout(error: str | None = None, email: str | None = None, **_): + from src.utils import TOUS_ABONNES + alert = None if error and error in ERROR_MESSAGES: alert = dbc.Alert(ERROR_MESSAGES[error], color="danger") + # Sous TOUS_ABONNES, on saute la page carte bancaire (mes-infos). + linkedin_next = ( + "/compte/abonnement" if TOUS_ABONNES else "/compte/abonnement/mes-infos" + ) + return dbc.Container( className="py-4", style={"maxWidth": "500px"}, @@ -70,7 +77,7 @@ def layout(error: str | None = None, email: str | None = None, **_): ], ), html.Div("ou", className="text-center text-muted my-2"), - linkedin_button("/compte/abonnement/mes-infos"), + linkedin_button(linkedin_next), html.Hr(), dcc.Link("Déjà un compte ? Se connecter", href="/connexion"), ], diff --git a/tests/auth/test_verify_email.py b/tests/auth/test_verify_email.py index f1db547..72e283c 100644 --- a/tests/auth/test_verify_email.py +++ b/tests/auth/test_verify_email.py @@ -16,6 +16,21 @@ def test_verify_email_valid_token_logs_in_and_redirects_to_mes_infos( assert sess.get("_user_id") == str(uid) +def test_verify_email_tous_abonnes_redirects_to_abonnement( + client, users_db_path, monkeypatch +): + monkeypatch.setattr("src.utils.TOUS_ABONNES", True) + db.init_schema() + uid = db.create_user("t@b.c", "hash") + token = tokens.create_verification_token(uid) + + resp = client.get(f"/auth/verify-email?token={token}") + assert resp.status_code == 302 + loc = resp.headers["Location"] + assert loc.endswith("/compte/abonnement") + assert "mes-infos" not in loc + + def test_verify_email_invalid_token(client): resp = client.get("/auth/verify-email?token=invalide") assert "error=invalid_token" in resp.headers["Location"] diff --git a/tests/subscriptions/test_compte_abonnement.py b/tests/subscriptions/test_compte_abonnement.py index a033839..eade738 100644 --- a/tests/subscriptions/test_compte_abonnement.py +++ b/tests/subscriptions/test_compte_abonnement.py @@ -6,6 +6,39 @@ def test_reabo_button_links_to_abonnement_page(): assert "/a-propos/abonnement" in text +def test_free_access_view_message(): + from src.pages.compte import abonnement as compte_abonnement + + text = str(compte_abonnement._free_access_view()) + assert "temporairement accès à toutes les fonctionnalités" in text + assert "Pensez à copier les liens vers vos vues" in text + + +def test_no_sub_view_uses_free_access_when_tous_abonnes(): + from src.pages.compte import abonnement as compte_abonnement + + text = str(compte_abonnement._no_sub_view(True, None)) + assert "temporairement accès" in text + assert "Abonnez-vous" not in text + + +def test_no_sub_view_uses_reabo_when_flag_off(): + from src.pages.compte import abonnement as compte_abonnement + + text = str(compte_abonnement._no_sub_view(False, None)) + assert "Abonnez-vous" in text + assert "temporairement accès" not in text + + +def test_no_sub_view_shows_expired_alert_when_flag_off(): + from src.pages.compte import abonnement as compte_abonnement + + row = {"status": "expired", "current_period_end": None} + text = str(compte_abonnement._no_sub_view(False, row)) + assert "expiré" in text + assert "Abonnez-vous" in text + + def test_active_view_shows_cancel(monkeypatch): from src.pages.compte import abonnement as compte_abonnement diff --git a/tests/subscriptions/test_mes_infos_plan.py b/tests/subscriptions/test_mes_infos_plan.py index 50ff472..37715cc 100644 --- a/tests/subscriptions/test_mes_infos_plan.py +++ b/tests/subscriptions/test_mes_infos_plan.py @@ -7,6 +7,18 @@ def _plan_env(monkeypatch): monkeypatch.setenv("FRISBII_PLAN_SOUTIEN", "plan_soutien") +def test_layout_redirects_to_abonnement_when_tous_abonnes(monkeypatch): + monkeypatch.setattr("src.utils.TOUS_ABONNES", True) + from src.pages.compte import abonnement_mes_infos as m + + text = str(m.layout()) + # redirection du flag vers /compte/abonnement (href exact), pas le guard de + # connexion ni un renvoi vers la page carte bancaire mes-infos + assert "href='/compte/abonnement'" in text + assert "Mes informations de facturation" not in text + assert "connexion" not in text + + def test_selectable_cards_render_both_plans(): from src.pages.compte import abonnement_mes_infos as m diff --git a/tests/test_abonnement_public.py b/tests/test_abonnement_public.py index 0f8a18b..b24a9c7 100644 --- a/tests/test_abonnement_public.py +++ b/tests/test_abonnement_public.py @@ -44,12 +44,23 @@ def test_subscribe_button_active_sub_manages(): assert "href='/compte/abonnement'" in text -def test_subscribe_button_disabled_when_tous_abonnes(): +def test_subscribe_button_tous_abonnes_visitor_free_signup(): from src.app import app # noqa: F401 from src.pages.a_propos import abonnement as page text = str(page._subscribe_button(False, False, True)) - assert "disabled" in text + assert "Je m'abonne gratuitement" in text + assert "href='/inscription'" in text + assert "disabled" not in text + + +def test_subscribe_button_tous_abonnes_authenticated_goes_to_compte(): + from src.app import app # noqa: F401 + from src.pages.a_propos import abonnement as page + + text = str(page._subscribe_button(True, False, True)) + assert "href='/compte/abonnement'" in text + assert "disabled" not in text def test_cgu_terms_trimmed_of_features_section(): diff --git a/tests/test_boutons.py b/tests/test_boutons.py deleted file mode 100644 index 52ee5b0..0000000 --- a/tests/test_boutons.py +++ /dev/null @@ -1,25 +0,0 @@ -from dash.testing.composite import DashComposite - - -def _rgb_tuple(css_color: str) -> tuple[int, int, int]: - """Parse 'rgb(r, g, b)' ou 'rgba(r, g, b, a)' en (r, g, b).""" - inner = css_color[css_color.index("(") + 1 : css_color.index(")")] - parts = [p.strip() for p in inner.split(",")] - return (int(parts[0]), int(parts[1]), int(parts[2])) - - -def test_btn_primary_is_terracotta(dash_duo: DashComposite): - """Garde de non-régression : le bouton primaire de l'accueil conserve son - dégradé terracotta et son texte blanc après l'override de la charte.""" - from src.app import app - - dash_duo.start_server(app) - dash_duo.wait_for_element("a.btn.btn-primary, button.btn.btn-primary", timeout=10) - btn = dash_duo.find_element("a.btn.btn-primary, button.btn.btn-primary") - - # Le dégradé terracotta est posé via background-image ; le texte est blanc. - bg_image = btn.value_of_css_property("background-image") - color = btn.value_of_css_property("color") - - assert "gradient" in bg_image # dégradé terracotta appliqué - assert _rgb_tuple(color) == (255, 255, 255) # texte blanc diff --git a/tests/test_linkedin_button.py b/tests/test_linkedin_button.py index 1003a72..a9e3444 100644 --- a/tests/test_linkedin_button.py +++ b/tests/test_linkedin_button.py @@ -25,3 +25,12 @@ def test_inscription_linkedin_targets_mes_infos(): assert "/auth/linkedin?next=/compte/abonnement/mes-infos" in str( inscription.layout() ) + + +def test_inscription_linkedin_targets_abonnement_when_tous_abonnes(monkeypatch): + monkeypatch.setattr("src.utils.TOUS_ABONNES", True) + # Import app first to initialize Dash + from src.app import app # noqa: F401 + from src.pages import inscription + + assert "/auth/linkedin?next=/compte/abonnement'" in str(inscription.layout())