addons-cm/website_sale_aplicoop/tests/test_group_order_slug.py
GitHub Copilot 23edee6154 [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/<slug>` (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/<id>` and `/eskaera/<id>/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 <noreply@anthropic.com>
2026-08-11 16:49:54 +02:00

158 lines
5.9 KiB
Python

# 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"))