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>
117 lines
4.0 KiB
Python
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')",
|
|
)
|