[DECOMPOSED] Admin schedule: data model + API for practices and events #230

Open
opened 2026-03-28 22:28:03 +00:00 by forgejo_admin · 3 comments

Type

Feature

Lineage

Standalone — scoped during admin interface review. No existing schedule entities in the domain model. Current schedule is hardcoded HTML in westside-app.

Repo

forgejo_admin/basketball-api

User Story

story:WS-S13 As an admin, I want to view and manage the program schedule (practices, tournaments, games) so that I have a single source of truth for all planned activities instead of relying on hardcoded HTML or memory.

Context

The public schedule page (westside-app/src/routes/(public)/schedule/+page.svelte) has all schedule data hardcoded: 3 Kings tournaments, 5 Queens tournaments, 8 practice slots across travel and local teams. Marcus has no admin visibility — adding or changing anything requires a code deploy.

This ticket adds the backend data model and API endpoints to basketball-api. The admin frontend view (westside-app /admin/schedule) and the public page refactor are separate follow-up tickets.

Architecture decision: two tables, not one.
Practices are recurring weekly (defined by day-of-week + time), while events are date-specific (tournaments, games). These are fundamentally different data shapes. Two tables with clean non-nullable columns beats one table with a maze of nullable fields.

Data Model:

Enum: EventTypetournament, game, camp, tryout, other

Table: practice_schedules

Column Type Notes
id Integer PK
tenant_id FK tenants.id Standard tenant scope
team_id FK teams.id, nullable Null = program-wide (e.g., all travel teams)
division Enum(Division), nullable kings/queens. Null = both programs
label String(200) Display name: "Travel Teams", "17U Local Kings"
day_of_week Integer 0=Monday ... 6=Sunday
start_time String(10) "14:00" format
end_time String(10) "20:00" format
location String(300)
notes Text, nullable "weights", "optional"
is_active Boolean, default true Soft-disable without deleting
created_at DateTime server_default=func.now()

Table: events

Column Type Notes
id Integer PK
tenant_id FK tenants.id
title String(300) "Utah State Invitational"
event_type Enum(EventType)
division Enum(Division), nullable Null = all programs
start_date Date
end_date Date, nullable Null for single-day
location String(300), nullable
team_id FK teams.id, nullable Null = program-wide
parent_event_id FK events.id, nullable Games nest under tournaments
opponent String(200), nullable For game-type events
notes Text, nullable
created_at DateTime server_default=func.now()

API Endpoints (all admin-auth, tenant-scoped):

  • GET /admin/schedule — combined { practices: [...], events: [...] }
  • GET /admin/schedule/practices — list (filter: division, is_active)
  • POST /admin/schedule/practices — create
  • PUT /admin/schedule/practices/{id} — update
  • DELETE /admin/schedule/practices/{id} — delete
  • GET /admin/schedule/events — list (filter: division, event_type, upcoming)
  • POST /admin/schedule/events — create
  • PUT /admin/schedule/events/{id} — update
  • DELETE /admin/schedule/events/{id} — delete

Seed data from current hardcoded schedule:

  • Kings: 8 practice slots (Sun/Tue/Thu travel, Mon/Tue 17U local, Mon/Fri 16U local), 3 tournaments
  • Queens: 5 tournaments (practices TBD — "Contact Director Abbie Sa")

File Targets

Files to create or modify:

  • src/basketball_api/models.py — add EventType enum, PracticeSchedule model, Event model
  • src/basketball_api/routes/admin.py — add schedule CRUD endpoints
  • src/basketball_api/schemas.py — add Pydantic request/response schemas for practices and events
  • alembic/versions/xxx_add_schedule_tables.py — migration for both tables
  • scripts/seed_schedule.py or data migration — populate current schedule data

Files NOT to touch:

  • src/basketball_api/routes/public.py — public API is a separate concern
  • Any westside-app files — frontend is a follow-up ticket

