Skip to content

Data Model Reference

The data model lives in the config Django app (config/models.py). All core domain entities -- clinicians, shifts, leave, alerts, audit logs -- are defined there using Django's ORM with Python enum.Enum classes for type-safe choice fields.

Enums

WorkingTermType

Value Description
SALARIED Salaried employee with fixed working pattern
PARTNER GP partner with flexible working pattern
LOCUM Locum doctor, works manually specified dates only
FY_DOCTOR Foundation Year trainee doctor
ST_DOCTOR Specialty Trainee doctor

WorkingTermType.get_resident_types() returns [FY_DOCTOR, ST_DOCTOR].

ShiftType

Value Counts as Worked Counts Toward Minimum Description
STANDARD Yes Yes Regular working shift
DUTY Yes Yes Duty doctor shift
STUDY_LEAVE No No Study leave (does not consume leave quota via Shift)
CORONERS Yes No Coroner's court attendance

ShiftDuration

Value Description
FULL Full day shift
HALF Half day shift

ShiftStatus

Value Description
SCHEDULED Shift is scheduled
COMPLETED Shift has been completed
CANCELLED Shift has been cancelled

ChangeType

Value Description
CREATED Shift was created
UPDATED Shift was modified
DELETED Shift was deleted

Used by PendingShiftNotification for batch-mode change tracking.

LeaveType

Value Quota-based Partner-only Description
ANNUAL_LEAVE Yes No Annual leave/vacation
NOT_WORKING No Yes Day not working (partners only)
PLANNED_SICK No No Planned sick leave
STUDY_LEAVE No No Study leave
CORONERS No No Coroner's court attendance

LeaveType.is_quota_based(value) and LeaveType.is_partner_only(value) are class methods.

LeaveStatus

Value Description
REQUESTED Submitted, awaiting processing
APPROVED Approved by admin
DENIED Denied by admin
CANCELLED Cancelled (by admin or clinician)

ManualAdjustmentType

Value Description
ANNUAL_LEAVE_ADJUSTMENT Adjustment to annual leave entitlement
SICK_SHIFT_COMPENSATION Compensation for sick shifts

AlertType

Value Label Description
BELOW_MINIMUM Short-staffed Staffing below minimum required
AT_WARNING_THRESHOLD At Minimum Staffing Staffing at minimum threshold
OVERWORKED_DOCTOR Overworked Doctor Doctor is overworked
CONSTRAINT_VIOLATION Scheduling Concern Rota constraint violation
INSUFFICIENT_DUTY_DOCTORS Not Enough Duty Doctors Insufficient duty doctors on a day

AlertSeverity

Value Description
WARNING Warning level alert
CRITICAL Critical level alert

AlertStatus

Value Auto-sets resolved_at Description
ACTIVE No Alert is currently active
RESOLVED Yes Alert has been resolved
DISMISSED Yes Alert has been dismissed

AuditAction

Value Description
CREATE Entity was created
UPDATE Entity was updated
DELETE Entity was deleted

SwapStatus

Value Description
REQUESTED Swap requested, awaiting target response
ACCEPTED Target accepted, pending admin approval
REJECTED_BY_TARGET Target declined
PENDING_ADMIN Accepted by target, awaiting admin
APPROVED Admin approved, swap executed
REJECTED_BY_ADMIN Admin declined
CANCELLED Requester cancelled

SickLeaveStatus

Value Description
ACTIVE Currently off sick
RETURNED Back to work
CANCELLED Recorded in error

BulkOperationType

Value Description
DELETE Bulk shift deletion
SWAP Bulk shift swap

Core Models

SystemConfiguration (singleton)

System-wide configuration. Only one instance should exist (enforced by verbose_name_plural). Access via SystemConfiguration.get_or_create() or SystemConfiguration.get_cached().

