Needed for the lint job of the CI to go green. Mostly automatic hook fixes (black, isort, prettier, pyupgrade, autoflake, whitespace). By hand: - flake8: two unused imports, an ambiguous variable name `l`. - pylint-odoo mandatory: manifest keys equal to their default value (installable, application, data, external_dependencies). - end-of-file-fixer skips *.patch too, like trailing-whitespace: the upstream patches stay as git format-patch wrote them. No behaviour change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
166 lines
6.7 KiB
Python
166 lines
6.7 KiB
Python
# Copyright 2026 - Today Criptomart
|
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
|
|
|
import logging
|
|
from urllib.parse import urlencode
|
|
|
|
from werkzeug.exceptions import NotFound
|
|
|
|
from odoo import http
|
|
from odoo.http import request
|
|
|
|
from odoo.addons.website_sale.controllers.main import WebsiteSale
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
LAZY_LOADING_ROUTE = "/shop/lazy_products"
|
|
GRID_ITEMS_TEMPLATE = "website_sale_lazy_loading.products_grid_items"
|
|
|
|
|
|
class WebsiteSaleLazyLoading(WebsiteSale):
|
|
"""Serve the /shop listing page by page over AJAX.
|
|
|
|
The standard ``/shop`` rendering is untouched: it still answers with the
|
|
first page and with its pager, so visitors without JavaScript (and search
|
|
engine crawlers) keep a fully navigable listing. What this controller adds
|
|
is a second entry point returning the very same product cards as an HTML
|
|
fragment, which the frontend appends to the grid already on screen.
|
|
"""
|
|
|
|
# ------------------------------------------------------------------
|
|
# Values for the /shop page
|
|
# ------------------------------------------------------------------
|
|
|
|
def _get_additional_extra_shop_values(self, values, **post):
|
|
"""Feed the lazy loading block of the ``website_sale.products`` page."""
|
|
res = super()._get_additional_extra_shop_values(values, **post)
|
|
res.update(self._prepare_lazy_loading_values(values))
|
|
return res
|
|
|
|
def _prepare_lazy_loading_values(self, values):
|
|
"""Return what the template needs to hand over to the frontend.
|
|
|
|
Everything is precomputed here (including the ``data-*`` payload) so
|
|
the template only reads plain values and the JavaScript side only
|
|
appends what the server sends back.
|
|
"""
|
|
pager = values.get("pager") or {}
|
|
page = pager.get("page", {}).get("num", 1)
|
|
page_count = pager.get("page_count", 1)
|
|
mode = request.website.shop_lazy_loading or "off"
|
|
# Nothing to lazy load on the last page: the block would only add an
|
|
# observer and a button that can never fetch anything.
|
|
if mode == "off" or page >= page_count:
|
|
return {"lazy_loading_active": False}
|
|
return {
|
|
"lazy_loading_active": True,
|
|
"lazy_loading_mode": mode,
|
|
"lazy_loading_url": self._get_lazy_loading_url(values),
|
|
"lazy_loading_page": page,
|
|
"lazy_loading_page_count": page_count,
|
|
}
|
|
|
|
def _get_lazy_loading_url(self, values):
|
|
"""Return the AJAX URL carrying the filters of the current listing.
|
|
|
|
The query string of the current request already holds the search
|
|
terms, the attribute values, the tags, the price range and the sort
|
|
order; only the category travels in the path, so it is added back as a
|
|
parameter. The page number is dropped: the frontend sets it.
|
|
"""
|
|
params = request.httprequest.args.to_dict(flat=False)
|
|
params.pop("page", None)
|
|
category = values.get("category")
|
|
if category:
|
|
params["category"] = [str(category.id)]
|
|
query = urlencode(params, doseq=True)
|
|
if not query:
|
|
return LAZY_LOADING_ROUTE
|
|
return f"{LAZY_LOADING_ROUTE}?{query}"
|
|
|
|
# ------------------------------------------------------------------
|
|
# AJAX endpoint
|
|
# ------------------------------------------------------------------
|
|
|
|
@http.route(
|
|
[LAZY_LOADING_ROUTE],
|
|
type="http",
|
|
auth="public",
|
|
website=True,
|
|
methods=["GET"],
|
|
sitemap=False,
|
|
)
|
|
def shop_lazy_products(self, page=1, category=None, **post):
|
|
"""Render one page of the shop listing as an HTML fragment.
|
|
|
|
:return: JSON with the ``html`` of the product cards, the ``page`` it
|
|
belongs to and whether a further page (``has_next``) exists.
|
|
"""
|
|
page = self._get_lazy_loading_page(page)
|
|
if category is not None and not str(category).isdigit():
|
|
# The category travels as a bare id; anything else is a broken URL
|
|
# rather than a category the visitor could have picked.
|
|
category = None
|
|
try:
|
|
response = self.shop(page=page, category=category, **post)
|
|
except NotFound:
|
|
return request.make_json_response(
|
|
{"error": "not_found", "html": "", "has_next": False}, status=404
|
|
)
|
|
values = getattr(response, "qcontext", None) or {}
|
|
if not getattr(response, "is_qweb", False) or "pager" not in values:
|
|
# /shop answered something else than the listing, e.g. the login
|
|
# redirect of a website whose eCommerce is restricted.
|
|
_logger.info("[LAZY_LOADING] /shop did not render the product listing")
|
|
return request.make_json_response(
|
|
{"error": "unavailable", "html": "", "has_next": False}, status=403
|
|
)
|
|
pager = values["pager"]
|
|
page_count = pager["page_count"]
|
|
if pager["page"]["num"] != page:
|
|
# Out of range: the pager clamps to the last page, and sending it
|
|
# again would duplicate the cards already on screen.
|
|
return request.make_json_response(
|
|
{"html": "", "page": page, "page_count": page_count, "has_next": False}
|
|
)
|
|
values = dict(values, **self._prepare_lazy_grid_values(values))
|
|
html = request.env["ir.ui.view"]._render_template(GRID_ITEMS_TEMPLATE, values)
|
|
_logger.debug(
|
|
"[LAZY_LOADING] page %s/%s rendered with %s products",
|
|
page,
|
|
page_count,
|
|
len(values["products"]),
|
|
)
|
|
return request.make_json_response(
|
|
{
|
|
"html": html,
|
|
"page": page,
|
|
"page_count": page_count,
|
|
"has_next": page < page_count,
|
|
}
|
|
)
|
|
|
|
def _get_lazy_loading_page(self, page):
|
|
"""Return ``page`` as a page number, defaulting to the first page."""
|
|
try:
|
|
page = int(page)
|
|
except (TypeError, ValueError):
|
|
return 1
|
|
return max(page, 1)
|
|
|
|
def _prepare_lazy_grid_values(self, values):
|
|
"""Complete the shop values with what the grid layout needs.
|
|
|
|
``website_sale.products`` computes these on the fly right before the
|
|
grid; the fragment is rendered on its own, so they are rebuilt here
|
|
from the same shop options.
|
|
"""
|
|
website = request.website
|
|
has_left_column = website.is_view_active(
|
|
"website_sale.products_categories"
|
|
) or website.is_view_active("website_sale.products_attributes")
|
|
return {
|
|
"grid_md_allow_custom_cols": has_left_column,
|
|
"grid_md_use_3col": not has_left_column and values.get("ppr") == 4,
|
|
"product_block_name": "Product",
|
|
}
|