diff --git a/addons/mc_education_admission/views/website_admission_templates.xml b/addons/mc_education_admission/views/website_admission_templates.xml index 9534405..d973bb7 100644 --- a/addons/mc_education_admission/views/website_admission_templates.xml +++ b/addons/mc_education_admission/views/website_admission_templates.xml @@ -1,5 +1,21 @@ + + + diff --git a/addons/mc_education_theme/__init__.py b/addons/mc_education_theme/__init__.py index a319498..614670c 100644 --- a/addons/mc_education_theme/__init__.py +++ b/addons/mc_education_theme/__init__.py @@ -1,4 +1,7 @@ # No models: every branding field this module configures (res.company # logo/name/primary_color/secondary_color/currency, res.partner address) -# already exists in stock Odoo. This module is data (demo/) and one view -# override (views/), nothing more. +# already exists in stock Odoo. This module is data (demo/) and view +# 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 . +from .hooks import post_init_hook diff --git a/addons/mc_education_theme/__manifest__.py b/addons/mc_education_theme/__manifest__.py index 2459c2b..5c35d5f 100644 --- a/addons/mc_education_theme/__manifest__.py +++ b/addons/mc_education_theme/__manifest__.py @@ -19,6 +19,9 @@ "data": [ "views/portal_branding_templates.xml", "views/homepage_templates.xml", + "views/contactus_templates.xml", + "views/login_templates.xml", + "views/footer_templates.xml", ], "demo": [ "demo/mc_theme_demo.xml", @@ -26,4 +29,5 @@ ], "installable": True, "application": False, + "post_init_hook": "post_init_hook", } diff --git a/addons/mc_education_theme/demo/mc_theme_demo.xml b/addons/mc_education_theme/demo/mc_theme_demo.xml index 4d2b290..8f72429 100644 --- a/addons/mc_education_theme/demo/mc_theme_demo.xml +++ b/addons/mc_education_theme/demo/mc_theme_demo.xml @@ -25,10 +25,24 @@ run, never as a later "apply a theme" change. The brand-new second company below has no such history, so its currency is set safely at creation. --> + St. Aloysius Public School - + 45 Avinashi Road Coimbatore + + +91 422 435 6789 + info@staloysiuscbe.edu.in #1B3A5C #D4A017 @@ -36,9 +50,12 @@ Waterloo Heights Academy - - + 75 University Avenue West Waterloo + + + +1 519 888 4400 + info@waterlooheights.ca #1E5C3A #C0C0C0 diff --git a/addons/mc_education_theme/hooks.py b/addons/mc_education_theme/hooks.py new file mode 100644 index 0000000..e7c0be5 --- /dev/null +++ b/addons/mc_education_theme/hooks.py @@ -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 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}) diff --git a/addons/mc_education_theme/static/img/favicon.png b/addons/mc_education_theme/static/img/favicon.png new file mode 100644 index 0000000..1823514 Binary files /dev/null and b/addons/mc_education_theme/static/img/favicon.png differ diff --git a/addons/mc_education_theme/static/img/logo.png b/addons/mc_education_theme/static/img/logo.png new file mode 100644 index 0000000..29569bc Binary files /dev/null and b/addons/mc_education_theme/static/img/logo.png differ diff --git a/addons/mc_education_theme/tests/test_theme.py b/addons/mc_education_theme/tests/test_theme.py index f629a46..3bc50ca 100644 --- a/addons/mc_education_theme/tests/test_theme.py +++ b/addons/mc_education_theme/tests/test_theme.py @@ -1,3 +1,5 @@ +from lxml import etree + from odoo.tests.common import HttpCase, TransactionCase @@ -23,6 +25,20 @@ class TestTheme(TransactionCase): # 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, @@ -71,6 +87,94 @@ class TestTheme(TransactionCase): 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 (""), 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 @@ -120,6 +224,16 @@ class TestTheme(TransactionCase): 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() @@ -134,3 +248,11 @@ class TestMarketingHomepageHttp(HttpCase): 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) diff --git a/addons/mc_education_theme/views/contactus_templates.xml b/addons/mc_education_theme/views/contactus_templates.xml new file mode 100644 index 0000000..966bed6 --- /dev/null +++ b/addons/mc_education_theme/views/contactus_templates.xml @@ -0,0 +1,161 @@ + + + + + diff --git a/addons/mc_education_theme/views/footer_templates.xml b/addons/mc_education_theme/views/footer_templates.xml new file mode 100644 index 0000000..f982dad --- /dev/null +++ b/addons/mc_education_theme/views/footer_templates.xml @@ -0,0 +1,137 @@ + + + + + + + + diff --git a/addons/mc_education_theme/views/homepage_templates.xml b/addons/mc_education_theme/views/homepage_templates.xml index 34c2a90..b190b77 100644 --- a/addons/mc_education_theme/views/homepage_templates.xml +++ b/addons/mc_education_theme/views/homepage_templates.xml @@ -58,11 +58,7 @@ linear-gradient(180deg, var(--navy) 0%, var(--navy-2) 100%); padding:26px 0 70px; color:#EDEFF7; } - .mc-topbar{ display:flex; align-items:center; justify-content:space-between; gap:16px; 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-topbar{ padding:8px 0 46px; } .mc-pill{ font-size:.72rem; letter-spacing:.06em; text-transform:uppercase; font-weight:600; color:#EFE9FF; background:linear-gradient(90deg, var(--indigo), var(--indigo-2)); @@ -141,18 +137,12 @@
+
- Built for Schools
diff --git a/addons/mc_education_theme/views/login_templates.xml b/addons/mc_education_theme/views/login_templates.xml new file mode 100644 index 0000000..8e7e1fd --- /dev/null +++ b/addons/mc_education_theme/views/login_templates.xml @@ -0,0 +1,53 @@ + + + + +