feat(saved-views): scope saved views by module source (backend)
Saved "smart views" were feed-only; the Plex module is getting them too. Add a
`source` discriminator to saved_views ("feed" | "plex"), default "feed" so every
existing row and any source-less client stays feed. Widen the per-user unique
name constraint to (user_id, source, name) so a feed view and a Plex view can
share a name, and scope create's position numbering + the is_default "clear the
others" to (user, source).
Migration 0073 (ADD source NOT NULL server_default 'feed' + swap the unique
constraint; reversible). Endpoints: GET/POST take `source` (default feed).
Tests: source isolation, per-source default, per-source name uniqueness.
This commit is contained in:
@@ -9,11 +9,19 @@ from app.db import get_db
|
||||
from app.models import SavedView, User
|
||||
from app.routes._common import owned_or_404
|
||||
|
||||
# Saved "smart views": per-user named snapshots of the feed's FeedFilters. Gated behind
|
||||
# require_human so the shared demo account can't pollute a communal list (mirrors how other
|
||||
# personal-state writes are scoped).
|
||||
# Saved "smart views": per-user named snapshots of a module's filters (feed or plex, keyed by
|
||||
# `source`). Gated behind require_human so the shared demo account can't pollute a communal list
|
||||
# (mirrors how other personal-state writes are scoped).
|
||||
router = APIRouter(prefix="/api/saved-views", tags=["saved-views"])
|
||||
|
||||
# The modules that own saved views. A view is scoped to exactly one; the default is "feed" so a caller
|
||||
# (or an older client) that omits `source` keeps the original feed behaviour.
|
||||
_SOURCES = {"feed", "plex"}
|
||||
|
||||
|
||||
def _norm_source(source: str) -> str:
|
||||
return source if source in _SOURCES else "feed"
|
||||
|
||||
|
||||
def _serialize(v: SavedView) -> dict:
|
||||
return {
|
||||
@@ -27,12 +35,15 @@ def _serialize(v: SavedView) -> dict:
|
||||
|
||||
@router.get("")
|
||||
def list_views(
|
||||
user: User = Depends(require_human), db: Session = Depends(get_db)
|
||||
source: str = "feed",
|
||||
user: User = Depends(require_human),
|
||||
db: Session = Depends(get_db),
|
||||
) -> list[dict]:
|
||||
source = _norm_source(source)
|
||||
rows = (
|
||||
db.execute(
|
||||
select(SavedView)
|
||||
.where(SavedView.user_id == user.id)
|
||||
.where(SavedView.user_id == user.id, SavedView.source == source)
|
||||
.order_by(SavedView.position, SavedView.id)
|
||||
)
|
||||
.scalars()
|
||||
@@ -44,6 +55,7 @@ def list_views(
|
||||
class CreateViewIn(BaseModel):
|
||||
name: str
|
||||
filters: dict
|
||||
source: str = "feed"
|
||||
|
||||
|
||||
@router.post("")
|
||||
@@ -55,13 +67,19 @@ def create_view(
|
||||
name = body.name.strip()
|
||||
if not name:
|
||||
raise HTTPException(status_code=400, detail="name is required")
|
||||
source = _norm_source(body.source)
|
||||
# Position is per (user, source), so each module's views number 0..N independently.
|
||||
maxpos = db.execute(
|
||||
select(func.coalesce(func.max(SavedView.position), -1)).where(
|
||||
SavedView.user_id == user.id
|
||||
SavedView.user_id == user.id, SavedView.source == source
|
||||
)
|
||||
).scalar_one()
|
||||
view = SavedView(
|
||||
user_id=user.id, name=name[:80], filters=body.filters, position=maxpos + 1
|
||||
user_id=user.id,
|
||||
source=source,
|
||||
name=name[:80],
|
||||
filters=body.filters,
|
||||
position=maxpos + 1,
|
||||
)
|
||||
db.add(view)
|
||||
try:
|
||||
@@ -107,10 +125,14 @@ def update_view(
|
||||
if "is_default" in sent:
|
||||
make_default = bool(body.is_default)
|
||||
if make_default:
|
||||
# At most one default per user: clear the flag on the others first.
|
||||
# At most one default per (user, source): clear the flag on this module's others first.
|
||||
db.execute(
|
||||
update(SavedView)
|
||||
.where(SavedView.user_id == user.id, SavedView.id != view.id)
|
||||
.where(
|
||||
SavedView.user_id == user.id,
|
||||
SavedView.source == view.source,
|
||||
SavedView.id != view.id,
|
||||
)
|
||||
.values(is_default=False)
|
||||
)
|
||||
view.is_default = make_default
|
||||
|
||||
Reference in New Issue
Block a user