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 @@
+
+
+
+
+
+
+
+
+ Membership
+
+ View your membership status, renew, or download your card
+
+
+
+ My Events
+
+ See events you have registered for and download your tickets
+
+
+
+ School
+
+ See your children's classes, schedule, and attendance
+
+
+
+ My Benefits
+
+ Benefits your membership tier entitles you to
+
+
+
+ My Classifieds
+
+ Manage the listings you have posted
+
+
+