Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
102 commits
Select commit Hold shift + click to select a range
d673970
asdfasdfasdf
nchen0124 Apr 7, 2026
290c50d
Merge branch 'staging' of https://github.com/ethnjs/nexus into feat/e…
ethnjs Apr 7, 2026
8989e70
Merge branch 'staging' of https://github.com/ethnjs/nexus into feat/e…
ethnjs Apr 12, 2026
dbf9ad9
feat: add alembic migration for time_blocks, categories, and membersh…
ethnjs Apr 15, 2026
ea4d362
fix: remove legacy blocks field and add missing imports in tournament…
ethnjs Apr 15, 2026
7b154ae
test: add test_time_blocks.py and fix conftest tournament fixtures
ethnjs Apr 15, 2026
251d589
test: add test_categories.py and update conftest with category seeding
ethnjs Apr 15, 2026
219fe62
test: update test_events.py for new schema
ethnjs Apr 15, 2026
0e19ebb
refactor: make manage_events explicit for tournament_director and sim…
ethnjs Apr 15, 2026
fe15acf
test: update test_tournaments.py for new schema
ethnjs Apr 15, 2026
e8255c9
fix: remove assigned_event_id and update ScheduleSlot to use time_blo…
ethnjs Apr 15, 2026
a9d0e4f
fix: update sync service and test_sync.py for time_blocks schema
ethnjs Apr 15, 2026
a180b01
fix: update init_db seed_dev_data for time_blocks and categories schema
ethnjs Apr 15, 2026
dbb11e8
fix: update test_sync_service.py mock to use time_blocks instead of b…
ethnjs Apr 15, 2026
158b0d7
feat: add overlap validation to time blocks
ethnjs Apr 15, 2026
4a75a45
feat: update api.ts types for events/time-blocks/categories schema
ethnjs Apr 15, 2026
6fd0fba
feat: add event/category/division color tokens to globals.css
ethnjs Apr 15, 2026
8c67d84
feat: build events page shell with tabs, import bar, and data fetching
ethnjs Apr 15, 2026
7d20127
refactor: extract time/date/color helpers into lib/formatters.ts
ethnjs Apr 15, 2026
32f0e04
fix: make time_blocks migration idempotent
ethnjs Apr 16, 2026
87cf4f0
fix: handle raw JSON strings in migration backfill
ethnjs Apr 16, 2026
229516b
fix: use batch_alter_table for SQLite-compatible column changes
ethnjs Apr 16, 2026
efe80e5
fix: remove inline ForeignKey from batch_alter_table add_column calls
ethnjs Apr 16, 2026
e56f5f7
feat: add TimeBlocksTable component and wire into events page
ethnjs Apr 16, 2026
b789a35
feat: add TimeBlocksTable component and wire into events page
ethnjs Apr 16, 2026
bd88ced
feat: add inline add/edit row to TimeBlocksTable
ethnjs Apr 16, 2026
cd4a97c
fix: stabilize TimeBlocksTable column widths and inline row appearance
ethnjs Apr 16, 2026
77f240c
feat: add DeleteBlockModal and wire delete flow in events page
ethnjs Apr 16, 2026
bc93229
feat: add EventSidePanel and wire event CRUD into events page
ethnjs Apr 16, 2026
3f3ff1c
refactor: consolidate icons into Icons.tsx and add IconUpload to shee…
ethnjs Apr 16, 2026
44894a6
fix: update IconUpload to match IconExport style
ethnjs Apr 16, 2026
e99236d
feat: inline category create in EventSidePanel
ethnjs Apr 16, 2026
21f1944
feat: add EventCard component
ethnjs Apr 16, 2026
4b26803
feat: add EventCardGrid component and wire Cards tab
ethnjs Apr 16, 2026
426471e
fix: ui polish on events cards tab and side panel
ethnjs Apr 16, 2026
dc5fe45
feat: seed 2026 Div B/C Science Olympiad events in dev db
ethnjs Apr 16, 2026
550938a
fix: guard against undefined time_block_ids in TimeBlocksTable
ethnjs Apr 16, 2026
7527f73
feat: add EventTable with inline editing and Cards/Table view toggle
ethnjs Apr 16, 2026
f8d1c92
fix: guard against undefined time_block_ids in EventTable and EventSi…
ethnjs Apr 16, 2026
281af4c
feat: move Table view to its own top-level tab
ethnjs Apr 16, 2026
dbafcfc
feat: add EventTable tab with toolbar, fix row background colors
ethnjs Apr 16, 2026
3a2ff41
fix: sync time_block_ids from API and prevent table remount on updates
ethnjs Apr 16, 2026
cce583a
fix: use model_fields_set to allow clearing nullable event fields via…
ethnjs Apr 16, 2026
1b60fe6
fix: optimistic category selection and correct active highlight in dr…
ethnjs Apr 16, 2026
8abd67c
feat: make time blocks picker in table view multi-select
ethnjs Apr 16, 2026
7dbd1b9
fix: show only block label on event cards, remove date/time metadata
ethnjs Apr 16, 2026
764065e
feat: add panel open/close animations and single-event fetch on open
ethnjs Apr 16, 2026
396cb4a
feat: add EventChip component for Timeline view
ethnjs Apr 16, 2026
2a77253
fix: resolve Tournament type mismatch between useTournament and api
ethnjs Apr 16, 2026
8d73352
fix: stub out assigned_event_id cell until event assignment is implem…
ethnjs Apr 16, 2026
ca70875
fix: remove stale blocks field from NewTournamentModal create payload
ethnjs Apr 16, 2026
20fa51b
feat: add EventTimeline component with zoom, grouping, and color-by c…
ethnjs Apr 16, 2026
72a72d3
fix: timeline group labels, division format, category colors, and sti…
ethnjs Apr 16, 2026
1cacccd
fix: include year in date formatter constructors
ethnjs Apr 16, 2026
1468908
feat: move controls into sticky header and fix unscheduled subheader …
ethnjs Apr 16, 2026
f8ff7a0
feat: add CsvImportBar with import preview modal
ethnjs Apr 16, 2026
450f5e2
fix: use --color-surface for CSV import guide popover background
ethnjs Apr 16, 2026
9f84a20
feat: add parseApiError helper and wire into events error handling
ethnjs Apr 16, 2026
cc92833
fix: closed gap between unscheduled sticky subheader and the main header
ethnjs Apr 16, 2026
8d98ed8
feat: consolidate import/export into CsvImportBar with Import/Export …
ethnjs Apr 16, 2026
10d37b5
refactor: update display text for elements in the import/export csv bar
ethnjs Apr 16, 2026
83c948e
feat: added template icon to import csv dropdown for events
ethnjs Apr 16, 2026
db7d421
fix: use tournament name in export CSV filenames
ethnjs Apr 16, 2026
6f96647
feat: update favicon
ethnjs Apr 18, 2026
8811640
fix: remove overlap validation from time block create/update
ethnjs Apr 18, 2026
a4a09ee
feat: drop blocks column from events CSV import/export
ethnjs Apr 18, 2026
631f10f
feat: overlapping block timeline rendering and multi-select mode
ethnjs Apr 18, 2026
1603911
feat: implement multi-edit side panel for bulk event editing
ethnjs Apr 18, 2026
6fc784e
feat: add batch PATCH endpoint for events
ethnjs Apr 18, 2026
fe634c0
test: add batch PATCH endpoint tests for events
ethnjs Apr 18, 2026
c336118
feat: add batchUpdate to eventsApi client
ethnjs Apr 18, 2026
5bc06d9
feat(events): multi-edit panel with no-change sentinels and batch save
ethnjs Apr 18, 2026
94c7ff2
feat(events): add onFilteredIdsChange callback to EventCardGrid
ethnjs Apr 18, 2026
e855001
feat(events): select-all toolbar button and table header checkbox
ethnjs Apr 19, 2026
d1d585d
refactor: split seed_dev_data into granular seed functions with env v…
ethnjs Apr 19, 2026
2b31903
fix: include tournament_id in event create payload to resolve 422
ethnjs Apr 19, 2026
0dc596c
fix: reordered the event patch routes; put 'batch' before '{event_id}…
ethnjs Apr 19, 2026
22a09b9
fix: timeline sticky header x-lock
ethnjs Apr 19, 2026
1d88cdf
feat: smooth timeline date header scrolling with proper border clamp
ethnjs Apr 19, 2026
9fa6641
fix: show save-and-add button only in add event mode
ethnjs Apr 19, 2026
bbb7d51
fix: align event table header and improve horizontal layout
ethnjs Apr 19, 2026
335b98e
feat: add smooth clamped horizontal scroll animation to event table h…
ethnjs Apr 19, 2026
59631a8
refactor: use transform-based animation for event table header labels
ethnjs Apr 19, 2026
d1d516e
fix: render frozen-column divider as inset line in event table
ethnjs Apr 19, 2026
85d171d
fix: updated column widths for events table view
ethnjs Apr 19, 2026
e30d109
feat: add shared cross-view event filters with modal and faster tab s…
ethnjs Apr 19, 2026
72f1dc5
perf: virtualize event table rows and memoize heavy event view compon…
ethnjs Apr 19, 2026
685ec02
refactor: extract event filters panel and modernize shared events too…
ethnjs Apr 19, 2026
6c90601
feat(events): restyle filter panel as left side panel and fix toolbar…
ethnjs Apr 19, 2026
624ac50
fix(events): update filter panel inputs, chip placement, and panel di…
ethnjs Apr 19, 2026
f2e276e
fix(events): restyle event search input to match volunteers page
ethnjs Apr 19, 2026
3cb44aa
feat(events): add plus icon to Add event button
ethnjs Apr 19, 2026
4995f8f
fix(events): restrict building/block filter to known options and show…
ethnjs Apr 19, 2026
b596717
fix(events): keep building/block dropdown open after adding a tag
ethnjs Apr 19, 2026
402518e
fix: use page-level vertical scrolling for event table with sticky he…
ethnjs Apr 19, 2026
a094004
fix: add category rename support and clean up default category UX text
ethnjs Apr 19, 2026
bdc71df
test: add backend coverage for category rename endpoint and default-c…
ethnjs Apr 19, 2026
66ad2e8
test: remove obsolete time block overlap conflict tests
ethnjs Apr 19, 2026
d040be1
fix: keep delete block modal actions visible with many events
ethnjs Apr 19, 2026
ab3340c
feat: add demo user to init db
ethnjs Apr 26, 2026
646f09d
fix: remove duplicate index creation in time_blocks migration
ethnjs Apr 29, 2026
d36abba
refactor: replace create_all with alembic upgrade head in init_db
ethnjs Apr 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
"""Add time_blocks, tournament_categories, event_time_blocks, membership_events

Revision ID: 133d0c5a5ffe
Revises: 23ff6e84620b
Create Date: 2026-04-11

Migration sequence:
1. Create time_blocks, tournament_categories tables
2. Create event_time_blocks, membership_events association tables
3. Backfill time_blocks from tournaments.blocks JSON
4. Backfill event_time_blocks from events.blocks integer list
5. Add events.category_id, backfill from events.category string (find-or-create TournamentCategory)
6. Backfill membership_events from memberships.assigned_event_id
7. Migrate memberships.schedule JSON: replace block integers with time_block_ids
8. Drop legacy columns: tournaments.blocks, events.blocks, events.category, memberships.assigned_event_id
"""

