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>
This commit is contained in:
metatroncubeswdev 2026-09-12 00:53:31 -04:00
parent ff96bf0f81
commit cf656377ef
12 changed files with 640 additions and 23 deletions

View File

@ -1,5 +1,21 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<odoo> <odoo>
<!-- Same type pairing as mc_education_theme's marketing homepage
(Manrope for headings, Work Sans for body) so a visitor moving
from the homepage to this page doesn't land on a visibly
different, unstyled product. Loaded here too, not only from
theme, so this module still looks intentional if installed on
its own - matching CLAUDE.md's "each addon installs
independently" rule; the browser dedupes the identical URL if
both happen to be loaded on the same site. -->
<template id="admission_fonts" inherit_id="website.layout" name="Admission form fonts">
<xpath expr="//head" position="inside">
<link rel="preconnect" href="https://fonts.googleapis.com"/>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="crossorigin"/>
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@600;700;800&amp;family=Work+Sans:wght@400;500;600;700&amp;display=swap" rel="stylesheet"/>
</xpath>
</template>
<!-- <!--
Hand-written to match the DOM structure/classes of Odoo core's own Hand-written to match the DOM structure/classes of Odoo core's own
s_website_form snippet (website/views/snippets/s_website_form.xml) s_website_form snippet (website/views/snippets/s_website_form.xml)
@ -9,13 +25,86 @@
Odoo's generic /website/form/<model> controller; see Odoo's generic /website/form/<model> controller; see
data/mc_applicant_website_form_data.xml for the field whitelist data/mc_applicant_website_form_data.xml for the field whitelist
that is the actual security boundary on that path. that is the actual security boundary on that path.
The hero band and "what happens next" strip around the form are
presentation only, matching the site design system - they do not
touch a single field, name or class inside <form>, since that
exact markup is what the stock form.js widget depends on.
--> -->
<template id="admission_apply_page" name="Admission Application"> <template id="admission_apply_page" name="Admission Application">
<t t-call="website.layout"> <t t-call="website.layout">
<div id="wrap"> <div id="wrap" class="mc-admissions">
<style>
.mc-admissions{ --navy:#131B2E; --navy-2:#1C2A46; --indigo:#4B3F92; --indigo-2:#2E63B8;
--paper:#F6F7FB; --card:#FFFFFF; --line:#E4E7F0; --ink:#16192B; --ink-soft:#5B6178;
--shadow:0 1px 2px rgba(19,27,46,.06), 0 14px 34px -16px rgba(19,27,46,.22);
font-family:'Work Sans', system-ui, sans-serif; color:var(--ink); background:var(--paper);
}
.mc-admissions *{ box-sizing:border-box; }
.mc-admissions h1, .mc-admissions h2, .mc-admissions h3{
font-family:'Manrope', system-ui, sans-serif; font-weight:800; letter-spacing:-.01em;
color:var(--ink); text-wrap:balance; margin:0;
}
.mc-adm-hero{
background: radial-gradient(900px 380px at 90% -10%, rgba(75,63,146,.5), transparent 60%),
linear-gradient(180deg, var(--navy), var(--navy-2));
color:#fff; padding:52px 24px 60px; text-align:center;
}
.mc-adm-hero .pill{
display:inline-block; font-size:.72rem; letter-spacing:.08em; text-transform:uppercase;
font-weight:600; color:#EFE9FF; background:linear-gradient(90deg, var(--indigo), var(--indigo-2));
padding:6px 14px; border-radius:20px; margin-bottom:16px;
}
.mc-adm-hero h1{ color:#fff; font-size:clamp(1.6rem,3.4vw,2.3rem); max-width:20ch; margin:0 auto; }
.mc-adm-hero p{ color:#C4C9E2; max-width:52ch; margin:14px auto 0; font-size:1rem; line-height:1.6; }
.mc-adm-body{ max-width:900px; margin:-30px auto 0; padding:0 20px 64px; position:relative; }
.mc-adm-card{
background:var(--card); border:1px solid var(--line); border-radius:16px;
box-shadow:var(--shadow); padding:36px 32px;
}
@media (max-width:600px){ .mc-adm-card{ padding:26px 18px; } }
.mc-adm-card h1{ font-size:1.4rem; margin-bottom:6px; }
.mc-adm-card .lede{ color:var(--ink-soft); margin-bottom:26px; font-size:.94rem; }
.mc-admissions .s_website_form_label{ font-weight:600; color:var(--ink); font-size:.88rem; }
.mc-admissions .s_website_form_mark{ color:var(--indigo); }
.mc-admissions .form-control, .mc-admissions .form-select{
border-color:var(--line); border-radius:9px; padding:10px 12px; font-family:'Work Sans', sans-serif;
}
.mc-admissions .form-control:focus, .mc-admissions .form-select:focus{
border-color:var(--indigo); box-shadow:0 0 0 3px rgba(75,63,146,.15);
}
.mc-admissions .btn-primary{
background:linear-gradient(90deg, var(--indigo), var(--indigo-2)); border:none;
padding:11px 26px; border-radius:9px; font-weight:600;
}
.mc-admissions .btn-primary:hover{ filter:brightness(1.08); }
.mc-adm-steps{ max-width:900px; margin:0 auto; padding:0 20px 70px; }
.mc-adm-steps h2{ font-size:1.1rem; text-align:center; margin-bottom:24px; }
.mc-steps-row{ display:grid; grid-template-columns:repeat(5,1fr); gap:10px; }
@media (max-width:760px){ .mc-steps-row{ grid-template-columns:repeat(2,1fr); } }
.mc-step{ background:var(--card); border:1px solid var(--line); border-radius:12px; padding:16px 12px; text-align:center; }
.mc-step .n{
width:26px; height:26px; border-radius:50%; background:var(--indigo); color:#fff;
display:flex; align-items:center; justify-content:center; margin:0 auto 8px;
font-family:'Manrope',sans-serif; font-weight:700; font-size:.8rem;
}
.mc-step .t{ font-size:.82rem; font-weight:600; color:var(--ink); }
</style>
<div class="mc-adm-hero">
<span class="pill">Admissions Open</span>
<h1>Apply for Admission</h1>
<p>Fill in your child's details, attach a birth certificate, and submit - no office visit needed. You'll get an application number the moment you submit, and can track its progress from Applied through to Enrolled.</p>
</div>
<div class="mc-adm-body">
<div class="mc-adm-card">
<section class="s_website_form pt32 pb32" data-snippet="s_website_form"> <section class="s_website_form pt32 pb32" data-snippet="s_website_form">
<div class="container-fluid"> <div class="container-fluid">
<h1 class="mb-4">Apply for Admission</h1> <h1 class="mb-4">Application Form</h1>
<form action="/website/form/" method="post" enctype="multipart/form-data" <form action="/website/form/" method="post" enctype="multipart/form-data"
class="o_mark_required" data-mark="*" class="o_mark_required" data-mark="*"
data-model_name="mc.applicant" data-model_name="mc.applicant"
@ -135,6 +224,19 @@
</div> </div>
</section> </section>
</div> </div>
</div>
<div class="mc-adm-steps">
<h2>What happens after you submit</h2>
<div class="mc-steps-row">
<div class="mc-step"><div class="n">1</div><div class="t">Applied</div></div>
<div class="mc-step"><div class="n">2</div><div class="t">Document Verification</div></div>
<div class="mc-step"><div class="n">3</div><div class="t">Interview</div></div>
<div class="mc-step"><div class="n">4</div><div class="t">Offered</div></div>
<div class="mc-step"><div class="n">5</div><div class="t">Enrolled</div></div>
</div>
</div>
</div>
</t> </t>
</template> </template>

View File

@ -1,4 +1,7 @@
# No models: every branding field this module configures (res.company # No models: every branding field this module configures (res.company
# logo/name/primary_color/secondary_color/currency, res.partner address) # logo/name/primary_color/secondary_color/currency, res.partner address)
# already exists in stock Odoo. This module is data (demo/) and one view # already exists in stock Odoo. This module is data (demo/) and view
# override (views/), nothing more. # overrides (views/), plus one post_init_hook (hooks.py) for the
# website logo/favicon - see hooks.py for why that can't be a plain
# XML <record>.
from .hooks import post_init_hook

View File

@ -19,6 +19,9 @@
"data": [ "data": [
"views/portal_branding_templates.xml", "views/portal_branding_templates.xml",
"views/homepage_templates.xml", "views/homepage_templates.xml",
"views/contactus_templates.xml",
"views/login_templates.xml",
"views/footer_templates.xml",
], ],
"demo": [ "demo": [
"demo/mc_theme_demo.xml", "demo/mc_theme_demo.xml",
@ -26,4 +29,5 @@
], ],
"installable": True, "installable": True,
"application": False, "application": False,
"post_init_hook": "post_init_hook",
} }

