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, )