from uuid import uuid4 from django.core.exceptions import ValidationError from django.core.validators import RegexValidator from django.db import models from django.db.models import Count, Q 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, )