Field Type Default Description
target_working_days_per_week DecimalField(4,2) 3.50 FTE working days per week
minimum_doctors JSONField dict Min doctors per weekday: {"Monday": 9, "Tuesday": 7, ...}
duty_doctors_required IntegerField 1 Duty doctors on normal days
duty_doctors_post_bank_holiday IntegerField 2 Duty doctors after bank holidays
post_bank_holiday_minimum CharField(20) "Monday" Which day's minimum to use post-bank-holiday
fixed_period_weeks IntegerField 6 Weeks in the fixed (non-auto-recalc) period
reporting_periods JSONField list Reporting period configs
uk_nation CharField(20) "england" UK nation for bank holiday detection
current_date DateField (nullable) -- Reference date for fixed period boundary
enable_wte_balance BooleanField True Enable WTE balance optimization phase
enable_shift_notifications BooleanField True Enable email notifications for shift changes
shift_notification_batch_mode BooleanField False Queue notifications instead of sending immediately
batch_mode_enabled_at DateTimeField (nullable) -- When batch mode was last enabled
batch_mode_enabled_by CharField(255) "" User who last enabled batch mode
wte_balance_deficit_weight DecimalField(3,2) 0.60 Weight for daily deficit in day selection (0-1)
wte_balance_day_distribution_weight DecimalField(3,2) 0.40 Weight for day-of-week balance in day selection (0-1)
optimization_iterations IntegerField 10 Randomized iterations for optimization
christmas_period_start_month IntegerField 12 Month Christmas restricted period starts
christmas_period_start_day IntegerField 24 Day Christmas restricted period starts
christmas_period_end_month IntegerField 1 Month Christmas restricted period ends
christmas_period_end_day IntegerField 2 Day Christmas restricted period ends

Notable behaviour: - save() runs full_clean(), creates audit log entries for critical field changes, and invalidates related caches. - uk_nation cannot be changed after shifts exist (prevents historical inconsistencies). - WTE balance weights must sum to 1.0.

ClinicianGroup

Groups for categorising clinicians (e.g., "FY Doctor", "FY Supervisor"). Separate from Django auth groups.

Field Type Description
name CharField(100, unique) Group name
description TextField (blank) Group purpose

Many-to-many relationship with Clinician via Clinician.groups.

Clinician

A doctor or clinician in the rota system.

Field Type Description
name CharField(255) Full name
email EmailField (nullable) Contact email for notifications
user OneToOneField(auth.User, nullable, SET_NULL) Linked login account; related_name clinician_profile
active BooleanField (default True) Currently active in the rota
groups ManyToManyField(ClinicianGroup) Clinician groups for rota rules
calendar_feed_secret CharField(64, unique, nullable) Secret key for calendar feed
calendar_feed_enabled BooleanField (default False) Whether calendar feed is active

Notable behaviour: - type is a property derived from the active WorkingTerm, not a stored field. Returns SALARIED as default. - is_resident_doctor() returns True if active WorkingTerm is FY_DOCTOR or ST_DOCTOR. - regenerate_calendar_secret() generates a new cryptographically secure token.

Invitation

Invitation for a user to create an account linked to a Clinician.

Field Type Description
clinician FK(Clinician, CASCADE) The clinician to invite
email EmailField Delivery address
token CharField(64, unique) Cryptographic signup token
expires_at DateTimeField Expiry (14 days from creation)
used BooleanField (default False) Whether invitation has been used
used_at DateTimeField (nullable) When it was used

Class method create_invitation(clinician, email, days_valid=14) handles token generation.

UserEmailPreferences

Admin email notification opt-in preferences.

Field Type Default Description
user OneToOneField(User, CASCADE) -- Owning user
email_pref_system_failures BooleanField False System failure emails
email_pref_admin_actions BooleanField False Admin action emails
email_pref_operational BooleanField False Operational notification emails

All preferences are opt-in (default False).

WorkingTerm

A period of employment with a specific working pattern. Clinicians can have multiple non-overlapping terms over time.

Field Type Description
clinician FK(Clinician, CASCADE) The clinician; related_name working_terms
type CharField(20, choices WorkingTermType) Employment type for this term
start_date DateField When the term begins
end_date DateField (nullable) When it ends (null = ongoing)
percentage DecimalField(5,2) FTE percentage, 0-100 (default 100)
fixed_working_days JSONField (list) Days the clinician must work (salaried)
fixed_half_days JSONField (list) Days that must be half days
cannot_work_days JSONField (list) Days the clinician cannot work (partners)
must_work_days JSONField (list) Days the clinician must work (partners)
annual_leave_entitlement JSONField (dict) Single-pool entitlement: {"total": 10.5}
participates_in_duty BooleanField (default True) Whether included in duty rotation
minimum_shifts_per_week IntegerField (nullable) Min shifts/week for partners (null = no minimum)
max_shifts_per_week IntegerField (nullable) Max shifts/week for partners (null = no maximum)

