Skip to main content
Version: 3.0

Observation Definition

An ObservationDefinition is master data describing how a particular observation is captured — its code, permitted data type, unit, method, body site, and (interpretation) reference ranges. You create one when you need a measurement collected consistently across forms; questionnaires reference it so the same observation (blood pressure, say) is recorded the same way everywhere, whether instance-wide or within a single facility. One observation code can back several definitions. The model loosely follows the FHIR ObservationDefinition resource.

The Django model is only storage: code, body_site, method, permitted_unit, component, and qualified_ranges are opaque JSONFields. The shape you write against — enums, value-set bindings, read/write contracts — lives in the Pydantic resource specs (care/emr/resources/observation_definition/spec.py), built on the EMRResource base.

Source:

Models

ModelPurpose
ObservationDefinitionMaster-data definition of how an observation is coded, typed, and measured

ObservationDefinition extends SlugBaseModel, which adds facility-scoped slug helpers on top of EMRBaseModel. From EMRBaseModel it inherits external_id, created_date/modified_date, soft-delete via deleted, created_by/updated_by, and the history/meta JSON fields.

ObservationDefinition fields

Identity & scope

FieldTypeNotes
facilityFK → Facility (PROTECT)Nullable, default=None. Set means the definition is scoped to that facility; null means instance-wide. Excluded from the base spec (__exclude__ = ["facility"]) and set server-side from the facility UUID on create
versionIntegerFieldDefaults to 1. Read-only — appears in ...ReadSpec only, never accepted on write
slugCharField(255)Set server-side from slug_value, then namespaced. See slug behaviour
titleCharField(1024)Required. Display name shown when authoring questionnaires; not the observation code
statusCharField(255)Required. Lifecycle code from ObservationStatusChoices — see Status values. Definitions move to retired rather than being destroyed
descriptionTextFieldRequired. Natural-language (markdown) description
derived_from_uriTextFieldExternal URI this definition was derived from. Optional (str | None, default None)

Coding & measurement

FieldTypeNotes
categoryCharField(255)Nullable in the model, but required in the API as ObservationCategoryChoices — see Category values
codeJSONFieldRequired. What is being observed: a Coding bound to the system-observation value set (LOINC) — see Coding shape
permitted_data_typeCharField(100)Required. QuestionType enum (questionnaire data types) — see Permitted data type values. group, display, and url are rejected
body_siteJSONFieldNullable. Coding bound to the system-body-site-observation value set (SNOMED CT)
methodJSONFieldNullable. Coding bound to the system-collection-method value set (SNOMED CT)
permitted_unitJSONFieldNullable. Coding bound to the system-ucum-units value set (UCUM). Care does not convert between units when displaying data

Components & ranges

FieldTypeNotes
componentJSONFieldNullable. list[ObservationDefinitionComponentSpec] | None. Several measured values under one definition (systolic + diastolic, for example), each with its own code, permitted_data_type, permitted_unit, and qualified_ranges — see ObservationDefinitionComponentSpec
qualified_rangesJSONFieldModel default list. Required in the API as list[QualifiedRangeSpec], though it may be empty. Condition-dependent reference ranges / interpretations — see QualifiedRangeSpec

Enum values

Status values

ObservationStatusChoices — bound to the status field.

Value
draft
active
retired
unknown

Category values

ObservationCategoryChoices — bound to the category field.

Value
social_history
vital_signs
imaging
laboratory
procedure
survey
exam
therapy
activity

Permitted data type values

QuestionType (from questionnaire/spec.py) — bound to permitted_data_type on both the definition and each component. validate_question_type rejects group, display, and url with "Cannot create a definition with this type".

ValueAllowed for a definition
groupNo (rejected)
booleanYes
decimalYes
integerYes
stringYes
textYes
displayNo (rejected)
dateYes
dateTimeYes
timeYes
choiceYes
urlNo (rejected)
quantityYes
structuredYes

JSON field shapes (nested specs)

Coding shape

The coded fields (code, body_site, method, permitted_unit) store a Coding object. The spec binds each to a value set via ValueSetBoundCoding[<slug>], so the submitted code is validated against that value set on write.

