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