chore(backend): improvements

This commit is contained in:
ITQ
2025-04-08 00:37:39 +03:00
parent ad0e795250
commit 9c121af053
15 changed files with 174 additions and 47 deletions
+1
View File
@@ -34,6 +34,7 @@ class UserSchema(ModelSchema):
model = User model = User
fields = ["id", "avatar", "email", "username", "created_at"] fields = ["id", "avatar", "email", "username", "created_at"]
class StatSchema(Schema): class StatSchema(Schema):
total_attempts: int total_attempts: int
solved_tasks: int solved_tasks: int
+7 -7
View File
@@ -1,11 +1,10 @@
from datetime import datetime from datetime import datetime
from http import HTTPStatus as status from http import HTTPStatus as status
from django.db.models import Count, Q
from apps.user.models import User
from django.contrib.auth.hashers import check_password, make_password from django.contrib.auth.hashers import check_password, make_password
from django.db.models import Count, Q
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from ninja import Router, File from ninja import Router
from ninja.errors import AuthenticationError from ninja.errors import AuthenticationError
from api.v1.auth import BearerAuth from api.v1.auth import BearerAuth
@@ -118,20 +117,21 @@ def get_my_stat(request):
total_attempts=len(user_submissions), solved_tasks=success_attempts_cnt total_attempts=len(user_submissions), solved_tasks=success_attempts_cnt
) )
@router.get( @router.get(
"leaderboard", "leaderboard",
auth=None, auth=None,
response={ response={
status.OK: list[UserSchema], status.OK: list[UserSchema],
} },
) )
def get_leaderboard(request): def get_leaderboard(request):
leaderboard = User.objects.annotate( leaderboard = User.objects.annotate(
checked_count=Count( checked_count=Count(
'competitiontasksubmission', "competitiontasksubmission",
filter=Q(competitiontasksubmission__status='checked') filter=Q(competitiontasksubmission__status="checked"),
) )
).order_by('-checked_count') ).order_by("-checked_count")
top_10 = leaderboard[:10] top_10 = leaderboard[:10]
return status.OK, top_10 return status.OK, top_10
@@ -14,4 +14,5 @@ class AchievementAdmin(admin.ModelAdmin):
"description", "description",
) )
admin.site.register(UserAchievement) admin.site.register(UserAchievement)
@@ -0,0 +1,34 @@
# Generated by Django 5.2 on 2025-04-07 21:25
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('achievement', '0002_initial'),
('user', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='userachievement',
options={'verbose_name': 'выданная ачивка', 'verbose_name_plural': 'выданные ачивки'},
),
migrations.AlterField(
model_name='userachievement',
name='achievement',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='achievement.achievement', verbose_name='ачивка'),
),
migrations.AlterField(
model_name='userachievement',
name='received_at',
field=models.DateTimeField(auto_now_add=True, verbose_name='дата получения'),
),
migrations.AlterField(
model_name='userachievement',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='user.user', verbose_name='пользователь'),
),
]
+9 -3
View File
@@ -27,10 +27,16 @@ class Achievement(BaseModel):
class UserAchievement(BaseModel): class UserAchievement(BaseModel):
achievement = models.ForeignKey(Achievement, verbose_name="ачивка", on_delete=models.CASCADE) achievement = models.ForeignKey(
user = models.ForeignKey("user.User", verbose_name="пользователь", on_delete=models.CASCADE) Achievement, verbose_name="ачивка", on_delete=models.CASCADE
)
user = models.ForeignKey(
"user.User", verbose_name="пользователь", on_delete=models.CASCADE
)
received_at = models.DateTimeField(verbose_name="дата получения", auto_now_add=True) received_at = models.DateTimeField(
verbose_name="дата получения", auto_now_add=True
)
class Meta: class Meta:
verbose_name = "выданная ачивка" verbose_name = "выданная ачивка"
+48 -28
View File
@@ -1,15 +1,14 @@
from django.contrib import admin from django.contrib import admin
from django.urls import path, reverse from django.db.models import Q, Sum
from django.http import HttpResponse
from django.template.response import TemplateResponse
from django.db.models import Count, Q, Sum
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.template.response import TemplateResponse
from django.urls import path, reverse
from django.utils.html import format_html from django.utils.html import format_html
from apps.user.models import User
from apps.competition.models import Competition, State from apps.competition.models import Competition, State
from apps.task.admin import CompetitionTaskInline from apps.task.admin import CompetitionTaskInline
from apps.task.models import CompetitionTaskSubmission, CompetitionTask from apps.task.models import CompetitionTask
from apps.user.models import User
@admin.register(Competition) @admin.register(Competition)
@@ -34,33 +33,46 @@ class CompetitionAdmin(admin.ModelAdmin):
def get_urls(self): def get_urls(self):
urls = super().get_urls() urls = super().get_urls()
custom_urls = [ custom_urls = [
path('leaderboard/', self.admin_site.admin_view(self.leaderboard_view), name='competition_leaderboard'), path(
path('<uuid:competition_id>/leaderboard/', self.admin_site.admin_view(self.competition_leaderboard_view), "leaderboard/",
name='competition_specific_leaderboard'), self.admin_site.admin_view(self.leaderboard_view),
name="competition_leaderboard",
),
path(
"<uuid:competition_id>/leaderboard/",
self.admin_site.admin_view(self.competition_leaderboard_view),
name="competition_specific_leaderboard",
),
] ]
return custom_urls + urls return custom_urls + urls
def view_leaderboard(self, obj): def view_leaderboard(self, obj):
url = reverse('admin:competition_specific_leaderboard', args=[obj.id]) url = reverse("admin:competition_specific_leaderboard", args=[obj.id])
return format_html('<a href="{}">перейти</a>', url) return format_html('<a href="{}">перейти</a>', url)
view_leaderboard.short_description = "Лидерборд" view_leaderboard.short_description = "Лидерборд"
view_leaderboard.allow_tags = True view_leaderboard.allow_tags = True
def competition_leaderboard_view(self, request, competition_id): def competition_leaderboard_view(self, request, competition_id):
competition = get_object_or_404(Competition, id=competition_id) competition = get_object_or_404(Competition, id=competition_id)
competition_tasks = CompetitionTask.objects.filter(competition=competition) competition_tasks = CompetitionTask.objects.filter(
competition=competition
leaderboard = User.objects.annotate( )
total_score=Sum(
'competitiontasksubmission__earned_points', leaderboard = (
filter=Q( User.objects.annotate(
competitiontasksubmission__status='checked', total_score=Sum(
competitiontasksubmission__task__in=competition_tasks "competitiontasksubmission__earned_points",
filter=Q(
competitiontasksubmission__status="checked",
competitiontasksubmission__task__in=competition_tasks,
),
) )
) )
).exclude(total_score__isnull=True).order_by('-total_score')[:20] .exclude(total_score__isnull=True)
.order_by("-total_score")[:20]
)
context = dict( context = dict(
self.admin_site.each_context(request), self.admin_site.each_context(request),
@@ -68,22 +80,30 @@ class CompetitionAdmin(admin.ModelAdmin):
leaderboard=leaderboard, leaderboard=leaderboard,
competition=competition, competition=competition,
) )
return TemplateResponse(request, "admin/competition_leaderboard.html", context) return TemplateResponse(
request, "admin/competition_leaderboard.html", context
)
def leaderboard_view(self, request): def leaderboard_view(self, request):
leaderboard = User.objects.annotate( leaderboard = (
total_score=Sum( User.objects.annotate(
'competitiontasksubmission__earned_points', total_score=Sum(
filter=Q(competitiontasksubmission__status='checked') "competitiontasksubmission__earned_points",
filter=Q(competitiontasksubmission__status="checked"),
)
) )
).exclude(total_score__isnull=True).order_by('-total_score')[:20] .exclude(total_score__isnull=True)
.order_by("-total_score")[:20]
)
context = dict( context = dict(
self.admin_site.each_context(request), self.admin_site.each_context(request),
title="Лидерборд соревнования", title="Лидерборд соревнования",
leaderboard=leaderboard, leaderboard=leaderboard,
) )
return TemplateResponse(request, "admin/competition_leaderboard.html", context) return TemplateResponse(
request, "admin/competition_leaderboard.html", context
)
admin.site.register(State) admin.site.register(State)
+1 -1
View File
@@ -7,4 +7,4 @@ class CompetitionsConfig(AppConfig):
verbose_name = "Соревнование" verbose_name = "Соревнование"
def ready(self): def ready(self):
import apps.competition.signals pass
@@ -0,0 +1,40 @@
# Generated by Django 5.2 on 2025-04-07 21:25
import datetime
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('competition', '0001_initial'),
('user', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='state',
options={'verbose_name': 'участие', 'verbose_name_plural': 'участия'},
),
migrations.AlterField(
model_name='state',
name='changed_at',
field=models.DateTimeField(default=datetime.datetime.now, verbose_name='изменено'),
),
migrations.AlterField(
model_name='state',
name='competition',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='competition.competition', verbose_name='соревнование'),
),
migrations.AlterField(
model_name='state',
name='state',
field=models.CharField(choices=[('not_started', 'Not Started'), ('started', 'Started'), ('finished', 'Finished')], default='not_started', max_length=11, verbose_name='статус'),
),
migrations.AlterField(
model_name='state',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='user.user', verbose_name='участник'),
),
]
+9 -3
View File
@@ -59,15 +59,21 @@ class State(BaseModel):
STARTED = "started" STARTED = "started"
FINISHED = "finished" FINISHED = "finished"
user = models.ForeignKey(User, verbose_name="участник", on_delete=models.CASCADE) user = models.ForeignKey(
competition = models.ForeignKey(Competition, verbose_name="соревнование", on_delete=models.CASCADE) User, verbose_name="участник", on_delete=models.CASCADE
)
competition = models.ForeignKey(
Competition, verbose_name="соревнование", on_delete=models.CASCADE
)
state = models.CharField( state = models.CharField(
choices=StateChoices.choices, choices=StateChoices.choices,
verbose_name="статус", verbose_name="статус",
max_length=11, max_length=11,
default=StateChoices.NOT_STARTED.value, default=StateChoices.NOT_STARTED.value,
) )
changed_at = models.DateTimeField(verbose_name="изменено", default=datetime.now) changed_at = models.DateTimeField(
verbose_name="изменено", default=datetime.now
)
class Meta: class Meta:
verbose_name = "участие" verbose_name = "участие"
+1 -1
View File
@@ -1,6 +1,6 @@
from django.contrib import admin from django.contrib import admin
from apps.review.models import Reviewer, Review from apps.review.models import Review, Reviewer
@admin.register(Reviewer) @admin.register(Reviewer)
+1 -1
View File
@@ -7,4 +7,4 @@ class CoreConfig(AppConfig):
verbose_name = "Проверка" verbose_name = "Проверка"
def ready(self): def ready(self):
import apps.review.signals pass
@@ -0,0 +1,18 @@
# Generated by Django 5.2 on 2025-04-07 21:25
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('review', '0002_initial'),
]
operations = [
migrations.AlterField(
model_name='review',
name='evaluation',
field=models.JSONField(blank=True, default=list, null=True, verbose_name='оценка'),
),
]
+1 -1
View File
@@ -45,7 +45,7 @@ class Review(BaseModel):
) )
def __str__(self): def __str__(self):
return str(self.id) return f"{str(self.reviewer)} -> {self.submission.task.title} ({self.submission.user.username})"
class Meta: class Meta:
verbose_name = "проверка" verbose_name = "проверка"
+1 -1
View File
@@ -7,4 +7,4 @@ class UsersConfig(AppConfig):
verbose_name = "контестанты" verbose_name = "контестанты"
def ready(self): def ready(self):
import apps.user.signals pass
+2 -1
View File
@@ -8,5 +8,6 @@ BASE_DIR = Path(__file__).resolve().parent
load_dotenv(BASE_DIR / ".env") load_dotenv(BASE_DIR / ".env")
DOCKER_IMAGE = os.getenv( DOCKER_IMAGE = os.getenv(
"DOCKER_IMAGE", default="registry.gitlab.com/megazordpobeda/datarush/custom-python:latest" "DOCKER_IMAGE",
default="registry.gitlab.com/megazordpobeda/datarush/custom-python:latest",
) )