feat(views): saved-views backend — table, model, CRUD + reorder API

Per-user named snapshots of the feed's FeedFilters. New saved_views table
(migration 0035, unique per user+name, position, is_default) + SavedView
model, and a /api/saved-views router (list/create/patch/delete/reorder)
gated behind require_human so the shared demo account can't pollute it.
Setting is_default clears the flag on the user's other views (one default).
This commit is contained in:
npeter83 2026-07-01 03:17:36 +02:00
parent 669994cc85
commit 3056734231
4 changed files with 235 additions and 0 deletions

View file

@ -0,0 +1,59 @@
"""per-user saved smart views
Revision ID: 0035_saved_views
Revises: 0034_blocked_channels
Create Date: 2026-07-01
Adds the `saved_views` table: per-user named snapshots of the feed's filter/sort state. Each
row stores the serialized FeedFilters (JSON), a sidebar `position` for drag-reorder, and an
`is_default` flag (at most one per user, enforced by the API) for the view applied on load.
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
revision: str = "0035_saved_views"
down_revision: Union[str, None] = "0034_blocked_channels"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.create_table(
"saved_views",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("user_id", sa.Integer(), nullable=False),
sa.Column("name", sa.String(length=80), nullable=False),
sa.Column("filters", sa.JSON(), nullable=False),
sa.Column(
"position", sa.Integer(), server_default=sa.text("0"), nullable=False
),
sa.Column(
"is_default",
sa.Boolean(),
server_default=sa.text("false"),
nullable=False,
),
sa.Column(
"created_at",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
sa.Column(
"updated_at",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("user_id", "name", name="uq_user_saved_view"),
)
op.create_index("ix_saved_views_user_id", "saved_views", ["user_id"])
def downgrade() -> None:
op.drop_index("ix_saved_views_user_id", table_name="saved_views")
op.drop_table("saved_views")