Add movie/series ratings and watchlist
Builds on the accounts foundation: PUT/DELETE a rating (0.5-10.0) or a
watchlist entry on any movie or series, all scoped to the bearer token's
own user, plus GET /account/ratings|watchlist/{movies,series} to list
them back. Both are polymorphic entity_type/entity_id tables, same shape
as watch_provider_offerings and trending_entries, so movie and series
share one repository/router each rather than duplicating per type.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
a38c2074a6
commit
0ab31a314c
@@ -0,0 +1,66 @@
|
||||
"""Data access for Rating."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
|
||||
from sqlalchemy import delete, func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.rating import Rating
|
||||
|
||||
|
||||
async def get_rating(
|
||||
session: AsyncSession, *, user_id: uuid.UUID, entity_type: str, entity_id: uuid.UUID
|
||||
) -> Rating | None:
|
||||
stmt = select(Rating).where(
|
||||
Rating.user_id == user_id, Rating.entity_type == entity_type, Rating.entity_id == entity_id
|
||||
)
|
||||
result = await session.execute(stmt)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def upsert_rating(
|
||||
session: AsyncSession, *, user_id: uuid.UUID, entity_type: str, entity_id: uuid.UUID, value: float
|
||||
) -> Rating:
|
||||
rating = await get_rating(session, user_id=user_id, entity_type=entity_type, entity_id=entity_id)
|
||||
if rating is not None:
|
||||
rating.value = value
|
||||
await session.flush()
|
||||
return rating
|
||||
|
||||
rating = Rating(user_id=user_id, entity_type=entity_type, entity_id=entity_id, value=value)
|
||||
session.add(rating)
|
||||
await session.flush()
|
||||
return rating
|
||||
|
||||
|
||||
async def delete_rating(
|
||||
session: AsyncSession, *, user_id: uuid.UUID, entity_type: str, entity_id: uuid.UUID
|
||||
) -> None:
|
||||
await session.execute(
|
||||
delete(Rating).where(
|
||||
Rating.user_id == user_id, Rating.entity_type == entity_type, Rating.entity_id == entity_id
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def list_rated(
|
||||
session: AsyncSession, *, user_id: uuid.UUID, entity_type: str, limit: int, offset: int
|
||||
) -> tuple[list[Rating], int]:
|
||||
"""Rated entities for a user, newest-rated first, paginated."""
|
||||
|
||||
total = await session.scalar(
|
||||
select(func.count())
|
||||
.select_from(Rating)
|
||||
.where(Rating.user_id == user_id, Rating.entity_type == entity_type)
|
||||
)
|
||||
stmt = (
|
||||
select(Rating)
|
||||
.where(Rating.user_id == user_id, Rating.entity_type == entity_type)
|
||||
.order_by(Rating.created_at.desc())
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
)
|
||||
result = await session.execute(stmt)
|
||||
return list(result.scalars().all()), total or 0
|
||||
@@ -0,0 +1,71 @@
|
||||
"""Data access for WatchlistEntry."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
|
||||
from sqlalchemy import delete, func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.watchlist import WatchlistEntry
|
||||
|
||||
|
||||
async def get_entry(
|
||||
session: AsyncSession, *, user_id: uuid.UUID, entity_type: str, entity_id: uuid.UUID
|
||||
) -> WatchlistEntry | None:
|
||||
stmt = select(WatchlistEntry).where(
|
||||
WatchlistEntry.user_id == user_id,
|
||||
WatchlistEntry.entity_type == entity_type,
|
||||
WatchlistEntry.entity_id == entity_id,
|
||||
)
|
||||
result = await session.execute(stmt)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def add_entry(
|
||||
session: AsyncSession, *, user_id: uuid.UUID, entity_type: str, entity_id: uuid.UUID
|
||||
) -> WatchlistEntry:
|
||||
"""Idempotent: adding an already-watchlisted entity just returns the
|
||||
existing row rather than raising on the unique constraint."""
|
||||
|
||||
entry = await get_entry(session, user_id=user_id, entity_type=entity_type, entity_id=entity_id)
|
||||
if entry is not None:
|
||||
return entry
|
||||
|
||||
entry = WatchlistEntry(user_id=user_id, entity_type=entity_type, entity_id=entity_id)
|
||||
session.add(entry)
|
||||
await session.flush()
|
||||
return entry
|
||||
|
||||
|
||||
async def remove_entry(
|
||||
session: AsyncSession, *, user_id: uuid.UUID, entity_type: str, entity_id: uuid.UUID
|
||||
) -> None:
|
||||
await session.execute(
|
||||
delete(WatchlistEntry).where(
|
||||
WatchlistEntry.user_id == user_id,
|
||||
WatchlistEntry.entity_type == entity_type,
|
||||
WatchlistEntry.entity_id == entity_id,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def list_entries(
|
||||
session: AsyncSession, *, user_id: uuid.UUID, entity_type: str, limit: int, offset: int
|
||||
) -> tuple[list[WatchlistEntry], int]:
|
||||
"""Watchlisted entities for a user, newest-added first, paginated."""
|
||||
|
||||
total = await session.scalar(
|
||||
select(func.count())
|
||||
.select_from(WatchlistEntry)
|
||||
.where(WatchlistEntry.user_id == user_id, WatchlistEntry.entity_type == entity_type)
|
||||
)
|
||||
stmt = (
|
||||
select(WatchlistEntry)
|
||||
.where(WatchlistEntry.user_id == user_id, WatchlistEntry.entity_type == entity_type)
|
||||
.order_by(WatchlistEntry.created_at.desc())
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
)
|
||||
result = await session.execute(stmt)
|
||||
return list(result.scalars().all()), total or 0
|
||||
Reference in New Issue
Block a user