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 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 Care 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