website_membership_signup_required: accept terms & conditions in signup form. Redirect to cart
This commit is contained in:
parent
07b0e546c4
commit
ba52c72b78
6 changed files with 151 additions and 22 deletions
|
|
@ -1,8 +1,9 @@
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
# 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.auth_signup.controllers.main import AuthSignupHome
|
||||||
from odoo.addons.web.controllers.home import SIGN_UP_REQUEST_PARAMS
|
from odoo.addons.web.controllers.home import SIGN_UP_REQUEST_PARAMS
|
||||||
|
from odoo.exceptions import UserError
|
||||||
from odoo.http import request
|
from odoo.http import request
|
||||||
|
|
||||||
# partner_firstname exposes `firstname` / `lastname` on res.users and
|
# 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`.
|
# roundtrips them transparently through `get_auth_signup_qcontext`.
|
||||||
SIGN_UP_REQUEST_PARAMS.add("firstname")
|
SIGN_UP_REQUEST_PARAMS.add("firstname")
|
||||||
SIGN_UP_REQUEST_PARAMS.add("lastname")
|
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.
|
# Session keys used by the membership signup flow.
|
||||||
MEMBERSHIP_SIGNUP_SESSION_KEYS = ("membership_signup_active", "membership_signup_product_id")
|
MEMBERSHIP_SIGNUP_SESSION_KEYS = ("membership_signup_active", "membership_signup_product_id")
|
||||||
|
|
@ -19,6 +25,15 @@ class AuthSignupHomeMembership(AuthSignupHome):
|
||||||
|
|
||||||
def _prepare_signup_values(self, qcontext):
|
def _prepare_signup_values(self, qcontext):
|
||||||
values = super()._prepare_signup_values(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`
|
# partner_firstname: the signup form collects `firstname` / `lastname`
|
||||||
# separately. We inject both into the signup values so the resulting
|
# separately. We inject both into the signup values so the resulting
|
||||||
# partner/user has them populated.
|
# partner/user has them populated.
|
||||||
|
|
@ -53,22 +68,59 @@ class AuthSignupHomeMembership(AuthSignupHome):
|
||||||
@http.route()
|
@http.route()
|
||||||
def web_auth_signup(self, *args, **kw):
|
def web_auth_signup(self, *args, **kw):
|
||||||
response = super().web_auth_signup(*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:
|
if request.session.uid:
|
||||||
for key in MEMBERSHIP_SIGNUP_SESSION_KEYS:
|
# Successful signup: if this is a membership flow, add the
|
||||||
request.session.pop(key, None)
|
# 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
|
return response
|
||||||
|
|
||||||
@http.route()
|
@http.route()
|
||||||
def web_login(self, *args, **kw):
|
def web_login(self, *args, **kw):
|
||||||
response = super().web_login(*args, **kw)
|
response = super().web_login(*args, **kw)
|
||||||
# Same cleanup on explicit login (covers the "Already have an
|
# Same handling on explicit login: covers the "Already have an
|
||||||
# account?" branch from the signup page).
|
# 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:
|
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:
|
for key in MEMBERSHIP_SIGNUP_SESSION_KEYS:
|
||||||
request.session.pop(key, None)
|
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")
|
||||||
|
|
@ -26,6 +26,22 @@ msgstr "p. ej. García"
|
||||||
msgid "e.g. John"
|
msgid "e.g. John"
|
||||||
msgstr "p. ej. Juan"
|
msgstr "p. ej. Juan"
|
||||||
|
|
||||||
|
#. module: website_membership_signup_required
|
||||||
|
#: model:ir.ui.view,arch_db:website_membership_signup_required.auth_signup_terms_checkbox
|
||||||
|
msgid "I agree to the"
|
||||||
|
msgstr "Acepto los "
|
||||||
|
|
||||||
|
#. module: website_membership_signup_required
|
||||||
|
#: model:ir.ui.view,arch_db:website_membership_signup_required.auth_signup_terms_checkbox
|
||||||
|
msgid "terms & conditions"
|
||||||
|
msgstr "términos y condiciones"
|
||||||
|
|
||||||
|
#. module: website_membership_signup_required
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/website_membership_signup_required/controllers/main.py:0
|
||||||
|
msgid "You must accept the terms and conditions."
|
||||||
|
msgstr "Debes aceptar los términos y condiciones."
|
||||||
|
|
||||||
#. module: website_membership_signup_required
|
#. module: website_membership_signup_required
|
||||||
#: model:ir.ui.view,arch_db:website_membership_signup_required.auth_signup_fields_firstname_lastname
|
#: model:ir.ui.view,arch_db:website_membership_signup_required.auth_signup_fields_firstname_lastname
|
||||||
msgid "First name"
|
msgid "First name"
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,22 @@ msgstr ""
|
||||||
msgid "e.g. John"
|
msgid "e.g. John"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: website_membership_signup_required
|
||||||
|
#: model:ir.ui.view,arch_db:website_membership_signup_required.auth_signup_terms_checkbox
|
||||||
|
msgid "I agree to the"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: website_membership_signup_required
|
||||||
|
#: model:ir.ui.view,arch_db:website_membership_signup_required.auth_signup_terms_checkbox
|
||||||
|
msgid "terms & conditions"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: website_membership_signup_required
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/website_membership_signup_required/controllers/main.py:0
|
||||||
|
msgid "You must accept the terms and conditions."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#. module: website_membership_signup_required
|
#. module: website_membership_signup_required
|
||||||
#: model:ir.ui.view,arch_db:website_membership_signup_required.auth_signup_fields_firstname_lastname
|
#: model:ir.ui.view,arch_db:website_membership_signup_required.auth_signup_fields_firstname_lastname
|
||||||
msgid "First name"
|
msgid "First name"
|
||||||
|
|
|
||||||
|
|
@ -59,23 +59,22 @@ registry.category("web_tour.tours").add("website_membership_signup_required_tour
|
||||||
run: "edit TourMember1!",
|
run: "edit TourMember1!",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
content: "Submit the signup form -> redirect back to product page",
|
content: "Accept the terms & conditions checkbox",
|
||||||
|
trigger: ".oe_signup_form input[name='accepted_terms']",
|
||||||
|
run: "click",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: "Submit the signup form -> redirect straight to cart "
|
||||||
|
+ "with the product already added (post-login auto-add)",
|
||||||
trigger: ".oe_signup_form button[type='submit']",
|
trigger: ".oe_signup_form button[type='submit']",
|
||||||
run: "click",
|
run: "click",
|
||||||
expectUnloadPage: true,
|
expectUnloadPage: true,
|
||||||
},
|
},
|
||||||
// -- Back on the product page, now authenticated --------------------
|
// -- Lands directly on /shop/cart with the membership line ---------
|
||||||
{
|
{
|
||||||
content: "We're back on the membership product page (authenticated)",
|
content: "We land on /shop/cart with the membership product",
|
||||||
trigger: `#product_details h1:contains("${PRODUCT_NAME}")`,
|
trigger: `#cart_products td.t-w-employee a:text("${PRODUCT_NAME}")`,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
content: "Add to cart as authenticated user",
|
|
||||||
trigger: "#add_to_cart",
|
|
||||||
run: "click",
|
|
||||||
expectUnloadPage: true,
|
|
||||||
},
|
|
||||||
tourUtils.goToCart(),
|
|
||||||
tourUtils.assertCartContains({ productName: PRODUCT_NAME }),
|
tourUtils.assertCartContains({ productName: PRODUCT_NAME }),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
import werkzeug.datastructures
|
import werkzeug.datastructures
|
||||||
|
|
||||||
|
from odoo.exceptions import UserError
|
||||||
from odoo.fields import Date
|
from odoo.fields import Date
|
||||||
|
|
||||||
from odoo.addons.website_membership_signup_required.controllers.main import (
|
from odoo.addons.website_membership_signup_required.controllers.main import (
|
||||||
|
|
@ -117,6 +118,7 @@ class TestMembershipSignup(TransactionCase):
|
||||||
"confirm_password": "verystrongpwd",
|
"confirm_password": "verystrongpwd",
|
||||||
"firstname": "John",
|
"firstname": "John",
|
||||||
"lastname": "Doe",
|
"lastname": "Doe",
|
||||||
|
"accepted_terms": "accepted",
|
||||||
}
|
}
|
||||||
with MockRequest(self.env, website=self.website):
|
with MockRequest(self.env, website=self.website):
|
||||||
values = controller._prepare_signup_values(qcontext)
|
values = controller._prepare_signup_values(qcontext)
|
||||||
|
|
@ -131,6 +133,34 @@ class TestMembershipSignup(TransactionCase):
|
||||||
self.assertIn("John", values["name"])
|
self.assertIn("John", values["name"])
|
||||||
self.assertIn("Doe", values["name"])
|
self.assertIn("Doe", values["name"])
|
||||||
|
|
||||||
|
def test_07b_prepare_signup_values_reject_without_terms(self):
|
||||||
|
"""Signup must be rejected if the legal terms checkbox is not
|
||||||
|
checked (`accepted_terms` missing or not 'accepted').
|
||||||
|
"""
|
||||||
|
import unittest
|
||||||
|
controller = AuthSignupHome()
|
||||||
|
base_qcontext = {
|
||||||
|
"login": "no.terms@example.com",
|
||||||
|
"password": "verystrongpwd",
|
||||||
|
"confirm_password": "verystrongpwd",
|
||||||
|
"firstname": "No",
|
||||||
|
"lastname": "Terms",
|
||||||
|
}
|
||||||
|
# Missing the key entirely.
|
||||||
|
with MockRequest(self.env, website=self.website):
|
||||||
|
with self.assertRaises(UserError):
|
||||||
|
controller._prepare_signup_values(dict(base_qcontext))
|
||||||
|
# Present but not 'accepted'.
|
||||||
|
qcontext = dict(base_qcontext, accepted_terms="")
|
||||||
|
with MockRequest(self.env, website=self.website):
|
||||||
|
with self.assertRaises(UserError):
|
||||||
|
controller._prepare_signup_values(qcontext)
|
||||||
|
# Present and 'accepted' -> does not raise.
|
||||||
|
qcontext = dict(base_qcontext, accepted_terms="accepted")
|
||||||
|
with MockRequest(self.env, website=self.website):
|
||||||
|
values = controller._prepare_signup_values(qcontext)
|
||||||
|
self.assertEqual(values.get("firstname"), "No")
|
||||||
|
|
||||||
# -- RF-08 -------------------------------------------------------------
|
# -- RF-08 -------------------------------------------------------------
|
||||||
|
|
||||||
def test_08_backend_sale_order_with_membership_product_without_user(self):
|
def test_08_backend_sale_order_with_membership_product_without_user(self):
|
||||||
|
|
|
||||||
|
|
@ -21,4 +21,20 @@
|
||||||
</xpath>
|
</xpath>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template id="auth_signup_terms_checkbox" name="Auth Signup legal terms checkbox"
|
||||||
|
inherit_id="auth_signup.signup">
|
||||||
|
<xpath expr="//div[hasclass('oe_login_buttons')]" position="before">
|
||||||
|
<div class="mb-3 field-accepted_terms">
|
||||||
|
<div class="form-check">
|
||||||
|
<input type="checkbox" name="accepted_terms" id="accepted_terms"
|
||||||
|
value="accepted" class="form-check-input"
|
||||||
|
required="required"/>
|
||||||
|
<label for="accepted_terms" class="form-check-label">
|
||||||
|
I agree to the <a href="/terms" target="_blank">terms & conditions</a>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</xpath>
|
||||||
|
</template>
|
||||||
|
|
||||||
</odoo>
|
</odoo>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue