Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions spp_cel_domain/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,18 @@ Dependencies
Changelog
=========

19.0.2.1.1
~~~~~~~~~~

- fix: recognise ``me`` as a CEL context identifier. The resolver
rewrites cached variables (and the DCI override rewrites dotted
accessors) into ``metric('<accessor>', me)`` before identifiers are
extracted; because ``me`` was missing from
``CEL_CONTEXT_IDENTIFIERS``, ``validate_expression`` /
``validate_formula_expression`` wrongly reported valid expressions as
``Undefined variables: me``. ``me`` is the individual record proxy in
the eval context, so it is now a recognised context identifier.

19.0.2.1.0
~~~~~~~~~~

Expand Down
2 changes: 1 addition & 1 deletion spp_cel_domain/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
{
"name": "CEL Domain Query Builder",
"summary": "Write simple CEL-like expressions to filter records (OpenSPP/OpenG2P friendly)",
"version": "19.0.2.1.0",
"version": "19.0.2.1.1",
"license": "LGPL-3",
"development_status": "Production/Stable",
"author": "OpenSPP.org, OpenSPP Community",
Expand Down
1 change: 1 addition & 0 deletions spp_cel_domain/models/cel_variable_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ def _get_reserved_words(self):
"or",
"r",
"m",
"me",
"members",
"enrollments",
"entitlements",
Expand Down
10 changes: 10 additions & 0 deletions spp_cel_domain/readme/HISTORY.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
### 19.0.2.1.1

- fix: recognise `me` as a CEL context identifier. The resolver rewrites cached
variables (and the DCI override rewrites dotted accessors) into
`metric('<accessor>', me)` before identifiers are extracted; because `me` was
missing from `CEL_CONTEXT_IDENTIFIERS`, `validate_expression` /
`validate_formula_expression` wrongly reported valid expressions as
`Undefined variables: me`. `me` is the individual record proxy in the eval
context, so it is now a recognised context identifier.

### 19.0.2.1.0

- feat(sql): compile CEL ternary expressions to SQL CASE via `to_sql_case`, with `case_when`/`comparison` builders and a right-associative ternary parsing fix
Expand Down
1 change: 1 addition & 0 deletions spp_cel_domain/services/cel_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ def __init__(self, kind: str, value: Any, pos: int):
# ADR-008: Added 'r' as the standard prefix for current record access
CEL_CONTEXT_IDENTIFIERS = {
"m",
"me",
"e",
"r",
"members",
Expand Down
15 changes: 14 additions & 1 deletion spp_cel_domain/static/description/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,19 @@ <h2><a class="toc-backref" href="#toc-entry-1">Changelog</a></h2>
</div>
</div>
<div class="section" id="section-1">
<h1>19.0.2.1.1</h1>
<ul class="simple">
<li>fix: recognise <tt class="docutils literal">me</tt> as a CEL context identifier. The resolver
rewrites cached variables (and the DCI override rewrites dotted
accessors) into <tt class="docutils literal"><span class="pre">metric('&lt;accessor&gt;',</span> me)</tt> before identifiers are
extracted; because <tt class="docutils literal">me</tt> was missing from
<tt class="docutils literal">CEL_CONTEXT_IDENTIFIERS</tt>, <tt class="docutils literal">validate_expression</tt> /
<tt class="docutils literal">validate_formula_expression</tt> wrongly reported valid expressions as
<tt class="docutils literal">Undefined variables: me</tt>. <tt class="docutils literal">me</tt> is the individual record proxy in
the eval context, so it is now a recognised context identifier.</li>
</ul>
</div>
<div class="section" id="section-2">
<h1>19.0.2.1.0</h1>
<ul class="simple">
<li>feat(sql): compile CEL ternary expressions to SQL CASE via
Expand All @@ -533,7 +546,7 @@ <h1>19.0.2.1.0</h1>
<li>test(translator): add coverage for the CEL translation cache helpers</li>
</ul>
</div>
<div class="section" id="section-2">
<div class="section" id="section-3">
<h1>19.0.2.0.0</h1>
<ul class="simple">
<li>Initial migration to OpenSPP2</li>
Expand Down
1 change: 1 addition & 0 deletions spp_cel_domain/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,4 @@
from . import test_cel_relational_predicate
from . import test_cel_smart_op_lookup
from . import test_cel_translator_cache
from . import test_cel_me_identifier
46 changes: 46 additions & 0 deletions spp_cel_domain/tests/test_cel_me_identifier.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Part of OpenSPP. See LICENSE file for full copyright and licensing details.
"""`me` is a record-root context identifier and must not be flagged as an
undefined variable during validation.

The resolver rewrites cached variables into ``metric('<accessor>', me)`` and
the DCI override rewrites dotted accessors the same way *before* the base
resolver extracts identifiers. ``me`` then appears as a bare identifier in the
scanned expression; unless it is a recognized context identifier,
``validate_expression`` / ``validate_formula_expression`` wrongly report
``Undefined variables: me``.
"""

from odoo.tests import TransactionCase, tagged


@tagged("post_install", "-at_install")
class TestMeContextIdentifier(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.resolver = cls.env["spp.cel.variable.resolver"]
cls.service = cls.env["spp.cel.service"]

def test_me_is_a_context_identifier(self):
from odoo.addons.spp_cel_domain.services.cel_parser import CEL_CONTEXT_IDENTIFIERS

self.assertIn("me", CEL_CONTEXT_IDENTIFIERS)

def test_expand_does_not_flag_me_as_missing(self):
result = self.resolver.expand_expression("metric('foo', me) == true")
self.assertNotIn("me", result["missing_variables"])

def test_validate_expression_accepts_bare_me(self):
result = self.resolver.validate_expression("metric('foo', me) == true")
self.assertTrue(
result["valid"],
f"expression with bare me should validate; errors: {result['errors']}",
)
self.assertNotIn(
"Undefined variables: me",
" ".join(result["errors"]),
)

def test_validate_formula_expression_accepts_bare_me(self):
result = self.service.validate_formula_expression("metric('foo', me)", "individual")
self.assertNotIn("Missing variables: me", result.get("error") or "")
1 change: 1 addition & 0 deletions spp_dci_indicators/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@
from . import test_dci_cel_params
from . import test_dci_cel_methods
from . import test_dci_cel_fetcher_errors
from . import test_dci_cel_validation
35 changes: 35 additions & 0 deletions spp_dci_indicators/tests/test_dci_cel_validation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Part of OpenSPP. See LICENSE file for full copyright and licensing details.
"""Validation must accept dotted DCI accessors.

The DCI resolver rewrites a dotted cached accessor like ``r.dci.crvs.is_alive``
into ``metric('r.dci.crvs.is_alive', me)`` before the base resolver extracts
identifiers, so ``me`` appears as a bare identifier in the scanned expression.
``validate_expression`` must not report it as an undefined variable.
"""

from odoo.tests import TransactionCase, tagged


@tagged("post_install", "-at_install")
class TestDCIDottedValidation(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.resolver = cls.env["spp.cel.variable.resolver"]
# Seeded active ttl variable with a dotted accessor.
cls.var = cls.env.ref("spp_dci_indicators.var_dci_crvs_is_alive")

def test_expand_dotted_accessor_does_not_flag_me(self):
result = self.resolver.expand_expression(f"{self.var.cel_accessor} == true", context_type="individual")
# The accessor was rewritten to a metric() call ...
self.assertIn(f"metric('{self.var.cel_accessor}', me)", result["expression"])
# ... and me is not treated as an undefined variable.
self.assertNotIn("me", result["missing_variables"])

def test_validate_dotted_accessor_expression_is_valid(self):
result = self.resolver.validate_expression(f"{self.var.cel_accessor} == true", context_type="individual")
self.assertTrue(
result["valid"],
f"dotted DCI accessor should validate; errors: {result['errors']}",
)
self.assertNotIn("Undefined variables: me", " ".join(result["errors"]))
Loading