Skip to main content
Version: 3.1

Base models & conventions

Every Care EMR resource inherits a shared base that supplies opaque IDs, audit fields, soft-delete, history, slugs, and feature flags. A resource author writes domain fields and validation; the base handles the rest. It spans two layers:

  • Storage layer — abstract Django models in care/emr/models and care/utils/models. Several of their columns are opaque JSONFields whose structure lives elsewhere; the model alone won't tell you what goes inside them.
  • API layer — Pydantic resource specs built on EMRResource (care/emr/resources/base.py). These define enums, field validation, the read/write schemas, and the structured shapes that fill those JSON columns via nested specs. The structured types in care/emr/resources/common/ are reused across nearly every resource.

Come here for the rules a resource gets for free: the soft-delete contract, what serialize/de_serialize do, how write and read schemas diverge, and the common spec types behind the JSON fields. Concrete resources such as Patient inherit a storage base and define their own specs on top of EMRResource.

Source: care/emr/models/base.py, care/utils/models/base.py, care/emr/resources/base.py, care/emr/resources/common/

Models

ModelLayerPurpose
BaseModelstorageLowest-level abstract base — opaque external_id, timestamps, and soft-delete
BaseManagerstorageDefault manager that hides soft-deleted rows
BaseFlagstorageAbstract base for feature-flag tables with cache-backed lookups
EMRBaseModelstorageStandard base for EMR resources — adds audit created_by/updated_by, history, and meta
SlugBaseModelstorageEMR base that adds facility-scoped or instance-scoped slug helpers
EMRResourceAPIPydantic base for all resource specs — serialize / de_serialize between DB objects and API schemas

All five storage models are abstract = True — none owns a database table. A concrete resource model inherits one and contributes its own columns.

The storage models inherit in one chain:

models.Model
└─ BaseModel (care/utils/models/base.py)
├─ BaseFlag (care/utils/models/base.py)
└─ EMRBaseModel (care/emr/models/base.py)
└─ SlugBaseModel

The API specs form a parallel hierarchy under pydantic.BaseModel:

pydantic.BaseModel
└─ EMRResource (care/emr/resources/base.py)
└─ <Resource>SpecBase / <Resource>CreateSpec / ...ReadSpec / ...RetrieveSpec

Storage layer

BaseModel fields

The root abstract model. Every other base — and so every persisted Care resource — carries these four columns.

FieldTypeRequiredDefaultNotes
external_idUUIDFieldyesuuid4unique, db_index. The opaque public identifier used in URLs and API payloads — never expose the integer pk
created_dateDateTimeFieldnoauto_now_addnullable, db_index. Set once on insert
modified_dateDateTimeFieldnoauto_nownullable, db_index. Bumped on every save
deletedBooleanFieldyesFalsedb_index. Soft-delete marker

objects is overridden to a BaseManager, so the default queryset returns only live rows.

Soft delete

BaseModel.delete() issues no SQL DELETE. It flips the flag and persists only that column:

def delete(self, *args):
self.deleted = True
self.save(update_fields=["deleted"])

Rows stay put for audit and referential integrity. To reach deleted rows, bypass the default manager — for example via Model._base_manager or an unfiltered queryset.

EMRBaseModel fields

The standard base for EMR resource models. On top of the BaseModel columns it adds audit, history, and metadata:

FieldTypeRequiredDefaultNotes
historyJSONFieldnodictFlattened JSON array of every version of the resource with its performer. Not returned by regular endpoints — served on request through a dedicated history API for audit and point-in-time reconstruction
metaJSONFieldnodictOpen metadata bag. EMRResource writes spec fields here when __store_metadata__ = True (see below)
created_byFK → users.UsernoNoneon_delete=SET_NULL, nullable. related_name="%(app_label)s_%(class)s_created_by"
updated_byFK → users.UsernoNoneon_delete=SET_NULL, nullable. related_name="%(app_label)s_%(class)s_updated_by"

