Skip to content

fix(spp_change_request_v2): let the registrant prefill through the freeze on an empty repaired detail (#443) - #536

Open
gonzalesedwin1123 wants to merge 5 commits into
19.0from
fix-443-cr-detail-prefill-freeze
Open

gonzalesedwin1123 wants to merge 5 commits into
19.0from
fix-443-cr-detail-prefill-freeze

Conversation

@gonzalesedwin1123

@gonzalesedwin1123 gonzalesedwin1123 commented Sep 21, 2026

Copy link
Copy Markdown
Member

Part of #443 (weekly ci-full red). Second per-module PR; a real production bug, not a test problem.

Problem

19.0.3.1.10 let _ensure_detail() bind a new detail row to a submitted change request (a request that never got one, or lost it, could not be opened from any context). The binding now passes, but _ensure_detail() then calls detail.prefill_from_registrant(), which writes the registrant's current values to the mapped fields, and _assert_content_editable refuses that write whenever the request type has field_mapping mappings. A fresh detail holds False everywhere, the prefill values are truthy, so "differs" → UserError: This change request has already been submitted for approval, so its proposed changes are locked.

Every shipped Edit Individual / Edit Group type carries those mappings through spp_cr_types_base, which every real deployment installs. The module's own test type is built from CR_TYPE_DEFS with no mappings, so the protected set was just {field_to_modify} and TestFrozenDetailBinding passed in per-module CI while failing in the weekly SP-MIS stack. action_open_detail / action_goto_details both go through _ensure_detail(), so on a real deployment the repair 19.0.3.1.10 set out to do still does not work.

Fix

The prefill moves into the detail's create(): _ensure_detail builds the row with _prefill_values() (the registrant's current values for the prefill mapping, record values as ids) already in the create vals. The repair therefore performs no post-submit write at all, and _assert_content_editable keeps no exemption: every write to a mapped field on a submitted request, the registrant's own value included, stays refused.

Why the row must be prefilled rather than left empty: field_mapping.apply writes empties on purpose ("a user may be intentionally clearing a field"), so approving an empty repaired detail would clear every mapped field on the registrant. Prefilled, it equals what the registrant already holds and apply skips it.

The first draft of this PR opened a shape-based carve-out in the freeze instead ("empty detail + registrant's own values"). The adversarial review showed it validated the no-op claim against the prefill mapping while apply uses the apply mapping (target_field, transform), and that its precondition is exactly the shape of an approved clear-every-field proposal, so it left a one-write slot. The create-time prefill removes that surface entirely.

prefill_from_registrant() keeps its public behaviour (a write, therefore subject to the freeze on submitted requests). spp_change_request_v2 19.0.3.1.16 → 19.0.3.1.17 + HISTORY. README/index.html left for CI's generator.

Tests (tests/test_frozen_detail_binding.py, 6 → 11)

The class now gives the test-created edit_individual / edit_group types the mappings spp_cr_types_base ships, so module CI exercises the real shape. Against the unfixed model that alone turns the repair tests red in module-only mode. New:

  • Edit Group can be repaired after submit (fully overlapping prefill/apply mappings too);
  • the repaired detail arrives already populated with the registrant's values, without any write;
  • prefill_from_registrant() on an empty submitted detail is refused (the freeze keeps no exemption);
  • another value on an empty submitted detail is refused;
  • restoring a single cleared field to the registrant's own value is refused.

Evidence: with -i spp_change_request_v2,spp_cr_types_base --test-tags /spp_change_request_v2:TestFrozenDetailBinding the class went from 0 failed, 2 error(s) of 6 to 0 failed, 0 error(s) of 11; module suite 0 failed, 0 error(s) of 425 tests. All pre-commit hooks pass. An SP-MIS ci-full is dispatched on this branch; its failing set should lose exactly the two TestFrozenDetailBinding errors against the 2026-09-20 baseline.

Pre-existing gaps found in review (not this PR's scope; filed separately)

spp.change.request.registrant_id is not among the fields frozen on submit; create() on detail models is unguarded, so a populated row can be crafted and bound to a detail-less submitted request through the 19.0.3.1.10 binding rule; Studio-generated detail models inherit neither the freeze nor the prefill.

…eeze on an empty repaired detail

_ensure_detail() may bind a new detail row to a submitted request
(19.0.3.1.10), but the registrant prefill that follows is a write to
the mapped fields, and _assert_content_editable refused it whenever the
request type carries field mappings, which every shipped Edit
Individual / Edit Group type does through spp_cr_types_base. The
module's own test type had no mappings, so the repair passed in
isolation and failed on every real deployment.

The freeze now accepts exactly one write past submission: the
registrant's current values onto a detail that holds no proposed
content yet. It is recognised by shape, not caller, so RPC cannot claim
it; a detail already carrying a proposal stays frozen even for the
registrant's own value, and any other value is refused on an empty
detail too. The frozen-detail tests give the test types the shipped
mappings so module CI exercises this shape, and cover Edit Group.

Refs #443
@codecov

codecov Bot commented Sep 21, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 76.91%. Comparing base (1a3c591) to head (d90820c).

Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff           @@
##             19.0     #536   +/-   ##
=======================================
  Coverage   76.91%   76.91%           
=======================================
  Files         704      704           
  Lines       45774    45784   +10     
=======================================
+ Hits        35205    35216   +11     
+ Misses      10569    10568    -1     
Flag Coverage Δ
spp_api_v2_change_request 73.37% <ø> (ø)
spp_base_common 91.07% <ø> (ø)
spp_change_request_v2 79.01% <100.00%> (+0.11%) ⬆️
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_mis_demo_v2 70.41% <ø> (ø)
spp_programs 67.58% <ø> (ø)
spp_registry 89.00% <ø> (ø)
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 Δ
spp_change_request_v2/models/change_request.py 84.70% <100.00%> (+0.27%) ⬆️
...ge_request_v2/models/change_request_detail_base.py 75.00% <100.00%> (+0.54%) ⬆️

... and 2 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.

…() and keep the freeze absolute

Review round: the write-path carve-out validated the no-op claim
against the prefill mapping while apply uses the apply mapping
(target_field, transform), and its precondition (a detail with no
protected content) is exactly the shape of an approved clear-every-
field proposal, so it left a one-write slot on such details.

_ensure_detail now passes _prefill_values() into the detail's create(),
so the repair performs no post-submit write at all and
_assert_content_editable keeps no exemption. _prefill_values() returns
ids for record values, since create() does not accept recordsets where
write() did. Tests assert the repaired detail arrives populated and that
every post-submit write to a mapped field, the registrant's own value
included, stays refused.

Refs #443
…opose a change

Second review round. The rebuilt row applies nothing only as far as the
prefill covers the apply mapping; a value the prefill declines to offer
(a legacy future date of birth, or a mapping added later for a field
the prefill does not know) would be applied as clearing that field, and
the frozen row could not be corrected. A field-mapping request repaired
after submission is now checked with the strategy's preview and refused
with an explicit message when anything would change; a post-submit
repair is logged. Tests apply a repaired request end to end and cover
the future-birthdate refusal, the draft-time drop, a Many2one prefill
value, and pin the test mappings against the shipped ones.

Refs #443
@gonzalesedwin1123

Copy link
Copy Markdown
Member Author

Full-stack evidence: SP-MIS ci-full dispatched on this branch (run 35565791203, at 440596d) vs the 2026-09-20 weekly baseline (run 35499069639).

@gonzalesedwin1123
gonzalesedwin1123 marked this pull request as ready for review September 21, 2026 06:37
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.

1 participant