Acceptance Criteria

  • Alembic migration creates practice_schedules and events tables
  • Models follow existing patterns (Mapped types, tenant FK, server_default timestamps)
  • GET /admin/schedule returns combined practices + events
  • CRUD endpoints work for both practices and events
  • Seed script populates all current hardcoded schedule data (8 practices, 8 tournaments)
  • Division filter works (kings-only, queens-only, all)
  • Existing tests still pass

Test Expectations

  • Unit tests: CRUD operations for PracticeSchedule and Event models
  • Integration tests: all 9 endpoints return correct status codes and response shapes
  • Tenant isolation: schedule data scoped to tenant
  • Filter tests: division filter, event_type filter, is_active filter
  • Run command: pytest tests/ -v

Constraints

  • Follow existing model patterns in models.py (Mapped types, enum classes, relationship style)
  • Follow existing route patterns in routes/admin.py (dependency injection, tenant scoping)
  • Use String(10) for time fields ("14:00" format) — avoids SQLAlchemy Time serialization complexity
  • Self-referencing FK on events (parent_event_id) for tournament→game nesting
  • No Season entity — out of scope, can be added later if needed

Checklist

  • PR opened
  • Tests pass
  • No unrelated changes
  • Migration tested (up and down)
  • project-westside-basketball — parent project
  • arch-domain-westside-basketball — domain model needs updating after merge (follow-up)
  • Follow-up: Admin schedule view in westside-app (/admin/schedule)
  • Follow-up: Public schedule page refactor to consume API