The %(app_label)s_%(class)s_… related_name template lets every concrete subclass reuse the same FK definitions without reverse-accessor collisions.

SlugBaseModel helpers

Extends EMRBaseModel for resources that expose a human-readable, scoped slug — value sets and definitions, for instance. It adds no columns of its own; concrete subclasses supply their own slug and, when facility-scoped, facility fields. It sets the class flag FACILITY_SCOPED = True.

MethodBehaviour
calculate_slug_from_facility(facility_external_id, slug)Class helper → f-<facility_external_id>-<slug>
calculate_slug_from_instance(slug)Class helper → i-<slug>
calculate_slug()Instance helper — facility-scoped form when FACILITY_SCOPED and facility are set, otherwise instance-scoped
parse_slug(slug)Reverses the encoding; returns {facility, slug_value} for f- slugs or {slug_value} for i- slugs; raises ValueError on invalid input

Slug encoding:

facility-scoped: f-<facility external_id (36-char UUID)>-<slug>
instance-scoped: i-<slug>

parse_slug reads the facility segment as slug[2:38] (36 chars), validates it as a UUID, takes slug[39:] as the slug value, and rejects any slug of length ≤ 2.


BaseManager

The default manager set as objects on BaseModel. Every query through Model.objects is implicitly scoped to deleted=False:

def get_queryset(self):
return super().get_queryset().filter(deleted=False)

BaseFlag

Abstract base for per-entity feature-flag tables (facility or organization flags, for example). It extends BaseModel, stores one validated flag name, and serves all lookups from cache.

FieldTypeNotes
flagCharField(max_length=1024)Flag name, validated against FlagRegistry for the subclass's flag_type

Subclasses configure these class attributes:

AttributePurpose
cache_key_templateCache key for a single (entity_id, flag_name) pair
all_flags_cache_key_templateCache key for all flags of one entity
flag_typeFlag category validated against FlagRegistry
entity_field_nameName of the FK field pointing at the owning entity

Helper properties and classmethods:

MemberBehaviour
entityResolves the related entity via entity_field_name
entity_idResolves <entity_field_name>_id without a join
validate_flag(flag_name)Validates the name against FlagRegistry for flag_type
check_entity_has_flag(entity_id, flag_name)Cached exists() lookup (TTL 1 day)
get_all_flags(entity_id)Cached tuple of all flag names for an entity (TTL 1 day)

API layer

EMRResource

The Pydantic base (care/emr/resources/base.py) that every resource spec extends. It moves data both ways between Django model instances and the API schema, and holds the serialization rules in one place.

Class attributeDefaultRole
__model__NoneThe Django model the spec serializes to/from
__exclude__[]Field names skipped in both serialize and de_serialize
__store_metadata__FalseWhen True, spec fields that are not DB columns are read from / written to the model's meta JSON bag
__version__0.1Stamped onto every serialized object as version
meta{}Open metadata dict carried on the spec itself
MethodDirectionBehaviour
serialize(obj, user=None)DB → APIBuilds a spec via model_construct from the DB object's columns; copies meta fields back out when __store_metadata__; calls perform_extra_serialization (always sets mapping["id"] = obj.external_id) and, when a user is passed, perform_extra_user_serialization; stamps version
de_serialize(obj=None, partial=False)API → DBDumps the spec (exclude_defaults=True), writes mapped fields onto a new or existing model instance (skipping __exclude__, id, external_id), routes non-column fields into meta when __store_metadata__, then calls perform_extra_deserialization(is_update, obj). is_update is True when an existing obj is supplied
perform_extra_serialization(mapping, obj)DB → APIHook for resolving derived/nested read fields; base sets id
perform_extra_deserialization(is_update, obj)API → DBHook for server-side side effects on write (e.g. appending to status_history, resolving FKs from external_id, validating coded values against a value set)
serialize_audit_users(mapping, obj)DB → APIHelper that fills created_by / updated_by from a cached UserSpec
to_json()model_dump(mode="json", exclude=["meta"])
get_database_mapping()Lists the model's non-FK column names, used to decide which spec fields map to columns

