From 52cb8f72ce03195b7a59549b568844ba147caa4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9=20=D0=A1=D1=83=D0=BC?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Sun, 2 Mar 2025 17:41:56 +0300 Subject: [PATCH 1/5] feat: fixed review not adding if reviewer added after creating --- services/backend/apps/review/apps.py | 3 +++ services/backend/apps/review/signals.py | 14 ++++++++++++++ services/backend/apps/task/models.py | 2 +- 3 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 services/backend/apps/review/signals.py diff --git a/services/backend/apps/review/apps.py b/services/backend/apps/review/apps.py index 27080a3..138bf7f 100644 --- a/services/backend/apps/review/apps.py +++ b/services/backend/apps/review/apps.py @@ -5,3 +5,6 @@ class CoreConfig(AppConfig): name = "apps.review" label = "review" verbose_name = "Проверка" + + def ready(self): + import apps.review.signals diff --git a/services/backend/apps/review/signals.py b/services/backend/apps/review/signals.py new file mode 100644 index 0000000..44da6c2 --- /dev/null +++ b/services/backend/apps/review/signals.py @@ -0,0 +1,14 @@ +# myapp/signals.py +from django.db.models.signals import m2m_changed +from django.dispatch import receiver + +from apps.review.models import Review +from apps.task.models import CompetitionTask, CompetitionTaskSubmission + + +@receiver(m2m_changed, sender=CompetitionTask.reviewers.through) +def print_reviewers(sender, instance, action, **kwargs): + if action in ['post_add', 'post_remove', 'post_clear']: + submissions = CompetitionTaskSubmission.objects.filter(task=instance) + for submission in submissions: + submission.send_on_review() \ No newline at end of file diff --git a/services/backend/apps/task/models.py b/services/backend/apps/task/models.py index 17232b8..dc917ef 100644 --- a/services/backend/apps/task/models.py +++ b/services/backend/apps/task/models.py @@ -182,7 +182,7 @@ class CompetitionTaskSubmission(BaseModel): ] # да это медленно работает и чо for reviewer in reviewers: - Review.objects.create( + Review.objects.update_or_create( reviewer=reviewer, submission=self, ) From c7c4640dc590eb78f4b2ea85ba8483dbf0bb5d57 Mon Sep 17 00:00:00 2001 From: rngsurrounded Date: Sun, 2 Mar 2025 23:45:13 +0900 Subject: [PATCH 2/5] feat: right file handling --- .../src/pages/CompetitionSession/index.tsx | 37 +++++++++++++++++-- .../modules/TaskSolution/index.tsx | 11 ++++-- services/frontend/src/shared/api/session.ts | 2 +- 3 files changed, 41 insertions(+), 9 deletions(-) diff --git a/services/frontend/src/pages/CompetitionSession/index.tsx b/services/frontend/src/pages/CompetitionSession/index.tsx index 451968e..2129c52 100644 --- a/services/frontend/src/pages/CompetitionSession/index.tsx +++ b/services/frontend/src/pages/CompetitionSession/index.tsx @@ -6,10 +6,12 @@ import TaskSolution from "./modules/TaskSolution"; import { getCompetitionTasks, submitTaskSolution } from "@/shared/api/session"; import { Loader2 } from "lucide-react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { TaskType } from "@/shared/types/task"; const CompetitionSession = () => { const { id, taskId } = useParams<{ id: string; taskId?: string }>(); const [answer, setAnswer] = useState(""); + const [selectedFile, setSelectedFile] = useState(null); const competitionId = id || ""; const queryClient = useQueryClient(); @@ -20,12 +22,27 @@ const CompetitionSession = () => { }); const submitMutation = useMutation({ - mutationFn: () => submitTaskSolution(competitionId, taskId || "", answer), + mutationFn: () => { + if (!currentTask || !competitionId) throw new Error("Missing task or competition ID"); + + if (currentTask.type === TaskType.FILE) { + if (!selectedFile) throw new Error("No file selected"); + return submitTaskSolution(competitionId, taskId || "", selectedFile); + } else { + if (!answer.trim()) throw new Error("Answer is empty"); + return submitTaskSolution(competitionId, taskId || "", answer); + } + }, onSuccess: () => { queryClient.invalidateQueries({ - queryKey: ['submissionHistory', competitionId, taskId] + queryKey: ['solutionHistory', competitionId, taskId] }); + setAnswer(""); + setSelectedFile(null); + }, + onError: (error) => { + console.error("Error submitting solution:", error); } }); @@ -45,8 +62,18 @@ const CompetitionSession = () => { } const handleSubmit = () => { - console.log(currentTask, competitionId, answer) - if (!currentTask || !competitionId || !answer.trim()) return; + if (!currentTask || !competitionId) return; + + if (currentTask.type === TaskType.FILE && !selectedFile) { + console.error("No file selected"); + return; + } + + if (currentTask.type !== TaskType.FILE && !answer.trim()) { + console.error("Answer is empty"); + return; + } + submitMutation.mutate(); }; @@ -77,6 +104,8 @@ const CompetitionSession = () => { solutions={[]} answer={answer} setAnswer={setAnswer} + selectedFile={selectedFile} + setSelectedFile={setSelectedFile} onSubmit={handleSubmit} /> diff --git a/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/index.tsx b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/index.tsx index d4b9c2c..c434666 100644 --- a/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/index.tsx +++ b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/index.tsx @@ -15,17 +15,20 @@ interface TaskSolutionProps { solutions: Solution[]; answer: string; setAnswer: (value: string) => void; + selectedFile: File | null; + setSelectedFile: (file: File | null) => void; onSubmit: () => void; } const TaskSolution: React.FC = ({ task, solutions = [], - answer, - setAnswer, - onSubmit, + answer, + setAnswer, + selectedFile, + setSelectedFile, + onSubmit, }) => { - const [selectedFile, setSelectedFile] = useState(null); const fileInputRef = useRef(null); const [isHistoryOpen, setIsHistoryOpen] = useState(false); const { id: competitionId } = useParams<{ id: string }>(); diff --git a/services/frontend/src/shared/api/session.ts b/services/frontend/src/shared/api/session.ts index a1dca15..6cfe8f2 100644 --- a/services/frontend/src/shared/api/session.ts +++ b/services/frontend/src/shared/api/session.ts @@ -27,7 +27,7 @@ export const submitTaskSolution = async ( }); } else { const formData = new FormData(); - formData.append('file', solution); + formData.append('content', solution); return await userFetch(endpoint, { method: 'POST', From 8accf88bb659af12e80d416846c9e6c11c828eac Mon Sep 17 00:00:00 2001 From: moolcoov Date: Sun, 2 Mar 2025 17:53:20 +0300 Subject: [PATCH 3/5] feat: review --- services/frontend/bun.lock | 3 ++ services/frontend/package.json | 1 + services/frontend/src/App.tsx | 4 +- .../frontend/src/components/ui/button.tsx | 2 +- services/frontend/src/main.tsx | 4 ++ .../pages/Review/components/review-card.tsx | 37 ++++++++++++++ services/frontend/src/pages/Review/index.tsx | 50 +++++++++++++++++-- .../src/pages/Review/modules/no-reviews.tsx | 13 +++++ .../src/pages/Review/modules/reviews-list.tsx | 21 ++++++++ services/frontend/src/shared/api/review.ts | 6 ++- services/frontend/src/shared/types/review.ts | 35 +++++++++++++ 11 files changed, 166 insertions(+), 10 deletions(-) create mode 100644 services/frontend/src/pages/Review/components/review-card.tsx create mode 100644 services/frontend/src/pages/Review/modules/no-reviews.tsx create mode 100644 services/frontend/src/pages/Review/modules/reviews-list.tsx diff --git a/services/frontend/bun.lock b/services/frontend/bun.lock index ecd048c..cfa6012 100644 --- a/services/frontend/bun.lock +++ b/services/frontend/bun.lock @@ -15,6 +15,7 @@ "autoprefixer": "^10.4.20", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "dayjs": "^1.11.13", "js-cookie": "^3.0.5", "katex": "^0.16.21", "lucide-react": "^0.476.0", @@ -392,6 +393,8 @@ "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + "dayjs": ["dayjs@1.11.13", "", {}, "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg=="], + "debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], "decode-named-character-reference": ["decode-named-character-reference@1.0.2", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg=="], diff --git a/services/frontend/package.json b/services/frontend/package.json index 9b45c78..e7ea22e 100644 --- a/services/frontend/package.json +++ b/services/frontend/package.json @@ -21,6 +21,7 @@ "autoprefixer": "^10.4.20", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "dayjs": "^1.11.13", "js-cookie": "^3.0.5", "katex": "^0.16.21", "lucide-react": "^0.476.0", diff --git a/services/frontend/src/App.tsx b/services/frontend/src/App.tsx index 9b3a5ae..7534997 100644 --- a/services/frontend/src/App.tsx +++ b/services/frontend/src/App.tsx @@ -42,9 +42,9 @@ const App = () => { /> } /> - - } /> + + } /> ); diff --git a/services/frontend/src/components/ui/button.tsx b/services/frontend/src/components/ui/button.tsx index 11b8b06..bc84fe8 100644 --- a/services/frontend/src/components/ui/button.tsx +++ b/services/frontend/src/components/ui/button.tsx @@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "@/shared/lib/utils"; const buttonVariants = cva( - "inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[color,box-shadow] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + "inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[color,box-shadow,scale] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive not-disabled:active:scale-[0.95] ", { variants: { variant: { diff --git a/services/frontend/src/main.tsx b/services/frontend/src/main.tsx index f046499..3ba38f1 100644 --- a/services/frontend/src/main.tsx +++ b/services/frontend/src/main.tsx @@ -2,6 +2,10 @@ import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; import { BrowserRouter } from "react-router"; import App from "./App.tsx"; +import dayjs from "dayjs"; + +import "dayjs/locale/ru"; +dayjs.locale("ru"); createRoot(document.getElementById("root")!).render( diff --git a/services/frontend/src/pages/Review/components/review-card.tsx b/services/frontend/src/pages/Review/components/review-card.tsx new file mode 100644 index 0000000..75003f8 --- /dev/null +++ b/services/frontend/src/pages/Review/components/review-card.tsx @@ -0,0 +1,37 @@ +import { Review, ReviewStatus } from "@/shared/types/review"; +import dayjs from "dayjs"; + +interface ReviewCardProps { + review: Review; +} + +export const ReviewCard = ({ review }: ReviewCardProps) => { + const id = review.id.split("-").at(-1)?.slice(0, 6); + + return ( +
+
+

