- New Calendar model and calendars table with system/default flags - Alembic migration 006: creates calendars, seeds Personal+Birthdays, migrates existing events - CalendarEvent model gains calendar_id FK and calendar_name/calendar_color properties - Updated CalendarEventCreate/Response schemas to include calendar fields - New /api/calendars CRUD router (blocks system calendar deletion/rename) - Events router: selectinload on all queries, default-calendar assignment on POST, virtual birthday event generation from People with birthdays when Birthdays calendar is visible Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
90 lines
2.7 KiB
Python
90 lines
2.7 KiB
Python
"""Add calendars table and calendar_id to calendar_events
|
|
|
|
Revision ID: 006
|
|
Revises: 005
|
|
Create Date: 2026-02-21 00:00:00.000000
|
|
|
|
"""
|
|
from typing import Sequence, Union
|
|
|
|
from alembic import op
|
|
import sqlalchemy as sa
|
|
from sqlalchemy.sql import table, column
|
|
|
|
|
|
# revision identifiers, used by Alembic.
|
|
revision: str = '006'
|
|
down_revision: Union[str, None] = '005'
|
|
branch_labels: Union[str, Sequence[str], None] = None
|
|
depends_on: Union[str, Sequence[str], None] = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
# 1. Create the calendars table
|
|
op.create_table(
|
|
'calendars',
|
|
sa.Column('id', sa.Integer(), primary_key=True, index=True),
|
|
sa.Column('name', sa.String(100), nullable=False),
|
|
sa.Column('color', sa.String(20), nullable=False),
|
|
sa.Column('is_default', sa.Boolean(), nullable=False, server_default='false'),
|
|
sa.Column('is_system', sa.Boolean(), nullable=False, server_default='false'),
|
|
sa.Column('is_visible', sa.Boolean(), nullable=False, server_default='true'),
|
|
sa.Column('created_at', sa.DateTime(), server_default=sa.func.now()),
|
|
sa.Column('updated_at', sa.DateTime(), server_default=sa.func.now()),
|
|
)
|
|
|
|
# 2. Seed the two default calendars
|
|
calendars_table = table(
|
|
'calendars',
|
|
column('name', sa.String),
|
|
column('color', sa.String),
|
|
column('is_default', sa.Boolean),
|
|
column('is_system', sa.Boolean),
|
|
column('is_visible', sa.Boolean),
|
|
)
|
|
|
|
op.bulk_insert(calendars_table, [
|
|
{
|
|
'name': 'Personal',
|
|
'color': '#3b82f6',
|
|
'is_default': True,
|
|
'is_system': False,
|
|
'is_visible': True,
|
|
},
|
|
{
|
|
'name': 'Birthdays',
|
|
'color': '#ec4899',
|
|
'is_default': False,
|
|
'is_system': True,
|
|
'is_visible': True,
|
|
},
|
|
])
|
|
|
|
# 3. Add calendar_id as nullable first so existing rows don't violate NOT NULL
|
|
op.add_column('calendar_events', sa.Column('calendar_id', sa.Integer(), nullable=True))
|
|
|
|
# 4. Set all existing events to the Personal (default) calendar
|
|
op.execute(
|
|
"""
|
|
UPDATE calendar_events
|
|
SET calendar_id = (SELECT id FROM calendars WHERE is_default = true LIMIT 1)
|
|
WHERE calendar_id IS NULL
|
|
"""
|
|
)
|
|
|
|
# 5. Alter to NOT NULL and add FK constraint
|
|
op.alter_column('calendar_events', 'calendar_id', nullable=False)
|
|
op.create_foreign_key(
|
|
'fk_calendar_events_calendar_id',
|
|
'calendar_events',
|
|
'calendars',
|
|
['calendar_id'],
|
|
['id'],
|
|
)
|
|
|
|
|
|
def downgrade() -> None:
|
|
op.drop_constraint('fk_calendar_events_calendar_id', 'calendar_events', type_='foreignkey')
|
|
op.drop_column('calendar_events', 'calendar_id')
|
|
op.drop_table('calendars')
|