website_membership_signup_required: accept terms & conditions in signup form. Redirect to cart

This commit is contained in:
Luis 2026-07-22 17:42:28 +02:00
parent 07b0e546c4
commit ba52c72b78
6 changed files with 151 additions and 22 deletions

View file

@ -1,8 +1,9 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import http
from odoo import _, http
from odoo.addons.auth_signup.controllers.main import AuthSignupHome
from odoo.addons.web.controllers.home import SIGN_UP_REQUEST_PARAMS
from odoo.exceptions import UserError
from odoo.http import request
# partner_firstname exposes `firstname` / `lastname` on res.users and
@ -10,6 +11,11 @@ from odoo.http import request
# roundtrips them transparently through `get_auth_signup_qcontext`.
SIGN_UP_REQUEST_PARAMS.add("firstname")
SIGN_UP_REQUEST_PARAMS.add("lastname")
# `accepted_terms` carries the "I agree to the terms & conditions"
# checkbox value from the signup form. Adding it here makes the value
# round-trip through `get_auth_signup_qcontext` so we can validate it
# server-side in `_prepare_signup_values`.
SIGN_UP_REQUEST_PARAMS.add("accepted_terms")
# Session keys used by the membership signup flow.
MEMBERSHIP_SIGNUP_SESSION_KEYS = ("membership_signup_active", "membership_signup_product_id")
@ -19,6 +25,15 @@ class AuthSignupHomeMembership(AuthSignupHome):
def _prepare_signup_values(self, qcontext):
values = super()._prepare_signup_values(qcontext)
# Legal terms acceptance: the signup form includes a mandatory
# checkbox "I agree to the terms & conditions". The POST value is
# 'accepted' when checked. Reject the submission with a UserError
# (the core web_auth_signup controller catches UserError and exposes
# it as `qcontext['error']` which the template renders as a red
# alert above the form, so no extra plumbing is needed). The browser
# also enforces it client-side via `required="required"`.
if qcontext.get("accepted_terms") != "accepted":
raise UserError(_("You must accept the terms and conditions."))
# partner_firstname: the signup form collects `firstname` / `lastname`
# separately. We inject both into the signup values so the resulting
# partner/user has them populated.
@ -53,22 +68,59 @@ class AuthSignupHomeMembership(AuthSignupHome):
@http.route()
def web_auth_signup(self, *args, **kw):
response = super().web_auth_signup(*args, **kw)
# On a successful signup the user is now authenticated (session.uid
# set) and the redirect to the product page will happen. Drop the
# membership markers from the session so they cannot leak into later
# /web/signup attempts by other visitors sharing the session (defence
# in depth).
if request.session.uid:
for key in MEMBERSHIP_SIGNUP_SESSION_KEYS:
request.session.pop(key, None)
# Successful signup: if this is a membership flow, add the
# pending membership product to the now-authenticated cart and
# redirect straight to /shop/cart (instead of going back to the
# product page as the core would, via the `redirect` query
# param). Otherwise fall through to the regular response.
redirect = self._membership_post_login_add_to_cart()
if redirect is not None:
return redirect
return response
@http.route()
def web_login(self, *args, **kw):
response = super().web_login(*args, **kw)
# Same cleanup on explicit login (covers the "Already have an
# account?" branch from the signup page).
# Same handling on explicit login: covers the "Already have an
# account?" branch from the signup page (option B agreed with the
# client): both signup and login auto-add the pending membership
# product and go straight to the cart.
if request.session.uid:
redirect = self._membership_post_login_add_to_cart()
if redirect is not None:
return redirect
return response
def _membership_post_login_add_to_cart(self):
"""Add the pending membership product (if any, set by the
add-to-cart interception on `/shop/cart/update`) to the
now-authenticated user's cart and redirect to `/shop/cart`.
Returns a Response (redirect to /shop/cart) when a membership
product was pending in the session, or `None` when none was
in the latter case the caller should fall through to the
original response (e.g., a normal login that did not originate
from the membership flow).
Side effect: always clears the membership session markers
(`membership_signup_active`, `membership_signup_product_id`),
whether a pending product was present or not, so they cannot
leak into a later request.
"""
product_id = request.session.get("membership_signup_product_id")
if not product_id:
for key in MEMBERSHIP_SIGNUP_SESSION_KEYS:
request.session.pop(key, None)
return response
return None
# Get (or create) the SO for the now-authenticated web user and
# add the pending membership product to it.
order = request.website.sale_get_order(force_create=True)
order._cart_update(product_id=int(product_id), add_qty=1)
# Keep the cart navbar badge in sync (the core sets this on its
# own /shop/cart/update_json path; we bypass that here).
request.session["website_sale_cart_quantity"] = order.cart_quantity
# Drop the markers: the membership flow is now complete.
for key in MEMBERSHIP_SIGNUP_SESSION_KEYS:
request.session.pop(key, None)
return request.redirect("/shop/cart")