From 3a589257646a27aea1a025c777369c737eafa46e Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Mon, 9 Mar 2026 20:00:06 +0800 Subject: [PATCH] add leaderboard --- account/api.py | 16 +++++++++++++ .../0002_alter_profile_total_score.py | 18 +++++++++++++++ account/models.py | 17 ++++++++++---- account/schemas.py | 6 +++++ submission/api.py | 23 +++++++++++++++++++ submission/models.py | 1 + submission/schemas.py | 8 +++++++ task/migrations/0006_challenge_pass_score.py | 18 +++++++++++++++ task/models.py | 1 + 9 files changed, 104 insertions(+), 4 deletions(-) create mode 100644 account/migrations/0002_alter_profile_total_score.py create mode 100644 task/migrations/0006_challenge_pass_score.py diff --git a/account/api.py b/account/api.py index 69c9597..00f6e58 100644 --- a/account/api.py +++ b/account/api.py @@ -6,6 +6,7 @@ from ninja.pagination import paginate from ninja.errors import HttpError from .schemas import ( BatchUsersIn, + LeaderboardEntry, UserListSchema, UserRegistrationSchema, UserLoginSchema, @@ -119,3 +120,18 @@ def toggle_user_is_active(request, id: int): } except User.DoesNotExist: raise HttpError(404, "查无此人") + + +@router.get("/leaderboard", response=List[LeaderboardEntry]) +def leaderboard(request): + from .models import Profile + profiles = ( + Profile.objects + .select_related("user") + .filter(total_score__gt=0) + .order_by("-total_score") + ) + return [ + LeaderboardEntry(rank=i + 1, username=p.user.username, total_score=p.total_score) + for i, p in enumerate(profiles) + ] diff --git a/account/migrations/0002_alter_profile_total_score.py b/account/migrations/0002_alter_profile_total_score.py new file mode 100644 index 0000000..aea8ab5 --- /dev/null +++ b/account/migrations/0002_alter_profile_total_score.py @@ -0,0 +1,18 @@ +# Generated by Django 6.0.1 on 2026-03-09 11:55 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='profile', + name='total_score', + field=models.FloatField(default=0.0), + ), + ] diff --git a/account/models.py b/account/models.py index 841cf24..4ea60a0 100644 --- a/account/models.py +++ b/account/models.py @@ -47,14 +47,23 @@ class User(AbstractUser): class Profile(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE) - total_score = models.IntegerField(default=0) + total_score = models.FloatField(default=0.0) def __str__(self): return self.user.username - def update_total_score(self, score: int): - self.total_score = self.total_score + score - self.save() + def recalculate_total_score(self): + from django.db.models import Max, Sum + from submission.models import Submission + total = ( + Submission.objects + .filter(user=self.user, task__task_type="challenge", score__gt=0) + .values("task_id") + .annotate(best=Max("score")) + .aggregate(total=Sum("best"))["total"] + ) or 0.0 + self.total_score = total + self.save(update_fields=["total_score"]) @receiver(post_save, sender=User) diff --git a/account/schemas.py b/account/schemas.py index 2110310..b082c02 100644 --- a/account/schemas.py +++ b/account/schemas.py @@ -46,3 +46,9 @@ class UserLoginSchema(Schema): class BatchUsersIn(Schema): names: List[str] classname: str + + +class LeaderboardEntry(Schema): + rank: int + username: str + total_score: float diff --git a/submission/api.py b/submission/api.py index 22dcd20..402b181 100644 --- a/submission/api.py +++ b/submission/api.py @@ -10,6 +10,7 @@ from django.db.models import OuterRef, Subquery, IntegerField from .schemas import ( FlagIn, + MyScoreOut, SubmissionFilter, SubmissionIn, SubmissionOut, @@ -81,6 +82,28 @@ def list_submissions(request, filters: SubmissionFilter = Query(...)): return submissions +@router.get("/my-scores", response=List[MyScoreOut]) +@login_required +def my_scores(request): + seen = {} + for s in Submission.objects.filter( + user=request.user, task__task_type="challenge" + ).order_by("-score").select_related("task"): + if s.task_id not in seen: + seen[s.task_id] = s + return [ + MyScoreOut( + task_id=s.task_id, + task_display=s.task.display, + task_title=s.task.title, + score=s.score, + created=s.created.isoformat(), + ) + for s in seen.values() + ] + + + @router.get("/{submission_id}", response=SubmissionOut) @login_required def get_submission(request, submission_id: UUID): diff --git a/submission/models.py b/submission/models.py index f396b51..1413c95 100644 --- a/submission/models.py +++ b/submission/models.py @@ -136,3 +136,4 @@ def update_submission_score_on_save(sender, instance, **kwargs): 当Rating保存时,更新对应的Submission的平均分 """ instance.submission.update_score() + instance.submission.user.profile.recalculate_total_score() diff --git a/submission/schemas.py b/submission/schemas.py index 5425c63..822667e 100644 --- a/submission/schemas.py +++ b/submission/schemas.py @@ -105,3 +105,11 @@ class SubmissionFilter(Schema): class FlagIn(Schema): flag: Optional[Literal["red", "blue", "green", "yellow"]] = None + + +class MyScoreOut(Schema): + task_id: int + task_display: int + task_title: str + score: float + created: str diff --git a/task/migrations/0006_challenge_pass_score.py b/task/migrations/0006_challenge_pass_score.py new file mode 100644 index 0000000..dab4b1c --- /dev/null +++ b/task/migrations/0006_challenge_pass_score.py @@ -0,0 +1,18 @@ +# Generated by Django 6.0.1 on 2026-03-09 11:47 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('task', '0005_alter_task_options_alter_task_display_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='challenge', + name='pass_score', + field=models.FloatField(blank=True, null=True, verbose_name='通过分数线'), + ), + ] diff --git a/task/models.py b/task/models.py index c781295..707d140 100644 --- a/task/models.py +++ b/task/models.py @@ -40,6 +40,7 @@ class Tutorial(Task): class Challenge(Task): score = models.IntegerField(default=0) + pass_score = models.FloatField(null=True, blank=True, verbose_name="通过分数线") def __str__(self): return self.title