feat: added achievements

This commit is contained in:
Андрей Сумин
2025-03-03 01:54:20 +03:00
parent 0b032100f8
commit ab90d362a2
26 changed files with 163 additions and 37 deletions
+13 -2
View File
@@ -1,6 +1,7 @@
from ninja import ModelSchema from ninja import ModelSchema, Schema
from pydantic import Field
from apps.achievement.models import Achievement from apps.achievement.models import Achievement, UserAchievement
class AchievementSchema(ModelSchema): class AchievementSchema(ModelSchema):
@@ -12,3 +13,13 @@ class AchievementSchema(ModelSchema):
"description", "description",
"icon", "icon",
) )
class UserAchievementSchema(Schema):
name: str = Field(..., alias="achievement.name")
description: str = Field(..., alias="achievement.description")
icon: str = Field(..., alias="achievement.icon")
class Meta:
model = UserAchievement
fields = ("received_at",)
+3 -2
View File
@@ -6,15 +6,16 @@ from api.v1.achievement.schemas import AchievementSchema
from api.v1.schemas import UnauthorizedError from api.v1.schemas import UnauthorizedError
from apps.achievement.models import Achievement from apps.achievement.models import Achievement
router = Router() router = Router(tags=["achievement"])
@router.get( @router.get(
"", "all",
response={ response={
status.OK: list[AchievementSchema], status.OK: list[AchievementSchema],
status.UNAUTHORIZED: UnauthorizedError, status.UNAUTHORIZED: UnauthorizedError,
}, },
auth=None,
) )
def get_all_achievements(request): def get_all_achievements(request):
return Achievement.objects.all() return Achievement.objects.all()
+6
View File
@@ -3,6 +3,7 @@ from functools import partial
from ninja import NinjaAPI from ninja import NinjaAPI
from api.v1 import handlers from api.v1 import handlers
from api.v1.achievement.views import router as achievement_router
from api.v1.auth import BearerAuth from api.v1.auth import BearerAuth
from api.v1.competition.views import router as competition_router from api.v1.competition.views import router as competition_router
from api.v1.ping.views import router as ping_router from api.v1.ping.views import router as ping_router
@@ -49,6 +50,11 @@ router.add_router(
team_router, team_router,
auth=BearerAuth(), auth=BearerAuth(),
) )
router.add_router(
"achievements",
achievement_router,
auth=BearerAuth(),
)
for exception, handler in handlers.exception_handlers: for exception, handler in handlers.exception_handlers:
+6
View File
@@ -12,6 +12,7 @@ from api.v1.task.schemas import (
TaskOutSchema, TaskOutSchema,
TaskSubmissionOut, TaskSubmissionOut,
) )
from apps.achievement.models import Achievement, UserAchievement
from apps.competition.models import State from apps.competition.models import State
from apps.task.models import ( from apps.task.models import (
Competition, Competition,
@@ -102,6 +103,11 @@ def submit_task(
CompetitionTask, competition=competition, id=task_id CompetitionTask, competition=competition, id=task_id
) )
if not CompetitionTaskSubmission.objects.filter(user=user).exists():
first_steps_achievement = Achievement.objects.get(slug="first_steps")
UserAchievement.objects.create(
user=user, achievement=first_steps_achievement
)
if task.type == CompetitionTask.CompetitionTaskType.INPUT: if task.type == CompetitionTask.CompetitionTaskType.INPUT:
submission = CompetitionTaskSubmission.objects.create( submission = CompetitionTaskSubmission.objects.create(
user=user, user=user,
+11 -1
View File
@@ -1,5 +1,7 @@
from ninja import ModelSchema, Schema from ninja import ModelSchema, Schema
from api.v1.achievement.schemas import UserAchievementSchema
from apps.achievement.models import UserAchievement
from apps.user.models import User from apps.user.models import User
@@ -20,9 +22,17 @@ class LoginSchema(ModelSchema):
class UserSchema(ModelSchema): class UserSchema(ModelSchema):
achievements: list[UserAchievementSchema] = None
@staticmethod
def resolve_achievements(self, context):
return UserAchievement.objects.filter(
user=context.get("request").auth
).order_by("-received_at")
class Meta: class Meta:
model = User model = User
fields = ["id", "email", "username", "created_at", "achievements"] fields = ["id", "email", "username", "created_at"]
class StatSchema(Schema): class StatSchema(Schema):
+9 -10
View File
@@ -11,17 +11,18 @@ from api.v1.schemas import (
BadRequestError, BadRequestError,
ConflictError, ConflictError,
ForbiddenError, ForbiddenError,
NotFoundError, UnauthorizedError, NotFoundError,
UnauthorizedError,
) )
from api.v1.user.schemas import ( from api.v1.user.schemas import (
LoginSchema, LoginSchema,
RegisterSchema, RegisterSchema,
StatSchema,
TokenSchema, TokenSchema,
UserSchema, UserSchema,
StatSchema
) )
from apps.task.models import CompetitionTaskSubmission
from apps.user.models import User from apps.user.models import User
from apps.task.models import CompetitionTaskSubmission, ReviewStatusChoices
router = Router(tags=["user"]) router = Router(tags=["user"])
@@ -91,16 +92,15 @@ def get_user(request, user_id: str):
@router.get( @router.get(
"/me/stat", "/me/stat",
response={ response={status.OK: StatSchema, status.UNAUTHORIZED: UnauthorizedError},
status.OK: StatSchema,
status.UNAUTHORIZED: UnauthorizedError
},
) )
def get_my_stat(request): def get_my_stat(request):
user_submissions = CompetitionTaskSubmission.objects.filter( user_submissions = CompetitionTaskSubmission.objects.filter(
user=request.auth user=request.auth
) )
checked_attempts = user_submissions.filter(status=CompetitionTaskSubmission.StatusChoices.CHECKED).all() checked_attempts = user_submissions.filter(
status=CompetitionTaskSubmission.StatusChoices.CHECKED
).all()
success_attempts_cnt = 0 success_attempts_cnt = 0
for attempt in checked_attempts: for attempt in checked_attempts:
@@ -112,6 +112,5 @@ def get_my_stat(request):
success_attempts_cnt += 1 success_attempts_cnt += 1
return StatSchema( return StatSchema(
total_attempts=len(user_submissions), total_attempts=len(user_submissions), solved_tasks=success_attempts_cnt
solved_tasks=success_attempts_cnt
) )
Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

@@ -0,0 +1,32 @@
from django.conf import settings
from django.core.files import File
from django.core.management import BaseCommand
from apps.achievement.models import Achievement
icons_dir = f"{settings.BASE_DIR}/apps/achievement/icons"
class Command(BaseCommand):
help = "Create achievement fixtures."
def handle(self, *args, **options):
if not Achievement.objects.filter(slug="first_steps").exists():
with open(f"{icons_dir}/first_steps.png", "rb") as f:
first_steps_icon = File(f, name="first_steps.png")
Achievement.objects.get_or_create(
name="Первые шаги",
description="Отправьте свое первое решение на задачу!",
slug="first_steps",
icon=first_steps_icon,
)
if not Achievement.objects.filter(slug="welcome").exists():
with open(f"{icons_dir}/welcome.png", "rb") as f:
welcome_icon = File(f, name="welcome.png")
Achievement.objects.get_or_create(
name="Добро пожаловать!",
description="Зарегистрируйтесь на платформе",
slug="welcome",
icon=welcome_icon,
)
@@ -1,6 +1,7 @@
# Generated by Django 5.1.6 on 2025-03-02 21:24 # Generated by Django 5.1.6 on 2025-03-02 22:53
import apps.achievement.models import apps.achievement.models
import django.db.models.deletion
import uuid import uuid
from django.db import migrations, models from django.db import migrations, models
@@ -27,4 +28,15 @@ class Migration(migrations.Migration):
'verbose_name_plural': 'ачивки', 'verbose_name_plural': 'ачивки',
}, },
), ),
migrations.CreateModel(
name='UserAchievement',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('received_at', models.DateTimeField(auto_now_add=True)),
('achievement', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='achievement.achievement')),
],
options={
'abstract': False,
},
),
] ]
@@ -0,0 +1,22 @@
# Generated by Django 5.1.6 on 2025-03-02 22:53
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('achievement', '0001_initial'),
('user', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='userachievement',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='user.user'),
),
]
+7 -3
View File
@@ -4,9 +4,6 @@ from apps.core.models import BaseModel
class Achievement(BaseModel): class Achievement(BaseModel):
class AchievementType(models.TextChoices):
CORRECT_TASKS = "correct_tasks", "Выполненные задания"
def image_url_upload_to(instance, filename): def image_url_upload_to(instance, filename):
return f"achievements/{instance.id}/icon/{filename}" return f"achievements/{instance.id}/icon/{filename}"
@@ -27,3 +24,10 @@ class Achievement(BaseModel):
class Meta: class Meta:
verbose_name = "ачивка" verbose_name = "ачивка"
verbose_name_plural = "ачивки" verbose_name_plural = "ачивки"
class UserAchievement(BaseModel):
achievement = models.ForeignKey(Achievement, on_delete=models.CASCADE)
user = models.ForeignKey("user.User", on_delete=models.CASCADE)
received_at = models.DateTimeField(auto_now_add=True)
@@ -1,4 +1,4 @@
# Generated by Django 5.1.6 on 2025-03-02 21:24 # Generated by Django 5.1.6 on 2025-03-02 22:53
import apps.competition.models import apps.competition.models
import datetime import datetime
@@ -9,7 +9,11 @@ from django.utils import timezone
from apps.competition.models import Competition, State from apps.competition.models import Competition, State
from apps.review.models import Reviewer from apps.review.models import Reviewer
from apps.task.models import CompetitionTask, CompetitionTaskSubmission, CompetitionTaskCriteria from apps.task.models import (
CompetitionTask,
CompetitionTaskCriteria,
CompetitionTaskSubmission,
)
from apps.user.models import User, UserRole from apps.user.models import User, UserRole
@@ -92,7 +96,7 @@ class Command(BaseCommand):
task_types = [ task_types = [
CompetitionTask.CompetitionTaskType.INPUT.value, CompetitionTask.CompetitionTaskType.INPUT.value,
CompetitionTask.CompetitionTaskType.REVIEW.value, CompetitionTask.CompetitionTaskType.REVIEW.value,
CompetitionTask.CompetitionTaskType.INPUT.value CompetitionTask.CompetitionTaskType.INPUT.value,
] ]
for comp in competitions: for comp in competitions:
# Create 3 tasks per competition # Create 3 tasks per competition
@@ -110,7 +114,10 @@ class Command(BaseCommand):
submission_reviewers_count=random.randint(2, 10), submission_reviewers_count=random.randint(2, 10),
max_attempts=random.randint(1, 10), max_attempts=random.randint(1, 10),
) )
if task_type == CompetitionTask.CompetitionTaskType.REVIEW.value: if (
task_type
== CompetitionTask.CompetitionTaskType.REVIEW.value
):
for j in range(5): for j in range(5):
CompetitionTaskCriteria.objects.create( CompetitionTaskCriteria.objects.create(
task=task, task=task,
+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
@@ -1,4 +1,4 @@
# Generated by Django 5.1.6 on 2025-03-02 21:24 # Generated by Django 5.1.6 on 2025-03-02 22:53
import uuid import uuid
from django.db import migrations, models from django.db import migrations, models
@@ -1,4 +1,4 @@
# Generated by Django 5.1.6 on 2025-03-02 21:24 # Generated by Django 5.1.6 on 2025-03-02 22:53
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models
+2 -3
View File
@@ -2,13 +2,12 @@
from django.db.models.signals import m2m_changed from django.db.models.signals import m2m_changed
from django.dispatch import receiver from django.dispatch import receiver
from apps.review.models import Review
from apps.task.models import CompetitionTask, CompetitionTaskSubmission from apps.task.models import CompetitionTask, CompetitionTaskSubmission
@receiver(m2m_changed, sender=CompetitionTask.reviewers.through) @receiver(m2m_changed, sender=CompetitionTask.reviewers.through)
def print_reviewers(sender, instance, action, **kwargs): def print_reviewers(sender, instance, action, **kwargs):
if action in ['post_add', 'post_remove', 'post_clear']: if action in ["post_add", "post_remove", "post_clear"]:
submissions = CompetitionTaskSubmission.objects.filter(task=instance) submissions = CompetitionTaskSubmission.objects.filter(task=instance)
for submission in submissions: for submission in submissions:
submission.send_on_review() submission.send_on_review()
+1 -1
View File
@@ -33,7 +33,7 @@ class CompetitionTaskSubmissionAdmin(admin.ModelAdmin):
"user__username", "user__username",
"user__email", "user__email",
) )
list_filter = ("plagiarism_checked", "status",) list_filter = ("plagiarism_checked", "status")
ordering = ["-timestamp"] ordering = ["-timestamp"]
def has_add_permission(self, request, obj=None): def has_add_permission(self, request, obj=None):
@@ -1,4 +1,4 @@
# Generated by Django 5.1.6 on 2025-03-02 21:24 # Generated by Django 5.1.6 on 2025-03-02 22:53
import apps.task.models import apps.task.models
import django.db.models.deletion import django.db.models.deletion
+2 -3
View File
@@ -1,8 +1,7 @@
import httpx import httpx
from celery import shared_task from celery import shared_task
from django.core.files.base import ContentFile
from django.conf import settings from django.conf import settings
from django.core.files.base import ContentFile
@shared_task(bind=True, max_retries=3) @shared_task(bind=True, max_retries=3)
@@ -40,7 +39,7 @@ def analyze_data_task(self, submission_id):
) )
submission.status = CompetitionTaskSubmission.StatusChoices.CHECKED submission.status = CompetitionTaskSubmission.StatusChoices.CHECKED
except httpx.RequestError as e: except httpx.RequestError:
self.retry(countdown=2**self.request.retries) self.retry(countdown=2**self.request.retries)
except Exception as e: except Exception as e:
submission.result = {"error": str(e), "success": False} submission.result = {"error": str(e), "success": False}
@@ -1,4 +1,4 @@
# Generated by Django 5.1.6 on 2025-03-02 21:24 # Generated by Django 5.1.6 on 2025-03-02 22:53
import django.db.models.deletion import django.db.models.deletion
import uuid import uuid
+3
View File
@@ -6,3 +6,6 @@ class UsersConfig(AppConfig):
name = "apps.user" name = "apps.user"
label = "user" label = "user"
verbose_name = "Пользователи (веб)" verbose_name = "Пользователи (веб)"
def ready(self):
pass
@@ -1,4 +1,4 @@
# Generated by Django 5.1.6 on 2025-03-02 21:24 # Generated by Django 5.1.6 on 2025-03-02 22:53
import uuid import uuid
from django.db import migrations, models from django.db import migrations, models
+14
View File
@@ -0,0 +1,14 @@
from django.db.models.signals import post_save
from django.dispatch import receiver
from apps.achievement.models import Achievement, UserAchievement
from apps.user.models import User
@receiver(post_save, sender=User)
def assign_welcome_achievement(sender, instance, created, **kwargs):
if created:
welcome_achievement = Achievement.objects.get(slug="welcome")
UserAchievement.objects.create(
user=instance, achievement=welcome_achievement
)
+2 -1
View File
@@ -7,8 +7,9 @@ from pathlib import Path
import django_stubs_ext import django_stubs_ext
import environ import environ
from health_check.plugins import plugin_dir
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from health_check.plugins import plugin_dir
from integrations.checker.healthcheck import CheckerHealthCheck from integrations.checker.healthcheck import CheckerHealthCheck
BASE_DIR = Path(__file__).resolve().parent.parent BASE_DIR = Path(__file__).resolve().parent.parent