### Type Feature ### Lineage Standalone — scoped during admin interface review. No existing schedule entities in the domain model. Current schedule is hardcoded HTML in westside-app. ### Repo `forgejo_admin/basketball-api` ### User Story `story:WS-S13` As an admin, I want to view and manage the program schedule (practices, tournaments, games) so that I have a single source of truth for all planned activities instead of relying on hardcoded HTML or memory. ### Context The public schedule page (`westside-app/src/routes/(public)/schedule/+page.svelte`) has all schedule data hardcoded: 3 Kings tournaments, 5 Queens tournaments, 8 practice slots across travel and local teams. Marcus has no admin visibility — adding or changing anything requires a code deploy. This ticket adds the **backend data model and API endpoints** to `basketball-api`. The admin frontend view (westside-app `/admin/schedule`) and the public page refactor are separate follow-up tickets. **Architecture decision: two tables, not one.** Practices are recurring weekly (defined by day-of-week + time), while events are date-specific (tournaments, games). These are fundamentally different data shapes. Two tables with clean non-nullable columns beats one table with a maze of nullable fields. **Data Model:** **Enum: `EventType`** — `tournament`, `game`, `camp`, `tryout`, `other` **Table: `practice_schedules`** | Column | Type | Notes | |--------|------|-------| | id | Integer PK | | | tenant_id | FK tenants.id | Standard tenant scope | | team_id | FK teams.id, nullable | Null = program-wide (e.g., all travel teams) | | division | Enum(Division), nullable | kings/queens. Null = both programs | | label | String(200) | Display name: "Travel Teams", "17U Local Kings" | | day_of_week | Integer | 0=Monday ... 6=Sunday | | start_time | String(10) | "14:00" format | | end_time | String(10) | "20:00" format | | location | String(300) | | | notes | Text, nullable | "weights", "optional" | | is_active | Boolean, default true | Soft-disable without deleting | | created_at | DateTime | server_default=func.now() | **Table: `events`** | Column | Type | Notes | |--------|------|-------| | id | Integer PK | | | tenant_id | FK tenants.id | | | title | String(300) | "Utah State Invitational" | | event_type | Enum(EventType) | | | division | Enum(Division), nullable | Null = all programs | | start_date | Date | | | end_date | Date, nullable | Null for single-day | | location | String(300), nullable | | | team_id | FK teams.id, nullable | Null = program-wide | | parent_event_id | FK events.id, nullable | Games nest under tournaments | | opponent | String(200), nullable | For game-type events | | notes | Text, nullable | | | created_at | DateTime | server_default=func.now() | **API Endpoints** (all admin-auth, tenant-scoped): - `GET /admin/schedule` — combined `{ practices: [...], events: [...] }` - `GET /admin/schedule/practices` — list (filter: division, is_active) - `POST /admin/schedule/practices` — create - `PUT /admin/schedule/practices/{id}` — update - `DELETE /admin/schedule/practices/{id}` — delete - `GET /admin/schedule/events` — list (filter: division, event_type, upcoming) - `POST /admin/schedule/events` — create - `PUT /admin/schedule/events/{id}` — update - `DELETE /admin/schedule/events/{id}` — delete **Seed data** from current hardcoded schedule: - Kings: 8 practice slots (Sun/Tue/Thu travel, Mon/Tue 17U local, Mon/Fri 16U local), 3 tournaments - Queens: 5 tournaments (practices TBD — "Contact Director Abbie Sa") ### File Targets Files to create or modify: - `src/basketball_api/models.py` — add `EventType` enum, `PracticeSchedule` model, `Event` model - `src/basketball_api/routes/admin.py` — add schedule CRUD endpoints - `src/basketball_api/schemas.py` — add Pydantic request/response schemas for practices and events - `alembic/versions/xxx_add_schedule_tables.py` — migration for both tables - `scripts/seed_schedule.py` or data migration — populate current schedule data Files NOT to touch: - `src/basketball_api/routes/public.py` — public API is a separate concern - Any westside-app files — frontend is a follow-up ticket ### Acceptance Criteria - [ ] Alembic migration creates `practice_schedules` and `events` tables - [ ] Models follow existing patterns (Mapped types, tenant FK, server_default timestamps) - [ ] `GET /admin/schedule` returns combined practices + events - [ ] CRUD endpoints work for both practices and events - [ ] Seed script populates all current hardcoded schedule data (8 practices, 8 tournaments) - [ ] Division filter works (kings-only, queens-only, all) - [ ] Existing tests still pass ### Test Expectations - [ ] Unit tests: CRUD operations for PracticeSchedule and Event models - [ ] Integration tests: all 9 endpoints return correct status codes and response shapes - [ ] Tenant isolation: schedule data scoped to tenant - [ ] Filter tests: division filter, event_type filter, is_active filter - Run command: `pytest tests/ -v` ### Constraints - Follow existing model patterns in `models.py` (Mapped types, enum classes, relationship style) - Follow existing route patterns in `routes/admin.py` (dependency injection, tenant scoping) - Use `String(10)` for time fields ("14:00" format) — avoids SQLAlchemy Time serialization complexity - Self-referencing FK on events (`parent_event_id`) for tournament→game nesting - No Season entity — out of scope, can be added later if needed ### Checklist - [ ] PR opened - [ ] Tests pass - [ ] No unrelated changes - [ ] Migration tested (up and down) ### Related - `project-westside-basketball` — parent project - `arch-domain-westside-basketball` — domain model needs updating after merge (follow-up) - Follow-up: Admin schedule view in `westside-app` (`/admin/schedule`) - Follow-up: Public schedule page refactor to consume API
Author
Owner

Scope Review: NEEDS_REFINEMENT

Review note: review-627-2026-03-28
Ticket is well-structured with complete template and traceability, but has a critical data model ambiguity and exceeds decomposition limits.

Issues found:

  • Division enum mismatch (CRITICAL): Ticket says division uses "kings/queens" values, but Division enum in models.py is boys/girls. The InterestLead model uses a separate freetext String(20) "program" field for kings/queens. This is a design decision the agent cannot make.
  • File target invalid: src/basketball_api/schemas.py does not exist. Codebase defines all Pydantic schemas inline in route files.
  • Route file placement: admin.py is already 1048 lines. Codebase has precedent for separate route files (jersey.py, checkout.py, coaches_api.py). Consider routes/schedule.py.
  • Recommend decomposition via template-board — 7 AC, 9 endpoints, 2 models, migration, seed script, 5 discrete changes exceeds both the 3-thing limit and 5-minute rule. Split into 3 sub-tickets: (1) data model + migration, (2) API endpoints, (3) seed script.
## Scope Review: NEEDS_REFINEMENT Review note: `review-627-2026-03-28` Ticket is well-structured with complete template and traceability, but has a critical data model ambiguity and exceeds decomposition limits. **Issues found:** - **Division enum mismatch (CRITICAL):** Ticket says `division` uses "kings/queens" values, but `Division` enum in `models.py` is `boys/girls`. The `InterestLead` model uses a separate freetext `String(20)` "program" field for kings/queens. This is a design decision the agent cannot make. - **File target invalid:** `src/basketball_api/schemas.py` does not exist. Codebase defines all Pydantic schemas inline in route files. - **Route file placement:** `admin.py` is already 1048 lines. Codebase has precedent for separate route files (jersey.py, checkout.py, coaches_api.py). Consider `routes/schedule.py`. - **Recommend decomposition via template-board** — 7 AC, 9 endpoints, 2 models, migration, seed script, 5 discrete changes exceeds both the 3-thing limit and 5-minute rule. Split into 3 sub-tickets: (1) data model + migration, (2) API endpoints, (3) seed script.
Author
Owner

Scope Review: NEEDS_REFINEMENT → DECOMPOSED

Review note: review-627-2026-03-28

Findings & Decisions

  1. Division enum mismatch (CRITICAL): Ticket said "kings/queens" but Division enum is boys/girls. Decision: Reuse existing Division(boys/girls). Kings=boys, Queens=girls — the mapping is stable and matches how Team.division already works. UI displays program names ("Kings"/"Queens"), DB stores division.

  2. Invalid file target: schemas.py doesn't exist. Codebase defines Pydantic schemas inline in route files. Decision: Define schemas inline in routes/schedule.py.

  3. Route file placement: admin.py is 1048 lines. Decision: New routes/schedule.py with its own router, following precedent of jersey.py, checkout.py, coaches_api.py.

  4. Scope exceeds limits: 5 discrete changes, 9 endpoints, estimated 10-15 min. Decision: Decompose into 2 sub-tickets:

    • #231: Data model + migration + seed (models, alembic, seed script)
    • #232: API endpoints (routes/schedule.py with inline schemas, tests)

This issue is now the parent. Marking as DECOMPOSED.

## Scope Review: NEEDS_REFINEMENT → DECOMPOSED Review note: `review-627-2026-03-28` ### Findings & Decisions 1. **Division enum mismatch (CRITICAL):** Ticket said "kings/queens" but `Division` enum is `boys/girls`. **Decision:** Reuse existing `Division(boys/girls)`. Kings=boys, Queens=girls — the mapping is stable and matches how `Team.division` already works. UI displays program names ("Kings"/"Queens"), DB stores division. 2. **Invalid file target:** `schemas.py` doesn't exist. Codebase defines Pydantic schemas inline in route files. **Decision:** Define schemas inline in `routes/schedule.py`. 3. **Route file placement:** `admin.py` is 1048 lines. **Decision:** New `routes/schedule.py` with its own router, following precedent of `jersey.py`, `checkout.py`, `coaches_api.py`. 4. **Scope exceeds limits:** 5 discrete changes, 9 endpoints, estimated 10-15 min. **Decision:** Decompose into 2 sub-tickets: - **#231**: Data model + migration + seed (models, alembic, seed script) - **#232**: API endpoints (routes/schedule.py with inline schemas, tests) This issue is now the parent. Marking as DECOMPOSED.
forgejo_admin changed title from Admin schedule view: data model + API endpoints for practices and events to [DECOMPOSED] Admin schedule: data model + API for practices and events 2026-03-28 22:37:27 +00:00
Author
Owner

Correction: Sub-tickets are #232 (data model) and #233 (API endpoints), not #231/#232 as stated above.

**Correction:** Sub-tickets are #232 (data model) and #233 (API endpoints), not #231/#232 as stated above.
Sign in to join this conversation.
No milestone
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
forgejo_admin/basketball-api#230
No description provided.