refactor(api): Pydantic request models for channels routes (R6 S1b)
update_channel: payload:dict → ChannelUpdateIn (priority/hidden/deep_requested, all optional for PATCH semantics). Kills the `int(payload["priority"])` 500 the prod report hit — a non-numeric/garbage value now returns 422, not 500. attach_tag: payload:dict → AttachTagIn(tag_id:int) — a non-int tag_id was a 500 in `db.get(Tag, ...)`; the personal-tag ownership check reuses owned_or_404.
This commit is contained in:
@@ -5,6 +5,7 @@ import logging
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from pydantic import BaseModel
|
||||||
from sqlalchemy import func, select, update
|
from sqlalchemy import func, select, update
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
@@ -24,6 +25,7 @@ from app.models import (
|
|||||||
User,
|
User,
|
||||||
Video,
|
Video,
|
||||||
)
|
)
|
||||||
|
from app.routes._common import owned_or_404
|
||||||
from app.routes.admin import admin_user
|
from app.routes.admin import admin_user
|
||||||
from app.sync.explore import explore_ingest_page
|
from app.sync.explore import explore_ingest_page
|
||||||
from app.sync.runner import run_recent_backfill
|
from app.sync.runner import run_recent_backfill
|
||||||
@@ -378,28 +380,37 @@ def _is_blocked(db: Session, user: User, channel_id: str) -> bool:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ChannelUpdateIn(BaseModel):
|
||||||
|
# PATCH semantics: a field left unset (None) is untouched. Every field is optional so the
|
||||||
|
# UI can send just the one control it changed. int/bool coercion + rejection of garbage
|
||||||
|
# (a non-numeric priority, a non-bool flag) is now Pydantic's job → 422, not a 500.
|
||||||
|
priority: int | None = None
|
||||||
|
hidden: bool | None = None
|
||||||
|
deep_requested: bool | None = None
|
||||||
|
|
||||||
|
|
||||||
@router.patch("/{channel_id}")
|
@router.patch("/{channel_id}")
|
||||||
def update_channel(
|
def update_channel(
|
||||||
channel_id: str,
|
channel_id: str,
|
||||||
payload: dict,
|
body: ChannelUpdateIn,
|
||||||
user: User = Depends(current_user),
|
user: User = Depends(current_user),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
) -> dict:
|
) -> dict:
|
||||||
sub = _user_subscription(db, user, channel_id)
|
sub = _user_subscription(db, user, channel_id)
|
||||||
channel = db.get(Channel, channel_id)
|
channel = db.get(Channel, channel_id)
|
||||||
deep_turned_on = False
|
deep_turned_on = False
|
||||||
if "priority" in payload:
|
if body.priority is not None:
|
||||||
sub.priority = int(payload["priority"])
|
sub.priority = body.priority
|
||||||
if "hidden" in payload:
|
if body.hidden is not None:
|
||||||
sub.hidden = bool(payload["hidden"])
|
sub.hidden = body.hidden
|
||||||
# The demo account is SHARED, so it must not persist deep_requested (a quota-relevant flag) onto
|
# The demo account is SHARED, so it must not persist deep_requested (a quota-relevant flag) onto
|
||||||
# the shared subscription — skip the write entirely for demo, which also keeps deep_turned_on
|
# the shared subscription — skip the write entirely for demo, which also keeps deep_turned_on
|
||||||
# False so the immediate backfill below can never fire for it.
|
# False so the immediate backfill below can never fire for it.
|
||||||
if "deep_requested" in payload and not user.is_demo:
|
if body.deep_requested is not None and not user.is_demo:
|
||||||
# Opt this channel into (or out of) full-history backfill. The deep scheduler
|
# Opt this channel into (or out of) full-history backfill. The deep scheduler
|
||||||
# picks the flag up on its next run; turning it off won't undo already-fetched
|
# picks the flag up on its next run; turning it off won't undo already-fetched
|
||||||
# videos, it just stops further deep paging if nobody else still wants it.
|
# videos, it just stops further deep paging if nobody else still wants it.
|
||||||
want = bool(payload["deep_requested"])
|
want = body.deep_requested
|
||||||
deep_turned_on = want and not sub.deep_requested
|
deep_turned_on = want and not sub.deep_requested
|
||||||
sub.deep_requested = want
|
sub.deep_requested = want
|
||||||
db.commit()
|
db.commit()
|
||||||
@@ -601,23 +612,25 @@ def unblock_channel(
|
|||||||
return {"unblocked": channel_id}
|
return {"unblocked": channel_id}
|
||||||
|
|
||||||
|
|
||||||
|
class AttachTagIn(BaseModel):
|
||||||
|
tag_id: int
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{channel_id}/tags")
|
@router.post("/{channel_id}/tags")
|
||||||
def attach_tag(
|
def attach_tag(
|
||||||
channel_id: str,
|
channel_id: str,
|
||||||
payload: dict,
|
body: AttachTagIn,
|
||||||
user: User = Depends(current_user),
|
user: User = Depends(current_user),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
) -> dict:
|
) -> dict:
|
||||||
_user_subscription(db, user, channel_id)
|
_user_subscription(db, user, channel_id)
|
||||||
tag_id = payload.get("tag_id")
|
# Only the user's own tags may be attached as personal links (owned_or_404 → 404 if the tag
|
||||||
tag = db.get(Tag, tag_id) if tag_id is not None else None
|
# is missing OR belongs to someone else, without leaking which).
|
||||||
# Only the user's own tags may be attached as personal links.
|
owned_or_404(db, Tag, body.tag_id, user, detail="Unknown tag")
|
||||||
if tag is None or tag.user_id != user.id:
|
|
||||||
raise HTTPException(status_code=404, detail="Unknown tag")
|
|
||||||
exists = db.execute(
|
exists = db.execute(
|
||||||
select(ChannelTag).where(
|
select(ChannelTag).where(
|
||||||
ChannelTag.channel_id == channel_id,
|
ChannelTag.channel_id == channel_id,
|
||||||
ChannelTag.tag_id == tag_id,
|
ChannelTag.tag_id == body.tag_id,
|
||||||
ChannelTag.user_id == user.id,
|
ChannelTag.user_id == user.id,
|
||||||
)
|
)
|
||||||
).scalar_one_or_none()
|
).scalar_one_or_none()
|
||||||
@@ -625,13 +638,13 @@ def attach_tag(
|
|||||||
db.add(
|
db.add(
|
||||||
ChannelTag(
|
ChannelTag(
|
||||||
channel_id=channel_id,
|
channel_id=channel_id,
|
||||||
tag_id=tag_id,
|
tag_id=body.tag_id,
|
||||||
user_id=user.id,
|
user_id=user.id,
|
||||||
source="user",
|
source="user",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
db.commit()
|
db.commit()
|
||||||
return {"channel_id": channel_id, "tag_id": tag_id}
|
return {"channel_id": channel_id, "tag_id": body.tag_id}
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{channel_id}/tags/{tag_id}")
|
@router.delete("/{channel_id}/tags/{tag_id}")
|
||||||
|
|||||||
Reference in New Issue
Block a user