Source: calendar_integration/models.py —
RecurrenceRule,RecurringMixin,EventRecurrenceException,BlockedTimeRecurrenceException,AvailableTimeRecurrenceException,*BulkModificationmodels. Helpers: calendar_integration/recurrence_utils.py.
Most things people schedule are recurring: clinics run weekly, on-call
shifts rotate, OR blocks repeat, conference rooms host the same meeting
every Monday. Storing one row per occurrence would be wasteful and
fragile. Instead, each CalendarEvent, BlockedTime, and
AvailableTime carries an optional RecurrenceRule — an RFC 5545
RRULE —
and the system expands occurrences on demand inside whatever date
range the caller asks about.
RecurrenceRuleStores the standard RRULE fields: frequency, interval, count,
until, by_weekday, by_month_day, by_month, by_hour, by_minute,
etc. to_rrule_string() and from_rrule_string(...) round-trip the
canonical string form so external systems (Google, Outlook) can read the
same rule.
Healthcare examples:
FREQ=WEEKLY;BYDAY=TU,TH — Dr. Patel's outpatient clinic, every
Tuesday and Thursday.FREQ=DAILY;BYDAY=MO,TU,WE,TH,FR;BYHOUR=8;BYMINUTE=0 — morning
rounds at 8 AM on weekdays.FREQ=MONTHLY;BYMONTHDAY=15 — monthly QA meeting on the 15th.FREQ=WEEKLY;INTERVAL=2;BYDAY=MO — bi-weekly on-call rotation
every other Monday.RecurringMixin)CalendarEvent, BlockedTime, and AvailableTime each inherit
RecurringMixin, which contributes:
start_time / end_time (timezone-aware computed fields backed by
start_time_tz_unaware / end_time_tz_unaware + timezone).recurrence_rule (1:1 to RecurrenceRule).recurrence_id (the original start of the occurrence this row
represents — used by exception rows).parent_recurring_object (FK back to the master row when this row is
a materialized exception or instance).is_recurring_exception (set on rows that override a single
occurrence).bulk_modification_parent (FK to the original master when this row is
a continuation of a split — see below).The mixin's get_occurrences_in_range(start, end) and
get_occurrences_in_range_with_bulk_modifications(start, end) methods
return a list of in-memory occurrences (cancelled ones filtered out,
modified ones substituted in).
A single occurrence can be cancelled or modified without
breaking the parent's rule. Each recurring model has a paired exception
model (EventRecurrenceException, BlockedTimeRecurrenceException,
AvailableTimeRecurrenceException) with two fields that matter:
exception_date — the original recurrence_id of the occurrence
being excepted.is_cancelled — True to skip this occurrence; False to substitute
a modified row.is_cancelled=False, a separate "modified" row (e.g.
modified_event) stores the overridden details (a new time, a new
title, etc.).Healthcare examples:
is_cancelled=True, exception_date=2026-12-23T09:00.is_cancelled=False, plus a modified CalendarEvent with the new
time.Single-occurrence exceptions are fine for "this Wednesday only," but for "every Wednesday from now on" they don't scale. The system models that case as a split:
UNTIL (helper:
RecurrenceRuleSplitter.truncate_rule_until_date).RecurrenceRule starting at the modification date
(RecurrenceRuleSplitter.create_continuation_rule). The continuation
carries bulk_modification_parent pointing back at the original.*BulkModification row
(EventBulkModification, BlockedTimeBulkModification,
AvailableTimeBulkModification) so callers can reason about the
chain.The original lives unchanged before the modification date; the
continuation expresses the new rule afterwards. Helpers like
get_occurrences_in_range_with_bulk_modifications and
only_calendars_available_in_ranges_with_bulk_modifications walk both
when computing occurrences/availability so callers see the
post-modification reality.
UNTIL at
the cutoff date and mark is_bulk_cancelled=True on the
*BulkModification record. No continuation is created.| Situation | Use |
|---|---|
| Cancel one Wednesday's clinic | EventRecurrenceException with is_cancelled=True. |
| Move one Wednesday's clinic to Friday | Exception with is_cancelled=False + modified event. |
| Move every future Wednesday clinic to Friday | Bulk modification (split). |
Add a one-off BlockedTime for a snow day |
Single BlockedTime row, no recurrence. |
| Permanently change clinic hours for one provider | Split the recurring AvailableTime. |
max_occurrences argument
(default 10000). Callers that ask for very long windows of very
fine-grained recurrences should pass an appropriate cap.annotate_recurring_occurrences_on_date_range push
expansion into SQL via Postgres functions so the API doesn't have to
pull master rows into Python first.*_with_bulk_modifications family is more expensive than the
base methods because it walks continuation chains. Only use it when
the caller needs the consolidated view.