Skip to content

Commit 4f35cb5

Browse files
jeremigonzalesedwin1123
authored andcommitted
refactor: address code review findings from simplify pass
- Consolidate 4 repeated get_effective_rule_for_user() calls into single _get_effective_rule() lookup per compute_aggregation() call - Revert simulation to call metric services directly: distribution has no access control, fairness needs target_type-aware base_domain - Fix stale README documenting removed privacy service methods - Rename Python classes Statistic->Indicator, StatisticContext-> IndicatorContext to match model names - Update _description fields to match new naming
1 parent 4812a98 commit 4f35cb5

File tree

5 files changed

+51
-45
lines changed

5 files changed

+51
-45
lines changed

spp_analytics/models/service_aggregation.py

Lines changed: 30 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ class AggregationService(models.AbstractModel):
2020
"""
2121

2222
_name = "spp.analytics.service"
23-
_description = "Aggregation Service"
23+
_description = "Analytics Service"
2424

2525
MAX_GROUP_BY_DIMENSIONS = 3
2626

@@ -62,16 +62,19 @@ def compute_aggregation(
6262
# Resolve scope
6363
scope_record = self._resolve_scope(scope)
6464

65+
# Resolve user access context once (single DB lookup)
66+
rule = self._get_effective_rule()
67+
6568
# Validate group_by dimensions
6669
group_by = group_by or []
67-
self._validate_group_by(group_by)
70+
self._validate_group_by(group_by, rule)
6871

69-
# Determine access level from user permissions
70-
access_level = self._determine_access_level()
71-
k_threshold = self._get_k_threshold()
72+
# Determine access level and k-threshold from the resolved rule
73+
access_level = self._access_level_from_rule(rule)
74+
k_threshold = self._k_threshold_from_rule(rule)
7275

7376
# Check scope is allowed for user
74-
self._check_scope_allowed(scope)
77+
self._check_scope_allowed(scope, rule)
7578

7679
# Check cache if enabled
7780
cache_service = self.env["spp.analytics.cache"]
@@ -136,11 +139,23 @@ def _resolve_scope(self, scope):
136139
# Assume it's already a record
137140
return scope
138141

139-
def _validate_group_by(self, group_by):
142+
def _get_effective_rule(self, user=None):
143+
"""
144+
Look up the effective access rule for a user (single DB query).
145+
146+
:param user: res.users record (defaults to current user)
147+
:returns: access rule record or None
148+
"""
149+
user = user or self.env.user
150+
# Use sudo() to read access rules - this is an internal security check
151+
return self.env["spp.analytics.access.rule"].sudo().get_effective_rule_for_user(user) # nosemgrep: odoo-sudo-without-context # noqa: E501 # fmt: skip
152+
153+
def _validate_group_by(self, group_by, rule=None):
140154
"""
141155
Validate group_by dimensions.
142156
143157
:param group_by: List of dimension names
158+
:param rule: Pre-resolved access rule (or None)
144159
:raises: ValidationError if invalid
145160
"""
146161
if len(group_by) > self.MAX_GROUP_BY_DIMENSIONS:
@@ -155,52 +170,42 @@ def _validate_group_by(self, group_by):
155170
raise ValidationError(_("Unknown dimension: %s") % dim_name)
156171

157172
# Check access rule dimension restrictions
158-
user = self.env.user
159-
# Use sudo() to read access rules - this is an internal security check
160-
rule = self.env["spp.analytics.access.rule"].sudo().get_effective_rule_for_user(user) # nosemgrep: odoo-sudo-without-context # noqa: E501 # fmt: skip
161173
if rule and group_by:
162174
rule.check_dimensions_allowed(group_by)
163175

164-
def _determine_access_level(self, user=None):
176+
def _access_level_from_rule(self, rule):
165177
"""
166-
Determine access level from user permissions.
178+
Extract access level from a pre-resolved rule.
167179
168-
:param user: res.users record (defaults to current user)
180+
:param rule: access rule record or None
169181
:returns: "aggregate" or "individual"
170182
:rtype: str
171183
"""
172-
user = user or self.env.user
173-
rule = self.env["spp.analytics.access.rule"].sudo().get_effective_rule_for_user(user) # nosemgrep: odoo-sudo-without-context # noqa: E501 # fmt: skip
174184
if rule:
175185
return rule.access_level
176186
# Default to aggregate-only for safety
177187
return "aggregate"
178188

179-
def _get_k_threshold(self, user=None):
189+
def _k_threshold_from_rule(self, rule):
180190
"""
181-
Get k-anonymity threshold for user.
191+
Extract k-anonymity threshold from a pre-resolved rule.
182192
183-
:param user: res.users record (defaults to current user)
193+
:param rule: access rule record or None
184194
:returns: k threshold value
185195
:rtype: int
186196
"""
187-
user = user or self.env.user
188-
rule = self.env["spp.analytics.access.rule"].sudo().get_effective_rule_for_user(user) # nosemgrep: odoo-sudo-without-context # noqa: E501 # fmt: skip
189197
if rule:
190198
return rule.minimum_k_anonymity
191199
return self.env["spp.metric.privacy"].DEFAULT_K_THRESHOLD
192200

193-
def _check_scope_allowed(self, scope):
201+
def _check_scope_allowed(self, scope, rule=None):
194202
"""
195203
Check if scope is allowed for current user.
196204
197205
:param scope: Scope record or dict
206+
:param rule: Pre-resolved access rule (or None)
198207
:raises: AccessError if not allowed
199208
"""
200-
user = self.env.user
201-
# Use sudo() to read access rules - this is an internal security check
202-
rule = self.env["spp.analytics.access.rule"].sudo().get_effective_rule_for_user(user) # nosemgrep: odoo-sudo-without-context # noqa: E501 # fmt: skip
203-
204209
if not rule:
205210
# No explicit rule - allow with defaults
206211
return

spp_indicator/models/statistic.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,22 @@
1010
_logger = logging.getLogger(__name__)
1111

1212

13-
class Statistic(models.Model):
14-
"""A publishable statistic based on a CEL variable.
13+
class Indicator(models.Model):
14+
"""A publishable indicator based on a CEL variable.
1515
16-
Statistics separate concerns:
16+
Indicators separate concerns:
1717
- CEL Variable: "What data and how to compute it"
18-
- Statistic: "Where to publish and how to present it"
18+
- Indicator: "Where to publish and how to present it"
1919
20-
A single CEL variable can be published as multiple statistics
20+
A single CEL variable can be published as multiple indicators
2121
with different presentations for different contexts.
2222
2323
Inherits from spp.metric.base for common metric fields
2424
(name, label, description, category_id, sequence, etc.)
2525
"""
2626

