This commit is contained in:
rngsurrounded
2025-03-02 19:09:47 +09:00
24 changed files with 6182 additions and 93 deletions
+1 -1
View File
@@ -253,7 +253,7 @@ services:
- name: web - name: web
target: 80 target: 80
published: 8003 published: 8003
host_ip: 127.0.0.1 host_ip: 0.0.0.0
protocol: tcp protocol: tcp
restart: unless-stopped restart: unless-stopped
secrets: secrets:
@@ -1,9 +0,0 @@
#!/bin/sh
set -e
echo "Installing required libs..."
pip install -r checker_requirements.txt
echo "Starting Celery worker..."
celery -A config worker -l INFO
+1 -1
View File
@@ -1 +1 @@
admin J2NofXLJa57mpHVQVdNFaltSmg9gjI
-2
View File
@@ -24,6 +24,4 @@ FROM docker.io/nginx:latest
COPY --from=builder /app/static /usr/share/nginx/html COPY --from=builder /app/static /usr/share/nginx/html
COPY checker_requirements.txt /usr/share/nginx/html
CMD ["nginx", "-g", "daemon off;"] CMD ["nginx", "-g", "daemon off;"]
+16 -3
View File
@@ -41,7 +41,11 @@ def get_submission(
review = Review.objects.get(reviewer=reviewer, submission=submission) review = Review.objects.get(reviewer=reviewer, submission=submission)
if review.state == ReviewStatusChoices.NOT_CHECKED.value: if review.state == ReviewStatusChoices.NOT_CHECKED.value:
review.state = ReviewStatusChoices.CHECKING.value review.state = ReviewStatusChoices.CHECKING.value
review.submission.state = (
CompetitionTaskSubmission.StatusChoices.CHECKING.value
)
review.save() review.save()
review.submission.save()
return status.OK, submission return status.OK, submission
@@ -79,13 +83,22 @@ def evaluate_submission(
evaluation = evaluation_info.dict()["evaluation"] evaluation = evaluation_info.dict()["evaluation"]
review.evaluation = evaluation review.evaluation = evaluation
review.state = ReviewStatusChoices.CHECKED.value review.state = ReviewStatusChoices.CHECKED.value
review.submission.reviewed_at = datetime.now() review.submission.checked_at = datetime.now()
points = 0 points = 0
for criterea in evaluation: for criterea in evaluation:
points += criterea["mark"] points += criterea["mark"]
review.submission.earned_points = points review.submission.earned_points = (
points # TODO: оценка не от последнего проверяющего а средняя по всем
)
review.save() review.save()
all_checked = not submission.reviews.exclude(
state=ReviewStatusChoices.CHECKED
).exists()
if all_checked:
review.submission.status = (
CompetitionTaskSubmission.StatusChoices.CHECKED.value
)
review.submission.save()
return status.OK, review.submission return status.OK, review.submission
+8 -2
View File
@@ -3,7 +3,7 @@ from uuid import UUID
from ninja import ModelSchema, Schema from ninja import ModelSchema, Schema
from apps.task.models import CompetitionTask, CompetitionTaskSubmission from apps.task.models import CompetitionTask, CompetitionTaskSubmission, CompetitionTaskAttachment
class TaskOutSchema(ModelSchema): class TaskOutSchema(ModelSchema):
@@ -29,4 +29,10 @@ class HistorySubmissionOut(ModelSchema):
class Meta: class Meta:
model = CompetitionTaskSubmission model = CompetitionTaskSubmission
fields = ("id", "earned_points", "timestamp") fields = ("id", "earned_points", "timestamp", "content",)
class TaskAttachmentSchema(ModelSchema):
class Meta:
model = CompetitionTaskAttachment
fields = ("id", "file", "public",)
+17
View File
@@ -8,6 +8,7 @@ from api.v1.ping.schemas import PingOut
from api.v1.schemas import ForbiddenError, NotFoundError, UnauthorizedError from api.v1.schemas import ForbiddenError, NotFoundError, UnauthorizedError
from api.v1.task.schemas import ( from api.v1.task.schemas import (
HistorySubmissionOut, HistorySubmissionOut,
TaskAttachmentSchema,
TaskOutSchema, TaskOutSchema,
TaskSubmissionOut, TaskSubmissionOut,
) )
@@ -15,6 +16,7 @@ from apps.competition.models import State
from apps.task.models import ( from apps.task.models import (
Competition, Competition,
CompetitionTask, CompetitionTask,
CompetitionTaskAttachment,
CompetitionTaskSubmission, CompetitionTaskSubmission,
) )
@@ -113,6 +115,7 @@ def submit_task(
status=CompetitionTaskSubmission.StatusChoices.SENT, status=CompetitionTaskSubmission.StatusChoices.SENT,
content=content, content=content,
) )
submission.send_on_review()
if task.type == CompetitionTask.CompetitionTaskType.CHECKER: if task.type == CompetitionTask.CompetitionTaskType.CHECKER:
submission = CompetitionTaskSubmission.objects.create( submission = CompetitionTaskSubmission.objects.create(
user=user, user=user,
@@ -140,3 +143,17 @@ def get_submissions_history(request, competition_id: UUID, task_id: UUID):
) )
return status.OK, submissions_history return status.OK, submissions_history
@router.get(
"competitions/{competition_id}/tasks/{task_id}/attachments",
response={
status.OK: list[TaskAttachmentSchema],
status.UNAUTHORIZED: UnauthorizedError,
},
)
def get_task_attachments(request, competition_id: UUID, task_id: UUID):
task = get_object_or_404(CompetitionTask, id=task_id)
return status.OK, CompetitionTaskAttachment.objects.filter(
competition_id=competition_id, task=task, user=request.auth
)
+1 -1
View File
@@ -22,4 +22,4 @@ class LoginSchema(ModelSchema):
class UserSchema(ModelSchema): class UserSchema(ModelSchema):
class Meta: class Meta:
model = User model = User
fields = ["id", "email", "username"] fields = ["id", "email", "username", "created_at",]
+2
View File
@@ -1,3 +1,4 @@
from datetime import datetime
from http import HTTPStatus as status from http import HTTPStatus as status
from django.contrib.auth.hashers import check_password, make_password from django.contrib.auth.hashers import check_password, make_password
@@ -35,6 +36,7 @@ router = Router(tags=["user"])
def sign_up(request, data: RegisterSchema): def sign_up(request, data: RegisterSchema):
user = User(**data.dict(exclude={"password"})) user = User(**data.dict(exclude={"password"}))
user.password = make_password(data.password) user.password = make_password(data.password)
user.created_at = datetime.now()
user.save() user.save()
token = BearerAuth.generate_jwt(user) token = BearerAuth.generate_jwt(user)
-1
View File
@@ -2,4 +2,3 @@ from django.contrib import admin
from django.contrib.auth.models import Group, User from django.contrib.auth.models import Group, User
admin.site.unregister(Group) admin.site.unregister(Group)
admin.site.unregister(User)
+17
View File
@@ -0,0 +1,17 @@
from django.contrib import admin
from apps.review.models import Review, Reviewer
@admin.register(Reviewer)
class ReviewAdmin(admin.ModelAdmin):
list_display = ("name", "surname",)
search_fields = ("name", "surname",)
@admin.register(Review)
class ReviewAdmin(admin.ModelAdmin):
list_display = ("id", "reviewer", "submission",)
search_fields = ("id", "reviewer__id", "reviewer__name", "reviewer__surname",
"submission__id", "submission__content")
list_filter = ("submission__plagiarism_checked", "submission__status",)
+1
View File
@@ -4,3 +4,4 @@ from django.apps import AppConfig
class CoreConfig(AppConfig): class CoreConfig(AppConfig):
name = "apps.review" name = "apps.review"
label = "review" label = "review"
verbose_name = "Проверка"
@@ -1,5 +1,6 @@
# Generated by Django 5.1.6 on 2025-03-02 06:13 # Generated by Django 5.1.6 on 2025-03-02 09:31
import django.db.models.deletion
import uuid import uuid
from django.db import migrations, models from django.db import migrations, models
@@ -9,30 +10,35 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
('task', '0003_remove_competitiontask_attachments'),
] ]
operations = [ operations = [
migrations.CreateModel(
name='Review',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('evaluation', models.JSONField(blank=True, default=list, null=True)),
('state', models.CharField(choices=[('not_checked', 'Not Checked'), ('checking', 'Checking'), ('checked', 'Checked')], default='not_checked', max_length=11)),
],
options={
'abstract': False,
},
),
migrations.CreateModel( migrations.CreateModel(
name='Reviewer', name='Reviewer',
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.CharField(max_length=100)), ('name', models.CharField(max_length=100, verbose_name='имя')),
('surname', models.CharField(max_length=100)), ('surname', models.CharField(max_length=100, verbose_name='фамилия')),
('token', models.CharField(max_length=100)), ('token', models.CharField(max_length=100, verbose_name='токен для входа')),
], ],
options={ options={
'abstract': False, 'verbose_name': 'проверяющий',
'verbose_name_plural': 'проверяющие',
},
),
migrations.CreateModel(
name='Review',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('evaluation', models.JSONField(blank=True, default=list, null=True, verbose_name='выполнение')),
('state', models.CharField(choices=[('not_checked', 'Not Checked'), ('checking', 'Checking'), ('checked', 'Checked')], default='not_checked', max_length=11, verbose_name='состояние')),
('submission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reviews', to='task.competitiontasksubmission', verbose_name='посылка')),
('reviewer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='review.reviewer', verbose_name='проверяющий')),
],
options={
'verbose_name': 'проверка',
'verbose_name_plural': 'проверки',
}, },
), ),
] ]
@@ -1,27 +0,0 @@
# Generated by Django 5.1.6 on 2025-03-02 06:13
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('review', '0001_initial'),
('task', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='review',
name='submission',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reviews', to='task.competitiontasksubmission'),
),
migrations.AddField(
model_name='review',
name='reviewer',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='review.reviewer'),
),
]
+24 -7
View File
@@ -1,14 +1,20 @@
from django.db import models from django.db import models
from apps.core.models import BaseModel from apps.core.models import BaseModel
from apps.task.models import CompetitionTaskSubmission
class Reviewer(BaseModel): class Reviewer(BaseModel):
name = models.CharField(max_length=100) name = models.CharField(max_length=100, verbose_name="имя")
surname = models.CharField(max_length=100) surname = models.CharField(max_length=100, verbose_name="фамилия")
token = models.CharField(max_length=100) token = models.CharField(max_length=100, verbose_name="токен для входа")
def __str__(self):
return self.name + " " + self.surname
class Meta:
verbose_name = "проверяющий"
verbose_name_plural = "проверяющие"
class ReviewStatusChoices(models.TextChoices): class ReviewStatusChoices(models.TextChoices):
@@ -18,16 +24,27 @@ class ReviewStatusChoices(models.TextChoices):
class Review(BaseModel): class Review(BaseModel):
reviewer = models.ForeignKey(Reviewer, on_delete=models.CASCADE) reviewer = models.ForeignKey(Reviewer, on_delete=models.CASCADE,
verbose_name="проверяющий")
submission = models.ForeignKey( submission = models.ForeignKey(
CompetitionTaskSubmission, "CompetitionTaskSubmission",
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name="reviews", related_name="reviews",
verbose_name="посылка"
) )
evaluation = models.JSONField(default=list, null=True, blank=True) evaluation = models.JSONField(default=list, null=True, blank=True,
verbose_name="выполнение")
state = models.CharField( state = models.CharField(
choices=ReviewStatusChoices.choices, choices=ReviewStatusChoices.choices,
default=ReviewStatusChoices.NOT_CHECKED.value, default=ReviewStatusChoices.NOT_CHECKED.value,
max_length=11, max_length=11,
verbose_name="состояние"
) )
def __str__(self):
return str(self.id)
class Meta:
verbose_name = "проверка"
verbose_name_plural = "проверки"
+16 -2
View File
@@ -1,6 +1,7 @@
from django.contrib import admin from django.contrib import admin
from apps.task.models import CompetitionTask, CompetitionTaskAttachment from apps.task.models import CompetitionTask, CompetitionTaskAttachment, \
CompetitionTaskSubmission
class CompletionAttachmentInline(admin.StackedInline): class CompletionAttachmentInline(admin.StackedInline):
@@ -11,7 +12,20 @@ class CompletionAttachmentInline(admin.StackedInline):
@admin.register(CompetitionTask) @admin.register(CompetitionTask)
class CompetitionTaskAdmin(admin.ModelAdmin): class CompetitionTaskAdmin(admin.ModelAdmin):
list_display = ("title", "type", "points") list_display = ("title", "type", "points")
inlines = [CompletionAttachmentInline]
@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): class CompetitionTaskInline(admin.StackedInline):
@@ -0,0 +1,40 @@
# 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,17 @@
# 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',
),
]
+58 -13
View File
@@ -1,10 +1,12 @@
from uuid import uuid4 from uuid import uuid4
from django.db import models from django.db import models
from django.db.models import Count, Q
from tinymce.models import HTMLField 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.review.models import Review, ReviewStatusChoices
from apps.user.models import User from apps.user.models import User
@@ -71,10 +73,12 @@ 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"
task = models.ForeignKey(CompetitionTask, on_delete=models.CASCADE) task = models.ForeignKey(CompetitionTask, on_delete=models.CASCADE,
file = models.FileField(upload_to=file_upload_at) verbose_name="задание")
bind_at = models.FilePathField() file = models.FileField(upload_to=file_upload_at,
public = models.BooleanField(default=False) verbose_name="файл")
bind_at = models.FilePathField(verbose_name="путь сохранения")
public = models.BooleanField(default=False, verbose_name="публичный")
class CompetitionTaskSubmission(BaseModel): class CompetitionTaskSubmission(BaseModel):
@@ -89,31 +93,72 @@ class CompetitionTaskSubmission(BaseModel):
def submission_stdout_upload_to(instance, filename) -> str: def submission_stdout_upload_to(instance, filename) -> str:
return f"/submissions/{instance.id}/stdout" return f"/submissions/{instance.id}/stdout"
user = models.ForeignKey(User, on_delete=models.CASCADE) user = models.ForeignKey(User, on_delete=models.CASCADE,
task = models.ForeignKey(CompetitionTask, on_delete=models.CASCADE) verbose_name="пользователь")
task = models.ForeignKey(CompetitionTask, on_delete=models.CASCADE,
verbose_name="задание")
status = models.CharField( status = models.CharField(
choices=StatusChoices.choices, choices=StatusChoices.choices,
default=StatusChoices.SENT, default=StatusChoices.SENT,
max_length=8, max_length=8,
verbose_name="статус"
) )
# code or text or file # 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 # only if task type is checker
stdout = models.FileField( 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="вывод чекера"
) )
# depends on task type: # depends on task type:
# - input: {"correct": boolean} # - input: {"correct": boolean}
# - file: {"total_points": integer, "by_criteria": ["criteria_name": integer]} # - file: {"total_points": integer, "by_criteria": ["criteria_name": integer]}
# - code: {"correct": boolean} # - 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 # 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) checked_at = models.DateTimeField(null=True, blank=True,
plagiarism_checked = models.BooleanField(default=False) verbose_name="дата и время проверки")
timestamp = models.DateTimeField(auto_now_add=True) plagiarism_checked = models.BooleanField(default=False,
verbose_name="проверено на плагиат")
timestamp = models.DateTimeField(auto_now_add=True,
verbose_name="дата отправки")
def __str__(self):
return str(self.id)
class Meta:
verbose_name = "посылка"
verbose_name_plural = "посылки"
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,
]
),
)
)
.order_by("pending_count")
.first()
)
Review.objects.create(
reviewer=reviewer,
submission=self,
)
-1
View File
@@ -1,4 +1,3 @@
from django.db import models from django.db import models
from apps.core.models import BaseModel from apps.core.models import BaseModel
@@ -0,0 +1,18 @@
# Generated by Django 5.1.6 on 2025-03-02 09:50
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('user', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='user',
name='created_at',
field=models.DateTimeField(auto_now=True),
),
]
+2
View File
@@ -14,6 +14,8 @@ class User(BaseModel):
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)
@staticmethod @staticmethod
def make_password(password: str): def make_password(password: str):
return make_password(password) return make_password(password)
@@ -1,7 +0,0 @@
pandas==2.2.3
numpy==2.2.3
matplotlib==3.10.1
scipy==1.15.2
scikit-learn==1.6.1
seaborn==0.13.2
statsmodels==0.14.4
+5921
View File
File diff suppressed because it is too large Load Diff