- create(): slugs were only checked against the database, so records created in the same batch (duplicating several orders from the list view, importing rows sharing a name) collided on group_order_slug_uniq. _generate_unique_slug now also skips the slugs already handed out in the batch. - _redirect_to_slug_url(): `post` is passed as a dict instead of splatted, so a query parameter named `suffix` no longer binds to the keyword argument of the same name (HTTP 500 on /eskaera/<id>/checkout?suffix=x, and a corrupted path on the shop route). - post-migrate: the backfill runs with tracking_disable, `slug` is tracked and the upgrade posted a chatter message on every pre-existing order. Tests for the two reachable cases: batch create and query parameters kept across the legacy redirect. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
193 lines
7.4 KiB
Python
193 lines
7.4 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 _group_order_vals(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 vals
|
|
|
|
def _create_group_order(self, name, **values):
|
|
return self.env["group.order"].create(self._group_order_vals(name, **values))
|
|
|
|
|
|
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_slug_is_unique_within_a_single_create(self):
|
|
"""Records created in one batch cannot see each other in the database."""
|
|
orders = self.env["group.order"].create(
|
|
[self._group_order_vals("Weekly Order") for _ in range(3)]
|
|
)
|
|
self.assertEqual(
|
|
orders.mapped("slug"),
|
|
["weekly-order", "weekly-order-2", "weekly-order-3"],
|
|
)
|
|
|
|
def test_generated_slug_avoids_an_explicit_one_in_the_same_create(self):
|
|
orders = self.env["group.order"].create(
|
|
[
|
|
self._group_order_vals("Something Else", slug="weekly-order"),
|
|
self._group_order_vals("Weekly Order"),
|
|
]
|
|
)
|
|
self.assertEqual(orders.mapped("slug"), ["weekly-order", "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_numeric_url_keeps_query_parameters(self):
|
|
"""A `suffix` parameter must stay in the query string, not in the path."""
|
|
self.authenticate(self.portal_login, self.portal_login)
|
|
response = self.url_open(
|
|
f"/eskaera/{self.group_order.id}/checkout?suffix=x&search=apple",
|
|
allow_redirects=False,
|
|
)
|
|
self.assertIn(response.status_code, (301, 302, 303))
|
|
location = response.headers.get("Location", "")
|
|
self.assertIn(f"/eskaera/{self.group_order.slug}/checkout?", location)
|
|
self.assertIn("suffix=x", location)
|
|
self.assertIn("search=apple", 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"))
|