Merge branches 'master' and 'master' of gitlab.prodcontest.ru:team-15/project

This commit is contained in:
ITQ
2025-03-02 17:08:05 +03:00
85 changed files with 2065 additions and 422 deletions
+29 -1
View File
@@ -1,6 +1,10 @@
from django.contrib import admin
from apps.task.models import CompetitionTask, CompetitionTaskAttachment
from apps.task.models import (
CompetitionTask,
CompetitionTaskAttachment,
CompetitionTaskSubmission,
)
class CompletionAttachmentInline(admin.StackedInline):
@@ -11,6 +15,30 @@ class CompletionAttachmentInline(admin.StackedInline):
@admin.register(CompetitionTask)
class CompetitionTaskAdmin(admin.ModelAdmin):
list_display = ("title", "type", "points")
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",
)
filter = ("plagiarism_checked",)
ordering = ["-timestamp"]
def has_add_permission(self, request, obj=None):
return False
def has_delete_permission(self, request, obj=None):
return False
class CompetitionTaskInline(admin.StackedInline):
@@ -1,4 +1,4 @@
# Generated by Django 5.1.6 on 2025-03-02 06:13
# Generated by Django 5.1.6 on 2025-03-02 10:28
import apps.task.models
import django.db.models.deletion
@@ -13,6 +13,7 @@ class Migration(migrations.Migration):
dependencies = [
('competition', '0001_initial'),
('review', '0001_initial'),
('user', '0001_initial'),
]
@@ -30,6 +31,7 @@ class Migration(migrations.Migration):
('points', models.IntegerField(blank=True, null=True, verbose_name='баллы за задание')),
('answer_file_path', models.TextField(blank=True, default='stdout', help_text='Путь до файла в котором ожидается результат. Пример: stdout или ./output.txt', null=True, verbose_name='куда сделать вывод программы участнику')),
('competition', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='competition.competition')),
('reviewers', models.ManyToManyField(blank=True, to='review.reviewer')),
],
options={
'verbose_name': 'задание',
@@ -40,10 +42,10 @@ class Migration(migrations.Migration):
name='CompetitionTaskAttachment',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('file', models.FileField(upload_to=apps.task.models.CompetitionTaskAttachment.file_upload_at)),
('bind_at', models.FilePathField()),
('public', models.BooleanField(default=False)),
('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='task.competitiontask')),
('file', models.FileField(upload_to=apps.task.models.CompetitionTaskAttachment.file_upload_at, verbose_name='файл')),
('bind_at', models.FilePathField(verbose_name='путь сохранения')),
('public', models.BooleanField(default=False, verbose_name='публичный')),
('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='task.competitiontask', verbose_name='задание')),
],
options={
'abstract': False,
@@ -67,7 +69,7 @@ class Migration(migrations.Migration):
name='CompetitionTaskSubmission',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('status', models.CharField(choices=[('sent', 'Sent'), ('checking', 'Checking'), ('checked', 'Checked')], default='sent', max_length=8)),
('status', models.CharField(choices=[('sent', 'Sent'), ('checking', 'Checking'), ('checked', 'Checked')], default='sent', max_length=8, verbose_name='статус')),
('content', models.FileField(upload_to=apps.task.models.CompetitionTaskSubmission.submission_content_upload_to)),
('stdout', models.FileField(blank=True, null=True, upload_to=apps.task.models.CompetitionTaskSubmission.submission_stdout_upload_to)),
('result', models.JSONField(blank=True, default=None, null=True)),
@@ -0,0 +1,71 @@
# Generated by Django 5.1.6 on 2025-03-02 12:09
import apps.task.models
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('review', '0002_initial'),
('task', '0001_initial'),
('user', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='competitiontasksubmission',
options={'verbose_name': 'посылка', 'verbose_name_plural': 'посылки'},
),
migrations.AlterField(
model_name='competitiontask',
name='reviewers',
field=models.ManyToManyField(blank=True, help_text='Справа отображаются действующие проверяющие, слева - доступные для выбора. Для перемещения можно кликнуть 2 раза по проверяющему', to='review.reviewer', verbose_name='ревьюверы'),
),
migrations.AlterField(
model_name='competitiontasksubmission',
name='checked_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='дата проверки'),
),
migrations.AlterField(
model_name='competitiontasksubmission',
name='content',
field=models.FileField(upload_to=apps.task.models.CompetitionTaskSubmission.submission_content_upload_to, verbose_name='содержание посылки'),
),
migrations.AlterField(
model_name='competitiontasksubmission',
name='earned_points',
field=models.IntegerField(blank=True, null=True, verbose_name='баллы за задание'),
),
migrations.AlterField(
model_name='competitiontasksubmission',
name='plagiarism_checked',
field=models.BooleanField(default=False, verbose_name='проверено на плагиат'),
),
migrations.AlterField(
model_name='competitiontasksubmission',
name='result',
field=models.JSONField(blank=True, default=None, null=True, verbose_name='результат проверки'),
),
migrations.AlterField(
model_name='competitiontasksubmission',
name='stdout',
field=models.FileField(blank=True, help_text='Используется только при проверке чекером', null=True, upload_to=apps.task.models.CompetitionTaskSubmission.submission_stdout_upload_to, verbose_name='вывод программы'),
),
migrations.AlterField(
model_name='competitiontasksubmission',
name='task',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='task.competitiontask', verbose_name='задание'),
),
migrations.AlterField(
model_name='competitiontasksubmission',
name='timestamp',
field=models.DateTimeField(auto_now_add=True, verbose_name='дата отправки'),
),
migrations.AlterField(
model_name='competitiontasksubmission',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='user.user', verbose_name='пользователь'),
),
]
@@ -1,40 +0,0 @@
# Generated by Django 5.1.6 on 2025-03-02 08:50
import apps.task.models
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('task', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='competitiontask',
name='attachments',
field=models.ManyToManyField(blank=True, related_name='tasks_attachments', to='task.competitiontaskattachment'),
),
migrations.AlterField(
model_name='competitiontaskattachment',
name='bind_at',
field=models.FilePathField(verbose_name='путь сохранения'),
),
migrations.AlterField(
model_name='competitiontaskattachment',
name='file',
field=models.FileField(upload_to=apps.task.models.CompetitionTaskAttachment.file_upload_at, verbose_name='файл'),
),
migrations.AlterField(
model_name='competitiontaskattachment',
name='public',
field=models.BooleanField(default=False, verbose_name='публичный'),
),
migrations.AlterField(
model_name='competitiontaskattachment',
name='task',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='task.competitiontask', verbose_name='задание'),
),
]
@@ -0,0 +1,19 @@
# Generated by Django 5.1.6 on 2025-03-02 12:23
import tinymce.models
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('task', '0002_alter_competitiontasksubmission_options_and_more'),
]
operations = [
migrations.AlterField(
model_name='competitiontask',
name='description',
field=tinymce.models.HTMLField(verbose_name='описание'),
),
]
@@ -1,17 +0,0 @@
# Generated by Django 5.1.6 on 2025-03-02 09:31
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('task', '0002_competitiontask_attachments_and_more'),
]
operations = [
migrations.RemoveField(
model_name='competitiontask',
name='attachments',
),
]
@@ -0,0 +1,24 @@
# Generated by Django 5.1.6 on 2025-03-02 12:49
import martor.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('task', '0003_alter_competitiontask_description'),
]
operations = [
migrations.AddField(
model_name='competitiontask',
name='submission_reviewers_count',
field=models.PositiveSmallIntegerField(blank=True, default=1, null=True),
),
migrations.AlterField(
model_name='competitiontask',
name='description',
field=martor.models.MartorField(verbose_name='описание'),
),
]
+80 -37
View File
@@ -1,10 +1,12 @@
from uuid import uuid4
from django.db import models
from tinymce.models import HTMLField
from django.db.models import Count, Q
from martor.models import MartorField
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
@@ -15,14 +17,14 @@ 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
)
competition = models.ForeignKey(Competition, on_delete=models.CASCADE)
title = models.CharField(verbose_name="заголовок", max_length=50)
description = HTMLField(verbose_name="описание", max_length=300)
description = MartorField(verbose_name="описание")
max_attempts = models.PositiveSmallIntegerField(null=True, blank=True)
type = models.CharField(
choices=CompetitionTaskType, max_length=8, verbose_name="тип проверки"
@@ -48,6 +50,17 @@ class CompetitionTask(BaseModel):
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
)
def __str__(self):
return self.title
@@ -69,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="публичный")
@@ -86,60 +99,90 @@ 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)
task = models.ForeignKey(CompetitionTask, on_delete=models.CASCADE)
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)
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="Используется только при проверке чекером",
)
# 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)
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)
earned_points = models.IntegerField(
null=True, blank=True, verbose_name="баллы за задание"
)
checked_at = models.DateTimeField(null=True, blank=True)
plagiarism_checked = models.BooleanField(default=False)
timestamp = models.DateTimeField(auto_now_add=True)
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 = "посылка"
verbose_name_plural = "посылки"
def __str__(self):
return str(self.id)
def send_on_review(self):
if not self.task.reviewers.exists():
return
reviewer = (
self.task.reviewers.annotate(
pending_count=Count(
"review",
filter=Q(
review__state__in=[
ReviewStatusChoices.NOT_CHECKED,
ReviewStatusChoices.CHECKING,
]
),
)
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.create(
reviewer=reviewer,
submission=self,
)
.order_by("pending_count")
.first()
)
review = Review.objects.create(
reviewer=reviewer,
submission=self,
)
@@ -28,5 +28,3 @@ with open("file.txt") as f:
print(result)
self.assertTrue(result["success"])
self.assertTrue(result["match"])