diff --git a/app/database/lazer_user.py b/app/database/lazer_user.py index 78d2d72..ca767f5 100644 --- a/app/database/lazer_user.py +++ b/app/database/lazer_user.py @@ -444,3 +444,8 @@ BASE_INCLUDES = [ "daily_challenge_user_stats", "statistics", ] + +RANKING_INCLUDES = [ + "team", + "statistics", +] diff --git a/app/database/statistics.py b/app/database/statistics.py index 47d0824..749f6fb 100644 --- a/app/database/statistics.py +++ b/app/database/statistics.py @@ -1,4 +1,4 @@ -from datetime import UTC, datetime +from datetime import UTC, datetime, timedelta import math from typing import TYPE_CHECKING @@ -6,6 +6,7 @@ from app.models.score import GameMode from .rank_history import RankHistory +from sqlalchemy.ext.asyncio import AsyncAttrs from sqlmodel import ( BigInteger, Column, @@ -20,7 +21,7 @@ from sqlmodel import ( from sqlmodel.ext.asyncio.session import AsyncSession if TYPE_CHECKING: - from .lazer_user import User + from .lazer_user import User, UserResp class UserStatisticsBase(SQLModel): @@ -43,7 +44,7 @@ class UserStatisticsBase(SQLModel): is_ranked: bool = Field(default=True) -class UserStatistics(UserStatisticsBase, table=True): +class UserStatistics(AsyncAttrs, UserStatisticsBase, table=True): __tablename__ = "lazer_user_statistics" # pyright: ignore[reportAssignmentType] id: int | None = Field(default=None, primary_key=True) user_id: int = Field( @@ -66,6 +67,8 @@ class UserStatistics(UserStatisticsBase, table=True): class UserStatisticsResp(UserStatisticsBase): + user: "UserResp | None" = None + rank_change_since_30_days: int | None = 0 global_rank: int | None = Field(default=None) country_rank: int | None = Field(default=None) grade_counts: dict[str, int] = Field( @@ -86,9 +89,13 @@ class UserStatisticsResp(UserStatisticsBase): @classmethod async def from_db( - cls, obj: UserStatistics, session: AsyncSession, user_country: str | None = None + cls, + obj: UserStatistics, + session: AsyncSession, + user_country: str | None = None, + include: list[str] = [], ) -> "UserStatisticsResp": - s = cls.model_validate(obj) + s = cls.model_validate(obj.model_dump()) s.grade_counts = { "ss": obj.grade_ss, "ssh": obj.grade_ssh, @@ -100,9 +107,32 @@ class UserStatisticsResp(UserStatisticsBase): "current": int(obj.level_current), "progress": int(math.fmod(obj.level_current, 1) * 100), } + if "user" in include: + from .lazer_user import RANKING_INCLUDES, UserResp + + user = await UserResp.from_db( + await obj.awaitable_attrs.user, session, include=RANKING_INCLUDES + ) + s.user = user + user_country = user.country_code s.global_rank = await get_rank(session, obj) s.country_rank = await get_rank(session, obj, user_country) + + if "rank_change_since_30_days" in include: + rank_best = ( + await session.exec( + select(func.max(RankHistory.rank)).where( + RankHistory.date > datetime.now(UTC) - timedelta(days=30), + RankHistory.user_id == obj.user_id, + ) + ) + ).first() + if rank_best is None or s.global_rank is None: + s.rank_change_since_30_days = 0 + else: + s.rank_change_since_30_days = rank_best - s.global_rank + return s diff --git a/app/models/score.py b/app/models/score.py index ca63f0b..703b55c 100644 --- a/app/models/score.py +++ b/app/models/score.py @@ -86,7 +86,7 @@ class GameMode(str, Enum): GameMode.TAIKO: GameMode.TAIKORX, GameMode.FRUITS: GameMode.FRUITSRX, }[self] - raise ValueError(f"Unknown game mode: {self}") + return self class Rank(str, Enum): diff --git a/app/router/v2/ranking.py b/app/router/v2/ranking.py index bd83cec..6e86d55 100644 --- a/app/router/v2/ranking.py +++ b/app/router/v2/ranking.py @@ -17,7 +17,7 @@ from sqlmodel.ext.asyncio.session import AsyncSession class CountryStatistics(BaseModel): - country_code: str + code: str active_users: int play_count: int ranked_score: int @@ -48,13 +48,15 @@ async def get_country_ranking( await session.exec( select(UserStatistics).where( UserStatistics.mode == ruleset, + UserStatistics.pp > 0, col(UserStatistics.user).has(country_code=country), + col(UserStatistics.user).has(is_active=True), ) ) ).all() pp = 0 country_stats = CountryStatistics( - country_code=country, + code=country, active_users=0, play_count=0, ranked_score=0, @@ -92,9 +94,15 @@ async def get_user_ranking( current_user: User = Security(get_current_user, scopes=["public"]), session: AsyncSession = Depends(get_db), ): - wheres = [col(UserStatistics.mode) == ruleset] + wheres = [ + col(UserStatistics.mode) == ruleset, + col(UserStatistics.pp) > 0, + col(UserStatistics.is_ranked).is_(True), + ] + include = ["user"] if type == "performance": order_by = col(UserStatistics.pp).desc() + include.append("rank_change_since_30_days") else: order_by = col(UserStatistics.ranked_score).desc() if country: @@ -106,9 +114,10 @@ async def get_user_ranking( .limit(50) .offset(50 * (page - 1)) ) - return TopUsersResponse( + resp = TopUsersResponse( ranking=[ - await UserStatisticsResp.from_db(statistics, session, None) + await UserStatisticsResp.from_db(statistics, session, None, include) for statistics in statistics_list ] ) + return resp