Toggle menu
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

References/Questionnaire: Difference between revisions

From OHC Network Wiki
Content deleted Content added
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/&lt;slug&gt;/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 = &quot;&quot;</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 &amp; save behaviour]]). Their specs (<code>questionnaire_organization.py</code>) exclude both FKs from serialization (<code>__exclude__ = [&quot;questionnaire&quot;, &quot;organization&quot;]</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 &amp; 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>{ &quot;answer&quot;: ..., &quot;question&quot;: ... }</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 != &quot;active&quot;</code> (<code>questionnaire_inactive</code>).
# Resolves <code>encounter</code> (required when <code>subject_type == &quot;encounter&quot;</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/&lt;slug&gt;/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

referencedefinitionsCARE 3.0+

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 / quantity types must have answer_option or answer_value_set.
  • group types must have at least one sub-question.
  • answer_value_set, when set, must reference an existing ValueSet.

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
Pages that link here: