From 87b1887552f497596cd8bdac750107249088bb11 Mon Sep 17 00:00:00 2001 From: ITQ Date: Mon, 3 Mar 2025 15:58:55 +0300 Subject: [PATCH 01/18] (scope): [body] [footer(s)] --- services/backend/api/v1/task/views.py | 4 +++- services/backend/apps/task/tasks.py | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/services/backend/api/v1/task/views.py b/services/backend/api/v1/task/views.py index f2e93c9..10a2785 100644 --- a/services/backend/api/v1/task/views.py +++ b/services/backend/api/v1/task/views.py @@ -116,14 +116,16 @@ def submit_task( return status.FORBIDDEN, ForbiddenError() if task.type == CompetitionTask.CompetitionTaskType.INPUT: + verdict = content.read() == task.correct_answer_file.read() submission = CompetitionTaskSubmission.objects.create( user=user, task=task, status=CompetitionTaskSubmission.StatusChoices.CHECKED, content=content, result={ - "correct": content.read() == task.correct_answer_file.read() + "correct": verdict }, + earned_points=task.points ) if task.type == CompetitionTask.CompetitionTaskType.REVIEW: submission = CompetitionTaskSubmission.objects.create( diff --git a/services/backend/apps/task/tasks.py b/services/backend/apps/task/tasks.py index 86588f4..4032da1 100644 --- a/services/backend/apps/task/tasks.py +++ b/services/backend/apps/task/tasks.py @@ -41,11 +41,11 @@ def analyze_data_task(self, submission_id): submission.stdout.save("output.txt", ContentFile(result["output"])) submission.result = { "correct": result["hash_match"], - "result_hash": result["result_hash"], + "hash_match": result["hash_match"], "error": result.get("error"), } submission.earned_points = ( - submission.task.points if result["hash_match"] else 0 + submission.task.points if result["correct"] else 0 ) submission.status = CompetitionTaskSubmission.StatusChoices.CHECKED From 664ded152a8112abbe47a983ce8c9417e393e444 Mon Sep 17 00:00:00 2001 From: rngsurrounded Date: Mon, 3 Mar 2025 22:04:25 +0900 Subject: [PATCH 02/18] test fix --- .../components/CompetitionHeader/index.tsx | 1 - .../modules/TaskSolution/index.tsx | 17 ++++++++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/services/frontend/src/pages/CompetitionSession/components/CompetitionHeader/index.tsx b/services/frontend/src/pages/CompetitionSession/components/CompetitionHeader/index.tsx index d4bab74..086abb1 100644 --- a/services/frontend/src/pages/CompetitionSession/components/CompetitionHeader/index.tsx +++ b/services/frontend/src/pages/CompetitionSession/components/CompetitionHeader/index.tsx @@ -24,7 +24,6 @@ const CompetitionHeader: React.FC = ({ const handleTaskSelect = (taskId: string) => { setAnswer(""); setSelectedFile(null); - console.log("SETTER ERROR") navigate(`/competition/${competitionId}/tasks/${taskId}`); } diff --git a/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/index.tsx b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/index.tsx index 5267dbb..8d4051e 100644 --- a/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/index.tsx +++ b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/index.tsx @@ -81,13 +81,24 @@ const TaskSolution: React.FC = ({ useEffect(() => { const loadSolutionContent = async () => { + // Clear previous inputs when task changes + if (prevTaskIdRef.current !== task.id) { + setAnswer(""); + setSelectedFile(null); + setSelectedSolutionUrl(null); + prevTaskIdRef.current = task.id; + } + if (!displayedSolution || !displayedSolution.content) return; try { + // Only load content for the appropriate task type if (task.type === TaskType.FILE) { + // For file tasks, we just set the URL - don't touch the answer field setSelectedFile(null); setSelectedSolutionUrl(displayedSolution.content); - } else { + } else if (task.type === TaskType.CODE || task.type === TaskType.INPUT) { + // For non-file tasks, fetch and set the answer text - don't touch file fields const response = await fetch(displayedSolution.content); if (!response.ok) { throw new Error(`Failed to fetch solution content: ${response.status}`); @@ -99,9 +110,9 @@ const TaskSolution: React.FC = ({ console.error('Error loading solution content:', error); } }; - + loadSolutionContent(); - }, [displayedSolution, task.type, setAnswer, setSelectedFile]); + }, [displayedSolution, task.id, task.type, setAnswer, setSelectedFile]); const handleOpenHistory = () => { setIsHistoryOpen(true); From 00121fff894e170cab381c924e03c4394017ea31 Mon Sep 17 00:00:00 2001 From: ITQ Date: Mon, 3 Mar 2025 16:12:15 +0300 Subject: [PATCH 03/18] (scope): [body] [footer(s)] --- services/backend/apps/task/admin.py | 2 +- ...ksubmission_plagiarism_checked_and_more.py | 22 +++++++++++++++++++ services/backend/apps/task/models.py | 4 ++-- 3 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 services/backend/apps/task/migrations/0002_remove_competitiontasksubmission_plagiarism_checked_and_more.py diff --git a/services/backend/apps/task/admin.py b/services/backend/apps/task/admin.py index ca097c0..af2477b 100644 --- a/services/backend/apps/task/admin.py +++ b/services/backend/apps/task/admin.py @@ -42,7 +42,7 @@ class CompetitionTaskSubmissionAdmin(admin.ModelAdmin): "user__username", "user__email", ) - list_filter = ("plagiarism_checked", "status") + list_filter = ("plagiarism_detected", "status") ordering = ["-timestamp"] def has_add_permission(self, request, obj=None): diff --git a/services/backend/apps/task/migrations/0002_remove_competitiontasksubmission_plagiarism_checked_and_more.py b/services/backend/apps/task/migrations/0002_remove_competitiontasksubmission_plagiarism_checked_and_more.py new file mode 100644 index 0000000..01c63d2 --- /dev/null +++ b/services/backend/apps/task/migrations/0002_remove_competitiontasksubmission_plagiarism_checked_and_more.py @@ -0,0 +1,22 @@ +# Generated by Django 5.1.6 on 2025-03-03 13:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('task', '0001_initial'), + ] + + operations = [ + migrations.RemoveField( + model_name='competitiontasksubmission', + name='plagiarism_checked', + ), + migrations.AddField( + model_name='competitiontasksubmission', + name='plagiarism_detected', + field=models.BooleanField(default=False, verbose_name='обнаружен плагиат'), + ), + ] diff --git a/services/backend/apps/task/models.py b/services/backend/apps/task/models.py index 8f6936e..77123b2 100644 --- a/services/backend/apps/task/models.py +++ b/services/backend/apps/task/models.py @@ -178,8 +178,8 @@ class CompetitionTaskSubmission(BaseModel): checked_at = models.DateTimeField( null=True, blank=True, verbose_name="дата проверки" ) - plagiarism_checked = models.BooleanField( - default=False, verbose_name="проверено на плагиат" + plagiarism_detected = models.BooleanField( + default=False, verbose_name="обнаружен плагиат" ) timestamp = models.DateTimeField( auto_now_add=True, verbose_name="дата отправки" From 757c1d80230bd4a44dcda42c799abcaf67925a15 Mon Sep 17 00:00:00 2001 From: rngsurrounded Date: Mon, 3 Mar 2025 22:16:59 +0900 Subject: [PATCH 04/18] input hot fix --- .../modules/TaskSolution/index.tsx | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/index.tsx b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/index.tsx index 8d4051e..5faf95e 100644 --- a/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/index.tsx +++ b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/index.tsx @@ -81,24 +81,16 @@ const TaskSolution: React.FC = ({ useEffect(() => { const loadSolutionContent = async () => { - // Clear previous inputs when task changes - if (prevTaskIdRef.current !== task.id) { - setAnswer(""); - setSelectedFile(null); - setSelectedSolutionUrl(null); - prevTaskIdRef.current = task.id; - } - if (!displayedSolution || !displayedSolution.content) return; try { - // Only load content for the appropriate task type if (task.type === TaskType.FILE) { - // For file tasks, we just set the URL - don't touch the answer field + setAnswer(""); setSelectedFile(null); setSelectedSolutionUrl(displayedSolution.content); - } else if (task.type === TaskType.CODE || task.type === TaskType.INPUT) { - // For non-file tasks, fetch and set the answer text - don't touch file fields + } else { + setSelectedFile(null); + setSelectedSolutionUrl(null); const response = await fetch(displayedSolution.content); if (!response.ok) { throw new Error(`Failed to fetch solution content: ${response.status}`); @@ -112,7 +104,7 @@ const TaskSolution: React.FC = ({ }; loadSolutionContent(); - }, [displayedSolution, task.id, task.type, setAnswer, setSelectedFile]); + }, [displayedSolution, task.type, setAnswer, setSelectedFile]); const handleOpenHistory = () => { setIsHistoryOpen(true); From d053d20825753e0b98eeefa7597ea7d07a8252ae Mon Sep 17 00:00:00 2001 From: rngsurrounded Date: Mon, 3 Mar 2025 22:23:15 +0900 Subject: [PATCH 05/18] competition timer test --- .../components/CompetitionHeader/index.tsx | 92 +++++++++++++++++-- .../src/pages/CompetitionSession/index.tsx | 5 + 2 files changed, 89 insertions(+), 8 deletions(-) diff --git a/services/frontend/src/pages/CompetitionSession/components/CompetitionHeader/index.tsx b/services/frontend/src/pages/CompetitionSession/components/CompetitionHeader/index.tsx index 086abb1..ba486e4 100644 --- a/services/frontend/src/pages/CompetitionSession/components/CompetitionHeader/index.tsx +++ b/services/frontend/src/pages/CompetitionSession/components/CompetitionHeader/index.tsx @@ -1,8 +1,8 @@ -import React from 'react'; -import { Link } from 'react-router-dom'; +import React, { useState, useEffect } from 'react'; +import { Link, useNavigate } from 'react-router-dom'; import { Task } from '@/shared/types/task'; -import { ArrowLeft } from 'lucide-react'; -import { useNavigate } from 'react-router-dom'; +import { ArrowLeft, Clock } from 'lucide-react'; +import { CompetitionType } from '@/shared/types/task'; interface CompetitionHeaderProps { title: string; @@ -10,6 +10,9 @@ interface CompetitionHeaderProps { competitionId: string; setAnswer: (value: string) => void; setSelectedFile: (file: File | null) => void; + competitionType?: CompetitionType; + startDate?: Date; + endDate?: Date; } const CompetitionHeader: React.FC = ({ @@ -17,16 +20,66 @@ const CompetitionHeader: React.FC = ({ tasks, competitionId, setAnswer, - setSelectedFile + setSelectedFile, + competitionType, + startDate, + endDate }) => { const navigate = useNavigate(); - + const [timeLeft, setTimeLeft] = useState(''); + const handleTaskSelect = (taskId: string) => { setAnswer(""); setSelectedFile(null); navigate(`/competition/${competitionId}/tasks/${taskId}`); } - + + const formatDate = (date?: Date) => { + if (!date) return ''; + + const dateObj = typeof date === 'string' ? new Date(date) : date; + return dateObj.toLocaleString('ru-RU', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); + }; + + useEffect(() => { + if (!endDate || competitionType !== CompetitionType.COMPETITIVE) return; + + const endDateObj = typeof endDate === 'string' ? new Date(endDate) : endDate; + + const updateTimer = () => { + const now = new Date(); + const diff = endDateObj.getTime() - now.getTime(); + + // If time is up, redirect to competition page + if (diff <= 0) { + navigate(`/competition/${competitionId}`); + return; + } + + // Calculate hours, minutes, seconds + const hours = Math.floor(diff / (1000 * 60 * 60)); + const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); + const seconds = Math.floor((diff % (1000 * 60)) / 1000); + + // Format time left + setTimeLeft(`${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`); + }; + + // Update timer every second + updateTimer(); + const timerInterval = setInterval(updateTimer, 1000); + + return () => clearInterval(timerInterval); + }, [endDate, competitionId, navigate, competitionType]); + + const showTimeSection = competitionType === CompetitionType.COMPETITIVE && (startDate || endDate); + return (
@@ -42,7 +95,30 @@ const CompetitionHeader: React.FC = ({ {title} -
+ {showTimeSection ? ( +
+ +
+ {startDate && ( + + Начало: {formatDate(startDate)} + + )} + {endDate && ( + + Конец: {formatDate(endDate)} + + )} + {timeLeft && ( + + Осталось: {timeLeft} + + )} +
+
+ ) : ( +
+ )}
diff --git a/services/frontend/src/pages/CompetitionSession/index.tsx b/services/frontend/src/pages/CompetitionSession/index.tsx index eb80913..4297e8e 100644 --- a/services/frontend/src/pages/CompetitionSession/index.tsx +++ b/services/frontend/src/pages/CompetitionSession/index.tsx @@ -8,6 +8,7 @@ import { getCompetition } from "@/shared/api/competitions"; import { Loader2 } from "lucide-react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { TaskType } from "@/shared/types/task"; +import { CompetitionType } from "@/shared/types/task"; const CompetitionSession = () => { const { id, taskId } = useParams<{ id: string; taskId?: string }>(); @@ -97,6 +98,9 @@ const CompetitionSession = () => { competitionId={competitionId} setAnswer={setAnswer} setSelectedFile={setSelectedFile} + competitionType={competition?.type} + startDate={competition?.start_date} + endDate={competition?.end_date} />
@@ -120,6 +124,7 @@ const CompetitionSession = () => { selectedFile={selectedFile} setSelectedFile={setSelectedFile} onSubmit={handleSubmit} + isSubmitting={submitMutation.isPending} />
) : ( From a31e5b63d3873a700b0fca8cc0d47a0989b46fcd Mon Sep 17 00:00:00 2001 From: rngsurrounded Date: Mon, 3 Mar 2025 22:29:11 +0900 Subject: [PATCH 06/18] timer fix --- .../components/CompetitionHeader/index.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/services/frontend/src/pages/CompetitionSession/components/CompetitionHeader/index.tsx b/services/frontend/src/pages/CompetitionSession/components/CompetitionHeader/index.tsx index ba486e4..87f93fe 100644 --- a/services/frontend/src/pages/CompetitionSession/components/CompetitionHeader/index.tsx +++ b/services/frontend/src/pages/CompetitionSession/components/CompetitionHeader/index.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react'; import { Link, useNavigate } from 'react-router-dom'; import { Task } from '@/shared/types/task'; import { ArrowLeft, Clock } from 'lucide-react'; -import { CompetitionType } from '@/shared/types/task'; +import { CompetitionType } from '@/shared/types/competition'; interface CompetitionHeaderProps { title: string; @@ -56,22 +56,18 @@ const CompetitionHeader: React.FC = ({ const now = new Date(); const diff = endDateObj.getTime() - now.getTime(); - // If time is up, redirect to competition page if (diff <= 0) { navigate(`/competition/${competitionId}`); return; } - // Calculate hours, minutes, seconds const hours = Math.floor(diff / (1000 * 60 * 60)); const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); const seconds = Math.floor((diff % (1000 * 60)) / 1000); - // Format time left setTimeLeft(`${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`); }; - // Update timer every second updateTimer(); const timerInterval = setInterval(updateTimer, 1000); From bc0c24fa5ab2b1ae95d1b40dc0a8e81edc1596a2 Mon Sep 17 00:00:00 2001 From: rngsurrounded Date: Mon, 3 Mar 2025 22:47:44 +0900 Subject: [PATCH 07/18] test --- .../components/CompetitionHeader/index.tsx | 24 ++++++++++--------- .../modules/TaskSolution/index.tsx | 8 ++++--- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/services/frontend/src/pages/CompetitionSession/components/CompetitionHeader/index.tsx b/services/frontend/src/pages/CompetitionSession/components/CompetitionHeader/index.tsx index 87f93fe..d912f1a 100644 --- a/services/frontend/src/pages/CompetitionSession/components/CompetitionHeader/index.tsx +++ b/services/frontend/src/pages/CompetitionSession/components/CompetitionHeader/index.tsx @@ -80,20 +80,22 @@ const CompetitionHeader: React.FC = ({
- - - - -

- {title} -

+
+ + + + +

+ {title} +

+
+ {showTimeSection ? (
-
{startDate && ( diff --git a/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/index.tsx b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/index.tsx index 5faf95e..5441408 100644 --- a/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/index.tsx +++ b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/index.tsx @@ -82,13 +82,14 @@ const TaskSolution: React.FC = ({ useEffect(() => { const loadSolutionContent = async () => { if (!displayedSolution || !displayedSolution.content) return; - + console.log(displayedSolution, solutionHistory, "CHECK") try { if (task.type === TaskType.FILE) { setAnswer(""); setSelectedFile(null); setSelectedSolutionUrl(displayedSolution.content); - } else { + } + else { setSelectedFile(null); setSelectedSolutionUrl(null); const response = await fetch(displayedSolution.content); @@ -96,6 +97,7 @@ const TaskSolution: React.FC = ({ throw new Error(`Failed to fetch solution content: ${response.status}`); } const text = await response.text(); + setAnswer(text); } } catch (error) { @@ -104,7 +106,7 @@ const TaskSolution: React.FC = ({ }; loadSolutionContent(); - }, [displayedSolution, task.type, setAnswer, setSelectedFile]); + }, [displayedSolution, setAnswer, setSelectedFile]); const handleOpenHistory = () => { setIsHistoryOpen(true); From 96e5585014f3c493c883a6a4d9eb527d39e7db31 Mon Sep 17 00:00:00 2001 From: ITQ Date: Mon, 3 Mar 2025 16:50:19 +0300 Subject: [PATCH 08/18] (scope): [body] [footer(s)] --- services/backend/apps/task/tasks.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/services/backend/apps/task/tasks.py b/services/backend/apps/task/tasks.py index 4032da1..c7f1baf 100644 --- a/services/backend/apps/task/tasks.py +++ b/services/backend/apps/task/tasks.py @@ -4,6 +4,7 @@ import httpx from celery import shared_task from django.conf import settings from django.core.files.base import ContentFile +from urllib.parse import urlparse from apps.task.models import CompetitionTaskSubmission @@ -12,10 +13,16 @@ from apps.task.models import CompetitionTaskSubmission def analyze_data_task(self, submission_id): submission = CompetitionTaskSubmission.objects.get(id=submission_id) try: - code_url = f"{settings.MINIO_DEFAULT_CUSTOM_ENDPOINT_URL}{submission.content.path}" + code_url = ( + f"{settings.MINIO_DEFAULT_CUSTOM_ENDPOINT_URL}/" + f"{urlparse(submission.content.file.url).path}" + ) files = [ { - "url": f"{settings.MINIO_DEFAULT_CUSTOM_ENDPOINT_URL}{attachment.path}", + "url": ( + f"{settings.MINIO_DEFAULT_CUSTOM_ENDPOINT_URL}/" + f"{urlparse(submission.content.file.url).path}" + ), "bind_path": attachment.bind_at, } for attachment in submission.task.attachments.filter( @@ -40,7 +47,7 @@ def analyze_data_task(self, submission_id): submission.stdout.save("output.txt", ContentFile(result["output"])) submission.result = { - "correct": result["hash_match"], + "correct": result["correct"], "hash_match": result["hash_match"], "error": result.get("error"), } From 49e792825043e8156bd29853c4ba9add1981ff47 Mon Sep 17 00:00:00 2001 From: ITQ Date: Mon, 3 Mar 2025 16:57:47 +0300 Subject: [PATCH 09/18] (scope): [body] [footer(s)] --- services/backend/apps/task/tasks.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/backend/apps/task/tasks.py b/services/backend/apps/task/tasks.py index c7f1baf..b68ecf2 100644 --- a/services/backend/apps/task/tasks.py +++ b/services/backend/apps/task/tasks.py @@ -15,13 +15,13 @@ def analyze_data_task(self, submission_id): try: code_url = ( f"{settings.MINIO_DEFAULT_CUSTOM_ENDPOINT_URL}/" - f"{urlparse(submission.content.file.url).path}" + f"{urlparse(submission.content.url).path}" ) files = [ { "url": ( f"{settings.MINIO_DEFAULT_CUSTOM_ENDPOINT_URL}/" - f"{urlparse(submission.content.file.url).path}" + f"{urlparse(submission.content.url).path}" ), "bind_path": attachment.bind_at, } From 304704d392d9c8a0e2e3c6e7f6a32f53fb47105e Mon Sep 17 00:00:00 2001 From: rngsurrounded Date: Mon, 3 Mar 2025 23:00:23 +0900 Subject: [PATCH 10/18] file solution fix --- .../modules/TaskSolution/components/FileSolution/index.tsx | 4 +--- .../pages/CompetitionSession/modules/TaskSolution/index.tsx | 1 - 2 files changed, 1 insertion(+), 4 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 a843612..992f117 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 @@ -8,7 +8,6 @@ interface FileSolutionProps { fileInputRef: React.RefObject; existingFileUrl?: string | null; onClearExistingFile?: () => void; // New prop to clear existing file URL - firstSolution: boolean } const FileSolution: React.FC = ({ @@ -17,7 +16,6 @@ const FileSolution: React.FC = ({ fileInputRef, existingFileUrl = null, onClearExistingFile, - firstSolution }) => { const handleFileChange = (event: React.ChangeEvent) => { if (event.target.files && event.target.files[0]) { @@ -68,7 +66,7 @@ const FileSolution: React.FC = ({ ? existingFileUrl.split('/').pop() || 'file' : ''; - const hasFile = !!selectedFile || (!!existingFileUrl && !firstSolution); + const hasFile = !!selectedFile || !!existingFileUrl; return ( <> diff --git a/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/index.tsx b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/index.tsx index 5441408..fd2574d 100644 --- a/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/index.tsx +++ b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/index.tsx @@ -149,7 +149,6 @@ const TaskSolution: React.FC = ({ fileInputRef={fileInputRef} existingFileUrl={selectedSolutionUrl} onClearExistingFile={handleClearExistingFile} - firstSolution={solutionHistory.length > 0} /> )} From 70acce4b9f836d51998795c0e23b557baf20a55a Mon Sep 17 00:00:00 2001 From: rngsurrounded Date: Mon, 3 Mar 2025 23:12:40 +0900 Subject: [PATCH 11/18] feat: access to competition is blocked if it is ended --- .../frontend/src/components/layout/header.tsx | 10 ++-- .../frontend/src/pages/Competition/index.tsx | 49 ++++++++++++++++--- services/frontend/src/shared/stores/user.ts | 7 ++- 3 files changed, 54 insertions(+), 12 deletions(-) diff --git a/services/frontend/src/components/layout/header.tsx b/services/frontend/src/components/layout/header.tsx index 4bd1cdd..9ebacc2 100644 --- a/services/frontend/src/components/layout/header.tsx +++ b/services/frontend/src/components/layout/header.tsx @@ -13,7 +13,13 @@ import { useUserStore } from "@/shared/stores/user"; const Header = () => { const user = useUserStore((state) => state.user); + const clearUser = useUserStore((state) => state.clearUser); const [isProfileOpen, setIsProfileOpen] = useState(false); + + const handleLogout = () => { + clearUser(); + setIsProfileOpen(false); + }; return (
@@ -69,9 +75,7 @@ const Header = () => { } label="Выйти" - onClick={() => { - setIsProfileOpen(false); - }} + onClick={handleLogout} />
diff --git a/services/frontend/src/pages/Competition/index.tsx b/services/frontend/src/pages/Competition/index.tsx index e472ddb..3381ad7 100644 --- a/services/frontend/src/pages/Competition/index.tsx +++ b/services/frontend/src/pages/Competition/index.tsx @@ -1,6 +1,6 @@ import { useParams, Link, useNavigate } from "react-router-dom"; import { Button } from "@/components/ui/button"; -import { ArrowLeft, Clock, Trophy, BookOpen } from "lucide-react"; +import { ArrowLeft, Clock, Trophy, BookOpen, BarChart2 } from "lucide-react"; import ReactMarkdown from "react-markdown"; import { useQuery, useMutation } from "@tanstack/react-query"; import { getCompetition, startCompetition } from "@/shared/api/competitions"; @@ -39,6 +39,7 @@ const CompetitionPage = () => { console.error("Failed to start competition:", error); } }); + const formatDate = (date?: Date | string) => { if (!date) return ""; @@ -56,6 +57,20 @@ const CompetitionPage = () => { const handleStart = () => { startMutation.mutate(); }; + + const handleViewResults = () => { + navigate(`/competition/${competitionId}/results`); + }; + + // Check if competition has ended + const isCompetitionEnded = () => { + if (!competitionQuery.data?.end_date) return false; + + const endDate = new Date(competitionQuery.data.end_date); + const now = new Date(); + + return now > endDate; + }; if (competitionQuery.isLoading) { return ; @@ -66,6 +81,7 @@ const CompetitionPage = () => { } const competition = competitionQuery.data; + const competitionEnded = isCompetitionEnded(); return (
@@ -103,6 +119,12 @@ const CompetitionPage = () => { )}
+ + {competitionEnded && competition.type === CompetitionType.COMPETITIVE && ( +
+ Завершено +
+ )}

@@ -133,13 +155,24 @@ const CompetitionPage = () => {

- + {competitionEnded && competition.type === CompetitionType.COMPETITIVE ? ( + + ) : ( + + )}
diff --git a/services/frontend/src/shared/stores/user.ts b/services/frontend/src/shared/stores/user.ts index 6e7509d..08921c6 100644 --- a/services/frontend/src/shared/stores/user.ts +++ b/services/frontend/src/shared/stores/user.ts @@ -7,6 +7,7 @@ interface UserState { loading: boolean; fetchUser: () => Promise; + clearUser: () => void; } const useUserStore = create((set) => ({ @@ -18,6 +19,10 @@ const useUserStore = create((set) => ({ const user = await getCurrentUser(); set({ user, loading: false }); }, + + clearUser: () => { + set({ user: null }); + }, })); -export { useUserStore }; +export { useUserStore }; \ No newline at end of file From c6bd18e9ba90f28119f7febf87d0f025674ea9fd Mon Sep 17 00:00:00 2001 From: rngsurrounded Date: Mon, 3 Mar 2025 23:19:08 +0900 Subject: [PATCH 12/18] fix history --- services/frontend/src/pages/Competition/index.tsx | 6 +++--- .../CompetitionSession/components/TaskContent/index.tsx | 2 +- services/frontend/src/pages/CompetitionSession/index.tsx | 1 - .../pages/CompetitionSession/modules/TaskSolution/index.tsx | 2 +- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/services/frontend/src/pages/Competition/index.tsx b/services/frontend/src/pages/Competition/index.tsx index 3381ad7..a7822a9 100644 --- a/services/frontend/src/pages/Competition/index.tsx +++ b/services/frontend/src/pages/Competition/index.tsx @@ -59,14 +59,14 @@ const CompetitionPage = () => { }; const handleViewResults = () => { - navigate(`/competition/${competitionId}/results`); + console.log("sorryan") }; // Check if competition has ended const isCompetitionEnded = () => { - if (!competitionQuery.data?.end_date) return false; + if (!competition?.end_date) return false; - const endDate = new Date(competitionQuery.data.end_date); + const endDate = new Date(competition.end_date); const now = new Date(); return now > endDate; diff --git a/services/frontend/src/pages/CompetitionSession/components/TaskContent/index.tsx b/services/frontend/src/pages/CompetitionSession/components/TaskContent/index.tsx index 0c49ca3..5bfc7ea 100644 --- a/services/frontend/src/pages/CompetitionSession/components/TaskContent/index.tsx +++ b/services/frontend/src/pages/CompetitionSession/components/TaskContent/index.tsx @@ -27,7 +27,7 @@ const TaskContent: React.FC = ({ task }) => { return (

- Задача {task.in_competition_position} + {task.title}

diff --git a/services/frontend/src/pages/CompetitionSession/index.tsx b/services/frontend/src/pages/CompetitionSession/index.tsx index 4297e8e..fd8ec50 100644 --- a/services/frontend/src/pages/CompetitionSession/index.tsx +++ b/services/frontend/src/pages/CompetitionSession/index.tsx @@ -8,7 +8,6 @@ import { getCompetition } from "@/shared/api/competitions"; import { Loader2 } from "lucide-react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { TaskType } from "@/shared/types/task"; -import { CompetitionType } from "@/shared/types/task"; const CompetitionSession = () => { const { id, taskId } = useParams<{ id: string; taskId?: string }>(); diff --git a/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/index.tsx b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/index.tsx index fd2574d..67c9171 100644 --- a/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/index.tsx +++ b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/index.tsx @@ -54,7 +54,7 @@ const TaskSolution: React.FC = ({ const latestSolution = solutionHistory[solutionHistory.length - 1]; setDisplayedSolution(latestSolution); } - }, [solutionHistory, displayedSolution]); + }, [solutionHistory]); useEffect(() => { if (prevTaskIdRef.current !== task.id) { From d7fc038a4556feb51526fe63c66f1ecad2b82c2b Mon Sep 17 00:00:00 2001 From: rngsurrounded Date: Mon, 3 Mar 2025 23:22:48 +0900 Subject: [PATCH 13/18] feat: added soon to start task --- .../frontend/src/pages/Competition/index.tsx | 48 ++++++++++++++----- 1 file changed, 36 insertions(+), 12 deletions(-) diff --git a/services/frontend/src/pages/Competition/index.tsx b/services/frontend/src/pages/Competition/index.tsx index a7822a9..20be3eb 100644 --- a/services/frontend/src/pages/Competition/index.tsx +++ b/services/frontend/src/pages/Competition/index.tsx @@ -1,6 +1,6 @@ import { useParams, Link, useNavigate } from "react-router-dom"; import { Button } from "@/components/ui/button"; -import { ArrowLeft, Clock, Trophy, BookOpen, BarChart2 } from "lucide-react"; +import { ArrowLeft, Clock, Trophy, BookOpen, BarChart2, AlertCircle } from "lucide-react"; import ReactMarkdown from "react-markdown"; import { useQuery, useMutation } from "@tanstack/react-query"; import { getCompetition, startCompetition } from "@/shared/api/competitions"; @@ -59,17 +59,7 @@ const CompetitionPage = () => { }; const handleViewResults = () => { - console.log("sorryan") - }; - - // Check if competition has ended - const isCompetitionEnded = () => { - if (!competition?.end_date) return false; - - const endDate = new Date(competition.end_date); - const now = new Date(); - - return now > endDate; + console.log("sorryan"); }; if (competitionQuery.isLoading) { @@ -81,7 +71,27 @@ const CompetitionPage = () => { } const competition = competitionQuery.data; + + const isCompetitionEnded = () => { + if (!competition?.end_date) return false; + + const endDate = new Date(competition.end_date); + const now = new Date(); + + return now > endDate; + }; + + const isCompetitionNotStarted = () => { + if (!competition?.start_date) return false; + + const startDate = new Date(competition.start_date); + const now = new Date(); + + return now < startDate; + }; + const competitionEnded = isCompetitionEnded(); + const competitionNotStarted = isCompetitionNotStarted(); return (
@@ -125,6 +135,12 @@ const CompetitionPage = () => { Завершено
)} + + {competitionNotStarted && competition.type === CompetitionType.COMPETITIVE && ( +
+ Скоро начнется +
+ )}

@@ -164,6 +180,14 @@ const CompetitionPage = () => { Смотреть результаты + ) : competitionNotStarted && competition.type === CompetitionType.COMPETITIVE ? ( + ) : (

- {competition.description || ""} + + {competition.description || ""} +
@@ -186,6 +194,7 @@ const CompetitionPage = () => { disabled={true} className="bg-gray-200 text-gray-500 cursor-not-allowed" > + Скоро начнется ) : ( diff --git a/services/frontend/src/pages/CompetitionSession/components/TaskContent/index.tsx b/services/frontend/src/pages/CompetitionSession/components/TaskContent/index.tsx index 5bfc7ea..df6c72d 100644 --- a/services/frontend/src/pages/CompetitionSession/components/TaskContent/index.tsx +++ b/services/frontend/src/pages/CompetitionSession/components/TaskContent/index.tsx @@ -2,6 +2,7 @@ import React from 'react'; import ReactMarkdown from 'react-markdown'; import remarkMath from 'remark-math'; import rehypeKatex from 'rehype-katex'; +import remarkGfm from 'remark-gfm'; import 'katex/dist/katex.min.css'; import { Task } from '@/shared/types/task'; import { useQuery } from '@tanstack/react-query'; @@ -32,7 +33,7 @@ const TaskContent: React.FC = ({ task }) => {
{task.description} From 368b8dd5283dab8583f2c22e56afee65b3c338e5 Mon Sep 17 00:00:00 2001 From: rngsurrounded Date: Mon, 3 Mar 2025 23:44:43 +0900 Subject: [PATCH 16/18] fix markdown? --- .../components/TaskContent/index.tsx | 13 +++++++++++-- services/frontend/src/shared/stores/user.ts | 7 +++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/services/frontend/src/pages/CompetitionSession/components/TaskContent/index.tsx b/services/frontend/src/pages/CompetitionSession/components/TaskContent/index.tsx index df6c72d..634a7a0 100644 --- a/services/frontend/src/pages/CompetitionSession/components/TaskContent/index.tsx +++ b/services/frontend/src/pages/CompetitionSession/components/TaskContent/index.tsx @@ -2,7 +2,7 @@ import React from 'react'; import ReactMarkdown from 'react-markdown'; import remarkMath from 'remark-math'; import rehypeKatex from 'rehype-katex'; -import remarkGfm from 'remark-gfm'; +import remarkGfm from 'remark-gfm'; import 'katex/dist/katex.min.css'; import { Task } from '@/shared/types/task'; import { useQuery } from '@tanstack/react-query'; @@ -25,6 +25,15 @@ const TaskContent: React.FC = ({ task }) => { const attachments = attachmentsQuery.data || []; + const convertToMarkdown = (text: string): string => { + if (!text) return ''; + + let markdown = text.replace(/\n/g, '\n\n'); + return markdown; + }; + + const markdownText = convertToMarkdown(task.description); + return (

@@ -36,7 +45,7 @@ const TaskContent: React.FC = ({ task }) => { remarkPlugins={[remarkMath, remarkGfm]} rehypePlugins={[rehypeKatex]} > - {task.description} + {markdownText}

diff --git a/services/frontend/src/shared/stores/user.ts b/services/frontend/src/shared/stores/user.ts index 08921c6..86bdc9b 100644 --- a/services/frontend/src/shared/stores/user.ts +++ b/services/frontend/src/shared/stores/user.ts @@ -1,6 +1,7 @@ import { create } from "zustand"; import { User } from "../types/user"; import { getCurrentUser } from "../api/user"; +import Cookies from "js-cookie"; interface UserState { user: User | null; @@ -22,6 +23,12 @@ const useUserStore = create((set) => ({ clearUser: () => { set({ user: null }); + + const cookies = Cookies.get(); + Object.keys(cookies).forEach(cookieName => { + Cookies.remove(cookieName, { path: '/' }); + Cookies.remove(cookieName); + }); }, })); From 41449101535392179ac8a0739327108b1fdce61e Mon Sep 17 00:00:00 2001 From: Timur Date: Mon, 3 Mar 2025 17:47:27 +0300 Subject: [PATCH 17/18] add endpoint to get competition result --- services/backend/api/v1/task/schemas.py | 5 ++++ services/backend/api/v1/task/views.py | 31 ++++++++++++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/services/backend/api/v1/task/schemas.py b/services/backend/api/v1/task/schemas.py index 3e07b92..a203f22 100644 --- a/services/backend/api/v1/task/schemas.py +++ b/services/backend/api/v1/task/schemas.py @@ -62,3 +62,8 @@ class TaskAttachmentSchema(ModelSchema): "file", "public", ) + + +class TaskStatusSchema(Schema): + task_name: str + result: int diff --git a/services/backend/api/v1/task/views.py b/services/backend/api/v1/task/views.py index 10a2785..137e73e 100644 --- a/services/backend/api/v1/task/views.py +++ b/services/backend/api/v1/task/views.py @@ -1,7 +1,7 @@ from http import HTTPStatus as status from uuid import UUID -from django.shortcuts import get_object_or_404 +from django.shortcuts import get_object_or_404, get_list_or_404 from ninja import File, Router, UploadedFile from api.v1.ping.schemas import PingOut @@ -11,6 +11,7 @@ from api.v1.task.schemas import ( TaskAttachmentSchema, TaskOutSchema, TaskSubmissionOut, + TaskStatusSchema, ) from apps.achievement.models import Achievement, UserAchievement from apps.competition.models import State @@ -175,3 +176,31 @@ 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() + + +@router.get( + "competitions/{competition_id}/results", + response={ + status.OK: list[TaskStatusSchema], + status.UNAUTHORIZED: UnauthorizedError + }, +) +def get_competition_results(request, competition_id: UUID): + tasks = get_list_or_404(CompetitionTask, competition_id=competition_id) + + data = [] + + for task in tasks: + submissions = CompetitionTaskSubmission.objects.filter( + user=request.auth, task=task + ).filter(status="checked").all() + if not submissions: + result = 0 + else: + result = submissions[0].earned_points + data.append(TaskStatusSchema( + task_name=task.title, + result=result + )) + + return status.OK, data From 6ce9da0060a06f9dee5e8b02e2670895d32c12cc Mon Sep 17 00:00:00 2001 From: rngsurrounded Date: Mon, 3 Mar 2025 23:49:31 +0900 Subject: [PATCH 18/18] history fix --- .../modules/TaskSolution/index.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/index.tsx b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/index.tsx index c061407..cd7906b 100644 --- a/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/index.tsx +++ b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/index.tsx @@ -70,13 +70,13 @@ const TaskSolution: React.FC = ({ } }, [task.id, solutionHistory]); - useEffect(() => { - if (solutionHistory.length > 0 && - (!displayedSolution || - (solutionHistory[solutionHistory.length - 1].id !== displayedSolution.id))) { - setDisplayedSolution(solutionHistory[solutionHistory.length - 1]); - } - }, [solutionHistory, displayedSolution]); + // useEffect(() => { + // if (solutionHistory.length > 0 && + // (!displayedSolution || + // (solutionHistory[solutionHistory.length - 1].id !== displayedSolution.id))) { + // setDisplayedSolution(solutionHistory[solutionHistory.length - 1]); + // } + // }, [solutionHistory, displayedSolution]); useEffect(() => { const loadSolutionContent = async () => {