This commit is contained in:
rngsurrounded
2025-03-03 18:56:16 +09:00
18 changed files with 98 additions and 68 deletions
-1
View File
@@ -109,7 +109,6 @@ deploy:
cd ~/deploy
docker system prune -a --force
docker compose pull > deploy.log 2>&1
docker compose down >> deploy.log 2>&1
docker compose up -d --remove-orphans --force-recreate >> deploy.log 2>&1
docker compose ps >> deploy.log 2>&1
EOF
+4 -1
View File
@@ -400,6 +400,9 @@ services:
- type: bind
source: /var/run/docker.sock
target: /var/run/docker.sock
- type: bind
source: /tmp
target: /tmp
proxy:
image: docker.io/nginx:1.27-alpine3.21
@@ -410,7 +413,7 @@ services:
test: ["CMD", "service", "nginx", "status", "||", " exit 1"]
interval: 1m30s
timeout: 5s
start_period: 5s
start_period: 15s
start_interval: 2s
retries: 5
ports:
@@ -1,3 +1,5 @@
from datetime import datetime
from ninja import ModelSchema, Schema
from pydantic import Field
@@ -19,6 +21,7 @@ class UserAchievementSchema(Schema):
name: str = Field(..., alias="achievement.name")
description: str = Field(..., alias="achievement.description")
icon: str = Field(..., alias="achievement.icon")
received_at: datetime
class Meta:
model = UserAchievement
+1 -1
View File
@@ -32,7 +32,7 @@ class UserSchema(ModelSchema):
class Meta:
model = User
fields = ["id", "email", "username", "created_at"]
fields = ["id", "avatar", "email", "username", "created_at"]
class StatSchema(Schema):
@@ -1,4 +1,4 @@
# Generated by Django 5.1.6 on 2025-03-03 07:20
# Generated by Django 5.1.6 on 2025-03-03 09:41
import apps.achievement.models
import django.db.models.deletion
@@ -1,4 +1,4 @@
# Generated by Django 5.1.6 on 2025-03-03 07:20
# Generated by Django 5.1.6 on 2025-03-03 09:41
import django.db.models.deletion
from django.db import migrations, models
@@ -1,4 +1,4 @@
# Generated by Django 5.1.6 on 2025-03-03 07:20
# Generated by Django 5.1.6 on 2025-03-03 09:41
import apps.competition.models
import datetime
@@ -1,4 +1,4 @@
# Generated by Django 5.1.6 on 2025-03-03 07:20
# Generated by Django 5.1.6 on 2025-03-03 09:41
import uuid
from django.db import migrations, models
@@ -1,4 +1,4 @@
# Generated by Django 5.1.6 on 2025-03-03 07:20
# Generated by Django 5.1.6 on 2025-03-03 09:41
import django.db.models.deletion
from django.db import migrations, models
+5 -5
View File
@@ -3,8 +3,8 @@ from django.contrib import admin
from apps.task.models import (
CompetitionTask,
CompetitionTaskAttachment,
CompetitionTaskCriteria,
CompetitionTaskSubmission,
CompetitionTaskCriteria
)
@@ -23,7 +23,10 @@ class CompetitionTaskAdmin(admin.ModelAdmin):
list_display = ("title", "type", "points")
filter_horizontal = ("reviewers",)
list_filter = ("type",)
inlines = (CompletionAttachmentInline, CompetitionCriteriaInline,)
inlines = (
CompletionAttachmentInline,
CompetitionCriteriaInline,
)
@admin.register(CompetitionTaskSubmission)
@@ -45,9 +48,6 @@ class CompetitionTaskSubmissionAdmin(admin.ModelAdmin):
def has_add_permission(self, request, obj=None):
return False
def has_delete_permission(self, request, obj=None):
return False
class CompetitionTaskInline(admin.StackedInline):
model = CompetitionTask
@@ -1,8 +1,8 @@
# Generated by Django 5.1.6 on 2025-03-03 07:20
# Generated by Django 5.1.6 on 2025-03-03 09:41
import apps.task.models
import django.db.models.deletion
import martor.models
import mdeditor.fields
import uuid
from django.db import migrations, models
@@ -22,17 +22,17 @@ class Migration(migrations.Migration):
name='CompetitionTask',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('in_competition_position', models.PositiveSmallIntegerField()),
('in_competition_position', models.PositiveSmallIntegerField(verbose_name='позиция в соревновании')),
('title', models.CharField(max_length=50, verbose_name='заголовок')),
('description', martor.models.MartorField(verbose_name='описание')),
('max_attempts', models.PositiveSmallIntegerField(blank=True, null=True)),
('description', mdeditor.fields.MDTextField(verbose_name='описание')),
('max_attempts', models.PositiveSmallIntegerField(blank=True, null=True, 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='файл с правильным ответом')),
('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='куда сделать вывод программы участнику')),
('submission_reviewers_count', models.PositiveSmallIntegerField(blank=True, default=1, null=True)),
('competition', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='competition.competition')),
('reviewers', models.ManyToManyField(blank=True, help_text='Справа отображаются действующие проверяющие, слева - доступные для выбора. Для перемещения можно кликнуть 2 раза по проверяющему', to='review.reviewer', verbose_name='ревьюверы')),
('correct_answer_file', models.FileField(blank=True, help_text='Имеет смысл только при автоматической (ввод ответа или кода) проверке.', null=True, upload_to=apps.task.models.CompetitionTask.answer_file_upload_to, verbose_name='файл с правильным ответом')),
('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='куда сделать вывод программы участнику')),
('submission_reviewers_count', models.PositiveSmallIntegerField(blank=True, default=1, null=True, verbose_name='кол-во проверяющих для зачета задачи')),
('competition', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='competition.competition', verbose_name='привязанное соревнование')),
('reviewers', models.ManyToManyField(blank=True, help_text='Справа отображаются действующие проверяющие, слева - доступные для выбора. Для перемещения можно кликнуть 2 раза по проверяющему. Имеет смысл только при ручном типе проверки.', to='review.reviewer', verbose_name='ревьюверы')),
],
options={
'verbose_name': 'задание',
@@ -57,14 +57,15 @@ class Migration(migrations.Migration):
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()),
('name', models.TextField(verbose_name='название')),
('slug', models.SlugField(verbose_name='техническое название')),
('description', models.TextField(verbose_name='описание критерии')),
('max_value', models.PositiveSmallIntegerField(verbose_name='максимальное кол-во баллов')),
('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='criteries', to='task.competitiontask')),
],
options={
'abstract': False,
'verbose_name': 'критерий',
'verbose_name_plural': 'критерии',
},
),
migrations.CreateModel(
+26 -17
View File
@@ -22,12 +22,16 @@ class CompetitionTask(BaseModel):
in_competition_position = models.PositiveSmallIntegerField(
verbose_name="позиция в соревновании"
)
competition = models.ForeignKey(Competition, on_delete=models.CASCADE,
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="максимальное кол-во попыток")
max_attempts = models.PositiveSmallIntegerField(
null=True, blank=True, verbose_name="максимальное кол-во попыток"
)
type = models.CharField(
choices=CompetitionTaskType, max_length=8, verbose_name="тип проверки"
)
@@ -38,9 +42,10 @@ class CompetitionTask(BaseModel):
null=True,
blank=True,
verbose_name="файл с правильным ответом",
help_text="Имеет смысл только при автоматической (ввод ответа или кода) проверке.",
)
points = models.IntegerField(
null=True, blank=True, verbose_name="баллы за задание"
null=True, blank=True, verbose_name="общий балл за задание"
)
# only when "checker" type
@@ -48,7 +53,10 @@ class CompetitionTask(BaseModel):
null=True,
blank=True,
verbose_name="куда сделать вывод программы участнику",
help_text="Путь до файла в котором ожидается результат. Пример: stdout или ./output.txt",
help_text=(
"Путь до файла в котором ожидается результат. "
"Пример: stdout или ./output.txt. Имеет смысл только при автоматическом типе проверки."
),
default="stdout",
)
@@ -57,10 +65,17 @@ class CompetitionTask(BaseModel):
Reviewer,
blank=True,
verbose_name="ревьюверы",
help_text="Справа отображаются действующие проверяющие, слева - доступные для выбора. Для перемещения можно кликнуть 2 раза по проверяющему",
help_text=(
"Справа отображаются действующие проверяющие, слева - доступные для выбора. "
"Для перемещения можно кликнуть 2 раза по проверяющему. Имеет смысл только"
" при ручном типе проверки."
),
)
submission_reviewers_count = models.PositiveSmallIntegerField(
default=1, null=True, blank=True, verbose_name="кол-во проверяющих для зачета задачи"
default=1,
null=True,
blank=True,
verbose_name="кол-во проверяющих для зачета задачи",
)
def __str__(self):
@@ -76,15 +91,9 @@ class CompetitionTaskCriteria(BaseModel):
CompetitionTask, on_delete=models.CASCADE, related_name="criteries"
)
name = models.TextField(
verbose_name="название"
)
slug = models.SlugField(
verbose_name="техническое название"
)
description = models.TextField(
verbose_name="описание критерии"
)
name = models.TextField(verbose_name="название")
slug = models.SlugField(verbose_name="техническое название")
description = models.TextField(verbose_name="описание критерии")
max_value = models.PositiveSmallIntegerField(
verbose_name="максимальное кол-во баллов"
)
+22 -10
View File
@@ -1,28 +1,40 @@
import hashlib
import httpx
from celery import shared_task
from django.conf import settings
from django.core.files.base import ContentFile
from apps.task.models import CompetitionTaskSubmission
@shared_task(bind=True, max_retries=3)
def analyze_data_task(self, submission_id):
from .models import CompetitionTaskSubmission
submission = CompetitionTaskSubmission.objects.get(id=submission_id)
try:
code = submission.content.read().decode()
code_url = (
f"{settings.MINIO_DEFAULT_CUSTOM_ENDPOINT_URL}{submission.path}"
)
files = [
(f.name, f.file.open("rb"))
for f in submission.task.attachments.filter(public=True)
{
"url": f"{settings.MINIO_DEFAULT_CUSTOM_ENDPOINT_URL}{attachment.path}",
"bind_path": attachment.bind_at,
}
for attachment in submission.task.attachments.filter(
bind_path__isnull=False
)
]
response = httpx.post(
f"{settings.CHECKER_API_ENDPOINT}/execute",
files=[("files", (f.name, f)) for f in files]
+ [
("code", code),
("expected_hash", submission.task.correct_answer_hash),
],
json={
"files": files,
"code_url": code_url,
"answer_file_path": submission.task.answer_file_path,
"expected_hash": hashlib.sha256(
submission.task.correct_answer_file.read().encode()
).hexdigest(),
},
timeout=30,
)
response.raise_for_status()
@@ -1,4 +1,4 @@
# Generated by Django 5.1.6 on 2025-03-03 07:20
# Generated by Django 5.1.6 on 2025-03-03 09:41
import django.db.models.deletion
import uuid
@@ -1,4 +1,4 @@
# Generated by Django 5.1.6 on 2025-03-03 07:20
# Generated by Django 5.1.6 on 2025-03-03 09:41
import uuid
from django.db import migrations, models
@@ -17,11 +17,12 @@ class Migration(migrations.Migration):
name='User',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('avatar', models.ImageField(blank=True, null=True, upload_to='', verbose_name='аватар')),
('email', models.EmailField(max_length=254, unique=True, verbose_name='почта')),
('username', models.SlugField(unique=True, verbose_name='юзернейм')),
('password', models.TextField(verbose_name='пароль')),
('created_at', models.DateTimeField(auto_now=True)),
('status', models.CharField(choices=[('student', 'Student'), ('metodist', 'Metodist')], default='student', max_length=10)),
('created_at', models.DateTimeField(auto_now=True, verbose_name='дата создания')),
('status', models.CharField(choices=[('student', 'Участник соревнований'), ('metodist', 'Методист (составитель заданий)')], default='student', max_length=10, verbose_name='роль участника')),
('achievements', models.ManyToManyField(blank=True, to='achievement.achievement', verbose_name='ачивки пользователя')),
],
options={
+8 -3
View File
@@ -11,11 +11,14 @@ class UserRole(models.TextChoices):
class User(BaseModel):
avatar = models.ImageField(verbose_name="аватар", null=True, blank=True)
email = models.EmailField(unique=True, verbose_name="почта")
username = models.SlugField(unique=True, verbose_name="юзернейм")
password = models.TextField(verbose_name="пароль")
created_at = models.DateTimeField(auto_now=True, verbose_name="дата создания")
created_at = models.DateTimeField(
auto_now=True, verbose_name="дата создания"
)
achievements = models.ManyToManyField(
Achievement, blank=True, verbose_name="ачивки пользователя"
@@ -29,8 +32,10 @@ class User(BaseModel):
return check_password(self.password, password)
status = models.CharField(
max_length=10, choices=UserRole.choices, default="student",
verbose_name="роль участника"
max_length=10,
choices=UserRole.choices,
default="student",
verbose_name="роль участника",
)
def __str__(self) -> str:
+1 -5
View File
@@ -12,12 +12,8 @@ admin.site.index_title = "DataRush"
urlpatterns = [
# tinymce
path("tinymce/", include("tinymce.urls")),
# martor
path("martor/", include("martor.urls")),
# mdeditor
path(r'mdeditor/', include('mdeditor.urls')),
path(r"mdeditor/", include("mdeditor.urls")),
# Admin urls
path("admin/", admin.site.urls),
# API urls
+1
View File
@@ -202,6 +202,7 @@ async def execute_code(request: ExecutionRequest) -> ExecutionResponse:
)
with tempfile.TemporaryDirectory() as tmp_dir:
print(tmp_dir)
bound_files = {}
if request.files:
async with aiohttp.ClientSession() as session: