feat: added task submission logic

This commit is contained in:
Андрей Сумин
2025-03-01 13:09:09 +03:00
parent cead40b03d
commit 22727dda92
18 changed files with 152 additions and 148 deletions
+26
View File
@@ -0,0 +1,26 @@
from abc import ABC
from typing import Optional
from django.http import HttpRequest
from django.shortcuts import get_object_or_404
from django.urls import resolve
from ninja.errors import AuthenticationError
from ninja.security import APIKeyQuery
from ninja.security.apikey import APIKeyBase
from apps.review.models import Reviewer
class APIKeyPath(APIKeyBase, ABC):
openapi_in: str = "path"
def _get_key(self, request: HttpRequest) -> Optional[str]:
func, args, kwargs = resolve(request.path)
return kwargs.get(self.param_name)
class ReviewerAuth(APIKeyPath):
param_name = "token"
def authenticate(self, request, token):
if not (reviewer := Reviewer.objects.filter(token=token).first()):
raise AuthenticationError
return reviewer
+37
View File
@@ -0,0 +1,37 @@
from typing import List, Literal
from uuid import UUID
from django.http import HttpRequest
from ninja import Schema, ModelSchema
from apps.review.models import Reviewer
from apps.task.models import CompetetionTaskSumbission
class PingOut(Schema):
status: str = "ok"
class ReviewerOut(ModelSchema):
id: UUID
class Meta:
model = Reviewer
exclude = ("token",)
class SubmissionOut(ModelSchema):
id: UUID
status: Literal["sent", "checking", "checked"]
class Meta:
model = CompetetionTaskSumbission
exclude = (
"user",
)
class SubmissionsOut(Schema):
submissions: list[SubmissionOut] = []
@staticmethod
def resolve_submissions(self, context: HttpRequest) -> List[SubmissionOut]:
print(CompetetionTaskSumbission.objects.all())
return list(CompetetionTaskSumbission.objects.all())
+34
View File
@@ -0,0 +1,34 @@
from http import HTTPStatus as status
from django.http import HttpRequest
from ninja import Router
from api.v1.review import schemas
from api.v1 import schemas as global_schemas
router = Router(tags=["review"])
@router.get(
"{token}/tasks",
response={
status.OK: schemas.SubmissionsOut,
},
)
def ping(request: HttpRequest, token) -> tuple[status, schemas.SubmissionsOut]:
return status.OK, schemas.SubmissionsOut()
@router.get(
"{token}",
response={
status.OK: schemas.ReviewerOut,
status.UNAUTHORIZED: global_schemas.UnauthorizedError
},
description="token есть и в сваггер авторизации, но оно не работает, не верьте. подставляйте токен вручную в query"
)
def get_reviewer(
request: HttpRequest,
token: str
):
return status.OK, request.auth
+8
View File
@@ -6,7 +6,9 @@ from api.v1 import handlers
from api.v1.auth import BearerAuth
from api.v1.competition.views import router as competition_router
from api.v1.ping.views import router as ping_router
from api.v1.review.auth import ReviewerAuth
from api.v1.user.views import router as user_router
from api.v1.review.views import router as review_router
router = NinjaAPI(
title="DataRush API",
@@ -30,6 +32,12 @@ router.add_router(
competition_router,
auth=BearerAuth(),
)
router.add_router(
"review",
review_router,
auth=ReviewerAuth(),
)
for exception, handler in handlers.exception_handlers:
@@ -1,32 +0,0 @@
# Generated by Django 5.1.6 on 2025-02-28 21:27
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Competition',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('title', models.CharField(max_length=100, verbose_name='Название')),
('description', models.TextField(verbose_name='Описание')),
('image_url', models.FileField(blank=True, null=True, upload_to='', verbose_name='Изображение соревнования')),
('end_date', models.DateTimeField(blank=True, null=True, verbose_name='Дедлайн участия')),
('start_date', models.DateTimeField(blank=True, null=True, verbose_name='Дедлайн участия')),
('type', models.CharField(choices=[('solo', 'Solo')], max_length=10, verbose_name='Тип участия')),
('participation_type', models.CharField(choices=[('edu', 'Edu'), ('competitive', 'Competitive')], max_length=11, verbose_name='Тип соревнования')),
],
options={
'verbose_name': 'соревнование',
'verbose_name_plural': 'соревнования',
},
),
]
@@ -1,19 +0,0 @@
# Generated by Django 5.1.6 on 2025-02-28 22:40
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('competition', '0001_initial'),
('user', '0002_user_status'),
]
operations = [
migrations.AddField(
model_name='competition',
name='participants',
field=models.ManyToManyField(related_name='participants', to='user.user'),
),
]
@@ -1,28 +0,0 @@
# Generated by Django 5.1.6 on 2025-02-28 23:26
import django.db.models.deletion
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('competition', '0002_competition_participants'),
('user', '0003_alter_user_status'),
]
operations = [
migrations.CreateModel(
name='State',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('state', models.CharField(choices=[('not_started', 'Not Started'), ('started', 'Started'), ('finished', 'Finished')], max_length=11)),
('competition', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='competition.competition')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='user.user')),
],
options={
'abstract': False,
},
),
]
+6
View File
@@ -0,0 +1,6 @@
from django.apps import AppConfig
class CoreConfig(AppConfig):
name = "apps.review"
label = "review"
+10
View File
@@ -0,0 +1,10 @@
from django.db import models
from apps.core.models import BaseModel
class Reviewer(BaseModel):
name = models.CharField(max_length=100)
surname = models.CharField(max_length=100)
token = models.CharField(max_length=100)
+29 -5
View File
@@ -1,11 +1,12 @@
from random import choice
from uuid import uuid4
from competition.models import Competition
from core.models import BaseModel
from django.db import models
from apps.task.validators import ContestTaskCriteriesValidator
from apps.competition.models import Competition
from apps.core.models import BaseModel
from apps.user.models import User
class CompetitionTask(BaseModel):
class CompetitionTaskType(models.TextChoices):
@@ -14,12 +15,12 @@ class CompetitionTask(BaseModel):
REVIEW = "review"
def answer_file_upload_to(instance, filename) -> str:
return f"/tasks/{instance.id}/answer/{uuid4}"
return f"/tasks/{instance.id}/answer/{uuid4()}/filename"
competition = models.ForeignKey(Competition, on_delete=models.CASCADE)
title = models.TextField(verbose_name="заголовок", max_length=50)
description = models.TextField(verbose_name="описание", max_length=300)
type = models.CharField(choices=CompetitionTaskType)
type = models.CharField(choices=CompetitionTaskType, max_length=8)
# only when "input" or "checker" type
correct_answer_file = models.FileField(upload_to=answer_file_upload_to)
@@ -32,3 +33,26 @@ class CompetitionTask(BaseModel):
def clean(self):
ContestTaskCriteriesValidator()(self)
class CompetetionTaskSumbission(BaseModel):
class StatusChoices(models.TextChoices):
SENT = "sent"
CHECKING = "checking"
CHECKED = "checked"
def submission_content_upload_to(instance, filename) -> str:
return f"/submissions/{instance.id}/content"
def submission_stdout_upload_to(instance, filename) -> str:
return f"/submissions/{instance.id}/stdout"
status = models.CharField(
choices=StatusChoices.choices, default=StatusChoices.SENT, max_length=8
)
user = models.ForeignKey(User, on_delete=models.CASCADE)
task = models.ForeignKey(CompetitionTask, on_delete=models.CASCADE)
content = models.FileField(upload_to=submission_content_upload_to)
stdout = models.FileField(upload_to=submission_stdout_upload_to)
result = models.JSONField(default={})
timestamp = models.DateTimeField(auto_now_add=True)
@@ -1,28 +0,0 @@
# Generated by Django 5.1.6 on 2025-02-28 20:46
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='User',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('email', models.EmailField(max_length=254, unique=True, verbose_name='Почта')),
('username', models.SlugField(unique=True, verbose_name='Юзернейм')),
('password', models.TextField(verbose_name='Пароль')),
],
options={
'verbose_name': 'пользователь',
'verbose_name_plural': 'пользователи',
},
),
]
@@ -1,18 +0,0 @@
# Generated by Django 5.1.6 on 2025-02-28 22:40
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('user', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='user',
name='status',
field=models.CharField(choices=[('student', 'Student'), ('metodist', 'Metodist')], default='student', max_length=10),
),
]
@@ -1,18 +0,0 @@
# Generated by Django 5.1.6 on 2025-02-28 22:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('user', '0002_user_status'),
]
operations = [
migrations.AlterField(
model_name='user',
name='status',
field=models.CharField(choices=[('student', 'Student'), ('metodist', 'Metodist')], default='student', max_length=10),
),
]
+2
View File
@@ -445,6 +445,8 @@ INSTALLED_APPS = [
"apps.core",
"apps.user",
"apps.competition",
"apps.review",
"apps.task"
]
# GUID