diff --git a/spp_cel_domain/README.rst b/spp_cel_domain/README.rst index 9a72fa2b4..fa6e8998d 100644 --- a/spp_cel_domain/README.rst +++ b/spp_cel_domain/README.rst @@ -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('', 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 ~~~~~~~~~~ diff --git a/spp_cel_domain/__manifest__.py b/spp_cel_domain/__manifest__.py index 550c3d0f3..d53b3c3a2 100644 --- a/spp_cel_domain/__manifest__.py +++ b/spp_cel_domain/__manifest__.py @@ -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", diff --git a/spp_cel_domain/models/cel_variable_resolver.py b/spp_cel_domain/models/cel_variable_resolver.py index be60996d4..eb2672e4a 100644 --- a/spp_cel_domain/models/cel_variable_resolver.py +++ b/spp_cel_domain/models/cel_variable_resolver.py @@ -109,6 +109,7 @@ def _get_reserved_words(self): "or", "r", "m", + "me", "members", "enrollments", "entitlements", diff --git a/spp_cel_domain/readme/HISTORY.md b/spp_cel_domain/readme/HISTORY.md index 1b4fd10d3..c54c30ceb 100644 --- a/spp_cel_domain/readme/HISTORY.md +++ b/spp_cel_domain/readme/HISTORY.md @@ -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('', 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 diff --git a/spp_cel_domain/services/cel_parser.py b/spp_cel_domain/services/cel_parser.py index 8222781bd..6d3e4b397 100644 --- a/spp_cel_domain/services/cel_parser.py +++ b/spp_cel_domain/services/cel_parser.py @@ -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", diff --git a/spp_cel_domain/static/description/index.html b/spp_cel_domain/static/description/index.html index f280ce2a1..db79f7d9d 100644 --- a/spp_cel_domain/static/description/index.html +++ b/spp_cel_domain/static/description/index.html @@ -522,6 +522,19 @@

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

  • feat(sql): compile CEL ternary expressions to SQL CASE via @@ -533,7 +546,7 @@

    19.0.2.1.0

  • test(translator): add coverage for the CEL translation cache helpers
-
+

19.0.2.0.0

  • Initial migration to OpenSPP2
  • diff --git a/spp_cel_domain/tests/__init__.py b/spp_cel_domain/tests/__init__.py index f9eb6bff3..828dfe71e 100644 --- a/spp_cel_domain/tests/__init__.py +++ b/spp_cel_domain/tests/__init__.py @@ -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 diff --git a/spp_cel_domain/tests/test_cel_me_identifier.py b/spp_cel_domain/tests/test_cel_me_identifier.py new file mode 100644 index 000000000..594bd0145 --- /dev/null +++ b/spp_cel_domain/tests/test_cel_me_identifier.py @@ -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('', 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 "") diff --git a/spp_dci_indicators/tests/__init__.py b/spp_dci_indicators/tests/__init__.py index 15c858873..4f9558b07 100644 --- a/spp_dci_indicators/tests/__init__.py +++ b/spp_dci_indicators/tests/__init__.py @@ -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 diff --git a/spp_dci_indicators/tests/test_dci_cel_validation.py b/spp_dci_indicators/tests/test_dci_cel_validation.py new file mode 100644 index 000000000..96b042bba --- /dev/null +++ b/spp_dci_indicators/tests/test_dci_cel_validation.py @@ -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"]))