metatroncubeswdev cf656377ef O9: align Admissions, Contact Us and Login with the homepage design, real Metatroncube logo/favicon, and a chain of placeholder-content leaks found underneath
Per discussion: the marketing homepage looked polished but the rest of
the public site (the admission form, contact us, the backend login
screen) was still bare stock Odoo, and the site nav still showed
stock Odoo's own default logo/favicon rather than Metatroncube's real
ones. This brings the same design system (Manrope + Work Sans, the
navy/indigo palette) to all three pages and wires up the two supplied
brand assets - static/img/logo.png (the wordmark, used as
website.logo, so it renders in every page's nav automatically) and
static/img/favicon.png (the cube mark, website.favicon). Loaded via a
post_init_hook (hooks.py) rather than a plain <record>: website's two
stock demo sites (website.default_website, website.website2) are both
noupdate=True, and Python is the tool that isn't gated by that the way
a <record>/<function> tag would be on an already-installed database
(see hooks.py's own docstring for the exact mechanism and its
fresh-install-only limitation).

Admissions keeps its exact form markup/field names/model untouched -
only the surrounding hero and "what happens next" strip are new, added
via position="before"/"after" around the existing <section> so the
stock s_website_form JS widget this page depends on is never touched.

Restyling contact us and the login screen surfaced a chain of real,
separate placeholder-content bugs, each found by looking at the next
page down rather than assuming the previous fix was complete:

1. website.contactus's sidebar hardcodes "My Company" and a fake US
   street address - shared/DEMO_SCRIPT.md explicitly bans placeholder
   content, and it had been live here the whole time. Reading
   res_company fields instead means it shows whichever school is
   actually active, same as the admission form's own live program list.
2. website.footer_custom - the footer on EVERY page, not just contact
   us - carries the identical yourcompany.example.com / +1
   555-555-5556 placeholder, plus a "Products/Services/Legal" link
   list to nowhere and dead social icons. Fixing #1 alone would have
   left this sitting directly underneath it on every single page.
3. website.header_text_element, the nav bar's own phone/email widget,
   carries the same fake number - a fourth, separate occurrence, this
   time in the header. First pass here fixed only its "phone_mail"
   variant, assumed (wrongly) to be what this site's header actually
   uses; the live page still showed the fake number afterward. It
   turned out both nav slots (desktop, mobile) use the plain default
   branch instead - caught only by re-checking the real page rather
   than trusting that assumption, and fixed every variant that carries
   contact info this time, not just the one guessed at.
4. Underneath all three page-level fixes: base.main_company itself
   still carried stock Odoo's own demo phone (+1 555-555-5556) and
   email (info@yourcompany.com) - the page-level fixes above only
   surfaced this because they read real company data for the first
   time; the data itself needed fixing too. Real Indian/Canadian
   contact details added to both demo companies in mc_theme_demo.xml.
5. web.login_layout hardcodes its own separate "Powered by Odoo" link
   - a third distinct occurrence, different from the portal sidebar
   text and the web.brand_promotion badge already fixed, reached by
   the very first screen anyone doing this demo sees.

Full suite (all 9 modules + web_responsive) re-verified together after
every fix in this chain: 0 failed, 0 error(s), tests up to 93.

One correction to this session's own earlier work: mc_cast_users_demo.xml
claimed <function> "isn't subject to" the noupdate/mode guard that
blocks a plain <record> on an already-installed database - false,
odoo/tools/convert.py's _tag_function has the exact same
"if self.noupdate and self.mode != 'init': return" check. What
actually fixed that earlier bug was applying the change as a one-off
manual Python snippet, not the tag choice - the same manual-application
pattern used again here for base.main_company's contact fields on the
already-running school_demo database.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-12 00:53:31 -04:00

259 lines
14 KiB
Python

from lxml import etree
from odoo.tests.common import HttpCase, TransactionCase
class TestTheme(TransactionCase):
def test_two_branded_companies_exist(self):
main = self.env.ref("base.main_company")
self.assertEqual(main.name, "St. Aloysius Public School")
# No currency assertion here on purpose: base.main_company's
# currency is deliberately left untouched by this module (see the
# comment in demo/mc_theme_demo.xml) - changing it would fail
# once any financial module has posted journal items for this
# company, which a combined install of the full suite does
# before this module's demo data runs.
self.assertTrue(main.logo)
waterloo = self.env.ref("mc_education_theme.demo_company_waterloo_heights")
self.assertEqual(waterloo.name, "Waterloo Heights Academy")
self.assertEqual(waterloo.currency_id, self.env.ref("base.CAD"))
self.assertTrue(waterloo.logo)
# The "swap" is switching companies - each must actually be
# distinct, not the same record twice under different refs.
self.assertNotEqual(main.id, waterloo.id)
# Contact fields specifically, not just name/colour/logo: the
# contact us page and site footer now read phone/email/street
# live off whichever company is active (see
# contactus_templates.xml, footer_templates.xml) - a company
# still carrying stock Odoo's own demo placeholder here
# (+1 555-555-5556, info@yourcompany.com) would put that
# placeholder right back in front of the demo audience, on a
# page built specifically to stop showing it.
for company in (main, waterloo):
self.assertTrue(company.phone)
self.assertNotIn("555-555-5556", company.phone)
self.assertTrue(company.email)
self.assertNotIn("yourcompany", company.email)
def test_portal_sidebar_has_no_odoo_branding(self):
html = str(self.env["ir.qweb"]._render("portal.portal_record_sidebar", {
"classes": "", "title": False, "entries": False,
}))
self.assertNotIn("Powered by", html)
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 <img> 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_login_page_has_no_odoo_branding(self):
# A third, separate "Powered by Odoo" occurrence - the backend
# login screen's own hardcoded link, distinct from the portal
# sidebar text and the web.brand_promotion badge above. Unlike
# website.homepage, web.login_layout isn't website-scoped (no
# per-website COW view involved), so _get_combined_arch() here
# isn't subject to the same-transaction cache-timing quirk
# documented on the homepage test - this is a reliable check.
#
# _get_combined_arch() returns an lxml Element, not a string -
# str(element) gives its memory-address repr ("<Element t at
# 0x...>"), which would make assertNotIn trivially, silently
# pass no matter what the template actually contains. Caught by
# spot-checking the header widget fix below the same way and
# seeing that exact non-answer instead of real markup.
# etree.tostring() is the real serialization.
arch = etree.tostring(self.env.ref("web.login_layout")._get_combined_arch()).decode()
self.assertNotIn("Powered by", arch)
self.assertNotIn("odoo.com", arch)
def test_contactus_page_shows_real_company_not_placeholder(self):
# Stock Odoo hardcodes "My Company" and a fake US street address
# in the contact sidebar - exactly the kind of placeholder
# content shared/DEMO_SCRIPT.md's "what must NOT appear" list
# bans, and it was live on this page before today. Checking the
# view's own arch rather than a live render for the same reason
# as the login test above - res_company resolution needs a
# website request context this TransactionCase doesn't have,
# and the substitution itself is fully verifiable from the
# template source: it reads res_company fields instead of
# hardcoding text.
arch = str(self.env.ref("mc_education_theme.contactus_page_branding").arch_db)
self.assertNotIn("My Company", arch)
self.assertNotIn("Fake Buena Vista", arch)
self.assertNotIn("yourcompany.example.com", arch)
self.assertIn("res_company.name", arch)
self.assertIn("res_company.email", arch)
def test_site_footer_shows_real_company_not_placeholder(self):
# website.footer_custom is active on EVERY website page, not
# just contact us - fixing the contact page's own sidebar and
# missing this would have left the identical
# yourcompany.example.com / +1 555-555-5556 / "Products,
# Services, Legal" placeholder visible underneath it, on every
# single page of the site.
arch = str(self.env.ref("mc_education_theme.footer_branding").arch_db)
self.assertNotIn("yourcompany.example.com", arch)
self.assertNotIn("555-555-5556", arch)
self.assertNotIn("passionate people", arch)
self.assertIn("res_company.name", arch)
self.assertIn("/admissions/apply", arch)
def test_header_phone_mail_widget_shows_real_company_not_placeholder(self):
# A fourth occurrence, in the header rather than the footer:
# website.header_text_element is a library of variants
# (sentence, list, phone_mail, mail, mail_stretched, a
# default), found after the footer fix above made the same
# fake number's continued presence, higher up the same page,
# obvious. First pass fixed only "phone_mail", assumed to be
# this site's active variant - wrong: the two spots actually
# rendering on the real page (desktop nav, mobile offcanvas)
# both turned out to use the plain default branch instead,
# caught only by re-checking the live page rather than trusting
# that assumption. Checking every variant with contact info in
# it here, not just the one that happened to be live on this
# particular header configuration - "sentence" and "list" carry
# no phone/email at all and are correctly untouched.
arch = etree.tostring(
self.env.ref("website.header_text_element")._get_combined_arch()
).decode()
self.assertNotIn("555-555-5556", arch)
self.assertNotIn("yourcompany.example.com", arch)
phone_mail_branch = arch.split("'phone_mail'", 1)[1].split("t-elif", 1)[0]
self.assertIn("res_company.phone", phone_mail_branch)
self.assertIn("res_company.email", phone_mail_branch)
mail_branch = arch.split("'mail'", 1)[1].split("t-elif", 1)[0]
self.assertIn("res_company.email", mail_branch)
mail_stretched_branch = arch.split("'mail_stretched'", 1)[1].split("t-elif", 1)[0]
self.assertIn("res_company.phone", mail_stretched_branch)
# The default (t-else) branch is last, has no following
# t-elif to split on - it runs to the end of the widget instead.
default_branch = arch.rsplit("t-else", 1)[1]
self.assertIn("res_company.phone", default_branch)
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
# mc_education_portal. The other four were explicitly deferred
# to this module (see demo/mc_cast_users_demo.xml) and, until
# that file existed, were never actually created anywhere -
# the demo script would have stalled at Scene 1 on a clean
# rehearsal. This test is the regression guard for that gap.
principal = self.env["res.users"].search([("login", "=", "principal@demo.school")])
self.assertTrue(principal)
self.assertIn(
self.env.ref("mc_education_base.group_school_administrator"), principal.group_ids
)
office = self.env["res.users"].search([("login", "=", "office@demo.school")])
self.assertTrue(office)
self.assertIn(self.env.ref("mc_education_base.group_school_staff"), office.group_ids)
accountant = self.env["res.users"].search([("login", "=", "accounts@demo.school")])
self.assertTrue(accountant)
self.assertIn(self.env.ref("mc_education_base.group_accountant"), accountant.group_ids)
teacher = self.env["res.users"].search([("login", "=", "teacher@demo.school")])
self.assertTrue(teacher)
self.assertIn(self.env.ref("mc_education_base.group_teacher"), teacher.group_ids)
# Not just "a teacher login exists" - it has to be *the* login
# that Grade 8-A's class_teacher_id points at, because that is
# what mc_education_attendance's record rule actually checks.
arun = self.env.ref("mc_education_base.demo_employee_arun_prakash")
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",
)
def test_website_logo_and_favicon_are_metatroncubes_own(self):
# hooks.post_init_hook loads these from static/img/ onto every
# website record - only runs on a genuinely fresh install (see
# hooks.py), which is exactly the situation this test runs
# under. An already-installed database needs the one-off manual
# application documented in hooks.py's own docstring.
for website in self.env["website"].search([]):
self.assertTrue(website.logo, f"website {website.name!r} has no logo")
self.assertTrue(website.favicon, f"website {website.name!r} has no favicon")
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)
def test_login_route_serves_successfully(self):
response = self.url_open("/web/login")
self.assertEqual(response.status_code, 200)
def test_contactus_route_serves_successfully(self):
response = self.url_open("/contactus")
self.assertEqual(response.status_code, 200)