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 |