diff --git a/addons/community_portal/__init__.py b/addons/community_portal/__init__.py index e69de29..e046e49 100644 --- a/addons/community_portal/__init__.py +++ b/addons/community_portal/__init__.py @@ -0,0 +1 @@ +from . import controllers diff --git a/addons/community_portal/__manifest__.py b/addons/community_portal/__manifest__.py index fb77ab5..84282ae 100644 --- a/addons/community_portal/__manifest__.py +++ b/addons/community_portal/__manifest__.py @@ -24,8 +24,11 @@ a missing module: 'currency': 'USD', 'depends': [ 'portal', + 'website', + ], + 'data': [ + 'views/portal_dashboard_templates.xml', ], - 'data': [], 'demo': [], 'images': ['static/description/banner.png'], 'application': False, diff --git a/addons/community_portal/controllers/__init__.py b/addons/community_portal/controllers/__init__.py new file mode 100644 index 0000000..8c3feb6 --- /dev/null +++ b/addons/community_portal/controllers/__init__.py @@ -0,0 +1 @@ +from . import portal diff --git a/addons/community_portal/controllers/portal.py b/addons/community_portal/controllers/portal.py new file mode 100644 index 0000000..621700f --- /dev/null +++ b/addons/community_portal/controllers/portal.py @@ -0,0 +1,57 @@ +from odoo import http +from odoo.addons.portal.controllers.portal import CustomerPortal +from odoo.http import request + +SOFT_DETECTED_MODULES = ( + 'community_membership', + 'event_qr_ticketing', + 'community_school', + 'community_benefits', + 'community_classifieds', +) + + +class CommunityPortalDashboard(CustomerPortal): + + def _get_installed_community_modules(self): + """Soft-detect which Community OS product modules are installed.""" + installed = request.env['ir.module.module'].sudo().search([ + ('name', 'in', list(SOFT_DETECTED_MODULES)), + ('state', '=', 'installed'), + ]) + return set(installed.mapped('name')) + + def _prepare_home_portal_values(self, counters): + values = super()._prepare_home_portal_values(counters) + partner = request.env.user.partner_id + installed = self._get_installed_community_modules() + + if 'event_registration_count' in counters and 'event_qr_ticketing' in installed: + values['event_registration_count'] = request.env['event.registration'].sudo().search_count([ + ('partner_id', '=', partner.id), + ]) + + if 'school_enrollment_count' in counters and 'community_school' in installed: + values['school_enrollment_count'] = request.env['community.school.student'].sudo().search_count([ + ('parent_partner_id', '=', partner.id), + ]) + + if 'benefits_count' in counters and 'community_benefits' in installed: + Benefit = request.env['community.benefit'].sudo() + entitled = Benefit.search(Benefit._entitled_domain_for_partner(partner)) + values['benefits_count'] = len(entitled.filtered(lambda b: b.is_entitled(partner))) + + if 'classifieds_count' in counters and 'community_classifieds' in installed: + values['classifieds_count'] = request.env['community.classified'].sudo().search_count([ + ('poster_partner_id', '=', partner.id), + ]) + + return values + + @http.route(['/my/school'], type='http', auth='user', website=True) + def portal_my_school(self, **kwargs): + partner = request.env.user.partner_id + students = request.env['community.school.student'].sudo().search([ + ('parent_partner_id', '=', partner.id), + ]) + return request.render('community_portal.portal_my_school', {'students': students}) diff --git a/addons/community_portal/tests/__init__.py b/addons/community_portal/tests/__init__.py index e69de29..31c4afb 100644 --- a/addons/community_portal/tests/__init__.py +++ b/addons/community_portal/tests/__init__.py @@ -0,0 +1 @@ +from . import test_portal_dashboard diff --git a/addons/community_portal/tests/test_portal_dashboard.py b/addons/community_portal/tests/test_portal_dashboard.py new file mode 100644 index 0000000..c9d4308 --- /dev/null +++ b/addons/community_portal/tests/test_portal_dashboard.py @@ -0,0 +1,34 @@ +from odoo.tests.common import HttpCase, tagged + +MODULE_TO_CARD_TITLE = { + 'community_membership': 'Membership', + 'event_qr_ticketing': 'My Events', + 'community_school': 'School', + 'community_benefits': 'My Benefits', + 'community_classifieds': 'My Classifieds', +} + + +@tagged('post_install', '-at_install') +class TestPortalDashboard(HttpCase): + + def test_portal_home_renders_without_error(self): + self.authenticate('admin', 'admin') + response = self.url_open('/my') + self.assertEqual(response.status_code, 200) + + def test_dashboard_only_shows_cards_for_installed_modules(self): + """The dashboard must never crash on a missing module, and must not show + a card for a module that isn't installed.""" + installed = set(self.env['ir.module.module'].search([ + ('name', 'in', list(MODULE_TO_CARD_TITLE)), ('state', '=', 'installed'), + ]).mapped('name')) + + self.authenticate('admin', 'admin') + response = self.url_open('/my') + self.assertEqual(response.status_code, 200) + content = response.content.decode() + + for module_name, card_title in MODULE_TO_CARD_TITLE.items(): + if module_name not in installed: + self.assertNotIn(card_title, content, f"{card_title} card should be hidden when {module_name} is not installed") diff --git a/addons/community_portal/views/portal_dashboard_templates.xml b/addons/community_portal/views/portal_dashboard_templates.xml new file mode 100644 index 0000000..834236c --- /dev/null +++ b/addons/community_portal/views/portal_dashboard_templates.xml @@ -0,0 +1,72 @@ + + + + + + diff --git a/addons/community_theme_base/__init__.py b/addons/community_theme_base/__init__.py index e69de29..0650744 100644 --- a/addons/community_theme_base/__init__.py +++ b/addons/community_theme_base/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/addons/community_theme_base/__manifest__.py b/addons/community_theme_base/__manifest__.py index a7bb227..2d94339 100644 --- a/addons/community_theme_base/__manifest__.py +++ b/addons/community_theme_base/__manifest__.py @@ -25,7 +25,10 @@ layout that reads these tokens. 'depends': [ 'website', ], - 'data': [], + 'data': [ + 'views/res_config_settings_views.xml', + 'views/theme_templates.xml', + ], 'demo': [], 'images': ['static/description/banner.png'], 'application': False, diff --git a/addons/community_theme_base/models/__init__.py b/addons/community_theme_base/models/__init__.py new file mode 100644 index 0000000..0deb68c --- /dev/null +++ b/addons/community_theme_base/models/__init__.py @@ -0,0 +1 @@ +from . import res_config_settings diff --git a/addons/community_theme_base/models/res_config_settings.py b/addons/community_theme_base/models/res_config_settings.py new file mode 100644 index 0000000..02f77b5 --- /dev/null +++ b/addons/community_theme_base/models/res_config_settings.py @@ -0,0 +1,36 @@ +from odoo import fields, models + +DEFAULT_PRIMARY_COLOR = '#2C3E50' +DEFAULT_SECONDARY_COLOR = '#7F8C8D' +DEFAULT_ACCENT_COLOR = '#3498DB' +DEFAULT_FONT = 'Inter, sans-serif' + + +class ResConfigSettings(models.TransientModel): + _inherit = 'res.config.settings' + + theme_primary_color = fields.Char( + string='Primary Colour', config_parameter='community_theme_base.primary_color', + default=DEFAULT_PRIMARY_COLOR, + ) + theme_secondary_color = fields.Char( + string='Secondary Colour', config_parameter='community_theme_base.secondary_color', + default=DEFAULT_SECONDARY_COLOR, + ) + theme_accent_color = fields.Char( + string='Accent Colour', config_parameter='community_theme_base.accent_color', + default=DEFAULT_ACCENT_COLOR, + ) + theme_logo = fields.Binary( + string='Logo', config_parameter='community_theme_base.logo', + help="Shown in the website navbar and on generated PDF reports/cards. " + "Falls back to the company logo if not set.", + ) + theme_heading_font = fields.Char( + string='Heading Font', config_parameter='community_theme_base.heading_font', + default=DEFAULT_FONT, + ) + theme_body_font = fields.Char( + string='Body Font', config_parameter='community_theme_base.body_font', + default=DEFAULT_FONT, + ) diff --git a/addons/community_theme_base/tests/__init__.py b/addons/community_theme_base/tests/__init__.py index e69de29..cdf175d 100644 --- a/addons/community_theme_base/tests/__init__.py +++ b/addons/community_theme_base/tests/__init__.py @@ -0,0 +1,2 @@ +from . import test_theme +from . import test_theme_http diff --git a/addons/community_theme_base/tests/test_theme.py b/addons/community_theme_base/tests/test_theme.py new file mode 100644 index 0000000..e9cee99 --- /dev/null +++ b/addons/community_theme_base/tests/test_theme.py @@ -0,0 +1,23 @@ +from odoo.tests.common import TransactionCase, tagged + + +@tagged('post_install', '-at_install') +class TestTheme(TransactionCase): + + def test_default_colors_are_neutral(self): + primary = self.env['ir.config_parameter'].sudo().get_param( + 'community_theme_base.primary_color', '#2C3E50' + ) + self.assertEqual(primary, '#2C3E50') + html = self.env['ir.qweb']._render('community_theme_base.theme_css_vars') + self.assertIn('#2C3E50', html, "Unconfigured deployments should render the neutral default") + + def test_custom_color_reflected_in_css_vars_render(self): + self.env['ir.config_parameter'].sudo().set_param('community_theme_base.primary_color', '#123456') + html = self.env['ir.qweb']._render('community_theme_base.theme_css_vars') + self.assertIn('#123456', html) + + def test_report_basic_layout_includes_theme_css_vars(self): + self.env['ir.config_parameter'].sudo().set_param('community_theme_base.primary_color', '#FEDCBA') + html = self.env['ir.qweb']._render('web.basic_layout', {'doc': self.env.company}) + self.assertIn('#FEDCBA', html) diff --git a/addons/community_theme_base/tests/test_theme_http.py b/addons/community_theme_base/tests/test_theme_http.py new file mode 100644 index 0000000..0b41fce --- /dev/null +++ b/addons/community_theme_base/tests/test_theme_http.py @@ -0,0 +1,11 @@ +from odoo.tests.common import HttpCase, tagged + + +@tagged('post_install', '-at_install') +class TestThemeHttp(HttpCase): + + def test_theme_css_vars_present_on_public_page(self): + self.env['ir.config_parameter'].sudo().set_param('community_theme_base.primary_color', '#654321') + response = self.url_open('/') + self.assertEqual(response.status_code, 200) + self.assertIn(b'#654321', response.content) diff --git a/addons/community_theme_base/views/res_config_settings_views.xml b/addons/community_theme_base/views/res_config_settings_views.xml new file mode 100644 index 0000000..1c38d16 --- /dev/null +++ b/addons/community_theme_base/views/res_config_settings_views.xml @@ -0,0 +1,37 @@ + + + + res.config.settings.view.form.theme + res.config.settings + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addons/community_theme_base/views/theme_templates.xml b/addons/community_theme_base/views/theme_templates.xml new file mode 100644 index 0000000..80ef21e --- /dev/null +++ b/addons/community_theme_base/views/theme_templates.xml @@ -0,0 +1,41 @@ + + + + + + + + + +