Jul 27 - Update code for scheduled tasks
This commit is contained in:
@@ -451,6 +451,7 @@ All are nullable and **legacy phase36 rows keep NULLs**, falling back to `_add_i
|
|||||||
- **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`.
|
- **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`.
|
- **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).
|
- Dashboard shows an **upcoming (next 7 days) / overdue** panel for non-customers (inspectors see only their own).
|
||||||
|
- **Instructions (July 2026).** `ScheduledInspectionForm.notes` is labelled **"Instructions"** and `scheduled_inspections/form.html` explains that the text reaches the inspector. The *field name*, `ScheduledInspection.notes`, the `scheduled_inspections.notes` column and the API key `notes` are all unchanged — the rename is a label only (rule 84). The text is surfaced to the inspector in two places: `inspections/execute.html` renders an indigo panel between the header and the form grid, guarded on `inspection.scheduled_inspection and .notes` (NULL for ad-hoc work and for schedules deleted after the start); the iPad shows it on the scheduled row, on the start screen and above the form.
|
||||||
- **"Scheduled" badge:** an inspection started from a schedule carries `scheduled_inspection_id`. `Inspection.scheduled_inspection` (relationship, foreign_keys on that column) resolves the source schedule (None if ad-hoc or the schedule was later deleted). The inspection **detail** view header shows a `bi-calendar-check` "Scheduled · <frequency>" badge, and the inspection **list** shows a compact "Scheduled" pill next to the template name — both gated on `scheduled_inspection_id` being set.
|
- **"Scheduled" badge:** an inspection started from a schedule carries `scheduled_inspection_id`. `Inspection.scheduled_inspection` (relationship, foreign_keys on that column) resolves the source schedule (None if ad-hoc or the schedule was later deleted). The inspection **detail** view header shows a `bi-calendar-check` "Scheduled · <frequency>" badge, and the inspection **list** shows a compact "Scheduled" pill next to the template name — both gated on `scheduled_inspection_id` being set.
|
||||||
|
|
||||||
Management (`/scheduled-inspections/new|edit|delete`) is `@project_manager_required`; **Start** is the **assigned inspector ONLY** (`sched.inspector_id == current_user.id`) — managers do NOT get a Start button and `GET /<id>/start` 403s for anyone who isn't the assignee (the inspection is theirs to do; a manager who must run it assigns it to themselves). The Start button is hidden for non-assignees on both the scheduled-inspections list and the dashboard panel. Inspectors' list/dashboard views are scoped to their own `inspector_id`.
|
Management (`/scheduled-inspections/new|edit|delete`) is `@project_manager_required`; **Start** is the **assigned inspector ONLY** (`sched.inspector_id == current_user.id`) — managers do NOT get a Start button and `GET /<id>/start` 403s for anyone who isn't the assignee (the inspection is theirs to do; a manager who must run it assigns it to themselves). The Start button is hidden for non-assignees on both the scheduled-inspections list and the dashboard panel. Inspectors' list/dashboard views are scoped to their own `inspector_id`.
|
||||||
@@ -1397,6 +1398,7 @@ timeout = 30
|
|||||||
| 80 | **Assignee dropdowns are `director`/`inspector`/`auditor` (admin removed, auditor added)** | The issue/inspection assignee `<select>`s query `User.role.in_([...])` — admin was removed and auditor added (the inspection flag-issue list also keeps `project_manager`). These lists control who can be *assigned*, distinct from who can *edit*. The issue-update route (`issues.view`) defensively appends any current `assigned_to` who is not in the set (e.g. a legacy admin assignment) to `form.assigned_to.choices` so saving the form never silently unassigns them. Do not remove that guard. |
|
| 80 | **Assignee dropdowns are `director`/`inspector`/`auditor` (admin removed, auditor added)** | The issue/inspection assignee `<select>`s query `User.role.in_([...])` — admin was removed and auditor added (the inspection flag-issue list also keeps `project_manager`). These lists control who can be *assigned*, distinct from who can *edit*. The issue-update route (`issues.view`) defensively appends any current `assigned_to` who is not in the set (e.g. a legacy admin assignment) to `form.assigned_to.choices` so saving the form never silently unassigns them. Do not remove that guard. |
|
||||||
| 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. |
|
| 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. |
|
| 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. |
|
||||||
| 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. |
|
| 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. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -263,6 +263,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{# ── Instructions from the schedule (phase36) ──
|
||||||
|
Only rendered when this inspection was started from a ScheduledInspection
|
||||||
|
that carries instructions. `scheduled_inspection` is NULL for ad-hoc work
|
||||||
|
and for schedules deleted after the inspection was started, so both the
|
||||||
|
relationship and the text are guarded. #}
|
||||||
|
{% if inspection.scheduled_inspection and inspection.scheduled_inspection.notes %}
|
||||||
|
<div style="background:#eef2ff;border:1px solid #c7d2fe;border-left:4px solid #6366f1;
|
||||||
|
padding:.9rem 1.1rem;margin-top:.85rem;border-radius:8px;">
|
||||||
|
<div class="fw-semibold mb-1" style="color:#3730a3;font-size:.9rem;">
|
||||||
|
<i class="bi bi-info-circle-fill"></i> Instructions for this inspection
|
||||||
|
</div>
|
||||||
|
<div style="white-space:pre-wrap;color:#1e1b4b;font-size:.9rem;">{{ inspection.scheduled_inspection.notes }}</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{# ── Form body ── #}
|
{# ── Form body ── #}
|
||||||
<div class="insp-body">
|
<div class="insp-body">
|
||||||
{% if form_fields %}
|
{% if form_fields %}
|
||||||
|
|||||||
@@ -112,7 +112,11 @@
|
|||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
{{ form.notes.label(class="form-label fw-semibold") }}
|
{{ form.notes.label(class="form-label fw-semibold") }}
|
||||||
{{ form.notes(class="form-control", rows=2, placeholder="Optional instructions for the inspector…") }}
|
{{ form.notes(class="form-control", rows=3, placeholder="e.g. Front lobby carpet needs extra attention. Check loading dock after 3 PM — key is at the front desk.") }}
|
||||||
|
<div class="form-text">
|
||||||
|
<i class="bi bi-info-circle"></i>
|
||||||
|
Shown to the assigned inspector when they open this inspection, on the web and on the iPad.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-check mb-3">
|
<div class="form-check mb-3">
|
||||||
|
|||||||
+4
-1
@@ -339,7 +339,10 @@ class ScheduledInspectionForm(FlaskForm):
|
|||||||
('weekly', 'Weekly'), ('monthly', 'Monthly'),
|
('weekly', 'Weekly'), ('monthly', 'Monthly'),
|
||||||
], validators=[DataRequired()])
|
], validators=[DataRequired()])
|
||||||
next_due_date = DateField('Start / Due Date', validators=[DataRequired()])
|
next_due_date = DateField('Start / Due Date', validators=[DataRequired()])
|
||||||
notes = TextAreaField('Notes', validators=[Optional(), Length(max=1000)])
|
# UI label only. The field name, the ScheduledInspection.notes attribute and
|
||||||
|
# the scheduled_inspections.notes column all stay `notes` — renaming any of
|
||||||
|
# them would break the API payload key the iPad decodes.
|
||||||
|
notes = TextAreaField('Instructions', validators=[Optional(), Length(max=1000)])
|
||||||
active = BooleanField('Active', default=True)
|
active = BooleanField('Active', default=True)
|
||||||
|
|
||||||
# ── Recurrence detail (phase43) ──────────────────────────────────────────
|
# ── Recurrence detail (phase43) ──────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user