From 86f68619cb429db8b4ffa20d1ce74ace14c57c34 Mon Sep 17 00:00:00 2001 From: rngsurrounded Date: Mon, 3 Mar 2025 03:15:19 +0900 Subject: [PATCH 1/9] test: history --- .../components/FileSolution/index.tsx | 42 +++++++++++++------ .../components/SolutionHistorySheet/index.tsx | 19 ++++++--- .../modules/TaskSolution/index.tsx | 22 ++++++++++ services/frontend/src/shared/types/task.ts | 3 +- 4 files changed, 67 insertions(+), 19 deletions(-) diff --git a/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/FileSolution/index.tsx b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/FileSolution/index.tsx index 5e103b6..ed1a191 100644 --- a/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/FileSolution/index.tsx +++ b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/FileSolution/index.tsx @@ -1,17 +1,19 @@ import React from 'react'; -import { FileIcon } from 'lucide-react'; +import { FileIcon, Download } from 'lucide-react'; import { Button } from "@/components/ui/button"; interface FileSolutionProps { selectedFile: File | null; setSelectedFile: (file: File | null) => void; fileInputRef: React.RefObject; + fileUrl?: string | null; } const FileSolution: React.FC = ({ selectedFile, setSelectedFile, - fileInputRef + fileInputRef, + fileUrl = null }) => { const handleFileChange = (event: React.ChangeEvent) => { if (event.target.files && event.target.files[0]) { @@ -42,6 +44,8 @@ const FileSolution: React.FC = ({ } }; + const fileName = selectedFile ? selectedFile.name : fileUrl ? fileUrl.split('/').pop() || 'file' : ''; + return ( <> = ({ accept=".jpg,.jpeg,.png,.pptx,.docx,.pdf,.xlsx,.txt" /> - {selectedFile ? ( + {(selectedFile || fileUrl) ? (
- {selectedFile.name} - {(selectedFile.size / 1024).toFixed(1)} KB - + {fileName} + +
+ {fileUrl && ( + + + Скачать + + )} + +
) : ( @@ -82,7 +98,7 @@ const FileSolution: React.FC = ({ Загрузить файл

- Доступные форматы: jpg, jpeg, png + Доступные форматы: jpg, jpeg, png, pptx, docx, pdf, xlsx, txt

)} diff --git a/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/SolutionHistorySheet/index.tsx b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/SolutionHistorySheet/index.tsx index dc8fe95..870d511 100644 --- a/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/SolutionHistorySheet/index.tsx +++ b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/SolutionHistorySheet/index.tsx @@ -3,20 +3,22 @@ import { Sheet, SheetClose, SheetContent, SheetHeader, SheetTitle } from "@/comp import { Button } from "@/components/ui/button"; import { X } from "lucide-react"; import SolutionStatus from '../SolutionStatus'; -import { Solution } from '@/shared/types/task'; +import { Solution, TaskType } from '@/shared/types/task'; interface SolutionHistorySheetProps { isOpen: boolean; onOpenChange: (open: boolean) => void; solutions: Solution[]; - maxPoints: number + maxPoints: number; + onSolutionSelect: (solution: Solution) => void; } const SolutionHistorySheet: React.FC = ({ isOpen, onOpenChange, solutions, - maxPoints + maxPoints, + onSolutionSelect }) => { return ( @@ -32,10 +34,17 @@ const SolutionHistorySheet: React.FC = ({ -
+
{solutions.length > 0 ? ( solutions.map((solution, index) => ( -
+
{ + onSolutionSelect(solution); + onOpenChange(false); + }} + >
)) diff --git a/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/index.tsx b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/index.tsx index ea0c35c..d78f313 100644 --- a/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/index.tsx +++ b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/index.tsx @@ -29,6 +29,7 @@ const TaskSolution: React.FC = ({ }) => { const fileInputRef = useRef(null); const [isHistoryOpen, setIsHistoryOpen] = useState(false); + const [selectedSolutionUrl, setSelectedSolutionUrl] = useState(null); const { id: competitionId } = useParams<{ id: string }>(); const solutionsQuery = useQuery({ @@ -45,6 +46,25 @@ const TaskSolution: React.FC = ({ const latestSolution = solutionHistory && solutionHistory.length > 0 ? solutionHistory[solutionHistory.length - 1] : null; + const handleSolutionSelect = async (solution: Solution) => { + if (!solution.content) return; + + setSelectedSolutionUrl(solution.content); + + try { + if (task.type !== TaskType.FILE) { + const response = await fetch(solution.content); + if (!response.ok) { + throw new Error(`Failed to fetch solution content: ${response.status}`); + } + const text = await response.text(); + setAnswer(text); + } + } catch (error) { + console.error('Error loading solution content:', error); + } + }; + return (
{latestSolution ? ( @@ -64,6 +84,7 @@ const TaskSolution: React.FC = ({ selectedFile={selectedFile} setSelectedFile={setSelectedFile} fileInputRef={fileInputRef} + existingFileUrl={selectedSolutionUrl} /> )} @@ -81,6 +102,7 @@ const TaskSolution: React.FC = ({ onOpenChange={setIsHistoryOpen} solutions={solutionHistory} maxPoints={task.points} + onSolutionSelect={handleSolutionSelect} />
); diff --git a/services/frontend/src/shared/types/task.ts b/services/frontend/src/shared/types/task.ts index 07c6fe1..40d5b4a 100644 --- a/services/frontend/src/shared/types/task.ts +++ b/services/frontend/src/shared/types/task.ts @@ -29,7 +29,8 @@ interface Solution { id: string, status: SolutionStatus, timestamp: string, - earned_points: number + earned_points: number, + content: string } export type {Task, Solution} From b9a8e9bc6705b2893e450cfab404469c910e019d Mon Sep 17 00:00:00 2001 From: rngsurrounded Date: Mon, 3 Mar 2025 03:48:21 +0900 Subject: [PATCH 2/9] minor fixes --- services/frontend/src/shared/types/task.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/frontend/src/shared/types/task.ts b/services/frontend/src/shared/types/task.ts index 40d5b4a..64e829e 100644 --- a/services/frontend/src/shared/types/task.ts +++ b/services/frontend/src/shared/types/task.ts @@ -15,8 +15,8 @@ export interface TaskAttachment { enum TaskType { INPUT = "input", - FILE = "checker", - CODE = "review", + FILE = "review", + CODE = "checker", } enum SolutionStatus { From 0f12404222b2ded4e55d9e1de61997ce19d1b020 Mon Sep 17 00:00:00 2001 From: rngsurrounded Date: Mon, 3 Mar 2025 03:53:56 +0900 Subject: [PATCH 3/9] minor fixes --- .../modules/TaskSolution/index.tsx | 190 +++++++++--------- 1 file changed, 97 insertions(+), 93 deletions(-) diff --git a/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/index.tsx b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/index.tsx index d78f313..cd2b1d4 100644 --- a/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/index.tsx +++ b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/index.tsx @@ -1,111 +1,115 @@ -import React, { useState, useRef } from 'react'; -import { useParams } from 'react-router-dom'; -import { Task, TaskType, Solution } from '@/shared/types/task'; -import { useQuery } from '@tanstack/react-query'; -import { getTaskSolutionHistory } from '@/shared/api/session'; -import SolutionStatus from './components/SolutionStatus'; -import InputSolution from './components/InputSolution'; -import FileSolution from './components/FileSolution'; -import CodeSolution from './components/CodeSolution'; -import ActionButtons from './components/ActionButtons'; -import SolutionHistorySheet from './components/SolutionHistorySheet'; +import React from 'react'; +import { FileIcon, Download } from 'lucide-react'; +import { Button } from "@/components/ui/button"; -interface TaskSolutionProps { - task: Task; - answer: string; - setAnswer: (value: string) => void; +interface FileSolutionProps { selectedFile: File | null; setSelectedFile: (file: File | null) => void; - onSubmit: () => void; + fileInputRef: React.RefObject; + existingFileUrl?: string | null; } -const TaskSolution: React.FC = ({ - task, - answer, - setAnswer, - selectedFile, +const FileSolution: React.FC = ({ + selectedFile, setSelectedFile, - onSubmit, + fileInputRef, + existingFileUrl = null }) => { - const fileInputRef = useRef(null); - const [isHistoryOpen, setIsHistoryOpen] = useState(false); - const [selectedSolutionUrl, setSelectedSolutionUrl] = useState(null); - const { id: competitionId } = useParams<{ id: string }>(); - - const solutionsQuery = useQuery({ - queryKey: ['solutionHistory', competitionId, task.id], - queryFn: () => getTaskSolutionHistory(competitionId || '', task.id), - enabled: !!(competitionId && task.id), - }); - - const solutionHistory = solutionsQuery.data || []; - - const handleOpenHistory = () => { - setIsHistoryOpen(true); + const handleFileChange = (event: React.ChangeEvent) => { + if (event.target.files && event.target.files[0]) { + setSelectedFile(event.target.files[0]); + } }; - const latestSolution = solutionHistory && solutionHistory.length > 0 ? solutionHistory[solutionHistory.length - 1] : null; - - const handleSolutionSelect = async (solution: Solution) => { - if (!solution.content) return; - - setSelectedSolutionUrl(solution.content); - - try { - if (task.type !== TaskType.FILE) { - const response = await fetch(solution.content); - if (!response.ok) { - throw new Error(`Failed to fetch solution content: ${response.status}`); - } - const text = await response.text(); - setAnswer(text); - } - } catch (error) { - console.error('Error loading solution content:', error); - } + const handleFileUploadClick = () => { + fileInputRef.current?.click(); }; + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault(); + e.currentTarget.classList.add('bg-gray-50'); + }; + + const handleDragLeave = (e: React.DragEvent) => { + e.preventDefault(); + e.currentTarget.classList.remove('bg-gray-50'); + }; + + const handleDrop = (e: React.DragEvent) => { + e.preventDefault(); + e.currentTarget.classList.remove('bg-gray-50'); + + if (e.dataTransfer.files && e.dataTransfer.files[0]) { + setSelectedFile(e.dataTransfer.files[0]); + } + }; + + const fileName = selectedFile + ? selectedFile.name + : existingFileUrl + ? existingFileUrl.split('/').pop() || 'file' + : ''; + + const hasFile = !!selectedFile || !!existingFileUrl; + return ( -
- {latestSolution ? ( - + <> + + + {hasFile ? ( +
+
+ + {fileName} + +
+ {existingFileUrl && !selectedFile && ( + + + Скачать + + )} + +
+
+
) : ( -
- Решение еще не отправлено +
+ + + Загрузить файл + +

+ Доступные форматы: jpg, jpeg, png, pptx, docx, pdf, xlsx, txt +

)} - - {task.type === TaskType.INPUT && ( - - )} - - {task.type === TaskType.FILE && ( - - )} - - {task.type === TaskType.CODE && ( - - )} - - - - -
+ ); }; -export default TaskSolution; \ No newline at end of file +export default FileSolution; \ No newline at end of file From 1136f7abb9b0e1807ee36e52aaf4b62470ce672b Mon Sep 17 00:00:00 2001 From: Timur Date: Sun, 2 Mar 2025 21:58:43 +0300 Subject: [PATCH 4/9] add user stat endpoint --- services/backend/api/v1/user/schemas.py | 5 ++++ services/backend/api/v1/user/views.py | 32 ++++++++++++++++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/services/backend/api/v1/user/schemas.py b/services/backend/api/v1/user/schemas.py index 832d91f..b97f6ac 100644 --- a/services/backend/api/v1/user/schemas.py +++ b/services/backend/api/v1/user/schemas.py @@ -23,3 +23,8 @@ class UserSchema(ModelSchema): class Meta: model = User fields = ["id", "email", "username", "created_at", "achievements"] + + +class StatSchema(Schema): + total_attempts: int + solved_tasks: int diff --git a/services/backend/api/v1/user/views.py b/services/backend/api/v1/user/views.py index 2b9cdbe..d7a3dfb 100644 --- a/services/backend/api/v1/user/views.py +++ b/services/backend/api/v1/user/views.py @@ -11,15 +11,17 @@ from api.v1.schemas import ( BadRequestError, ConflictError, ForbiddenError, - NotFoundError, + NotFoundError, UnauthorizedError, ) from api.v1.user.schemas import ( LoginSchema, RegisterSchema, TokenSchema, UserSchema, + StatSchema ) from apps.user.models import User +from apps.task.models import CompetitionTaskSubmission, ReviewStatusChoices router = Router(tags=["user"]) @@ -85,3 +87,31 @@ def get_me(request): def get_user(request, user_id: str): user = get_object_or_404(User, id=user_id) return status.OK, user + + +@router.get( + "/me/stat", + response={ + status.OK: StatSchema, + status.UNAUTHORIZED: UnauthorizedError + }, +) +def get_my_stat(request): + user_submissions = CompetitionTaskSubmission.objects.filter( + user=request.auth + ) + checked_attempts = user_submissions.filter(status=CompetitionTaskSubmission.StatusChoices.CHECKED).all() + success_attempts_cnt = 0 + + for attempt in checked_attempts: + is_correct = attempt.result.get("correct", None) + if is_correct is None: + is_correct = attempt.result.get("total_points", 0) > 0 + + if is_correct: + success_attempts_cnt += 1 + + return StatSchema( + total_attempts=len(user_submissions), + solved_tasks=success_attempts_cnt + ) From fd01fbd9038c8b968995b8667dde3621f19c1377 Mon Sep 17 00:00:00 2001 From: Timur Date: Sun, 2 Mar 2025 23:09:34 +0300 Subject: [PATCH 5/9] fix competitions tests --- services/backend/apps/competition/tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/backend/apps/competition/tests.py b/services/backend/apps/competition/tests.py index 03f94c6..b4a1609 100644 --- a/services/backend/apps/competition/tests.py +++ b/services/backend/apps/competition/tests.py @@ -35,7 +35,7 @@ class CompetitionEndpointTests(TestCase): self.valid_headers = {"HTTP_AUTHORIZATION": f"Bearer {token}"} def get_url(self, competition_id): - return f"/api/v1/competition/{competition_id}" + return f"/api/v1/competitions/{competition_id}" def test_get_competition_success(self): response = self.client.get( From ebb79117d4cd311cc023a5618eca0942f9174a34 Mon Sep 17 00:00:00 2001 From: Timur Date: Sun, 2 Mar 2025 23:16:38 +0300 Subject: [PATCH 6/9] add verbose names to submissions check status --- services/backend/apps/task/models.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/services/backend/apps/task/models.py b/services/backend/apps/task/models.py index dc917ef..8bf11e3 100644 --- a/services/backend/apps/task/models.py +++ b/services/backend/apps/task/models.py @@ -94,9 +94,9 @@ class CompetitionTaskAttachment(BaseModel): class CompetitionTaskSubmission(BaseModel): class StatusChoices(models.TextChoices): - SENT = "sent" - CHECKING = "checking" - CHECKED = "checked" + SENT = "sent", "Отправлено на проверку" + CHECKING = "checking", "Проверка" + CHECKED = "checked", "Проверено" def submission_content_upload_to(instance, filename) -> str: return f"submissions/{instance.id}/content/{filename}" From 8ecc380c014d171d72097b171b12026ea5a553f4 Mon Sep 17 00:00:00 2001 From: Timur Date: Sun, 2 Mar 2025 23:20:12 +0300 Subject: [PATCH 7/9] add status filtering for submissions admin --- services/backend/apps/task/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/backend/apps/task/admin.py b/services/backend/apps/task/admin.py index 1cf4361..4a8ff15 100644 --- a/services/backend/apps/task/admin.py +++ b/services/backend/apps/task/admin.py @@ -31,7 +31,7 @@ class CompetitionTaskSubmissionAdmin(admin.ModelAdmin): "user__username", "user__email", ) - filter = ("plagiarism_checked",) + list_filter = ("plagiarism_checked", "status",) ordering = ["-timestamp"] def has_add_permission(self, request, obj=None): From 121c29c2f3582af0ad21e3f97499b59921fabab3 Mon Sep 17 00:00:00 2001 From: ITQ Date: Sun, 2 Mar 2025 23:28:14 +0300 Subject: [PATCH 8/9] its okay --- compose.yaml | 7 +++++- infrastructure/checker/.env.template | 2 ++ services/backend/api/v1/task/views.py | 6 ++--- services/backend/apps/task/tasks.py | 8 +++--- services/checker/config.py | 18 ++++++++++++++ services/checker/main.py | 36 ++++++++++++++++----------- services/checker/pyproject.toml | 1 + 7 files changed, 55 insertions(+), 23 deletions(-) create mode 100644 infrastructure/checker/.env.template create mode 100644 services/checker/config.py diff --git a/compose.yaml b/compose.yaml index e4401a3..eabd04b 100644 --- a/compose.yaml +++ b/compose.yaml @@ -375,13 +375,18 @@ services: build: context: ./services/checker dockerfile: Dockerfile - restart: unless-stopped + env_file: + - path: ./infrastructure/checker/.env.template + required: true + - path: ./infrastructure/checker/.env + required: false ports: - name: web target: 8000 published: 8009 host_ip: 0.0.0.0 protocol: tcp + restart: unless-stopped volumes: - type: bind source: /var/run/docker.sock diff --git a/infrastructure/checker/.env.template b/infrastructure/checker/.env.template new file mode 100644 index 0000000..7c6f9d3 --- /dev/null +++ b/infrastructure/checker/.env.template @@ -0,0 +1,2 @@ +REGISTRY_LOGIN=devitq +REGISTRY_PASSWORD=14zQrbzDTM0WXK@CogMQikAvP74Rj4 diff --git a/services/backend/api/v1/task/views.py b/services/backend/api/v1/task/views.py index 91f7477..afa91cf 100644 --- a/services/backend/api/v1/task/views.py +++ b/services/backend/api/v1/task/views.py @@ -19,6 +19,7 @@ from apps.task.models import ( CompetitionTaskAttachment, CompetitionTaskSubmission, ) +from apps.task.tasks import analyze_data_task router = Router(tags=["competition"]) @@ -123,6 +124,7 @@ def submit_task( status=CompetitionTaskSubmission.StatusChoices.CHECKING, content=content, ) + analyze_data_task.delay(submission_id=submission.id) return TaskSubmissionOut(submission_id=submission.id) @@ -154,6 +156,4 @@ def get_submissions_history(request, competition_id: UUID, task_id: UUID): ) def get_task_attachments(request, competition_id: UUID, task_id: UUID): task = get_object_or_404(CompetitionTask, id=task_id) - return status.OK, CompetitionTaskAttachment.objects.filter( - task=task - ).all() + return status.OK, CompetitionTaskAttachment.objects.filter(task=task).all() diff --git a/services/backend/apps/task/tasks.py b/services/backend/apps/task/tasks.py index 0c0a6a9..2f3d50c 100644 --- a/services/backend/apps/task/tasks.py +++ b/services/backend/apps/task/tasks.py @@ -1,4 +1,4 @@ -import requests +import httpx from celery import shared_task from django.core.files.base import ContentFile @@ -17,7 +17,7 @@ def analyze_data_task(self, submission_id): for f in submission.task.attachments.filter(public=True) ] - response = requests.post( + response = httpx.post( f"{settings.CHECKER_API_ENDPOINT}/execute", files=[("files", (f.name, f)) for f in files] + [ @@ -40,10 +40,10 @@ def analyze_data_task(self, submission_id): ) submission.status = CompetitionTaskSubmission.StatusChoices.CHECKED - except requests.exceptions.RequestException as e: + except httpx.RequestError as e: self.retry(countdown=2**self.request.retries) except Exception as e: - submission.result = {"error": str(e)} + submission.result = {"error": str(e), "success": False} submission.status = CompetitionTaskSubmission.StatusChoices.CHECKED submission.earned_points = 0 finally: diff --git a/services/checker/config.py b/services/checker/config.py new file mode 100644 index 0000000..b297cc8 --- /dev/null +++ b/services/checker/config.py @@ -0,0 +1,18 @@ +import os +from pathlib import Path + +from dotenv import load_dotenv + +BASE_DIR = Path(__file__).resolve().parent + +load_dotenv(BASE_DIR / ".env") + +REGISTRY_LOGIN = os.getenv("REGISTRY_USERNAME", None) + +REGISTRY_PASSWORD = os.getenv("REGISTRY_USERNAME", None) + +REGISTRY_URL = os.getenv("REGISTRY_URL", "gitlab.prodcontest.ru:5050") + +DOCKER_IMAGE = os.getenv( + "IMAGE", default="gitlab.prodcontest.ru:5050/team-15/project/custom-python" +) diff --git a/services/checker/main.py b/services/checker/main.py index 4b01fac..f30a3de 100644 --- a/services/checker/main.py +++ b/services/checker/main.py @@ -10,19 +10,25 @@ import tempfile import logging from urllib.parse import urlparse import re - -app = FastAPI() -docker_client = docker.from_env() -logger = logging.getLogger(__name__) -logging.basicConfig(level=logging.INFO) +import config -DOCKER_IMAGE = "gitlab.python:3-slim" CONTAINER_TIMEOUT = 60 MAX_FILE_SIZE = 4 * 1024 * 1024 ALLOWED_FILENAME_CHARS = r"[^a-zA-Z0-9_\-.]" +app = FastAPI() +docker_client = docker.from_env() +logger = logging.getLoggerQ(__name__) +logging.basicConfig(level=logging.INFO) +docker_client.login( + username=config.REGISTRY_LOGIN, + password=config.REGISTRY_PASSWORD, + registry=config.REGISTRY_URL, +) + + class FileDetails(BaseModel): url: HttpUrl = Field( ..., description="URL to download the file from (supports HTTP/HTTPS)" @@ -130,7 +136,7 @@ def run_container_safely( volumes[host_path] = {"bind": container_path, "mode": "ro"} container = docker_client.containers.run( - image=DOCKER_IMAGE, + image=config.DOCKER_IMAGE, command=command, volumes=volumes, working_dir="/execution", @@ -166,6 +172,14 @@ def run_container_safely( pass +def validate_file_path(path: str) -> bool: + return ( + not os.path.isabs(path) + and os.path.basename(path) == path + and all(c.isalnum() or c in {"_", "-", "."} for c in path) + ) + + @app.post("/execute", response_model=ExecutionResponse) async def execute_code(request: ExecutionRequest) -> ExecutionResponse: try: @@ -279,11 +293,3 @@ async def health_check() -> HealthCheckResponse: return HealthCheckResponse(status="healthy", docker="connected") except docker.errors.DockerException: return HealthCheckResponse(status="degraded", docker="unavailable") - - -def validate_file_path(path: str) -> bool: - return ( - not os.path.isabs(path) - and os.path.basename(path) == path - and all(c.isalnum() or c in {"_", "-", "."} for c in path) - ) diff --git a/services/checker/pyproject.toml b/services/checker/pyproject.toml index 2e47424..aaa49d4 100644 --- a/services/checker/pyproject.toml +++ b/services/checker/pyproject.toml @@ -7,6 +7,7 @@ dependencies = [ "aiohttp>=3.11.13", "docker>=7.1.0", "fastapi>=0.115.11", + "python-dotenv>=1.0.1", "python-multipart>=0.0.20", "regex>=2024.11.6", "uvicorn>=0.34.0", From 93e26431ca92e4097571918706292ace7c3a34cb Mon Sep 17 00:00:00 2001 From: rngsurrounded Date: Mon, 3 Mar 2025 05:28:27 +0900 Subject: [PATCH 9/9] input fix --- .../components/FileSolution/index.tsx | 20 +- .../modules/TaskSolution/index.tsx | 186 +++++++++--------- 2 files changed, 104 insertions(+), 102 deletions(-) diff --git a/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/FileSolution/index.tsx b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/FileSolution/index.tsx index ed1a191..cd2b1d4 100644 --- a/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/FileSolution/index.tsx +++ b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/FileSolution/index.tsx @@ -6,14 +6,14 @@ interface FileSolutionProps { selectedFile: File | null; setSelectedFile: (file: File | null) => void; fileInputRef: React.RefObject; - fileUrl?: string | null; + existingFileUrl?: string | null; } const FileSolution: React.FC = ({ selectedFile, setSelectedFile, fileInputRef, - fileUrl = null + existingFileUrl = null }) => { const handleFileChange = (event: React.ChangeEvent) => { if (event.target.files && event.target.files[0]) { @@ -44,7 +44,13 @@ const FileSolution: React.FC = ({ } }; - const fileName = selectedFile ? selectedFile.name : fileUrl ? fileUrl.split('/').pop() || 'file' : ''; + const fileName = selectedFile + ? selectedFile.name + : existingFileUrl + ? existingFileUrl.split('/').pop() || 'file' + : ''; + + const hasFile = !!selectedFile || !!existingFileUrl; return ( <> @@ -56,16 +62,16 @@ const FileSolution: React.FC = ({ accept=".jpg,.jpeg,.png,.pptx,.docx,.pdf,.xlsx,.txt" /> - {(selectedFile || fileUrl) ? ( + {hasFile ? (
diff --git a/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/index.tsx b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/index.tsx index cd2b1d4..d78f313 100644 --- a/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/index.tsx +++ b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/index.tsx @@ -1,115 +1,111 @@ -import React from 'react'; -import { FileIcon, Download } from 'lucide-react'; -import { Button } from "@/components/ui/button"; +import React, { useState, useRef } from 'react'; +import { useParams } from 'react-router-dom'; +import { Task, TaskType, Solution } from '@/shared/types/task'; +import { useQuery } from '@tanstack/react-query'; +import { getTaskSolutionHistory } from '@/shared/api/session'; +import SolutionStatus from './components/SolutionStatus'; +import InputSolution from './components/InputSolution'; +import FileSolution from './components/FileSolution'; +import CodeSolution from './components/CodeSolution'; +import ActionButtons from './components/ActionButtons'; +import SolutionHistorySheet from './components/SolutionHistorySheet'; -interface FileSolutionProps { +interface TaskSolutionProps { + task: Task; + answer: string; + setAnswer: (value: string) => void; selectedFile: File | null; setSelectedFile: (file: File | null) => void; - fileInputRef: React.RefObject; - existingFileUrl?: string | null; + onSubmit: () => void; } -const FileSolution: React.FC = ({ - selectedFile, +const TaskSolution: React.FC = ({ + task, + answer, + setAnswer, + selectedFile, setSelectedFile, - fileInputRef, - existingFileUrl = null + onSubmit, }) => { - const handleFileChange = (event: React.ChangeEvent) => { - if (event.target.files && event.target.files[0]) { - setSelectedFile(event.target.files[0]); - } + const fileInputRef = useRef(null); + const [isHistoryOpen, setIsHistoryOpen] = useState(false); + const [selectedSolutionUrl, setSelectedSolutionUrl] = useState(null); + const { id: competitionId } = useParams<{ id: string }>(); + + const solutionsQuery = useQuery({ + queryKey: ['solutionHistory', competitionId, task.id], + queryFn: () => getTaskSolutionHistory(competitionId || '', task.id), + enabled: !!(competitionId && task.id), + }); + + const solutionHistory = solutionsQuery.data || []; + + const handleOpenHistory = () => { + setIsHistoryOpen(true); }; - const handleFileUploadClick = () => { - fileInputRef.current?.click(); - }; + const latestSolution = solutionHistory && solutionHistory.length > 0 ? solutionHistory[solutionHistory.length - 1] : null; - const handleDragOver = (e: React.DragEvent) => { - e.preventDefault(); - e.currentTarget.classList.add('bg-gray-50'); - }; - - const handleDragLeave = (e: React.DragEvent) => { - e.preventDefault(); - e.currentTarget.classList.remove('bg-gray-50'); - }; - - const handleDrop = (e: React.DragEvent) => { - e.preventDefault(); - e.currentTarget.classList.remove('bg-gray-50'); + const handleSolutionSelect = async (solution: Solution) => { + if (!solution.content) return; - if (e.dataTransfer.files && e.dataTransfer.files[0]) { - setSelectedFile(e.dataTransfer.files[0]); - } + setSelectedSolutionUrl(solution.content); + + try { + if (task.type !== TaskType.FILE) { + const response = await fetch(solution.content); + if (!response.ok) { + throw new Error(`Failed to fetch solution content: ${response.status}`); + } + const text = await response.text(); + setAnswer(text); + } + } catch (error) { + console.error('Error loading solution content:', error); + } }; - const fileName = selectedFile - ? selectedFile.name - : existingFileUrl - ? existingFileUrl.split('/').pop() || 'file' - : ''; - - const hasFile = !!selectedFile || !!existingFileUrl; - return ( - <> - - - {hasFile ? ( -
-
- - {fileName} - -
- {existingFileUrl && !selectedFile && ( - - - Скачать - - )} - -
-
-
+
+ {latestSolution ? ( + ) : ( -
- - - Загрузить файл - -

- Доступные форматы: jpg, jpeg, png, pptx, docx, pdf, xlsx, txt -

+
+ Решение еще не отправлено
)} - + + {task.type === TaskType.INPUT && ( + + )} + + {task.type === TaskType.FILE && ( + + )} + + {task.type === TaskType.CODE && ( + + )} + + + + +
); }; -export default FileSolution; \ No newline at end of file +export default TaskSolution; \ No newline at end of file