Skip to content

[19.0][MIG] ni_questionnaire: Migration to 19.0 - #161

Merged
piruin merged 36 commits into
nirun-life:19.0from
wasud:19.0-mig-p1-questionnaire
Oct 21, 2026
Merged

piruin merged 36 commits into
nirun-life:19.0from
wasud:19.0-mig-p1-questionnaire

Conversation

@wasud

@wasud wasud commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Decisions to review

What changed

  • [IMP] pre-commit (no changes) and [MIG] ni_questionnaire: Migration to 19.0 on top of the 18.0 PR.
  • Migrator: _cr/_context → env.cr/env.context, menu groups_id → group_ids, version.
  • Manual work:
    • self.env._(...) everywhere, with a named placeholder for the deceased-patient warning.
    • Redundant labels dropped.
  • Checked unchanged on 19:
    • _mark_done(), _create_answer(), action_start_survey(answer=) / action_print_survey(answer=), _description_selection.
    • answer_score/answer_is_correct are computed but still stored, so the monthly report still reads them as columns.
  • th.po regenerated, 18.0 translations merged.

Why

Closes #140 (P1-5, epic #131).

Migration notes

  • Effort: S.
  • What the migrator did: env.cr/env.context, group_ids.
  • Manual work: self.env._, lint.
  • Surprises: 19 asks for a submit confirmation on the last page of a survey (core behaviour, no change needed).

How tested

  • Unit/integration tests: 33 tests pass on 19, fresh DB, no WARNING lines.
  • Manual check (browser, Odoo 19, no demo data). Same questionnaire as on 18:
    • start from the encounter, conditional question, scale 7, submit (with confirmation);
    • grade "High", score 10;
    • observations "Smoke Score = 10" and "Craving Scale = 7";
    • the encounter's latest response and grade update at once;
    • "latest survey" view lists it, and print uses the response's token;
    • no JS console errors.
  • pre-commit clean on ni_questionnaire.

Documentation

  • Module README.md (19.0)

Notes for reviewer

@piruin piruin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review of ni_questionnaire only (the patient/observation/survey commits belong to the stacked PRs). The findings marked "all three" on #159 also apply here; repeated below for completeness.

Medium — subject wizard now shows the patient/encounter pickers that 16.0 hid (wizard/survey_subject_views.xml)

On 16.0, subject_ni_patient / subject_ni_encounter had invisible="context.get('default_subject_ni_…')" next to attrs. A truthy context value overrode the attrs domain (16.0 core ir_ui_view.py, context-dependent modifiers). The 17.0 conversion kept only type not in [...], so opening the wizard from an encounter or observation ("Start Survey") now shows editable patient/encounter fields. Suggested:

invisible="context.get('default_subject_ni_patient') or type not in ['ni.patient', 'ni.encounter']"

(and the same with default_subject_ni_encounter for subject_ni_encounter). This is the only element in the module that had both.

Low — the latest-response view and compute break ties differently (report/ni_patient_survey_latest.py)

The survey_latest views order by create_date DESC with no tie-breaker, while ni.encounter._compute_response sorts by (create_date, id). If two done responses share a create_date (e.g. created in one transaction), response_latest_id can point to a row the view's DISTINCT ON dropped. Adding , id DESC to the view's ORDER BY aligns them (and matches the index, which already includes id).

Nit — the error branch of _answer_group_observation raises AttributeError (models/survey_user_input.py)

code is group.observation_code_id, already an ni.observation.type, which has no observation_type_id. So code.observation_type_id.name / .value_type crash instead of raising the ValidationError. Mostly unreachable thanks to the field domain, but these lines were just rewritten; use code.name / code.value_type.

Note (non-blocking) — creating the answer with user=env.user counts attempts against the clinician's partner, not the patient's. On a survey with limited attempts, a clinician runs out across patients. The survey_subject wizard behaves the same way.

Low — 17.0 commit 0843dfac is missing from this stack

#159's last commit ([FIX] ni_questionnaire: recompute the encounters' latest response on upgrade: the 17.0 post-migration script and the README "Upgrading from 16.0" section) is not in the replayed 17.0 history here. Upgrades still work (16→17→18→19 runs the script from 17.0 code), but the README here doesn't mention the step. Decide whether this branch should carry it.

Checked and fine: self.env._, group_ids, env.cr, the named placeholder in the deceased-patient warning. The encounter _create_answer access issue from #159 does not apply on 19.0 (officers may create responses on unrestricted surveys).

@wasud
wasud force-pushed the 19.0-mig-p1-questionnaire branch 2 times, most recently from de6e944 to 56baea5 Compare October 21, 2026 01:59
@wasud

wasud commented Oct 21, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review. Pushed (head 56baea57).

