Clinical care
Tagging
A TagConfig defines one tag: its label, category, scope, and place in a tag tree. Tags are hierarchical labels you attach to resources — patients, encounters, charge items — to classify and filter them. You manage the definitions through…
3.1 · versioned documentation · 7 min read
A TagConfig defines one tag: its label, category, scope, and place in a tag tree. Tags are hierarchical labels you attach to resources — patients, encounters, charge items — to classify and filter them. You manage the definitions through TagConfig; the applied tags live as integer ID arrays on the tagged resources.
Two layers back the tag. The Django model (care/emr/models/tag_config.py) is storage — it holds opaque JSONFields (metadata, cached_parent_json) whose structure it never describes. The Pydantic resource specs (care/emr/resources/tag/config_spec.py) are the API and implementation layer: they pin down the enums, give those JSON fields a real shape, run field validation, and split read from write.
Source:
- Model:
care/emr/models/tag_config.py - Resource spec:
care/emr/resources/tag/config_spec.py - Cache invalidation:
care/emr/resources/tag/cache_invalidation.py
Models
| Model | Purpose |
|---|---|
TagConfig |
Definition of a single tag — its display, category, scope, status, metadata, and position in the tag tree |
TagConfig extends EMRBaseModel, the shared MANSOQ EMR base providing external_id, the audit fields created_by/updated_by and created_date/modified_date, soft-delete via deleted, and history/meta JSON. Applied tag IDs live on the tagged resource itself — for example Patient.instance_tags / Patient.facility_tags and Encounter.tags.
TagConfig fields
Scope
A tag is scoped to at most one owner — an instance-wide organization, a facility-scoped organization, or a facility.
| Field | Type | Notes |
|---|---|---|
facility |
FK → Facility |
PROTECT, nullable, default=None. Facility that owns the tag |
facility_organization |
FK → FacilityOrganization |
CASCADE, nullable. Owning facility organization. Not allowed on instance-level (no-facility) tags |
organization |
FK → Organization |
CASCADE, nullable. Owning instance-wide organization |
resource |
CharField(255) |
The resource type the tag applies to. Constrained by TagResource in the write spec (see enum) |
Display & classification
| Field | Type | Notes |
|---|---|---|
status |
CharField(255) |
Lifecycle status. Constrained by TagStatus in the spec — active / archived (see enum) |
display |
CharField(255) |
Human-readable label. Required in the spec |
description |
TextField |
Nullable, blank-able. Required key in the base spec but accepts null |
category |
CharField(255) |
Grouping category. Constrained by TagCategoryChoices in the spec (see enum) |
priority |
IntegerField |
default=100. Ordering weight |
metadata |
JSONField |
default=None, nullable. Shape defined by TagConfigMetadata: { color: str?, icon: str? } (see nested spec) |
Tree structure & caches
TagConfig is self-referential — tags form a tree, and denormalized caches are rebuilt on insert to avoid recursive joins (the same pattern as Organization). These fields are platform-maintained and never writable through the create/update specs.
| Field | Type | Notes |
|---|---|---|
parent |
FK → self |
CASCADE, nullable. related_name="children". Null parent means a root tag |
root_tag_config |
FK → self |
CASCADE, nullable. related_name="root". Top of the tree, derived on save |
has_children |
BooleanField |
default=False. Flipped to True on the parent when a child is created |
level_cache |
IntegerField |
default=0. Depth in the tree (parent.level_cache + 1) |
parent_cache |
ArrayField[int] |
default=list. Full ancestor id chain (parent.parent_cache + [parent.id]) |
cached_parent_json |
JSONField |
default=dict. Materialized parent record with a cache_expiry; rebuilt after cache_expiry_days (15). Shape: { id, display, description, category, parent (nested same shape), level_cache, cache_expiry } |
Enums
TagCategoryChoices values
category binds to this enum in every spec (str values).
| Value |
|---|
diet |
drug |
lab |
admin |
contact |
clinical |
behavioral |
research |
advance_directive |
safety |
TagResource values
resource binds to this enum in TagConfigWriteSpec (set only on create; the read specs surface it as a plain str).
| Value |
|---|
encounter |
activity_definition |
service_request |
charge_item |
charge_item_definition |
patient |
token_booking |
medication_request_prescription |
supply_request_order |
supply_delivery_order |
account |
TagStatus values
status binds to this enum in every spec (str values).
| Value | Meaning |
|---|---|
active |
Tag is available for use |
archived |
Tag retired; retained for historical references |
Nested specs (shape of JSON fields)
TagConfigMetadata (shape of metadata)
Gives the opaque metadata JSONField a real structure. A plain Pydantic BaseModel, not an EMRResource.
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
color |
str | None |
no | None |
Display color hint |
icon |
str | None |
no | None |
Display icon hint |
Resource specs (API schema)
Every spec extends EMRResource (care/emr/resources/base.py), which supplies serialize (DB object → spec) and de_serialize (spec → DB object) plus the perform_extra_serialization / perform_extra_deserialization hooks. __exclude__ on the base spec keeps the FK fields (facility, facility_organization, organization, parent) out of the generic field copy so each spec can resolve them explicitly.
| Spec class | Role | Notes |
|---|---|---|
TagConfigBaseSpec |
shared | __model__ = TagConfig; __exclude__ = [facility, facility_organization, organization, parent]. Fields: id (UUID4?), display, category (TagCategoryChoices), description (str | None), priority (int = 100), status (TagStatus), metadata (TagConfigMetadata?) |
TagConfigWriteSpec |
write · create | Base + facility?, facility_organization?, organization?, parent? (all UUID4), resource (TagResource, required). Runs two model_validator(after) checks and resolves FKs in perform_extra_deserialization |
TagConfigUpdateSpec |
write · update | Base + facility_organization?, organization? (UUID4). Cannot change facility, parent, or resource. Resolves organization/facility_organization in perform_extra_deserialization (facility organization scoped to obj.facility) |
TagConfigReadSpec |
read · list | @cacheable. Base + level_cache (int = 0), system_generated (bool), has_children (bool), parent (dict | None), resource (str), facility (dict | None). Inlines parent via get_parent_json() and a FacilityBareMinimumSpec for facility |
TagConfigRetrieveSpec |
read · detail | Extends read spec + created_by (dict), updated_by (dict), facility_organization (dict | None), organization (dict | None). Adds audit users and full org/facility-org serialization |
Write-spec validation (TagConfigWriteSpec)
Two @model_validator(mode="after") checks run before deserialization:
validate_exists:- If
facilityis set, it must exist; iffacility_organizationis also set it must belong to that facility, elseValidationError("Facility Organization not found"). - If
organizationis set, it must exist, elseValidationError("Organization not found"). - If
parentis set, aTagConfigwith thatexternal_idand the sameresourcemust exist (and, whenfacilityis set, belong to that facility), elseValueError("Parent tag config not found").
- If
validate_organizations: afacility_organizationwithout afacilityis rejected —ValueError("Facility Organization not allowed in instance level tag configs").
Server-maintained behaviour
TagConfigWriteSpec.perform_extra_deserializationresolvesparent,organization, andfacility(plusfacility_organizationscoped to the resolved facility) from theirexternal_ids onto the model instance;parentis explicitly set toNonewhen absent.TagConfigReadSpec.perform_extra_serializationsetsid = external_id, inlines the parent record viaobj.get_parent_json()(only when non-empty), and serializesfacilitywithFacilityBareMinimumSpec({ id, name }).TagConfigRetrieveSpec.perform_extra_serializationcalls the read-spec hook, thenserialize_audit_users(populatescreated_by/updated_byfrom cache) and serializesfacility_organization/organizationwith their full read specs.- Tree caches (
level_cache,parent_cache,root_tag_config,has_children) are populated by the model'sset_tag_config_cache()on insert — never accepted from clients. system_generatedis declared onTagConfigReadSpecbut is not a field on the currentTagConfigmodel (nor onEMRBaseModel). The genericserializeonly copies model fields, so it is not populated from storage unless something else provides it. Treat it as a read-only flag and verify against the live model before relying on it.
Cache invalidation
A post_save signal connects invalidate_tag_config_cache to TagConfig. Every save does three things.
- Invalidates the tag's own
TagConfigReadSpeccache and resetscached_parent_json = {}. - Invalidates all descendants (rows whose
parent_cacheoverlaps the tag id) — they embed parent data viaget_parent_json(). - Invalidates the parent's cache (its
has_childrenmay have changed).
Methods & save behaviour
set_tag_config_cache()
When a parent is set, recomputes parent_cache (parent.parent_cache + [parent.id]) and level_cache (parent.level_cache + 1), then derives root_tag_config from the parent — the parent itself if the parent is a root, otherwise the parent's root_tag_config. If the parent had no children, it flips parent.has_children = True, persisting only that field. Finishes with super().save().
get_parent_json()
Returns cached_parent_json when it is present and its cache_expiry is still in the future. Otherwise it recursively rebuilds the nested parent record (id = parent external_id, display, description, category, nested parent, level_cache), stamps a fresh expiry (cache_expiry_days = 15), persists cached_parent_json, and returns it. Returns {} for root tags.
save() side effects
On insert (no id yet), the base save() runs first, then set_tag_config_cache() populates the tree caches in a second write pass. On update, save() persists normally without recomputing caches. Both paths trigger the post_save cache-invalidation signal described above.
API integration notes
The REST API exposes only the definitions. Applied tags are stored as integer ID arrays on each tagged resource (e.g. Patient.instance_tags/facility_tags, Encounter.tags), not as join rows.
Each operation maps to one spec:
- Create —
TagConfigWriteSpec. Setsresource, the scope FKs, andparent.resourcemust match aTagResourcevalue. - Update —
TagConfigUpdateSpec. Reassigns org/facility-org only, plus the shared display/category/status/metadata fields;facility,parent, andresourceare fixed. - List —
TagConfigReadSpec. Detail —TagConfigRetrieveSpec.
metadata carries display hints (color, icon) without a schema migration, shaped by TagConfigMetadata.
Related
- Reference: Organization — tags reuse the same tree-cache pattern and can be org-scoped
- Reference: Patient — carries
instance_tags/facility_tags - Reference: Encounter — carries
tags - Reference: Facility — facility-scoped tags; read spec inlines a
FacilityBareMinimumSpec - Reference: Base models & conventions
- Source: tag_config.py on GitHub
- Source: config_spec.py on GitHub
Source evidence
References are pinned to the private MANSOQ source repository. Repository access is required to open these links.