+ {review.competition_name} +

+

{review.task_title}

+
+
+
+

{id}

+

+

+ {review.review_status === ReviewStatus.NOT_CHECKED + ? `Дата посылки: ${dayjs(review.submitted_at).format("D MMMM, HH:mm")}` + : `Дата проверки: ${review.checked_at}`} +

+
+

+ {review.review_status === ReviewStatus.NOT_CHECKED + ? "Не проверено" + : ""} +

+
+
+ ); +}; diff --git a/services/frontend/src/pages/Review/index.tsx b/services/frontend/src/pages/Review/index.tsx index 4d21663..0701e14 100644 --- a/services/frontend/src/pages/Review/index.tsx +++ b/services/frontend/src/pages/Review/index.tsx @@ -1,9 +1,12 @@ -import { Loading } from "@/components/ui/Loading"; +import { Loading } from "@/components/ui/loading"; import { getReviewer, getReviewerSubmissions } from "@/shared/api/review"; import { useQuery } from "@tanstack/react-query"; import { useNavigate, useParams } from "react-router"; import { ReviewHeader } from "./modules/review-header"; -import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { ReviewsList } from "./modules/reviews-list"; +import React from "react"; +import { ReviewStatus } from "@/shared/types/review"; const ReviewPage = () => { const { token } = useParams<{ token: string }>(); @@ -20,6 +23,22 @@ const ReviewPage = () => { retry: 0, }); + const availableReviews = React.useMemo( + () => + (submissionsQuery.data?.submissions || []).filter( + (s) => s.review_status === ReviewStatus.NOT_CHECKED, + ), + [submissionsQuery.data], + ); + + const checkedReviews = React.useMemo( + () => + (submissionsQuery.data?.submissions || []).filter( + (s) => s.review_status === ReviewStatus.CHECKED, + ), + [submissionsQuery.data], + ); + if (reviewerQuery.isLoading || submissionsQuery.isLoading) { return ; } @@ -34,14 +53,35 @@ const ReviewPage = () => {
- +
-

Посылки

+

Решения

- Доступные + + Доступные + {availableReviews.length > 0 && ( +
+ {availableReviews.length} +
+ )} +
Проверенные
+ + + + + + + +
diff --git a/services/frontend/src/pages/Review/modules/no-reviews.tsx b/services/frontend/src/pages/Review/modules/no-reviews.tsx new file mode 100644 index 0000000..f82ec5b --- /dev/null +++ b/services/frontend/src/pages/Review/modules/no-reviews.tsx @@ -0,0 +1,13 @@ +import { Check } from "lucide-react"; + +export const NoReviews = () => { + return ( +
+ +
+

Посылок пока нет

+

Можете расслабиться

+
+
+ ); +}; diff --git a/services/frontend/src/pages/Review/modules/reviews-list.tsx b/services/frontend/src/pages/Review/modules/reviews-list.tsx new file mode 100644 index 0000000..0715a77 --- /dev/null +++ b/services/frontend/src/pages/Review/modules/reviews-list.tsx @@ -0,0 +1,21 @@ +import { Review } from "@/shared/types/review"; +import { ReviewCard } from "../components/review-card"; +import { NoReviews } from "./no-reviews"; + +interface ReviewsListProp { + reviews: Review[]; +} + +export const ReviewsList = ({ reviews }: ReviewsListProp) => { + if (reviews.length === 0) { + return ; + } + + return ( +
+ {reviews.map((review) => ( + + ))} +
+ ); +}; diff --git a/services/frontend/src/shared/api/review.ts b/services/frontend/src/shared/api/review.ts index 887999b..1acc8d0 100644 --- a/services/frontend/src/shared/api/review.ts +++ b/services/frontend/src/shared/api/review.ts @@ -1,10 +1,12 @@ import { apiFetch } from "."; -import { Reviewer } from "../types/review"; +import { Review, Reviewer } from "../types/review"; export const getReviewer = async (token: string) => { return await apiFetch(`/review/${token}`); }; export const getReviewerSubmissions = async (token: string) => { - return await apiFetch(`/review/${token}/submissions`); + return await apiFetch<{ submissions: Review[] }>( + `/review/${token}/submissions`, + ); }; diff --git a/services/frontend/src/shared/types/review.ts b/services/frontend/src/shared/types/review.ts index f3a9094..47e0a14 100644 --- a/services/frontend/src/shared/types/review.ts +++ b/services/frontend/src/shared/types/review.ts @@ -3,3 +3,38 @@ export interface Reviewer { name: string; surname: string; } + +export interface Review { + id: string; + review_status: ReviewStatus; + evaluation?: ReviewEvaluation[]; + criteries?: ReviewCriteria[]; + submitted_at: Date; + competition: string; + competition_name: string; + task: string; + content: string; + stdout?: string; + result?: {}; + earned_points?: number; + checked_at?: Date; + task_title: string; +} + +export enum ReviewStatus { + NOT_CHECKED = "not_checked", + CHECKED = "checked", + CHECKING = "checking", +} + +export interface ReviewEvaluation { + slug: string; + mark: number; +} + +export interface ReviewCriteria { + name: string; + slug: string; + max_value: number; + min_value: number; +} From fc17430ff4d68a88a5266479797b96506bd54d85 Mon Sep 17 00:00:00 2001 From: rngsurrounded Date: Mon, 3 Mar 2025 00:57:19 +0900 Subject: [PATCH 4/5] minor fixes --- services/frontend/src/pages/CompetitionSession/index.tsx | 1 - .../pages/CompetitionSession/modules/TaskSolution/index.tsx | 4 +--- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/services/frontend/src/pages/CompetitionSession/index.tsx b/services/frontend/src/pages/CompetitionSession/index.tsx index 2129c52..09fc40c 100644 --- a/services/frontend/src/pages/CompetitionSession/index.tsx +++ b/services/frontend/src/pages/CompetitionSession/index.tsx @@ -101,7 +101,6 @@ const CompetitionSession = () => { void; selectedFile: File | null; @@ -22,7 +21,6 @@ interface TaskSolutionProps { const TaskSolution: React.FC = ({ task, - solutions = [], answer, setAnswer, selectedFile, @@ -45,7 +43,7 @@ const TaskSolution: React.FC = ({ setIsHistoryOpen(true); }; - const latestSolution = solutions && solutions.length > 0 ? solutions[0] : null; + const latestSolution = solutionHistory && solutionHistory.length > 0 ? solutionHistory[0] : null; return (
From e76d1e8a6e9817cfc25afcc8e9d107621f567739 Mon Sep 17 00:00:00 2001 From: rngsurrounded Date: Mon, 3 Mar 2025 01:06:25 +0900 Subject: [PATCH 5/5] minor fixes --- services/frontend/bun.lock | 3 ++ services/frontend/package.json | 1 + .../components/SolutionStatus/index.tsx | 54 ++++++++++--------- 3 files changed, 32 insertions(+), 26 deletions(-) diff --git a/services/frontend/bun.lock b/services/frontend/bun.lock index cfa6012..02e8791 100644 --- a/services/frontend/bun.lock +++ b/services/frontend/bun.lock @@ -15,6 +15,7 @@ "autoprefixer": "^10.4.20", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "date-fns": "^4.1.0", "dayjs": "^1.11.13", "js-cookie": "^3.0.5", "katex": "^0.16.21", @@ -393,6 +394,8 @@ "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + "date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="], + "dayjs": ["dayjs@1.11.13", "", {}, "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg=="], "debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], diff --git a/services/frontend/package.json b/services/frontend/package.json index e7ea22e..6a2fce5 100644 --- a/services/frontend/package.json +++ b/services/frontend/package.json @@ -21,6 +21,7 @@ "autoprefixer": "^10.4.20", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "date-fns": "^4.1.0", "dayjs": "^1.11.13", "js-cookie": "^3.0.5", "katex": "^0.16.21", diff --git a/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/SolutionStatus/index.tsx b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/SolutionStatus/index.tsx index 33a412a..b67c5ac 100644 --- a/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/SolutionStatus/index.tsx +++ b/services/frontend/src/pages/CompetitionSession/modules/TaskSolution/components/SolutionStatus/index.tsx @@ -1,29 +1,31 @@ -import React from 'react'; -import { Solution } from '@/shared/types/task'; -import { getSolutionBgColor, getSolutionTextColor, getStatusText } from '@/pages/CompetitionSession/utils/utils'; + import React from 'react'; + import { Solution } from '@/shared/types/task'; + import { getSolutionBgColor, getSolutionTextColor, getStatusText } from '@/pages/CompetitionSession/utils/utils'; + import { format, parseISO } from 'date-fns'; + import { ru } from 'date-fns/locale'; + + interface SolutionStatusProps { + solution: Solution; + maxPoints: number; + } -interface SolutionStatusProps { - solution: Solution; - maxPoints: number; -} - -const SolutionStatus: React.FC = ({ solution, maxPoints }) => { - - return ( -
-
- - Решение {solution.id} - - - {getStatusText(solution.status, solution.earned_points, maxPoints)} - + const SolutionStatus: React.FC = ({ solution, maxPoints }) => { + const formattedDate = solution.timestamp ? format(parseISO(solution.timestamp), "d MMMM, HH:mm", { locale: ru }) : ''; + return ( +
+
+ + Решение {solution.id} + + + {getStatusText(solution.status, solution.earned_points, maxPoints)} + +
+
+ {formattedDate} +
-
- {solution.timestamp} -
-
- ); -}; + ); + }; -export default SolutionStatus; + export default SolutionStatus;