Merge branch 'master' of gitlab.prodcontest.ru:team-15/project

This commit is contained in:
ITQ
2025-03-01 14:15:36 +03:00
14 changed files with 191 additions and 57 deletions
+4 -5
View File
@@ -1,26 +1,25 @@
from abc import ABC from abc import ABC
from typing import Optional
from django.http import HttpRequest from django.http import HttpRequest
from django.shortcuts import get_object_or_404
from django.urls import resolve from django.urls import resolve
from ninja.errors import AuthenticationError from ninja.errors import AuthenticationError
from ninja.security import APIKeyQuery
from ninja.security.apikey import APIKeyBase from ninja.security.apikey import APIKeyBase
from apps.review.models import Reviewer from apps.review.models import Reviewer
class APIKeyPath(APIKeyBase, ABC): class APIKeyPath(APIKeyBase, ABC):
openapi_in: str = "path" openapi_in: str = "path"
def _get_key(self, request: HttpRequest) -> Optional[str]: def _get_key(self, request: HttpRequest) -> str | None:
func, args, kwargs = resolve(request.path) func, args, kwargs = resolve(request.path)
return kwargs.get(self.param_name) return kwargs.get(self.param_name)
class ReviewerAuth(APIKeyPath): class ReviewerAuth(APIKeyPath):
param_name = "token" param_name = "token"
def authenticate(self, request, token): def authenticate(self, request, token):
if not (reviewer := Reviewer.objects.filter(token=token).first()): if not (reviewer := Reviewer.objects.filter(token=token).first()):
raise AuthenticationError raise AuthenticationError
return reviewer return reviewer
+8 -8
View File
@@ -1,8 +1,8 @@
from typing import List, Literal from typing import Literal
from uuid import UUID from uuid import UUID
from django.http import HttpRequest from django.http import HttpRequest
from ninja import Schema, ModelSchema from ninja import ModelSchema, Schema
from apps.review.models import Reviewer from apps.review.models import Reviewer
from apps.task.models import CompetetionTaskSumbission from apps.task.models import CompetetionTaskSumbission
@@ -11,6 +11,7 @@ from apps.task.models import CompetetionTaskSumbission
class PingOut(Schema): class PingOut(Schema):
status: str = "ok" status: str = "ok"
class ReviewerOut(ModelSchema): class ReviewerOut(ModelSchema):
id: UUID id: UUID
@@ -18,20 +19,19 @@ class ReviewerOut(ModelSchema):
model = Reviewer model = Reviewer
exclude = ("token",) exclude = ("token",)
class SubmissionOut(ModelSchema): class SubmissionOut(ModelSchema):
id: UUID id: UUID
status: Literal["sent", "checking", "checked"] status: Literal["sent", "checking", "checked"]
class Meta: class Meta:
model = CompetetionTaskSumbission model = CompetetionTaskSumbission
exclude = ( exclude = ("user",)
"user",
)
class SubmissionsOut(Schema): class SubmissionsOut(Schema):
submissions: list[SubmissionOut] = [] submissions: list[SubmissionOut] = []
@staticmethod @staticmethod
def resolve_submissions(self, context: HttpRequest) -> List[SubmissionOut]: def resolve_submissions(self, context: HttpRequest) -> list[SubmissionOut]:
print(CompetetionTaskSumbission.objects.all()) return list(CompetetionTaskSumbission.objects.all())
return list(CompetetionTaskSumbission.objects.all())
+8 -10
View File
@@ -3,19 +3,20 @@ from http import HTTPStatus as status
from django.http import HttpRequest from django.http import HttpRequest
from ninja import Router from ninja import Router
from api.v1.review import schemas
from api.v1 import schemas as global_schemas from api.v1 import schemas as global_schemas
from api.v1.review import schemas
router = Router(tags=["review"]) router = Router(tags=["review"])
@router.get( @router.get(
"{token}/tasks", "{token}/submissions",
response={ response={
status.OK: schemas.SubmissionsOut, status.OK: schemas.SubmissionsOut,
}, },
description="Список отправок, на проверку которых назначен ревьюер"
) )
def ping(request: HttpRequest, token) -> tuple[status, schemas.SubmissionsOut]: def get_submissions(request: HttpRequest, token) -> tuple[status, schemas.SubmissionsOut]:
return status.OK, schemas.SubmissionsOut() return status.OK, schemas.SubmissionsOut()
@@ -23,12 +24,9 @@ def ping(request: HttpRequest, token) -> tuple[status, schemas.SubmissionsOut]:
"{token}", "{token}",
response={ response={
status.OK: schemas.ReviewerOut, status.OK: schemas.ReviewerOut,
status.UNAUTHORIZED: global_schemas.UnauthorizedError status.UNAUTHORIZED: global_schemas.UnauthorizedError,
}, },
description="token есть и в сваггер авторизации, но оно не работает, не верьте. подставляйте токен вручную в query" description="token есть и в сваггер авторизации, но оно не работает, не верьте. подставляйте токен вручную в query",
) )
def get_reviewer( def get_reviewer_profile(request: HttpRequest, token: str):
request: HttpRequest, return status.OK, request.auth
token: str
):
return status.OK, request.auth
+1 -2
View File
@@ -7,8 +7,8 @@ from api.v1.auth import BearerAuth
from api.v1.competition.views import router as competition_router from api.v1.competition.views import router as competition_router
from api.v1.ping.views import router as ping_router from api.v1.ping.views import router as ping_router
from api.v1.review.auth import ReviewerAuth 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 from api.v1.review.views import router as review_router
from api.v1.user.views import router as user_router
router = NinjaAPI( router = NinjaAPI(
title="DataRush API", title="DataRush API",
@@ -39,6 +39,5 @@ router.add_router(
) )
for exception, handler in handlers.exception_handlers: for exception, handler in handlers.exception_handlers:
router.add_exception_handler(exception, partial(handler, router=router)) router.add_exception_handler(exception, partial(handler, router=router))
+1 -1
View File
@@ -1,8 +1,8 @@
from typing import Literal from typing import Literal
from uuid import UUID from uuid import UUID
from ninja import ModelSchema, Schema from ninja import ModelSchema, Schema
from apps.competition.models import State
from apps.task.models import CompetitionTask from apps.task.models import CompetitionTask
+13 -10
View File
@@ -3,21 +3,20 @@ from uuid import UUID
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from ninja import Router 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.ping.schemas import PingOut
from api.v1.schemas import ForbiddenError, NotFoundError, UnauthorizedError
from api.v1.task.schemas import ( from api.v1.task.schemas import (
TaskOutSchema, TaskOutSchema,
TaskSubmissionOut,
TaskSubmissionIn, TaskSubmissionIn,
) TaskSubmissionOut,
from apps.task.models import (
Competition,
CompetitionTask,
CompetetionTaskSumbission,
) )
from apps.competition.models import State from apps.competition.models import State
from apps.task.models import (
CompetetionTaskSumbission,
Competition,
CompetitionTask,
)
router = Router(tags=["competition"]) router = Router(tags=["competition"])
@@ -49,7 +48,9 @@ def start_competition(request, competition_id: UUID) -> PingOut:
status.NOT_FOUND: NotFoundError, status.NOT_FOUND: NotFoundError,
}, },
) )
def get_competition_tasks(request, competition_id: UUID) -> list[TaskOutSchema]: def get_competition_tasks(
request, competition_id: UUID
) -> list[TaskOutSchema]:
competition = get_object_or_404(Competition, pk=competition_id) competition = get_object_or_404(Competition, pk=competition_id)
state = State.objects.filter( state = State.objects.filter(
user=request.auth, competition=competition, state="started" user=request.auth, competition=competition, state="started"
@@ -57,7 +58,9 @@ def get_competition_tasks(request, competition_id: UUID) -> list[TaskOutSchema]:
if not state: if not state:
return 403, ForbiddenError() return 403, ForbiddenError()
return status.OK, CompetitionTask.objects.filter(competition=competition).all() return status.OK, CompetitionTask.objects.filter(
competition=competition
).all()
@router.get( @router.get(
+1
View File
@@ -64,6 +64,7 @@ def sign_in(request, data: LoginSchema):
def get_me(request): def get_me(request):
return 200, request.auth return 200, request.auth
@router.get( @router.get(
path="/user/{user_id}", path="/user/{user_id}",
response={ response={
@@ -0,0 +1,136 @@
import random
import uuid
from datetime import timedelta
from django.contrib.auth.hashers import make_password
from django.core.files.base import ContentFile
from django.core.management.base import BaseCommand
from django.utils import timezone
from apps.competition.models import Competition, State
from apps.task.models import CompetetionTaskSumbission, CompetitionTask
from apps.user.models import User, UserRole
class Command(BaseCommand):
help = "Generate sample data for Users, Competitions, Tasks, Submissions, and States."
def handle(self, *args, **options):
self.stdout.write("Starting data generation...")
users = self.create_users(5)
competitions = self.create_competitions(2, users)
tasks = self.create_tasks(competitions)
self.create_submissions(tasks, users)
self.create_states(competitions, users)
self.stdout.write("Data generation completed.")
def create_users(self, count):
users = []
for i in range(1, count + 1):
email = f"user{i}@example.com"
username = f"user{i}"
password = (
"password123" # In production, use proper password handling.
)
role = random.choice(
[UserRole.STUDENT.value, UserRole.METODIST.value]
)
user, created = User.objects.get_or_create(
email=email,
defaults={
"username": username,
"password": make_password(password),
"status": role,
},
)
users.append(user)
self.stdout.write(f"Created user: {username}")
return users
def create_competitions(self, count, users):
competitions = []
now = timezone.now()
for i in range(1, count + 1):
title = f"Competition {i}"
description = f"Description for competition {i}"
start_date = now - timedelta(days=random.randint(1, 10))
end_date = now + timedelta(days=random.randint(1, 10))
competition = Competition.objects.create(
title=title,
description=description,
start_date=start_date,
end_date=end_date,
type="solo", # assuming only one type for now
participation_type=random.choice(["edu", "competitive"]),
)
# Add random participants
selected_users = random.sample(
users, k=min(len(users), random.randint(1, len(users)))
)
competition.participants.add(*selected_users)
competitions.append(competition)
self.stdout.write(f"Created competition: {title}")
return competitions
def create_tasks(self, competitions):
tasks = []
task_types = [
CompetitionTask.CompetitionTaskType.INPUT.value,
]
for comp in competitions:
# Create 3 tasks per competition
for i in range(1, 4):
task_type = random.choice(task_types)
title = f"Task {i} for {comp.title}"
description = f"Task description for task {i} in {comp.title}"
task = CompetitionTask.objects.create(
competition=comp,
title=title,
description=description,
type=task_type,
points=random.randint(1, 10),
)
tasks.append(task)
self.stdout.write(f"Created task: {title} (type: {task_type})")
return tasks
def create_submissions(self, tasks, users):
for task in tasks:
# Each task will get between 1 and 3 submissions
num_submissions = random.randint(1, 3)
for _ in range(num_submissions):
user = random.choice(users)
# Create a dummy content file
dummy_content = ContentFile(
b"Submission content",
name=f"submission_{uuid.uuid4().hex}.txt",
)
submission = CompetetionTaskSumbission.objects.create(
user=user,
task=task,
earned_points=random.randint(
0, task.points if task.points else 10
),
content=dummy_content,
)
submission.save()
self.stdout.write(
f"Created submission for task '{task.title}' by user '{user.username}'"
)
def create_states(self, competitions, users):
# For each competition, create a State for some of its participants
state_choices = [choice for choice in State.StateChoices.values]
for comp in competitions:
for user in comp.participants.all():
state_obj, created = State.objects.get_or_create(
user=user,
competition=comp,
defaults={
"state": random.choice(state_choices),
"changed_at": timezone.now(),
},
)
self.stdout.write(
f"Created state '{state_obj.state}' for user '{user.username}' in competition '{comp.title}'"
)
+1 -1
View File
@@ -7,4 +7,4 @@ class Reviewer(BaseModel):
name = models.CharField(max_length=100) name = models.CharField(max_length=100)
surname = models.CharField(max_length=100) surname = models.CharField(max_length=100)
token = models.CharField(max_length=100) token = models.CharField(max_length=100)
+3 -3
View File
@@ -1,13 +1,13 @@
from random import choice
from uuid import uuid4 from uuid import uuid4
from django.db import models from django.db import models
from apps.task.validators import ContestTaskCriteriesValidator
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.task.validators import ContestTaskCriteriesValidator
from apps.user.models import User from apps.user.models import User
class CompetitionTask(BaseModel): class CompetitionTask(BaseModel):
class CompetitionTaskType(models.TextChoices): class CompetitionTaskType(models.TextChoices):
INPUT = "input" INPUT = "input"
@@ -45,7 +45,7 @@ class CompetetionTaskSumbission(BaseModel):
CHECKED = "checked" CHECKED = "checked"
def submission_content_upload_to(instance, filename) -> str: def submission_content_upload_to(instance, filename) -> str:
return f"/submissions/{instance.id}/content" return f"submissions/{instance.id}/content"
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"
+11 -10
View File
@@ -1,9 +1,10 @@
import tempfile import ast
import hashlib
import os import os
import sys import sys
import ast import tempfile
from io import StringIO from io import StringIO
import hashlib
from config.celery import app from config.celery import app
ALLOWED_MODULES = { ALLOWED_MODULES = {
@@ -29,7 +30,7 @@ def validate_code(code_str):
try: try:
tree = ast.parse(code_str) tree = ast.parse(code_str)
except SyntaxError as e: except SyntaxError as e:
raise SecurityException(f"Syntax error: {str(e)}") raise SecurityException(f"Syntax error: {e!s}")
class ImportVisitor(ast.NodeVisitor): class ImportVisitor(ast.NodeVisitor):
def visit_Import(self, node): def visit_Import(self, node):
@@ -56,10 +57,10 @@ def validate_code(code_str):
try: try:
ImportVisitor().visit(tree) ImportVisitor().visit(tree)
SecurityVisitor().visit(tree) SecurityVisitor().visit(tree)
except SecurityException as e: except SecurityException:
raise raise
except Exception as e: except Exception as e:
raise SecurityException(f"Security check failed: {str(e)}") raise SecurityException(f"Security check failed: {e!s}")
def secure_exec(code_str, result_path): def secure_exec(code_str, result_path):
@@ -95,7 +96,7 @@ def secure_exec(code_str, result_path):
result_content = f.read() result_content = f.read()
except Exception as e: except Exception as e:
raise RuntimeError(f"Execution error: {str(e)}") raise RuntimeError(f"Execution error: {e!s}")
finally: finally:
os.chdir(original_dir) os.chdir(original_dir)
sys.stdout = original_stdout sys.stdout = original_stdout
@@ -121,8 +122,8 @@ def analyze_data_task(self, code_str, result_path, expected_bytes):
} }
except SecurityException as e: except SecurityException as e:
return {"success": False, "error": f"Security violation: {str(e)}"} return {"success": False, "error": f"Security violation: {e!s}"}
except RuntimeError as e: except RuntimeError as e:
return {"success": False, "error": f"Execution error: {str(e)}"} return {"success": False, "error": f"Execution error: {e!s}"}
except Exception as e: except Exception as e:
return {"success": False, "error": f"Unexpected error: {str(e)}"} return {"success": False, "error": f"Unexpected error: {e!s}"}
+2 -2
View File
@@ -12,12 +12,12 @@ class Criteria(BaseModel):
class ContestTaskCriteriesValidator: class ContestTaskCriteriesValidator:
def __call__(self, instance): def __call__(self, instance):
if instance.criterties and not isinstance(instance.criterties, list): if instance.criteries and not isinstance(instance.criteries, list):
err = "criteries must be a valid dictionary" err = "criteries must be a valid dictionary"
raise ValidationError(err) raise ValidationError(err)
try: try:
for criteria in instance.criterties: for criteria in instance.criteries if instance.criteries else []:
Criteria(**criteria) Criteria(**criteria)
except PydanticValidationError: except PydanticValidationError:
err = "invalid criteries data" err = "invalid criteries data"
+1 -4
View File
@@ -24,9 +24,6 @@ class TestSignUp(TestCase):
user.full_clean() user.full_clean()
def test_missing_params(self): def test_missing_params(self):
user = User( user = User(password="123123", username="132131232131")
password="123123",
username="132131232131"
)
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
user.full_clean() user.full_clean()
+1 -1
View File
@@ -446,7 +446,7 @@ INSTALLED_APPS = [
"apps.user", "apps.user",
"apps.competition", "apps.competition",
"apps.review", "apps.review",
"apps.task" "apps.task",
] ]
# GUID # GUID