View File

@ -25,10 +25,24 @@
run, never as a later "apply a theme" change. The brand-new run, never as a later "apply a theme" change. The brand-new
second company below has no such history, so its currency is second company below has no such history, so its currency is
set safely at creation. --> set safely at creation. -->
<!-- phone/email/street: found missing while restyling the public
website - the contact us page and site footer now genuinely
read these fields live (see mc_education_theme's
contactus_templates.xml/footer_templates.xml), which is
exactly what surfaced that this record was still carrying
stock Odoo's own demo company's placeholder phone
(+1 555-555-5556), email (info@yourcompany.com) and a fake US
street address underneath the parts that WERE branded (name,
city, colours, logo). shared/DEMO_SCRIPT.md bans placeholder
content outright; a page that reads real data doesn't fix that
on its own if the data underneath is still fake. -->
<record id="base.main_company" model="res.company"> <record id="base.main_company" model="res.company">
<field name="name">St. Aloysius Public School</field> <field name="name">St. Aloysius Public School</field>
<field name="country_id" ref="base.in"/> <field name="street">45 Avinashi Road</field>
<field name="city">Coimbatore</field> <field name="city">Coimbatore</field>
<field name="country_id" ref="base.in"/>
<field name="phone">+91 422 435 6789</field>
<field name="email">info@staloysiuscbe.edu.in</field>
<field name="primary_color">#1B3A5C</field> <field name="primary_color">#1B3A5C</field>
<field name="secondary_color">#D4A017</field> <field name="secondary_color">#D4A017</field>
<field name="logo" type="base64" file="mc_education_theme/static/demo/logo_st_aloysius.png"/> <field name="logo" type="base64" file="mc_education_theme/static/demo/logo_st_aloysius.png"/>
@ -36,9 +50,12 @@
<record id="demo_company_waterloo_heights" model="res.company"> <record id="demo_company_waterloo_heights" model="res.company">
<field name="name">Waterloo Heights Academy</field> <field name="name">Waterloo Heights Academy</field>
<field name="currency_id" ref="base.CAD"/> <field name="street">75 University Avenue West</field>
<field name="country_id" ref="base.ca"/>
<field name="city">Waterloo</field> <field name="city">Waterloo</field>
<field name="country_id" ref="base.ca"/>
<field name="currency_id" ref="base.CAD"/>
<field name="phone">+1 519 888 4400</field>
<field name="email">info@waterlooheights.ca</field>
<field name="primary_color">#1E5C3A</field> <field name="primary_color">#1E5C3A</field>
<field name="secondary_color">#C0C0C0</field> <field name="secondary_color">#C0C0C0</field>
<field name="logo" type="base64" file="mc_education_theme/static/demo/logo_waterloo_heights.png"/> <field name="logo" type="base64" file="mc_education_theme/static/demo/logo_waterloo_heights.png"/>

