Jul 28 - Update schedule page's referred time

This commit is contained in:
2026-07-28 15:09:12 -04:00
parent 4bb345da39
commit 81aeeac3d2
6 changed files with 203 additions and 18 deletions
+20 -2
View File
@@ -223,8 +223,26 @@ shows only `is_published` topics (sections with no published topics are hidden).
the posted time must be in that list, the date must parse, be today-or-later,
within `DEMO_MAX_DAYS_AHEAD`, and be a weekday. Dates/times are stored exactly
as picked; `DEMO_TIMEZONE_LABEL` is display-only (no tz conversion anywhere).
- **One appointment per slot:** a request for a (date, time) that already has a
non-`cancelled` row is rejected with a flash. Cancelling frees the slot.
- **One appointment per slot**, enforced in three places that share the same
helpers (`taken_slots`, `available_slots`, `demo_day_status` in `app.py`):
1. `GET /demo/slots?date=YYYY-MM-DD` → JSON `{open, reason, slots[]}`;
`static/js/demo-booking.js` rebuilds the time `<select>` on every date
change, so a booked time is never offered. Closed days (weekend / past /
beyond `DEMO_MAX_DAYS_AHEAD`) come back `open:false` with the reason, and
the picker is emptied + disabled. Stale responses are dropped by a request
counter; a fetch failure fails OPEN (keeps the list, server still decides).
2. The server renders only free slots when the form already has a date (so a
re-render after a validation error can't offer a slot taken meanwhile), and
the whole grid when it doesn't — the no-JS path still works.
3. `POST /demo` remains the authority: a (date, time) with a non-`cancelled`
row is rejected with "that time was just taken". Cancelling frees the slot.
- The endpoint is public and read-only: it reveals which times are free —
exactly what the form shows anyway — and nothing about who booked them.
- Two people submitting the same free slot in the same instant can still both
pass the check-then-insert; the second one is simply a duplicate row for the
owner to sort out. Closing that needs a DB-level constraint (a generated
`slot_key` NULL-ed for cancelled rows + UNIQUE index), which we have not
added.
- **Abuse control:** hidden `website` honeypot (filled → fake success, nothing
stored) + per-IP hourly cap `DEMO_RATE_LIMIT` held in `_demo_hits` (in-memory,
therefore per gunicorn worker — it's a nuisance filter, not a DDoS defence;