mirror of
https://gitlab.com/megazordpobeda/DataRush.git
synced 2026-05-23 21:27:10 +00:00
chore(backend): improvements
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
+34
@@ -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='пользователь'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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 = "выданная ачивка"
|
||||||
|
|||||||
@@ -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,14 +33,21 @@ 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 = "Лидерборд"
|
||||||
@@ -50,17 +56,23 @@ class CompetitionAdmin(admin.ModelAdmin):
|
|||||||
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(
|
leaderboard = (
|
||||||
|
User.objects.annotate(
|
||||||
total_score=Sum(
|
total_score=Sum(
|
||||||
'competitiontasksubmission__earned_points',
|
"competitiontasksubmission__earned_points",
|
||||||
filter=Q(
|
filter=Q(
|
||||||
competitiontasksubmission__status='checked',
|
competitiontasksubmission__status="checked",
|
||||||
competitiontasksubmission__task__in=competition_tasks
|
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 = (
|
||||||
|
User.objects.annotate(
|
||||||
total_score=Sum(
|
total_score=Sum(
|
||||||
'competitiontasksubmission__earned_points',
|
"competitiontasksubmission__earned_points",
|
||||||
filter=Q(competitiontasksubmission__status='checked')
|
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)
|
||||||
|
|||||||
@@ -7,4 +7,4 @@ class CompetitionsConfig(AppConfig):
|
|||||||
verbose_name = "Соревнование"
|
verbose_name = "Соревнование"
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
import apps.competition.signals
|
pass
|
||||||
|
|||||||
+40
@@ -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='участник'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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,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)
|
||||||
|
|||||||
@@ -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='оценка'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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 = "проверка"
|
||||||
|
|||||||
@@ -7,4 +7,4 @@ class UsersConfig(AppConfig):
|
|||||||
verbose_name = "контестанты"
|
verbose_name = "контестанты"
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
import apps.user.signals
|
pass
|
||||||
|
|||||||
@@ -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",
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user