feat: add awards to REST response
This commit is contained in:
parent
f0f69c1954
commit
b0f5ec4cc9
5 changed files with 142 additions and 5 deletions
|
|
@ -32,6 +32,74 @@ def admin_client() -> TestClient:
|
|||
return client
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_ratings_for_group_with_awards(
|
||||
conn: db.Connection, unauthorized_client: TestClient
|
||||
):
|
||||
user = models.User(
|
||||
imdb_id="ur12345678",
|
||||
name="user-1",
|
||||
secret="secret-1", # noqa: S106
|
||||
groups=[],
|
||||
)
|
||||
group = models.Group(
|
||||
name="group-1",
|
||||
users=[models.GroupUser(id=str(user.id), name=user.name)],
|
||||
)
|
||||
user.groups = [models.UserGroup(id=str(group.id), access="r")]
|
||||
path = app.url_path_for("get_ratings_for_group", group_id=str(group.id))
|
||||
|
||||
await db.add(conn, user)
|
||||
await db.add(conn, group)
|
||||
|
||||
movie1 = models.Movie(
|
||||
title="test movie",
|
||||
release_year=2013,
|
||||
media_type="Movie",
|
||||
imdb_id="tt12345678",
|
||||
genres={"genre-1"},
|
||||
)
|
||||
await db.add(conn, movie1)
|
||||
movie2 = models.Movie(
|
||||
title="test movie 2",
|
||||
release_year=2014,
|
||||
media_type="Movie",
|
||||
imdb_id="tt12345679",
|
||||
genres={"genre-2"},
|
||||
)
|
||||
await db.add(conn, movie2)
|
||||
|
||||
award1 = models.Award(
|
||||
movie_id=movie1.id, category="imdb-top-250", details='{"position":23}'
|
||||
)
|
||||
award2 = models.Award(
|
||||
movie_id=movie2.id, category="imdb-top-250", details='{"position":99}'
|
||||
)
|
||||
await db.add(conn, award1)
|
||||
await db.add(conn, award2)
|
||||
|
||||
rating = models.Rating(
|
||||
movie_id=movie1.id, user_id=user.id, score=66, rating_date=datetime.now(tz=UTC)
|
||||
)
|
||||
await db.add(conn, rating)
|
||||
|
||||
rating_aggregate = {
|
||||
"canonical_title": movie1.title,
|
||||
"imdb_score": movie1.imdb_score,
|
||||
"imdb_votes": movie1.imdb_votes,
|
||||
"link": imdb.movie_url(movie1.imdb_id),
|
||||
"media_type": movie1.media_type,
|
||||
"original_title": movie1.original_title,
|
||||
"user_scores": [rating.score],
|
||||
"year": movie1.release_year,
|
||||
"awards": ["imdb-top-250:23"],
|
||||
}
|
||||
|
||||
resp = unauthorized_client.get(path)
|
||||
assert resp.status_code == 200
|
||||
assert resp.json() == [rating_aggregate]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_ratings_for_group(
|
||||
conn: db.Connection, unauthorized_client: TestClient
|
||||
|
|
@ -82,6 +150,7 @@ async def test_get_ratings_for_group(
|
|||
"original_title": movie.original_title,
|
||||
"user_scores": [rating.score],
|
||||
"year": movie.release_year,
|
||||
"awards": [],
|
||||
}
|
||||
|
||||
resp = unauthorized_client.get(path)
|
||||
|
|
@ -158,6 +227,7 @@ async def test_list_movies(
|
|||
"original_title": m.original_title,
|
||||
"user_scores": [],
|
||||
"year": m.release_year,
|
||||
"awards": [],
|
||||
}
|
||||
|
||||
response = authorized_client.get(path, params={"imdb_id": m.imdb_id})
|
||||
|
|
|
|||
22
unwind/db.py
22
unwind/db.py
|
|
@ -12,12 +12,14 @@ import alembic.migration
|
|||
|
||||
from . import config
|
||||
from .models import (
|
||||
Award,
|
||||
Model,
|
||||
Movie,
|
||||
Progress,
|
||||
Rating,
|
||||
User,
|
||||
asplain,
|
||||
awards,
|
||||
fromplain,
|
||||
metadata,
|
||||
movies,
|
||||
|
|
@ -430,6 +432,26 @@ async def add_or_update_rating(conn: Connection, /, rating: Rating) -> bool:
|
|||
return False
|
||||
|
||||
|
||||
type MovieImdbId = str
|
||||
|
||||
|
||||
async def get_awards(
|
||||
conn: Connection, /, imdb_ids: list[MovieImdbId]
|
||||
) -> dict[MovieImdbId, list[Award]]:
|
||||
query = (
|
||||
sa.select(Award, movies.c.imdb_id)
|
||||
.join(movies, awards.c.movie_id == movies.c.id)
|
||||
.where(movies.c.imdb_id.in_(imdb_ids))
|
||||
)
|
||||
rows = await fetch_all(conn, query)
|
||||
awards_dict: dict[MovieImdbId, list[Award]] = {}
|
||||
for row in rows:
|
||||
awards_dict.setdefault(row.imdb_id, []).append(
|
||||
fromplain(Award, row._mapping, serialized=True)
|
||||
)
|
||||
return awards_dict
|
||||
|
||||
|
||||
def sql_escape(s: str, char: str = "#") -> str:
|
||||
return s.replace(char, 2 * char).replace("%", f"{char}%").replace("_", f"{char}_")
|
||||
|
||||
|
|
|
|||
|
|
@ -182,12 +182,17 @@ def fromplain[T](cls: Type[T], d: Mapping, *, serialized: bool = False) -> T:
|
|||
If `serialized` is `True`, collection types (lists, dicts, etc.) will be
|
||||
deserialized from string. This is the opposite operation of `serialize` for
|
||||
`asplain`.
|
||||
Fields in the data that cannot be mapped to the given type are simply ignored.
|
||||
"""
|
||||
load = json.loads if serialized else _id
|
||||
|
||||
dd: JSONObject = {}
|
||||
for f in fields(cls):
|
||||
target: Any = f.type
|
||||
if isinstance(target, TypeAliasType):
|
||||
# Support type aliases.
|
||||
target = target.__value__
|
||||
|
||||
otype = optional_type(f.type)
|
||||
is_opt = otype is not None
|
||||
if is_opt:
|
||||
|
|
@ -198,6 +203,12 @@ def fromplain[T](cls: Type[T], d: Mapping, *, serialized: bool = False) -> T:
|
|||
v = d[f.name]
|
||||
if is_opt and v is None:
|
||||
dd[f.name] = v
|
||||
elif target is Literal:
|
||||
# Support literal types.
|
||||
vals = get_args(f.type.__value__)
|
||||
if v not in vals:
|
||||
raise ValueError(f"Invalid value: {f.name!a}: {v!a}")
|
||||
dd[f.name] = v
|
||||
elif isinstance(v, target):
|
||||
dd[f.name] = v
|
||||
elif target in {set, list}:
|
||||
|
|
@ -530,5 +541,23 @@ class Award:
|
|||
created: datetime = field(default_factory=utcnow)
|
||||
updated: datetime = field(default_factory=utcnow)
|
||||
|
||||
@property
|
||||
def _details(self) -> JSONObject:
|
||||
return json.loads(self.details or "{}")
|
||||
|
||||
@_details.setter
|
||||
def _details(self, details: JSONObject):
|
||||
self.details = json_dump(details)
|
||||
|
||||
@property
|
||||
def position(self) -> int:
|
||||
return self._details["position"]
|
||||
|
||||
@position.setter
|
||||
def position(self, position: int):
|
||||
details = self._details
|
||||
details["position"] = position
|
||||
self._details = details
|
||||
|
||||
|
||||
awards = Award.__table__
|
||||
|
|
|
|||
|
|
@ -235,11 +235,13 @@ async def get_ratings_for_group(request: Request) -> JSONResponse:
|
|||
user_ids=user_ids,
|
||||
)
|
||||
|
||||
ratings = (web_models.Rating(**r) for r in rows)
|
||||
ratings = [web_models.Rating(**r) for r in rows]
|
||||
|
||||
aggr = web_models.aggregate_ratings(ratings, user_ids)
|
||||
awards = await db.get_awards(conn, imdb_ids=[r.movie_imdb_id for r in ratings])
|
||||
|
||||
resp = tuple(asplain(r) for r in aggr)
|
||||
aggrs = web_models.aggregate_ratings(ratings, user_ids, awards_dict=awards)
|
||||
|
||||
resp = tuple(asplain(r) for r in aggrs)
|
||||
|
||||
return JSONResponse(resp)
|
||||
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ class RatingAggregate:
|
|||
original_title: str | None
|
||||
user_scores: list[Score100]
|
||||
year: int
|
||||
awards: list[str]
|
||||
|
||||
@classmethod
|
||||
def from_movie(cls, movie: models.Movie, *, ratings: Iterable[models.Rating] = []):
|
||||
|
|
@ -56,15 +57,27 @@ class RatingAggregate:
|
|||
original_title=movie.original_title,
|
||||
user_scores=[r.score for r in ratings],
|
||||
year=movie.release_year,
|
||||
awards=[],
|
||||
)
|
||||
|
||||
|
||||
type ImdbMovieId = str
|
||||
type UserId = str
|
||||
|
||||
|
||||
def aggregate_ratings(
|
||||
ratings: Iterable[Rating], user_ids: Container[str]
|
||||
ratings: Iterable[Rating],
|
||||
user_ids: Container[UserId],
|
||||
*,
|
||||
awards_dict: dict[ImdbMovieId, list[models.Award]] | None = None,
|
||||
) -> Iterable[RatingAggregate]:
|
||||
aggr: dict[str, RatingAggregate] = {}
|
||||
if awards_dict is None:
|
||||
awards_dict = {}
|
||||
|
||||
aggr: dict[ImdbMovieId, RatingAggregate] = {}
|
||||
|
||||
for r in ratings:
|
||||
awards = awards_dict.get(r.movie_imdb_id, [])
|
||||
mov = aggr.setdefault(
|
||||
r.movie_imdb_id,
|
||||
RatingAggregate(
|
||||
|
|
@ -76,6 +89,7 @@ def aggregate_ratings(
|
|||
original_title=r.original_title,
|
||||
user_scores=[],
|
||||
year=r.release_year,
|
||||
awards=[f"{a.category}:{a.position}" for a in awards],
|
||||
),
|
||||
)
|
||||
# XXX do we need this? why don't we just get the ratings we're supposed to aggregate?
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue