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

From OHC Network Wiki
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 Extends the write spec with organizations: list[UUID4] (min_length=1). perform_extra_deserialization stashes them on obj._organizations; the view then links them through QuestionnaireOrganization, which rebuilds organization_cache
QuestionnaireUpdateSpec write · update Same as QuestionnaireWriteSpec, minus organizations
QuestionnaireReadSpec read · list/detail id (= external_id), slug, version, title, description, status, subject_type, styling_metadata, questions (raw list), created_by / updated_by (resolved via serialize_audit_users)

Value sets bound on questions: codesystem-observation (LOINC), unitsystem-ucum-units (UCUM). answer_value_set references any ValueSet by slug.

Questionnaire response

Spec Role Key fields / behaviour
EMRQuestionnaireResponseBase shared __model__ = QuestionnaireResponse
QuestionnaireResponseUpdate write · update status: QuestionnaireResponseStatusChoices (default completed) — the only handle for voiding a response by marking it entered_in_error
QuestionnaireResponseReadSpec read · detail id, status, questionnaire (nested QuestionnaireReadSpec), subject_id, responses (raw list), encounter (external id or None), structured_responses, structured_response_type, created_by / updated_by (UserSpec), created_date, modified_date

Submit request schema (not a DB write spec)

The submit endpoint (/questionnaire/<slug>/submit/) takes a plain Pydantic request, which handle_response() in utils.py validates and persists:

Spec Shape
QuestionnaireSubmitRequest None, patient: UUID4, results: list[QuestionnaireSubmitResult], form_submission: UUID4 | None }
QuestionnaireSubmitResult UUID5, body_site: Coding | None, method: Coding | None, taken_at: datetime | None, values: list[QuestionnaireSubmitResultValue], note: str | None, sub_results: list[list[QuestionnaireSubmitResult]] }
QuestionnaireSubmitResultValue None, unit: Coding | None, coding: Coding | None }

Form submission

Spec Role Key fields / behaviour
BaseFormSubmissionSpec shared None
FormSubmissionUpdateSpec write · update status: FormSubmissionStatusChoices, response_dump: dict
FormSubmissionWriteSpec write · create None. perform_extra_deserialization resolves the questionnaire by slug and the patient/encounter by external_id; when an encounter is given it overrides patient with the encounter's patient
FormSubmissionReadSpec read · detail id, status, response_dump, created_date, modified_date, created_by / updated_by (UserSpec)

Questionnaire response template

Spec Role Key fields / behaviour
QuestionnaireResponseTemplateBaseSpec shared None, template_data: TemplateData, name: str, description: str = ""
QuestionnaireResponseTemplateCreateSpec write · create None (slug), facility: UUID4 | None, users: list[str], facility_organizations: list[UUID4]. Requires facility whenever facility_organizations is set. Resolves questionnaire/facility and recomputes available_keys from the non-empty keys of template_data
QuestionnaireResponseTemplateUpdateSpec write · update users, facility_organizations; recomputes available_keys
QuestionnaireResponseTemplateReadSpec read · list created_date, modified_date; id = external_id
QuestionnaireResponseTemplateRetrieveSpec read · detail Extends the read spec with resolved users: list[dict], facility_organizations: list[dict], created_by, updated_by

template_data (TemplateData) shape

QuestionnaireResponseTemplate.template_data is an opaque JSONField that the spec validates as:

Field Type Notes

