diff --git a/services/backend/api/v1/review/auth.py b/services/backend/api/v1/review/auth.py index 9fbd270..70e24bb 100644 --- a/services/backend/api/v1/review/auth.py +++ b/services/backend/api/v1/review/auth.py @@ -1,26 +1,25 @@ 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]: + def _get_key(self, request: HttpRequest) -> str | None: 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 + return reviewer diff --git a/services/backend/api/v1/review/schemas.py b/services/backend/api/v1/review/schemas.py index 824e62d..70bf9b3 100644 --- a/services/backend/api/v1/review/schemas.py +++ b/services/backend/api/v1/review/schemas.py @@ -1,8 +1,8 @@ -from typing import List, Literal +from typing import Literal from uuid import UUID from django.http import HttpRequest -from ninja import Schema, ModelSchema +from ninja import ModelSchema, Schema from apps.review.models import Reviewer from apps.task.models import CompetetionTaskSumbission @@ -11,6 +11,7 @@ from apps.task.models import CompetetionTaskSumbission class PingOut(Schema): status: str = "ok" + class ReviewerOut(ModelSchema): id: UUID @@ -18,20 +19,19 @@ class ReviewerOut(ModelSchema): model = Reviewer exclude = ("token",) + class SubmissionOut(ModelSchema): id: UUID status: Literal["sent", "checking", "checked"] class Meta: model = CompetetionTaskSumbission - exclude = ( - "user", - ) + 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 + def resolve_submissions(self, context: HttpRequest) -> list[SubmissionOut]: + return list(CompetetionTaskSumbission.objects.all()) diff --git a/services/backend/api/v1/review/views.py b/services/backend/api/v1/review/views.py index b3e1bdf..d628faf 100644 --- a/services/backend/api/v1/review/views.py +++ b/services/backend/api/v1/review/views.py @@ -3,19 +3,20 @@ 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 +from api.v1.review import schemas router = Router(tags=["review"]) @router.get( - "{token}/tasks", + "{token}/submissions", response={ 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() @@ -23,12 +24,9 @@ def ping(request: HttpRequest, token) -> tuple[status, schemas.SubmissionsOut]: "{token}", response={ status.OK: schemas.ReviewerOut, - status.UNAUTHORIZED: global_schemas.UnauthorizedError + status.UNAUTHORIZED: global_schemas.UnauthorizedError, }, - description="token есть и в сваггер авторизации, но оно не работает, не верьте. подставляйте токен вручную в query" + description="token есть и в сваггер авторизации, но оно не работает, не верьте. подставляйте токен вручную в query", ) -def get_reviewer( - request: HttpRequest, - token: str -): - return status.OK, request.auth \ No newline at end of file +def get_reviewer_profile(request: HttpRequest, token: str): + return status.OK, request.auth diff --git a/services/backend/api/v1/router.py b/services/backend/api/v1/router.py index e85570a..56a5206 100644 --- a/services/backend/api/v1/router.py +++ b/services/backend/api/v1/router.py @@ -7,8 +7,8 @@ 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 +from api.v1.user.views import router as user_router router = NinjaAPI( title="DataRush API", @@ -39,6 +39,5 @@ router.add_router( ) - for exception, handler in handlers.exception_handlers: router.add_exception_handler(exception, partial(handler, router=router)) diff --git a/services/backend/api/v1/task/schemas.py b/services/backend/api/v1/task/schemas.py index ecc98f3..e5a4046 100644 --- a/services/backend/api/v1/task/schemas.py +++ b/services/backend/api/v1/task/schemas.py @@ -1,8 +1,8 @@ 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 diff --git a/services/backend/api/v1/task/views.py b/services/backend/api/v1/task/views.py index 6710c95..208dd36 100644 --- a/services/backend/api/v1/task/views.py +++ b/services/backend/api/v1/task/views.py @@ -3,21 +3,20 @@ 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.schemas import ForbiddenError, NotFoundError, UnauthorizedError from api.v1.task.schemas import ( TaskOutSchema, - TaskSubmissionOut, TaskSubmissionIn, -) -from apps.task.models import ( - Competition, - CompetitionTask, - CompetetionTaskSumbission, + TaskSubmissionOut, ) from apps.competition.models import State +from apps.task.models import ( + CompetetionTaskSumbission, + Competition, + CompetitionTask, +) router = Router(tags=["competition"]) @@ -49,7 +48,9 @@ def start_competition(request, competition_id: UUID) -> PingOut: 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) state = State.objects.filter( user=request.auth, competition=competition, state="started" @@ -57,7 +58,9 @@ def get_competition_tasks(request, competition_id: UUID) -> list[TaskOutSchema]: if not state: return 403, ForbiddenError() - return status.OK, CompetitionTask.objects.filter(competition=competition).all() + return status.OK, CompetitionTask.objects.filter( + competition=competition + ).all() @router.get( diff --git a/services/backend/api/v1/user/views.py b/services/backend/api/v1/user/views.py index f4629b7..ff49988 100644 --- a/services/backend/api/v1/user/views.py +++ b/services/backend/api/v1/user/views.py @@ -64,6 +64,7 @@ def sign_in(request, data: LoginSchema): def get_me(request): return 200, request.auth + @router.get( path="/user/{user_id}", response={ diff --git a/services/backend/apps/core/management/commands/generate_data.py b/services/backend/apps/core/management/commands/generate_data.py new file mode 100644 index 0000000..c781811 --- /dev/null +++ b/services/backend/apps/core/management/commands/generate_data.py @@ -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}'" + ) diff --git a/services/backend/apps/review/models.py b/services/backend/apps/review/models.py index 58bd512..02b74af 100644 --- a/services/backend/apps/review/models.py +++ b/services/backend/apps/review/models.py @@ -7,4 +7,4 @@ 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 + token = models.CharField(max_length=100) diff --git a/services/backend/apps/task/models.py b/services/backend/apps/task/models.py index 8b38b3a..08fe998 100644 --- a/services/backend/apps/task/models.py +++ b/services/backend/apps/task/models.py @@ -1,13 +1,13 @@ -from random import choice from uuid import uuid4 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.task.validators import ContestTaskCriteriesValidator from apps.user.models import User + class CompetitionTask(BaseModel): class CompetitionTaskType(models.TextChoices): INPUT = "input" @@ -45,7 +45,7 @@ class CompetetionTaskSumbission(BaseModel): CHECKED = "checked" 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: return f"/submissions/{instance.id}/stdout" diff --git a/services/backend/apps/task/tasks.py b/services/backend/apps/task/tasks.py index b2f0286..9899246 100644 --- a/services/backend/apps/task/tasks.py +++ b/services/backend/apps/task/tasks.py @@ -1,9 +1,10 @@ -import tempfile +import ast +import hashlib import os import sys -import ast +import tempfile from io import StringIO -import hashlib + from config.celery import app ALLOWED_MODULES = { @@ -29,7 +30,7 @@ def validate_code(code_str): try: tree = ast.parse(code_str) except SyntaxError as e: - raise SecurityException(f"Syntax error: {str(e)}") + raise SecurityException(f"Syntax error: {e!s}") class ImportVisitor(ast.NodeVisitor): def visit_Import(self, node): @@ -56,10 +57,10 @@ def validate_code(code_str): try: ImportVisitor().visit(tree) SecurityVisitor().visit(tree) - except SecurityException as e: + except SecurityException: raise 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): @@ -95,7 +96,7 @@ def secure_exec(code_str, result_path): result_content = f.read() except Exception as e: - raise RuntimeError(f"Execution error: {str(e)}") + raise RuntimeError(f"Execution error: {e!s}") finally: os.chdir(original_dir) sys.stdout = original_stdout @@ -121,8 +122,8 @@ def analyze_data_task(self, code_str, result_path, expected_bytes): } 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: - return {"success": False, "error": f"Execution error: {str(e)}"} + return {"success": False, "error": f"Execution error: {e!s}"} except Exception as e: - return {"success": False, "error": f"Unexpected error: {str(e)}"} + return {"success": False, "error": f"Unexpected error: {e!s}"} diff --git a/services/backend/apps/task/validators.py b/services/backend/apps/task/validators.py index f028f52..ec8024f 100644 --- a/services/backend/apps/task/validators.py +++ b/services/backend/apps/task/validators.py @@ -12,12 +12,12 @@ class Criteria(BaseModel): class ContestTaskCriteriesValidator: 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" raise ValidationError(err) try: - for criteria in instance.criterties: + for criteria in instance.criteries if instance.criteries else []: Criteria(**criteria) except PydanticValidationError: err = "invalid criteries data" diff --git a/services/backend/apps/user/test.py b/services/backend/apps/user/test.py index 437da0d..cad7709 100644 --- a/services/backend/apps/user/test.py +++ b/services/backend/apps/user/test.py @@ -24,9 +24,6 @@ class TestSignUp(TestCase): user.full_clean() def test_missing_params(self): - user = User( - password="123123", - username="132131232131" - ) + user = User(password="123123", username="132131232131") with self.assertRaises(ValidationError): user.full_clean() diff --git a/services/backend/config/settings.py b/services/backend/config/settings.py index af79c52..63e2122 100644 --- a/services/backend/config/settings.py +++ b/services/backend/config/settings.py @@ -446,7 +446,7 @@ INSTALLED_APPS = [ "apps.user", "apps.competition", "apps.review", - "apps.task" + "apps.task", ] # GUID