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
More languages
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}}