This commit is contained in:
chenjintang-shrimp
2025-08-12 14:38:26 +00:00
9 changed files with 395 additions and 16 deletions

View File

@@ -1,13 +1,13 @@
from .achievement import UserAchievement, UserAchievementResp
from .auth import OAuthClient, OAuthToken
from .beatmap import (
Beatmap as Beatmap,
BeatmapResp as BeatmapResp,
Beatmap,
BeatmapResp,
)
from .beatmap_playcounts import BeatmapPlaycounts, BeatmapPlaycountsResp
from .beatmapset import (
Beatmapset as Beatmapset,
BeatmapsetResp as BeatmapsetResp,
Beatmapset,
BeatmapsetResp,
)
from .best_score import BestScore
from .counts import (
@@ -30,6 +30,7 @@ from .playlist_attempts import (
from .playlist_best_score import PlaylistBestScore
from .playlists import Playlist, PlaylistResp
from .pp_best_score import PPBestScore
from .rank_history import RankHistory, RankHistoryResp, RankTop
from .relationship import Relationship, RelationshipResp, RelationshipType
from .room import APIUploadedRoom, Room, RoomResp
from .room_participated_user import RoomParticipatedUser
@@ -58,6 +59,7 @@ __all__ = [
"Beatmap",
"BeatmapPlaycounts",
"BeatmapPlaycountsResp",
"BeatmapResp",
"Beatmapset",
"BeatmapsetResp",
"BestScore",
@@ -78,6 +80,9 @@ __all__ = [
"PlaylistAggregateScore",
"PlaylistBestScore",
"PlaylistResp",
"RankHistory",
"RankHistoryResp",
"RankTop",
"Relationship",
"RelationshipResp",
"RelationshipType",

View File

@@ -4,12 +4,13 @@ from typing import TYPE_CHECKING, NotRequired, TypedDict
from app.database.events import Event
from app.models.model import UTCBaseModel
from app.models.score import GameMode
from app.models.user import Country, Page, RankHistory
from app.models.user import Country, Page
from .achievement import UserAchievement, UserAchievementResp
from .beatmap_playcounts import BeatmapPlaycounts
from .counts import CountResp, MonthlyPlaycounts, ReplayWatchedCount
from .daily_challenge import DailyChallengeStats, DailyChallengeStatsResp
from .rank_history import RankHistory, RankHistoryResp, RankTop
from .statistics import UserStatistics, UserStatisticsResp
from .team import Team, TeamMember
from .user_account_history import UserAccountHistory, UserAccountHistoryResp
@@ -152,6 +153,10 @@ class User(AsyncAttrs, UserBase, table=True):
favourite_beatmapsets: list["FavouriteBeatmapset"] = Relationship(
back_populates="user"
)
rank_history: list[RankHistory] = Relationship(
back_populates="user",
)
events: list["Event"] = Relationship(back_populates="user")
email: str = Field(max_length=254, unique=True, index=True, exclude=True)
priv: int = Field(default=1, exclude=True)
@@ -191,8 +196,8 @@ class UserResp(UserBase):
monthly_playcounts: list[CountResp] = Field(default_factory=list)
replay_watched_counts: list[CountResp] = Field(default_factory=list)
unread_pm_count: int = 0 # TODO
rank_history: RankHistory | None = None # TODO
rank_highest: RankHighest | None = None # TODO
rank_history: RankHistoryResp | None = None
rank_highest: RankHighest | None = None
statistics: UserStatisticsResp | None = None
statistics_rulesets: dict[str, UserStatisticsResp] | None = None
user_achievements: list[UserAchievementResp] = Field(default_factory=list)
@@ -290,14 +295,18 @@ class UserResp(UserBase):
current_stattistics = i
break
u.statistics = (
UserStatisticsResp.from_db(current_stattistics)
await UserStatisticsResp.from_db(
current_stattistics, session, obj.country_code
)
if current_stattistics
else None
)
if "statistics_rulesets" in include:
u.statistics_rulesets = {
i.mode.value: UserStatisticsResp.from_db(i)
i.mode.value: await UserStatisticsResp.from_db(
i, session, obj.country_code
)
for i in await obj.awaitable_attrs.statistics
}
@@ -318,6 +327,27 @@ class UserResp(UserBase):
UserAchievementResp.from_db(ua)
for ua in await obj.awaitable_attrs.achievement
]
if "rank_history" in include:
rank_history = await RankHistoryResp.from_db(session, obj.id, ruleset)
if len(rank_history.data) != 0:
u.rank_history = rank_history
rank_top = (
await session.exec(
select(RankTop).where(
RankTop.user_id == obj.id, RankTop.mode == ruleset
)
)
).first()
if rank_top:
u.rank_highest = (
RankHighest(
rank=rank_top.rank,
updated_at=datetime.combine(rank_top.date, datetime.min.time()),
)
if rank_top
else None
)
u.favourite_beatmapset_count = (
await session.exec(
@@ -384,6 +414,7 @@ ALL_INCLUDED = [
"achievements",
"monthly_playcounts",
"replays_watched_counts",
"rank_history",
]
@@ -395,6 +426,7 @@ SEARCH_INCLUDED = [
"achievements",
"monthly_playcounts",
"replays_watched_counts",
"rank_history",
]
BASE_INCLUDES = [

View File

@@ -0,0 +1,78 @@
from datetime import (
UTC,
date as dt,
datetime,
)
from typing import TYPE_CHECKING, Optional
from app.models.score import GameMode
from pydantic import BaseModel
from sqlmodel import (
BigInteger,
Column,
Date,
Field,
ForeignKey,
Relationship,
SQLModel,
col,
select,
)
from sqlmodel.ext.asyncio.session import AsyncSession
if TYPE_CHECKING:
from .lazer_user import User
class RankHistory(SQLModel, table=True):
__tablename__ = "rank_history" # pyright: ignore[reportAssignmentType]
id: int | None = Field(default=None, sa_column=Column(BigInteger, primary_key=True))
user_id: int = Field(
sa_column=Column(BigInteger, ForeignKey("lazer_users.id"), index=True)
)
mode: GameMode
rank: int
date: dt = Field(
default_factory=lambda: datetime.now(UTC).date(),
sa_column=Column(Date, index=True),
)
user: Optional["User"] = Relationship(back_populates="rank_history")
class RankTop(SQLModel, table=True):
__tablename__ = "rank_top" # pyright: ignore[reportAssignmentType]
id: int | None = Field(default=None, sa_column=Column(BigInteger, primary_key=True))
user_id: int = Field(
sa_column=Column(BigInteger, ForeignKey("lazer_users.id"), index=True)
)
mode: GameMode
rank: int
date: dt = Field(
default_factory=lambda: datetime.now(UTC).date(),
sa_column=Column(Date, index=True),
)
class RankHistoryResp(BaseModel):
mode: GameMode
data: list[int]
@classmethod
async def from_db(
cls, session: AsyncSession, user_id: int, mode: GameMode
) -> "RankHistoryResp":
results = (
await session.exec(
select(RankHistory)
.where(RankHistory.user_id == user_id, RankHistory.mode == mode)
.order_by(col(RankHistory.date).desc())
.limit(90)
)
).all()
data = [result.rank for result in results]
data.reverse()
return cls(mode=mode, data=data)

View File

@@ -1,7 +1,10 @@
from datetime import UTC, datetime
from typing import TYPE_CHECKING
from app.models.score import GameMode
from .rank_history import RankHistory
from sqlmodel import (
BigInteger,
Column,
@@ -9,23 +12,24 @@ from sqlmodel import (
ForeignKey,
Relationship,
SQLModel,
col,
func,
select,
)
from sqlmodel.ext.asyncio.session import AsyncSession
if TYPE_CHECKING:
from .lazer_user import User
class UserStatisticsBase(SQLModel):
mode: GameMode
mode: GameMode = Field(index=True)
count_100: int = Field(default=0, sa_column=Column(BigInteger))
count_300: int = Field(default=0, sa_column=Column(BigInteger))
count_50: int = Field(default=0, sa_column=Column(BigInteger))
count_miss: int = Field(default=0, sa_column=Column(BigInteger))
global_rank: int | None = Field(default=None)
country_rank: int | None = Field(default=None)
pp: float = Field(default=0.0)
pp: float = Field(default=0.0, index=True)
ranked_score: int = Field(default=0)
hit_accuracy: float = Field(default=0.00)
total_score: int = Field(default=0, sa_column=Column(BigInteger))
@@ -62,6 +66,8 @@ class UserStatistics(UserStatisticsBase, table=True):
class UserStatisticsResp(UserStatisticsBase):
global_rank: int | None = Field(default=None)
country_rank: int | None = Field(default=None)
grade_counts: dict[str, int] = Field(
default_factory=lambda: {
"ss": 0,
@@ -79,7 +85,9 @@ class UserStatisticsResp(UserStatisticsBase):
)
@classmethod
def from_db(cls, obj: UserStatistics) -> "UserStatisticsResp":
async def from_db(
cls, obj: UserStatistics, session: AsyncSession, user_country: str
) -> "UserStatisticsResp":
s = cls.model_validate(obj)
s.grade_counts = {
"ss": obj.grade_ss,
@@ -92,4 +100,56 @@ class UserStatisticsResp(UserStatisticsBase):
"current": obj.level_current,
"progress": obj.level_progress,
}
s.global_rank = await get_rank(session, obj)
s.country_rank = await get_rank(session, obj, user_country)
return s
async def get_rank(
session: AsyncSession, statistics: UserStatistics, country: str | None = None
) -> int | None:
from .lazer_user import User
query = select(
UserStatistics.user_id,
func.row_number().over(order_by=col(UserStatistics.pp).desc()).label("rank"),
).where(
UserStatistics.mode == statistics.mode,
UserStatistics.pp > 0,
col(UserStatistics.is_ranked).is_(True),
)
if country is not None:
query = query.join(User).where(User.country_code == country)
subq = query.subquery()
result = await session.exec(
select(subq.c.rank).where(subq.c.user_id == statistics.user_id)
)
rank = result.first()
if rank is None:
return None
today = datetime.now(UTC).date()
rank_history = (
await session.exec(
select(RankHistory).where(
RankHistory.user_id == statistics.user_id,
RankHistory.mode == statistics.mode,
RankHistory.date == today,
)
)
).first()
if rank_history is None:
rank_history = RankHistory(
user_id=statistics.user_id,
mode=statistics.mode,
date=today,
rank=rank,
)
session.add(rank_history)
else:
rank_history.rank = rank
return rank