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:
- Model:
care/emr/models/notes.py - Specs:
resources/notes/thread_spec.py,resources/notes/notes_spec.py - Base:
resources/base.py
Models
| Model | Purpose |
|---|---|
NoteThread | A discussion thread attached to a patient (optionally scoped to an encounter) |
NoteMessage | An 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
| Field | Type | Notes |
|---|---|---|
title | CharField(255) | Nullable and blank-allowed in the column, but the spec requires it on write (title: str, max_length=255) |
patient | FK → Patient | on_delete=CASCADE; deleting the patient deletes the thread. Excluded from spec serialization (__exclude__) and set from the URL/viewset context, never the request body |
encounter | FK → Encounter | Nullable, 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.
Related models
NoteMessage
One message inside a thread.
| Field | Type | Notes |
|---|---|---|
thread | FK → NoteThread | on_delete=CASCADE; deleting the thread deletes its messages. Excluded from spec serialization (__exclude__) and set from the URL/viewset context |
message | TextField | Free-text body. Required on write (message: str) |
message_history | JSONField | Defaults 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
| Spec | Role | Fields exposed | Notes |
|---|---|---|---|
NoteThreadSpec | shared base | id (UUID, read), title (str, required, ≤255) | __model__ = NoteThread; __exclude__ = ["patient", "encounter"] |
NoteThreadCreateSpec | write · create | base + encounter (UUID, optional) | Validates encounter exists (raises Encounter not found), then resolves the UUID to an Encounter FK in perform_extra_deserialization |
NoteThreadUpdateSpec | write · update | base (title) | No extra behaviour; encounter and patient cannot be reassigned through update |
NoteThreadReadSpec | read · detail/list | base + 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:
titleis required on write at the spec layer (Field(..., max_length=255)), even though the column is nullable.NoteThreadCreateSpec.encounterrunsvalidate_encounter_exists: a non-null value must match an existingEncounter.external_id, or it raisesValueError("Encounter not found").- On create,
perform_extra_deserializationlooks up theEncounterbyexternal_idand assigns the FK.patientbelongs to no thread spec — it comes from the request context.
Message specs
| Spec | Role | Fields exposed | Notes |
|---|---|---|---|
NoteMessageSpec | shared base | id (UUID, read), message (str, required) | __model__ = NoteMessage; __exclude__ = ["thread"] |
NoteMessageCreateSpec | write · create | base (message) | No extra behaviour |
NoteMessageUpdateSpec | write · update | base (message) | perform_extra_deserialization appends the pre-edit message to message_history["history"] (see below) |
NoteMessageReadSpec | read · detail/list | base + 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:
messageis required on create and update.threadis never in the request body (__exclude__); it comes from the URL/viewset context.- On update,
NoteMessageUpdateSpec.perform_extra_deserializationfetches the existingNoteMessagebyexternal_id, initializesmessage_history["history"] = []if empty, then appends an entry: the priormessage, the prior author (username+external_id),edited_at(timezone.now()), andcreated_at(the priormodified_date). Clients cannot writemessage_historydirectly.
Methods & save behaviour
NoteThreadandNoteMessageinherit save and audit behaviour fromEMRBaseModel:external_id,created_by/updated_by,created_date/modified_date, and soft delete viadeleted.- Read specs populate audit users through
EMRResource.serialize_audit_users, which resolvescreated_by/updated_byto cachedUserSpecdicts. message_historychanges only insideNoteMessageUpdateSpec.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.
patientandthreadnever appear in a request body; they come from the URL/viewset context.- Send
encounteron create as the encounter'sexternal_id(UUID); it is validated to exist. It cannot be reassigned on update. titleandmessageare required on write, even though the underlying columns are nullable.message_historyis read-only. Each edit appends the previous body and authorship tohistoryserver-side.created_by,updated_by,created_date, andmodified_dateare read-only audit fields fromEMRBaseModel; soft deletes usedeleted.
Related
- Source: notes.py on GitHub
- Specs: thread_spec.py, notes_spec.py
- Reference: Patient
- Reference: Encounter
- Reference: Base model