Schedule & Availability
A schedule is a repeatable block of time during which a resource — a practitioner, location, or healthcare service — can be booked. You attach availabilities to it (these decide how the block splits into slots) and exceptions that block out specific dates.
Source:
- Model:
care/emr/models/scheduling/schedule.py - Resource specs:
schedule/spec.py,availability_exception/spec.py,resource/spec.py
The Django model (care/emr/models/scheduling/schedule.py) is storage only, down to the opaque Availability.availability JSONField. The contract lives in the Pydantic resource specs (care/emr/resources/scheduling/...): the enums, the real shape of that JSON field, the validation rules, and the separate read/write schemas. Everything below ## Models describes the specs.
Models
| Model | Purpose |
|---|---|
SchedulableResource | A bookable resource (a user, location, or healthcare service) within a facility |
Schedule | A named, date-bounded block of availability attached to a resource |
Availability | Slot configuration for a schedule (slot size, tokens, weekly recurrence) |
AvailabilityException | A date/time range that blocks a resource (leave, holidays) |
All four extend EMRBaseModel, which supplies external_id, created_date/modified_date, soft-delete via deleted, created_by/updated_by, and history/meta JSON.
Schedule fields
| Field | Type | Req. | Notes |
|---|---|---|---|
resource | FK → SchedulableResource (CASCADE) | server | Resolved server-side from resource_type + resource_id (get-or-create). Reject this in the body |
name | CharField(255) | yes | Display name (e.g. "Morning OPD") |
valid_from | DateTimeField | yes | Start of the effective window. Must be ≥ now and ≤ valid_to |
valid_to | DateTimeField | yes | End of the effective window. Must be ≥ now |
revisit_allowed_days | IntegerField | no | Nullable. Window within which a follow-up uses the revisit charge. Settable only via the set_charge_item_definition action |
is_public | BooleanField | yes | Exposes the schedule for public booking. Model default False |
Billing links
Both charge links are set only through the set_charge_item_definition action (see Resource specs) — never the create/update body.
| Field | Type | Notes |
|---|---|---|
charge_item_definition | FK → ChargeItemDefinition (PROTECT) | Nullable; charge applied to bookings against this schedule. related_name="schedule_charge_item_definition" |
revisit_charge_item_definition | FK → ChargeItemDefinition (PROTECT) | Nullable; charge applied when a booking falls within revisit_allowed_days. related_name="schedule_revisit_charge_item_definition" |
Enums
SchedulableResourceTypeOptions
Discriminates the kind of entity a schedule or exception attaches to. On write, send it as resource_type; the matching resource_id is that entity's external_id.
| Value | Resolves to |
|---|---|
practitioner | A User who is a member of the facility (serialized via UserSpec) |
location | A FacilityLocation in the facility (serialized via FacilityLocationListSpec) |
healthcare_service | A HealthcareService in the facility (serialized via HealthcareServiceReadSpec) |
The SchedulableResource.resource_type model column defaults to the string "practitioner".
SlotTypeOptions
The type of an individual Availability block (Availability.slot_type on the model, slot_type in the spec).
| Value | Meaning |
|---|---|
open | Open block; slot_size_in_minutes / tokens_per_slot are cleared to null on save |
appointment | Time-precise appointment block; requires both slot_size_in_minutes and tokens_per_slot |
closed | Closed block; slot_size_in_minutes / tokens_per_slot are cleared to null on save |
Related models
SchedulableResource
The bookable entity within a facility, and the join point that booking hangs off. Exactly one of user, location, or healthcare_service points at the underlying resource; resource_type says which. You never create one directly: Schedule and AvailabilityException writes call get_or_create_resource(resource_type, resource_id, facility) server-side, which checks the target belongs to the facility first.
facility → FK Facility (CASCADE)
resource_type → CharField(255), default "practitioner"
user → FK User (CASCADE, nullable)
location → FK FacilityLocation (CASCADE, nullable)
healthcare_service → FK HealthcareService (CASCADE, nullable)
Three UniqueConstraints keep one SchedulableResource per facility, resource_type, and target:
| Constraint | Fields |
|---|---|
unique_facility_resource_user | facility, resource_type, user |
unique_facility_resource_location | facility, resource_type, location |
unique_facility_resource_healthcare_service | facility, resource_type, healthcare_service |
On read, serialize_resource(obj) (resource/spec.py) dispatches on resource_type to UserSpec / HealthcareServiceReadSpec / FacilityLocationListSpec.
Availability
Decides how a schedule's time divides into bookable slots. One schedule can carry several availabilities — say, a morning block and an afternoon block.
| Field | Type | Req. | Spec detail |
|---|---|---|---|
schedule | FK Schedule (CASCADE) | server | Excluded from the availability spec; set from the URL / parent schedule |
name | CharField(255) | yes | Block name |
slot_type | CharField | yes | One of SlotTypeOptions |
slot_size_in_minutes | IntegerField (nullable) | conditional | int | None, ge=1. Required when slot_type == appointment; forced to null otherwise |
tokens_per_slot | IntegerField (nullable) | conditional | int | None, ge=1. Capacity per slot. Required when slot_type == appointment; forced to null otherwise |
create_tokens | BooleanField | no | Default False. When True, each booking gets a token (queue/token workflows) |
reason | TextField (nullable) | no | Spec default "" |
availability | JSONField, default dict | yes | Despite the dict default, the spec models it as a list[AvailabilityDateTimeSpec] (weekly recurrence). See below |
availability JSON shape — AvailabilityDateTimeSpec
Each entry is one weekly recurrence window. The field stores a list of these objects, not a single dict.
AvailabilityDateTimeSpec {
day_of_week: int # 0–6, validated le=6 (Monday=0 … Sunday=6, ISO-style)
start_time: time # HH:MM:SS
end_time: time # HH:MM:SS
}
The list is validated by AvailabilityForScheduleSpec.validate_availability + validate_for_slot_type:
- Every entry needs
start_time < end_time(strict). - No two entries on the same
day_of_weekmay overlap (has_overlapping_availability: ranges overlap whena.start ≤ b.end and b.start ≤ a.end). On create, overlap is checked across all availabilities of the same schedule. - For
slot_type == appointment:slot_size_in_minutesandtokens_per_slotare mandatory, each window's duration must be an exact multiple ofslot_size_in_minutes, and the resulting slot count must not exceedsettings.MAX_SLOTS_PER_AVAILABILITY(default30).
AvailabilityException
Blocks a resource for a date/time range regardless of its schedules — leave, holidays, one-off closures.
| Field | Type | Req. | Spec detail |
|---|---|---|---|
resource | FK SchedulableResource (CASCADE) | server | Resolved from resource_type + resource_id (get-or-create); excluded from the spec body |
name | CharField(255) | yes | Exception name |
reason | TextField (nullable) | no | str | None |
valid_from | DateField | yes | date. Must be ≥ today and ≤ valid_to |
valid_to | DateField | yes | date. Must be ≥ today |
start_time | TimeField | yes | time |
end_time | TimeField | yes | time |
Resource specs (API schema)
Every spec extends EMRResource (base.py), which provides serialize() (DB → pydantic), de_serialize() (pydantic → DB), and the perform_extra_serialization / perform_extra_deserialization hooks. __exclude__ lists fields skipped during (de)serialization.
| Spec | Role | Exposes / behaviour |
|---|---|---|
ScheduleBaseSpec | shared | id, is_public. __exclude__ = ["resource", "facility"] |
ScheduleCreateSpec | write · create | facility, name, valid_from, valid_to, availabilities: list[AvailabilityForScheduleSpec], resource_type, resource_id, is_public. Validates dates ≥ now, valid_from ≤ valid_to, and cross-availability non-overlap. perform_extra_deserialization stashes facility, _resource_id, _resource_type, and availabilities on the instance for the viewset |
ScheduleUpdateSpec | write · update | name, valid_from, valid_to, is_public. perform_extra_deserialization blocks narrowing validity that would drop allocated slots — it compares Sum(TokenSlot.allocated) across the old vs new range and raises if they differ |
ScheduleReadSpec | read · list + detail | All of the above plus availabilities (re-serialized from Availability rows), resource_type, charge_item_definition / revisit_charge_item_definition (full ChargeItemDefinitionReadSpec or null), revisit_allowed_days, created_by, updated_by |
AvailabilityBaseSpec | shared | id. __exclude__ = ["schedule"] |
AvailabilityForScheduleSpec | write/read (nested in schedule) | name, slot_type, slot_size_in_minutes, tokens_per_slot, create_tokens, reason, availability: list[AvailabilityDateTimeSpec]. Carries the availability + slot-type validators |
AvailabilityCreateSpec | write · create (standalone) | Adds schedule: UUID4; on create, re-checks overlap against all existing availabilities of that schedule |
AvailabilityDateTimeSpec | nested | day_of_week (le=6), start_time, end_time — the shape of the availability JSON field |
AvailabilityExceptionBaseSpec | shared | id, reason, valid_from, valid_to, start_time, end_time. __exclude__ = ["resource", "facility"] |
AvailabilityExceptionWriteSpec | write · create/upsert | Adds facility, resource_type, resource_id. Validates dates ≥ today and valid_from ≤ valid_to; stashes _resource_type/_resource_id |
AvailabilityExceptionReadSpec | read · list + detail | Base fields; perform_extra_serialization maps id → external_id |
ChargeItemDefinitionSetSpec | write (action body) | charge_item_definition: str | None, re_visit_allowed_days: int, re_visit_charge_item_definition: str | None — body for POST .../set_charge_item_definition (charge fields resolved by slug within the facility) |
Server-maintained behaviour (viewset hooks)
- Resource resolution. On schedule or exception create,
get_or_create_resource(resource_type, resource_id, facility)checks the target is in the facility (a practitioner must be aFacilityOrganizationUser; a location or healthcare service must belong to the facility), then gets-or-creates theSchedulableResource.facilityis injected from the URL viaclean_create_data. - Nested availability create.
ScheduleViewSet.perform_create(atomic) setsresource, saves the schedule, then de-serializes and saves eachavailabilitylinked to it. - Charge items.
revisit_allowed_days,charge_item_definition, andrevisit_charge_item_definitionmove only through theset_charge_item_definitiondetail action, never the create/update body. - Locking + slot guards. Update and destroy take a
Lock("booking:resource:<id>"). Deleting a schedule or availability is rejected when future allocatedTokenSlots exist; otherwise availabilities and slots are soft-deleted (deleted=True). - Exception slot clearing. Creating an
AvailabilityExceptionsoft-deletes overlappingTokenSlots, but rejects the request if any are already allocated ("There are bookings during this exception"). - List requires resource. The
ScheduleandAvailabilityExceptionlist endpoints requireresource_typeandresource_idquery params and scope results to thatSchedulableResource.
Integration notes
- Slots are computed from
Availabilityat read time, not stored as rows on the schedule. - When reading a
SchedulableResource, dispatch onresource_typeto pick the populated relation (user,location, orhealthcare_service) — only one is set, and you can't assume which. - A
ChargeItemDefinitionreferenced by a schedule can't be deleted while the schedule exists (PROTECT).
Related
- Reference: Booking
- Reference: Token
- Reference: Charge item definition
- Reference: Healthcare service
- Reference: Location
- Reference: Facility
- Reference: User
- Source (model): schedule.py
- Source (specs): schedule/spec.py, availability_exception/spec.py, resource/spec.py