feat: add iCal calendar subscriptions
ci / gitleaks (push) Successful in 56s
ci / docker (push) Successful in 5m55s

This commit is contained in:
2026-09-20 16:37:43 +08:00
parent af52fe0cad
commit f3ad1eec03
25 changed files with 1108 additions and 41 deletions
@@ -1,15 +1,15 @@
"""tombstone for reverted calendar subscriptions
"""restore calendar subscriptions after the reverted release
Revision ID: 0020_calendar_subscriptions
Revises: 0019_backup_imports
The calendar subscription feature (backend/calendar.py, backend/calendar_router.py)
was reverted from main, but this revision id was already applied to the production
database. Keeping an empty migration under the same revision id lets
`alembic upgrade head` succeed on databases stamped at this revision.
The original revision reached production before the feature was reverted. Existing
databases may therefore already contain the table while fresh databases do not.
Keep the revision id and make the schema operation idempotent for both cases.
"""
from alembic import op # noqa: F401
import sqlalchemy as sa
from alembic import op
revision = "0020_calendar_subscriptions"
down_revision = "0019_backup_imports"
@@ -18,11 +18,35 @@ depends_on = None
def upgrade() -> None:
# Intentionally empty: the feature was reverted. The leftover
# calendar_subscriptions table on already-migrated databases is harmless
# and intentionally left in place.
pass
bind = op.get_bind()
if "calendar_subscriptions" in sa.inspect(bind).get_table_names():
return
op.create_table(
"calendar_subscriptions",
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("user_id", sa.Uuid(), nullable=False),
sa.Column("name", sa.String(length=120), nullable=False),
sa.Column("url", sa.Text(), nullable=False),
sa.Column("color", sa.String(length=32), nullable=False),
sa.Column("enabled", sa.Boolean(), nullable=False),
sa.Column("ics_cache", sa.Text(), nullable=True),
sa.Column("etag", sa.String(length=512), nullable=True),
sa.Column("last_modified", sa.String(length=512), nullable=True),
sa.Column("refreshed_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("last_error", sa.Text(), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(
"ix_calendar_subscriptions_user_id", "calendar_subscriptions", ["user_id"]
)
def downgrade() -> None:
pass
bind = op.get_bind()
if "calendar_subscriptions" not in sa.inspect(bind).get_table_names():
return
op.drop_index("ix_calendar_subscriptions_user_id", table_name="calendar_subscriptions")
op.drop_table("calendar_subscriptions")