UMBRA/backend/alembic/versions/054_event_invitations.py
Kyle Pope 8652c9f2ce Implement event invitation feature (invite, RSVP, per-occurrence override, leave)
Full-stack implementation of event invitations allowing users to invite connected
contacts to calendar events. Invitees can respond Going/Tentative/Declined, with
per-occurrence overrides for recurring series. Invited events appear on the invitee's
calendar with a Users icon indicator. LeaveEventDialog replaces delete for invited events.

Backend: Migration 054 (2 tables + notification types), EventInvitation model with
lazy="raise", service layer, dual-router (events + event-invitations), cascade on
disconnect, events/dashboard queries extended with OR for invited events.

Frontend: Types, useEventInvitations hook, InviteeSection (view list + RSVP buttons +
invite search), LeaveEventDialog, event invite toast with 3 response buttons, calendar
eventContent render with Users icon for invited events.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 02:47:27 +08:00

117 lines
4.0 KiB
Python

"""Event invitations tables and notification types.
Revision ID: 054
Revises: 053
"""
from alembic import op
import sqlalchemy as sa
revision = "054"
down_revision = "053"
branch_labels = None
depends_on = None
def upgrade():
# ── event_invitations table ──
op.create_table(
"event_invitations",
sa.Column("id", sa.Integer(), primary_key=True),
sa.Column(
"event_id",
sa.Integer(),
sa.ForeignKey("calendar_events.id", ondelete="CASCADE"),
nullable=False,
),
sa.Column(
"user_id",
sa.Integer(),
sa.ForeignKey("users.id", ondelete="CASCADE"),
nullable=False,
),
sa.Column(
"invited_by",
sa.Integer(),
sa.ForeignKey("users.id", ondelete="SET NULL"),
nullable=True,
),
sa.Column("status", sa.String(20), nullable=False, server_default="pending"),
sa.Column("invited_at", sa.DateTime(), nullable=False, server_default=sa.func.now()),
sa.Column("responded_at", sa.DateTime(), nullable=True),
sa.UniqueConstraint("event_id", "user_id", name="uq_event_invitations_event_user"),
sa.CheckConstraint(
"status IN ('pending', 'accepted', 'tentative', 'declined')",
name="ck_event_invitations_status",
),
)
op.create_index(
"ix_event_invitations_user_status",
"event_invitations",
["user_id", "status"],
)
op.create_index(
"ix_event_invitations_event_id",
"event_invitations",
["event_id"],
)
# ── event_invitation_overrides table ──
op.create_table(
"event_invitation_overrides",
sa.Column("id", sa.Integer(), primary_key=True),
sa.Column(
"invitation_id",
sa.Integer(),
sa.ForeignKey("event_invitations.id", ondelete="CASCADE"),
nullable=False,
),
sa.Column(
"occurrence_id",
sa.Integer(),
sa.ForeignKey("calendar_events.id", ondelete="CASCADE"),
nullable=False,
),
sa.Column("status", sa.String(20), nullable=False),
sa.Column("responded_at", sa.DateTime(), nullable=False, server_default=sa.func.now()),
sa.UniqueConstraint("invitation_id", "occurrence_id", name="uq_invitation_override"),
sa.CheckConstraint(
"status IN ('accepted', 'tentative', 'declined')",
name="ck_invitation_override_status",
),
)
op.create_index(
"ix_invitation_overrides_lookup",
"event_invitation_overrides",
["invitation_id", "occurrence_id"],
)
# ── Expand notification type check constraint ──
op.drop_constraint("ck_notifications_type", "notifications", type_="check")
op.create_check_constraint(
"ck_notifications_type",
"notifications",
"type IN ('connection_request', 'connection_accepted', 'connection_rejected', "
"'calendar_invite', 'calendar_invite_accepted', 'calendar_invite_rejected', "
"'event_invite', 'event_invite_response', "
"'info', 'warning', 'reminder', 'system')",
)
def downgrade():
op.drop_index("ix_invitation_overrides_lookup", table_name="event_invitation_overrides")
op.drop_table("event_invitation_overrides")
op.drop_index("ix_event_invitations_event_id", table_name="event_invitations")
op.drop_index("ix_event_invitations_user_status", table_name="event_invitations")
op.drop_table("event_invitations")
# Restore original notification type constraint
op.drop_constraint("ck_notifications_type", "notifications", type_="check")
op.create_check_constraint(
"ck_notifications_type",
"notifications",
"type IN ('connection_request', 'connection_accepted', 'connection_rejected', "
"'calendar_invite', 'calendar_invite_accepted', 'calendar_invite_rejected', "
"'info', 'warning', 'reminder', 'system')",
)