Jul 27 - Update code for scheduled tasks 2

This commit is contained in:
Nguyen Ngo
2026-07-27 15:26:06 -04:00
parent 44b577b59f
commit 455534ccda
9 changed files with 306 additions and 5 deletions
+56 -1
View File
@@ -17,6 +17,12 @@ POST /scheduled-inspections/run?token=DIGEST_SECRET:
- 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.
Two dates, deliberately distinct (phase44):
next_due_date — mutable state. The next occurrence. Rewritten by fulfill()
after every completed inspection.
end_date — fixed boundary. The last date an occurrence may fall on,
set by the manager and never rewritten. NULL = forever.
"""
import calendar
@@ -85,6 +91,11 @@ class ScheduledInspection(db.Model):
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)
# Fixed boundary set by the manager, never rewritten by the app — unlike
# next_due_date, which fulfill() advances after every completed inspection.
# NULL = repeat indefinitely. Only meaningful for recurring schedules; the
# create/edit routes force it to NULL when frequency == 'once'.
end_date = db.Column(db.Date, nullable=True)
active = db.Column(db.Boolean, nullable=False, default=True)
notes = db.Column(db.Text, nullable=True)
@@ -227,10 +238,48 @@ class ScheduledInspection(db.Model):
today = today or now_eastern().date()
return self.active and self.next_due_date < today
# ── End-date boundary (phase44) ──────────────────────────────────────────
def is_within_end_date(self, d):
"""True if date *d* is on or before the end date (inclusive).
No end date means the schedule repeats indefinitely, so every date
qualifies.
"""
return self.end_date is None or d <= self.end_date
@property
def is_expired(self):
"""True once the end date has passed.
Independent of `active`: a schedule can be inactive because it expired
or because a manager switched it off, and the list view distinguishes
the two. Compare against the *end date* rather than `next_due_date`,
which may have been advanced past the boundary by fulfill().
"""
if self.end_date is None:
return False
return self.end_date < now_eastern().date()
def expire_if_past_end_date(self, today=None):
"""Deactivate a schedule whose end date has passed. Caller commits.
Returns True if this call changed anything. Needed because a schedule
can reach its end date *without ever being completed* — fulfill() never
runs, so the boundary would otherwise be checked nowhere and the cron
would keep firing overdue alerts forever. Called from run_reminders().
"""
today = today or now_eastern().date()
if self.active and self.end_date is not None and self.end_date < today:
self.active = False
return True
return False
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."""
reminder flags. A recurring schedule whose next occurrence would fall
past its end date deactivates instead. Caller commits."""
self.last_completed_at = now_eastern()
if self.frequency == 'once':
self.active = False
@@ -243,6 +292,12 @@ class ScheduledInspection(db.Model):
nxt = self.next_occurrence_after(nxt)
guard += 1
self.next_due_date = nxt
# Past the manager's boundary: this was the last occurrence. next_due_date
# is left at the computed value rather than clamped, so the row still
# shows which occurrence it stopped before.
if not self.is_within_end_date(nxt):
self.active = False
return
self.advance_notified = False
self.due_notified = False
self.overdue_notified = False