Each concrete resource exposes a small set of specs built on EMRResource, named by convention:

Spec classKindRole
<X>SpecBasesharedCommon fields shared by the write/read specs
<X>CreateSpecwrite · createRequest body for POST; de_serialize builds a new model instance
<X>UpdateSpecwrite · updateRequest body for PUT/PATCH; de_serialize updates an existing instance (is_update=True)
<X>ListSpecread · listLightweight response for list endpoints
<X>ReadSpec / <X>RetrieveSpecread · detailFull response for the detail endpoint (often resolving nested/coded fields in perform_extra_serialization)

Coded fields commonly bind to a value set — a system/code allow-list. The resource spec declares the binding (e.g. via json_schema_extra={"slug": "<valueset-slug>"} on the field) and validates the submitted Coding against it in perform_extra_deserialization. Status fields commonly maintain a server-side status_history appended on create/update. Each resource page documents its exact bindings and side effects.

PeriodSpec

Defined alongside EMRResource in base.py. The validated period type used by write specs — distinct from the looser Period read type in common/.

FieldTypeRequiredDefaultValidation
startdatetimenoNonemust be timezone-aware
enddatetimenoNonemust be timezone-aware

When both are set, start must be ≤ end. Naive datetimes are rejected with "Start/End Date must be timezone aware".

PhoneNumber

An Annotated type exported from base.py: a phone string validated by PhoneNumberValidator with number_format="E164", no default region, and no region restriction. Use it wherever a spec field accepts a phone number.


Shared common specs

care/emr/resources/common/ holds the structured types reused across resources — the actual shapes behind many of the opaque JSONFields in the storage layer.

Coding

A single code from a code system. model_config = extra="forbid".

FieldTypeRequiredDefaultNotes
systemstrnoNoneURI of the code system
versionstrnoNoneCode-system version
codestryesThe code value
displaystrnoNoneHuman-readable label

CodeableConcept

A concept expressed as one or more codings plus free text. extra="forbid".

FieldTypeRequiredDefaultNotes
idstrnoNone
codinglist[Coding]noNoneOne or more Coding entries
textstr | Noneyes (field present)Free-text rendering; declared without a default, so the key must be supplied (may be null)

Period

The read/storage period shape — looser than PeriodSpec, with no timezone or ordering validation. extra="forbid".

FieldTypeRequiredDefault
idstrnoNone
startdatetimenoNone
enddatetimenoNone

Quantity

A measured amount, optionally with coded units. extra="forbid".

FieldTypeRequiredDefaultNotes
valueDecimalnoNonemax_digits=20, decimal_places=6
unitCodingnoNoneHuman-readable unit
metadictnoNone
codeCodingnoNoneMachine-processable unit

Ratio (same file) wraps two required Quantity values: numerator and denominator.

ContactPoint

A means of contact (phone, email, etc.). All three fields are required.

FieldTypeRequiredNotes
systemContactPointSystemChoicesyesenum below
valuestryesThe contact value
useContactPointUseChoicesyesenum below

ContactPointSystemChoices values

Value
phone
fax
email
pager
url
sms
other

ContactPointUseChoices values

Value
home
work
temp
old
mobile

MonetaryComponent and pricing types

Pricing line components used by billing resources (see Charge Item Definition, Charge Item).

MonetaryComponent:

FieldTypeRequiredDefaultNotes
monetary_component_typeMonetaryComponentTypeyesenum below
codeCodingnoNone
factorDecimalnoNonemax_digits=20, decimal_places=6
amountDecimalnoNonemax_digits=20, decimal_places=6
tax_included_amountDecimalnoNonemax_digits=20, decimal_places=6; only allowed when type is base
global_componentboolnoFalse
conditionslist[EvaluatorConditionSpec]no[]Must be empty for base components

Model validators enforce:

  • tax_included_amount is only allowed when monetary_component_type == base.
  • A base component must have no conditions and must set amount.
  • amount and factor are mutually exclusive (not both).
  • Either amount or factor must be present — unless global_component is set together with a code.