Tests: 102 pass on a fresh 19.0 database; pre-commit passes.

piruin and others added 24 commits October 21, 2026 10:56
Clinical instruments such as MMSE are not scored out of a fixed total: a
respondent with no schooling is not given three of the items, so the test
is scored out of 23 instead of 30 and the normal/abnormal cutoff moves
with education level, at 14, 17 or 22 points.

Core already shrinks the maximum, since _mark_done() drops the inactive
conditional questions from predefined_question_ids. What was missing is
everything downstream, so survey.grade gains two things: a points basis,
because a fixed points cutoff stops being a fixed percentage once the
total moves, and triggering_answer_ids, so one survey can hold several
sets of ranges keyed on the answer that decided which items were asked.
Those ranges have to overlap numerically, so the unique and overlap rules
move from _sql_constraints to Python keyed on _scope_key() - a Many2many
cannot take part in a SQL unique index.

Observations are built in _mark_done() after super(). The old write()
hook ran before core trimmed the denominator and so recorded the pre-trim
percentage.

survey.grade.interpretation_id lets the observation follow the grade
rather than ni.observation.reference.range, which scopes only by age and
gender and therefore cannot read a total whose maximum varies.

grading_basis, observation_answer_type and observation_score_type are
deliberately not required: a NOT NULL column on survey.survey or
survey.question breaks the tests of every module upstream of these two,
which run before either is in the registry.

Also corrects the value-mapped answer branch, which dispatched on
truthiness: a numerical answer of 0 was taken for "no answer given" and
datetime had no branch at all, both of which blocked survey completion.

The tests are modelled on MMSE-Thai 2002 itself so the shrinking maximum
and the three cutoffs are checked against a real instrument. Stock Odoo
16 gates a conditional question on a single answer, so the skipped items
hang off a binary "Attended school?" question while the cutoff bands hang
off a separate three-level "Education level" question.

Grade UI follows: triggering answers read "Question: Answer" since many
surveys reuse "Yes"/"No" across questions; a scoped grade is flagged in
the tree with the fa-code-fork icon the question tree already uses, with
the answers themselves on a "Conditional" tab of the grade form; and the
patient scoping (gender, age, interpretation) is reachable from the grade
form too, gender drawn as a mars/venus icon in the tree.

Both modules are committed together because the change is not separable -
ni_questionnaire overrides the very constraint and grade lookup that
survey_grading reworks, so neither half passes its own tests alone.

Took 3 minutes

Took 8 minutes
Core gates the entire score block of the completion page on
scoring_success_min, but a graded questionnaire has no meaningful passing
percentage - a points-based instrument such as ADL leaves it at 0. The
badge sat inside that block and so never reached the page.

Widening the gate to `scoring_success_min or grade_ids` is preferred over
moving the badge out of the block, because survey_subject rewrites the
same column through a positional t[1]/div xpath: inserting any element
ahead of that target breaks it with "cannot be located in parent view".
It also brings back survey_subject's own score line, which shared the
gate.

Passed/failed stays tied to scoring_success_min. Without one,
scoring_success is 0 >= 0 and every respondent would be congratulated,
and since a bare t-else always fires, the failure branch has to become a
t-elif on the same condition.

Both verdict specs address direct children of the scoring block rather
than matching t-else anywhere in the template. survey_subject carries a
t-else of its own and the apply order between the two modules follows
install order, so an unscoped match rewrites the wrong node - silently,
leaving a broken score line and an ungated verdict.

The guard for that combination lives in ni_questionnaire, the only module
that depends on both.

Took 49 minutes
grade_for() narrows the bands by the patient's gender and age, which is
invisible to whoever reads the result: two patients with the same score
can hold different grades with nothing on screen to explain it. Extend
grade_conditions() so the grading reference says which traits applied.

Only traits the survey actually grades differently by are named - a
gender when any band carries one, an age when the bands fall into more
than one window. Printing them unconditionally would put an age and a
gender on every result page that had no bearing on the grade.

Took 48 minutes
…upgrade

16.0 computed the encounter's latest response from a SQL view before the
response reached the database, so the stored response_latest_id, its grade and
date are empty or stale in 16.0 databases. 17.0 computes them from the
responses, but only when a response changes. Recompute them once in the 17.0
post-migration script, so that migrated encounters show their latest grade and
the "Print Questionnaire" button.

Found by the Phase 1 data migration pilot (nirun-life#141).
@wasud
wasud force-pushed the 19.0-mig-p1-questionnaire branch from 56baea5 to dbc4712 Compare October 21, 2026 03:57
@piruin
piruin merged commit 4d15cca into nirun-life:19.0 Oct 21, 2026
4 checks passed
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.

4 participants