Clinical care
Base models & conventions
Every MANSOQ 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: -…
3.1 · versioned documentation · 13 min read
Every MANSOQ 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 MANSOQ 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 MANSOQ'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
Source evidence
References are pinned to the private MANSOQ source repository. Repository access is required to open these links.