diff --git a/services/backend/api/v1/competition/views.py b/services/backend/api/v1/competition/views.py
index 095549c..1f7eb77 100644
--- a/services/backend/api/v1/competition/views.py
+++ b/services/backend/api/v1/competition/views.py
@@ -18,6 +18,7 @@ router = Router(tags=["competition"])
status.OK: schemas.CompetitionOut,
status.BAD_REQUEST: global_schemas.BadRequestError,
status.UNAUTHORIZED: global_schemas.UnauthorizedError,
+ status.NOT_FOUND: global_schemas.NotFoundError,
},
)
def get_competition(
@@ -30,14 +31,14 @@ def get_competition(
@router.get(
"competitions",
response={
- status.OK: list[schemas.CompetitionListInstanceOut],
+ status.OK: list[schemas.CompetitionOut],
status.BAD_REQUEST: global_schemas.BadRequestError,
status.UNAUTHORIZED: global_schemas.UnauthorizedError,
},
)
def list_competitions(
request: HttpRequest, is_participating: bool
-) -> tuple[status, list[schemas.CompetitionListInstanceOut]]:
+) -> tuple[status, list[schemas.CompetitionOut]]:
user = request.auth
if is_participating:
competitions = Competition.objects.filter(participants=user)
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/api/v1/task/__init__.py b/services/backend/api/v1/task/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/services/backend/api/v1/task/schemas.py b/services/backend/api/v1/task/schemas.py
new file mode 100644
index 0000000..ecc98f3
--- /dev/null
+++ b/services/backend/api/v1/task/schemas.py
@@ -0,0 +1,21 @@
+from typing import Literal
+from uuid import UUID
+from ninja import ModelSchema, Schema
+
+from apps.competition.models import State
+from apps.task.models import CompetitionTask
+
+
+class TaskOutSchema(ModelSchema):
+ class Meta:
+ model = CompetitionTask
+ fields = ["id", "competition", "title", "description", "type"]
+
+
+class TaskSubmissionIn(Schema):
+ type: Literal["input", "file", "code"]
+ content: str
+
+
+class TaskSubmissionOut(Schema):
+ submission_id: UUID
diff --git a/services/backend/api/v1/task/views.py b/services/backend/api/v1/task/views.py
new file mode 100644
index 0000000..6710c95
--- /dev/null
+++ b/services/backend/api/v1/task/views.py
@@ -0,0 +1,115 @@
+from http import HTTPStatus as status
+from uuid import UUID
+
+from django.shortcuts import get_object_or_404
+from ninja import Router
+from django.shortcuts import get_object_or_404
+
+from api.v1.schemas import NotFoundError, UnauthorizedError, ForbiddenError
+from api.v1.ping.schemas import PingOut
+from api.v1.task.schemas import (
+ TaskOutSchema,
+ TaskSubmissionOut,
+ TaskSubmissionIn,
+)
+from apps.task.models import (
+ Competition,
+ CompetitionTask,
+ CompetetionTaskSumbission,
+)
+from apps.competition.models import State
+
+router = Router(tags=["competition"])
+
+
+@router.post(
+ "competitions/{competition_id}/start",
+ description="Start a competition completing (open access to tasks)",
+ response={
+ status.OK: PingOut,
+ status.UNAUTHORIZED: UnauthorizedError,
+ status.NOT_FOUND: NotFoundError,
+ },
+)
+def start_competition(request, competition_id: UUID) -> PingOut:
+ competition = get_object_or_404(Competition, pk=competition_id)
+ state_obj, _ = State.objects.update_or_create(
+ user=request.auth, competition=competition, state="started"
+ )
+ return status.OK, PingOut()
+
+
+@router.get(
+ "competitions/{competition_id}/tasks",
+ description="Get all tasks of competition (works only if user started competition)",
+ response={
+ status.OK: list[TaskOutSchema],
+ status.UNAUTHORIZED: UnauthorizedError,
+ status.FORBIDDEN: ForbiddenError,
+ status.NOT_FOUND: NotFoundError,
+ },
+)
+def get_competition_tasks(request, competition_id: UUID) -> list[TaskOutSchema]:
+ competition = get_object_or_404(Competition, pk=competition_id)
+ state = State.objects.filter(
+ user=request.auth, competition=competition, state="started"
+ ).first()
+ if not state:
+ return 403, ForbiddenError()
+
+ return status.OK, CompetitionTask.objects.filter(competition=competition).all()
+
+
+@router.get(
+ "competitions/{competition_id}/tasks/{task_id}",
+ description="Get a task of competition task",
+ response={
+ status.OK: TaskOutSchema,
+ status.UNAUTHORIZED: UnauthorizedError,
+ status.FORBIDDEN: ForbiddenError,
+ status.NOT_FOUND: NotFoundError,
+ },
+)
+def get_task(request, competition_id: str, task_id: str) -> TaskOutSchema: ...
+
+
+@router.post(
+ "competitions/{competition_id}/tasks/{task_id}/submit",
+ description="Submit task solution",
+ response={
+ status.OK: TaskSubmissionOut,
+ status.UNAUTHORIZED: UnauthorizedError,
+ status.FORBIDDEN: ForbiddenError,
+ status.NOT_FOUND: NotFoundError,
+ },
+)
+def submit_task(
+ request, competition_id: str, task_id: str, submission: TaskSubmissionIn
+) -> PingOut:
+ user = request.auth
+ competetion = get_object_or_404(Competition, id=competition_id)
+ task = get_object_or_404(
+ CompetitionTask, competetion=competetion, id=task_id
+ )
+
+ if task.type == CompetitionTask.CompetitionTaskType.INPUT:
+ CompetetionTaskSumbission.objects.create(
+ user=user,
+ task=task,
+ status=CompetetionTaskSumbission.StatusChoices.CHECKED,
+ result={"correct": submission.content == task.answer_file_path},
+ )
+ if task.type == CompetitionTask.CompetitionTaskType.REVIEW:
+ CompetetionTaskSumbission.objects.create(
+ user=user,
+ task=task,
+ status=CompetetionTaskSumbission.StatusChoices.SENT,
+ )
+ if task.type == CompetitionTask.CompetitionTaskType.CHECKER:
+ CompetetionTaskSumbission.objects.create(
+ user=user,
+ task=task,
+ status=CompetetionTaskSumbission.StatusChoices.CHECKING,
+ )
+
+ return TaskSubmissionOut(id=CompetetionTaskSumbission.id)
diff --git a/services/backend/apps/competition/migrations/0001_initial.py b/services/backend/apps/competition/migrations/0001_initial.py
index dd16963..2699fe9 100644
--- a/services/backend/apps/competition/migrations/0001_initial.py
+++ b/services/backend/apps/competition/migrations/0001_initial.py
@@ -1,5 +1,6 @@
-# Generated by Django 5.1.6 on 2025-02-28 21:27
+# 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
@@ -9,6 +10,7 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
+ ('user', '0001_initial'),
]
operations = [
@@ -23,10 +25,23 @@ class Migration(migrations.Migration):
('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/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/competition/migrations/0004_state_changed_at.py b/services/backend/apps/competition/migrations/0004_state_changed_at.py
new file mode 100644
index 0000000..365f995
--- /dev/null
+++ b/services/backend/apps/competition/migrations/0004_state_changed_at.py
@@ -0,0 +1,19 @@
+# Generated by Django 5.1.6 on 2025-03-01 08:10
+
+import datetime
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('competition', '0003_state'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='state',
+ name='changed_at',
+ field=models.DateTimeField(default=datetime.datetime.now),
+ ),
+ ]
diff --git a/services/backend/apps/competition/models.py b/services/backend/apps/competition/models.py
index 644f733..589ce91 100644
--- a/services/backend/apps/competition/models.py
+++ b/services/backend/apps/competition/models.py
@@ -1,3 +1,5 @@
+from datetime import datetime
+
from django.db import models
from apps.core.models import BaseModel
@@ -49,3 +51,4 @@ class State(BaseModel):
user = models.ForeignKey(User, on_delete=models.CASCADE)
competition = models.ForeignKey(Competition, on_delete=models.CASCADE)
state = models.CharField(choices=StateChoices.choices, max_length=11)
+ changed_at = models.DateTimeField(default=datetime.now)
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/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/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/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/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..8b38b3a 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,21 +15,64 @@ 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)
+ correct_answer_file = models.FileField(
+ upload_to=answer_file_upload_to, null=True, blank=True
+ )
+ points = models.IntegerField(null=True, blank=True)
# only when "checker" type
- answer_file_path = models.TextField()
+ answer_file_path = models.TextField(null=True, blank=True)
# only when "review" type
criteries = models.JSONField(blank=True, null=True)
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"
+
+ user = models.ForeignKey(User, on_delete=models.CASCADE)
+ task = models.ForeignKey(CompetitionTask, on_delete=models.CASCADE)
+
+ status = models.CharField(
+ choices=StatusChoices.choices,
+ default=StatusChoices.SENT,
+ max_length=8,
+ )
+
+ # code or text or file
+ content = models.FileField(upload_to=submission_content_upload_to)
+
+ # only if task type is checker
+ stdout = models.FileField(
+ upload_to=submission_stdout_upload_to, null=True, blank=True
+ )
+
+ # depends on task type:
+ # - input: {"correct": boolean}
+ # - file: {"total_points": integer, "by_criteria": ["criteria_name": integer]}
+ # - code: {"correct": boolean}
+ result = models.JSONField(default=None, null=True, blank=True)
+ # just more readable result representation, maybe will be calcuated somehow more complex depends on criteria
+ earned_points = models.IntegerField()
+
+ timestamp = models.DateTimeField(auto_now_add=True)
diff --git a/services/backend/apps/task/tasks.py b/services/backend/apps/task/tasks.py
new file mode 100644
index 0000000..b2f0286
--- /dev/null
+++ b/services/backend/apps/task/tasks.py
@@ -0,0 +1,128 @@
+import tempfile
+import os
+import sys
+import ast
+from io import StringIO
+import hashlib
+from config.celery import app
+
+ALLOWED_MODULES = {
+ "pandas",
+ "numpy",
+ "matplotlib",
+ "seaborn",
+ "scipy",
+ "sklearn",
+ "datetime",
+ "json",
+ "csv",
+ "math",
+ "statistics",
+}
+
+
+class SecurityException(Exception):
+ pass
+
+
+def validate_code(code_str):
+ try:
+ tree = ast.parse(code_str)
+ except SyntaxError as e:
+ raise SecurityException(f"Syntax error: {str(e)}")
+
+ class ImportVisitor(ast.NodeVisitor):
+ def visit_Import(self, node):
+ for alias in node.names:
+ module = alias.name.split(".")[0]
+ if module not in ALLOWED_MODULES:
+ raise SecurityException(f"Disallowed import: {module}")
+
+ def visit_ImportFrom(self, node):
+ if node.module:
+ module = node.module.split(".")[0]
+ if module not in ALLOWED_MODULES:
+ raise SecurityException(
+ f"Disallowed import from: {module}"
+ )
+
+ class SecurityVisitor(ast.NodeVisitor):
+ def generic_visit(self, node):
+ if isinstance(node, (ast.Call, ast.Attribute)):
+ if "system" in getattr(node, "attr", ""):
+ raise SecurityException("Dangerous system call detected")
+ super().generic_visit(node)
+
+ try:
+ ImportVisitor().visit(tree)
+ SecurityVisitor().visit(tree)
+ except SecurityException as e:
+ raise
+ except Exception as e:
+ raise SecurityException(f"Security check failed: {str(e)}")
+
+
+def secure_exec(code_str, result_path):
+ original_dir = os.getcwd()
+ original_stdout = sys.stdout
+ sys.stdout = captured_stdout = StringIO()
+ result_content = None
+
+ with tempfile.TemporaryDirectory() as temp_dir:
+ try:
+ os.chdir(temp_dir)
+ restricted_globals = {
+ "__builtins__": {
+ "open": lambda f, *a, **kw: open(f, *a, **kw),
+ "print": print,
+ "str": str,
+ "int": int,
+ "float": float,
+ "bool": bool,
+ "list": list,
+ "dict": dict,
+ "tuple": tuple,
+ "set": set,
+ }
+ }
+
+ exec(code_str, restricted_globals)
+
+ if result_path == "stdout":
+ result_content = captured_stdout.getvalue().encode("utf-8")
+ else:
+ with open(result_path, "rb") as f:
+ result_content = f.read()
+
+ except Exception as e:
+ raise RuntimeError(f"Execution error: {str(e)}")
+ finally:
+ os.chdir(original_dir)
+ sys.stdout = original_stdout
+
+ return result_content
+
+
+@app.task(bind=True)
+def analyze_data_task(self, code_str, result_path, expected_bytes):
+ try:
+ validate_code(code_str)
+
+ result_content = secure_exec(code_str, result_path)
+
+ result_hash = hashlib.sha256(result_content).hexdigest()
+ expected_hash = hashlib.sha256(expected_bytes).hexdigest()
+
+ return {
+ "success": True,
+ "match": result_hash == expected_hash,
+ "result_hash": result_hash,
+ "expected_hash": expected_hash,
+ }
+
+ except SecurityException as e:
+ return {"success": False, "error": f"Security violation: {str(e)}"}
+ except RuntimeError as e:
+ return {"success": False, "error": f"Execution error: {str(e)}"}
+ except Exception as e:
+ return {"success": False, "error": f"Unexpected error: {str(e)}"}
diff --git a/services/backend/apps/user/migrations/0001_initial.py b/services/backend/apps/user/migrations/0001_initial.py
index 41491e9..6fb8be0 100644
--- a/services/backend/apps/user/migrations/0001_initial.py
+++ b/services/backend/apps/user/migrations/0001_initial.py
@@ -1,4 +1,4 @@
-# Generated by Django 5.1.6 on 2025-02-28 20:46
+# Generated by Django 5.1.6 on 2025-03-01 08:47
import uuid
from django.db import migrations, models
@@ -19,6 +19,7 @@ class Migration(migrations.Migration):
('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': 'пользователь',
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
diff --git a/services/frontend/src/App.tsx b/services/frontend/src/App.tsx
index 820d663..d7bd1eb 100644
--- a/services/frontend/src/App.tsx
+++ b/services/frontend/src/App.tsx
@@ -3,17 +3,21 @@ import "./styles/globals.css";
import CompetitionsPage from "./pages/CompetitionsPage";
import CompetitionPreviewPage from "./pages/CompetitionPreviewPage";
import CompetitionRunnerPage from "./pages/CompetitionRunnerPage";
-
+import { NavbarLayout } from "./widgets/navbar-layout";
const App = () => {
return (