From 22727dda92cdbac880066e69916f17032756eb4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9=20=D0=A1=D1=83=D0=BC?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Sat, 1 Mar 2025 13:09:09 +0300 Subject: [PATCH 1/2] feat: added task submission logic --- services/backend/api/v1/review/__init__.py | 0 services/backend/api/v1/review/auth.py | 26 +++++++++++++ services/backend/api/v1/review/schemas.py | 37 +++++++++++++++++++ services/backend/api/v1/review/views.py | 34 +++++++++++++++++ services/backend/api/v1/router.py | 8 ++++ .../competition/migrations/0001_initial.py | 32 ---------------- .../0002_competition_participants.py | 19 ---------- .../apps/competition/migrations/0003_state.py | 28 -------------- services/backend/apps/review/__init__.py | 0 services/backend/apps/review/apps.py | 6 +++ .../apps/review/migrations/__init__.py | 0 services/backend/apps/review/models.py | 10 +++++ .../backend/apps/task/migrations/__init__.py | 0 services/backend/apps/task/models.py | 34 ++++++++++++++--- .../apps/user/migrations/0001_initial.py | 28 -------------- .../apps/user/migrations/0002_user_status.py | 18 --------- .../user/migrations/0003_alter_user_status.py | 18 --------- services/backend/config/settings.py | 2 + 18 files changed, 152 insertions(+), 148 deletions(-) create mode 100644 services/backend/api/v1/review/__init__.py create mode 100644 services/backend/api/v1/review/auth.py create mode 100644 services/backend/api/v1/review/schemas.py create mode 100644 services/backend/api/v1/review/views.py delete mode 100644 services/backend/apps/competition/migrations/0001_initial.py delete mode 100644 services/backend/apps/competition/migrations/0002_competition_participants.py delete mode 100644 services/backend/apps/competition/migrations/0003_state.py create mode 100644 services/backend/apps/review/__init__.py create mode 100644 services/backend/apps/review/apps.py create mode 100644 services/backend/apps/review/migrations/__init__.py create mode 100644 services/backend/apps/review/models.py create mode 100644 services/backend/apps/task/migrations/__init__.py delete mode 100644 services/backend/apps/user/migrations/0001_initial.py delete mode 100644 services/backend/apps/user/migrations/0002_user_status.py delete mode 100644 services/backend/apps/user/migrations/0003_alter_user_status.py diff --git a/services/backend/api/v1/review/__init__.py b/services/backend/api/v1/review/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/backend/api/v1/review/auth.py b/services/backend/api/v1/review/auth.py new file mode 100644 index 0000000..9fbd270 --- /dev/null +++ b/services/backend/api/v1/review/auth.py @@ -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 \ No newline at end of file diff --git a/services/backend/api/v1/review/schemas.py b/services/backend/api/v1/review/schemas.py new file mode 100644 index 0000000..824e62d --- /dev/null +++ b/services/backend/api/v1/review/schemas.py @@ -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()) \ No newline at end of file diff --git a/services/backend/api/v1/review/views.py b/services/backend/api/v1/review/views.py new file mode 100644 index 0000000..b3e1bdf --- /dev/null +++ b/services/backend/api/v1/review/views.py @@ -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 \ No newline at end of file diff --git a/services/backend/api/v1/router.py b/services/backend/api/v1/router.py index 1ad7924..e85570a 100644 --- a/services/backend/api/v1/router.py +++ b/services/backend/api/v1/router.py @@ -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: diff --git a/services/backend/apps/competition/migrations/0001_initial.py b/services/backend/apps/competition/migrations/0001_initial.py deleted file mode 100644 index dd16963..0000000 --- a/services/backend/apps/competition/migrations/0001_initial.py +++ /dev/null @@ -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': 'соревнования', - }, - ), - ] diff --git a/services/backend/apps/competition/migrations/0002_competition_participants.py b/services/backend/apps/competition/migrations/0002_competition_participants.py deleted file mode 100644 index a15dafd..0000000 --- a/services/backend/apps/competition/migrations/0002_competition_participants.py +++ /dev/null @@ -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'), - ), - ] diff --git a/services/backend/apps/competition/migrations/0003_state.py b/services/backend/apps/competition/migrations/0003_state.py deleted file mode 100644 index 2212552..0000000 --- a/services/backend/apps/competition/migrations/0003_state.py +++ /dev/null @@ -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, - }, - ), - ] diff --git a/services/backend/apps/review/__init__.py b/services/backend/apps/review/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/backend/apps/review/apps.py b/services/backend/apps/review/apps.py new file mode 100644 index 0000000..fc4d048 --- /dev/null +++ b/services/backend/apps/review/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class CoreConfig(AppConfig): + name = "apps.review" + label = "review" diff --git a/services/backend/apps/review/migrations/__init__.py b/services/backend/apps/review/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/backend/apps/review/models.py b/services/backend/apps/review/models.py new file mode 100644 index 0000000..58bd512 --- /dev/null +++ b/services/backend/apps/review/models.py @@ -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) \ No newline at end of file diff --git a/services/backend/apps/task/migrations/__init__.py b/services/backend/apps/task/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/backend/apps/task/models.py b/services/backend/apps/task/models.py index eee9f4f..e7adf8a 100644 --- a/services/backend/apps/task/models.py +++ b/services/backend/apps/task/models.py @@ -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) diff --git a/services/backend/apps/user/migrations/0001_initial.py b/services/backend/apps/user/migrations/0001_initial.py deleted file mode 100644 index 41491e9..0000000 --- a/services/backend/apps/user/migrations/0001_initial.py +++ /dev/null @@ -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': 'пользователи', - }, - ), - ] diff --git a/services/backend/apps/user/migrations/0002_user_status.py b/services/backend/apps/user/migrations/0002_user_status.py deleted file mode 100644 index 281d8fd..0000000 --- a/services/backend/apps/user/migrations/0002_user_status.py +++ /dev/null @@ -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), - ), - ] diff --git a/services/backend/apps/user/migrations/0003_alter_user_status.py b/services/backend/apps/user/migrations/0003_alter_user_status.py deleted file mode 100644 index a7c766f..0000000 --- a/services/backend/apps/user/migrations/0003_alter_user_status.py +++ /dev/null @@ -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), - ), - ] diff --git a/services/backend/config/settings.py b/services/backend/config/settings.py index f4f2ba2..af79c52 100644 --- a/services/backend/config/settings.py +++ b/services/backend/config/settings.py @@ -445,6 +445,8 @@ INSTALLED_APPS = [ "apps.core", "apps.user", "apps.competition", + "apps.review", + "apps.task" ] # GUID From 9e6a1a6bfeb4ecd9dc863193464114768e1ad756 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9=20=D0=A1=D1=83=D0=BC?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Sat, 1 Mar 2025 13:12:38 +0300 Subject: [PATCH 2/2] added migrations --- .../competition/migrations/0001_initial.py | 47 +++++++++++++++++ .../apps/review/migrations/0001_initial.py | 27 ++++++++++ .../apps/task/migrations/0001_initial.py | 51 +++++++++++++++++++ .../apps/user/migrations/0001_initial.py | 29 +++++++++++ 4 files changed, 154 insertions(+) create mode 100644 services/backend/apps/competition/migrations/0001_initial.py create mode 100644 services/backend/apps/review/migrations/0001_initial.py create mode 100644 services/backend/apps/task/migrations/0001_initial.py create mode 100644 services/backend/apps/user/migrations/0001_initial.py diff --git a/services/backend/apps/competition/migrations/0001_initial.py b/services/backend/apps/competition/migrations/0001_initial.py new file mode 100644 index 0000000..2699fe9 --- /dev/null +++ b/services/backend/apps/competition/migrations/0001_initial.py @@ -0,0 +1,47 @@ +# Generated by Django 5.1.6 on 2025-03-01 08:47 + +import django.db.models.deletion +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('user', '0001_initial'), + ] + + 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='Тип соревнования')), + ('participants', models.ManyToManyField(related_name='participants', to='user.user')), + ], + options={ + 'verbose_name': 'соревнование', + 'verbose_name_plural': 'соревнования', + }, + ), + 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, + }, + ), + ] diff --git a/services/backend/apps/review/migrations/0001_initial.py b/services/backend/apps/review/migrations/0001_initial.py new file mode 100644 index 0000000..ceed39d --- /dev/null +++ b/services/backend/apps/review/migrations/0001_initial.py @@ -0,0 +1,27 @@ +# Generated by Django 5.1.6 on 2025-03-01 08:47 + +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Reviewer', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('name', models.CharField(max_length=100)), + ('surname', models.CharField(max_length=100)), + ('token', models.CharField(max_length=100)), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/services/backend/apps/task/migrations/0001_initial.py b/services/backend/apps/task/migrations/0001_initial.py new file mode 100644 index 0000000..5549424 --- /dev/null +++ b/services/backend/apps/task/migrations/0001_initial.py @@ -0,0 +1,51 @@ +# Generated by Django 5.1.6 on 2025-03-01 09:42 + +import apps.task.models +import django.db.models.deletion +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('competition', '0001_initial'), + ('user', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='CompetitionTask', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('title', models.TextField(max_length=50, verbose_name='заголовок')), + ('description', models.TextField(max_length=300, verbose_name='описание')), + ('type', models.CharField(choices=[('input', 'Input'), ('checker', 'Checker'), ('review', 'Review')], max_length=8)), + ('correct_answer_file', models.FileField(upload_to=apps.task.models.CompetitionTask.answer_file_upload_to)), + ('answer_file_path', models.TextField()), + ('criteries', models.JSONField(blank=True, null=True)), + ('competition', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='competition.competition')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='CompetetionTaskSumbission', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('status', models.CharField(choices=[('sent', 'Sent'), ('checking', 'Checking'), ('checked', 'Checked')], default='sent', max_length=8)), + ('content', models.FileField(upload_to=apps.task.models.CompetetionTaskSumbission.submission_content_upload_to)), + ('stdout', models.FileField(upload_to=apps.task.models.CompetetionTaskSumbission.submission_stdout_upload_to)), + ('result', models.JSONField(default={})), + ('timestamp', models.DateTimeField(auto_now_add=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='user.user')), + ('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='task.competitiontask')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/services/backend/apps/user/migrations/0001_initial.py b/services/backend/apps/user/migrations/0001_initial.py new file mode 100644 index 0000000..6fb8be0 --- /dev/null +++ b/services/backend/apps/user/migrations/0001_initial.py @@ -0,0 +1,29 @@ +# Generated by Django 5.1.6 on 2025-03-01 08:47 + +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='Пароль')), + ('status', models.CharField(choices=[('student', 'Student'), ('metodist', 'Metodist')], default='student', max_length=10)), + ], + options={ + 'verbose_name': 'пользователь', + 'verbose_name_plural': 'пользователи', + }, + ), + ]