Skip to content

fix(change_request): let a routing key be served by several mappings - #462

Merged
gonzalesedwin1123 merged 3 commits into
19.0from
fix/dynamic-routing-key-mapping
Aug 28, 2026
Merged

fix(change_request): let a routing key be served by several mappings#462
gonzalesedwin1123 merged 3 commits into
19.0from
fix/dynamic-routing-key-mapping

Conversation

@kneckinator

Copy link
Copy Markdown
Contributor

A selectable field on a dynamic-approval type can only be applied if a mapping's source_field matches it exactly. When one selectable value is applied through several mappings, nothing matches and the request applies nothing.

The assumption

Apply is narrowed to the field a request was routed and approved on — correct and load-bearing, since the whole point is that only the approved change is written:

return mappings.filtered(lambda m: m.source_field == selected)

But selected comes from selected_field_name, which is synced from the detail's field_to_modify — and that is chosen from _get_field_to_modify_selection(), an arbitrary list of selectable values. Nothing requires those values to be physical source fields.

A legitimate and not unusual shape: a name presented to the operator as one choice, but captured and applied as separate components. One selectable value, several mappings, none of whose source_field equals it. The filter matches nothing, and the request applies nothing at all.

Until recently that failed silently — apply returned success having written nothing, and the request was stamped applied. It now raises, which is how this became visible, but the underlying configuration still cannot be expressed.

The fix

A mapping may declare the selectable value it serves:

routing_field = fields.Char(...)   # defaults to source_field

return mappings.filtered(lambda m: (m.routing_field or m.source_field) == selected)

Empty means today's behaviour, so existing configurations are unchanged and no migration is needed.

The narrowing is unchanged

This widens what a routing key can name, not what gets applied. Both directions are tested:

Full spp_change_request_v2 suite: 400 tests, 0 failures.

Considered and rejected: a config-time constraint

A check that every selectable value resolves to at least one mapping would catch this at install rather than at apply — for field_mapping types an unmapped selectable value can never be applied, so it is always a misconfiguration.

I did not add one. As an @api.constrains it would fire during XML data loading, where the type record is created before its mapping records exist, and fail a clean install. Making it robust needs a post-load report or a lint rather than a constraint, which is worth doing separately rather than bundling here.

Merge order

19.0.3.1.10 → 19.0.3.1.13, leaving 3.1.11 to #459 and 3.1.12 to #461. Independent of both in content; only the version and changelog conflict, and only if merged out of order.

Apply on a dynamic-approval type is narrowed to the field the request
was routed and approved on, matched against the mapping's source_field.
That assumed every value returned by _get_field_to_modify_selection() is
a physical source field.

They need not be. A name may be offered as a single choice but stored as
separate components, so one selectable value legitimately drives several
mappings — and matching on source_field alone matched none of them, so
such a request applied nothing at all. It did so silently until applying
with nothing to write began raising.

A mapping can now declare the selectable value it serves through
routing_field, defaulting to source_field, so existing configurations
are unchanged and no migration is needed.

The narrowing itself is untouched, and tested in both directions: a
mapping belonging to a different routing key is still not applied, and a
non-dynamic type is still not narrowed at all.
@codecov

codecov Bot commented Aug 26, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 76.02%. Comparing base (ac0e38e) to head (2bbd2a4).

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             19.0     #462      +/-   ##
==========================================
- Coverage   76.18%   76.02%   -0.16%     
==========================================
  Files         661      627      -34     
  Lines       44109    43025    -1084     
