diff --git a/img/postman_e2e.json b/img/postman_e2e.json new file mode 100644 index 0000000..9c14a52 --- /dev/null +++ b/img/postman_e2e.json @@ -0,0 +1,768 @@ +{ + "info": { + "_postman_id": "fd33ed4f-84b5-4045-b926-22d989876fb0", + "name": "Datarush tests", + "schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json", + "_exporter_id": "29887339" + }, + "item": [ + { + "name": "User", + "item": [ + { + "name": "Correct registration", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "// Генерация случайных данных\r", + "const randomNamePrefix = `company_${Math.random().toString(36).substring(2, 8)}`;\r", + "const randomUsername = `${Math.random().toString(36).substring(3, 9)}`\r", + "const randomEmail = `${Math.random().toString(36).substring(2, 8)}@timka.test`;\r", + "const randomPassword = `${Math.random().toString(36).substring(2, 12)}!A1`;\r", + "\r", + "// Формирование тела запроса\r", + "const requestData = {\r", + " username: randomUsername,\r", + " email: randomEmail,\r", + " password: randomPassword\r", + "};\r", + "\r", + "// Сохранение данных в переменные окружения\r", + "pm.environment.set(\"randomNamePrefix\", randomNamePrefix);\r", + "pm.environment.set(\"randomEmail\", randomEmail);\r", + "pm.environment.set(\"randomPassword\", randomPassword);\r", + "pm.environment.set(\"randomUsername\", randomUsername)\r", + "\r", + "// Сохранение JSON-объекта в переменную для дальнейшего использования\r", + "pm.environment.set(\"requestData\", JSON.stringify(requestData));\r", + "pm.environment.set(\"requestNameREGISTRATION\", JSON.stringify(randomNamePrefix));\r", + "pm.environment.set(\"requestEmailREGISTRATION\", JSON.stringify(randomEmail));\r", + "pm.environment.set(\"requestusernameREGISTRATION\", JSON.stringify(randomUsername))\r", + "pm.environment.set(\"requestPasswordREGISTRATION\", JSON.stringify(randomPassword));" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 201', function () {\r", + " pm.response.to.have.status(201);\r", + "});\r", + "\r", + "pm.test('Response has token', function () {\r", + " const jsonData = pm.response.json();\r", + " pm.expect(jsonData.token).to.be.a('string');\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{{requestData}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": "{{BASE_HOST}}/v1/sign-up" + }, + "response": [] + }, + { + "name": "Correct sign in", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const email = pm.environment.get(\"randomEmail\")\r", + "const password = pm.environment.get(\"randomPassword\")\r", + "\r", + "const requestData = {\r", + " email: email,\r", + " password: password\r", + "}\r", + "pm.environment.set(\"requestData\", JSON.stringify(requestData));\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test('Response has token', function () {\r", + " const jsonData = pm.response.json();\r", + " pm.expect(jsonData.token).to.be.a('string');\r", + " pm.environment.set(\"token\", jsonData.token)\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{{requestData}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": "{{BASE_HOST}}/v1/sign-in" + }, + "response": [] + }, + { + "name": "Duplicated reg data", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const email = pm.environment.get(\"randomEmail\")\r", + "const password = pm.environment.get(\"randomPassword\")\r", + "const username = pm.environment.get(\"randomUsername\")\r", + "\r", + "const requestData = {\r", + " email: email,\r", + " password: password,\r", + " username: username\r", + "}\r", + "pm.environment.set(\"requestData\", JSON.stringify(requestData));\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 409', function () {\r", + " pm.response.to.have.status(409);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{{requestData}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": "{{BASE_HOST}}/v1/sign-up" + }, + "response": [] + }, + { + "name": "Reg without username", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const email = pm.environment.get(\"randomEmail\")\r", + "const password = pm.environment.get(\"randomPassword\")\r", + "\r", + "const requestData = {\r", + " email: email,\r", + " password: password\r", + "}\r", + "pm.environment.set(\"requestData\", JSON.stringify(requestData));\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 400', function () {\r", + " pm.response.to.have.status(400);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{{requestData}}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": "{{BASE_HOST}}/v1/sign-up" + }, + "response": [] + } + ] + }, + { + "name": "Competitions", + "item": [ + { + "name": "Get competition with no partipication", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test('Response has only 2 elements', function () {\r", + " const jsonData = pm.response.json();\r", + " pm.expect(jsonData).to.has.length(2,\"Response has non-2 length, try to reset to only test data state\");\r", + " pm.environment.set(\"competition_id\", jsonData[0].id)\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": { + "token": "{{token}}" + } + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{BASE_HOST}}/v1/competitions?is_participating=false", + "host": [ + "{{BASE_HOST}}" + ], + "path": [ + "v1", + "competitions" + ], + "query": [ + { + "key": "is_participating", + "value": "false" + } + ] + } + }, + "response": [] + }, + { + "name": "Get competition with partipication", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test('Response has only 2 elements', function () {\r", + " const jsonData = pm.response.json();\r", + " pm.expect(jsonData).to.has.length(0,\"Response has non-0 length, try to reset to only test data state\");\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": { + "token": "{{token}}" + } + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{BASE_HOST}}/v1/competitions?is_participating=true", + "host": [ + "{{BASE_HOST}}" + ], + "path": [ + "v1", + "competitions" + ], + "query": [ + { + "key": "is_participating", + "value": "true" + } + ] + } + }, + "response": [] + }, + { + "name": "Get tasks without partipicating in test", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 403', function () {\r", + " pm.response.to.have.status(403);\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": { + "token": "{{token}}" + } + }, + "method": "GET", + "header": [], + "url": "{{BASE_HOST}}/v1/competitions/{{competition_id}}/tasks" + }, + "response": [] + }, + { + "name": "Submit task sol without partipicaating", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 403', function () {\r", + " pm.response.to.have.status(403);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": { + "token": "{{token}}" + } + }, + "method": "POST", + "header": [], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "file", + "type": "file", + "src": "/C:/Users/timka/AppData/Local/Postman/app-11.33.4/libGLESv2.dll" + } + ] + }, + "url": "{{BASE_HOST}}/v1/competitions/{{competition_id}}/tasks/{{task_id}}/submit" + }, + "response": [] + }, + { + "name": "Partipicate in competition", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": { + "token": "{{token}}" + } + }, + "method": "POST", + "header": [], + "url": "{{BASE_HOST}}/v1/competitions/{{competition_id}}/start" + }, + "response": [] + }, + { + "name": "Get competition tasks", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test('Response has tasks', function () {\r", + " const jsonData = pm.response.json();\r", + " pm.environment.set(\"task_id\", jsonData[0].id)\r", + " pm.expect(jsonData).to.has.length(9)\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": { + "token": "{{token}}" + } + }, + "method": "GET", + "header": [], + "url": "{{BASE_HOST}}/v1/competitions/{{competition_id}}/tasks" + }, + "response": [] + }, + { + "name": "Get task history", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test('Response has token', function () {\r", + " const jsonData = pm.response.json();\r", + " pm.expect(jsonData).to.has.length(0);\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": { + "token": "{{token}}" + } + }, + "method": "GET", + "header": [], + "url": "{{BASE_HOST}}/v1/competitions/{{competition_id}}/tasks/{{task_id}}/history" + }, + "response": [] + }, + { + "name": "Submit task sol", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test('Response has submission id', function () {\r", + " const jsonData = pm.response.json();\r", + " pm.expect(jsonData.submission_id).to.be.a('string');\r", + " pm.environment.set(\"sub_id\", jsonData.submission_id)\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": { + "token": "{{token}}" + } + }, + "method": "POST", + "header": [], + "body": { + "mode": "formdata", + "formdata": [ + { + "key": "file", + "type": "file", + "src": "/C:/Users/timka/AppData/Local/Postman/app-11.33.4/libGLESv2.dll" + } + ] + }, + "url": "{{BASE_HOST}}/v1/competitions/{{competition_id}}/tasks/{{task_id}}/submit" + }, + "response": [] + }, + { + "name": "Get task attachments", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test('Response has empty array', function () {\r", + " const jsonData = pm.response.json();\r", + " pm.expect(jsonData).to.has.length(0);\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "bearer", + "bearer": { + "token": "{{token}}" + } + }, + "method": "GET", + "header": [], + "url": "{{BASE_HOST}}/v1/competitions/{{competition_id}}/tasks/{{task_id}}/attachments" + }, + "response": [] + } + ] + }, + { + "name": "Revieews", + "item": [ + { + "name": "Get reviewer profile", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test('Response has correct data', function () {\r", + " const jsonData = pm.response.json();\r", + " pm.expect(jsonData.name).to.eq(pm.environment.get(\"reviewer_name\"));\r", + " pm.expect(jsonData.surname).to.eq(pm.environment.get(\"reviewer_surname\"))\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": "{{BASE_HOST}}/v1/review/{{reviewer_key}}" + }, + "response": [] + }, + { + "name": "Get submissions", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "\r", + "pm.test('Response has correct data', function () {\r", + " const jsonData = pm.response.json();\r", + " pm.expect(jsonData).to.has.length(0)\r", + "});\r", + "\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": "{{BASE_HOST}}/v1/review/{{reviewer_key}}/submissions/{{sub_id}}" + }, + "response": [] + } + ] + }, + { + "name": "Healthcheck", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('Status code is 200', function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{BASE_HOST}}/health?format=json", + "host": [ + "{{BASE_HOST}}" + ], + "path": [ + "health" + ], + "query": [ + { + "key": "format", + "value": "json" + } + ] + } + }, + "response": [] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "packages": {}, + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "packages": {}, + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "BASE_HOST", + "value": "https://prod-team-15-2pc0i3lc.final.prodcontest.ru/api", + "type": "string" + }, + { + "key": "requestData", + "value": "", + "type": "string" + }, + { + "key": "token", + "value": "", + "type": "string" + }, + { + "key": "competition_id", + "value": "", + "type": "string" + }, + { + "key": "task_id", + "value": "", + "type": "string" + }, + { + "key": "reviewer_key", + "value": "", + "type": "string" + }, + { + "key": "reviewer_name", + "value": "", + "type": "string" + }, + { + "key": "reviewer_surname", + "value": "", + "type": "string" + }, + { + "key": "sub_id", + "value": "", + "type": "string" + } + ] +} \ No newline at end of file diff --git a/services/backend/apps/task/models.py b/services/backend/apps/task/models.py index a23b315..ffb3c08 100644 --- a/services/backend/apps/task/models.py +++ b/services/backend/apps/task/models.py @@ -1,7 +1,9 @@ +from sys import stdout from uuid import uuid4 from django.db import models from django.db.models import Count, Q +from django.core.exceptions import ValidationError from mdeditor.fields import MDTextField from apps.competition.models import Competition @@ -78,6 +80,36 @@ class CompetitionTask(BaseModel): verbose_name="кол-во проверяющих для зачета задачи", ) + def clean(self): + super().clean() + if self.correct_answer_file and self.type not in ["checker", "input"]: + raise ValidationError({ + "type": "Если загружен файл правильного ответа, то тип проверки не может быть ручным" + }) + elif not self.correct_answer_file and self.type == "review": + raise ValidationError({ + "correct_answer_file": "Загрузите правильный ответ" + }) + + if self.answer_file_path and not self.type == "checker": + raise ValidationError({ + "type": "Укажите другой тип задания: этот не совместим с путем правильного ответа" + }) + elif not self.answer_file_path and self.type == "checker": + raise ValidationError({ + "answer_file_path": "Введите путь правильного ответа - это нужно для корректной работы чекера" + }) + + if not self.reviewers and self.type == "review": + raise ValidationError({ + "reviewers": "Загрузите ревьюверов - кто будет проверять задания, если не они?" + }) + elif self.reviewers and not self.type == "review": + raise ValidationError({ + "type": "Проверьте тип - вы ввели ревьюверов, но задание не является ручным" + }) + + def __str__(self): return self.title diff --git a/services/backend/apps/task/tasks.py b/services/backend/apps/task/tasks.py index 64f4658..568602a 100644 --- a/services/backend/apps/task/tasks.py +++ b/services/backend/apps/task/tasks.py @@ -15,6 +15,7 @@ def analyze_data_task(self, submission_id): submission = CompetitionTaskSubmission.objects.get(id=submission_id) try: code = submission.content.read() + print("YA SSF") files = [ { "url": ( @@ -42,6 +43,7 @@ def analyze_data_task(self, submission_id): ) response.raise_for_status() result = response.json() + print("HOHOHO") submission.stdout.save("output.txt", ContentFile(result["output"])) submission.result = { diff --git a/services/frontend/src/components/layout/header.tsx b/services/frontend/src/components/layout/header.tsx index ae5a16d..0563f7d 100644 --- a/services/frontend/src/components/layout/header.tsx +++ b/services/frontend/src/components/layout/header.tsx @@ -30,12 +30,13 @@ export const Header = () => {
- - - + + + Материалы + @@ -50,6 +51,16 @@ export const Header = () => { Аккаунт + +
+ + + + Материалы + + +
+ = ( }) => { const renderResultValue = (result: number, maxPoints: number) => { if (result === -1) { - return На проверке; + return ( + + На проверке + + ); } else if (result === -2) { - return Нет ответа; + return ( + + Нет ответа + + ); + } else if (result === 0) { + return ( + + Неверно (0/{maxPoints}) + + ); + } else if (result < maxPoints) { + return ( + + Частично верно ({result}/{maxPoints}) + + ); } else { return ( - - Зачтено {result}/{maxPoints} баллов + + Верно ({result}/{maxPoints}) ); } @@ -71,7 +117,7 @@ export const CompetitionResultsModal: React.FC = ( className="flex flex-col md:flex-row justify-between items-start md:items-center p-4 bg-gray-50 rounded-lg border" >
{result.task_name}
-
+
{renderResultValue(result.result, result.max_points)}
diff --git a/services/frontend/src/pages/Competition/index.tsx b/services/frontend/src/pages/Competition/index.tsx index 6571c4f..4cf4911 100644 --- a/services/frontend/src/pages/Competition/index.tsx +++ b/services/frontend/src/pages/Competition/index.tsx @@ -93,7 +93,6 @@ const CompetitionPage = () => { return now < startDate; }; - // Check if competition has ended const isCompetitionEnded = () => { if (!competition?.end_date) return false; @@ -212,7 +211,6 @@ const CompetitionPage = () => {
diff --git a/services/frontend/src/pages/CompetitionSession/components/CompetitionHeader/index.tsx b/services/frontend/src/pages/CompetitionSession/components/CompetitionHeader/index.tsx index d912f1a..9042c91 100644 --- a/services/frontend/src/pages/CompetitionSession/components/CompetitionHeader/index.tsx +++ b/services/frontend/src/pages/CompetitionSession/components/CompetitionHeader/index.tsx @@ -1,8 +1,9 @@ import React, { useState, useEffect } from 'react'; -import { Link, useNavigate } from 'react-router-dom'; +import { Link, useNavigate, useParams } from 'react-router-dom'; import { Task } from '@/shared/types/task'; -import { ArrowLeft, Clock } from 'lucide-react'; +import { ArrowLeft } from 'lucide-react'; import { CompetitionType } from '@/shared/types/competition'; +import { CompetitionResult } from '@/shared/types/competition'; interface CompetitionHeaderProps { title: string; @@ -11,8 +12,9 @@ interface CompetitionHeaderProps { setAnswer: (value: string) => void; setSelectedFile: (file: File | null) => void; competitionType?: CompetitionType; - startDate?: Date; - endDate?: Date; + startDate?: Date | string; + endDate?: Date | string; + taskResults?: CompetitionResult[]; } const CompetitionHeader: React.FC = ({ @@ -23,9 +25,11 @@ const CompetitionHeader: React.FC = ({ setSelectedFile, competitionType, startDate, - endDate + endDate, + taskResults = [] }) => { const navigate = useNavigate(); + const { taskId } = useParams<{ taskId?: string }>(); const [timeLeft, setTimeLeft] = useState(''); const handleTaskSelect = (taskId: string) => { @@ -34,7 +38,7 @@ const CompetitionHeader: React.FC = ({ navigate(`/competition/${competitionId}/tasks/${taskId}`); } - const formatDate = (date?: Date) => { + const formatDate = (date?: Date | string) => { if (!date) return ''; const dateObj = typeof date === 'string' ? new Date(date) : date; @@ -74,6 +78,42 @@ const CompetitionHeader: React.FC = ({ return () => clearInterval(timerInterval); }, [endDate, competitionId, navigate, competitionType]); + const getTaskStatus = (task: Task) => { + const result = taskResults.find(r => r.task_name === task.title); + + let bgColor = 'var(--color-task-uncleared)'; + let textColor = 'var(--color-task-text-uncleared)'; + + if (result) { + if (result.result === -1) { + bgColor = 'var(--color-task-checking)'; + textColor = 'var(--color-task-text-checking)'; + } else if (result.result === -2) { + bgColor = 'var(--color-task-uncleared)'; + textColor = 'var(--color-task-text-uncleared)'; + } else if (result.result === 0) { + bgColor = 'var(--color-task-wrong)'; + textColor = 'var(--color-task-text-wrong)'; + } else if (result.result < result.max_points) { + bgColor = 'var(--color-task-partial)'; + textColor = 'var(--color-task-text-partial)'; + } else if (result.result === result.max_points) { + bgColor = 'var(--color-task-correct)'; + textColor = 'var(--color-task-text-correct)'; + } + } + + const isActive = task.id === taskId; + const activeBorder = isActive ? 'border-2 border-blue-500' : ''; + + return { + backgroundColor: bgColor, + color: textColor, + className: `rounded-lg px-3 py-1.5 font-medium text-sm font-hse-sans cursor-pointer + transition-all hover:brightness-95 flex-shrink-0 ${activeBorder}` + }; + }; + const showTimeSection = competitionType === CompetitionType.COMPETITIVE && (startDate || endDate); return ( @@ -120,18 +160,19 @@ const CompetitionHeader: React.FC = ({
- {tasks.map((task) => ( - - ))} + {tasks.map((task) => { + const { backgroundColor, color, className } = getTaskStatus(task); + return ( + + ); + })}
diff --git a/services/frontend/src/pages/CompetitionSession/index.tsx b/services/frontend/src/pages/CompetitionSession/index.tsx index 42ea9c4..4f92cae 100644 --- a/services/frontend/src/pages/CompetitionSession/index.tsx +++ b/services/frontend/src/pages/CompetitionSession/index.tsx @@ -1,10 +1,10 @@ import { useState, useEffect } from "react"; -import { useParams, Navigate, useNavigate } from "react-router-dom"; +import { useParams, Navigate } from "react-router-dom"; import CompetitionHeader from "./components/CompetitionHeader"; import TaskContent from "./components/TaskContent"; import TaskSolution from "./modules/TaskSolution"; import { getCompetitionTasks, submitTaskSolution } from "@/shared/api/session"; -import { getCompetition } from "@/shared/api/competitions"; +import { getCompetition, getCompetitionResults } from "@/shared/api/competitions"; import { Loader2 } from "lucide-react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { TaskType } from "@/shared/types/task"; @@ -16,7 +16,6 @@ const CompetitionSession = () => { const [isReloading, setIsReloading] = useState(false); const competitionId = id || ""; const queryClient = useQueryClient(); - const navigate = useNavigate(); const competitionQuery = useQuery({ queryKey: ["competition", competitionId], @@ -30,6 +29,12 @@ const CompetitionSession = () => { enabled: !!competitionId, }); + const resultsQuery = useQuery({ + queryKey: ["competitionResults", competitionId], + queryFn: () => getCompetitionResults(competitionId), + enabled: !!competitionId, + }); + const submitMutation = useMutation({ mutationFn: () => { if (!currentTask || !competitionId) throw new Error("Missing task or competition ID"); @@ -47,10 +52,14 @@ const CompetitionSession = () => { queryKey: ['solutionHistory', competitionId, taskId] }); + queryClient.invalidateQueries({ + queryKey: ['competitionResults', competitionId] + }); + setIsReloading(true); setTimeout(() => { - window.location.reload() + window.location.reload(); setIsReloading(false); }, 2500); }, @@ -61,6 +70,7 @@ const CompetitionSession = () => { const competition = competitionQuery.data; const tasks = tasksQuery.data || []; + const results = resultsQuery.data || []; const isLoading = tasksQuery.isLoading || competitionQuery.isLoading; const error = tasksQuery.error || competitionQuery.error ? "Не удалось загрузить данные. Пожалуйста, попробуйте позже." @@ -113,6 +123,7 @@ const CompetitionSession = () => { competitionType={competition?.type} startDate={competition?.start_date} endDate={competition?.end_date} + taskResults={results} />
@@ -138,6 +149,16 @@ const CompetitionSession = () => { onSubmit={handleSubmit} isSubmitting={isSubmitting} /> + {isReloading && ( +
+
+ +

+ Решение отправлено! Пожалуйста, подождите... +

+
+
+ )} ) : (
diff --git a/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/ActionButtons/index.tsx b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/ActionButtons/index.tsx index d22ae9c..03eda32 100644 --- a/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/ActionButtons/index.tsx +++ b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/ActionButtons/index.tsx @@ -30,10 +30,9 @@ const ActionButtons: React.FC = ({ {isCleared ? ( ) : hasSubmissionsLeft ? ( diff --git a/services/frontend/src/pages/Profile/widgets/user-stats.tsx b/services/frontend/src/pages/Profile/widgets/user-stats.tsx index 8a62607..2029b6c 100644 --- a/services/frontend/src/pages/Profile/widgets/user-stats.tsx +++ b/services/frontend/src/pages/Profile/widgets/user-stats.tsx @@ -24,7 +24,7 @@ export const UserStatsSections = () => { />
) : (