From 0bcda79f91e9ed90eaa8a404de028d95d698a4c2 Mon Sep 17 00:00:00 2001 From: ITQ Date: Sat, 1 Mar 2025 13:54:49 +0300 Subject: [PATCH 1/8] added backend-staticfiles and minio to nginx --- infrastructure/nginx/nginx.conf | 70 +++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/infrastructure/nginx/nginx.conf b/infrastructure/nginx/nginx.conf index 0286b05..79380d3 100644 --- a/infrastructure/nginx/nginx.conf +++ b/infrastructure/nginx/nginx.conf @@ -109,6 +109,24 @@ http { proxy_read_timeout 600s; } + location /static { + proxy_pass http://backend-staticfiles:80; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_cache_bypass $http_upgrade; + proxy_hide_header X-Powered-By; + + proxy_connect_timeout 75s; + proxy_send_timeout 600s; + proxy_read_timeout 600s; + } + location /api { proxy_pass http://backend:8080; proxy_http_version 1.1; @@ -142,4 +160,56 @@ http { return 444; } } + + server { + listen 443 ssl http2 default_server; + listen [::]:443 ssl http2 default_server; + server_name prod-team-15-minio-2pc0i3lc.final.prodcontest.ru; + + ssl_certificate /etc/nginx/certs/fullchain.pem; + ssl_certificate_key /etc/nginx/certs/privkey.pem; + + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + + ignore_invalid_headers off; + client_max_body_size 0; + proxy_buffering off; + proxy_request_buffering off; + + location / { + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_connect_timeout 300; + proxy_http_version 1.1; + proxy_set_header Connection ""; + chunked_transfer_encoding off; + + proxy_pass http://minio:9000; + } + + location /minio/ui/ { + rewrite ^/minio/ui/(.*) /$1 break; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-NginX-Proxy true; + + real_ip_header X-Real-IP; + + proxy_connect_timeout 300; + + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + chunked_transfer_encoding off; + + proxy_pass http://minio:9001; + } + } } From 3d8260c7bc000e165d56d1edd83c165b16ea5e0f Mon Sep 17 00:00:00 2001 From: ITQ Date: Sat, 1 Mar 2025 13:56:44 +0300 Subject: [PATCH 2/8] too lazy to explain --- infrastructure/backend/.env.template | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/infrastructure/backend/.env.template b/infrastructure/backend/.env.template index c964cde..8866be3 100644 --- a/infrastructure/backend/.env.template +++ b/infrastructure/backend/.env.template @@ -1,11 +1,11 @@ DJANGO_SECRET_KEY=secretees DJANGO_DEBUG=False DJANGO_ALLOWED_HOSTS=* -DJANGO_CSRF_TRUSTED_ORIGINS=http://localhost,http://127.0.0.1 +DJANGO_CSRF_TRUSTED_ORIGINS=http://localhost,http://127.0.0.1,https://*.final.prodcontest.ru DJANGO_CORS_ALLOWED_ORIGINS=* DJANGO_INTERNAL_IPS=127.0.0.1 -DJANGO_LANGUAGE_CODE=en-us -DJANGO_STATIC_URL=http://localhost:13241/ +DJANGO_LANGUAGE_CODE=ru +DJANGO_STATIC_URL=static/ REDIS_URI=redis://redis:6379 DJANGO_DB_URI=postgresql://postgres:postgres@postgres/postgres @@ -15,6 +15,6 @@ DJANGO_SUPERUSER_EMAIL=admin@mail.com DJANGO_SUPERUSER_PASSWORD=admin MINIO_ENDPOINT=minio:9000 -MINIO_CUSTOM_ENDPOINT_URL=http://127.0.0.1:13244 +MINIO_CUSTOM_ENDPOINT_URL=https://prod-team-15-minio-2pc0i3lc.final.prodcontest.ru MINIO_ACCESS_KEY=admin MINIO_SECRET_KEY=password From e1adf638e10c5263214f0b031cf3324ec6310632 Mon Sep 17 00:00:00 2001 From: moolcoov Date: Sat, 1 Mar 2025 14:00:37 +0300 Subject: [PATCH 3/8] fix: build --- services/frontend/src/pages/.gitkeep | 0 .../pages/CompetitionPreviewPage/index.tsx | 106 ------------------ .../utils/utils.ts | 0 .../frontend/src/pages/Competitions/types.ts | 0 4 files changed, 106 deletions(-) delete mode 100644 services/frontend/src/pages/.gitkeep delete mode 100644 services/frontend/src/pages/CompetitionPreviewPage/index.tsx rename services/frontend/src/pages/{CompetitionRunnerPage => CompetitionSession}/utils/utils.ts (100%) delete mode 100644 services/frontend/src/pages/Competitions/types.ts diff --git a/services/frontend/src/pages/.gitkeep b/services/frontend/src/pages/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/services/frontend/src/pages/CompetitionPreviewPage/index.tsx b/services/frontend/src/pages/CompetitionPreviewPage/index.tsx deleted file mode 100644 index 15bcdc8..0000000 --- a/services/frontend/src/pages/CompetitionPreviewPage/index.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import { useEffect, useState } from "react"; -import { useParams, useNavigate } from "react-router-dom"; -import Navbar from "@/widgets/Navbar"; -import { Button } from "@/components/ui/button"; -import { ArrowLeft } from "lucide-react"; -import { Competition } from "@/shared/types"; -import { mockCompetitions, mockTasks } from "@/shared/mocks/mocks"; - -const CompetitionPreview = () => { - const { id } = useParams<{ id: string }>(); - const navigate = useNavigate(); - const [competition, setCompetition] = useState(null); - const [isLoading, setIsLoading] = useState(true); - - useEffect(() => { - const fetchCompetition = async () => { - try { - setTimeout(() => { - const found = mockCompetitions.find((comp) => comp.id === id); - setCompetition(found || null); - setIsLoading(false); - }, 500); - } catch (error) { - console.error("Error fetching competition:", error); - setIsLoading(false); - } - }; - - fetchCompetition(); - }, [id]); - - const handleBack = () => { - navigate(-1); - }; - - const handleContinue = () => { - if (competition?.id) { - if (mockTasks && mockTasks.length > 0) { - const firstTaskId = mockTasks[0].id; - navigate(`/competition/${competition.id}/tasks/${firstTaskId}`); - } else { - navigate(`/competition/${competition.id}/tasks`); - } - } - }; - - return ( - <> - -
- - - {isLoading ? ( -
-

Загрузка...

-
- ) : competition ? ( -
-
- {competition.name} -
- -
-
-

- {competition.name} -

- -
- -
-

{competition.description}

-
-
-
- ) : ( -
-

- Соревнование не найдено -

-

- Запрошенное соревнование не существует или было удалено. -

-
- )} -
- - ); -}; - -export default CompetitionPreview; diff --git a/services/frontend/src/pages/CompetitionRunnerPage/utils/utils.ts b/services/frontend/src/pages/CompetitionSession/utils/utils.ts similarity index 100% rename from services/frontend/src/pages/CompetitionRunnerPage/utils/utils.ts rename to services/frontend/src/pages/CompetitionSession/utils/utils.ts diff --git a/services/frontend/src/pages/Competitions/types.ts b/services/frontend/src/pages/Competitions/types.ts deleted file mode 100644 index e69de29..0000000 From 7e1891a81b41549cebc03b9adc4f6d1a3ac18071 Mon Sep 17 00:00:00 2001 From: moolcoov Date: Sat, 1 Mar 2025 14:02:28 +0300 Subject: [PATCH 4/8] fix: build --- .../src/pages/CompetitionSession/index.tsx | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/services/frontend/src/pages/CompetitionSession/index.tsx b/services/frontend/src/pages/CompetitionSession/index.tsx index f554a58..6a50cd2 100644 --- a/services/frontend/src/pages/CompetitionSession/index.tsx +++ b/services/frontend/src/pages/CompetitionSession/index.tsx @@ -45,26 +45,18 @@ const CompetitionRunnerPage = () => { }; return ( -<<<<<<< HEAD:services/frontend/src/pages/CompetitionRunnerPage/index.tsx - <> -
-
-
-

{competitionTitle}

-======= <> -
-
-
-

+
+
+
+

{competitionTitle}

->>>>>>> 58f493250150ba62ac4f325a0708b96eb88661e9:services/frontend/src/pages/CompetitionSession/index.tsx
{tasks.map((task) => ( -
handleTaskClick(task.id)} From eded5b729b843f1752dfcc2969f7b61f47256b14 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 14:08:36 +0300 Subject: [PATCH 5/8] feat: added dumb review, formatted --- services/backend/api/v1/review/auth.py | 9 +- services/backend/api/v1/review/schemas.py | 16 +-- services/backend/api/v1/review/views.py | 18 ++- services/backend/api/v1/router.py | 3 +- services/backend/api/v1/task/schemas.py | 2 +- services/backend/api/v1/task/views.py | 23 +-- services/backend/api/v1/user/views.py | 1 + .../core/management/commands/generate_data.py | 136 ++++++++++++++++++ services/backend/apps/review/models.py | 2 +- services/backend/apps/task/models.py | 6 +- services/backend/apps/task/tasks.py | 21 +-- services/backend/apps/task/validators.py | 4 +- services/backend/apps/user/test.py | 5 +- services/backend/config/settings.py | 2 +- 14 files changed, 191 insertions(+), 57 deletions(-) create mode 100644 services/backend/apps/core/management/commands/generate_data.py 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 From da843adfe301ce3fb86f60cc3bb8363e819433c2 Mon Sep 17 00:00:00 2001 From: rngsurrounded Date: Sat, 1 Mar 2025 20:14:52 +0900 Subject: [PATCH 6/8] fix: merge conflicts --- services/frontend/src/App.tsx | 14 +- .../index.tsx | 19 +- .../pages/CompetitionPreviewPage/index.tsx | 106 ----------- .../src/pages/CompetitionSession/index.tsx | 164 ------------------ .../index.tsx | 5 +- .../utils/utils.ts | 0 .../components/CompetitionCard/index.tsx | 7 - .../modules/CompetitionGrid/index.tsx | 2 +- 8 files changed, 26 insertions(+), 291 deletions(-) rename services/frontend/src/pages/{Competition => CompetitionPreview}/index.tsx (72%) delete mode 100644 services/frontend/src/pages/CompetitionPreviewPage/index.tsx delete mode 100644 services/frontend/src/pages/CompetitionSession/index.tsx rename services/frontend/src/pages/{CompetitionRunnerPage => CompetitionSesssion}/index.tsx (98%) rename services/frontend/src/pages/{CompetitionRunnerPage => CompetitionSesssion}/utils/utils.ts (100%) diff --git a/services/frontend/src/App.tsx b/services/frontend/src/App.tsx index 9c7f555..11abf83 100644 --- a/services/frontend/src/App.tsx +++ b/services/frontend/src/App.tsx @@ -1,19 +1,19 @@ import { Routes, Route } from "react-router"; import "./styles/globals.css"; -import CompetitionsPage from "./pages/Competitions"; -import CompetitionPage from "./pages/Competition"; -import CompetitionRunnerPage from "./pages/CompetitionSession"; +import Competitions from "./pages/Competitions"; +import CompetitionPreview from './pages/CompetitionPreview' +import CompetitionSession from "./pages/CompetitionSesssion"; import { NavbarLayout } from "./widgets/navbar-layout"; const App = () => { return ( }> - } /> - } /> + } /> + } /> } + path="/competition/:id/tasks/:taskId" + element={} /> diff --git a/services/frontend/src/pages/Competition/index.tsx b/services/frontend/src/pages/CompetitionPreview/index.tsx similarity index 72% rename from services/frontend/src/pages/Competition/index.tsx rename to services/frontend/src/pages/CompetitionPreview/index.tsx index bdddd5a..1f07d24 100644 --- a/services/frontend/src/pages/Competition/index.tsx +++ b/services/frontend/src/pages/CompetitionPreview/index.tsx @@ -1,16 +1,29 @@ + import { useState } from "react"; -import { useParams, Link } from "react-router-dom"; +import { useParams, Link, useNavigate } from "react-router-dom"; import { Button } from "@/components/ui/button"; import { ArrowLeft } from "lucide-react"; import { Competition } from "@/shared/types"; -import { mockCompetitions } from "@/shared/mocks/mocks"; +import { mockCompetitions, mockTasks } from "@/shared/mocks/mocks"; const CompetitionPage = () => { const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); const [competition] = useState( mockCompetitions.find((comp) => comp.id === id)!, ); + const handleContinue = () => { + if (competition?.id) { + if (mockTasks && mockTasks.length > 0) { + const firstTaskId = mockTasks[0].id; + navigate(`/competition/${competition.id}/tasks/${firstTaskId}`); + } else { + navigate(`/competition/${competition.id}/tasks`); + } + } + }; + return (
{
- +
diff --git a/services/frontend/src/pages/CompetitionPreviewPage/index.tsx b/services/frontend/src/pages/CompetitionPreviewPage/index.tsx deleted file mode 100644 index 15bcdc8..0000000 --- a/services/frontend/src/pages/CompetitionPreviewPage/index.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import { useEffect, useState } from "react"; -import { useParams, useNavigate } from "react-router-dom"; -import Navbar from "@/widgets/Navbar"; -import { Button } from "@/components/ui/button"; -import { ArrowLeft } from "lucide-react"; -import { Competition } from "@/shared/types"; -import { mockCompetitions, mockTasks } from "@/shared/mocks/mocks"; - -const CompetitionPreview = () => { - const { id } = useParams<{ id: string }>(); - const navigate = useNavigate(); - const [competition, setCompetition] = useState(null); - const [isLoading, setIsLoading] = useState(true); - - useEffect(() => { - const fetchCompetition = async () => { - try { - setTimeout(() => { - const found = mockCompetitions.find((comp) => comp.id === id); - setCompetition(found || null); - setIsLoading(false); - }, 500); - } catch (error) { - console.error("Error fetching competition:", error); - setIsLoading(false); - } - }; - - fetchCompetition(); - }, [id]); - - const handleBack = () => { - navigate(-1); - }; - - const handleContinue = () => { - if (competition?.id) { - if (mockTasks && mockTasks.length > 0) { - const firstTaskId = mockTasks[0].id; - navigate(`/competition/${competition.id}/tasks/${firstTaskId}`); - } else { - navigate(`/competition/${competition.id}/tasks`); - } - } - }; - - return ( - <> - -
- - - {isLoading ? ( -
-

Загрузка...

-
- ) : competition ? ( -
-
- {competition.name} -
- -
-
-

- {competition.name} -

- -
- -
-

{competition.description}

-
-
-
- ) : ( -
-

- Соревнование не найдено -

-

- Запрошенное соревнование не существует или было удалено. -

-
- )} -
- - ); -}; - -export default CompetitionPreview; diff --git a/services/frontend/src/pages/CompetitionSession/index.tsx b/services/frontend/src/pages/CompetitionSession/index.tsx deleted file mode 100644 index f554a58..0000000 --- a/services/frontend/src/pages/CompetitionSession/index.tsx +++ /dev/null @@ -1,164 +0,0 @@ -import { useState, useEffect } from "react"; -import { useParams, useNavigate } from "react-router-dom"; -import { Task } from "@/shared/types"; -import { getTaskBgColor, getTaskTextColor } from "./utils/utils"; -import { mockTasks } from "@/shared/mocks/mocks"; -import { Button } from "@/components/ui/button"; -import { Calendar } from "lucide-react"; - -const CompetitionRunnerPage = () => { - const { id, taskId } = useParams<{ id: string; taskId?: string }>(); - const navigate = useNavigate(); - const [competitionTitle, setCompetitionTitle] = useState( - "Олимпиада DANO 2025. Индивидуальный этап", - ); - const [tasks] = useState(mockTasks); - const [selectedTaskId, setSelectedTaskId] = useState( - taskId || null, - ); - const [answer, setAnswer] = useState(""); - - useEffect(() => { - if (taskId) { - setSelectedTaskId(taskId); - } else if (tasks.length > 0) { - navigate(`/competition/${id}/tasks/${tasks[0].id}`, { replace: true }); - } - }, [taskId, tasks, id, navigate]); - - const handleTaskClick = (taskId: string) => { - if (selectedTaskId !== taskId) { - setSelectedTaskId(taskId); - navigate(`/competition/${id}/tasks/${taskId}`); - } - }; - - const currentTask = tasks.find((t) => t.id === selectedTaskId); - - const handleSubmit = () => { - console.log("Submitting answer:", answer); - // Submit logic here - }; - - const handleHistoryClick = () => { - console.log("View history"); - }; - - return ( -<<<<<<< HEAD:services/frontend/src/pages/CompetitionRunnerPage/index.tsx - <> -
-
-
-

{competitionTitle}

-======= - <> -
-
-
-

- {competitionTitle} -

->>>>>>> 58f493250150ba62ac4f325a0708b96eb88661e9:services/frontend/src/pages/CompetitionSession/index.tsx -
- -
- {tasks.map((task) => ( -
handleTaskClick(task.id)} - > - {task.number} -
- ))} -
-
-
- -
-
- {currentTask ? ( -
- {/* Left Container - Task Description */} -
-

- Задача {currentTask.number} -

- -
-

- Рассмотрим последовательность чисел 2, 3, 5, 9, 17, 33, 65, - 129, ... Каждый член этой последовательности, начиная с - третьего, равен сумме двух предыдущих членов. -

-

- Найдите сумму первых 15 членов этой последовательности. -

-

В ответе укажите целое число.

-
-
- - {/* Right Container - Solution Area */} -
- {/* Solution Status Card */} -
-
- - Решение 12345 - - - Зачтено 5/10 баллов - -
-
- 1 марта, 08:41 -
-
- - {/* Answer Input */} -
-