==========================================
- Hits        33603    32711     -892     
+ Misses      10506    10314     -192     
Flag Coverage Δ
spp_api_v2_change_request 73.37% <ø> (ø)
spp_api_v2_products ?
spp_base_common 91.07% <ø> (ø)
spp_change_request_v2 78.63% <100.00%> (+0.03%) ⬆️
spp_consent ?
spp_cr_type_assign_program 92.50% <ø> (ø)
spp_dci_demo 94.28% <ø> (ø)
spp_farmer_registry_cr 61.24% <ø> (ø)
spp_farmer_registry_demo 63.39% <ø> (ø)
spp_irrigation ?
spp_mis_demo_v2 70.38% <ø> (ø)
spp_programs 66.97% <ø> (ø)
spp_registry 87.79% <ø> (ø)
spp_security 69.56% <ø> (ø)
spp_starter_sp_mis 86.84% <ø> (-2.05%) ⬇️
spp_studio_change_requests 84.85% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...e_request_v2/models/change_request_type_mapping.py 100.00% <100.00%> (ø)
spp_change_request_v2/strategies/field_mapping.py 82.60% <100.00%> (ø)

... and 37 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@gonzalesedwin1123 gonzalesedwin1123 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One small must-fix (inline), otherwise this is an approve — the mechanism is right and the blast radius is genuinely zero for existing configs.

Verified:

  • The premise holds: selected_field_name comes from _get_field_to_modify_selection(), which nothing requires to be physical source fields, so source_field-only matching cannot express one-choice-several-components and matched nothing.
  • The #264 narrowing is preserved, and more importantly the #422 detection lockstep is untouched: _proposed_changed_fields deliberately iterates every configured mapping by physical source_field, so widening what apply's routing key can name doesn't desync detection — mappings serving a compound key still carry physical source fields and are detected per-field.
  • No privilege change: routing_field is mapping config, authored at the same trust level as source_field/target_field themselves.
  • Tests pin both directions (multi-mapping applies, foreign-key mapping doesn't), the default (source_field when unset), the shadowing semantic (set means replaces, not adds — matching the help text), and non-dynamic types staying un-narrowed.
  • Version 19.0.3.1.13 correctly leaves .11/.12 to #459/#461; HISTORY conflicts only textually if merged out of order, as flagged.
  • Skipping the config-time constraint is the right call for the XML-load reason given; a post-load lint is worth its own issue so it isn't lost.

required=True,
help="Field name on detail model",
)
routing_field = fields.Char(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Must fix: routing_field is not exposed in any view. The inline mapping list in change_request_type_views.xml (~line 161) shows sequence / source_field / target_field / transform / transform_expression — an admin configuring a type through the UI can neither set this field nor see it. That cuts both ways: the feature is XML/API-only, and — per your own test_routing_field_shadows_source_field_for_matching — a value set via API makes source_field silently stop matching with nothing in the form explaining why apply now refuses. One line fixes it:

<field name="routing_field" optional="hide" />

(optional="hide" keeps the common case uncluttered while making the shadowing visible to whoever goes looking.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in baa249f — exactly the one line suggested, <field name="routing_field" optional="hide" />, placed right after source_field since that is the field it shadows. Also pinned with a test (test_routing_field_is_exposed_in_the_type_form_view) so it cannot silently drop out of the view again. Full module suite: 401 tests, 0 failures.

…list

routing_field shadows source_field for matching, so a value set only
through XML or the API made apply refuse with nothing in the form
explaining why. Show it in the inline mapping list, hidden by default
to keep the common case uncluttered.

@gonzalesedwin1123 gonzalesedwin1123 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving — routing_field is now in the inline mapping list with optional="hide", and the view-arch test pins it so the field can't silently fall out of the form again. CI fully green.

Merge order: after #459 and #461 (3.1.13); same mechanical HISTORY/manifest conflict expected at that point.

#459 and #461 were squash-merged (481cc27, ac0e38e), conflicting with
this branch in the version line, HISTORY, tests/__init__.py, and the
generated README/index.html. Resolved to 19.0.3.1.13 with the HISTORY
entries stacked (3.1.13 over 3.1.12 over 3.1.11), both test imports
kept, and README/index.html regenerated from the resolved fragments
with CI's table-width rendering applied.
@gonzalesedwin1123
gonzalesedwin1123 merged commit 925bd4b into 19.0 Aug 28, 2026
34 checks passed
@gonzalesedwin1123
gonzalesedwin1123 deleted the fix/dynamic-routing-key-mapping branch August 28, 2026 05:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants