<type>(scope): <description>

[body]

[footer(s)]
This commit is contained in:
ITQ
2025-03-02 09:15:29 +03:00
parent f7ed2984fa
commit 71736c04aa
12 changed files with 83 additions and 76 deletions
+1 -5
View File
@@ -88,7 +88,7 @@ services:
image: gitlab.prodcontest.ru:5050/team-15/project/backend:latest image: gitlab.prodcontest.ru:5050/team-15/project/backend:latest
build: build:
context: ./services/backend context: ./services/backend
entrypoint: ["/app/scripts/celery-worker-entrypoint.sh"] command: celery -A config worker -l INFO
depends_on: depends_on:
redis: redis:
restart: false restart: false
@@ -106,10 +106,6 @@ services:
retries: 3 retries: 3
start_period: 10s start_period: 10s
start_interval: 2s start_interval: 2s
volumes:
- type: bind
source: ./infrastructure/backend/scripts
target: /app/scripts
restart: unless-stopped restart: unless-stopped
celery-exporter: celery-exporter:
+1 -1
View File
@@ -73,7 +73,7 @@ class SubmissionOut(ModelSchema):
"stdout", "stdout",
"result", "result",
"earned_points", "earned_points",
"reviewed_at", "checked_at",
) )
@@ -1,4 +1,4 @@
# Generated by Django 5.1.6 on 2025-03-02 00:16 # Generated by Django 5.1.6 on 2025-03-02 06:13
import apps.competition.models import apps.competition.models
import datetime import datetime
@@ -23,8 +23,8 @@ class Migration(migrations.Migration):
('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='описание')),
('image_url', models.ImageField(blank=True, null=True, upload_to=apps.competition.models.Competition.image_url_upload_to, verbose_name='изображение соревнования')), ('image_url', models.ImageField(blank=True, null=True, upload_to=apps.competition.models.Competition.image_url_upload_to, verbose_name='изображение соревнования')),
('end_date', models.DateTimeField(blank=True, null=True, verbose_name='дедлайн участия')), ('end_date', models.DateTimeField(blank=True, null=True, verbose_name='окончание соревнования')),
('start_date', models.DateTimeField(blank=True, null=True, verbose_name='дедлайн участия')), ('start_date', models.DateTimeField(blank=True, null=True, verbose_name='начало соревнования')),
('type', models.CharField(choices=[('edu', 'Образовательный'), ('competitive', 'Соревновательный')], max_length=11, verbose_name='тип участия')), ('type', models.CharField(choices=[('edu', 'Образовательный'), ('competitive', 'Соревновательный')], max_length=11, verbose_name='тип участия')),
('participation_type', models.CharField(choices=[('solo', 'Индивидуальный')], max_length=11, verbose_name='тип соревнования')), ('participation_type', models.CharField(choices=[('solo', 'Индивидуальный')], max_length=11, verbose_name='тип соревнования')),
('participants', models.ManyToManyField(blank=True, editable=False, related_name='participants', to='user.user')), ('participants', models.ManyToManyField(blank=True, editable=False, related_name='participants', to='user.user')),
@@ -1,18 +0,0 @@
# Generated by Django 5.1.6 on 2025-03-02 00:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('competition', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='state',
name='state',
field=models.CharField(choices=[('not_started', 'Not Started'), ('started', 'Started'), ('finished', 'Finished')], default='not_started', max_length=11),
),
]
@@ -1,4 +1,4 @@
# Generated by Django 5.1.6 on 2025-03-02 05:41 # Generated by Django 5.1.6 on 2025-03-02 06:13
import uuid import uuid
from django.db import migrations, models from django.db import migrations, models
@@ -1,4 +1,4 @@
# Generated by Django 5.1.6 on 2025-03-02 05:41 # Generated by Django 5.1.6 on 2025-03-02 06:13
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models
@@ -1,4 +1,4 @@
# Generated by Django 5.1.6 on 2025-03-02 05:41 # Generated by Django 5.1.6 on 2025-03-02 06:13
import apps.task.models import apps.task.models
import django.db.models.deletion import django.db.models.deletion
@@ -12,7 +12,7 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
('competition', '0002_alter_state_state'), ('competition', '0001_initial'),
('user', '0001_initial'), ('user', '0001_initial'),
] ]
@@ -25,11 +25,10 @@ class Migration(migrations.Migration):
('title', models.CharField(max_length=50, verbose_name='заголовок')), ('title', models.CharField(max_length=50, verbose_name='заголовок')),
('description', tinymce.models.HTMLField(max_length=300, verbose_name='описание')), ('description', tinymce.models.HTMLField(max_length=300, verbose_name='описание')),
('max_attempts', models.PositiveSmallIntegerField(blank=True, null=True)), ('max_attempts', models.PositiveSmallIntegerField(blank=True, null=True)),
('type', models.CharField(choices=[('input', 'Ввод правильного ответа'), ('checker', 'Вывод кода'), ('review', 'Ручная')], max_length=8, verbose_name='тип проверки')), ('type', models.CharField(choices=[('input', 'Ввод правильного ответа'), ('checker', 'Ввод кода'), ('review', 'Ручная')], max_length=8, verbose_name='тип проверки')),
('correct_answer_file', models.FileField(blank=True, null=True, upload_to=apps.task.models.CompetitionTask.answer_file_upload_to, verbose_name='файл с правильным ответом')), ('correct_answer_file', models.FileField(blank=True, null=True, upload_to=apps.task.models.CompetitionTask.answer_file_upload_to, verbose_name='файл с правильным ответом')),
('points', models.IntegerField(blank=True, null=True, verbose_name='баллы за задание')), ('points', models.IntegerField(blank=True, null=True, verbose_name='баллы за задание')),
('answer_file_path', models.TextField(blank=True, default='stdout', null=True, verbose_name='куда сохранять решения')), ('answer_file_path', models.TextField(blank=True, default='stdout', help_text='Путь до файла в котором ожидается результат. Пример: stdout или ./output.txt', null=True, verbose_name='куда сделать вывод программы участнику')),
('criteries', models.JSONField(blank=True, null=True, verbose_name='критерии')),
('competition', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='competition.competition')), ('competition', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='competition.competition')),
], ],
options={ options={
@@ -50,6 +49,20 @@ class Migration(migrations.Migration):
'abstract': False, 'abstract': False,
}, },
), ),
migrations.CreateModel(
name='CompetitionTaskCriteria',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('name', models.TextField()),
('slug', models.SlugField()),
('description', models.TextField()),
('max_value', models.PositiveSmallIntegerField()),
('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='criteries', to='task.competitiontask')),
],
options={
'abstract': False,
},
),
migrations.CreateModel( migrations.CreateModel(
name='CompetitionTaskSubmission', name='CompetitionTaskSubmission',
fields=[ fields=[
@@ -59,7 +72,8 @@ class Migration(migrations.Migration):
('stdout', models.FileField(blank=True, null=True, upload_to=apps.task.models.CompetitionTaskSubmission.submission_stdout_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)), ('result', models.JSONField(blank=True, default=None, null=True)),
('earned_points', models.IntegerField(blank=True, null=True)), ('earned_points', models.IntegerField(blank=True, null=True)),
('reviewed_at', models.DateTimeField(blank=True, null=True)), ('checked_at', models.DateTimeField(blank=True, null=True)),
('plagiarism_checked', models.BooleanField(default=False)),
('timestamp', models.DateTimeField(auto_now_add=True)), ('timestamp', models.DateTimeField(auto_now_add=True)),
('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='task.competitiontask')), ('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='task.competitiontask')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='user.user')), ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='user.user')),
+16 -15
View File
@@ -5,14 +5,13 @@ from tinymce.models import HTMLField
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.task.validators import ContestTaskCriteriesValidator
from apps.user.models import User from apps.user.models import User
class CompetitionTask(BaseModel): class CompetitionTask(BaseModel):
class CompetitionTaskType(models.TextChoices): class CompetitionTaskType(models.TextChoices):
INPUT = "input", "Ввод правильного ответа" INPUT = "input", "Ввод правильного ответа"
CHECKER = "checker", "Вывод кода" CHECKER = "checker", "Ввод кода"
REVIEW = "review", "Ручная" REVIEW = "review", "Ручная"
def answer_file_upload_to(instance, filename) -> str: def answer_file_upload_to(instance, filename) -> str:
@@ -44,21 +43,11 @@ class CompetitionTask(BaseModel):
answer_file_path = models.TextField( answer_file_path = models.TextField(
null=True, null=True,
blank=True, blank=True,
verbose_name="куда сохранять решения", verbose_name="куда сделать вывод программы участнику",
help_text="Путь до файла в котором ожидается результат. Пример: stdout или ./output.txt",
default="stdout", default="stdout",
) )
# only when "review" type
# TODO make it more humanize
criteries = models.JSONField(
blank=True,
null=True,
verbose_name="критерии",
)
def clean(self):
ContestTaskCriteriesValidator()(self)
def __str__(self): def __str__(self):
return self.title return self.title
@@ -67,6 +56,17 @@ class CompetitionTask(BaseModel):
verbose_name_plural = "задания" verbose_name_plural = "задания"
class CompetitionTaskCriteria(BaseModel):
task = models.ForeignKey(
CompetitionTask, on_delete=models.CASCADE, related_name="criteries"
)
name = models.TextField()
slug = models.SlugField()
description = models.TextField()
max_value = models.PositiveSmallIntegerField()
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"
@@ -114,5 +114,6 @@ class CompetitionTaskSubmission(BaseModel):
# 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(null=True, blank=True)
reviewed_at = models.DateTimeField(null=True, blank=True) checked_at = models.DateTimeField(null=True, blank=True)
plagiarism_checked = models.BooleanField(default=False)
timestamp = models.DateTimeField(auto_now_add=True) timestamp = models.DateTimeField(auto_now_add=True)
+7 -1
View File
@@ -1,4 +1,5 @@
import ast import ast
import contextlib
import hashlib import hashlib
import os import os
import sys import sys
@@ -6,6 +7,7 @@ import tempfile
from io import StringIO from io import StringIO
from config.celery import app from config.celery import app
from apps.task.models import CompetitionTaskSubmission
ALLOWED_MODULES = { ALLOWED_MODULES = {
"pandas", "pandas",
@@ -117,7 +119,7 @@ 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_bytes, input_files=[] self, code_str, result_path, expected_file_link, submission_id, input_files=[]
): ):
try: try:
validate_code(code_str) validate_code(code_str)
@@ -127,6 +129,10 @@ def analyze_data_task(
result_hash = hashlib.sha256(result_content).hexdigest() result_hash = hashlib.sha256(result_content).hexdigest()
expected_hash = hashlib.sha256(expected_bytes).hexdigest() expected_hash = hashlib.sha256(expected_bytes).hexdigest()
with contextlib.suppress(CompetitionTaskSubmission.DoesNotExist):
submission = CompetitionTaskSubmission.objects.get(id=submission_id)
submission.result = {"correct": True}
return { return {
"success": True, "success": True,
"match": result_hash == expected_hash, "match": result_hash == expected_hash,
@@ -0,0 +1,32 @@
import unittest
from apps.task.tasks import analyze_data_task
class TestAnalyzeDataTask(unittest.TestCase):
def test_task_execution_basic(self):
code_str = 'print("Hello, World!")'
result_path = "stdout"
expected_bytes = b"Hello, World!\n"
result = analyze_data_task(code_str, result_path, expected_bytes)
self.assertTrue(result["success"])
self.assertTrue(result["match"])
def test_task_execution_with_files(self):
code_str = """
with open("file.txt") as f:
print(f.read())
"""
result_path = "stdout"
expected_bytes = b"some_content\n"
result = analyze_data_task(
code_str,
result_path,
expected_bytes,
input_files=[{"bind_at": "file.txt", "content": b"some_content"}],
)
print(result)
self.assertTrue(result["success"])
self.assertTrue(result["match"])
-24
View File
@@ -1,24 +0,0 @@
from django.core.exceptions import ValidationError
from pydantic import BaseModel
from pydantic import ValidationError as PydanticValidationError
class Criteria(BaseModel):
name: str
slug: str
max_value: int
min_value: int
class ContestTaskCriteriesValidator:
def __call__(self, instance):
if instance.criteries and not isinstance(instance.criteries, list):
err = "criteries must be a valid dictionary"
raise ValidationError(err)
try:
for criteria in instance.criteries if instance.criteries else []:
Criteria(**criteria)
except PydanticValidationError:
err = "invalid criteries data"
raise ValidationError(err)
@@ -1,4 +1,4 @@
# Generated by Django 5.1.6 on 2025-03-02 00:16 # Generated by Django 5.1.6 on 2025-03-02 06:13
import django.db.models.deletion import django.db.models.deletion
import uuid import uuid