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/modelsandcare/utils/models. Several of their columns are opaqueJSONFields 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 incare/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
| Model | Layer | Purpose |
|---|---|---|
BaseModel | storage | Lowest-level abstract base — opaque external_id, timestamps, and soft-delete |
BaseManager | storage | Default manager that hides soft-deleted rows |
BaseFlag | storage | Abstract base for feature-flag tables with cache-backed lookups |
EMRBaseModel | storage | Standard base for EMR resources — adds audit created_by/updated_by, history, and meta |
SlugBaseModel | storage | EMR base that adds facility-scoped or instance-scoped slug helpers |
EMRResource | API | Pydantic 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.
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
external_id | UUIDField | yes | uuid4 | unique, db_index. The opaque public identifier used in URLs and API payloads — never expose the integer pk |
created_date | DateTimeField | no | auto_now_add | nullable, db_index. Set once on insert |
modified_date | DateTimeField | no | auto_now | nullable, db_index. Bumped on every save |
deleted | BooleanField | yes | False | db_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:
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
history | JSONField | no | dict | Flattened 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 |
meta | JSONField | no | dict | Open metadata bag. EMRResource writes spec fields here when __store_metadata__ = True (see below) |
created_by | FK → users.User | no | None | on_delete=SET_NULL, nullable. related_name="%(app_label)s_%(class)s_created_by" |
updated_by | FK → users.User | no | None | on_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.
| Method | Behaviour |
|---|---|
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.
Related models
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.
| Field | Type | Notes |
|---|---|---|
flag | CharField(max_length=1024) | Flag name, validated against FlagRegistry for the subclass's flag_type |
Subclasses configure these class attributes:
| Attribute | Purpose |
|---|---|
cache_key_template | Cache key for a single (entity_id, flag_name) pair |
all_flags_cache_key_template | Cache key for all flags of one entity |
flag_type | Flag category validated against FlagRegistry |
entity_field_name | Name of the FK field pointing at the owning entity |
Helper properties and classmethods:
| Member | Behaviour |
|---|---|
entity | Resolves the related entity via entity_field_name |
entity_id | Resolves <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 attribute | Default | Role |
|---|---|---|
__model__ | None | The Django model the spec serializes to/from |
__exclude__ | [] | Field names skipped in both serialize and de_serialize |
__store_metadata__ | False | When True, spec fields that are not DB columns are read from / written to the model's meta JSON bag |
__version__ | 0.1 | Stamped onto every serialized object as version |
meta | {} | Open metadata dict carried on the spec itself |
| Method | Direction | Behaviour |
|---|---|---|
serialize(obj, user=None) | DB → API | Builds 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 → DB | Dumps 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 → API | Hook for resolving derived/nested read fields; base sets id |
perform_extra_deserialization(is_update, obj) | API → DB | Hook 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 → API | Helper 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 class | Kind | Role |
|---|---|---|
<X>SpecBase | shared | Common fields shared by the write/read specs |
<X>CreateSpec | write · create | Request body for POST; de_serialize builds a new model instance |
<X>UpdateSpec | write · update | Request body for PUT/PATCH; de_serialize updates an existing instance (is_update=True) |
<X>ListSpec | read · list | Lightweight response for list endpoints |
<X>ReadSpec / <X>RetrieveSpec | read · detail | Full 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/.
| Field | Type | Required | Default | Validation |
|---|---|---|---|---|
start | datetime | no | None | must be timezone-aware |
end | datetime | no | None | must 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".
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
system | str | no | None | URI of the code system |
version | str | no | None | Code-system version |
code | str | yes | — | The code value |
display | str | no | None | Human-readable label |
CodeableConcept
A concept expressed as one or more codings plus free text. extra="forbid".
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
id | str | no | None | |
coding | list[Coding] | no | None | One or more Coding entries |
text | str | None | yes (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".
| Field | Type | Required | Default |
|---|---|---|---|
id | str | no | None |
start | datetime | no | None |
end | datetime | no | None |
Quantity
A measured amount, optionally with coded units. extra="forbid".
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
value | Decimal | no | None | max_digits=20, decimal_places=6 |
unit | Coding | no | None | Human-readable unit |
meta | dict | no | None | |
code | Coding | no | None | Machine-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.
| Field | Type | Required | Notes |
|---|---|---|---|
system | ContactPointSystemChoices | yes | enum below |
value | str | yes | The contact value |
use | ContactPointUseChoices | yes | enum 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:
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
monetary_component_type | MonetaryComponentType | yes | — | enum below |
code | Coding | no | None | |
factor | Decimal | no | None | max_digits=20, decimal_places=6 |
amount | Decimal | no | None | max_digits=20, decimal_places=6 |
tax_included_amount | Decimal | no | None | max_digits=20, decimal_places=6; only allowed when type is base |
global_component | bool | no | False | |
conditions | list[EvaluatorConditionSpec] | no | [] | Must be empty for base components |
Model validators enforce:
tax_included_amountis only allowed whenmonetary_component_type == base.- A
basecomponent must have noconditionsand must setamount. amountandfactorare mutually exclusive (not both).- Either
amountorfactormust be present — unlessglobal_componentis set together with acode.
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:
| Field | Type | Notes |
|---|---|---|
max_applicable | int | ge=0 |
applicability_order | DiscountApplicability | enum below |
DiscountApplicability values
| Value |
|---|
total_asc |
total_desc |
EvaluatorConditionSpec
A single condition evaluated against a registered metric — used inside MonetaryComponent.conditions.
| Field | Type | Required | Notes |
|---|---|---|---|
metric | str | yes | Must resolve to a registered evaluator in EvaluatorMetricsRegistry, else "Invalid metric" |
operation | str | yes | Validated by the resolved evaluator's validate_rule |
value | dict | str | yes | Validated 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.conceptandfilterare mutually exclusive.ValueSetConcept—id,code,display(all optional).ValueSetFilter—id,property,op,value.opmust 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.
| Name | Value |
|---|---|
create | create_password |
reset | reset_password |
Methods & save behaviour
BaseModel.delete()soft-deletes: setsdeleted=Trueand saves only that field, never a hardDELETE.BaseFlag.save()validatesflagagainstFlagRegistry, 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_flagspopulate those caches on read with a 1-day TTL (FLAGS_CACHE_TTL = 60 * 60 * 24).SlugBaseModelexposes slug encode/parse helpers but does not overridesave(); subclasses decide when to callcalculate_slug().EMRResource.de_serializeis where write-time side effects happen — subclasses overrideperform_extra_deserializationto append to status histories, resolve FKs fromexternal_id, and validate coded fields against bound value sets.cacheable(...)(inbase.py) is a decorator that marks a spec cacheable and wires apost_savesignal to invalidate the per-instance serializer cache;model_from_cachethen serves serialized specs from cache bypk/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.serializesurfaces it asid, andde_serializerefuses to writeid/external_idfrom request bodies.- Deletes are soft — a resource removed through the API still exists in the database with
deleted=True, hidden by the default manager. historyis 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.metais the supported way to attach system metadata without a schema migration; specs with__store_metadata__ = Trueround-trip extra fields through it.created_by/updated_byare 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-sidePeriodtype does not enforce this. - Feature-flag state (
BaseFlagsubclasses) is read through the cached classmethods, not queried row-by-row.
Related
- Reference: Patient — a representative
EMRBaseModel+EMRResourcesubclass - Reference: ValueSet — uses
SlugBaseModeland the ValueSet compose types - Reference: Charge Item Definition — uses
MonetaryComponentpricing types - Source:
base.py(emr models) - Source:
base.py(utils models) - Source:
base.py(resources /EMRResource) - Source:
common/shared spec types