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