Adaptation de l'effet de TOUS_ABONNES
This commit is contained in:
+6
-1
@@ -99,7 +99,12 @@ def verify_email():
|
|||||||
return redirect("/verification-email?error=invalid_token")
|
return redirect("/verification-email?error=invalid_token")
|
||||||
db.set_email_verified(user_id)
|
db.set_email_verified(user_id)
|
||||||
login_user(User(db.get_user_by_id(user_id)), remember=True)
|
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"])
|
@auth_bp.route("/login", methods=["POST"])
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ def _subscribe_button(
|
|||||||
authenticated: bool, has_active_subscription: bool, tous_abonnes: bool
|
authenticated: bool, has_active_subscription: bool, tous_abonnes: bool
|
||||||
):
|
):
|
||||||
if tous_abonnes:
|
if tous_abonnes:
|
||||||
|
href = "/compte/abonnement" if authenticated else "/inscription"
|
||||||
return html.Div(
|
return html.Div(
|
||||||
[
|
[
|
||||||
dbc.Alert(
|
dbc.Alert(
|
||||||
@@ -104,9 +105,9 @@ def _subscribe_button(
|
|||||||
color="info",
|
color="info",
|
||||||
),
|
),
|
||||||
html.A(
|
html.A(
|
||||||
"Je m'abonne",
|
"Je m'abonne gratuitement",
|
||||||
href="#",
|
href=href,
|
||||||
className="btn btn-secondary disabled",
|
className="btn btn-secondary",
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
className="text-center my-4",
|
className="text-center my-4",
|
||||||
|
|||||||
@@ -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):
|
def _active_view(row):
|
||||||
meta = plans.plan_meta(row["plan"]) or {"label": row["plan"]}
|
meta = plans.plan_meta(row["plan"]) or {"label": row["plan"]}
|
||||||
end = format_date_french(row["current_period_end"])
|
end = format_date_french(row["current_period_end"])
|
||||||
@@ -262,13 +295,9 @@ def layout(**query):
|
|||||||
body.append(_active_view(row))
|
body.append(_active_view(row))
|
||||||
body.append(_resiliation_modal(row["current_period_end"]))
|
body.append(_resiliation_modal(row["current_period_end"]))
|
||||||
else:
|
else:
|
||||||
if row is not None and row["status"] == "expired":
|
from src.utils import TOUS_ABONNES
|
||||||
body.append(
|
|
||||||
dbc.Alert(
|
body.append(_no_sub_view(TOUS_ABONNES, row))
|
||||||
"Votre abonnement a expiré.", color="warning", className="mb-4"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
body.append(_reabo_button())
|
|
||||||
|
|
||||||
body.append(_salaire_modal)
|
body.append(_salaire_modal)
|
||||||
return account_shell("abonnement", html.Div(body))
|
return account_shell("abonnement", html.Div(body))
|
||||||
|
|||||||
@@ -191,6 +191,15 @@ def _cgu_modal():
|
|||||||
|
|
||||||
|
|
||||||
def layout(**query):
|
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)
|
guard = account_guard("/compte/abonnement/mes-infos", require_subscription=False)
|
||||||
if guard is not None:
|
if guard is not None:
|
||||||
return guard
|
return guard
|
||||||
|
|||||||
@@ -23,10 +23,17 @@ ERROR_MESSAGES = {
|
|||||||
|
|
||||||
|
|
||||||
def layout(error: str | None = None, email: str | None = None, **_):
|
def layout(error: str | None = None, email: str | None = None, **_):
|
||||||
|
from src.utils import TOUS_ABONNES
|
||||||
|
|
||||||
alert = None
|
alert = None
|
||||||
if error and error in ERROR_MESSAGES:
|
if error and error in ERROR_MESSAGES:
|
||||||
alert = dbc.Alert(ERROR_MESSAGES[error], color="danger")
|
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(
|
return dbc.Container(
|
||||||
className="py-4",
|
className="py-4",
|
||||||
style={"maxWidth": "500px"},
|
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"),
|
html.Div("ou", className="text-center text-muted my-2"),
|
||||||
linkedin_button("/compte/abonnement/mes-infos"),
|
linkedin_button(linkedin_next),
|
||||||
html.Hr(),
|
html.Hr(),
|
||||||
dcc.Link("Déjà un compte ? Se connecter", href="/connexion"),
|
dcc.Link("Déjà un compte ? Se connecter", href="/connexion"),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -16,6 +16,21 @@ def test_verify_email_valid_token_logs_in_and_redirects_to_mes_infos(
|
|||||||
assert sess.get("_user_id") == str(uid)
|
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):
|
def test_verify_email_invalid_token(client):
|
||||||
resp = client.get("/auth/verify-email?token=invalide")
|
resp = client.get("/auth/verify-email?token=invalide")
|
||||||
assert "error=invalid_token" in resp.headers["Location"]
|
assert "error=invalid_token" in resp.headers["Location"]
|
||||||
|
|||||||
@@ -6,6 +6,39 @@ def test_reabo_button_links_to_abonnement_page():
|
|||||||
assert "/a-propos/abonnement" in text
|
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):
|
def test_active_view_shows_cancel(monkeypatch):
|
||||||
from src.pages.compte import abonnement as compte_abonnement
|
from src.pages.compte import abonnement as compte_abonnement
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,18 @@ def _plan_env(monkeypatch):
|
|||||||
monkeypatch.setenv("FRISBII_PLAN_SOUTIEN", "plan_soutien")
|
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():
|
def test_selectable_cards_render_both_plans():
|
||||||
from src.pages.compte import abonnement_mes_infos as m
|
from src.pages.compte import abonnement_mes_infos as m
|
||||||
|
|
||||||
|
|||||||
@@ -44,12 +44,23 @@ def test_subscribe_button_active_sub_manages():
|
|||||||
assert "href='/compte/abonnement'" in text
|
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.app import app # noqa: F401
|
||||||
from src.pages.a_propos import abonnement as page
|
from src.pages.a_propos import abonnement as page
|
||||||
|
|
||||||
text = str(page._subscribe_button(False, False, True))
|
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():
|
def test_cgu_terms_trimmed_of_features_section():
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -25,3 +25,12 @@ def test_inscription_linkedin_targets_mes_infos():
|
|||||||
assert "/auth/linkedin?next=/compte/abonnement/mes-infos" in str(
|
assert "/auth/linkedin?next=/compte/abonnement/mes-infos" in str(
|
||||||
inscription.layout()
|
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())
|
||||||
|
|||||||
Reference in New Issue
Block a user