2727
_name = "spp.indicator"
28-
_description = "Publishable Statistic"
28+
_description = "Publishable Indicator"
2929
_inherit = ["spp.metric.base"]
3030
_order = "category_id, sequence, name"
3131

spp_indicator/models/statistic_context.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,16 @@
44
from odoo import fields, models
55

66

7-
class StatisticContext(models.Model):
8-
"""Context-specific presentation configuration for a statistic.
7+
class IndicatorContext(models.Model):
8+
"""Context-specific presentation configuration for an indicator.
99
1010
Allows overriding default presentation settings for specific contexts
11-
(GIS, dashboard, API, reports). For example, a statistic might use
11+
(GIS, dashboard, API, reports). For example, an indicator might use
1212
a different label or grouping in the GIS context vs. dashboard.
1313
"""
1414

1515
_name = "spp.indicator.context"
16-
_description = "Statistic Context Configuration"
16+
_description = "Indicator Context Configuration"
1717
_order = "statistic_id, context"
1818

1919
statistic_id = fields.Many2one(

spp_metric_service/README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -198,8 +198,7 @@ Enforces k-anonymity privacy protection on aggregation results.
198198

199199
```python
200200
enforce(result, k_threshold=None, access_level="aggregate")
201-
validate_access_level(user=None)
202-
get_k_threshold(user=None, context=None)
201+
suppress_value(value, count, k_threshold=None, stat_config=None)
203202
```
204203

205204
**Features:**

spp_simulation/services/simulation_service.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -65,17 +65,19 @@ def execute_simulation(self, scenario):
6565
amounts = self._apply_budget_strategy(scenario, amounts)
6666

6767
# Step 4: Distribution stats
68-
# Route through analytics service for consistent access control
69-
analytics_service = self.env["spp.analytics.service"]
70-
distribution_data = analytics_service.compute_distribution(amounts)
68+
distribution_service = self.env["spp.metric.distribution"]
69+
distribution_data = distribution_service.compute_distribution(amounts)
7170
gini = distribution_data.get("gini_coefficient", 0.0)
7271

7372
# Step 5: Fairness analysis
74-
# Route through analytics service with explicit scope
75-
from odoo.addons.spp_analytics.services import build_explicit_scope
76-
77-
scope = build_explicit_scope(beneficiary_ids)
78-
fairness_data = analytics_service.compute_fairness(scope)
73+
fairness_service = self.env["spp.metric.fairness"]
74+
# Derive base domain from target type so fairness compares against
75+
# the correct population (groups vs individuals)
76+
profile = "registry_groups" if scenario.target_type == "group" else "registry_individuals"
77+
registry = self.env["spp.cel.registry"]
78+
cfg = registry.load_profile(profile)
79+
base_domain = cfg.get("base_domain", [])
80+
fairness_data = fairness_service.compute_fairness(beneficiary_ids, base_domain)
7981
equity_score = fairness_data.get("equity_score", 100.0)
8082
has_disparity = fairness_data.get("has_disparity", False)
8183

0 commit comments

Comments
 (0)