Key class methods:

Method Returns Description
get_active_term_for_date(clinician, date) WorkingTerm or None Term overlapping the given date
get_active_term(clinician, as_of_date=None) WorkingTerm or None Alias, defaults to today
get_term_near_date(clinician, date, months_window=6) WorkingTerm or None Active, upcoming, or recent past term
get_terms_in_period(clinician, start, end) QuerySet All terms overlapping a date range
get_latest_term(clinician) WorkingTerm or None Most recent term by start_date

Properties: total_leave_adjustments sums all linked LeaveAdjustment amounts; get_effective_entitlement() adds adjustments to base entitlement.

SupervisionRelationship

Links a trainee's WorkingTerm to a supervisor Clinician. One supervisor per WorkingTerm (unique constraint).

Field Type Description
supervisor FK(Clinician, CASCADE) The supervising clinician
working_term FK(WorkingTerm, CASCADE) The trainee's working term

Validation: a clinician cannot supervise themselves.

Shift

A scheduled working day for a clinician.

Field Type Description
clinician FK(Clinician, CASCADE) Assigned clinician; related_name shifts
date DateField Shift date (indexed)
type CharField(20, choices ShiftType) Shift type (default STANDARD)
duration CharField(10, choices ShiftDuration) FULL or HALF (default FULL)
status CharField(20, choices ShiftStatus) SCHEDULED, COMPLETED, CANCELLED
is_pinned BooleanField (default False) Immune to automatic rebalancing
is_off_sick BooleanField (default False) Marked as off sick
systm1_synced BooleanField (default False) Synced to Systm1
is_locum_booking BooleanField (default False) Locum booking, bypasses standard validation
created_by CharField(255) User who created
modified_by CharField(255) User who last modified

Database constraint: one SCHEDULED non-locum shift per clinician per day.

Properties: - counts_as_worked -- False for STUDY_LEAVE only. - counts_toward_minimum -- False for STUDY_LEAVE, CORONERS, off-sick shifts, and resident doctor (FY/ST) shifts.

save() accepts send_notification=False kwarg to suppress email notifications (for bulk operations). delete() also accepts this kwarg.

PendingShiftNotification

Queued shift change notification for batch mode. When SystemConfiguration.shift_notification_batch_mode is active, shift changes are recorded here instead of sending emails immediately.

Field Type Description
clinician FK(Clinician, SET_NULL, nullable) Assigned clinician
clinician_name CharField(255) Denormalised name (survives shift deletion)
shift_date DateField Date of the shift
shift_type CharField(20) Shift type
shift_duration CharField(10) FULL or HALF
change_type CharField(10, choices ChangeType) CREATED, UPDATED, or DELETED
change_description TextField Human-readable description
created_by CharField(255) User who made the change

Class method consolidate_for_shift(clinician, date) merges redundant pairs (e.g., CREATED + DELETED = remove both).

LeaveRequest

A request for time off by a clinician.

Field Type Description
clinician FK(Clinician, CASCADE) Requesting clinician; related_name leave_requests
type CharField(20, choices LeaveType) Leave type
start_date DateField First day of leave
end_date DateField Last day of leave
status CharField(20, choices LeaveStatus) REQUESTED, APPROVED, DENIED, CANCELLED
processed_at DateTimeField (nullable) When processed
processed_by CharField(255) User who processed
denial_reason TextField (blank) Required when status is DENIED
affected_shift_count FloatField (default 0.0) Calculated shifts affected
exceeded_quota BooleanField (default False) Whether approval exceeded remaining quota

Properties: is_quota_based delegates to LeaveType.is_quota_based(). type_display returns human-readable label.

save() accepts skip_validation=True kwarg for status-only changes.

LeaveAdjustment

Manual adjustment to a clinician's leave quota. This is the only mechanism for carry-over (no automatic carry-over exists).

Field Type Description
working_term FK(WorkingTerm, CASCADE) Term this adjustment applies to; related_name leave_adjustments
amount DecimalField(5,2) Positive to add, negative to subtract (cannot be zero)
reason TextField Explanation for the adjustment
created_by CharField(255) Admin who created it

ManualAdjustment

Legacy adjustment model (superseded by LeaveAdjustment for leave quota changes).

