diff --git a/services/backend/api/v1/achievement/schemas.py b/services/backend/api/v1/achievement/schemas.py index a869f0c..ecc1c5d 100644 --- a/services/backend/api/v1/achievement/schemas.py +++ b/services/backend/api/v1/achievement/schemas.py @@ -1,4 +1,4 @@ -from ninja import ModelSchema, Schema +from ninja import ModelSchema from apps.achievement.models import Achievement @@ -6,4 +6,9 @@ from apps.achievement.models import Achievement class AchievementSchema(ModelSchema): class Meta: model = Achievement - fields = ("id", "name", "description", "icon",) + fields = ( + "id", + "name", + "description", + "icon", + ) diff --git a/services/backend/api/v1/achievement/views.py b/services/backend/api/v1/achievement/views.py index d6e2a5a..804348a 100644 --- a/services/backend/api/v1/achievement/views.py +++ b/services/backend/api/v1/achievement/views.py @@ -2,9 +2,9 @@ from http import HTTPStatus as status from ninja import Router -from apps.achievement.models import Achievement from api.v1.achievement.schemas import AchievementSchema from api.v1.schemas import UnauthorizedError +from apps.achievement.models import Achievement router = Router() @@ -14,7 +14,7 @@ router = Router() response={ status.OK: list[AchievementSchema], status.UNAUTHORIZED: UnauthorizedError, - } + }, ) def get_all_achievements(request): return Achievement.objects.all() diff --git a/services/backend/api/v1/review/views.py b/services/backend/api/v1/review/views.py index 772ade8..05c2e3a 100644 --- a/services/backend/api/v1/review/views.py +++ b/services/backend/api/v1/review/views.py @@ -87,7 +87,9 @@ def evaluate_submission( review.submission.checked_at = datetime.now() review.save() - submission_evaluations = Review.objects.filter(submission=submission).values_list('evaluation', flat=True) + submission_evaluations = Review.objects.filter( + submission=submission + ).values_list("evaluation", flat=True) marks = [] for evaluation in submission_evaluations: @@ -97,9 +99,7 @@ def evaluate_submission( marks.append(mark) earned_points = median(marks) - review.submission.earned_points = ( - earned_points - ) + review.submission.earned_points = earned_points all_checked = not submission.reviews.exclude( state=ReviewStatusChoices.CHECKED diff --git a/services/backend/api/v1/task/schemas.py b/services/backend/api/v1/task/schemas.py index 054a9b5..ef00e01 100644 --- a/services/backend/api/v1/task/schemas.py +++ b/services/backend/api/v1/task/schemas.py @@ -2,17 +2,24 @@ from typing import Literal from uuid import UUID from ninja import ModelSchema, Schema -from pydantic import Field -from apps.task.models import CompetitionTask, CompetitionTaskSubmission, CompetitionTaskAttachment +from apps.task.models import ( + CompetitionTask, + CompetitionTaskAttachment, + CompetitionTaskSubmission, +) class TaskOutSchema(ModelSchema): status: Literal["sent", "checked", "checking", "not_submitted"] = None @staticmethod - def resolve_status(self, context) -> Literal["sent", "checked", "checking", "not_submitted"]: - if submission := CompetitionTaskSubmission.objects.filter(task=self, user=context.get("request").auth).first(): + def resolve_status( + self, context + ) -> Literal["sent", "checked", "checking", "not_submitted"]: + if submission := CompetitionTaskSubmission.objects.filter( + task=self, user=context.get("request").auth + ).first(): return submission.status return "not_submitted" @@ -38,10 +45,19 @@ class HistorySubmissionOut(ModelSchema): class Meta: model = CompetitionTaskSubmission - fields = ("id", "earned_points", "timestamp", "content",) + fields = ( + "id", + "earned_points", + "timestamp", + "content", + ) class TaskAttachmentSchema(ModelSchema): class Meta: model = CompetitionTaskAttachment - fields = ("id", "file", "public",) + fields = ( + "id", + "file", + "public", + ) diff --git a/services/backend/apps/achievement/admin.py b/services/backend/apps/achievement/admin.py index 4657244..9d7822d 100644 --- a/services/backend/apps/achievement/admin.py +++ b/services/backend/apps/achievement/admin.py @@ -5,5 +5,11 @@ from apps.achievement.models import Achievement @admin.register(Achievement) class AchievementAdmin(admin.ModelAdmin): - list_display = ("id", "name",) - search_fields = ("name", "description",) + list_display = ( + "id", + "name", + ) + search_fields = ( + "name", + "description", + ) diff --git a/services/backend/apps/achievement/apps.py b/services/backend/apps/achievement/apps.py index 2e3e606..4c9ddeb 100644 --- a/services/backend/apps/achievement/apps.py +++ b/services/backend/apps/achievement/apps.py @@ -2,6 +2,6 @@ from django.apps import AppConfig class AchievementConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'apps.achievement' + default_auto_field = "django.db.models.BigAutoField" + name = "apps.achievement" verbose_name = "Ачивки" diff --git a/services/backend/apps/achievement/models.py b/services/backend/apps/achievement/models.py index ef0689a..2c7724f 100644 --- a/services/backend/apps/achievement/models.py +++ b/services/backend/apps/achievement/models.py @@ -2,24 +2,24 @@ from django.db import models from apps.core.models import BaseModel + class Achievement(BaseModel): class AchievementType(models.TextChoices): CORRECT_TASKS = "correct_tasks", "Выполненные задания" def image_url_upload_to(instance, filename): - return f"/achievements/{instance.id}/icon" + return f"achievements/{instance.id}/icon/{filename}" - name = models.CharField(max_length=30, verbose_name="название", - unique=True) + name = models.CharField( + max_length=30, verbose_name="название", unique=True + ) description = models.TextField(verbose_name="описание") icon = models.FileField( verbose_name="иконка достижения", upload_to=image_url_upload_to, ) - slug = models.SlugField( - verbose_name="слаг", unique=True - ) + slug = models.SlugField(verbose_name="слаг", unique=True) def __str__(self): return self.name diff --git a/services/backend/apps/competition/models.py b/services/backend/apps/competition/models.py index 18212f4..5d9880f 100644 --- a/services/backend/apps/competition/models.py +++ b/services/backend/apps/competition/models.py @@ -15,7 +15,7 @@ class Competition(BaseModel): SOLO = "solo", "Индивидуальный" def image_url_upload_to(instance, filename): - return f"/competitions/{instance.id}/image" + return f"competitions/{instance.id}/image/{filename}" title = models.CharField(max_length=100, verbose_name="название") description = models.TextField(verbose_name="описание") diff --git a/services/backend/apps/core/admin.py b/services/backend/apps/core/admin.py index 3bc8edf..4027d38 100644 --- a/services/backend/apps/core/admin.py +++ b/services/backend/apps/core/admin.py @@ -1,4 +1,4 @@ from django.contrib import admin -from django.contrib.auth.models import Group, User +from django.contrib.auth.models import Group admin.site.unregister(Group) diff --git a/services/backend/apps/core/management/commands/generate_data.py b/services/backend/apps/core/management/commands/generate_data.py index 754f5e0..d78d0a5 100644 --- a/services/backend/apps/core/management/commands/generate_data.py +++ b/services/backend/apps/core/management/commands/generate_data.py @@ -8,7 +8,7 @@ from django.core.management.base import BaseCommand from django.utils import timezone from apps.competition.models import Competition, State -from apps.review.models import Review, Reviewer +from apps.review.models import Reviewer from apps.task.models import CompetitionTask, CompetitionTaskSubmission from apps.user.models import User, UserRole diff --git a/services/backend/apps/review/admin.py b/services/backend/apps/review/admin.py index c173af7..653d999 100644 --- a/services/backend/apps/review/admin.py +++ b/services/backend/apps/review/admin.py @@ -1,9 +1,15 @@ from django.contrib import admin -from apps.review.models import Review, Reviewer +from apps.review.models import Reviewer @admin.register(Reviewer) class ReviewersAdmin(admin.ModelAdmin): - list_display = ("name", "surname",) - search_fields = ("name", "surname",) + list_display = ( + "name", + "surname", + ) + search_fields = ( + "name", + "surname", + ) diff --git a/services/backend/apps/review/models.py b/services/backend/apps/review/models.py index 5fd4fb9..3c1201a 100644 --- a/services/backend/apps/review/models.py +++ b/services/backend/apps/review/models.py @@ -24,22 +24,24 @@ class ReviewStatusChoices(models.TextChoices): class Review(BaseModel): - reviewer = models.ForeignKey(Reviewer, on_delete=models.CASCADE, - verbose_name="проверяющий") + reviewer = models.ForeignKey( + Reviewer, on_delete=models.CASCADE, verbose_name="проверяющий" + ) submission = models.ForeignKey( "task.CompetitionTaskSubmission", on_delete=models.CASCADE, related_name="reviews", - verbose_name="посылка" + verbose_name="посылка", ) - evaluation = models.JSONField(default=list, null=True, blank=True, - verbose_name="выполнение") + evaluation = models.JSONField( + default=list, null=True, blank=True, verbose_name="выполнение" + ) state = models.CharField( choices=ReviewStatusChoices.choices, default=ReviewStatusChoices.NOT_CHECKED.value, max_length=11, - verbose_name="состояние" + verbose_name="состояние", ) def __str__(self): diff --git a/services/backend/apps/task/admin.py b/services/backend/apps/task/admin.py index a09f852..1cf4361 100644 --- a/services/backend/apps/task/admin.py +++ b/services/backend/apps/task/admin.py @@ -1,7 +1,10 @@ from django.contrib import admin -from apps.task.models import CompetitionTask, CompetitionTaskAttachment, \ - CompetitionTaskSubmission +from apps.task.models import ( + CompetitionTask, + CompetitionTaskAttachment, + CompetitionTaskSubmission, +) class CompletionAttachmentInline(admin.StackedInline): @@ -12,15 +15,22 @@ class CompletionAttachmentInline(admin.StackedInline): @admin.register(CompetitionTask) class CompetitionTaskAdmin(admin.ModelAdmin): list_display = ("title", "type", "points") - filter_horizontal = ( - "reviewers", - ) + filter_horizontal = ("reviewers",) @admin.register(CompetitionTaskSubmission) class CompetitionTaskSubmissionAdmin(admin.ModelAdmin): - list_display = ("task", "user", "status",) - search_fields = ("task__id", "task__title", "user__username", "user__email") + list_display = ( + "task", + "user", + "status", + ) + search_fields = ( + "task__id", + "task__title", + "user__username", + "user__email", + ) filter = ("plagiarism_checked",) ordering = ["-timestamp"] diff --git a/services/backend/apps/task/models.py b/services/backend/apps/task/models.py index 58d26bc..17232b8 100644 --- a/services/backend/apps/task/models.py +++ b/services/backend/apps/task/models.py @@ -2,12 +2,11 @@ from uuid import uuid4 from django.db import models from django.db.models import Count, Q -from tinymce.models import HTMLField from martor.models import MartorField from apps.competition.models import Competition from apps.core.models import BaseModel -from apps.review.models import Review, ReviewStatusChoices, Reviewer +from apps.review.models import Review, Reviewer, ReviewStatusChoices from apps.user.models import User @@ -18,7 +17,7 @@ class CompetitionTask(BaseModel): REVIEW = "review", "Ручная" def answer_file_upload_to(instance, filename) -> str: - return f"/tasks/{instance.id}/answer/{uuid4()}/filename" + return f"tasks/{instance.id}/answer/{uuid4()}/{filename}" in_competition_position = models.PositiveSmallIntegerField( null=True, blank=True @@ -56,9 +55,11 @@ class CompetitionTask(BaseModel): Reviewer, blank=True, verbose_name="ревьюверы", - help_text="Справа отображаются действующие проверяющие, слева - доступные для выбора. Для перемещения можно кликнуть 2 раза по проверяющему" + help_text="Справа отображаются действующие проверяющие, слева - доступные для выбора. Для перемещения можно кликнуть 2 раза по проверяющему", + ) + submission_reviewers_count = models.PositiveSmallIntegerField( + default=1, null=True, blank=True ) - submission_reviewers_count = models.PositiveSmallIntegerField(default=1, null=True, blank=True) def __str__(self): return self.title @@ -81,12 +82,12 @@ class CompetitionTaskCriteria(BaseModel): class CompetitionTaskAttachment(BaseModel): def file_upload_at(instance, filename): - return f"/attachment/{instance.id}/file" + return f"attachment/{instance.id}/file/{filename}" - task = models.ForeignKey(CompetitionTask, on_delete=models.CASCADE, - verbose_name="задание") - file = models.FileField(upload_to=file_upload_at, - verbose_name="файл") + task = models.ForeignKey( + CompetitionTask, on_delete=models.CASCADE, verbose_name="задание" + ) + file = models.FileField(upload_to=file_upload_at, verbose_name="файл") bind_at = models.FilePathField(verbose_name="путь сохранения") public = models.BooleanField(default=False, verbose_name="публичный") @@ -98,50 +99,61 @@ class CompetitionTaskSubmission(BaseModel): CHECKED = "checked" def submission_content_upload_to(instance, filename) -> str: - return f"submissions/{instance.id}/content" + return f"submissions/{instance.id}/content/{filename}" def submission_stdout_upload_to(instance, filename) -> str: - return f"/submissions/{instance.id}/stdout" + return f"submissions/{instance.id}/stdout/{filename}" - user = models.ForeignKey(User, on_delete=models.CASCADE, - verbose_name="пользователь") - task = models.ForeignKey(CompetitionTask, on_delete=models.CASCADE, - verbose_name="задание") + user = models.ForeignKey( + User, on_delete=models.CASCADE, verbose_name="пользователь" + ) + task = models.ForeignKey( + CompetitionTask, on_delete=models.CASCADE, verbose_name="задание" + ) status = models.CharField( choices=StatusChoices.choices, default=StatusChoices.SENT, max_length=8, - verbose_name="статус" + verbose_name="статус", ) # code or text or file - content = models.FileField(upload_to=submission_content_upload_to, - verbose_name="содержание посылки") + content = models.FileField( + upload_to=submission_content_upload_to, + verbose_name="содержание посылки", + ) # only if task type is checker stdout = models.FileField( - upload_to=submission_stdout_upload_to, null=True, blank=True, + upload_to=submission_stdout_upload_to, + null=True, + blank=True, verbose_name="вывод программы", - help_text="Используется только при проверке чекером" + help_text="Используется только при проверке чекером", ) # depends on task type: # - input: {"correct": boolean} # - file: {"total_points": integer, "by_criteria": ["criteria_name": integer]} # - code: {"correct": boolean} - result = models.JSONField(default=None, null=True, blank=True, - verbose_name="результат проверки") + result = models.JSONField( + default=None, null=True, blank=True, verbose_name="результат проверки" + ) # just more readable result representation, maybe will be calcuated somehow more complex depends on criteria - earned_points = models.IntegerField(null=True, blank=True, - verbose_name="баллы за задание") + earned_points = models.IntegerField( + null=True, blank=True, verbose_name="баллы за задание" + ) - checked_at = models.DateTimeField(null=True, blank=True, - verbose_name="дата проверки") - plagiarism_checked = models.BooleanField(default=False, - verbose_name="проверено на плагиат") - timestamp = models.DateTimeField(auto_now_add=True, - verbose_name="дата отправки") + checked_at = models.DateTimeField( + null=True, blank=True, verbose_name="дата проверки" + ) + plagiarism_checked = models.BooleanField( + default=False, verbose_name="проверено на плагиат" + ) + timestamp = models.DateTimeField( + auto_now_add=True, verbose_name="дата отправки" + ) class Meta: verbose_name = "посылка" @@ -156,16 +168,18 @@ class CompetitionTaskSubmission(BaseModel): reviewers_count = self.task.submission_reviewers_count reviewers = self.task.reviewers.annotate( - pending_count=Count( - "review", - filter=Q( - review__state__in=[ - ReviewStatusChoices.NOT_CHECKED, - ReviewStatusChoices.CHECKING, - ] - ), - ) - ).order_by("pending_count")[:reviewers_count] # да это медленно работает и чо + pending_count=Count( + "review", + filter=Q( + review__state__in=[ + ReviewStatusChoices.NOT_CHECKED, + ReviewStatusChoices.CHECKING, + ] + ), + ) + ).order_by("pending_count")[ + :reviewers_count + ] # да это медленно работает и чо for reviewer in reviewers: Review.objects.create( diff --git a/services/backend/apps/task/tasks.py b/services/backend/apps/task/tasks.py index a4a8eee..56997ea 100644 --- a/services/backend/apps/task/tasks.py +++ b/services/backend/apps/task/tasks.py @@ -6,8 +6,8 @@ import sys import tempfile from io import StringIO -from config.celery import app from apps.task.models import CompetitionTaskSubmission +from config.celery import app ALLOWED_MODULES = { "pandas", @@ -119,7 +119,12 @@ def secure_exec(code_str, result_path, input_files=None): @app.task(bind=True) def analyze_data_task( - self, code_str, result_path, expected_file_link, submission_id, input_files=[] + self, + code_str, + result_path, + expected_file_link, + submission_id, + input_files=[], ): try: validate_code(code_str) @@ -130,7 +135,9 @@ def analyze_data_task( expected_hash = hashlib.sha256(expected_bytes).hexdigest() with contextlib.suppress(CompetitionTaskSubmission.DoesNotExist): - submission = CompetitionTaskSubmission.objects.get(id=submission_id) + submission = CompetitionTaskSubmission.objects.get( + id=submission_id + ) submission.result = {"correct": True} return { diff --git a/services/backend/apps/task/tests/test_tasks.py b/services/backend/apps/task/tests/test_tasks.py index 4209d70..b61bb11 100644 --- a/services/backend/apps/task/tests/test_tasks.py +++ b/services/backend/apps/task/tests/test_tasks.py @@ -28,5 +28,3 @@ with open("file.txt") as f: print(result) self.assertTrue(result["success"]) self.assertTrue(result["match"]) - - diff --git a/services/backend/apps/user/models.py b/services/backend/apps/user/models.py index 61a9b9c..aaa0ec0 100644 --- a/services/backend/apps/user/models.py +++ b/services/backend/apps/user/models.py @@ -17,8 +17,9 @@ class User(BaseModel): created_at = models.DateTimeField(auto_now=True) - achievements = models.ManyToManyField(Achievement, blank=True, - verbose_name="ачивки пользователя") + achievements = models.ManyToManyField( + Achievement, blank=True, verbose_name="ачивки пользователя" + ) @staticmethod def make_password(password: str): diff --git a/services/backend/config/settings.py b/services/backend/config/settings.py index 266990b..c89135b 100644 --- a/services/backend/config/settings.py +++ b/services/backend/config/settings.py @@ -478,29 +478,38 @@ TINYMCE_DEFAULT_CONFIG = { {"start": "######", "format": "h6"}, {"start": "1. ", "cmd": "InsertOrderedList"}, {"start": "* ", "cmd": "InsertUnorderedList"}, - {"start": "- ", "cmd": "InsertUnorderedList"} - ] + {"start": "- ", "cmd": "InsertUnorderedList"}, + ], } # martor -MARTOR_THEME = 'bootstrap' +MARTOR_THEME = "bootstrap" MARTOR_ENABLE_CONFIGS = { - 'emoji': 'true', # to enable/disable emoji icons. - 'imgur': 'true', # to enable/disable imgur/custom uploader. - 'mention': 'false', # to enable/disable mention - 'jquery': 'true', # to include/revoke jquery (require for admin default django) - 'living': 'false', # to enable/disable live updates in preview - 'spellcheck': 'false', # to enable/disable spellcheck in form textareas - 'hljs': 'true', # to enable/disable hljs highlighting in preview + "emoji": "true", # to enable/disable emoji icons. + "imgur": "true", # to enable/disable imgur/custom uploader. + "mention": "false", # to enable/disable mention + "jquery": "true", # to include/revoke jquery (require for admin default django) + "living": "false", # to enable/disable live updates in preview + "spellcheck": "false", # to enable/disable spellcheck in form textareas + "hljs": "true", # to enable/disable hljs highlighting in preview } MARTOR_TOOLBAR_BUTTONS = [ - 'bold', 'italic', 'horizontal', 'heading', 'pre-code', - 'blockquote', 'unordered-list', 'ordered-list', - 'link', 'emoji', - 'direct-mention', 'toggle-maximize', 'help' + "bold", + "italic", + "horizontal", + "heading", + "pre-code", + "blockquote", + "unordered-list", + "ordered-list", + "link", + "emoji", + "direct-mention", + "toggle-maximize", + "help", ] # GUID diff --git a/services/backend/config/urls.py b/services/backend/config/urls.py index 4e50a9a..6fe96c2 100644 --- a/services/backend/config/urls.py +++ b/services/backend/config/urls.py @@ -15,7 +15,7 @@ urlpatterns = [ # tinymce path("tinymce/", include("tinymce.urls")), # martor - path('martor/', include('martor.urls')), + path("martor/", include("martor.urls")), # Admin urls path("admin/", admin.site.urls), # API urls