Skip to main content
Version: 3.0

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
codestryes—The 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_typeMonetaryComponentTypeyes—enum 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.

  • ValueSet — name: str (required), status: str | None, compose: ValueSetCompose (required).
  • ValueSetCompose — id, include: list[ValueSetInclude] (required), exclude: list[ValueSetInclude] | None, property: list[str] | None.
  • ValueSetInclude — id, system, version, concept: list[ValueSetConcept] | None, filter: list[ValueSetFilter] | None. concept and filter are mutually exclusive.
  • ValueSetConcept — id, code, display (all optional).
  • ValueSetFilter — id, 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.