-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #56 from mkumar-02/17.0-sr-dashboard
Added Social Registry Dashboard Module.
- Loading branch information
Showing
17 changed files
with
537 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# OpenG2P Social Registry Dashboard | ||
|
||
Refer to https://docs.openg2p.org. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,170 @@ | ||
# Part of OpenG2P. See LICENSE file for full copyright and licensing details. | ||
|
||
from . import models | ||
|
||
from odoo import _ | ||
from odoo.exceptions import MissingError | ||
import logging | ||
|
||
_logger = logging.getLogger(__name__) | ||
|
||
|
||
def init_materialized_view(env): | ||
""" | ||
Initializes or refreshes the materialized views for the res_partner_dashboard_data. | ||
""" | ||
cr = env.cr | ||
|
||
matviews_to_check = [ | ||
"g2p_gender_count_view", | ||
"g2p_age_distribution_view", | ||
"g2p_total_registrants_view", | ||
"g2p_sr_dashboard_data", | ||
] | ||
|
||
try: | ||
cr.execute( | ||
""" | ||
SELECT matviewname | ||
FROM pg_matviews | ||
WHERE matviewname IN %s; | ||
""", | ||
(tuple(matviews_to_check),), | ||
) | ||
|
||
existing_views = set([row[0] for row in cr.fetchall()]) | ||
|
||
if "g2p_gender_count_view" not in existing_views: | ||
gender_query = """ | ||
CREATE MATERIALIZED VIEW g2p_gender_count_view AS | ||
SELECT | ||
rp.company_id, | ||
gt.code AS gender, | ||
COUNT(rp.id) AS gender_count | ||
FROM | ||
res_partner rp | ||
LEFT JOIN | ||
gender_type gt ON rp.gender = gt.value | ||
WHERE | ||
rp.is_registrant = True | ||
AND rp.active = True | ||
AND rp.is_group = False | ||
GROUP BY | ||
rp.company_id, gt.code; | ||
""" | ||
cr.execute(gender_query) | ||
_logger.info("Created materialized view: g2p_gender_count_view") | ||
|
||
if "g2p_age_distribution_view" not in existing_views: | ||
age_distribution_query = """ | ||
CREATE MATERIALIZED VIEW g2p_age_distribution_view AS | ||
SELECT | ||
rp.company_id, | ||
jsonb_build_object( | ||
'below_18', COUNT(rp.id) FILTER ( | ||
WHERE EXTRACT(YEAR FROM AGE(rp.birthdate)) < 18 | ||
), | ||
'18_to_30', COUNT(rp.id) FILTER ( | ||
WHERE EXTRACT(YEAR FROM AGE(rp.birthdate)) BETWEEN 18 AND 30 | ||
), | ||
'31_to_40', COUNT(rp.id) FILTER ( | ||
WHERE EXTRACT(YEAR FROM AGE(rp.birthdate)) BETWEEN 31 AND 40 | ||
), | ||
'41_to_50', COUNT(rp.id) FILTER ( | ||
WHERE EXTRACT(YEAR FROM AGE(rp.birthdate)) BETWEEN 41 AND 50 | ||
), | ||
'above_50', COUNT(rp.id) FILTER ( | ||
WHERE EXTRACT(YEAR FROM AGE(rp.birthdate)) > 50 | ||
) | ||
) AS age_distribution | ||
FROM | ||
res_partner rp | ||
WHERE | ||
rp.is_registrant = True | ||
AND rp.active = True | ||
AND rp.is_group = False | ||
GROUP BY | ||
rp.company_id; | ||
""" | ||
cr.execute(age_distribution_query) | ||
_logger.info("Created materialized view: g2p_age_distribution_view") | ||
|
||
if "g2p_total_registrants_view" not in existing_views: | ||
total_registrants_query = """ | ||
CREATE MATERIALIZED VIEW g2p_total_registrants_view AS | ||
SELECT | ||
rp.company_id, | ||
jsonb_build_object( | ||
'total_individuals', COUNT(rp.id) FILTER (WHERE rp.is_group = False), | ||
'total_groups', COUNT(rp.id) FILTER (WHERE rp.is_group = True) | ||
) AS total_registrants | ||
FROM | ||
res_partner rp | ||
WHERE | ||
rp.is_registrant = True | ||
AND rp.active = True | ||
GROUP BY | ||
rp.company_id; | ||
""" | ||
cr.execute(total_registrants_query) | ||
_logger.info("Created materialized view: g2p_total_registrants_view") | ||
|
||
if "g2p_sr_dashboard_data" not in existing_views: | ||
dashboard_query = """ | ||
CREATE MATERIALIZED VIEW g2p_sr_dashboard_data AS | ||
SELECT | ||
trv.company_id, | ||
trv.total_registrants, | ||
COALESCE( | ||
jsonb_object_agg(gc.gender, gc.gender_count) FILTER (WHERE gc.gender IS NOT NULL), | ||
'{}' | ||
) AS gender_spec, | ||
adv.age_distribution | ||
FROM | ||
g2p_total_registrants_view trv | ||
LEFT JOIN | ||
g2p_gender_count_view gc ON trv.company_id = gc.company_id | ||
LEFT JOIN | ||
g2p_age_distribution_view adv ON trv.company_id = adv.company_id | ||
GROUP BY | ||
trv.company_id, trv.total_registrants, adv.age_distribution; | ||
""" | ||
cr.execute(dashboard_query) | ||
_logger.info("Created materialized view: g2p_sr_dashboard_data") | ||
|
||
except Exception as exc: | ||
_logger.error("Error while creating materialized views: %s", str(exc)) | ||
raise MissingError( | ||
_( | ||
"Failed to create the materialized views." | ||
"Please check the logs for details or Manually create it." | ||
) | ||
) from exc | ||
|
||
|
||
def drop_materialized_view(env): | ||
""" | ||
Drop all the materialized views related to the dashboard. | ||
""" | ||
cr = env.cr | ||
|
||
matviews_to_drop = [ | ||
"g2p_sr_dashboard_data", | ||
"g2p_gender_count_view", | ||
"g2p_age_distribution_view", | ||
"g2p_total_registrants_view", | ||
] | ||
|
||
try: | ||
for matview in matviews_to_drop: | ||
cr.execute(f"DROP MATERIALIZED VIEW IF EXISTS {matview} CASCADE;") # pylint: disable=sql-injection | ||
_logger.info("Dropped materialized view: %s", matview) | ||
|
||
except Exception as exc: | ||
_logger.error("Error while dropping materialized views: %s", str(exc)) | ||
raise MissingError( | ||
_( | ||
"Failed to drop the materialized views." | ||
"Please check the logs for details or manually delete the view." | ||
) | ||
) from exc |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
# Part of OpenG2P Registry. See LICENSE file for full copyright and licensing details. | ||
{ | ||
"name": "OpenG2P Social Registry: Dashboard", | ||
"category": "G2P", | ||
"version": "17.0.0.0.0", | ||
"sequence": 1, | ||
"author": "OpenG2P", | ||
"website": "https://openg2p.org", | ||
"license": "LGPL-3", | ||
"depends": ["g2p_social_registry"], | ||
"external_dependencies": {}, | ||
"data": ["data/cron_job.xml", "views/menu.xml"], | ||
"assets": { | ||
"web.assets_backend": [ | ||
"g2p_social_registry_dashboard/static/src/components/chart/**/*", | ||
"g2p_social_registry_dashboard/static/src/components/kpi/**/*", | ||
"g2p_social_registry_dashboard/static/src/js/dashboard.js", | ||
"g2p_social_registry_dashboard/static/src/xml/dashboard.xml", | ||
], | ||
}, | ||
"demo": [], | ||
"images": [], | ||
"application": True, | ||
"installable": True, | ||
"auto_install": False, | ||
"post_init_hook": "init_materialized_view", | ||
"uninstall_hook": "drop_materialized_view", | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
<?xml version="1.0" encoding="UTF-8" ?> | ||
<!-- | ||
Part of OpenG2P. See LICENSE file for full copyright and licensing details. | ||
--> | ||
<odoo> | ||
<record id="ir_cron_refresh_materialized_view" model="ir.cron"> | ||
<field name="name">Refresh Materialized View Cron Job</field> | ||
<field name="model_id" ref="model_ir_cron" /> | ||
<field name="state">code</field> | ||
<field name="code">model._refresh_dashboard_materialized_view()</field> | ||
<field name="interval_number">10</field> | ||
<field name="interval_type">hours</field> | ||
<field name="numbercall">-1</field> | ||
<field name="doall" eval="False" /> | ||
<field name="active" eval="True" /> | ||
</record> | ||
</odoo> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
# Part of OpenG2P. See LICENSE file for full copyright and licensing details. | ||
|
||
from . import cron | ||
from . import registrant |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import logging | ||
|
||
from odoo import _, models | ||
from odoo.exceptions import MissingError | ||
|
||
_logger = logging.getLogger(__name__) | ||
|
||
|
||
class DashboardCron(models.Model): | ||
_inherit = "ir.cron" | ||
|
||
def _refresh_dashboard_materialized_view(self): | ||
""" | ||
Refreshes all the materialized views related to the Dashboard. | ||
""" | ||
cr = self.env.cr | ||
matviews_to_refresh = [ | ||
"g2p_gender_count_view", | ||
"g2p_age_distribution_view", | ||
"g2p_total_registrants_view", | ||
"g2p_sr_dashboard_data", | ||
] | ||
|
||
for matview in matviews_to_refresh: | ||
try: | ||
cr.execute( | ||
""" | ||
SELECT matviewname | ||
FROM pg_matviews | ||
WHERE matviewname = %s; | ||
""", | ||
(matview,), | ||
) | ||
|
||
if not cr.fetchall(): | ||
raise MissingError( | ||
_("Materialized view '%s' does not exist. Please create it first.") % matview | ||
) | ||
|
||
cr.execute(f"REFRESH MATERIALIZED VIEW {matview}") # pylint: disable=sql-injection | ||
|
||
except Exception as exc: | ||
_logger.error("Error refreshing materialized view '%s': %s", matview, str(exc)) | ||
raise MissingError( | ||
_("Failed to refresh materialized view '%s'. Please check the logs for details.") | ||
% matview | ||
) from exc |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
# Part of OpenG2P. See LICENSE file for full copyright and licensing details. | ||
|
||
from odoo import api, models | ||
|
||
|
||
class ResPartnerDashboard(models.Model): | ||
_inherit = "res.partner" | ||
|
||
@api.model | ||
def get_dashboard_data(self): | ||
"""Fetch data from materialized view and prepare it for charts.""" | ||
company_id = self.env.company.id | ||
|
||
query = """ | ||
SELECT total_registrants, gender_spec, age_distribution | ||
FROM g2p_sr_dashboard_data | ||
WHERE company_id = %s | ||
""" | ||
self.env.cr.execute(query, (company_id,)) | ||
result = self.env.cr.fetchone() | ||
|
||
total_registrants, gender_spec, age_distribution = result | ||
|
||
return { | ||
"total_individuals": total_registrants.get("total_individuals", 0), | ||
"total_groups": total_registrants.get("total_groups", 0), | ||
"gender_distribution": gender_spec, | ||
"age_distribution": { | ||
"Below 18": age_distribution.get("below_18", 0), | ||
"18 to 30": age_distribution.get("18_to_30", 0), | ||
"31 to 40": age_distribution.get("31_to_40", 0), | ||
"41 to 50": age_distribution.get("41_to_50", 0), | ||
"Above 50": age_distribution.get("above_50", 0), | ||
}, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
[build-system] | ||
requires = ["whool"] | ||
build-backend = "whool.buildapi" |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
50 changes: 50 additions & 0 deletions
50
g2p_social_registry_dashboard/static/src/components/chart/chart.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
/** @odoo-module */ | ||
/* global Chart */ | ||
|
||
import {Component, onMounted, onWillStart, useRef} from "@odoo/owl"; | ||
import {loadJS} from "@web/core/assets"; | ||
|
||
export class ChartComponent extends Component { | ||
setup() { | ||
this.canvasRef = useRef("canvas"); | ||
|
||
onWillStart(async () => { | ||
await loadJS("https://cdn.jsdelivr.net/npm/chart.js"); | ||
}); | ||
|
||
onMounted(() => this.renderChart()); | ||
} | ||
|
||
renderChart() { | ||
const ctx = this.canvasRef.el.getContext("2d"); | ||
|
||
// eslint-disable-next-line no-new | ||
new Chart(ctx, { | ||
type: this.props.type, | ||
data: { | ||
labels: this.props.labels, | ||
datasets: [ | ||
{ | ||
label: this.props.data_label, | ||
data: this.props.data, | ||
backgroundColor: this.props.backgroundColor, | ||
hoverOffset: 2, | ||
}, | ||
], | ||
}, | ||
options: {...this.props.options}, | ||
}); | ||
} | ||
} | ||
|
||
ChartComponent.template = "g2p_social_registry_dashboard.ChartTemplate"; | ||
|
||
ChartComponent.props = { | ||
type: {type: String, optional: true}, | ||
labels: {type: Array, optional: true}, | ||
data_label: {type: String, optional: true}, | ||
data: {type: Array, optional: true}, | ||
backgroundColor: {type: Array, optional: true}, | ||
options: {type: Object, optional: true}, | ||
size: {type: String, optional: true}, | ||
}; |
12 changes: 12 additions & 0 deletions
12
g2p_social_registry_dashboard/static/src/components/chart/chart.xml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
<templates> | ||
<t t-name="g2p_social_registry_dashboard.ChartTemplate" owl="1"> | ||
<div t-att-class="props.size"> | ||
<h3 | ||
t-if="props.title" | ||
t-esc="props.title" | ||
style="font-size: 1rem; color: #7f8c8d; text-align: center; margin-bottom: 10px;" | ||
/> | ||
<canvas id="chart" t-ref="canvas" style="width: 100%; height: auto;" /> | ||
</div> | ||
</t> | ||
</templates> |
Oops, something went wrong.