Field Type Description
clinician FK(Clinician, CASCADE) Adjusted clinician; related_name manual_adjustments
type CharField(30, choices ManualAdjustmentType) Adjustment type
adjustment_value FloatField Value (can be negative)
reporting_period CharField(50, blank) Reporting period label
reason TextField Reason
created_by CharField(255) Admin who created it

LocumBooking

Scheduled dates for a locum doctor. Locums work manually specified dates only and are not part of automatic shift allocation.

Field Type Description
clinician FK(Clinician, CASCADE) The locum clinician; related_name locum_bookings
dates JSONField (list) Specific dates: ["2024-06-15", ...]
start_date DateField (nullable) Range booking start
end_date DateField (nullable) Range booking end
created_by CharField(255) User who created

SickLeave

Tracks when a clinician is off sick. Created and managed by admins.

Field Type Description
clinician FK(Clinician, CASCADE) Clinician off sick; related_name sick_leaves
start_date DateField First day of sick leave
end_date DateField (nullable) Last day (null = ongoing)
status CharField(20, choices SickLeaveStatus) ACTIVE, RETURNED, CANCELLED
notes TextField (blank) Admin notes (not visible to clinician)
recorded_by CharField(255) Admin who recorded it

Class method get_active_for_date(clinician, date) returns active sick leave for a specific date.

ShiftSwapRequest

Request to swap shifts between two clinicians with approval workflow: target accepts/declines, then admin approves/rejects.

Field Type Description
requester_shift FK(Shift, CASCADE) Shift being offered
target_shift FK(Shift, CASCADE) Shift being requested
requester FK(Clinician, CASCADE) Clinician initiating swap
target_clinician FK(Clinician, CASCADE) Clinician whose shift is requested
status CharField(25, choices SwapStatus) Current status
responded_at DateTimeField (nullable) When target responded
admin_processed_at DateTimeField (nullable) When admin processed
response_by FK(User, SET_NULL, nullable) User who responded on behalf of target
admin_response_by FK(User, SET_NULL, nullable) Admin who approved/rejected
rejection_reason TextField (blank) Reason for rejection

Same-type rule: shifts must match type (duty with duty, standard with standard, locum with locum).

Alert

An alert about a rota issue (staffing, overwork, constraints).

Field Type Description
type CharField(30, choices AlertType) Alert category
severity CharField(10, choices AlertSeverity) WARNING or CRITICAL
date DateField Affected date
message CharField(500) Alert message
details JSONField Additional context
status CharField(20, choices AlertStatus) ACTIVE, RESOLVED, DISMISSED
resolved_at DateTimeField (nullable) Auto-set when status leaves ACTIVE

AuditLog

Tracks all changes to important entities.

Field Type Description
entity_type CharField(50) Entity type name (e.g., "Shift")
entity_id CharField(255) Entity primary key
action CharField(20, choices AuditAction) CREATE, UPDATE, DELETE
user_id CharField(255, nullable) User who made the change
previous_state JSONField Snapshot before change
new_state JSONField Snapshot after change
reason TextField (blank) Reason for change

BulkShiftOperation

Tracks bulk shift operations (delete, swap) for undo within a 7-day window.

Field Type Description
operation_type CharField(20, choices BulkOperationType) DELETE or SWAP
performed_by CharField(255) User who performed the operation
performed_at DateTimeField When it was performed
deleted_shifts_data JSONField Data of deleted shifts (for DELETE)
swap_original_data JSONField Original states (for SWAP)
is_undone BooleanField Whether undone
undone_at DateTimeField (nullable) When undone
undone_by CharField(255) User who undid it

can_undo() returns True if within 7 days and not already undone.

CustomRule

Defines custom rules for specific rota requirements (e.g., trainer must be present on Mondays).

Field Type Description
name CharField(255) Rule name
role_required CharField(100) Required role (e.g., "trainer")
applicable_days JSONField (list) Day names or specific dates
description TextField (blank) Rule description
active BooleanField (default True) Currently active

Relationships

