diff --git a/docker/scripts/odoo_website_bootstrap.py b/docker/scripts/odoo_website_bootstrap.py index e0c0e15..8119668 100644 --- a/docker/scripts/odoo_website_bootstrap.py +++ b/docker/scripts/odoo_website_bootstrap.py @@ -192,15 +192,42 @@ def _print_bootstrap_readback( print(f"website_bootstrap_logo_present={_marker_bool(not logo_expected or logo_present)}") -def _select_website(website_model: Any, *, canonical_url: str) -> Any: +def _select_website( + website_model: Any, + *, + canonical_url: str, + primary_page: Any | None = None, + default_website: Any | None = None, +) -> Any: + if primary_page and "website_id" in primary_page._fields: + page_website = _field_value(primary_page, "website_id") + if page_website: + return page_website.sudo() canonical_host = _canonical_host(canonical_url) if canonical_host and "domain" in website_model._fields: website = website_model.search([("domain", "in", (canonical_host, canonical_url))], order="id", limit=1) if website: return website + if default_website: + return default_website.sudo() return website_model.search([], order="id", limit=1) +def _clear_duplicate_canonical_domains(website_model: Any, *, website: Any, canonical_url: str) -> None: + canonical_host = _canonical_host(canonical_url) + if not canonical_host or "domain" not in website_model._fields: + return + duplicates = website_model.search( + [ + ("id", "!=", website.id), + ("domain", "in", (canonical_host, canonical_url)), + ], + order="id", + ) + if duplicates: + duplicates.sudo().write({"domain": ""}) + + def _homepage_values(website: Any, *, homepage_url: str, homepage_page: Any | None) -> dict[str, object]: values: dict[str, object] = {} if homepage_page and "homepage_id" in website._fields: @@ -307,8 +334,21 @@ def apply_website_bootstrap(env: Any, parsed_payload: dict[str, object] | None) canonical_url = str(website_payload.get("canonical_url") or _payload_web_base_url(parsed_payload) or "").strip() + homepage_url = str(website_payload.get("homepage_url") or "").strip() + primary_page_xmlid = str(website_payload.get("primary_page_xmlid") or "").strip() + primary_page = _find_website_page_by_xmlid(env, xmlid=primary_page_xmlid) + primary_page_xmlid_found = bool(primary_page) + if primary_page_xmlid and not primary_page: + raise RuntimeError(f"Website bootstrap primary page XML ID not found: {primary_page_xmlid}") + default_website = env.ref("website.default_website", raise_if_not_found=False) + website_model = env["website"].sudo() - website = _select_website(website_model, canonical_url=canonical_url) + website = _select_website( + website_model, + canonical_url=canonical_url, + primary_page=primary_page, + default_website=default_website, + ) if not website: default_name = str(website_payload.get("name") or "Website").strip() or "Website" create_values = _field_values(website_model, {"name": default_name}) @@ -323,6 +363,7 @@ def apply_website_bootstrap(env: Any, parsed_payload: dict[str, object] | None) _require_existing_fields(website, ("domain",), label="canonical domain") _set_config_parameter(env, "web.base.url", canonical_url) _set_config_parameter(env, "web.base.url.freeze", "True") + _clear_duplicate_canonical_domains(website_model, website=website, canonical_url=canonical_url) website_values["domain"] = _canonical_host(canonical_url) default_lang = str(website_payload.get("default_lang") or "").strip() if default_lang and "default_lang_id" in website._fields: @@ -344,16 +385,12 @@ def apply_website_bootstrap(env: Any, parsed_payload: dict[str, object] | None) if logo_expected: _assert_binary_field_value(website, "logo", logo_value, label="website logo") - homepage_url = str(website_payload.get("homepage_url") or "").strip() - primary_page_xmlid = str(website_payload.get("primary_page_xmlid") or "").strip() - homepage_page, primary_page_xmlid_found = _find_website_page(env, website, xmlid=primary_page_xmlid, url=homepage_url) + homepage_page = primary_page or _find_website_page_by_url(env, website, url=homepage_url) if homepage_page: page_values: dict[str, object] = {"is_published": True, "website_published": True} if "website_id" in homepage_page._fields: page_values["website_id"] = website.id _write_existing_fields(homepage_page, page_values) - elif primary_page_xmlid: - raise RuntimeError(f"Website bootstrap primary page XML ID not found: {primary_page_xmlid}") _write_existing_fields(website, _homepage_values(website, homepage_url=homepage_url, homepage_page=homepage_page)) final_homepage_url = homepage_url final_homepage_page = homepage_page diff --git a/tests/test_odoo_website_bootstrap.py b/tests/test_odoo_website_bootstrap.py index 1fd2adb..5a5c81f 100644 --- a/tests/test_odoo_website_bootstrap.py +++ b/tests/test_odoo_website_bootstrap.py @@ -71,11 +71,13 @@ def __init__( self.record = record if record is not None else FakeRecord(truthy=False) self._fields = set(fields) self.records = records + self.searches: list[tuple[tuple[object, ...], dict[str, object]]] = [] def sudo(self) -> FakeModel: return self def search(self, *unused_args: object, **unused_kwargs: object) -> FakeRecord: + self.searches.append((unused_args, unused_kwargs)) if self.records is not None: return self.records[0] if self.records else FakeRecord(truthy=False) return self.record @@ -106,16 +108,19 @@ def get_param(self, key: str) -> str | None: class FakeEnv: def __init__(self) -> None: self.website = FakeRecord(fields=("name", "domain", "homepage_id", "homepage_url", "logo")) + self.default_website = self.website self.config_parameter = FakeConfigParameter() self.modules = FakeModel(record=FakeRecord(fields=(), truthy=True)) self.pages = FakeModel(record=FakeRecord(fields=(), truthy=False)) self.langs = FakeModel(record=FakeRecord(fields=(), truthy=False)) self.refs: dict[str, FakeRecord] = {} self.registry = {"website": object()} + self.website_model: FakeModel | None = None def __getitem__(self, model_name: str) -> Any: return { - "website": FakeModel(record=self.website, fields=("name", "domain", "homepage_id", "homepage_url", "logo")), + "website": self.website_model + or FakeModel(record=self.website, fields=("name", "domain", "homepage_id", "homepage_url", "logo")), "ir.config_parameter": self.config_parameter, "ir.module.module": self.modules, "website.page": self.pages, @@ -124,6 +129,8 @@ def __getitem__(self, model_name: str) -> Any: }[model_name] def ref(self, xmlid: str, *unused_args: object, **unused_kwargs: object) -> FakeRecord | None: + if xmlid == "website.default_website": + return self.default_website return self.refs.get(xmlid) @@ -206,6 +213,61 @@ def test_page_backed_homepage_requires_primary_page_and_persists_it(self) -> Non self.assertEqual(env.website.homepage_url, "/cell-mechanic") self.assertIn({"is_published": True, "website_published": True, "website_id": 1}, page.writes) + def test_primary_page_website_wins_over_existing_canonical_domain_match(self) -> None: + env = FakeEnv() + page_bound_website = FakeRecord( + record_id=1, + fields=("name", "domain", "homepage_id", "homepage_url", "logo"), + values={"name": "My Website", "domain": ""}, + ) + canonical_website = FakeRecord( + record_id=2, + fields=("name", "domain", "homepage_id", "homepage_url", "logo"), + values={"name": "Old Target", "domain": "cm-website-testing.example.com"}, + ) + env.website = page_bound_website + env.default_website = page_bound_website + env.website_model = FakeModel( + record=page_bound_website, + fields=("name", "domain", "homepage_id", "homepage_url", "logo"), + records=[canonical_website], + ) + page = FakeRecord( + record_id=42, + fields=("is_published", "website_published", "website_id"), + values={"model_name": "website.page", "website_id": page_bound_website}, + ) + env.refs["cm_website.website_page_cell_mechanic"] = page + payload = { + "website_bootstrap": { + "name": "Cell Mechanic", + "canonical_url": "https://cm-website-testing.example.com", + "homepage_url": "/cell-mechanic", + "primary_page_xmlid": "cm_website.website_page_cell_mechanic", + } + } + + website_bootstrap.apply_website_bootstrap(env, payload) + + self.assertEqual(page_bound_website.name, "Cell Mechanic") + self.assertEqual(page_bound_website.domain, "cm-website-testing.example.com") + self.assertEqual(page_bound_website.homepage_id, page.id) + self.assertEqual(canonical_website.name, "Old Target") + self.assertEqual(canonical_website.domain, "") + self.assertIn({"is_published": True, "website_published": True, "website_id": 1}, page.writes) + self.assertIn( + ( + ( + [ + ("id", "!=", page_bound_website.id), + ("domain", "in", ("cm-website-testing.example.com", "https://cm-website-testing.example.com")), + ], + ), + {"order": "id"}, + ), + env.website_model.searches, + ) + def test_missing_primary_page_fails_before_delegating_to_installed_module(self) -> None: env = FakeEnv() payload = { @@ -225,6 +287,8 @@ def test_missing_primary_page_fails_before_delegating_to_installed_module(self) with self.assertRaisesRegex(RuntimeError, "primary page XML ID not found"): website_bootstrap.apply_website_bootstrap(env, payload) + self.assertEqual(env.config_parameter.values, {}) + self.assertEqual(env.website.writes, []) def test_bad_primary_page_xmlid_fails_even_when_url_fallback_page_exists(self) -> None: env = FakeEnv() @@ -247,6 +311,8 @@ def test_bad_primary_page_xmlid_fails_even_when_url_fallback_page_exists(self) - with self.assertRaisesRegex(RuntimeError, "primary page XML ID not found"): website_bootstrap.apply_website_bootstrap(env, payload) + self.assertEqual(env.config_parameter.values, {}) + self.assertEqual(env.website.writes, []) def test_route_homepage_readback_reports_final_route_homepage(self) -> None: env = FakeEnv()