Source: calendar_integration/models.py —
CalendarGroup,CalendarGroupSlot,CalendarGroupSlotMembership,CalendarEventGroupSelection,CalendarEvent.calendar_group. Service: calendar_integration/services/calendar_group_service.py. Plan: ai-plans/2026-04-20-CALENDAR_GROUP_PLAN.md.
A CalendarGroup is a booking template. It says: "to book this
kind of appointment, the caller must provide one (or more) calendar
from each of these named slots, and they all have to be free at the
same time."
It is the right primitive whenever a booking needs to combine calendars that are picked from a pool at booking time — which is the dominant pattern in clinical scheduling.
Compared to a bundle: bundles fix the membership ("always Dr. Lee + this one room"); groups fix the roles and let you pick from a pool per role ("any cardiologist + any cath lab").
CalendarGroup "Cardiology Outpatient"
│
├── CalendarGroupSlot name="Physicians" order=0 required_count=1
│ └─ CalendarGroupSlotMembership rows (the pool):
│ • Dr. Lee
│ • Dr. Patel
│ • Dr. Okafor
│
├── CalendarGroupSlot name="Rooms" order=1 required_count=1
│ └─ Pool: { Exam Room 3, Exam Room 4, Exam Room 5 }
│
└── CalendarGroupSlot name="Nurses" order=2 required_count=2
└─ Pool: { Nurse Ana, Nurse Ben, Nurse Cho, Nurse Dan }
Booking on this group requires the caller to pick:
…and the system will only allow the booking if every picked calendar is free for the requested time window.
CalendarGroupThe template aggregate. Per-organization unique by name. Has many
CalendarGroupSlots (its slots) and many CalendarEvents (the
bookings made through it).
CalendarGroupSlotA required role inside a group. Carries:
name — human-meaningful ("Physicians", "Rooms", "Nurses").order — display/iteration order. The lowest-order slot is by
convention the primary slot for booking (its first selection
becomes the event's primary calendar).required_count — how many calendars from the pool must be picked at
booking time (default 1; 2 for the "two nurses" example above).calendars — many-to-many to Calendar through
CalendarGroupSlotMembership. This is the pool.CalendarGroupSlotMembershipThrough table linking a Calendar to a CalendarGroupSlot. One row
per (slot, calendar). A given calendar may belong to multiple slots
across the same or different groups.
CalendarEventGroupSelectionA row per (event, slot, calendar) recording which calendars satisfied
which slot for a particular booking. When CalendarEvent.calendar_group
is non-null, the event has one or more selection rows describing the
picks. The unique constraint enforces no duplicate (event, slot,
calendar) tuples.
CalendarEvent.calendar_fk (the event's "primary" calendar) is also
recorded inside the selections — it's the picked calendar from the
lowest-order slot.
CalendarGroupService.create_grouped_eventThe service's create_grouped_event method:
>= required_count
calendars, all from that slot's pool, with no duplicates.(start_time, end_time) via
Calendar.objects.only_calendars_available_in_ranges. The whole
booking is rejected if even one calendar is busy.order slot.CalendarService.create_event on the primary calendar
so existing side-effects (external-provider sync, permissions,
attendee invites) all run unchanged.CalendarEventGroupSelection rows for every (slot,
calendar) pick.BlockedTime on every non-primary selected calendar so
they appear busy. The service skips the BlockedTime for cases
where an external-provider invite will reliably create an equivalent
event natively (same provider on both sides, with an attendee link).CalendarGroupQuerySet.only_groups_bookable_in_ranges(ranges) —
returns groups where every slot has at least required_count
available calendars for every range. Use when listing which groups
a patient can book against.CalendarGroupService.check_group_availability(group_id, ranges) —
for one group, returns per-range, per-slot lists of which pool
calendars are available. Use when rendering "who's free?" UIs.CalendarGroupService.find_bookable_slots(group_id, search_window, duration, slot_step) — walks the search window in fixed steps and
returns timestamps where every slot is satisfiable. Use to drive a
"show me bookable times this week" picker.The driving example. A physician + a room.
"Cardiology Outpatient".Physicians, pool of 4 cardiology personal calendars,
required_count=1.Rooms, pool of 3 exam-room resource calendars,
required_count=1.When a patient books a 2:30 PM Tuesday consult:
BlockedTime.Surgery for a patient typically needs a surgeon, an anaesthesiologist, an OR, and a circulating nurse. The pools are fluid: any qualified surgeon, any of the OR's matching that surgeon's specialty, etc.
"General Surgery".Surgeons (req 1), Anaesthesiologists (req 1), ORs
(req 1), Scrub Nurse (req 1), Circulating Nurse (req 1).find_bookable_slots does. Once a window is
picked, the user (or an algorithm) chooses one calendar from each
slot.A hospitalist on-call shift covers Friday 7 PM – Saturday 7 AM. The shift is satisfied by one hospitalist picked from a roster pool.
"Overnight Hospitalist On-Call".Hospitalist, pool = the hospitalist personal calendars,
required_count=1.The "pick any free one from the pool" semantics make CalendarGroup
useful even for single-resource bookings where the caller wants a
generic "anyone" affordance instead of pre-selecting.
A complex chronic-care visit might require:
That's five slots, with required_count=2 on the medical-assistants
slot — exactly what the model is shaped to do.
For a telehealth visit that needs a third-party interpreter:
Physicians (pool of physicians on the platform),
required_count=1.Interpreters (pool of interpreter virtual calendars filtered
by language), required_count=1.Telehealth Endpoints (pool of virtual room calendars),
required_count=1.The same machinery handles purely-virtual bookings.
update_group reconciles slots and pool memberships against the
incoming spec. Slots missing from the incoming data are deleted, but
the service refuses to drop a slot, or evict a calendar from a
slot's pool, if a future booking selects that calendar in that
slot — to avoid orphaning live appointments.delete_group refuses if there are any past or future events
referencing the group (the FK from CalendarEvent is PROTECT).required_countMost slots are 1-of-N ("one cardiologist"), but a few need N-of-M:
required_count lets the same model express both without a special
case. The pool size must always be at least required_count.
CalendarEvent — recurrence,
attendances, external attendees, and resource allocations all work as
documented in events.md and recurrence.md.only_calendars_available_in_ranges — see
availability.md. Improvements to that method
automatically improve group bookability.