{{Field|model=Questionnaire|section=template_data (TemplateData) shape|name=medication_request|type=list[MedicationRequestTemplateSpec] | None|notes=Extends the [[References/Medication Request}}

questionnaire list[QuestionnaireAnswer] QuestionnaireAnswer = { question_id: str, answer: dict, meta: dict }
activity_definition list[ActivityDefinitionTemplateSpec] { slug (validated against ActivityDefinition), service_request: ServiceRequestUpdateSpec }
meta dict

available_keys (model ArrayField) is server-maintained: on create/update it is set to the template_data keys whose value is truthy.

FormSubmission

A submission event linking a questionnaire to a patient and, optionally, an encounter.

questionnaire → FK Questionnaire (CASCADE)
patient       → FK emr.Patient (CASCADE)
encounter     → FK emr.Encounter (CASCADE, nullable)
status        → CharField(255)         # FormSubmissionStatusChoices
response_dump → JSONField (default {})

QuestionnaireResponse

The answers for a subject. The questionnaire FK is nullable, so a response can outlive or detach from its definition.

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"

QuestionnaireOrganization / QuestionnaireFacilityOrganization

Through-models that scope a questionnaire to organizations — the first to an instance-level Organization, the second to a FacilityOrganization.

questionnaire → FK Questionnaire (CASCADE)
organization  → FK Organization | FacilityOrganization (CASCADE)

Each overrides save() to recompute the matching cache on the parent Questionnaire (see Methods & save behaviour). Their specs (questionnaire_organization.py) exclude both FKs from serialization (__exclude__ = ["questionnaire", "organization"]): the write spec accepts organization: UUID4, the read spec returns organization: dict.

QuestionnaireResponseTemplate

A reusable template that prefills questionnaire responses, optionally scoped to a facility and to specific facility organizations or users.

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

Methods & save behaviour

Questionnaire.get_questions_by_id()

Walks the questions tree, recursing into nested questions, and returns a { str(question_id): question } dict. The result is memoized on the instance via _questions_by_id_cache. If questions is not a list, it returns an empty dict.

QuestionnaireResponse.render_responses()

Joins stored responses against the live questionnaire definition. For each answer it looks up the question through questionnaire.get_questions_by_id() and returns a list of { "answer": ..., "question": ... }. It returns an empty list when there are no responses or no linked questionnaire, and skips any answer whose question_id is no longer in the definition.

Submission handling — handle_response()

resources/questionnaire/utils.py runs this server-side on every submission:

  1. Rejects the submission if the questionnaire status != "active" (questionnaire_inactive).
  2. Resolves encounter (required when subject_type == "encounter") and patient by external_id.
  3. Rejects empty submissions (questionnaire_empty).
  4. Prunes the question tree by enable_when rules; answers to disabled questions raise enable_when_failed.
  5. Validates each answer against its type (type checks per QuestionType), required, answer_value_set (the coding must belong to the value set), and quantity units. Any failure aborts with an errors payload.
  6. Builds ObservationSpec objects for coded/group questions and creates a QuestionnaireResponse (raw responses = the dumped results).
  7. When an encounter is present, bulk-creates the derived Observations, linked back to the response.

Organization cache sync

QuestionnaireOrganization.save() and QuestionnaireFacilityOrganization.save() call super().save(), then sync_questionnaire_cache(), which:

  1. Loads all through-rows for the parent questionnaire.
  2. Collects each linked organization's id plus its parent_cache (ancestor chain).
  3. De-duplicates the IDs.
  4. Writes them back via questionnaire.save(update_fields=[...])organization_cache for instance orgs, internal_organization_cache for facility orgs.

Saving any organization link triggers a second write to the Questionnaire row.

API integration notes

  • Questionnaires expose CRUD plus a submit endpoint (/questionnaire/<slug>/submit/). Create uses QuestionnaireSpec (requires ≥1 organizations); update uses QuestionnaireUpdateSpec; reads use QuestionnaireReadSpec.
  • questions and styling_metadata are open JSONFields at the DB layer, but the API validates questions against the recursive Question spec — unique link_id/id, the choice/quantity/group constraints, and value-set existence. styling_metadata is never validated.
  • organization_cache / internal_organization_cache and QuestionnaireResponseTemplate.available_keys are platform-maintained. Don't set them from clients; write through the through-models or template_data.
  • Submitting answers validates type/required/value-set server-side and, for clinical questions, materializes Observations. To void a response, set its status to entered_in_error via QuestionnaireResponseUpdate.
  • FormSubmissionWriteSpec resolves the questionnaire by slug and, when an encounter is supplied, forces the submission's patient to the encounter's patient.

Pages that link here: