Skip to content

feat/structured experience tables - #51

Merged
ethnjs merged 34 commits into
mainfrom
feat/structured-experience-tables
Jul 25, 2026
Merged

feat/structured experience tables#51
ethnjs merged 34 commits into
mainfrom
feat/structured-experience-tables

Conversation

@ethnjs

@ethnjs ethnjs commented Jul 25, 2026

Copy link
Copy Markdown
Owner

Background

Free-text competition_exp and volunteer_exp columns on User can't be searched or filtered. TDs need to find volunteers by event experience. This replaces the blobs with structured entries that support filtering by event and expertise.

Changes

Canonical Event / EventCategory catalog

Note: this is a different concept from TournamentEvent (a per-tournament event instance with room/building/volunteers-needed, renamed from the old tournament-scoped Event in this same commit range). Event/EventCategory here is a global catalog used only for tagging user experience entries — don't confuse the two.

  • EventCategory: id, name (unique)
  • Event: id, name (unique), category_id FK to EventCategory (required)
  • Admin-managed CRUD for both: GET/POST/PATCH/DELETE /events/{event_id}/, GET/POST/PATCH/DELETE /event-categories/{category_id}/
  • DELETE is a hard delete on both, blocked with 409 if the row is still referenced (an event with experience entries, or a category with events)
  • Canonical seed data loaded on startup (app/db/seed_canon_events.py, idempotent INSERT ... ON CONFLICT DO NOTHING)
  • Routers registered, tests added (test_events.py)

UserCompetitionExperience table

  • Model: id, user_id FK, event_id FK, school, notes
  • Self-service routes under /users/me/competition-experience/: POST, PATCH /{id}/, DELETE /{id}/
  • Permission check restricts edits to the user's own entries (404 if not the owner)
  • No list/filter endpoint yet — filtering by event is not implemented in this PR

UserVolunteerExperience table

  • Model: id, user_id FK, tournament_name (required), year (required), event_id FK (nullable — some roles are tournament-wide staff not tied to a specific event), role (bounded string), notes (JSON: {event, other})
  • Manual entry only — no link to TournamentMembership/Tournament; auto-populating entries from archived NEXUS tournaments is future work, not part of this PR
  • Validator enforces exactly one of event_id or notes.event (a free-text event name) is set
  • Self-service routes under /users/me/volunteer-experience/: POST, PATCH /{id}/, DELETE /{id}/, same 404-on-not-owner check as competition experience

User model changes

  • Added has_competition_experience and has_volunteer_experience (Boolean, nullable=True) — null=unanswered, False=explicitly none, True=claims entries exist
  • Dropped competition_exp and volunteering_exp (Text columns) — confirmed no real data existed before dropping
  • Profile-completeness logic split by response shape, computed via helper functions in app/core/profile_status.py (not a stored property):
    • compute_missing_profile_fields(user)list[str], used by UserMeFullResponse.missing_profile_fields
    • is_profile_complete(user)bool, used by UserMeSlimResponse.is_profile_complete
    • Both apply the same three-state logic: has_*_experience is None → incomplete; False → skipped (explicitly none); True with no matching entries → still incomplete
  • Migration covers all of the above

Reads

  • GET /users/me/?full=true returns UserMeFullResponse with nested competition_experience/volunteer_experience, eagerly loaded via selectinload
  • GET /admin/users/{user_id}/ (admin) returns the same nested lists via UserFullResponse, but lazy-loaded (not eager)
  • Breaking change: GET /auth/me/ was removed and folded into GET /users/me/ to reduce ambiguity — any remaining callers need to migrate
  • Bundled bugfix: check_if_email_exists now excludes the current user's own row, fixing a 409 that fired when a user PATCHed their profile without changing their email

Also included — frontend wiring

