Jul 8 - Update scheduled inspection
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
"""
|
||||
app/models/scheduled_inspection.py
|
||||
-----------------------------------
|
||||
Planned/recurring inspection assignments (phase36).
|
||||
|
||||
A ScheduledInspection is a PLAN, not an inspection: it names a facility, a
|
||||
template, the responsible inspector, and a due date. The inspector opens it
|
||||
("Start"), which creates a normal in_progress Inspection linked back via
|
||||
Inspection.scheduled_inspection_id; when that inspection is completed the
|
||||
schedule is marked fulfilled — deactivated (one-time) or rolled forward to the
|
||||
next occurrence (recurring).
|
||||
|
||||
Reminders are dispatched by the cron endpoint
|
||||
POST /scheduled-inspections/run?token=DIGEST_SECRET:
|
||||
- advance reminder to the inspector 1 day before the due date
|
||||
- due reminder to the inspector on the due date
|
||||
- overdue alert to admin/director once the due date passes uncompleted
|
||||
The *_notified flags make each of those fire at most once per occurrence and
|
||||
reset when a recurring schedule rolls forward.
|
||||
"""
|
||||
|
||||
from datetime import timedelta
|
||||
from app import db
|
||||
from app.utils.time_utils import now_eastern
|
||||
|
||||
|
||||
FREQUENCY_CHOICES = ('once', 'daily', 'weekly', 'monthly')
|
||||
|
||||
|
||||
class ScheduledInspection(db.Model):
|
||||
__tablename__ = 'scheduled_inspections'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
facility_id = db.Column(db.Integer,
|
||||
db.ForeignKey('facilities.id', ondelete='CASCADE'),
|
||||
nullable=False, index=True)
|
||||
template_id = db.Column(db.Integer,
|
||||
db.ForeignKey('inspection_templates.id', ondelete='CASCADE'),
|
||||
nullable=False)
|
||||
inspector_id = db.Column(db.Integer,
|
||||
db.ForeignKey('users.id', ondelete='SET NULL'),
|
||||
nullable=True, index=True)
|
||||
frequency = db.Column(db.Enum(*FREQUENCY_CHOICES), nullable=False, default='once')
|
||||
next_due_date = db.Column(db.Date, nullable=False, index=True)
|
||||
active = db.Column(db.Boolean, nullable=False, default=True)
|
||||
notes = db.Column(db.Text, nullable=True)
|
||||
|
||||
created_by = db.Column(db.Integer, db.ForeignKey('users.id', ondelete='SET NULL'),
|
||||
nullable=True)
|
||||
created_at = db.Column(db.DateTime, nullable=False, default=now_eastern)
|
||||
last_completed_at = db.Column(db.DateTime, nullable=True)
|
||||
|
||||
# Per-occurrence reminder de-dup flags (reset when a recurring one rolls forward)
|
||||
advance_notified = db.Column(db.Boolean, nullable=False, default=False)
|
||||
due_notified = db.Column(db.Boolean, nullable=False, default=False)
|
||||
overdue_notified = db.Column(db.Boolean, nullable=False, default=False)
|
||||
|
||||
# Relationships
|
||||
facility = db.relationship('Facility', foreign_keys=[facility_id])
|
||||
template = db.relationship('InspectionTemplate', foreign_keys=[template_id])
|
||||
inspector = db.relationship('User', foreign_keys=[inspector_id])
|
||||
creator = db.relationship('User', foreign_keys=[created_by])
|
||||
|
||||
FREQUENCY_LABELS = {
|
||||
'once': 'One-time',
|
||||
'daily': 'Daily',
|
||||
'weekly': 'Weekly',
|
||||
'monthly': 'Monthly',
|
||||
}
|
||||
|
||||
@property
|
||||
def frequency_label(self):
|
||||
return self.FREQUENCY_LABELS.get(self.frequency, self.frequency)
|
||||
|
||||
@staticmethod
|
||||
def _add_interval(d, frequency):
|
||||
"""Return d advanced by one interval of the given frequency."""
|
||||
if frequency == 'daily':
|
||||
return d + timedelta(days=1)
|
||||
if frequency == 'weekly':
|
||||
return d + timedelta(weeks=1)
|
||||
if frequency == 'monthly':
|
||||
# Add ~1 month by stepping 28–31 days to the same day-of-month where possible.
|
||||
month = d.month + 1
|
||||
year = d.year + (1 if month > 12 else 0)
|
||||
month = 1 if month > 12 else month
|
||||
day = min(d.day, 28) # clamp to avoid invalid dates (e.g. Feb 30)
|
||||
return d.replace(year=year, month=month, day=day)
|
||||
return d # 'once' has no next interval
|
||||
|
||||
def is_overdue(self, today=None):
|
||||
today = today or now_eastern().date()
|
||||
return self.active and self.next_due_date < today
|
||||
|
||||
def fulfill(self):
|
||||
"""Mark this occurrence complete. One-time schedules deactivate;
|
||||
recurring ones roll their due date forward past today and reset the
|
||||
reminder flags. Caller commits."""
|
||||
self.last_completed_at = now_eastern()
|
||||
if self.frequency == 'once':
|
||||
self.active = False
|
||||
return
|
||||
# Recurring: advance until the next due date is in the future.
|
||||
today = now_eastern().date()
|
||||
nxt = self._add_interval(self.next_due_date, self.frequency)
|
||||
guard = 0
|
||||
while nxt <= today and guard < 400:
|
||||
nxt = self._add_interval(nxt, self.frequency)
|
||||
guard += 1
|
||||
self.next_due_date = nxt
|
||||
self.advance_notified = False
|
||||
self.due_notified = False
|
||||
self.overdue_notified = False
|
||||
|
||||
def __repr__(self):
|
||||
return (f'<ScheduledInspection {self.id} facility={self.facility_id} '
|
||||
f'freq={self.frequency} due={self.next_due_date}>')
|
||||
Reference in New Issue
Block a user