From 9c121af053ae9eb545f26203f6813ccfff83c9f2 Mon Sep 17 00:00:00 2001 From: ITQ Date: Tue, 8 Apr 2025 00:37:39 +0300 Subject: [PATCH] chore(backend): improvements --- services/backend/api/v1/user/schemas.py | 1 + services/backend/api/v1/user/views.py | 14 ++-- services/backend/apps/achievement/admin.py | 1 + ..._alter_userachievement_options_and_more.py | 34 +++++++++ services/backend/apps/achievement/models.py | 12 ++- services/backend/apps/competition/admin.py | 76 ++++++++++++------- services/backend/apps/competition/apps.py | 2 +- ...options_alter_state_changed_at_and_more.py | 40 ++++++++++ services/backend/apps/competition/models.py | 12 ++- services/backend/apps/review/admin.py | 2 +- services/backend/apps/review/apps.py | 2 +- .../0003_alter_review_evaluation.py | 18 +++++ services/backend/apps/review/models.py | 2 +- services/backend/apps/user/apps.py | 2 +- services/checker/config.py | 3 +- 15 files changed, 174 insertions(+), 47 deletions(-) create mode 100644 services/backend/apps/achievement/migrations/0003_alter_userachievement_options_and_more.py create mode 100644 services/backend/apps/competition/migrations/0002_alter_state_options_alter_state_changed_at_and_more.py create mode 100644 services/backend/apps/review/migrations/0003_alter_review_evaluation.py diff --git a/services/backend/api/v1/user/schemas.py b/services/backend/api/v1/user/schemas.py index c3edd74..2109f8e 100644 --- a/services/backend/api/v1/user/schemas.py +++ b/services/backend/api/v1/user/schemas.py @@ -34,6 +34,7 @@ class UserSchema(ModelSchema): model = User fields = ["id", "avatar", "email", "username", "created_at"] + class StatSchema(Schema): total_attempts: int solved_tasks: int diff --git a/services/backend/api/v1/user/views.py b/services/backend/api/v1/user/views.py index 0570f29..ff9dd3e 100644 --- a/services/backend/api/v1/user/views.py +++ b/services/backend/api/v1/user/views.py @@ -1,11 +1,10 @@ from datetime import datetime 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.db.models import Count, Q from django.shortcuts import get_object_or_404 -from ninja import Router, File +from ninja import Router from ninja.errors import AuthenticationError 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 ) + @router.get( "leaderboard", auth=None, response={ status.OK: list[UserSchema], - } + }, ) def get_leaderboard(request): leaderboard = User.objects.annotate( checked_count=Count( - 'competitiontasksubmission', - filter=Q(competitiontasksubmission__status='checked') + "competitiontasksubmission", + filter=Q(competitiontasksubmission__status="checked"), ) - ).order_by('-checked_count') + ).order_by("-checked_count") top_10 = leaderboard[:10] return status.OK, top_10 diff --git a/services/backend/apps/achievement/admin.py b/services/backend/apps/achievement/admin.py index a4059ac..7a09044 100644 --- a/services/backend/apps/achievement/admin.py +++ b/services/backend/apps/achievement/admin.py @@ -14,4 +14,5 @@ class AchievementAdmin(admin.ModelAdmin): "description", ) + admin.site.register(UserAchievement) diff --git a/services/backend/apps/achievement/migrations/0003_alter_userachievement_options_and_more.py b/services/backend/apps/achievement/migrations/0003_alter_userachievement_options_and_more.py new file mode 100644 index 0000000..124277b --- /dev/null +++ b/services/backend/apps/achievement/migrations/0003_alter_userachievement_options_and_more.py @@ -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='пользователь'), + ), + ] diff --git a/services/backend/apps/achievement/models.py b/services/backend/apps/achievement/models.py index d1d026d..0497158 100644 --- a/services/backend/apps/achievement/models.py +++ b/services/backend/apps/achievement/models.py @@ -27,10 +27,16 @@ class Achievement(BaseModel): class UserAchievement(BaseModel): - achievement = models.ForeignKey(Achievement, verbose_name="ачивка", on_delete=models.CASCADE) - user = models.ForeignKey("user.User", verbose_name="пользователь", on_delete=models.CASCADE) + achievement = models.ForeignKey( + 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: verbose_name = "выданная ачивка" diff --git a/services/backend/apps/competition/admin.py b/services/backend/apps/competition/admin.py index 45a62cb..0a955d3 100644 --- a/services/backend/apps/competition/admin.py +++ b/services/backend/apps/competition/admin.py @@ -1,15 +1,14 @@ from django.contrib import admin -from django.urls import path, reverse -from django.http import HttpResponse -from django.template.response import TemplateResponse -from django.db.models import Count, Q, Sum +from django.db.models import Q, Sum 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 apps.user.models import User from apps.competition.models import Competition, State 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) @@ -34,33 +33,46 @@ class CompetitionAdmin(admin.ModelAdmin): def get_urls(self): urls = super().get_urls() custom_urls = [ - path('leaderboard/', self.admin_site.admin_view(self.leaderboard_view), name='competition_leaderboard'), - path('/leaderboard/', self.admin_site.admin_view(self.competition_leaderboard_view), - name='competition_specific_leaderboard'), + path( + "leaderboard/", + self.admin_site.admin_view(self.leaderboard_view), + name="competition_leaderboard", + ), + path( + "/leaderboard/", + self.admin_site.admin_view(self.competition_leaderboard_view), + name="competition_specific_leaderboard", + ), ] return custom_urls + urls 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('перейти', url) - + view_leaderboard.short_description = "Лидерборд" view_leaderboard.allow_tags = True def competition_leaderboard_view(self, request, competition_id): competition = get_object_or_404(Competition, id=competition_id) - - competition_tasks = CompetitionTask.objects.filter(competition=competition) - - leaderboard = User.objects.annotate( - total_score=Sum( - 'competitiontasksubmission__earned_points', - filter=Q( - competitiontasksubmission__status='checked', - competitiontasksubmission__task__in=competition_tasks + + competition_tasks = CompetitionTask.objects.filter( + competition=competition + ) + + leaderboard = ( + User.objects.annotate( + total_score=Sum( + "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( self.admin_site.each_context(request), @@ -68,22 +80,30 @@ class CompetitionAdmin(admin.ModelAdmin): leaderboard=leaderboard, competition=competition, ) - return TemplateResponse(request, "admin/competition_leaderboard.html", context) + return TemplateResponse( + request, "admin/competition_leaderboard.html", context + ) def leaderboard_view(self, request): - leaderboard = User.objects.annotate( - total_score=Sum( - 'competitiontasksubmission__earned_points', - filter=Q(competitiontasksubmission__status='checked') + leaderboard = ( + User.objects.annotate( + total_score=Sum( + "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( self.admin_site.each_context(request), title="Лидерборд соревнования", leaderboard=leaderboard, ) - return TemplateResponse(request, "admin/competition_leaderboard.html", context) + return TemplateResponse( + request, "admin/competition_leaderboard.html", context + ) admin.site.register(State) diff --git a/services/backend/apps/competition/apps.py b/services/backend/apps/competition/apps.py index f10ee38..8aba210 100644 --- a/services/backend/apps/competition/apps.py +++ b/services/backend/apps/competition/apps.py @@ -7,4 +7,4 @@ class CompetitionsConfig(AppConfig): verbose_name = "Соревнование" def ready(self): - import apps.competition.signals + pass diff --git a/services/backend/apps/competition/migrations/0002_alter_state_options_alter_state_changed_at_and_more.py b/services/backend/apps/competition/migrations/0002_alter_state_options_alter_state_changed_at_and_more.py new file mode 100644 index 0000000..825675b --- /dev/null +++ b/services/backend/apps/competition/migrations/0002_alter_state_options_alter_state_changed_at_and_more.py @@ -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='участник'), + ), + ] diff --git a/services/backend/apps/competition/models.py b/services/backend/apps/competition/models.py index 930e8f2..228a974 100644 --- a/services/backend/apps/competition/models.py +++ b/services/backend/apps/competition/models.py @@ -59,15 +59,21 @@ class State(BaseModel): STARTED = "started" FINISHED = "finished" - user = models.ForeignKey(User, verbose_name="участник", on_delete=models.CASCADE) - competition = models.ForeignKey(Competition, verbose_name="соревнование", on_delete=models.CASCADE) + user = models.ForeignKey( + User, verbose_name="участник", on_delete=models.CASCADE + ) + competition = models.ForeignKey( + Competition, verbose_name="соревнование", on_delete=models.CASCADE + ) state = models.CharField( choices=StateChoices.choices, verbose_name="статус", max_length=11, 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: verbose_name = "участие" diff --git a/services/backend/apps/review/admin.py b/services/backend/apps/review/admin.py index 5bf4557..e40477c 100644 --- a/services/backend/apps/review/admin.py +++ b/services/backend/apps/review/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from apps.review.models import Reviewer, Review +from apps.review.models import Review, Reviewer @admin.register(Reviewer) diff --git a/services/backend/apps/review/apps.py b/services/backend/apps/review/apps.py index 138bf7f..b144226 100644 --- a/services/backend/apps/review/apps.py +++ b/services/backend/apps/review/apps.py @@ -7,4 +7,4 @@ class CoreConfig(AppConfig): verbose_name = "Проверка" def ready(self): - import apps.review.signals + pass diff --git a/services/backend/apps/review/migrations/0003_alter_review_evaluation.py b/services/backend/apps/review/migrations/0003_alter_review_evaluation.py new file mode 100644 index 0000000..8c0f21b --- /dev/null +++ b/services/backend/apps/review/migrations/0003_alter_review_evaluation.py @@ -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='оценка'), + ), + ] diff --git a/services/backend/apps/review/models.py b/services/backend/apps/review/models.py index 6a45e54..89c472f 100644 --- a/services/backend/apps/review/models.py +++ b/services/backend/apps/review/models.py @@ -45,7 +45,7 @@ class Review(BaseModel): ) def __str__(self): - return str(self.id) + return f"{str(self.reviewer)} -> {self.submission.task.title} ({self.submission.user.username})" class Meta: verbose_name = "проверка" diff --git a/services/backend/apps/user/apps.py b/services/backend/apps/user/apps.py index 5a1c14f..5674400 100644 --- a/services/backend/apps/user/apps.py +++ b/services/backend/apps/user/apps.py @@ -7,4 +7,4 @@ class UsersConfig(AppConfig): verbose_name = "контестанты" def ready(self): - import apps.user.signals + pass diff --git a/services/checker/config.py b/services/checker/config.py index 6cf9978..c6b7dd9 100644 --- a/services/checker/config.py +++ b/services/checker/config.py @@ -8,5 +8,6 @@ BASE_DIR = Path(__file__).resolve().parent load_dotenv(BASE_DIR / ".env") 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", )