References/Notes
More languages
More actions
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: [https://github.com/ohcnetwork/care/blob/develop/care/emr/models/notes.py
care/emr/models/notes.py] - Specs: [https://github.com/ohcnetwork/care/blob/develop/care/emr/resources/notes/thread_spec.py
resources/notes/thread_spec.py], [https://github.com/ohcnetwork/care/blob/develop/care/emr/resources/notes/notes_spec.pyresources/notes/notes_spec.py] - Base: [https://github.com/ohcnetwork/care/blob/develop/care/emr/resources/base.py
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)
{{Field|model=Notes|section=NoteThread fields|name=patient|type=FK → Patient|notes= |
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
Pages that link here:
- Reference: References/Base models & conventions