View File

@ -0,0 +1,28 @@
import base64
from odoo.tools.misc import file_open
def post_init_hook(env):
"""Metatroncube's own logo and favicon for the public website - a
fixed brand asset, not tied to whichever school company happens to
be active, matching the marketing homepage
(views/homepage_templates.xml).
Odoo ships two demo website records out of the box
(website.default_website, website.website2), both created with
noupdate=True. A plain <record> write targeting either from this
module's own data files is silently skipped - no error, nothing in
the log - exactly the same trap demo/mc_cast_users_demo.xml already
documented for an unrelated field. Running this as real Python
instead isn't subject to that guard.
Only runs on a fresh install of this module, per Odoo's own
post_init_hook contract - it will not re-fire on `-u` of a database
that already had this module installed before this hook existed.
"""
with file_open("mc_education_theme/static/img/logo.png", "rb") as f:
logo = base64.b64encode(f.read())
with file_open("mc_education_theme/static/img/favicon.png", "rb") as f:
favicon = base64.b64encode(f.read())
env["website"].search([]).write({"logo": logo, "favicon": favicon})

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -1,3 +1,5 @@
from lxml import etree
from odoo.tests.common import HttpCase, TransactionCase from odoo.tests.common import HttpCase, TransactionCase
@ -23,6 +25,20 @@ class TestTheme(TransactionCase):
# distinct, not the same record twice under different refs. # distinct, not the same record twice under different refs.
self.assertNotEqual(main.id, waterloo.id) 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): def test_portal_sidebar_has_no_odoo_branding(self):
html = str(self.env["ir.qweb"]._render("portal.portal_record_sidebar", { html = str(self.env["ir.qweb"]._render("portal.portal_record_sidebar", {
"classes": "", "title": False, "entries": False, "classes": "", "title": False, "entries": False,
@ -71,6 +87,94 @@ class TestTheme(TransactionCase):
self.assertIn("One platform", arch) self.assertIn("One platform", arch)
self.assertIn('position="replace"', 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): def test_backend_cast_logins_exist_in_the_right_groups(self):
# shared/DEMO_SCRIPT.md's cast table names six logins. Two # shared/DEMO_SCRIPT.md's cast table names six logins. Two
# (parent@demo.school, student@demo.school) are created by # (parent@demo.school, student@demo.school) are created by
@ -120,6 +224,16 @@ class TestTheme(TransactionCase):
f"{user.login} should have Internal User access via group_school_staff's implied_ids", 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): class TestMarketingHomepageHttp(HttpCase):
"""Content correctness is covered in TestTheme via _get_combined_arch() """Content correctness is covered in TestTheme via _get_combined_arch()
@ -134,3 +248,11 @@ class TestMarketingHomepageHttp(HttpCase):
def test_homepage_route_serves_successfully(self): def test_homepage_route_serves_successfully(self):
response = self.url_open("/") response = self.url_open("/")
self.assertEqual(response.status_code, 200) 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)

View File

@ -0,0 +1,161 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Same brand/type system as the marketing homepage and the
admission form: Manrope + Work Sans, the navy/indigo palette.
Only #wrap is replaced - the t-set/hidden span above it in
website.contactus (contactus_form_values, used by the
data-pre-fill JS on the form) live outside #wrap and are left
completely alone.
Two real, separate things fixed while restyling this, not
just a coat of paint:
1. Stock Odoo's sidebar hardcodes "My Company" / a fake US
street address / a placeholder phone and email -
shared/DEMO_SCRIPT.md's "what must NOT appear" list bans
exactly this kind of placeholder content, and it was live
on this page before today. Reading res_company (already
provided by website.layout) instead means it shows
whichever school is actually active - correct for the
same reason the admission form's own program dropdown
reads live data instead of a fixed list.
2. The field names/ids/model (mail.mail) are byte-identical
to stock - this form posts through the exact same
/website/form/ controller Odoo ships, so no submit
behaviour changes, only what it looks like. -->
<template id="contactus_page_branding" inherit_id="website.contactus" name="Contact Us - Metatroncube design">
<xpath expr="//div[@id='wrap']" position="replace">
<div id="wrap" class="mc-contact">
<style>
.mc-contact{ --navy:#131B2E; --navy-2:#1C2A46; --indigo:#4B3F92; --indigo-2:#2E63B8;
--paper:#F6F7FB; --card:#FFFFFF; --line:#E4E7F0; --ink:#16192B; --ink-soft:#5B6178;
--shadow:0 1px 2px rgba(19,27,46,.06), 0 14px 34px -16px rgba(19,27,46,.22);
font-family:'Work Sans', system-ui, sans-serif; color:var(--ink); background:var(--paper);
}
.mc-contact *{ box-sizing:border-box; }
.mc-contact h1, .mc-contact h2, .mc-contact h5{
font-family:'Manrope', system-ui, sans-serif; font-weight:800; letter-spacing:-.01em;
color:var(--ink); margin:0;
}
.mc-contact-hero{
background: radial-gradient(900px 380px at 90% -10%, rgba(75,63,146,.5), transparent 60%),
linear-gradient(180deg, var(--navy), var(--navy-2));
color:#fff; padding:52px 24px 60px; text-align:center;
}
.mc-contact-hero h1{ color:#fff; font-size:clamp(1.6rem,3.4vw,2.3rem); }
.mc-contact-hero p{ color:#C4C9E2; max-width:52ch; margin:14px auto 0; font-size:1rem; line-height:1.6; }
.mc-contact-body{
max-width:1000px; margin:-30px auto 0; padding:0 20px 70px;
display:grid; grid-template-columns:1.4fr 1fr; gap:20px; position:relative;
}
@media (max-width:760px){ .mc-contact-body{ grid-template-columns:1fr; } }
.mc-contact-card{
background:var(--card); border:1px solid var(--line); border-radius:16px;
box-shadow:var(--shadow); padding:32px 30px;
}
@media (max-width:600px){ .mc-contact-card{ padding:24px 18px; } }
.mc-contact .s_website_form_label{ font-weight:600; color:var(--ink); font-size:.85rem; width:auto !important; }
.mc-contact .s_website_form_mark{ color:var(--indigo); }
.mc-contact .form-control{ border-color:var(--line); border-radius:9px; padding:9px 12px; font-family:'Work Sans', sans-serif; }
.mc-contact .form-control:focus{ border-color:var(--indigo); box-shadow:0 0 0 3px rgba(75,63,146,.15); }
.mc-contact .btn-primary{
background:linear-gradient(90deg, var(--indigo), var(--indigo-2)); border:none;
padding:10px 24px; border-radius:9px; font-weight:600;
}
.mc-contact-info h5{ font-size:1.02rem; margin-bottom:14px; }
.mc-contact-info ul{ list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:12px; }
.mc-contact-info li{ display:flex; align-items:flex-start; gap:10px; font-size:.9rem; color:var(--ink-soft); }
.mc-contact-info .ico{
width:30px; height:30px; border-radius:8px; background:#F4EEFB; color:var(--indigo);
display:flex; align-items:center; justify-content:center; flex-shrink:0;
}
</style>
<div class="mc-contact-hero">
<h1>Contact us</h1>
<p>Questions about admissions, fees or anything else - send us a note and we'll get back to you shortly.</p>
</div>
<div class="mc-contact-body">
<div class="mc-contact-card">
<section class="s_website_form" data-snippet="s_website_form">
<form id="contactus_form" action="/website/form/" method="post" enctype="multipart/form-data" class="o_mark_required" data-mark="*" data-model_name="mail.mail" data-success-mode="redirect" data-success-page="/contactus-thank-you" data-pre-fill="true">
<div class="s_website_form_rows row s_col_no_bgcolor">
<div class="mb-3 col-lg-6 s_website_form_field s_website_form_custom s_website_form_required" data-type="char" data-name="Field">
<label class="s_website_form_label" for="contact1">
<span class="s_website_form_label_content">Name</span>
<span class="s_website_form_mark"> *</span>
</label>
<input id="contact1" type="text" class="form-control s_website_form_input" name="name" required="" data-fill-with="name"/>
</div>
<div class="mb-3 col-lg-6 s_website_form_field s_website_form_custom" data-type="char" data-name="Field">
<label class="s_website_form_label" for="contact2">
<span class="s_website_form_label_content">Phone Number</span>
</label>
<input id="contact2" type="tel" class="form-control s_website_form_input" name="phone" data-fill-with="phone"/>
</div>
<div class="mb-3 col-lg-6 s_website_form_field s_website_form_required s_website_form_model_required" data-type="email" data-name="Field">
<label class="s_website_form_label" for="contact3">
<span class="s_website_form_label_content">Email</span>
<span class="s_website_form_mark"> *</span>
</label>
<input id="contact3" type="email" class="form-control s_website_form_input" name="email_from" required="" data-fill-with="email"/>
</div>
<div class="mb-3 col-lg-6 s_website_form_field s_website_form_custom" data-type="char" data-name="Field">
<label class="s_website_form_label" for="contact4">
<span class="s_website_form_label_content">Company</span>
</label>
<input id="contact4" type="text" class="form-control s_website_form_input" name="company" data-fill-with="commercial_company_name"/>
</div>
<div class="mb-3 col-12 s_website_form_field s_website_form_required s_website_form_model_required" data-type="char" data-name="Field">
<label class="s_website_form_label" for="contact5">
<span class="s_website_form_label_content">Subject</span>
<span class="s_website_form_mark"> *</span>
</label>
<input id="contact5" type="text" class="form-control s_website_form_input" name="subject" required=""/>
</div>
<div class="mb-3 col-12 s_website_form_field s_website_form_custom s_website_form_required" data-type="text" data-name="Field">
<label class="s_website_form_label" for="contact6">
<span class="s_website_form_label_content">Question</span>
<span class="s_website_form_mark"> *</span>
</label>
<textarea id="contact6" class="form-control s_website_form_input" name="description" required="" rows="6"></textarea>
</div>
<div class="mb-3 col-12 s_website_form_field s_website_form_dnone">
<input id="contact7" type="hidden" class="form-control s_website_form_input" name="email_to" t-att-value="res_company.email"/>
</div>
<div class="mb-0 py-2 col-12 s_website_form_submit s_website_form_no_submit_label text-end" data-name="Submit Button">
<div class="s_website_form_label"/>
<a href="#" role="button" class="btn btn-primary s_website_form_send">Submit</a>
<span id="s_website_form_result"></span>
</div>
</div>
</form>
</section>
</div>
<div class="mc-contact-card mc-contact-info">
<h5 t-esc="res_company.name"/>
<ul>
<li t-if="res_company.street">
<span class="ico"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M12 21s7-6.3 7-11.5A7 7 0 0 0 5 9.5C5 14.7 12 21 12 21z"/><circle cx="12" cy="9.5" r="2.3"/></svg></span>
<span>
<t t-esc="res_company.street"/><br t-if="res_company.city"/>
<t t-esc="res_company.city"/><t t-if="res_company.country_id"><t t-esc="', '"/><t t-esc="res_company.country_id.name"/></t>
</span>
</li>
<li t-if="res_company.phone">
<span class="ico"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M22 16.9v3a2 2 0 0 1-2.2 2 19.8 19.8 0 0 1-8.6-3.1 19.5 19.5 0 0 1-6-6 19.8 19.8 0 0 1-3.1-8.6A2 2 0 0 1 4.1 2h3a2 2 0 0 1 2 1.7c.1 1 .3 2 .7 3a2 2 0 0 1-.5 2.1L8 10a16 16 0 0 0 6 6l1.2-1.3a2 2 0 0 1 2.1-.5c1 .4 2 .6 3 .7a2 2 0 0 1 1.7 2z"/></svg></span>
<span t-esc="res_company.phone"/>
</li>
<li t-if="res_company.email">
<span class="ico"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="M3 7l9 6 9-6"/></svg></span>
<span t-esc="res_company.email"/>
</li>
</ul>
</div>
</div>
</div>
</xpath>
</template>
</odoo>

View File

@ -0,0 +1,137 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- website.footer_custom (stock Odoo's default footer, active on
every website page - homepage, admissions, contact us, all of
them) hardcodes a "Products / Services / Legal" link list that
goes nowhere (href="#"), a generic "we are a team of
passionate people" paragraph, dead social icons pointing at
unconfigured /website/social/* routes, and the same
yourcompany.example.com / +1 555-555-5556 placeholder already
fixed on the contact us page itself - all of it visible on
every single page of the site, not just one. Real find: fixing
the contact us page alone left this identical placeholder
content sitting in the footer underneath it.
Replaced with real links into this app (Home, Admissions,
Contact us) and the active school's own name/contact info via
res_company, the same dynamic source the contact us page uses -
no fixed Metatroncube branding here, this footer is genuinely
per-school content and swaps with the active company like the
admission form does. -->
<!-- A fourth occurrence, in the header this time, not the footer:
website.header_text_element is Odoo's generic, reusable
"phone/email nav widget" snippet - a library of variants
(sentence, list, phone_mail, mail, mail_stretched, a default)
every one of which hardcodes +1 555-555-5556 /
info@yourcompany.example.com. Found only by looking at the
live nav bar after the footer fix above made it obvious the
same fake number was still showing up higher on the page.
First pass here fixed only "phone_mail", assumed to be this
site's active variant - wrong. The two spots actually
rendering on the live page (desktop nav, mobile offcanvas nav)
both turned out to use the plain default (t-else) branch
instead, still showing the fake number after that first fix,
caught by re-checking the real page rather than trusting the
assumption. Fixing every variant with a phone or email in it
(phone_mail, mail, mail_stretched, the default) rather than
guessing again which one a given header configuration
actually selects - "list" and "sentence" carry no contact
info at all and are left alone. -->
<template id="header_phone_mail_branding" inherit_id="website.header_text_element" name="Header phone/email - real company info">
<!-- phone_mail: both phone and email, no icon/text lookalikes -->
<xpath expr="//t[@t-elif=&quot;_txt_elt_content == 'phone_mail'&quot;]//a[starts-with(@href,'tel:')]" position="attributes">
<attribute name="href"></attribute>
<attribute name="t-attf-href">tel:#{res_company.phone}</attribute>
</xpath>
<xpath expr="//t[@t-elif=&quot;_txt_elt_content == 'phone_mail'&quot;]//a[@t-attf-href][contains(@t-attf-href,'tel')]//span[hasclass('o_force_ltr')]" position="replace">
<span class="o_force_ltr" t-esc="res_company.phone"/>
</xpath>
<xpath expr="//t[@t-elif=&quot;_txt_elt_content == 'phone_mail'&quot;]//a[starts-with(@href,'mailto:')]" position="attributes">
<attribute name="href"></attribute>
<attribute name="t-attf-href">mailto:#{res_company.email}</attribute>
</xpath>
<xpath expr="//t[@t-elif=&quot;_txt_elt_content == 'phone_mail'&quot;]//a[@t-attf-href][contains(@t-attf-href,'mailto')]/small" position="replace">
<small>
<i class="fa fa-1x fa-fw fa-envelope me-1"/>
<t t-esc="res_company.email"/>
</small>
</xpath>
<!-- mail: email only -->
<xpath expr="//t[@t-elif=&quot;_txt_elt_content == 'mail'&quot;]//a[starts-with(@href,'mailto:')]" position="attributes">
<attribute name="href"></attribute>
<attribute name="t-attf-href">mailto:#{res_company.email}</attribute>
</xpath>
<xpath expr="//t[@t-elif=&quot;_txt_elt_content == 'mail'&quot;]//a[@t-attf-href]/small" position="replace">
<small><i class="fa fa-1x fa-fw fa-envelope me-1"/> <t t-esc="res_company.email"/></small>
</xpath>
<!-- mail_stretched: phone only, the "stretched clickable nav pill" look -->
<xpath expr="//t[@t-elif=&quot;_txt_elt_content == 'mail_stretched'&quot;]//a[starts-with(@href,'tel:')]" position="attributes">
<attribute name="href"></attribute>
<attribute name="t-attf-href">tel:#{res_company.phone}</attribute>
</xpath>
<xpath expr="//t[@t-elif=&quot;_txt_elt_content == 'mail_stretched'&quot;]//a[@t-attf-href]//span[hasclass('o_force_ltr')]" position="replace">
<span class="o_force_ltr"><small t-esc="res_company.phone"/></span>
</xpath>
<!-- the default branch: phone only - this is the one actually
rendering on this site's own header, both desktop and
mobile. -->
<xpath expr="//t[@t-else][not(@t-elif)]//a[starts-with(@href,'tel:')]" position="attributes">
<attribute name="href"></attribute>
<attribute name="t-attf-href">tel:#{res_company.phone}</attribute>
</xpath>
<xpath expr="//t[@t-else][not(@t-elif)]//a[@t-attf-href]//span[hasclass('o_force_ltr')]" position="replace">
<span class="o_force_ltr"><small t-esc="res_company.phone"/></span>
</xpath>
</template>
<template id="footer_branding" inherit_id="website.footer_custom" name="Footer - real links, no placeholder">
<xpath expr="//div[@id='footer']" position="replace">
<div id="footer" class="oe_structure oe_structure_solo border text-break" t-ignore="true" t-if="not no_footer" style="--box-border-left-width: 0px; --box-border-right-width: 0px;">
<style>
.mc-footer{ font-family:'Work Sans', system-ui, sans-serif; }
.mc-footer h5{ font-family:'Manrope', system-ui, sans-serif; font-weight:800; }
.mc-footer .ico{ display:inline-flex; width:16px; }
</style>
<section class="s_text_block pt40 pb16 mc-footer" data-snippet="s_text_block" data-name="Container">
<div class="container">
<div class="row">
<div class="col-lg-3 pt24 pb24">
<h5>Quick Links</h5>
<ul class="list-unstyled">
<li><a href="/">Home</a></li>
<li><a href="/admissions/apply">Admissions</a></li>
<li><a href="/contactus">Contact us</a></li>
</ul>
</div>
<div class="col-lg-5 pt24 pb24">
<h5 t-esc="res_company.name"/>
<p>Admissions, fees, attendance, timetable, exams and the parent portal - all in one connected system, all reachable from this site.</p>
</div>
<div class="col-lg-4 pt24 pb24">
<h5>Get in touch</h5>
<ul class="list-unstyled">
<li t-if="res_company.phone">
<span class="ico"><i class="fa fa-phone fa-fw me-2"/></span>
<a t-attf-href="tel:#{res_company.phone}"><span class="o_force_ltr" t-esc="res_company.phone"/></a>
</li>
<li t-if="res_company.email">
<span class="ico"><i class="fa fa-envelope fa-fw me-2"/></span>
<a t-attf-href="mailto:#{res_company.email}" t-esc="res_company.email"/>
</li>
<li t-if="res_company.city">
<span class="ico"><i class="fa fa-map-marker fa-fw me-2"/></span>
<span t-esc="res_company.city"/>
</li>
</ul>
</div>
</div>
</div>
</section>
</div>
</xpath>
</template>
</odoo>

View File

@ -58,11 +58,7 @@
linear-gradient(180deg, var(--navy) 0%, var(--navy-2) 100%); linear-gradient(180deg, var(--navy) 0%, var(--navy-2) 100%);
padding:26px 0 70px; color:#EDEFF7; padding:26px 0 70px; color:#EDEFF7;
} }
.mc-topbar{ display:flex; align-items:center; justify-content:space-between; gap:16px; padding:8px 0 46px; } .mc-topbar{ padding:8px 0 46px; }
.mc-logo{ display:flex; align-items:center; gap:11px; }
.mc-logo .mark{ width:34px; height:34px; }
.mc-logo .wordmark{ font-family:'Manrope',sans-serif; font-weight:800; font-size:1.28rem; color:#fff; line-height:1; }
.mc-logo .tagline{ font-size:.6rem; letter-spacing:.16em; color:#AEB6D6; margin-top:3px; }
.mc-pill{ .mc-pill{
font-size:.72rem; letter-spacing:.06em; text-transform:uppercase; font-weight:600; font-size:.72rem; letter-spacing:.06em; text-transform:uppercase; font-weight:600;
color:#EFE9FF; background:linear-gradient(90deg, var(--indigo), var(--indigo-2)); color:#EFE9FF; background:linear-gradient(90deg, var(--indigo), var(--indigo-2));
@ -141,18 +137,12 @@
<!-- ================= HERO ================= --> <!-- ================= HERO ================= -->
<div class="mc-hero"> <div class="mc-hero">
<div class="mc-shell"> <div class="mc-shell">
<!-- The site's own nav (website.layout's header)
already shows the real Metatroncube logo -
set via hooks.py onto website.logo - on
every page including this one, so the hero
doesn't repeat it. -->
<div class="mc-topbar"> <div class="mc-topbar">
<div class="mc-logo">
<svg class="mark" viewBox="0 0 40 40" fill="none">
<path d="M20 3 L35 11.5 V28.5 L20 37 L5 28.5 V11.5 Z" stroke="#8F86D6" stroke-width="1.6"/>
<path d="M20 3 V20 L35 11.5" stroke="#6FA8DE" stroke-width="1.6"/>
<path d="M20 20 L5 11.5 M20 20 V37" stroke="#6FA8DE" stroke-width="1.6"/>
</svg>
<div>
<div class="wordmark">Metatroncube</div>
<div class="tagline">SMART · INTEGRATED · EDUCATION</div>
</div>
</div>
<span class="mc-pill">Built for Schools</span> <span class="mc-pill">Built for Schools</span>
</div> </div>

View File

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- A third, separate "Powered by Odoo" occurrence, distinct from
the two already hidden in portal_branding_templates.xml
(portal.portal_record_sidebar's text link, and
web.brand_promotion's badge): web.login_layout hardcodes its
own "Powered by <span>Odoo</span>" link straight in the
template, reached by every backend login screen - the very
first thing anyone doing this demo sees before they've even
logged in. Same brand/type system as the rest of the site
(Manrope + Work Sans, navy/indigo) wraps it; the logo shown
stays /web/binary/company_logo, i.e. whichever school is
actually logging in - a login screen is inherently
per-tenant, unlike the marketing homepage. -->
<template id="login_layout_branding" inherit_id="web.login_layout" name="Login - Metatroncube design">
<xpath expr="//a[@href='https://www.odoo.com?utm_source=db&amp;utm_medium=auth']" position="replace"/>
<xpath expr="//div[hasclass('container') and hasclass('py-5')]" position="attributes">
<attribute name="class" add="mc-login-shell" separator=" "/>
</xpath>
<xpath expr="//div[hasclass('container') and hasclass('py-5')]" position="before">
<link rel="preconnect" href="https://fonts.googleapis.com"/>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="crossorigin"/>
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@600;700;800&amp;family=Work+Sans:wght@400;500;600;700&amp;display=swap" rel="stylesheet"/>
<div class="mc-login-bg"/>
<style>
.mc-login-bg{
position:fixed; inset:0; z-index:-1;
background:
radial-gradient(900px 420px at 85% -10%, rgba(75,63,146,.55), transparent 60%),
linear-gradient(180deg, #131B2E 0%, #1C2A46 100%);
}
.mc-login-shell{ font-family:'Work Sans', system-ui, sans-serif; padding-top:8vh !important; }
.mc-login-shell .card{
border-radius:16px !important; box-shadow:0 24px 60px -20px rgba(0,0,0,.45);
padding:6px;
}
.mc-login-shell h1, .mc-login-shell .form-label{
font-family:'Manrope', system-ui, sans-serif;
}
.mc-login-shell .form-label{ font-weight:600; font-size:.85rem; }
.mc-login-shell .form-control{ border-radius:9px; padding:10px 12px; }
.mc-login-shell .form-control:focus{ border-color:#4B3F92; box-shadow:0 0 0 3px rgba(75,63,146,.15); }
.mc-login-shell .btn-primary{
background:linear-gradient(90deg, #4B3F92, #2E63B8); border:none;
border-radius:9px; padding:10px 0; font-weight:600; font-family:'Manrope', sans-serif;
}
.mc-login-shell .btn-primary:hover{ filter:brightness(1.08); }
</style>
</xpath>
</template>
</odoo>