References/Questionnaire: Difference between revisions
More languages
More actions
OHC identity seed |
Automated edit (via update-page on MediaWiki MCP Server) |
||
| Line 351: | Line 351: | ||
| <code>QuestionnaireSpec</code> |
| <code>QuestionnaireSpec</code> |
||
| write · create |
| write · create |
||
| Extends the write spec with <code>organizations: list[UUID4]</code> (<code>min_length=1</code>). <code>perform_extra_deserialization</code> stashes them on <code>obj._organizations</code>; the view then links them through <code>QuestionnaireOrganization</code>, which rebuilds <code>organization_cache</code> |
|||
|- |
|||
| <code>QuestionnaireUpdateSpec</code> |
|||
| write · update |
|||
| Same as <code>QuestionnaireWriteSpec</code>, minus <code>organizations</code> |
|||
|- |
|||
| <code>QuestionnaireReadSpec</code> |
|||
| read · list/detail |
|||
| <code>id</code> (= <code>external_id</code>), <code>slug</code>, <code>version</code>, <code>title</code>, <code>description</code>, <code>status</code>, <code>subject_type</code>, <code>styling_metadata</code>, <code>questions</code> (raw list), <code>created_by</code> / <code>updated_by</code> (resolved via <code>serialize_audit_users</code>) |
|||
|} |
|||
{{Navbox definitions}} |
|||
Value sets bound on questions: <code>code</code> → <code>system-observation</code> (LOINC), <code>unit</code> → <code>system-ucum-units</code> (UCUM). <code>answer_value_set</code> references any [[References/Value Set|<code>ValueSet</code>]] by slug. |
|||
=== Questionnaire response === |
|||
{| class="wikitable" |
|||
|- |
|||
! Spec |
|||
! Role |
|||
! Key fields / behaviour |
|||
|- |
|||
| <code>EMRQuestionnaireResponseBase</code> |
|||
| shared |
|||
| <code>__model__ = QuestionnaireResponse</code> |
|||
|- |
|||
| <code>QuestionnaireResponseUpdate</code> |
|||
| write · update |
|||
| <code>status: QuestionnaireResponseStatusChoices</code> (default <code>completed</code>) — the only handle for voiding a response by marking it <code>entered_in_error</code> |
|||
|- |
|||
| <code>QuestionnaireResponseReadSpec</code> |
|||
| read · detail |
|||
| <code>id</code>, <code>status</code>, <code>questionnaire</code> (nested <code>QuestionnaireReadSpec</code>), <code>subject_id</code>, <code>responses</code> (raw list), <code>encounter</code> (external id or <code>None</code>), <code>structured_responses</code>, <code>structured_response_type</code>, <code>created_by</code> / <code>updated_by</code> (<code>UserSpec</code>), <code>created_date</code>, <code>modified_date</code> |
|||
|} |
|||
==== Submit request schema (not a DB write spec) ==== |
|||
The submit endpoint (<code>/questionnaire/<slug>/submit/</code>) takes a plain Pydantic request, which <code>handle_response()</code> in <code>utils.py</code> validates and persists: |
|||
{| class="wikitable" |
|||
|- |
|||
! Spec |
|||
! Shape |
|||
|- |
|||
| <code>QuestionnaireSubmitRequest</code> |
|||
| <code>{ resource_id: UUID4, encounter: UUID4 | None, patient: UUID4, results: list[QuestionnaireSubmitResult], form_submission: UUID4 | None }</code> |
|||
|- |
|||
| <code>QuestionnaireSubmitResult</code> |
|||
| <code>{ question_id: UUID4|UUID5, body_site: Coding | None, method: Coding | None, taken_at: datetime | None, values: list[QuestionnaireSubmitResultValue], note: str | None, sub_results: list[list[QuestionnaireSubmitResult]] }</code> |
|||
|- |
|||
| <code>QuestionnaireSubmitResultValue</code> |
|||
| <code>{ value: str | None, unit: Coding | None, coding: Coding | None }</code> |
|||
|} |
|||
=== Form submission === |
|||
{| class="wikitable" |
|||
|- |
|||
! Spec |
|||
! Role |
|||
! Key fields / behaviour |
|||
|- |
|||
| <code>BaseFormSubmissionSpec</code> |
|||
| shared |
|||
| <code>__model__ = FormSubmission</code>, <code>id: UUID4 | None</code> |
|||
|- |
|||
| <code>FormSubmissionUpdateSpec</code> |
|||
| write · update |
|||
| <code>status: FormSubmissionStatusChoices</code>, <code>response_dump: dict</code> |
|||
|- |
|||
| <code>FormSubmissionWriteSpec</code> |
|||
| write · create |
|||
| Adds <code>questionnaire: str</code> (slug), <code>patient: UUID4</code>, <code>encounter: UUID4 | None</code>. <code>perform_extra_deserialization</code> resolves the questionnaire by slug and the patient/encounter by <code>external_id</code>; when an encounter is given it overrides <code>patient</code> with the encounter's patient |
|||
|- |
|||
| <code>FormSubmissionReadSpec</code> |
|||
| read · detail |
|||
| <code>id</code>, <code>status</code>, <code>response_dump</code>, <code>created_date</code>, <code>modified_date</code>, <code>created_by</code> / <code>updated_by</code> (<code>UserSpec</code>) |
|||
|} |
|||
=== Questionnaire response template === |
|||
{| class="wikitable" |
|||
|- |
|||
! Spec |
|||
! Role |
|||
! Key fields / behaviour |
|||
|- |
|||
| <code>QuestionnaireResponseTemplateBaseSpec</code> |
|||
| shared |
|||
| <code>__model__ = QuestionnaireResponseTemplate</code>, <code>id: UUID4 | None</code>, <code>template_data: TemplateData</code>, <code>name: str</code>, <code>description: str = ""</code> |
|||
|- |
|||
| <code>QuestionnaireResponseTemplateCreateSpec</code> |
|||
| write · create |
|||
| Adds <code>questionnaire: str | None</code> (slug), <code>facility: UUID4 | None</code>, <code>users: list[str]</code>, <code>facility_organizations: list[UUID4]</code>. Requires <code>facility</code> whenever <code>facility_organizations</code> is set. Resolves questionnaire/facility and recomputes <code>available_keys</code> from the non-empty keys of <code>template_data</code> |
|||
|- |
|||
| <code>QuestionnaireResponseTemplateUpdateSpec</code> |
|||
| write · update |
|||
| <code>users</code>, <code>facility_organizations</code>; recomputes <code>available_keys</code> |
|||
|- |
|||
| <code>QuestionnaireResponseTemplateReadSpec</code> |
|||
| read · list |
|||
| <code>created_date</code>, <code>modified_date</code>; <code>id</code> = <code>external_id</code> |
|||
|- |
|||
| <code>QuestionnaireResponseTemplateRetrieveSpec</code> |
|||
| read · detail |
|||
| Extends the read spec with resolved <code>users: list[dict]</code>, <code>facility_organizations: list[dict]</code>, <code>created_by</code>, <code>updated_by</code> |
|||
|} |
|||
==== <code>template_data</code> (<code>TemplateData</code>) shape ==== |
|||
<code>QuestionnaireResponseTemplate.template_data</code> is an opaque <code>JSONField</code> that the spec validates as: |
|||
{| class="wikitable" |
|||
|- |
|||
! Field |
|||
! Type |
|||
! Notes |
|||
{{Field|model=Questionnaire|section=template_data (TemplateData) shape|name=medication_request|type=list[MedicationRequestTemplateSpec] | None|notes=Extends the [[References/Medication Request}} |
|||
{{Field|model=Questionnaire|section=template_data (TemplateData) shape|name=questionnaire|type=list[QuestionnaireAnswer] | None|notes=<code>QuestionnaireAnswer = { question_id: str, answer: dict, meta: dict }</code>}} |
|||
{{Field|model=Questionnaire|section=template_data (TemplateData) shape|name=activity_definition|type=list[ActivityDefinitionTemplateSpec] | None|notes=<code>{ slug (validated against ActivityDefinition), service_request: ServiceRequestUpdateSpec }</code>}} |
|||
{{Field|model=Questionnaire|section=template_data (TemplateData) shape|name=meta|type=dict | None|notes=}} |
|||
|} |
|||
<code>available_keys</code> (model <code>ArrayField</code>) is server-maintained: on create/update it is set to the <code>template_data</code> keys whose value is truthy. |
|||
== Related models == |
|||
=== <code>FormSubmission</code> === |
|||
A submission event linking a questionnaire to a patient and, optionally, an encounter. |
|||
<syntaxhighlight lang="text">questionnaire → FK Questionnaire (CASCADE) |
|||
patient → FK emr.Patient (CASCADE) |
|||
encounter → FK emr.Encounter (CASCADE, nullable) |
|||
status → CharField(255) # FormSubmissionStatusChoices |
|||
response_dump → JSONField (default {})</syntaxhighlight> |
|||
=== <code>QuestionnaireResponse</code> === |
|||
The answers for a subject. The <code>questionnaire</code> FK is nullable, so a response can outlive or detach from its definition. |
|||
<syntaxhighlight lang="text">questionnaire → FK Questionnaire (CASCADE, nullable) |
|||
subject_id → UUIDField |
|||
responses → JSONField (default []) # raw submitted results |
|||
structured_responses → JSONField (default {}) # extracted/structured data |
|||
structured_response_type → CharField (nullable) |
|||
patient → FK emr.Patient (CASCADE) |
|||
encounter → FK emr.Encounter (CASCADE, nullable) |
|||
form_submission → FK FormSubmission (CASCADE, nullable) |
|||
status → CharField(255), default "completed"</syntaxhighlight> |
|||
=== <code>QuestionnaireOrganization</code> / <code>QuestionnaireFacilityOrganization</code> === |
|||
Through-models that scope a questionnaire to organizations — the first to an instance-level <code>Organization</code>, the second to a <code>FacilityOrganization</code>. |
|||
<syntaxhighlight lang="text">questionnaire → FK Questionnaire (CASCADE) |
|||
organization → FK Organization | FacilityOrganization (CASCADE)</syntaxhighlight> |
|||
Each overrides <code>save()</code> to recompute the matching cache on the parent <code>Questionnaire</code> (see [[#organization-cache-sync|Methods & save behaviour]]). Their specs (<code>questionnaire_organization.py</code>) exclude both FKs from serialization (<code>__exclude__ = ["questionnaire", "organization"]</code>): the write spec accepts <code>organization: UUID4</code>, the read spec returns <code>organization: dict</code>. |
|||
=== <code>QuestionnaireResponseTemplate</code> === |
|||
A reusable template that prefills questionnaire responses, optionally scoped to a facility and to specific facility organizations or users. |
|||
<syntaxhighlight lang="text">facility → FK facility.Facility (CASCADE, nullable) |
|||
name → CharField(255) |
|||
description → TextField (default "") |
|||
template_data → JSONField (default {}) # TemplateData shape |
|||
questionnaire → FK Questionnaire (CASCADE, nullable, default None) |
|||
facility_organizations → ArrayField[int] (default []) |
|||
users → ArrayField[int] (default []) |
|||
available_keys → ArrayField[CharField(255)] (default []) # server-maintained</syntaxhighlight> |
|||
== Methods & save behaviour == |
|||
=== <code>Questionnaire.get_questions_by_id()</code> === |
|||
Walks the <code>questions</code> tree, recursing into nested <code>questions</code>, and returns a <code>{ str(question_id): question }</code> dict. The result is memoized on the instance via <code>_questions_by_id_cache</code>. If <code>questions</code> is not a list, it returns an empty dict. |
|||
=== <code>QuestionnaireResponse.render_responses()</code> === |
|||
Joins stored <code>responses</code> against the live questionnaire definition. For each answer it looks up the question through <code>questionnaire.get_questions_by_id()</code> and returns a list of <code>{ "answer": ..., "question": ... }</code>. It returns an empty list when there are no responses or no linked questionnaire, and skips any answer whose <code>question_id</code> is no longer in the definition. |
|||
=== Submission handling — <code>handle_response()</code> === |
|||
<code>resources/questionnaire/utils.py</code> runs this server-side on every submission: |
|||
# Rejects the submission if the questionnaire <code>status != "active"</code> (<code>questionnaire_inactive</code>). |
|||
# Resolves <code>encounter</code> (required when <code>subject_type == "encounter"</code>) and <code>patient</code> by <code>external_id</code>. |
|||
# Rejects empty submissions (<code>questionnaire_empty</code>). |
|||
# Prunes the question tree by <code>enable_when</code> rules; answers to disabled questions raise <code>enable_when_failed</code>. |
|||
# Validates each answer against its <code>type</code> (type checks per [[#questiontype-values|QuestionType]]), <code>required</code>, <code>answer_value_set</code> (the coding must belong to the value set), and <code>quantity</code> units. Any failure aborts with an <code>errors</code> payload. |
|||
# Builds <code>ObservationSpec</code> objects for coded/group questions and creates a <code>QuestionnaireResponse</code> (raw <code>responses</code> = the dumped results). |
|||
# When an encounter is present, bulk-creates the derived [[References/Observation|Observations]], linked back to the response. |
|||
=== Organization cache sync === |
|||
<code>QuestionnaireOrganization.save()</code> and <code>QuestionnaireFacilityOrganization.save()</code> call <code>super().save()</code>, then <code>sync_questionnaire_cache()</code>, which: |
|||
# Loads all through-rows for the parent questionnaire. |
|||
# Collects each linked organization's <code>id</code> plus its <code>parent_cache</code> (ancestor chain). |
|||
# De-duplicates the IDs. |
|||
# Writes them back via <code>questionnaire.save(update_fields=[...])</code> — <code>organization_cache</code> for instance orgs, <code>internal_organization_cache</code> for facility orgs. |
|||
Saving any organization link triggers a second write to the <code>Questionnaire</code> row. |
|||
== API integration notes == |
|||
* Questionnaires expose CRUD plus a submit endpoint (<code>/questionnaire/<slug>/submit/</code>). Create uses <code>QuestionnaireSpec</code> (requires ≥1 <code>organizations</code>); update uses <code>QuestionnaireUpdateSpec</code>; reads use <code>QuestionnaireReadSpec</code>. |
|||
* <code>questions</code> and <code>styling_metadata</code> are open <code>JSONField</code>s at the DB layer, but the API validates <code>questions</code> against the recursive [[#question-nested-shape|<code>Question</code>]] spec — unique <code>link_id</code>/<code>id</code>, the choice/quantity/group constraints, and value-set existence. <code>styling_metadata</code> is never validated. |
|||
* <code>organization_cache</code> / <code>internal_organization_cache</code> and <code>QuestionnaireResponseTemplate.available_keys</code> are platform-maintained. Don't set them from clients; write through the through-models or <code>template_data</code>. |
|||
* Submitting answers validates type/required/value-set server-side and, for clinical questions, materializes [[References/Observation|Observations]]. To void a response, set its status to <code>entered_in_error</code> via <code>QuestionnaireResponseUpdate</code>. |
|||
* <code>FormSubmissionWriteSpec</code> resolves the questionnaire by slug and, when an encounter is supplied, forces the submission's patient to the encounter's patient. |
|||
{{Related}} |
{{Related}} |
||
Latest revision as of 09:31, 6 July 2026
A Questionnaire is a versioned, FHIR-inspired form: a tree of questions answered about a patient or an encounter. You touch it whenever you define a data-collection form, render one for filling, or submit answers — which can flow back out as Observations. It captures both clinical data and arbitrary non-clinical structured data.
The Django model is only the storage layer. The API schema you code against lives in the Pydantic resource specs under care/emr/resources/questionnaire*: enums, the nested shape of the JSON fields, validation, and the split between read and write contracts.
Source (model): [https://github.com/ohcnetwork/care/blob/develop/care/emr/models/questionnaire.py care/emr/models/questionnaire.py]
Source (specs): [https://github.com/ohcnetwork/care/blob/develop/care/emr/resources/questionnaire/spec.py resources/questionnaire/spec.py] · [https://github.com/ohcnetwork/care/blob/develop/care/emr/resources/questionnaire/utils.py resources/questionnaire/utils.py] · [https://github.com/ohcnetwork/care/blob/develop/care/emr/resources/questionnaire/questionnaire_organization.py resources/questionnaire/questionnaire_organization.py] · [https://github.com/ohcnetwork/care/blob/develop/care/emr/resources/questionnaire_response/spec.py resources/questionnaire_response/spec.py] · [https://github.com/ohcnetwork/care/blob/develop/care/emr/resources/questionnaire_response_template/spec.py resources/questionnaire_response_template/spec.py] · [https://github.com/ohcnetwork/care/blob/develop/care/emr/resources/form_submission/spec.py resources/form_submission/spec.py]
Models
Six models back the forms feature. The Questionnaire is the definition; everything else records submissions, answers, organization scope, or reusable prefill.
| Model | Purpose |
|---|---|
Questionnaire
|
A versioned form definition: questions, styling, subject type, and status |
FormSubmission
|
A submission of a questionnaire against a patient (and optional encounter) |
QuestionnaireResponse
|
A stored set of answers for a subject, optionally tied to a submission |
QuestionnaireOrganization
|
Scopes a questionnaire to an instance-level Organization
|
QuestionnaireFacilityOrganization
|
Scopes a questionnaire to a FacilityOrganization
|
QuestionnaireResponseTemplate
|
Reusable prefill template for questionnaire responses |
All extend EMRBaseModel, which supplies external_id, audit fields, and soft-delete semantics.
Questionnaire fields
Definition
| Field | Type | Req | Notes |
|---|---|---|---|
version |
CharField(255) |
yes | |
slug |
CharField(255) |
yes | |
title |
CharField(255) |
yes | |
description |
TextField |
no | |
subject_type |
CharField(255) |
yes | |
status |
CharField(255) |
yes | |
styling_metadata |
JSONField |
no | |
questions |
JSONField |
yes | |
organization_cache |
ArrayField[int] |
— | |
internal_organization_cache |
ArrayField[int] |
— |
The write spec also carries a type: str field (default "custom") that has no backing model column.
Organization scope caches
organization_cache and internal_organization_cache hold flattened organization IDs so access filtering can run without deep joins. The through-models below own them; clients never write them directly.
| Field | Maintained by |
|---|---|
organization_cache
|
QuestionnaireOrganization.sync_questionnaire_cache() — instance Organization IDs plus each org's parent_cache
|
internal_organization_cache
|
QuestionnaireFacilityOrganization.sync_questionnaire_cache() — FacilityOrganization IDs plus each org's parent_cache
|
Enum values
QuestionnaireStatus values
The lifecycle state of the definition, modeled on FHIR publication-status. Once a questionnaire is active, edit and delete are off the table — move it to retired instead.
| Value |
|---|
active
|
retired
|
draft
|
SubjectType values
What kind of resource the form is about. This drives whether an encounter is required at submit time.
| Value |
|---|
patient
|
encounter
|
QuestionType values
The type of each question (Question.type). The commented-out members open_choice, attachment, and reference are not implemented.
| Value | Notes |
|---|---|
group
|
Container; must have ≥1 sub-question |
boolean
|
Validated against true/false/1/0 on submit
|
decimal
|
|
integer
|
|
string
|
|
text
|
Length capped by settings.MAX_QUESTIONNAIRE_TEXT_RESPONSE_SIZE on submit
|
display
|
Display-only, no answer |
date
|
ISO date |
dateTime
|
Must include timezone on submit |
time
|
%H:%M:%S
|
choice
|
Requires answer_option or answer_value_set
|
url
|
Must have scheme + netloc |
quantity
|
Requires answer_option or answer_value_set; answers need a unit
|
structured
|
Skipped by response validation |
EnableOperator values
The comparison for an enable_when condition (EnableWhen.operator).
| Value |
|---|
exists
|
equals
|
not_equals
|
greater
|
less
|
greater_or_equals
|
less_or_equals
|
EnableBehavior values
How multiple enable_when conditions combine (Question.enable_behavior).
| Value | Meaning |
|---|---|
all
|
Enable only if all conditions pass (default on submit) |
any
|
Enable if any condition passes |
DisabledDisplay values
How a disabled question renders (Question.disabled_display).
| Value |
|---|
hidden
|
protected
|
AnswerConstraint values
Whether free input outside the options is allowed (Question.answer_constraint).
| Value |
|---|
required
|
optional
|
QuestionnaireResponseStatusChoices values
QuestionnaireResponse.status (model default "completed").
| Value |
|---|
completed
|
entered_in_error
|
FormSubmissionStatusChoices values
FormSubmission.status.
| Value |
|---|
draft
|
submitted
|
entered_in_error
|
Question nested shape
Questionnaire.questions is an opaque JSONField, but the API contract is a recursive list of Question (QuestionnaireBaseSpec). Fields, with their real types and validation:
| Field | Type | Req | Default | Notes |
|---|---|---|---|---|
link_id |
str |
yes | ||
id |
UUID4 |
no | ||
code |
Coding bound to value set system-observation |
no | ||
collect_time |
bool |
no | ||
collect_performer |
bool |
no | ||
text |
str |
yes | ||
description |
str |
no | ||
type |
QuestionType |
yes | ||
structured_type |
str |
no | ||
enable_when |
list[EnableWhen] |
no | ||
enable_behavior |
EnableBehavior |
no | ||
disabled_display |
DisabledDisplay |
no | ||
collect_body_site |
bool |
no | ||
collect_method |
bool |
no | ||
required |
bool |
no | ||
repeats |
bool |
no | ||
read_only |
bool |
no | ||
max_length |
int |
no | ||
answer_constraint |
AnswerConstraint |
no | ||
answer_option |
list[AnswerOption] |
no | ||
answer_value_set |
str |
no | ||
is_observation |
bool |
no | ||
unit |
Coding bound to value set system-ucum-units |
no | ||
questions |
list[Question] |
no | ||
formula |
str |
no | ||
styling_metadata |
dict |
no | ||
templates |
list[TemplateConfig] |
no | ||
is_component |
bool |
no |
A model_validator (mode after) enforces three rules:
choice/quantitytypes must haveanswer_optionoranswer_value_set.grouptypes must have at least one sub-question.answer_value_set, when set, must reference an existingValueSet.
Sub-specs of Question
| Spec | Shape |
|---|---|
EnableWhen
|
{ question: str (link_id), operator: EnableOperator, answer: Any }
|
AnswerOption
|
{ value: Any (non-blank, stripped), initial_selected: bool = false }
|
Performer
|
None, text: str | None } |
TemplateConfig
|
None, meta: dict | None } |
Resource specs (API schema)
Every spec builds on EMRResource, which provides serialize / de_serialize. Read specs run perform_extra_serialization; write specs run perform_extra_deserialization.
Questionnaire
| Spec | Role | Key fields / behaviour |
|---|---|---|
QuestionnaireBaseSpec
|
shared | __model__ = Questionnaire
|
QuestionnaireWriteSpec
|
write (base) | version (frozen "1.0"), slug (SlugType), title, description, type ("custom"), status, subject_type, styling_metadata, questions. Validates slug uniqueness and that it doesn't shadow internal types, a non-empty title, and unique link_ids and ids across the whole tree
|
QuestionnaireSpec
|
write · create
Related
|