This range also sweeps in frontend commits that support the tables above and were developed alongside them, not called out as separate tasks in the original issue:

  • New Combobox and RadioGroup components (used for event/school selection on experience entries)
  • Frontend API interfaces and useAuth updated to match the new UserMeSlimResponse/UserMeFullResponse split
  • Birthdate and pronoun fields added to sign-up (unrelated to experience tables, but landed in this same commit stretch)
  • Minor fixes: phone formatting on sign-up (backspace across separators), combobox error-display timing, dashboard API-shape updates, API router doc tags

Also included — tournament membership age verification

This range also includes a related-but-separate feature that landed in the same commit stretch: age verification on TournamentMembership.

  • is_over_18 / is_over_21 (hybrid properties, computed from birthdate) added to TournamentMembership, so TDs can age-verify registered volunteers
  • Membership renamed to TournamentMembership; temporary profile fields that had been stored there removed (superseded by the User-level fields in this PR)
  • Migration removing those temp tournament-membership profile fields

Testing

  • Full backend test suite, including new test_events, competition/volunteer experience route tests, and profile-completeness coverage for all three boolean states across both Slim and Full responses
  • Manually verified ?full=true returns nested experience lists correctly
  • Manually verified the event_id/notes.event exactly-one-of validator
  • Confirmed branch builds/runs in isolation

Notes

  • event_id on UserVolunteerExperience is nullable — some roles are tournament-wide staff not tied to a specific event
  • Expertise and role fields were dropped from competition experience after design review — entries are event + school + notes only
  • Age-verification changes to TournamentMembership (see above) are unrelated to this issue's scope but included in this PR for commit-grouping reasons
  • Birthdate/pronoun sign-up fields and the combobox/radio-group components (see "Also included — frontend wiring" above) are likewise unrelated to this issue's scope but included for the same reason

ethnjs added 30 commits July 25, 2026 15:41
…able name from Event to TournamentEvent and Membership to TournamentMembership
…'s past school and event; updated migrations
…ually added past tournaments and NEXUS tournamens; updated migrations
…since TournamentMembership can be used to auto-populate a user's profile later
…Membership so that TDs can age verify their registered volunteers
…ment_memberships; remove temp profile fields that were being stored in tournament memberships
… exp to user full response; updated missing profile fields logic to check for empty user exp lists
…ema; moved student/employer, exp, shirt, and dietary fields from slim to full
…ields while FullResponse returns the list; missing profile fields is computed in the routes via helper functions
… routes; update TestMissingProfileFields; moved test suite from SQLite to a dedicated Postgres database
…ubmitted their own unchanged email because check_if_email_exists still checks their own email; added verify email tets; added admin permission tests on users route
…mbiguity; GET /users/me/ supports param ?full=true to return UserMeFullResponse
…leted /auth/me/; added tests to cover the ?full=true param on /users/me/
…ed all POST PATCH DELETE routes for both competition and volunteer user experience
…red event helper fixtures; refactored test_events to use the new event fixtures
…fields; extract profile questions; create new combobox component; create new radio group component to handle groups of radio options for a question
ethnjs added 3 commits July 25, 2026 15:41
…ove gap between input and dropdown; for custom allowed combobox custom option shows at bottom of dropdown
@ethnjs ethnjs linked an issue Jul 25, 2026 that may be closed by this pull request
19 tasks
@vercel

vercel Bot commented Jul 25, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
nexus Error Error Jul 25, 2026 10:58pm

@railway-app
railway-app Bot temporarily deployed to nexus / nexus-pr-51 July 25, 2026 22:48 Destroyed
@railway-app

railway-app Bot commented Jul 25, 2026

Copy link
Copy Markdown

🚅 Deployed to the nexus-pr-51 environment in nexus

Service Status Web Updated (UTC)
nexus ✅ Success (View Logs) Jul 25, 2026 at 11:01 pm

@railway-app
railway-app Bot temporarily deployed to nexus / nexus-pr-51 July 25, 2026 22:57 Destroyed
@ethnjs
ethnjs merged commit f966638 into main Jul 25, 2026
2 of 4 checks passed
@ethnjs
ethnjs deleted the feat/structured-experience-tables branch July 25, 2026 23:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: structured competition and volunteer experience

1 participant