[Scheduling] Recurring session series with per-occurrence edits #98

Closed
opened 2026-07-14 19:48:49 +00:00 by claude-bot · 0 comments
Contributor

Context / Motivation

Weekly groups are the core Quest Board user, yet every session is created one-off today: models/session.py has no series concept — just scheduling_mode (vote/direct/tentative, SchedulingMode enum lines 47-52), status (proposed/confirmed/in_progress/completed/cancelled), confirmed_time/end_time UTC columns. A GM running "every Tuesday at 19:00" recreates the session by hand every week.

Spec

Data model — new SessionSeries:

  • id, campaign_id (FK, CASCADE)
  • cadence: weekly | biweekly | monthly_by_weekday (e.g. "2nd Tuesday")
  • weekday + time_of_day interpreted in the campaign timezone (campaign.timezone, models/campaign.py:41) so DST shifts keep the wall-clock time stable
  • start_date; end condition: until_date | count | open-ended (nullable pair)
  • default_duration_minutes (materialized into end_time); reminders inherit from the campaign's reminder_offsets_minutes — no per-series reminder config
  • title_template (optional), created_by, timestamps, active flag
  • sessions.series_id (nullable FK, SET NULL) linking materialized occurrences

Materialization — a Celery Beat task (alongside poll_session_reminders / auto_complete_sessions in webapp/backend/app/tasks/reminder_tasks.py:998/:1145) materializes concrete sessions a rolling window ahead (default 4 weeks) as normal direct-mode sessions with status=confirmed, linked by series_id. Idempotent: an occurrence key (series_id + occurrence date) must guarantee exactly-once creation even if the task overlaps or reruns.

Edit semantics:

  • Per-occurrence edits (reschedule / cancel / detach from series) operate on the materialized session row only and never rewrite the series definition. A detached session keeps its data, drops series_id.
  • Editing the series affects only future unmaterialized occurrences by default; the UI prompts the GM whether to also update future materialized-but-unmodified occurrences.
  • Deleting/deactivating a series stops materialization; existing sessions remain (prompt to cancel future ones).

UI: a "Make this recurring" option on session create (direct mode), plus a small series management panel in campaign settings (list series, edit cadence/end, deactivate).

Reminders: materialized sessions are ordinary confirmed sessions, so the existing reminder poller (poll_session_reminders, offsets from campaign.reminder_offsets_minutes or [7*24*60, 24*60, 60], reminder_tasks.py:1054; idempotency via SessionReminderSent) picks them up with no special-casing.

Out of scope

  • Vote-mode recurrence (direct/tentative only initially).
  • Automatic skip on holidays / attendance-aware skipping.
  • Cross-campaign series.

Acceptance criteria

  • Creating a weekly series materializes the next N occurrences exactly once; rerunning the task creates no duplicates (idempotency test).
  • Cancelling one occurrence leaves the rest of the series intact; detaching removes the link without deleting the session.
  • Series edit prompts and applies only to the chosen scope (future unmaterialized vs also future materialized).
  • Reminders fire for materialized sessions via the existing poller with no code changes to reminder logic.
  • DST boundary test: a weekly 19:00 campaign-time series keeps 19:00 wall-clock across a DST transition.

References

  • webapp/backend/app/models/session.py (SchedulingMode 47-52, SessionStatus 55-62, confirmed_time/end_time 89-92)
  • webapp/backend/app/tasks/reminder_tasks.py:998 (poll_session_reminders), :1054 (offsets), :1145 (auto_complete_sessions — Beat task pattern to follow)
  • webapp/backend/app/models/session_reminder_sent.py (reminder idempotency pattern to mirror for materialization)
  • webapp/backend/app/models/campaign.py:41 (timezone)

Filed from the July 2026 full-project review.

## Context / Motivation Weekly groups are the core Quest Board user, yet every session is created one-off today: `models/session.py` has no series concept — just `scheduling_mode` (`vote`/`direct`/`tentative`, `SchedulingMode` enum lines 47-52), `status` (`proposed`/`confirmed`/`in_progress`/`completed`/`cancelled`), `confirmed_time`/`end_time` UTC columns. A GM running "every Tuesday at 19:00" recreates the session by hand every week. ## Spec **Data model** — new `SessionSeries`: - `id`, `campaign_id` (FK, CASCADE) - `cadence`: `weekly` | `biweekly` | `monthly_by_weekday` (e.g. "2nd Tuesday") - `weekday` + `time_of_day` interpreted in the **campaign timezone** (`campaign.timezone`, `models/campaign.py:41`) so DST shifts keep the wall-clock time stable - `start_date`; end condition: `until_date` | `count` | open-ended (nullable pair) - `default_duration_minutes` (materialized into `end_time`); reminders inherit from the campaign's `reminder_offsets_minutes` — no per-series reminder config - `title_template` (optional), `created_by`, timestamps, `active` flag - `sessions.series_id` (nullable FK, SET NULL) linking materialized occurrences **Materialization** — a Celery Beat task (alongside `poll_session_reminders` / `auto_complete_sessions` in `webapp/backend/app/tasks/reminder_tasks.py:998/:1145`) materializes concrete sessions a rolling window ahead (default 4 weeks) as **normal direct-mode sessions** with `status=confirmed`, linked by `series_id`. Idempotent: an occurrence key (series_id + occurrence date) must guarantee exactly-once creation even if the task overlaps or reruns. **Edit semantics**: - Per-occurrence edits (reschedule / cancel / detach from series) operate on the materialized session row only and never rewrite the series definition. A detached session keeps its data, drops `series_id`. - Editing the series affects only future **unmaterialized** occurrences by default; the UI prompts the GM whether to also update future materialized-but-unmodified occurrences. - Deleting/deactivating a series stops materialization; existing sessions remain (prompt to cancel future ones). **UI**: a "Make this recurring" option on session create (direct mode), plus a small series management panel in campaign settings (list series, edit cadence/end, deactivate). **Reminders**: materialized sessions are ordinary confirmed sessions, so the existing reminder poller (`poll_session_reminders`, offsets from `campaign.reminder_offsets_minutes or [7*24*60, 24*60, 60]`, reminder_tasks.py:1054; idempotency via `SessionReminderSent`) picks them up with **no special-casing**. ## Out of scope - Vote-mode recurrence (direct/tentative only initially). - Automatic skip on holidays / attendance-aware skipping. - Cross-campaign series. ## Acceptance criteria - Creating a weekly series materializes the next N occurrences exactly once; rerunning the task creates no duplicates (idempotency test). - Cancelling one occurrence leaves the rest of the series intact; detaching removes the link without deleting the session. - Series edit prompts and applies only to the chosen scope (future unmaterialized vs also future materialized). - Reminders fire for materialized sessions via the existing poller with no code changes to reminder logic. - DST boundary test: a weekly 19:00 campaign-time series keeps 19:00 wall-clock across a DST transition. ## References - `webapp/backend/app/models/session.py` (`SchedulingMode` 47-52, `SessionStatus` 55-62, `confirmed_time`/`end_time` 89-92) - `webapp/backend/app/tasks/reminder_tasks.py:998` (`poll_session_reminders`), `:1054` (offsets), `:1145` (`auto_complete_sessions` — Beat task pattern to follow) - `webapp/backend/app/models/session_reminder_sent.py` (reminder idempotency pattern to mirror for materialization) - `webapp/backend/app/models/campaign.py:41` (`timezone`) _Filed from the July 2026 full-project review._
rbrooks referenced this issue from a commit 2026-07-17 22:29:39 +00:00
Sign in to join this conversation.
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
rbrooks/Quest-Board#98
No description provided.