MonetaryComponentType values

Value
base
surcharge
discount
tax
informational

MonetaryComponentsWithoutBase (a RootModel over list[MonetaryComponent]) adds two list-level rules: no duplicate code.code values across the list; and, when a base component declares tax_included_amount, the sum of tax-component amounts (or base.amount * factor) plus tax_included_amount must equal the base amount. MonetaryComponents extends it with one more: at most one base component.

MonetaryComponentDefinition extends MonetaryComponent for definition-time use: it adds a required title: str, disables the duplicate-code / amount-or-factor checks, and forbids a base component entirely.

DiscountConfiguration:

FieldTypeNotes
max_applicableintge=0
applicability_orderDiscountApplicabilityenum below

DiscountApplicability values

Value
total_asc
total_desc

EvaluatorConditionSpec

A single condition evaluated against a registered metric — used inside MonetaryComponent.conditions.

FieldTypeRequiredNotes
metricstryesMust resolve to a registered evaluator in EvaluatorMetricsRegistry, else "Invalid metric"
operationstryesValidated by the resolved evaluator's validate_rule
valuedict | stryesValidated by the resolved evaluator's validate_rule

ValueSet definition types

The shapes used to define a value set (the FHIR compose structure), used by ValueSet.

  • ValueSetname: str (required), status: str | None, compose: ValueSetCompose (required).
  • ValueSetComposeid, include: list[ValueSetInclude] (required), exclude: list[ValueSetInclude] | None, property: list[str] | None.
  • ValueSetIncludeid, system, version, concept: list[ValueSetConcept] | None, filter: list[ValueSetFilter] | None. concept and filter are mutually exclusive.
  • ValueSetConceptid, code, display (all optional).
  • ValueSetFilterid, property, op, value. op must be one of: =, is-a, descendent-of, is-not-a, regex, in, not-in, generalizes, child-of, descendent-leaf, exists.

All ValueSet types use extra="forbid".

MailTypeChoices

Defined in common/mail_type.py.

NameValue
createcreate_password
resetreset_password

Methods & save behaviour

  • BaseModel.delete() soft-deletes: sets deleted=True and saves only that field, never a hard DELETE.
  • BaseFlag.save() validates flag against FlagRegistry, then evicts the single-flag and all-flags cache entries for the entity before persisting, so cached lookups stay consistent.
  • BaseFlag.check_entity_has_flag / get_all_flags populate those caches on read with a 1-day TTL (FLAGS_CACHE_TTL = 60 * 60 * 24).
  • SlugBaseModel exposes slug encode/parse helpers but does not override save(); subclasses decide when to call calculate_slug().
  • EMRResource.de_serialize is where write-time side effects happen — subclasses override perform_extra_deserialization to append to status histories, resolve FKs from external_id, and validate coded fields against bound value sets.
  • cacheable(...) (in base.py) is a decorator that marks a spec cacheable and wires a post_save signal to invalidate the per-instance serializer cache; model_from_cache then serves serialized specs from cache by pk / id / external_id.

API integration notes

  • external_id (UUID) is the identifier used across Care's REST API and FHIR resources; the integer primary key is internal and never exposed. EMRResource.serialize surfaces it as id, and de_serialize refuses to write id / external_id from request bodies.
  • Deletes are soft — a resource removed through the API still exists in the database with deleted=True, hidden by the default manager.
  • history is not returned by standard endpoints; every version (including migration-time changes) is stored flattened with its performer and served through a separate, on-request audit API.
  • meta is the supported way to attach system metadata without a schema migration; specs with __store_metadata__ = True round-trip extra fields through it. created_by / updated_by are platform-maintained audit fields — clients must not set them directly.
  • Periods sent on write (PeriodSpec) must be timezone-aware and ordered (start ≤ end); the read-side Period type does not enforce this.
  • Feature-flag state (BaseFlag subclasses) is read through the cached classmethods, not queried row-by-row.