add website_membership_signup_required: force to create user before add to cart a membership product
This commit is contained in:
parent
80f24bcc46
commit
c4d32c6add
22 changed files with 1202 additions and 0 deletions
|
|
@ -0,0 +1,4 @@
|
|||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from . import main
|
||||
from . import website_sale
|
||||
74
website_membership_signup_required/controllers/main.py
Normal file
74
website_membership_signup_required/controllers/main.py
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
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.http import request
|
||||
|
||||
# partner_firstname exposes `firstname` / `lastname` on res.users and
|
||||
# res.partner; we need them as accepted qcontext params so the signup form
|
||||
# roundtrips them transparently through `get_auth_signup_qcontext`.
|
||||
SIGN_UP_REQUEST_PARAMS.add("firstname")
|
||||
SIGN_UP_REQUEST_PARAMS.add("lastname")
|
||||
|
||||
# Session keys used by the membership signup flow.
|
||||
MEMBERSHIP_SIGNUP_SESSION_KEYS = ("membership_signup_active", "membership_signup_product_id")
|
||||
|
||||
|
||||
class AuthSignupHomeMembership(AuthSignupHome):
|
||||
|
||||
def _prepare_signup_values(self, qcontext):
|
||||
values = super()._prepare_signup_values(qcontext)
|
||||
# partner_firstname: the signup form collects `firstname` / `lastname`
|
||||
# separately. We inject both into the signup values so the resulting
|
||||
# partner/user has them populated.
|
||||
#
|
||||
# We ALSO keep a composed `name` (firstname + " " + lastname) in the
|
||||
# values, on purpose. The core's `_create_user_from_template`
|
||||
# (`auth_signup/models/res_users.py`) rejects the signup with
|
||||
# `ValueError: Signup: no name or partner given for new user` if
|
||||
# neither `partner_id` nor `name` is present in the values — that
|
||||
# check runs BEFORE `template_user.copy(values)` reaches
|
||||
# `res.partner.create`, where `partner_firstname` would otherwise
|
||||
# recompute `name` from the split fields.
|
||||
#
|
||||
# By providing the composed `name`, the check passes; then, inside
|
||||
# `res.partner.create`, partner_firstname's override detects that both
|
||||
# `firstname` and `lastname` are in the vals and DISCARDS the composed
|
||||
# `name` (see `partner_firstname/models/res_partner.py:_name_fields_in_vals`
|
||||
# branch `del vals["name"]`), recomputing it from the split fields.
|
||||
# Net effect: the partner ends up with the proper `firstname`,
|
||||
# `lastname` and a recomputed `name`, exactly as intended.
|
||||
firstname = qcontext.get("firstname", "")
|
||||
lastname = qcontext.get("lastname", "")
|
||||
if firstname or lastname:
|
||||
values["firstname"] = firstname
|
||||
values["lastname"] = lastname
|
||||
partner_model = request.env["res.partner"]
|
||||
values["name"] = partner_model._get_computed_name(
|
||||
lastname, firstname
|
||||
)
|
||||
return values
|
||||
|
||||
@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)
|
||||
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).
|
||||
if request.session.uid:
|
||||
for key in MEMBERSHIP_SIGNUP_SESSION_KEYS:
|
||||
request.session.pop(key, None)
|
||||
return response
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from odoo import http
|
||||
from odoo.addons.website_sale.controllers.main import WebsiteSale
|
||||
from odoo.http import request
|
||||
|
||||
SIGNUP_FLAG = "membership_signup=1"
|
||||
|
||||
|
||||
class WebsiteSaleMembershipSignup(WebsiteSale):
|
||||
|
||||
def _membership_signup_is_required(self, product_id):
|
||||
"""Return True iff an anonymous visitor adding `product_id` to the
|
||||
cart must be redirected to /web/signup first.
|
||||
"""
|
||||
website = request.website
|
||||
return website._membership_signup_required_for(
|
||||
product_id, request.env.user._is_public()
|
||||
)
|
||||
|
||||
def _membership_signup_redirect_url(self, product_id):
|
||||
"""Build the `/web/signup?membership_signup=1&redirect=<product_url>`
|
||||
URL preserving the current language.
|
||||
"""
|
||||
product = request.env["product.product"].browse(int(product_id))
|
||||
tmpl = product.product_tmpl_id
|
||||
product_url = "/shop/%s" % request.env["ir.http"]._slug(tmpl)
|
||||
query = urlencode({"membership_signup": "1", "redirect": product_url})
|
||||
return "/web/signup?%s" % query
|
||||
|
||||
def _membership_signup_mark_session(self, product_id):
|
||||
request.session["membership_signup_active"] = True
|
||||
request.session["membership_signup_product_id"] = int(product_id)
|
||||
|
||||
@http.route()
|
||||
def cart_update(
|
||||
self, product_id, add_qty=1, set_qty=0,
|
||||
product_custom_attribute_values=None,
|
||||
no_variant_attribute_value_ids=None, **kwargs
|
||||
):
|
||||
if self._membership_signup_is_required(product_id):
|
||||
self._membership_signup_mark_session(product_id)
|
||||
return request.redirect(self._membership_signup_redirect_url(product_id))
|
||||
return super().cart_update(
|
||||
product_id=product_id, add_qty=add_qty, set_qty=set_qty,
|
||||
product_custom_attribute_values=product_custom_attribute_values,
|
||||
no_variant_attribute_value_ids=no_variant_attribute_value_ids,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
@http.route()
|
||||
def cart_update_json(
|
||||
self, product_id, line_id=None, add_qty=None, set_qty=None, display=True,
|
||||
product_custom_attribute_values=None, no_variant_attribute_value_ids=None, **kwargs
|
||||
):
|
||||
if self._membership_signup_is_required(product_id):
|
||||
self._membership_signup_mark_session(product_id)
|
||||
# The JSON route cannot perform an HTTP redirect on its own; signal
|
||||
# the frontend so it can navigate to the signup URL. The standard
|
||||
# "Add to Cart" on a product page uses /shop/cart/update (http),
|
||||
# which is the primary path for this flow (RF-01). The JSON route
|
||||
# covers add-to-cart from snippets/wishlist on the same page
|
||||
# (`stayOnPageOption` / `o_we_buy_now` express).
|
||||
#
|
||||
# The JS handler in
|
||||
# `static/src/js/website_membership_signup_required.js` reads
|
||||
# `data.force_redirect` and navigates to it. The other keys are
|
||||
# NOT consumed by our handler: they exist to keep the core's
|
||||
# `cartHandlerMixin._addToCartInPage` (in
|
||||
# `website_sale/static/src/js/website_sale_utils.js`) from crashing.
|
||||
# That method does, in order:
|
||||
# if (data.cart_quantity && ...) updateCartNavBar(data);
|
||||
# showCartNotification(call, data.notification_info);
|
||||
# `showCartNotification` accesses `props.lines` and `props.warning`
|
||||
# on `data.notification_info`; if it is `undefined` (as it was in
|
||||
# the first iteration, which only returned `{force_redirect: ...}`)
|
||||
# the access raises `TypeError: can't access property "lines",
|
||||
# props is undefined` and the redirect never fires.
|
||||
#
|
||||
# Shape (mirrors the empty-cart branch in
|
||||
# `WebsiteSaleController.cart_update_json`):
|
||||
# cart_quantity = 0 -> falsy, so `updateCartNavBar` is skipped.
|
||||
# notification_info = {lines: [], warning: ""}
|
||||
# -> `showCartNotification` no-ops on both empty fields.
|
||||
return {
|
||||
"force_redirect": self._membership_signup_redirect_url(product_id),
|
||||
"cart_quantity": 0,
|
||||
"notification_info": {"lines": [], "warning": ""},
|
||||
}
|
||||
return super().cart_update_json(
|
||||
product_id=product_id, line_id=line_id, add_qty=add_qty,
|
||||
set_qty=set_qty, display=display,
|
||||
product_custom_attribute_values=product_custom_attribute_values,
|
||||
no_variant_attribute_value_ids=no_variant_attribute_value_ids,
|
||||
**kwargs
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue