From 23edee615488feb965ec24acf7a0b3810baeed04 Mon Sep 17 00:00:00 2001 From: GitHub Copilot Date: Tue, 11 Aug 2026 16:49:54 +0200 Subject: [PATCH] [IMP] website_sale_aplicoop: readable slug in the group order URL Group order pages were published under their database id (`/eskaera/1`), which says nothing to the member opening the link. `group.order` gains a `slug` field and the public pages move to `/eskaera/` (e.g. `/eskaera/escola-fructuos`). The slug is generated from the order name on creation, is unique, and can be edited under the name on the form; emptying it regenerates it from the current name. Values that would make the order unreachable are rejected: a plain number (the legacy numeric URLs win that match) and the static routes served under `/eskaera/` (`labels`, `save-order`, `i18n`, ...). `/eskaera/` and `/eskaera//checkout` are kept as redirects to their slug URL, so the links already shared with members keep working. The AJAX endpoints (`load-page`, `save-order`, `confirm`, ...) stay numeric: they never show up in the address bar. Consequently the frontend now reads the order id from the `data-order-id` attribute only, as the URL no longer carries it. The post-migration script fills the slug of the orders that already existed. Renaming the `/eskaera` prefix itself (`/escolas` for a schools deployment) needs no code: a *308 Redirect / Rewrite* rule per public route in Website > Configuration > Redirects serves the pages on the new prefix, rewrites the links in the templates and redirects the old URLs, per website. Documented in `readme/CONFIGURE.rst`. Co-Authored-By: Claude Opus 5 --- website_sale_aplicoop/CHANGELOG.md | 20 +++ website_sale_aplicoop/README.rst | 7 + website_sale_aplicoop/__manifest__.py | 2 +- .../controllers/website_sale.py | 72 +++++++- .../migrations/18.0.1.13.0/post-migrate.py | 30 ++++ website_sale_aplicoop/models/group_order.py | 131 +++++++++++++++ website_sale_aplicoop/readme/CONFIGURE.rst | 34 ++++ .../static/src/js/home_delivery.js | 8 +- .../static/src/js/website_sale.js | 9 +- website_sale_aplicoop/tests/__init__.py | 1 + .../tests/test_group_order_slug.py | 158 ++++++++++++++++++ .../views/group_order_views.xml | 6 + .../views/load_from_history_templates.xml | 3 +- .../views/website_templates.xml | 10 +- 14 files changed, 463 insertions(+), 28 deletions(-) create mode 100644 website_sale_aplicoop/migrations/18.0.1.13.0/post-migrate.py create mode 100644 website_sale_aplicoop/tests/test_group_order_slug.py diff --git a/website_sale_aplicoop/CHANGELOG.md b/website_sale_aplicoop/CHANGELOG.md index 1e1e3fe..cba0818 100644 --- a/website_sale_aplicoop/CHANGELOG.md +++ b/website_sale_aplicoop/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog - Website Sale Aplicoop +## [18.0.1.13.0] - 2026-08-11 + +### Added + +- **Readable URLs for group orders**: `group.order` gains a `slug` field and the + public pages move from `/eskaera/` to `/eskaera/` (e.g. + `/eskaera/escola-fructuos`). The slug is generated from the order name on + creation, is editable on the form (empty it to regenerate it from the name), + is unique, and rejects values that would collide with the shop's own routes + (`labels`, `save-order`, …) or with a plain number. A post-migration script + fills the slug of the orders that already existed. + +### Changed + +- `/eskaera/` and `/eskaera//checkout` are kept as redirects to their + slug URL, so links already shared with members keep working. The AJAX + endpoints (`/eskaera//load-page`, `/eskaera/save-order`, …) stay numeric. +- The frontend reads the order id from the `data-order-id` attribute only; it + can no longer be recovered from the URL, which now carries the slug. + ## [18.0.1.12.0] - 2026-08-06 ### Removed diff --git a/website_sale_aplicoop/README.rst b/website_sale_aplicoop/README.rst index a73a6a3..3a39b35 100644 --- a/website_sale_aplicoop/README.rst +++ b/website_sale_aplicoop/README.rst @@ -95,6 +95,13 @@ Configuration 2. Set pricing and availability per group order 3. Assign products to categories used in group orders +**Public URLs** + +1. Each group order is published under a readable slug: ``/eskaera/`` +2. The slug is generated from the order name and can be edited on the form (empty it to regenerate it) +3. Links to ``/eskaera/`` still work and redirect to the slug URL +4. The ``/eskaera`` prefix can be renamed per website (e.g. ``/escolas``) with a **308 Redirect / Rewrite** rule in Website > Configuration > Redirects — see ``readme/CONFIGURE.rst`` + **Date & Time Validation** - ``start_date`` must be ≤ ``end_date`` (when both filled) diff --git a/website_sale_aplicoop/__manifest__.py b/website_sale_aplicoop/__manifest__.py index 5baedf4..d46ed47 100644 --- a/website_sale_aplicoop/__manifest__.py +++ b/website_sale_aplicoop/__manifest__.py @@ -3,7 +3,7 @@ { # noqa: B018 "name": "Website Sale - Aplicoop", - "version": "18.0.1.12.0", + "version": "18.0.1.13.0", "category": "Website/Sale", "summary": "Modern replacement of legacy Aplicoop - Collaborative consumption group orders", "author": "Odoo Community Association (OCA), Criptomart", diff --git a/website_sale_aplicoop/controllers/website_sale.py b/website_sale_aplicoop/controllers/website_sale.py index 8de6551..2111a04 100644 --- a/website_sale_aplicoop/controllers/website_sale.py +++ b/website_sale_aplicoop/controllers/website_sale.py @@ -3,6 +3,7 @@ import json import logging +from urllib.parse import urlencode from odoo import fields from odoo import http @@ -703,18 +704,52 @@ class AplicoopWebsiteSale(WebsiteSale): request, ) + def _get_group_order_by_slug(self, group_order_slug): + """Return the consumer group order published under `group_order_slug`.""" + return ( + request.env["group.order"] + .sudo() + .search([("slug", "=", group_order_slug)], limit=1) + ) + + def _eskaera_url(self, group_order, suffix=""): + """Return the public URL of `group_order`, falling back to the list.""" + if not group_order or not group_order.slug: + return "/eskaera" + return f"/eskaera/{group_order.slug}{suffix}" + + def _redirect_to_slug_url(self, order_id, suffix="", **post): + """Send a legacy /eskaera/ URL to its /eskaera/ equivalent.""" + group_order = request.env["group.order"].sudo().browse(order_id).exists() + url = self._eskaera_url(group_order, suffix=suffix) + if post and url != "/eskaera": + url = f"{url}?{urlencode(post)}" + return request.redirect(url) + @http.route(["/eskaera/"], type="http", auth="user", website=True) - def eskaera_shop(self, order_id, **post): + def eskaera_shop_legacy(self, order_id, **post): + """Keep the old numeric shop URL working, pointing at the slug one.""" + return self._redirect_to_slug_url(order_id, **post) + + @http.route( + ["/eskaera/"], + type="http", + auth="user", + website=True, + ) + def eskaera_shop(self, group_order_slug, **post): """Página de tienda para un pedido específico (eskaera). Muestra productos del pedido y gestiona el carrito separado. Soporta búsqueda y filtrado por categoría. """ - group_order = request.env["group.order"].sudo().browse(order_id) + group_order = self._get_group_order_by_slug(group_order_slug) - if not group_order.exists(): + if not group_order: return request.redirect("/eskaera") + order_id = group_order.id + # Verificar que el pedido está activo if group_order.state != "open": return request.redirect("/eskaera") @@ -1259,11 +1294,21 @@ class AplicoopWebsiteSale(WebsiteSale): @http.route( ["/eskaera//checkout"], type="http", auth="user", website=True ) - def eskaera_checkout(self, order_id, **post): - """Checkout page to close the cart for the order (eskaera).""" - group_order = request.env["group.order"].sudo().browse(order_id) + def eskaera_checkout_legacy(self, order_id, **post): + """Keep the old numeric checkout URL working, pointing at the slug one.""" + return self._redirect_to_slug_url(order_id, suffix="/checkout", **post) - if not group_order.exists(): + @http.route( + ["/eskaera//checkout"], + type="http", + auth="user", + website=True, + ) + def eskaera_checkout(self, group_order_slug, **post): + """Checkout page to close the cart for the order (eskaera).""" + group_order = self._get_group_order_by_slug(group_order_slug) + + if not group_order: return request.redirect("/eskaera") # Verificar que el pedido está activo @@ -2125,7 +2170,9 @@ class AplicoopWebsiteSale(WebsiteSale): # Verify the order belongs to the requested group_order if sale_order.group_order_id.id != group_order_id: - return request.redirect("/eskaera/%d" % sale_order.group_order_id.id) + return request.redirect( + self._eskaera_url(sale_order.group_order_id.sudo()) + ) # Get the current group_order (the one being viewed, not necessarily the one from the history) group_order = request.env["group.order"].sudo().browse(group_order_id) @@ -2198,6 +2245,9 @@ class AplicoopWebsiteSale(WebsiteSale): "website_sale_aplicoop.eskaera_load_from_history", { "group_order_id": group_order_id, + # sessionStorage keys stay keyed by id; the redirect at + # the end of the template needs the public URL instead. + "group_order_url": self._eskaera_url(group_order), "items_json": json.dumps( available_items ), # Pass ONLY available items @@ -2224,7 +2274,11 @@ class AplicoopWebsiteSale(WebsiteSale): import traceback _logger.error(traceback.format_exc()) - return request.redirect("/eskaera/%d" % group_order_id) + return request.redirect( + self._eskaera_url( + request.env["group.order"].sudo().browse(group_order_id).exists() + ) + ) @http.route( ["/eskaera//confirm/"], diff --git a/website_sale_aplicoop/migrations/18.0.1.13.0/post-migrate.py b/website_sale_aplicoop/migrations/18.0.1.13.0/post-migrate.py new file mode 100644 index 0000000..e29ebf1 --- /dev/null +++ b/website_sale_aplicoop/migrations/18.0.1.13.0/post-migrate.py @@ -0,0 +1,30 @@ +"""Backfill the URL slug of consumer group orders created before this version. + +Public pages moved from /eskaera/ to /eskaera/. Orders that already +existed have no slug yet, so they would not be reachable until saved again. +Generate one from their name, keeping it unique. +""" + +import logging + +from odoo import SUPERUSER_ID +from odoo import api + +_logger = logging.getLogger(__name__) + + +def migrate(cr, version): + if not version: + return + + env = api.Environment(cr, SUPERUSER_ID, {}) + orders = env["group.order"].search([("slug", "=", False)], order="id") + for order in orders: + order.slug = order._generate_unique_slug(order.name) + + if orders: + _logger.info( + "Generated URL slugs for %d consumer group orders: %s", + len(orders), + ", ".join(orders.mapped("slug")), + ) diff --git a/website_sale_aplicoop/models/group_order.py b/website_sale_aplicoop/models/group_order.py index ef81ddd..c21c31d 100644 --- a/website_sale_aplicoop/models/group_order.py +++ b/website_sale_aplicoop/models/group_order.py @@ -2,6 +2,7 @@ # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) import logging +import re from datetime import timedelta from dateutil.relativedelta import relativedelta @@ -13,6 +14,29 @@ from odoo.exceptions import ValidationError _logger = logging.getLogger(__name__) +# Lowercase words separated by single hyphens, the usual shape of a URL slug. +SLUG_PATTERN = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$") + +# Static single-segment routes served under /eskaera/. A slug equal to any of +# them would be shadowed by the static route and its order unreachable. +RESERVED_SLUGS = frozenset( + { + "add-to-cart", + "check-status", + "clear-cart", + "confirm", + "i18n", + "labels", + "load-draft", + "save-cart", + "save-order", + } +) + +# Fallback base used when the order name yields no usable slug (e.g. a name +# made only of punctuation or of characters that do not transliterate). +DEFAULT_SLUG_BASE = "eskaera" + class GroupOrder(models.Model): _name = "group.order" @@ -80,6 +104,14 @@ class GroupOrder(models.Model): translate=True, help="Display name of this consumer group order", ) + slug = fields.Char( + # No index=True: the uniqueness constraint already creates one. + copy=False, + tracking=True, + help="Readable identifier used in the public URL of this consumer " + "group order (/eskaera/). Generated from the name when left " + "empty; changing it breaks the links already shared with members.", + ) group_ids = fields.Many2many( "res.partner", "group_order_group_rel", @@ -310,6 +342,105 @@ class GroupOrder(models.Model): self.env._("Start date cannot be greater than end date") ) + # === URL slug === + + _sql_constraints = [ + ( + "slug_uniq", + "unique(slug)", + "Another consumer group order already uses this URL slug. " + "Slugs identify orders in the public URL, so they must be unique.", + ), + ] + + @api.constrains("slug") + def _check_slug(self): + """Reject slugs that are malformed or that a route already takes.""" + for record in self: + slug = record.slug + if not slug: + continue + if not SLUG_PATTERN.match(slug): + raise ValidationError( + self.env._( + "'%(slug)s' is not a valid URL slug. Use lowercase " + "letters, digits and single hyphens, for example " + "'weekly-order'.", + slug=slug, + ) + ) + if slug.isdigit(): + raise ValidationError( + self.env._( + "The URL slug cannot be a number: '%(slug)s' would " + "collide with the legacy /eskaera/ URLs.", + slug=slug, + ) + ) + if slug in RESERVED_SLUGS: + raise ValidationError( + self.env._( + "'%(slug)s' is reserved by the shop itself, the order " + "would not be reachable. Choose another URL slug.", + slug=slug, + ) + ) + + @api.model + def _normalize_slug(self, value): + """Turn any text into a URL friendly slug (may return an empty string).""" + return self.env["ir.http"]._slugify(value or "") + + def _is_slug_taken(self, slug): + """Return whether another consumer group order already uses `slug`.""" + domain = [("slug", "=", slug)] + if self.ids: + domain.append(("id", "not in", self.ids)) + return bool(self.sudo().search_count(domain, limit=1)) + + def _generate_unique_slug(self, name): + """Build a free slug out of `name`, adding a counter when needed.""" + base = self._normalize_slug(name) + if not base or base.isdigit() or base in RESERVED_SLUGS: + base = f"{base}-{DEFAULT_SLUG_BASE}" if base else DEFAULT_SLUG_BASE + candidate = base + counter = 2 + while self._is_slug_taken(candidate): + candidate = f"{base}-{counter}" + counter += 1 + return candidate + + @api.model_create_multi + def create(self, vals_list): + """Give every consumer group order a slug for its public URL.""" + vals_list = [ + dict( + vals, + slug=( + self._normalize_slug(vals.get("slug")) + or self._generate_unique_slug(vals.get("name")) + ), + ) + for vals in vals_list + ] + return super().create(vals_list) + + def write(self, vals): + """Normalize the slug, regenerating it from the name when emptied.""" + if "slug" not in vals: + return super().write(vals) + slug = self._normalize_slug(vals["slug"]) + if slug: + return super().write(dict(vals, slug=slug)) + # Emptying the slug rebuilds it from the name: each record needs its + # own value, so they cannot be written in one go. + for record in self: + name = vals.get("name") or record.name + super(GroupOrder, record).write( + dict(vals, slug=record._generate_unique_slug(name)) + ) + return True + def action_open(self): """Open order for purchases.""" self.write({"state": "open"}) diff --git a/website_sale_aplicoop/readme/CONFIGURE.rst b/website_sale_aplicoop/readme/CONFIGURE.rst index a5758c4..77f0e33 100644 --- a/website_sale_aplicoop/readme/CONFIGURE.rst +++ b/website_sale_aplicoop/readme/CONFIGURE.rst @@ -24,3 +24,37 @@ To configure this module, you need to: #. Link products to categories used in group orders #. Configure pricing and taxes for products #. Set product availability per supplier + +**Public URL of a group order (v18.0.1.13.0+):** + +Every group order is published under a readable slug, ``/eskaera/`` +(for example ``/eskaera/escola-fructuos``), instead of its database id. + +#. Open the group order form: the slug is shown right under the name +#. Leave it as generated or type your own (lowercase, digits and hyphens) +#. Empty it and save to generate it again from the current name +#. Old ``/eskaera/`` links keep working: they redirect to the slug URL + +Changing the slug of an order that members already bookmarked breaks those +links, so pick it before publishing the order. + +**Renaming the /eskaera prefix per website:** + +The prefix can be changed without touching the code, per website, with +Odoo's own rewrite rules — useful when the site does not speak Basque +(``/escolas``, ``/pedidos``…): + +#. Go to Website → Configuration → Redirects and click New +#. Choose the action **308 Redirect / Rewrite** and the website to apply it to +#. Add one rule per public page, keeping the parameter untouched: + + * ``/eskaera`` → ``/escolas`` + * ``/eskaera/`` → ``/escolas/`` + * ``/eskaera//checkout`` → ``/escolas//checkout`` + +#. Update the URL of the website menu (Website → Site → Content → Menus) + +Odoo then serves the pages on the new prefix, rewrites the links in the +templates and redirects the old URLs. The AJAX endpoints +(``/eskaera/labels``, ``/eskaera/save-order``…) are never shown in the +address bar and do not need a rule. diff --git a/website_sale_aplicoop/static/src/js/home_delivery.js b/website_sale_aplicoop/static/src/js/home_delivery.js index d7d3d5a..2388e42 100644 --- a/website_sale_aplicoop/static/src/js/home_delivery.js +++ b/website_sale_aplicoop/static/src/js/home_delivery.js @@ -73,16 +73,12 @@ var cartContainer = document.getElementById("cart-items-container"); var orderIdElement = confirmBtn || cartContainer; + // The URL is not a fallback here: it carries the slug of the order, + // not its id. if (orderIdElement) { this.orderId = orderIdElement.getAttribute("data-order-id"); } - // If still not found, try to extract from URL - if (!this.orderId) { - var urlMatch = window.location.pathname.match(/\/eskaera\/(\d+)/); - this.orderId = urlMatch ? urlMatch[1] : null; - } - console.log("[HomeDelivery] orderId resolved:", this.orderId); // Handle checkbox (only exists on checkout page) diff --git a/website_sale_aplicoop/static/src/js/website_sale.js b/website_sale_aplicoop/static/src/js/website_sale.js index 1777c57..1cc0cee 100644 --- a/website_sale_aplicoop/static/src/js/website_sale.js +++ b/website_sale_aplicoop/static/src/js/website_sale.js @@ -27,12 +27,9 @@ return false; } - // Get the order ID from the data attribute or from the URL + // Get the order ID from the data attribute. It cannot be read from + // the URL: that one carries the slug of the order, not its id. this.orderId = orderIdElement.getAttribute("data-order-id"); - if (!this.orderId) { - var urlMatch = window.location.pathname.match(/\/eskaera\/(\d+)/); - this.orderId = urlMatch ? urlMatch[1] : null; - } if (!this.orderId) { console.error("Order ID not found"); @@ -757,7 +754,7 @@ // Map of href patterns to label keys var hrefPatterns = [ { pattern: /\/checkout$/, labelKey: "proceed_to_checkout" }, - { pattern: /\/eskaera\/\d+$/, labelKey: "back_to_cart" }, + { pattern: /\/eskaera\/[^/]+$/, labelKey: "back_to_cart" }, ]; // Find all elements with data-bs-toggle="tooltip" diff --git a/website_sale_aplicoop/tests/__init__.py b/website_sale_aplicoop/tests/__init__.py index 0ca620d..6ea162f 100644 --- a/website_sale_aplicoop/tests/__init__.py +++ b/website_sale_aplicoop/tests/__init__.py @@ -2,6 +2,7 @@ # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) from . import test_group_order # noqa: F401 +from . import test_group_order_slug # noqa: F401 from . import test_res_partner # noqa: F401 from . import test_product_extension # noqa: F401 from . import test_eskaera_shop # noqa: F401 diff --git a/website_sale_aplicoop/tests/test_group_order_slug.py b/website_sale_aplicoop/tests/test_group_order_slug.py new file mode 100644 index 0000000..63e463d --- /dev/null +++ b/website_sale_aplicoop/tests/test_group_order_slug.py @@ -0,0 +1,158 @@ +# Copyright 2026 Criptomart +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) + +from datetime import datetime +from datetime import timedelta + +from odoo.exceptions import ValidationError +from odoo.tests import tagged +from odoo.tests.common import HttpCase +from odoo.tests.common import TransactionCase + + +class GroupOrderSlugCommon: + """Helpers to build consumer group orders with the mandatory fields.""" + + def _create_group_order(self, name, **values): + start_date = datetime.now().date() + vals = { + "name": name, + "group_ids": [(6, 0, [self.group.id])], + "type": "regular", + "start_date": start_date, + "end_date": start_date + timedelta(days=7), + "period": "weekly", + "pickup_day": "3", + "cutoff_day": "0", + } + vals.update(values) + return self.env["group.order"].create(vals) + + +class TestGroupOrderSlug(GroupOrderSlugCommon, TransactionCase): + """URL slug generation, normalization and validation.""" + + def setUp(self): + super().setUp() + self.group = self.env["res.partner"].create( + { + "name": "Slug Test Group", + "is_company": True, + "is_group": True, + "email": "slug-group@test.com", + } + ) + + def test_slug_generated_from_name(self): + order = self._create_group_order("Escola Fructuós Gelabert") + self.assertEqual(order.slug, "escola-fructuos-gelabert") + + def test_slug_is_unique(self): + first = self._create_group_order("Weekly Order") + second = self._create_group_order("Weekly Order") + self.assertEqual(first.slug, "weekly-order") + self.assertEqual(second.slug, "weekly-order-2") + + def test_explicit_slug_is_normalized(self): + order = self._create_group_order("Weekly Order", slug="Fructuós Gelabert") + self.assertEqual(order.slug, "fructuos-gelabert") + + def test_slug_kept_when_name_changes(self): + order = self._create_group_order("Weekly Order") + order.name = "Renamed Order" + self.assertEqual(order.slug, "weekly-order") + + def test_emptying_slug_regenerates_it(self): + order = self._create_group_order("Weekly Order") + order.write({"name": "Renamed Order", "slug": False}) + self.assertEqual(order.slug, "renamed-order") + + def test_reserved_slug_is_avoided_on_generation(self): + order = self._create_group_order("Labels") + self.assertEqual(order.slug, "labels-eskaera") + + def test_reserved_slug_is_rejected(self): + with self.assertRaises(ValidationError): + self._create_group_order("Weekly Order", slug="save-order") + + def test_numeric_slug_is_rejected(self): + with self.assertRaises(ValidationError): + self._create_group_order("Weekly Order", slug="42") + + def test_name_without_slug_characters_falls_back(self): + order = self._create_group_order("!!!") + self.assertEqual(order.slug, "eskaera") + + +@tagged("post_install", "-at_install") +class TestGroupOrderSlugRoutes(GroupOrderSlugCommon, HttpCase): + """The shop pages answer on the slug URL, the numeric one redirects.""" + + def setUp(self): + super().setUp() + # is_group matters: both `res.partner.group_ids` and + # `group.order.group_ids` filter on it when they are read. + self.group = self.env["res.partner"].create( + { + "name": "Slug Routes Group", + "is_company": True, + "is_group": True, + "email": "slug-routes-group@test.com", + } + ) + member = self.env["res.partner"].create( + {"name": "Slug Routes Member", "email": "slug-routes-member@test.com"} + ) + self.group.member_ids = [(4, member.id)] + + login = "portal.slug.routes@test.com" + self.env["res.users"].create( + { + "name": "Slug Routes User", + "login": login, + "password": login, + "partner_id": member.id, + "groups_id": [(4, self.env.ref("base.group_portal").id)], + } + ) + self.portal_login = login + + self.group_order = self._create_group_order("Slug Routes Order") + self.group_order.action_open() + + # The HTTP requests below share this transaction but not the ORM cache: + # the many2many rows (group members, order groups) have to be in the + # database before the controller reads them. + self.env.flush_all() + + def test_slug_urls_answer(self): + self.authenticate(self.portal_login, self.portal_login) + for url in ( + f"/eskaera/{self.group_order.slug}", + f"/eskaera/{self.group_order.slug}/checkout", + ): + response = self.url_open(url, allow_redirects=False) + self.assertEqual(response.status_code, 200, url) + + def test_numeric_urls_redirect_to_slug(self): + self.authenticate(self.portal_login, self.portal_login) + for url, expected in ( + (f"/eskaera/{self.group_order.id}", f"/eskaera/{self.group_order.slug}"), + ( + f"/eskaera/{self.group_order.id}/checkout", + f"/eskaera/{self.group_order.slug}/checkout", + ), + ): + response = self.url_open(url, allow_redirects=False) + self.assertIn(response.status_code, (301, 302, 303), url) + self.assertTrue( + response.headers.get("Location", "").endswith(expected), + f"{url} should redirect to {expected}, " + f"got {response.headers.get('Location')}", + ) + + def test_unknown_slug_falls_back_to_the_list(self): + self.authenticate(self.portal_login, self.portal_login) + response = self.url_open("/eskaera/does-not-exist", allow_redirects=False) + self.assertIn(response.status_code, (301, 302, 303)) + self.assertTrue(response.headers.get("Location", "").endswith("/eskaera")) diff --git a/website_sale_aplicoop/views/group_order_views.xml b/website_sale_aplicoop/views/group_order_views.xml index be74408..e3d1a05 100644 --- a/website_sale_aplicoop/views/group_order_views.xml +++ b/website_sale_aplicoop/views/group_order_views.xml @@ -11,6 +11,7 @@ + @@ -45,6 +46,10 @@

+
+ /eskaera/ + +
@@ -116,6 +121,7 @@ + diff --git a/website_sale_aplicoop/views/load_from_history_templates.xml b/website_sale_aplicoop/views/load_from_history_templates.xml index e17a2dc..833b818 100644 --- a/website_sale_aplicoop/views/load_from_history_templates.xml +++ b/website_sale_aplicoop/views/load_from_history_templates.xml @@ -13,6 +13,7 @@ // Items are embedded directly in the script (pre-serialized JSON from controller) var itemsJson = ; // This is a JSON array/string var groupOrderId = ; + var groupOrderUrl = ''; var saleOrderName = ''; var pickupDay = ''; var pickupDate = ''; @@ -67,7 +68,7 @@ // Redirect to group order page // The JavaScript on that page will detect this and load the items - window.location.href = '/eskaera/' + groupOrderId; + window.location.href = groupOrderUrl; diff --git a/website_sale_aplicoop/views/website_templates.xml b/website_sale_aplicoop/views/website_templates.xml index 9754f29..c1b7478 100644 --- a/website_sale_aplicoop/views/website_templates.xml +++ b/website_sale_aplicoop/views/website_templates.xml @@ -28,7 +28,7 @@ - +
@@ -47,7 +47,7 @@
- +