Skip to main content
Version: 3.1

Notes

Notes are threaded discussions attached to a patient. A NoteThread groups messages; a NoteMessage is one message in that thread, carrying a server-maintained edit history.

The Django models (care/emr/models/notes.py) store the data: NoteThread holds the patient and optional encounter link, and NoteMessage stores each body next to an opaque message_history JSONField. The Pydantic resource specs (care/emr/resources/notes/) extend EMRResource and define what the API exposes — the shape of message_history, the read/write schema split, and the edit-history logic that runs on every update.

Source:

Models

ModelPurpose
NoteThreadA discussion thread attached to a patient (optionally scoped to an encounter)
NoteMessageAn individual message posted within a NoteThread

Both extend EMRBaseModel, which supplies external_id, created_date/modified_date, soft-delete via deleted, created_by/updated_by, and the history/meta JSON fields.

NoteThread fields

FieldTypeNotes
titleCharField(255)Nullable and blank-allowed in the column, but the spec requires it on write (title: str, max_length=255)
patientFK → Patienton_delete=CASCADE; deleting the patient deletes the thread. Excluded from spec serialization (__exclude__) and set from the URL/viewset context, never the request body
encounterFK → EncounterNullable, blank-allowed, on_delete=CASCADE. Scopes the thread to one encounter. Excluded from spec serialization; on create it is resolved from a UUID in the request body (see specs below)

A thread with no encounter is patient-level; a thread with an encounter is pinned to that visit. A TODO in the source flags organization-based access restriction as planned but not yet implemented.

NoteMessage

One message inside a thread.

FieldTypeNotes
threadFK → NoteThreadon_delete=CASCADE; deleting the thread deletes its messages. Excluded from spec serialization (__exclude__) and set from the URL/viewset context
messageTextFieldFree-text body. Required on write (message: str)
message_historyJSONFieldDefaults to {}. Opaque at the model layer; the spec gives it the shape below and rewrites it server-side on each edit. Read-only on the API

message_history shape

The update spec writes a fixed structure, not a free-form blob. Each edit appends the previous message body to a history list:

{
"history": [
{
"message": "string", // the prior message body (pre-edit)
"created_by": {
"username": "string",
"external_id": "uuid" // editor of the prior version
},
"edited_at": "datetime str", // timezone.now() at the time of this edit
"created_at": "datetime str" // the prior version's modified_date
}
// ... one entry per edit, oldest first
]
}

Clients never write message_history. It is read-only on the API and rebuilt server-side on each edit (see NoteMessageUpdateSpec below).

Resource specs (API schema)

Every spec extends EMRResource (resources/base.py), which provides serialize (DB object → pydantic, via perform_extra_serialization) and de_serialize (pydantic → DB object, via perform_extra_deserialization). Fields named in __exclude__ are skipped in both directions and handled by hand.

Thread specs

SpecRoleFields exposedNotes
NoteThreadSpecshared baseid (UUID, read), title (str, required, ≤255)__model__ = NoteThread; __exclude__ = ["patient", "encounter"]
NoteThreadCreateSpecwrite · createbase + encounter (UUID, optional)Validates encounter exists (raises Encounter not found), then resolves the UUID to an Encounter FK in perform_extra_deserialization
NoteThreadUpdateSpecwrite · updatebase (title)No extra behaviour; encounter and patient cannot be reassigned through update
NoteThreadReadSpecread · detail/listbase + created_by, updated_by (dict, nullable), created_date, modified_date (datetime)perform_extra_serialization sets id = external_id and serializes audit users via serialize_audit_users

Validation and server behaviour:

  • title is required on write at the spec layer (Field(..., max_length=255)), even though the column is nullable.
  • NoteThreadCreateSpec.encounter runs validate_encounter_exists: a non-null value must match an existing Encounter.external_id, or it raises ValueError("Encounter not found").
  • On create, perform_extra_deserialization looks up the Encounter by external_id and assigns the FK. patient belongs to no thread spec — it comes from the request context.

Message specs

SpecRoleFields exposedNotes
NoteMessageSpecshared baseid (UUID, read), message (str, required)__model__ = NoteMessage; __exclude__ = ["thread"]
NoteMessageCreateSpecwrite · createbase (message)No extra behaviour
NoteMessageUpdateSpecwrite · updatebase (message)perform_extra_deserialization appends the pre-edit message to message_history["history"] (see below)
NoteMessageReadSpecread · detail/listbase + message_history (dict), created_by, updated_by (dict, nullable), created_date, modified_date (datetime)perform_extra_serialization sets id = external_id and serializes audit users

Validation and server behaviour:

  • message is required on create and update.
  • thread is never in the request body (__exclude__); it comes from the URL/viewset context.
  • On update, NoteMessageUpdateSpec.perform_extra_deserialization fetches the existing NoteMessage by external_id, initializes message_history["history"] = [] if empty, then appends an entry: the prior message, the prior author (username + external_id), edited_at (timezone.now()), and created_at (the prior modified_date). Clients cannot write message_history directly.

Methods & save behaviour

  • NoteThread and NoteMessage inherit save and audit behaviour from EMRBaseModel: external_id, created_by/updated_by, created_date/modified_date, and soft delete via deleted.
  • Read specs populate audit users through EMRResource.serialize_audit_users, which resolves created_by/updated_by to cached UserSpec dicts.
  • message_history changes only inside NoteMessageUpdateSpec.perform_extra_deserialization — never from client input.
  • Cascade deletes flow downward: deleting a patient removes its threads, and deleting a thread removes its messages.

API integration notes

Payload field names follow the spec classes above, not the Django model names.

  • patient and thread never appear in a request body; they come from the URL/viewset context.
  • Send encounter on create as the encounter's external_id (UUID); it is validated to exist. It cannot be reassigned on update.
  • title and message are required on write, even though the underlying columns are nullable.
  • message_history is read-only. Each edit appends the previous body and authorship to history server-side.
  • created_by, updated_by, created_date, and modified_date are read-only audit fields from EMRBaseModel; soft deletes use deleted.