Jul 27 - Update code for scheduled tasks 2
This commit is contained in:
@@ -448,6 +448,9 @@ All are nullable and **legacy phase36 rows keep NULLs**, falling back to `_add_i
|
||||
**A plan, not an inspection.** Names a facility + template + assigned inspector + `next_due_date`. Lifecycle:
|
||||
- The assigned inspector (or a manager) clicks **Start** → `scheduled_inspections.start` creates a normal `in_progress` Inspection with `scheduled_inspection_id` set, then redirects to the execute flow.
|
||||
- On **completion** (execute route, status → `completed`), `ScheduledInspection.fulfill()` runs in the same atomic commit: `once` → `active=False`; recurring → `next_due_date` rolls forward past today via `_add_interval()` and the three `*_notified` flags reset.
|
||||
- **End date (phase44).** `end_date` is the manager's boundary; NULL = forever, and it is forced NULL for `once`. Inclusive — an occurrence landing exactly on it still runs. Two enforcement points, both needed: `fulfill()` deactivates when the rolled-forward `next_due_date` passes the boundary (the schedule that ends by being *completed*), and `run_reminders()` calls `expire_if_past_end_date()` on every active schedule before doing any reminder work (the schedule that reaches its boundary *without ever being done* — otherwise it re-alerts as overdue forever). `next_due_date` is left unclamped on expiry so the row shows which occurrence it stopped before.
|
||||
- **Form validation.** `ScheduledInspectionForm.validate()` rejects an end date on a one-time schedule and one earlier than the due date. That is not sufficient alone: `align_due_date()` can push the picked date forward onto the rule (a Tuesday pick on a Mon/Wed/Fri schedule becomes Wednesday), so `_reject_if_past_end_date()` re-checks after `_apply_recurrence()` in both create and edit. Edit rolls back first — `sched` is persistent and already mutated at that point.
|
||||
- **Three status states** in the list: Active, **Ended** (`is_expired` — ran its course), Inactive (a manager switched it off).
|
||||
- **Assignment notification** (immediate): on **create**, the assigned inspector gets an in-app + email "assigned to you" notification; on **edit**, only when the inspector actually changes (a "reassigned to you" notification to the new assignee). Via `_notify_assignee()` in the blueprint using `event_type=EVENT_SCHEDULED_INSPECTION`.
|
||||
- **Reminders** are dispatched by the cron endpoint (see §11): advance (1 day before) + due-date to the inspector, overdue to admin/director — each fires at most once per occurrence via the `*_notified` flags. Uses `notify()` with `event_type=EVENT_SCHEDULED_INSPECTION`.
|
||||
- Dashboard shows an **upcoming (next 7 days) / overdue** panel for non-customers (inspectors see only their own).
|
||||
@@ -863,6 +866,10 @@ phase1_projects_roles → phase6_features → phase7_mobile_api → phase8_notif
|
||||
→ phase43_sched_recurrence ← HEAD
|
||||
```
|
||||
|
||||
#### phase44 — scheduled inspection end date
|
||||
|
||||
Revision id `phase44_sched_end_date` (file `phase44_scheduled_end_date.py`, down_revision `phase43_sched_recurrence`). Adds `scheduled_inspections.end_date DATE NULL` — the last date a recurring schedule may produce an occurrence. **No backfill**: NULL means "repeat indefinitely", which is exactly what every existing row does today, so nothing changes cadence on deploy. Splits the two meanings `next_due_date` was carrying (see §5 `ScheduledInspection` and rule 85). `INFORMATION_SCHEMA` column-existence check — safe to re-run.
|
||||
|
||||
### phase43_sched_recurrence
|
||||
|
||||
Revision id `phase43_sched_recurrence`. Adds the five nullable recurrence-detail columns to `scheduled_inspections` (`weekdays`, `month_mode`, `day_of_month`, `nth_week`, `nth_weekday`) so weekly schedules can name their weekdays and monthly schedules can use either a day-of-month or an nth-weekday rule — see §5 `ScheduledInspection`. **No backfill**: existing rows keep NULLs and retain their current cadence. `month_mode` is VARCHAR, not ENUM, so a future recurrence style needs no 3-step ENUM migration (rule 3). `INFORMATION_SCHEMA` column-existence checks — safe to re-run.
|
||||
@@ -1399,6 +1406,7 @@ timeout = 30
|
||||
| 83 | **A bad `scheduled_inspection_id` must NEVER fail the inspection submission** | `_resolve_schedule()` in `app/api/inspections.py` drops an unknown or foreign link and logs a warning instead of returning 404/403. The app is offline-first: a completed inspection can sit in the outbox for days, during which the schedule may be deleted, reassigned, or rolled forward. Erroring would burn the 5 sync retries and permanently strand that inspection **and its photos** on the device. A missed fulfil is fixable from the web; a stranded submission is not. The ownership check still refuses to *link* a foreign schedule (one inspector must not fulfil another's) — it just accepts the inspection anyway. |
|
||||
| 82 | **A schedule's recurrence columns must be CLEARED when they don't apply to the chosen frequency** | `_apply_recurrence()` in `routes/scheduled_inspections.py` is the single write path for `frequency` + `weekdays`/`month_mode`/`day_of_month`/`nth_week`/`nth_weekday`, and it NULLs the blocks that don't apply. Setting `sched.frequency` directly (as create/edit used to) leaves stale settings behind — a weekly→monthly switch would keep `weekdays` and `recurrence_label` would lie. The hidden form blocks still POST their values, so client-side hiding is not enough. |
|
||||
| 84 | **"Instructions" is a LABEL over `notes` — never rename the field, attribute, column or API key** | `ScheduledInspectionForm.notes` renders as "Instructions" and both the web execute page and the iPad say "Instructions". The wire key stays `notes` (`api/scheduled.py::_scheduled_payload`), which is what `APIScheduledInspection.notes` decodes into `LocalScheduledInspection.notes`; the iPad exposes it through a computed `instructions` accessor that also trims blank text. Renaming any of the storage identifiers would silently break the iPad decode — the field is `try?`-decoded, so it would fail to nil rather than throwing. |
|
||||
| 85 | **`next_due_date` is mutable state, `end_date` is a fixed boundary — never conflate them** | `fulfill()` rewrites `next_due_date` after every completed inspection; `end_date` is set by the manager and never touched by the app. The old single label "Start / Due Date" said both at once, which is what users reported as confusing. The label now follows context — `form.next_due_date.label.text` is set to "Start Date" in `create()` and "Next Due Date" in `edit()`. Do not rename the `next_due_date` column to match a label: it is indexed, it is the API payload key the iPad decodes, and the reminder cron filters on it. |
|
||||
| 81 | **Photo timestamp/geo overlay is burned at UPLOAD, never on `PATCH /issues/<id>/photos`** | That PATCH receives only path strings — the bytes are already in storage and the payload carries no capture metadata. Burning there would need a read-modify-write per key plus an overwrite-in-place primitive (`storage.save()` mints a NEW uuid key, and §22 requires key == DB path), and would risk a **double burn** since the endpoint is deliberately idempotent/retry-safe (rule 45). Stamp in `POST /photos/upload`, where the raw bytes + EXIF are in hand and each call writes exactly one already-stamped object. Stamping failures must always fall back to storing the ORIGINAL bytes — never lose a photo to a stamping bug. See §23. |
|
||||
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user