from alembic import op
import sqlalchemy as sa
from sqlalchemy import text

# revision identifiers
revision = '133d0c5a5ffe'
down_revision = '23ff6e84620b'
branch_labels = None
depends_on = None


def _has_column(conn, table: str, column: str) -> bool:
insp = sa.inspect(conn)
return any(c['name'] == column for c in insp.get_columns(table))


def upgrade():
conn = op.get_bind()
insp = sa.inspect(conn)

# ------------------------------------------------------------------
# 1. Create time_blocks
# ------------------------------------------------------------------
if not insp.has_table('time_blocks'):
op.create_table(
'time_blocks',
sa.Column('id', sa.Integer(), primary_key=True, index=True, nullable=False),
sa.Column('tournament_id', sa.Integer(), sa.ForeignKey('tournaments.id', ondelete='CASCADE'), nullable=False),
sa.Column('label', sa.String(255), nullable=False),
sa.Column('date', sa.String(10), nullable=False),
sa.Column('start', sa.String(5), nullable=False),
sa.Column('end', sa.String(5), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
)

# ------------------------------------------------------------------
# 2. Create tournament_categories
# ------------------------------------------------------------------
if not insp.has_table('tournament_categories'):
op.create_table(
'tournament_categories',
sa.Column('id', sa.Integer(), primary_key=True, index=True, nullable=False),
sa.Column('tournament_id', sa.Integer(), sa.ForeignKey('tournaments.id', ondelete='CASCADE'), nullable=False),
sa.Column('name', sa.String(255), nullable=False),
sa.Column('is_custom', sa.Boolean(), nullable=False, server_default=sa.false()),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.UniqueConstraint('tournament_id', 'name', name='uq_tournament_category_name'),
)

# ------------------------------------------------------------------
# 3. Create event_time_blocks association table
# ------------------------------------------------------------------
if not insp.has_table('event_time_blocks'):
op.create_table(
'event_time_blocks',
sa.Column('event_id', sa.Integer(), sa.ForeignKey('events.id', ondelete='CASCADE'), primary_key=True, nullable=False),
sa.Column('time_block_id', sa.Integer(), sa.ForeignKey('time_blocks.id', ondelete='CASCADE'), primary_key=True, nullable=False),
)

# ------------------------------------------------------------------
# 4. Create membership_events association table
# ------------------------------------------------------------------
if not insp.has_table('membership_events'):
op.create_table(
'membership_events',
sa.Column('membership_id', sa.Integer(), sa.ForeignKey('memberships.id', ondelete='CASCADE'), primary_key=True, nullable=False),
sa.Column('event_id', sa.Integer(), sa.ForeignKey('events.id', ondelete='CASCADE'), primary_key=True, nullable=False),
)

# ------------------------------------------------------------------
# 5. Backfill time_blocks from tournaments.blocks JSON
# tournaments.blocks is a list of objects:
# [{"number": 1, "label": "Block 1", "date": "YYYY-MM-DD", "start": "HH:MM", "end": "HH:MM"}, ...]
# We build a mapping: (tournament_id, old_number) -> new time_block id
# ------------------------------------------------------------------
import json
from datetime import datetime, timezone

now = datetime.now(timezone.utc).replace(tzinfo=None)

# old_number_to_id[tournament_id][block_number] = new time_block id
old_number_to_id = {}

tournaments = conn.execute(text("SELECT id, blocks FROM tournaments WHERE blocks IS NOT NULL")).fetchall()
for t_id, blocks_json in tournaments:
if not blocks_json:
continue
if isinstance(blocks_json, str):
blocks_json = json.loads(blocks_json)
old_number_to_id[t_id] = {}
for block in blocks_json:
number = block.get("number")
label = block.get("label", f"Block {number}")
date = block.get("date", "")
start = block.get("start", "00:00")
end = block.get("end", "00:00")

result = conn.execute(
text(
"INSERT INTO time_blocks (tournament_id, label, date, start, end, created_at, updated_at) "
"VALUES (:tid, :label, :date, :start, :end, :now, :now) RETURNING id"
),
{"tid": t_id, "label": label, "date": date, "start": start, "end": end, "now": now},
)
new_id = result.fetchone()[0]
if number is not None:
old_number_to_id[t_id][number] = new_id

# ------------------------------------------------------------------
# 6. Backfill event_time_blocks from events.blocks (list of ints)
# ------------------------------------------------------------------
events = conn.execute(text("SELECT id, tournament_id, blocks FROM events WHERE blocks IS NOT NULL")).fetchall()
for e_id, t_id, blocks_json in events:
if not blocks_json:
continue
if isinstance(blocks_json, str):
blocks_json = json.loads(blocks_json)
t_map = old_number_to_id.get(t_id, {})
for block_num in blocks_json:
tb_id = t_map.get(block_num)
if tb_id is None:
continue
conn.execute(
text("INSERT INTO event_time_blocks (event_id, time_block_id) VALUES (:eid, :tbid) ON CONFLICT DO NOTHING"),
{"eid": e_id, "tbid": tb_id},
)

# ------------------------------------------------------------------
# 7. Add events.category_id and backfill from events.category string
# ------------------------------------------------------------------
if not _has_column(conn, 'events', 'category_id'):
with op.batch_alter_table('events') as batch_op:
batch_op.add_column(sa.Column('category_id', sa.Integer(), nullable=True))

# For each distinct (tournament_id, category) string pair, find-or-create a TournamentCategory row
rows = conn.execute(text(
"SELECT DISTINCT tournament_id, category FROM events WHERE category IS NOT NULL AND category != ''"
)).fetchall()

cat_cache = {} # (tournament_id, name) -> category_id
for t_id, cat_name in rows:
key = (t_id, cat_name)
if key in cat_cache:
continue
existing = conn.execute(
text("SELECT id FROM tournament_categories WHERE tournament_id = :tid AND name = :name"),
{"tid": t_id, "name": cat_name},
).fetchone()
if existing:
cat_cache[key] = existing[0]
else:
result = conn.execute(
text(
"INSERT INTO tournament_categories (tournament_id, name, is_custom, created_at) "
"VALUES (:tid, :name, TRUE, :now) RETURNING id"
),
{"tid": t_id, "name": cat_name, "now": now},
)
cat_cache[key] = result.fetchone()[0]

# Update events.category_id
for (t_id, cat_name), cat_id in cat_cache.items():
conn.execute(
text("UPDATE events SET category_id = :cid WHERE tournament_id = :tid AND category = :name"),
{"cid": cat_id, "tid": t_id, "name": cat_name},
)

# ------------------------------------------------------------------
# 8. Backfill membership_events from memberships.assigned_event_id
# ------------------------------------------------------------------
memberships = conn.execute(
text("SELECT id, assigned_event_id FROM memberships WHERE assigned_event_id IS NOT NULL")
).fetchall()
for m_id, e_id in memberships:
conn.execute(
text("INSERT INTO membership_events (membership_id, event_id) VALUES (:mid, :eid) ON CONFLICT DO NOTHING"),
{"mid": m_id, "eid": e_id},
)

# ------------------------------------------------------------------
# 9. Migrate memberships.schedule JSON:
# [{block: int, duty: str}] -> [{time_block_id: int, duty: str}]
# ------------------------------------------------------------------
memberships_with_schedule = conn.execute(
text("SELECT id, tournament_id, schedule FROM memberships WHERE schedule IS NOT NULL")
).fetchall()
for m_id, t_id, schedule_json in memberships_with_schedule:
if not schedule_json:
continue
if isinstance(schedule_json, str):
schedule_json = json.loads(schedule_json)
t_map = old_number_to_id.get(t_id, {})
new_schedule = []
changed = False
for entry in schedule_json:
block_num = entry.get("block")
if block_num is not None and block_num in t_map:
new_schedule.append({"time_block_id": t_map[block_num], "duty": entry.get("duty", "")})
changed = True
else:
new_schedule.append(entry)
if changed:
conn.execute(
text("UPDATE memberships SET schedule = :s WHERE id = :mid"),
{"s": json.dumps(new_schedule), "mid": m_id},
)

# ------------------------------------------------------------------
# 10. Drop legacy columns (batch mode required for SQLite)
# ------------------------------------------------------------------
if _has_column(conn, 'tournaments', 'blocks'):
with op.batch_alter_table('tournaments') as batch_op:
batch_op.drop_column('blocks')
if _has_column(conn, 'events', 'blocks') or _has_column(conn, 'events', 'category'):
with op.batch_alter_table('events') as batch_op:
if _has_column(conn, 'events', 'blocks'):
batch_op.drop_column('blocks')
if _has_column(conn, 'events', 'category'):
batch_op.drop_column('category')
if _has_column(conn, 'memberships', 'assigned_event_id'):
with op.batch_alter_table('memberships') as batch_op:
batch_op.drop_column('assigned_event_id')


def downgrade():
conn = op.get_bind()
insp = sa.inspect(conn)

# Re-add legacy columns (batch mode required for SQLite)
with op.batch_alter_table('memberships') as batch_op:
batch_op.add_column(sa.Column('assigned_event_id', sa.Integer(), nullable=True))
with op.batch_alter_table('events') as batch_op:
batch_op.add_column(sa.Column('category', sa.String(255), nullable=True))
batch_op.add_column(sa.Column('blocks', sa.JSON(), nullable=True))
with op.batch_alter_table('tournaments') as batch_op:
batch_op.add_column(sa.Column('blocks', sa.JSON(), nullable=True))

# Restore events.category from category_id
conn.execute(text("""
UPDATE events
SET category = (
SELECT tc.name FROM tournament_categories tc WHERE tc.id = events.category_id
)
WHERE events.category_id IS NOT NULL
"""))

if _has_column(conn, 'events', 'category_id'):
with op.batch_alter_table('events') as batch_op:
batch_op.drop_column('category_id')

# Drop new tables (cascade handles association tables)
if insp.has_table('membership_events'):
op.drop_table('membership_events')
if insp.has_table('event_time_blocks'):
op.drop_table('event_time_blocks')
if insp.has_table('tournament_categories'):
op.drop_table('tournament_categories')
if insp.has_table('time_blocks'):
op.drop_table('time_blocks')
Loading
Loading