erDiagram
    User ||--o| Clinician : "clinician_profile (SET_NULL)"
    Clinician ||--o{ WorkingTerm : "working_terms (CASCADE)"
    Clinician ||--o{ Shift : "shifts (CASCADE)"
    Clinician ||--o{ LeaveRequest : "leave_requests (CASCADE)"
    Clinician ||--o{ ManualAdjustment : "manual_adjustments (CASCADE)"
    Clinician ||--o{ LocumBooking : "locum_bookings (CASCADE)"
    Clinician ||--o{ Invitation : "invitations (CASCADE)"
    Clinician ||--o{ SickLeave : "sick_leaves (CASCADE)"
    Clinician }o--o{ ClinicianGroup : "groups (M2M)"
    Clinician ||--o{ ShiftSwapRequest : "initiated_swap_requests"
    Clinician ||--o{ ShiftSwapRequest : "received_swap_requests"

    WorkingTerm ||--o{ LeaveAdjustment : "leave_adjustments (CASCADE)"
    WorkingTerm ||--o| SupervisionRelationship : "supervision_relationships (CASCADE)"
    Clinician ||--o{ SupervisionRelationship : "supervision_as_supervisor (CASCADE)"

    Shift ||--o{ ShiftSwapRequest : "outgoing_swap_requests"
    Shift ||--o{ ShiftSwapRequest : "incoming_swap_requests"

    Clinician ||--o{ PendingShiftNotification : "pending_notifications (SET_NULL)"

    User ||--o| UserEmailPreferences : "email_prefs (CASCADE)"
    User ||--o{ ShiftSwapRequest : "swap_responses (SET_NULL)"
    User ||--o{ ShiftSwapRequest : "swap_admin_responses (SET_NULL)"

    SystemConfiguration {
        int id
    }

    AuditLog {
        string entity_type
        string entity_id
    }

    Alert {
        string type
        date date
    }

    CustomRule {
        string name
    }

    BulkShiftOperation {
        string operation_type
    }

Validation Rules

SystemConfiguration

Rule Field(s)
target_working_days_per_week must be > 0 and <= 7 target_working_days_per_week
minimum_doctors must be a dict with Mon-Fri keys, positive integer values minimum_doctors
duty_doctors_required must be > 0 duty_doctors_required
duty_doctors_post_bank_holiday must be > 0 duty_doctors_post_bank_holiday
post_bank_holiday_minimum must be a valid day name (Mon-Sun) post_bank_holiday_minimum
fixed_period_weeks must be > 0 fixed_period_weeks
uk_nation must be one of: england, scotland, wales, northern_ireland uk_nation
uk_nation cannot change after shifts exist uk_nation
WTE balance weights must be between 0 and 1 and sum to 1.0 wte_balance_deficit_weight, wte_balance_day_distribution_weight
optimization_iterations must be >= 1 optimization_iterations
Christmas period month/day fields must be valid christmas_period_*

WorkingTerm

Rule Field(s)
end_date >= start_date (if both set) start_date, end_date
percentage must be 0-100 percentage
Day fields must contain only valid weekday names fixed_working_days, fixed_half_days, cannot_work_days, must_work_days
No overlapping terms for the same clinician start_date, end_date
annual_leave_entitlement.total must be non-negative numeric annual_leave_entitlement
Partner entitlement capped at 52; salaried at 260 annual_leave_entitlement

Shift

Rule Field(s)
Clinician must have an active WorkingTerm on the shift date date, clinician
Only one SCHEDULED non-locum shift per clinician per day (DB constraint) clinician, date, status, is_locum_booking

LeaveRequest

Rule Field(s)
end_date >= start_date start_date, end_date
denial_reason required when status is DENIED denial_reason, status
NOT_WORKING type only available to partners type, clinician
Partners must request ANNUAL_LEAVE for full calendar weeks (Mon-Fri) start_date, end_date, type

LeaveAdjustment

Rule Field(s)
amount cannot be zero amount

SickLeave

Rule Field(s)
end_date >= start_date (if set) start_date, end_date

SupervisionRelationship

Rule Field(s)
Supervisor cannot be the same clinician as the working term's clinician supervisor, working_term
One supervisor per working term (unique constraint) working_term

Cascade Summary

Parent Child On Delete
Clinician WorkingTerm CASCADE
Clinician Shift CASCADE
Clinician LeaveRequest CASCADE
Clinician ManualAdjustment CASCADE
Clinician LocumBooking CASCADE
Clinician Invitation CASCADE
Clinician SickLeave CASCADE
Clinician SupervisionRelationship (as supervisor) CASCADE
WorkingTerm LeaveAdjustment CASCADE
WorkingTerm SupervisionRelationship CASCADE
Shift ShiftSwapRequest CASCADE
User Clinician (via user FK) SET_NULL
User UserEmailPreferences CASCADE