Files
DataRush/services/backend/apps/task/models.py
T
2025-03-04 01:13:16 +03:00

263 lines
9.8 KiB
Python

from sys import stdout
from uuid import uuid4
from django.db import models
from django.db.models import Count, Q
from django.core.validators import RegexValidator
from django.core.exceptions import ValidationError
from mdeditor.fields import MDTextField
from apps.competition.models import Competition
from apps.core.models import BaseModel
from apps.review.models import Review, Reviewer, ReviewStatusChoices
from apps.user.models import User
class CompetitionTask(BaseModel):
class CompetitionTaskType(models.TextChoices):
INPUT = "input", "Ввод правильного ответа"
CHECKER = "checker", "Ввод кода"
REVIEW = "review", "Ручная"
def answer_file_upload_to(instance, filename) -> str:
return f"tasks/{instance.id}/answer/{uuid4()}/{filename}"
in_competition_position = models.PositiveSmallIntegerField(
verbose_name="позиция в соревновании"
)
competition = models.ForeignKey(
Competition,
on_delete=models.CASCADE,
verbose_name="привязанное соревнование",
)
title = models.CharField(verbose_name="заголовок", max_length=50)
description = MDTextField(verbose_name="описание")
max_attempts = models.PositiveSmallIntegerField(
null=True, blank=True, verbose_name="максимальное кол-во попыток"
)
type = models.CharField(
choices=CompetitionTaskType, max_length=8, verbose_name="тип проверки"
)
# only when "input" or "checker" type
correct_answer_file = models.FileField(
upload_to=answer_file_upload_to,
null=True,
blank=True,
verbose_name="файл с правильным ответом",
help_text="Имеет смысл только при автоматической (ввод ответа или кода) проверке.",
)
points = models.IntegerField(
null=True, blank=True, verbose_name="общий балл за задание"
)
# only when "checker" type
answer_file_path = models.TextField(
null=True,
blank=True,
verbose_name="куда сделать вывод программы участнику",
help_text=(
"Путь до файла в котором ожидается результат. "
"Пример: stdout или ./output.txt. Имеет смысл только при автоматическом типе проверки."
),
default="stdout",
)
# only when "review" type
reviewers = models.ManyToManyField(
Reviewer,
blank=True,
verbose_name="ревьюверы",
help_text=(
"Справа отображаются действующие проверяющие, слева - доступные для выбора. "
"Для перемещения можно кликнуть 2 раза по проверяющему. Имеет смысл только"
" при ручном типе проверки."
),
)
submission_reviewers_count = models.PositiveSmallIntegerField(
default=1,
null=True,
blank=True,
verbose_name="кол-во проверяющих для зачета задачи",
)
def clean(self):
super().clean()
# if self.correct_answer_file and self.type not in ["checker", "input"]:
# raise ValidationError({
# "type": "Если загружен файл правильного ответа, то тип проверки не может быть ручным"
# })
if not self.correct_answer_file and self.type != "review":
raise ValidationError({
"correct_answer_file": "Загрузите правильный ответ"
})
# if self.answer_file_path and not self.type == "checker":
# raise ValidationError({
# "type": "Укажите другой тип задания: этот не совместим с путем правильного ответа"
# })
if not self.answer_file_path and self.type == "checker":
raise ValidationError({
"answer_file_path": "Введите путь правильного ответа - это нужно для корректной работы чекера"
})
if not self.reviewers and self.type == "review":
raise ValidationError({
"reviewers": "Загрузите ревьюверов - кто будет проверять задания, если не они?"
})
# elif self.reviewers and not self.type == "review":
# raise ValidationError({
# "type": "Проверьте тип - вы ввели ревьюверов, но задание не является ручным"
# })
def __str__(self):
return self.title
class Meta:
verbose_name = "задание"
verbose_name_plural = "задания"
class CompetitionTaskCriteria(BaseModel):
task = models.ForeignKey(
CompetitionTask, on_delete=models.CASCADE, related_name="criteries"
)
name = models.TextField(verbose_name="название")
slug = models.SlugField(verbose_name="техническое название")
description = models.TextField(verbose_name="описание критерии")
max_value = models.PositiveSmallIntegerField(
verbose_name="максимальное кол-во баллов"
)
def __str__(self):
return self.name
class Meta:
verbose_name = "критерий"
verbose_name_plural = "критерии"
class CompetitionTaskAttachment(BaseModel):
def file_upload_at(instance, filename) -> str:
return f"attachments/{instance.id}/file/{filename}"
task = models.ForeignKey(
CompetitionTask,
on_delete=models.CASCADE,
verbose_name="задание",
related_name="attachments",
)
file = models.FileField(upload_to=file_upload_at, verbose_name="файл")
bind_at = models.CharField(
verbose_name="путь сохранения",
max_length=255,
validators=[
RegexValidator(
r"^(?:[a-zA-Z]:\\(?:[^<>:\"\/\\|?*]*\\)*|/(?:[^<>:\"\/\\|?*]+/?)*)$",
message="Введите абсолютный путь до папки",
)
],
)
public = models.BooleanField(default=False, verbose_name="публичный")
class Meta:
verbose_name = "вложение"
verbose_name_plural = "вложения"
class CompetitionTaskSubmission(BaseModel):
class StatusChoices(models.TextChoices):
SENT = "sent", "Отправлено на проверку"
CHECKING = "checking", "Проверка"
CHECKED = "checked", "Проверено"
def submission_content_upload_to(instance, filename) -> str:
return f"submissions/{instance.id}/content/{filename}"
def submission_stdout_upload_to(instance, filename) -> str:
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="задание"
)
status = models.CharField(
choices=StatusChoices.choices,
default=StatusChoices.SENT,
max_length=8,
verbose_name="статус",
)
# code or text or file
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,
verbose_name="вывод программы",
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="результат проверки"
)
# 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="баллы за задание"
)
checked_at = models.DateTimeField(
null=True, blank=True, verbose_name="дата проверки"
)
plagiarism_detected = models.BooleanField(
default=False, verbose_name="обнаружен плагиат"
)
timestamp = models.DateTimeField(
auto_now_add=True, verbose_name="дата отправки"
)
class Meta:
verbose_name = "посылка"
verbose_name_plural = "посылки"
def __str__(self):
return str(self.id)
def send_on_review(self):
if not self.task.reviewers.exists():
return
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
] # да это медленно работает и чо
for reviewer in reviewers:
Review.objects.update_or_create(
reviewer=reviewer,
submission=self,
)