Coding {
system: str | null
version: str | null
code: str # required
display: str | null
} # extra="forbid" — unknown keys rejected
FieldBound value set (slug)Code system
codesystem-observationLOINC (http://loinc.org)
body_sitesystem-body-site-observationSNOMED CT (http://snomed.info/sct)
methodsystem-collection-methodSNOMED CT
permitted_unitsystem-ucum-unitsUCUM (http://unitsofmeasure.org)

ObservationDefinitionComponentSpec

Element of the component list — one per measured value when a definition carries several.

ObservationDefinitionComponentSpec {
code: ValueSetBoundCoding[system-observation] # required
permitted_data_type: QuestionType # required; group/display/url rejected
permitted_unit: ValueSetBoundCoding[system-ucum-units] | null = null
qualified_ranges: list[QualifiedRangeSpec] # required
}

QualifiedRangeSpec

Element of qualified_ranges (on the definition and on each component). A qualified range is either numeric (ranges) or categorical (coded value sets) — never both.

QualifiedRangeSpec {
title: str | null = null
conditions: list[EvaluatorConditionSpec] = []
ranges: list[NumericRangeSpec] = []
default_interpretation: InterpretationSpec | null = null
normal_coded_value_set: str | null = ""
critical_coded_value_set: str | null = ""
abnormal_coded_value_set: str | null = ""
valueset_interpretation: list[ValueSetInterpretationSpec] = []
}

validate_categorical_or_numeric enforces:

  • Either ranges (numeric) or at least one coded value set / valueset_interpretation (categorical) — one is required.
  • Numeric ranges and coded value sets cannot both appear.
  • Numeric ranges must not overlap (sorted by min, treating missing min/max as -∞/+∞).
  • Coded value-set slugs (normal_coded_value_set, critical_coded_value_set, abnormal_coded_value_set, and each valueset_interpretation.valuset) must be unique — duplicates are rejected.

NumericRangeSpec

NumericRangeSpec {
interpretation: InterpretationSpec # required
min: Decimal(max_digits=20, decimal_places=6) | null = null
max: Decimal(max_digits=20, decimal_places=6) | null = null
}

At least one of min/max must be provided (validate_range).

InterpretationSpec

InterpretationSpec {
display: str # required
icon: str | null = ""
color: str | null = ""
highlight: bool | null = false
code: Coding | null = {}
}

ValueSetInterpretationSpec

ValueSetInterpretationSpec {
interpretation: InterpretationSpec # required
valuset: str # value-set slug (note: spelling "valuset")
}

EvaluatorConditionSpec

Element of QualifiedRangeSpec.conditions. Validated against EvaluatorMetricsRegistry: an unknown metric raises, and the registered evaluator validates operation/value.

EvaluatorConditionSpec {
metric: str # must resolve in EvaluatorMetricsRegistry
operation: str
value: dict | str
}

Resource specs (API schema)

All specs subclass BaseObservationDefinitionSpec (itself a subclass of EMRResource, with __model__ = ObservationDefinition and __exclude__ = ["facility"]). EMRResource provides serialize/de_serialize plus the perform_extra_serialization / perform_extra_deserialization hooks.

Spec classRoleNotable fields / behaviour
BaseObservationDefinitionSpecShared baseid, title, status, description, category, code, permitted_data_type, component, body_site, method, permitted_unit, derived_from_uri, qualified_ranges. Runs validate_data_type and validate_qualified_ranges_consistency
ObservationDefinitionCreateSpecWrite · createAdds facility: UUID4 | null and slug_value: SlugType. Validates the facility exists; perform_extra_deserialization resolves the facility and sets obj.slug = slug_value
ObservationDefinitionUpdateSpecWrite · updateAdds slug_value: SlugType; perform_extra_deserialization sets obj.slug = slug_value. The facility is not re-bound on update
ObservationDefinitionReadSpecRead · list & detailAdds version: int | null, facility: dict | null, slug_config: dict, slug: str. perform_extra_serialization sets id = external_id, serializes facility via FacilityBareMinimumSpec, and sets slug_config = parse_slug(slug)

Notes:

  • Value-set bindingscode, body_site, method, permitted_unit (and component equivalents) are ValueSetBoundCoding[...]; the submitted coding is validated against the bound value set (system-observation, system-body-site-observation, system-collection-method, system-ucum-units).
  • Data-type guardpermitted_data_type (definition and component) rejects group, display, url.
  • Range consistencyvalidate_qualified_ranges_consistency requires every qualified_ranges entry on a definition to be the same kind: all numeric ranges or all coded value sets. Mixing raises.
  • slug_valueSlugType: 5–50 chars, URL-safe ([a-zA-Z0-9][a-zA-Z0-9_-]*[a-zA-Z0-9]), starting and ending alphanumeric. Stored namespaced as obj.slug (see below).
  • No metadata store__store_metadata__ is False, so spec fields map directly to model columns rather than into meta.

Methods & save behaviour

ObservationDefinition overrides nothing of its own. Slug handling comes from SlugBaseModel, and the create/update side effects live in the API specs.

Slug behaviour

SlugBaseModel sets FACILITY_SCOPED = True, so the stored slug is namespaced:

facility-scoped: f-<facility.external_id>-<slug_value>
instance-scoped: i-<slug_value>
  • calculate_slug() returns the f-… form when facility is set, otherwise the i-… form.
  • parse_slug(slug) validates the prefix and returns a dict: {"facility": <uuid>, "slug_value": <bare slug>} for f- slugs, or {"slug_value": <bare slug>} for i- slugs. ObservationDefinitionReadSpec exposes this as slug_config.

The create/update specs receive the bare slug_value and store it on obj.slug; the model's slug helpers apply the namespacing.

API integration notes

  • The facility binding is write-once: the create spec resolves the facility UUID to an FK; the update spec only re-sets slug_value and never re-binds the facility.
  • Retire definitions via status (retired) instead of deleting. The inherited deleted flag gives you ORM-level soft-delete if you need it.
  • permitted_unit codings follow UCUM, but Care never converts between units at display time — store data in the unit you intend to read.
  • convert_od_to_observation (observation.py) builds an Observation from a definition for a given encounter, carrying over category and code and defaulting status to final.