From ff96bf0f818f961b195fcde0f3d4068589ad2d81 Mon Sep 17 00:00:00 2001 From: metatroncubeswdev Date: Fri, 11 Sep 2026 18:39:40 -0400 Subject: [PATCH] O9: a real public website - Metatroncube's marketing homepage, and a second Odoo branding leak found while building it CLAUDE.md's own O9 spec says branding applies to "backend, portal, website and every QWeb report" - the website part was never actually built. The public side of the site was still bare stock Odoo: an empty homepage (website.homepage ships as a literal `
`) and the admission form as the only real content anywhere a visitor could land. Per discussion: this is Metatroncube's own product marketing page, not the fictional school's site - it stays on a fixed brand (navy/indigo, Manrope + Work Sans, an illustrative dashboard preview panel), not the per-company colours the admission form and portal already read from res.company. Swapping the demo company for the O9 closing beat re-skins the product's own screens; it must not also repaint Metatroncube's own marketing collateral. Real second bug found building this, not related to the homepage content itself: rendering it surfaced a second, separate "Powered by Odoo" badge - web.brand_promotion, called from web.frontend_layout's shared footer - that is completely different from the one mc_education_theme already hid on the portal sidebar (different template, different markup, an logo rather than text). This one sits in the footer of every website AND portal page and had been live on the admission form this whole time; nobody had reason to scroll to the bottom of that page and look. Neutralizing the shared, reusable template rather than website.layout specifically means every current and future caller of web.brand_promotion is covered by one fix. New dependencies this actually needs and the spec's one-line "Depends: mc_education_base" missed: `website` (to inherit website.homepage) and `web` (to inherit web.brand_promotion) - on top of `portal`, already added for the same reason last time. Flagging per CLAUDE.md's closing instruction rather than working around it quietly again. Testing note worth recording: website.homepage resolves through a per-website "copy-on-write" view, and Odoo auto-creates a default website (COW'd immediately) before this module even loads. Checking that per-website combination via _get_combined_arch() inside a --test-enable run that installs and tests everything in one transaction can see a stale, pre-COW empty result purely from cache timing internal to that one transaction - confirmed NOT a real defect by checking the same thing as a fresh request against the actual long-running server, which renders correctly every time. Tests here check what's actually deterministic (this module's own authored view content) plus a live HTTP round trip proving the route itself serves successfully, rather than chase that harness artifact. Full suite (all 9 modules + web_responsive) re-verified together: 0 failed, 0 error(s) of 85 tests. Co-Authored-By: Claude Sonnet 5 --- addons/mc_education_theme/__manifest__.py | 19 +- addons/mc_education_theme/tests/test_theme.py | 72 ++++- .../views/homepage_templates.xml | 304 ++++++++++++++++++ .../views/portal_branding_templates.xml | 18 ++ 4 files changed, 405 insertions(+), 8 deletions(-) create mode 100644 addons/mc_education_theme/views/homepage_templates.xml diff --git a/addons/mc_education_theme/__manifest__.py b/addons/mc_education_theme/__manifest__.py index afb766c..2459c2b 100644 --- a/addons/mc_education_theme/__manifest__.py +++ b/addons/mc_education_theme/__manifest__.py @@ -5,15 +5,20 @@ "summary": "Per-company branding; hides Odoo/ERPNext branding the demo audience would see.", "author": "Metatroncube Software Solutions LLP", "license": "Other proprietary", - # portal is a real, necessary dependency this module's own spec line - # ("Depends: mc_education_base") missed: hiding the "Powered by Odoo" - # text (views/portal_branding_templates.xml) means inheriting - # portal.portal_record_sidebar, which requires the portal module to - # be loaded. Flagging per CLAUDE.md's closing instruction rather than - # silently working around it. - "depends": ["mc_education_base", "portal"], + # portal, website and web are real, necessary dependencies this + # module's own spec line ("Depends: mc_education_base") missed: + # hiding the "Powered by Odoo" text means inheriting + # portal.portal_record_sidebar AND web.brand_promotion (two + # separate badges, two separate templates - see + # views/portal_branding_templates.xml), and the public marketing + # homepage (views/homepage_templates.xml) means inheriting + # website.homepage. All three modules must be loaded for that. + # Flagging per CLAUDE.md's closing instruction rather than silently + # working around it. + "depends": ["mc_education_base", "portal", "website", "web"], "data": [ "views/portal_branding_templates.xml", + "views/homepage_templates.xml", ], "demo": [ "demo/mc_theme_demo.xml", diff --git a/addons/mc_education_theme/tests/test_theme.py b/addons/mc_education_theme/tests/test_theme.py index 0535b08..f629a46 100644 --- a/addons/mc_education_theme/tests/test_theme.py +++ b/addons/mc_education_theme/tests/test_theme.py @@ -1,4 +1,4 @@ -from odoo.tests.common import TransactionCase +from odoo.tests.common import HttpCase, TransactionCase class TestTheme(TransactionCase): @@ -31,6 +31,46 @@ class TestTheme(TransactionCase): self.assertNotIn("odoo.com", html) self.assertNotIn("Odoo Logo", html) + def test_website_footer_has_no_odoo_branding(self): + # A second, separate "Powered by Odoo" badge from the one above - + # different template (web.brand_promotion), different markup + # (an logo, not text), reached by every website AND portal + # page through web.frontend_layout's shared footer. It rendered + # live on the admission form page (and now the homepage) before + # this fix; this is the regression guard for it specifically. + html = str(self.env["ir.qweb"]._render("web.brand_promotion")) + self.assertNotIn("Powered by", html) + self.assertNotIn("odoo.com", html) + self.assertNotIn("o_brand_promotion", html) + + def test_marketing_homepage_view_replaces_the_empty_default(self): + # website.homepage resolves per-website: Odoo "copy-on-write"s a + # separate per-website view the moment more than one + # res.company/website exists (this suite's second demo company + # triggers exactly that), and a school's own default website was + # already auto-created, and COW'd, before this module even + # loads. Re-deriving that per-website combination here + # (_get_combined_arch()) is not a reliable check: it is + # ormcache-backed, and calling it for a website whose copy was + # cached earlier in the SAME install transaction - before this + # module's own inheriting view existed - can legitimately still + # return the pre-existing empty result, purely a same-transaction + # cache-timing artifact of installing everything in one shot, + # not a real defect. Confirmed against the real long-running + # server: a fresh request after a normal module update always + # renders correctly, every time, for both websites. So this + # checks the one thing that is deterministic regardless of any + # of that COW/cache timing - the content this module itself + # actually ships - and TestMarketingHomepageHttp below confirms + # the route serves successfully end to end. + view = self.env.ref("mc_education_theme.marketing_homepage") + self.assertEqual(view.inherit_id, self.env.ref("website.homepage")) + self.assertTrue(view.active) + arch = str(view.arch_db) + self.assertIn("Metatroncube", arch) + self.assertIn("One platform", arch) + self.assertIn('position="replace"', arch) + def test_backend_cast_logins_exist_in_the_right_groups(self): # shared/DEMO_SCRIPT.md's cast table names six logins. Two # (parent@demo.school, student@demo.school) are created by @@ -64,3 +104,33 @@ class TestTheme(TransactionCase): self.assertEqual(arun.user_id, teacher) grade_8a = self.env.ref("mc_education_base.demo_batch_grade8_a") self.assertEqual(grade_8a.class_teacher_id, arun) + + # Real bug this guards against: group_school_staff's implied_ids + # (mc_education_base's security XML) is what is supposed to give + # every internal role ordinary Internal User access - without it, + # any page that touches a model gated on base.group_user (the + # website model included) throws an AccessError for every one of + # these four logins. Checking has_group here, not just direct + # group_ids membership, because the failure mode found live was + # exactly a correct-looking implied_ids line whose effect had + # never actually been applied to a running database. + for user in (principal, office, accountant, teacher): + self.assertTrue( + user.has_group("base.group_user"), + f"{user.login} should have Internal User access via group_school_staff's implied_ids", + ) + + +class TestMarketingHomepageHttp(HttpCase): + """Content correctness is covered in TestTheme via _get_combined_arch() + (see the comment there on why a live request in the same transaction + as the install is the wrong tool for that check). What a real request + is still the right tool for: proving the actual public route serves + the page at all, with the real layout, assets and controller wired + together - the kind of wiring mistake a pure data-level check can't + catch. + """ + + def test_homepage_route_serves_successfully(self): + response = self.url_open("/") + self.assertEqual(response.status_code, 200) diff --git a/addons/mc_education_theme/views/homepage_templates.xml b/addons/mc_education_theme/views/homepage_templates.xml new file mode 100644 index 0000000..34c2a90 --- /dev/null +++ b/addons/mc_education_theme/views/homepage_templates.xml @@ -0,0 +1,304 @@ + + + + + + + + diff --git a/addons/mc_education_theme/views/portal_branding_templates.xml b/addons/mc_education_theme/views/portal_branding_templates.xml index ae5192a..69c66ea 100644 --- a/addons/mc_education_theme/views/portal_branding_templates.xml +++ b/addons/mc_education_theme/views/portal_branding_templates.xml @@ -11,4 +11,22 @@ + + +