Skip to content

Commit

Permalink
Merge pull request #56 from mkumar-02/17.0-sr-dashboard
Browse files Browse the repository at this point in the history
Added Social Registry Dashboard Module.
  • Loading branch information
shibu-narayanan authored Dec 9, 2024
2 parents 98a345c + a455bd8 commit 9f15d93
Show file tree
Hide file tree
Showing 17 changed files with 537 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ addon | version | maintainers | summary
[g2p_registry_g2p_connect_rest_api](g2p_registry_g2p_connect_rest_api/) | 17.0.0.0.0 | | OpenG2P Registry: G2P Connect REST API
[g2p_registry_id_deduplication](g2p_registry_id_deduplication/) | 17.0.0.0.0 | | OpenG2P Registry ID Deduplication
[g2p_social_registry](g2p_social_registry/) | 17.0.0.0.0 | | OpenG2P Social Registry
[g2p_social_registry_dashboard](g2p_social_registry_dashboard/) | 17.0.0.0.0 | | OpenG2P Social Registry: Dashboard
[g2p_social_registry_model](g2p_social_registry_model/) | 17.0.0.0.0 | | G2P Social Registry: Demo
[g2p_social_registry_proxy_means_test](g2p_social_registry_proxy_means_test/) | 17.0.0.0.0 | | G2P Social Registry: PMT
[g2p_social_registry_theme](g2p_social_registry_theme/) | 17.0.0.0.0 | | OpenG2P Social Registry: Theme
Expand Down
3 changes: 3 additions & 0 deletions g2p_social_registry_dashboard/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# OpenG2P Social Registry Dashboard

Refer to https://docs.openg2p.org.
170 changes: 170 additions & 0 deletions g2p_social_registry_dashboard/__init__.py
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
28 changes: 28 additions & 0 deletions g2p_social_registry_dashboard/__manifest__.py
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",
}
17 changes: 17 additions & 0 deletions g2p_social_registry_dashboard/data/cron_job.xml
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>
4 changes: 4 additions & 0 deletions g2p_social_registry_dashboard/models/__init__.py
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
47 changes: 47 additions & 0 deletions g2p_social_registry_dashboard/models/cron.py
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
35 changes: 35 additions & 0 deletions g2p_social_registry_dashboard/models/registrant.py
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),
},
}
3 changes: 3 additions & 0 deletions g2p_social_registry_dashboard/pyproject.toml
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 g2p_social_registry_dashboard/static/src/components/chart/chart.js
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},
};
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>
Loading

0 comments on commit 9f15d93

Please sign in to comment.