From 35f27a6eb72f87fdbadcb63fea37296f13e21e58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D0=BE=D0=BD=D1=81=D1=82=D0=B0=D0=BD=D1=82=D0=B8?= =?UTF-8?q?=D0=BD=20=D0=9B=D0=B5=D0=B1=D0=B5=D0=B4=D0=B8=D0=BD=D1=81=D0=BA?= =?UTF-8?q?=D0=B8=D0=B9?= Date: Wed, 1 Apr 2026 23:09:17 +0500 Subject: [PATCH] =?UTF-8?q?feat(tests):=20=D0=BE=D0=B1=D1=89=D0=B8=D0=B5?= =?UTF-8?q?=20=D1=82=D0=B5=D1=81=D1=82=D1=8B=20=D1=81=20=D0=B0=D0=B2=D1=82?= =?UTF-8?q?=D0=BE=D1=80=D0=BE=D0=BC,=20=D0=B2=20=D0=B8=D1=81=D1=82=D0=BE?= =?UTF-8?q?=D1=80=D0=B8=D0=B8=20=D1=82=D0=BE=D0=BB=D1=8C=D0=BA=D0=BE=20?= =?UTF-8?q?=D1=80=D0=B5=D0=B7=D1=83=D0=BB=D1=8C=D1=82=D0=B0=D1=82=D1=8B=20?= =?UTF-8?q?=D1=83=D1=87=D0=B5=D0=BD=D0=B8=D0=BA=D0=B0=20(=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20=D0=B2=D1=81=D0=B5=D1=85=20=D1=80=D0=BE=D0=BB=D0=B5?= =?UTF-8?q?=D0=B9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Made-with: Cursor --- .../migration.sql | 26 ++++++++++ backend/prisma/schema.prisma | 13 +++-- backend/src/routes/tests.ts | 47 +++++++++++++------ frontend/src/pages/TestPage.tsx | 12 ++++- 4 files changed, 79 insertions(+), 19 deletions(-) create mode 100644 backend/prisma/migrations/20260402180000_tests_shared_author_student_results/migration.sql diff --git a/backend/prisma/migrations/20260402180000_tests_shared_author_student_results/migration.sql b/backend/prisma/migrations/20260402180000_tests_shared_author_student_results/migration.sql new file mode 100644 index 0000000..756ceba --- /dev/null +++ b/backend/prisma/migrations/20260402180000_tests_shared_author_student_results/migration.sql @@ -0,0 +1,26 @@ +-- Общие тесты в контексте пары: studentId = id назначенного ученика; authorId = кто создал. +-- Результаты (TestResult) привязаны к ученику (studentId). + +ALTER TABLE "Test" ADD COLUMN "authorId" INTEGER; + +UPDATE "Test" SET "authorId" = "studentId"; + +UPDATE "Test" AS t +SET "studentId" = ta."studentId" +FROM "TutorAssignment" AS ta +WHERE t."authorId" = ta."tutorId"; + +ALTER TABLE "Test" ALTER COLUMN "authorId" SET NOT NULL; + +ALTER TABLE "Test" ADD CONSTRAINT "Test_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +ALTER TABLE "TestResult" ADD COLUMN "studentId" INTEGER; + +UPDATE "TestResult" AS tr +SET "studentId" = t."studentId" +FROM "Test" AS t +WHERE tr."testId" = t."id"; + +ALTER TABLE "TestResult" ALTER COLUMN "studentId" SET NOT NULL; + +ALTER TABLE "TestResult" ADD CONSTRAINT "TestResult_studentId_fkey" FOREIGN KEY ("studentId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 3b940f8..7cc0085 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -26,7 +26,9 @@ model User { questions Question[] chatMessages ChatMessage[] textbooks Textbook[] - tests Test[] + testsInScope Test[] @relation("TestStudentScope") + testsAuthored Test[] @relation("TestAuthor") + testResults TestResult[] reports Report[] hallPhotos HallPhoto[] } @@ -75,8 +77,11 @@ model Textbook { model Test { id Int @id @default(autoincrement()) + /// Контекст пары наставник–ученик (id назначенного ученика); общие тесты для обоих studentId Int - student User @relation(fields: [studentId], references: [id], onDelete: Cascade) + student User @relation("TestStudentScope", fields: [studentId], references: [id], onDelete: Cascade) + authorId Int + author User @relation("TestAuthor", fields: [authorId], references: [id], onDelete: Cascade) topic String questions String createdAt DateTime @default(now()) @@ -86,11 +91,13 @@ model Test { model TestResult { id Int @id @default(autoincrement()) testId Int + test Test @relation(fields: [testId], references: [id], onDelete: Cascade) + studentId Int + student User @relation(fields: [studentId], references: [id], onDelete: Cascade) answers String score Int total Int createdAt DateTime @default(now()) - test Test @relation(fields: [testId], references: [id], onDelete: Cascade) } model Report { diff --git a/backend/src/routes/tests.ts b/backend/src/routes/tests.ts index 3675847..2e6e569 100644 --- a/backend/src/routes/tests.ts +++ b/backend/src/routes/tests.ts @@ -17,27 +17,41 @@ const DEFAULT_TEST_PROMPT = `Ты — составитель тестов. Сг ВАЖНО: Верни ТОЛЬКО JSON-массив, без markdown-обёрток (\`\`\`json), без пояснений, ЧИСТЫЙ JSON: [{"question":"текст вопроса","options":{"a":"вариант А","b":"вариант Б","c":"вариант В","d":"вариант Г"},"correct":"a"}]`; -/** Тесты привязаны к текущему пользователю (ученик — свои; наставник — свои черновые, не к ученику). */ -function testOwnerId(req: AuthRequest): number { - return req.user!.id; +/** Контекст пары: id назначенного ученика (общие тесты для наставника и ученика). */ +function pairStudentScopeId(req: AuthRequest): number { + return req.studentId!; } +const authorSelect = { username: true, displayName: true } as const; + router.get("/", async (req: AuthRequest, res: Response) => { - const ownerId = testOwnerId(req); + const scopeId = pairStudentScopeId(req); const tests = await prisma.test.findMany({ - where: { studentId: ownerId }, + where: { studentId: scopeId }, orderBy: { createdAt: "desc" }, - include: { results: true }, + include: { + author: { select: authorSelect }, + results: { + where: { studentId: scopeId }, + orderBy: { createdAt: "asc" }, + }, + }, }); res.json(tests); }); router.get("/:id", async (req: AuthRequest, res: Response) => { const id = parseInt(req.params.id as string, 10); - const ownerId = testOwnerId(req); + const scopeId = pairStudentScopeId(req); const test = await prisma.test.findFirst({ - where: { id, studentId: ownerId }, - include: { results: true }, + where: { id, studentId: scopeId }, + include: { + author: { select: authorSelect }, + results: { + where: { studentId: scopeId }, + orderBy: { createdAt: "asc" }, + }, + }, }); if (!test) { @@ -50,8 +64,7 @@ router.get("/:id", async (req: AuthRequest, res: Response) => { router.post("/generate", async (req: AuthRequest, res: Response) => { const { topic, fromQuestions } = req.body; - const ownerId = testOwnerId(req); - /** Вопросы «по моим вопросам» берутся из базы назначенного ученика (для наставника — его ученик). */ + const scopeId = pairStudentScopeId(req); const questionBankStudentId = req.studentId!; const client = await getDeepSeekClient(); @@ -92,7 +105,12 @@ router.post("/generate", async (req: AuthRequest, res: Response) => { data: { topic: topic || "По прошлым вопросам", questions: questionsJson, - studentId: ownerId, + studentId: scopeId, + authorId: req.user!.id, + }, + include: { + author: { select: authorSelect }, + results: true, }, }); @@ -104,10 +122,10 @@ router.post("/generate", async (req: AuthRequest, res: Response) => { router.post("/:id/submit", async (req: AuthRequest, res: Response) => { const id = parseInt(req.params.id as string, 10); - const ownerId = testOwnerId(req); + const scopeId = pairStudentScopeId(req); const { answers } = req.body as { answers: Record }; - const test = await prisma.test.findFirst({ where: { id, studentId: ownerId } }); + const test = await prisma.test.findFirst({ where: { id, studentId: scopeId } }); if (!test) { res.status(404).json({ error: "Test not found" }); return; @@ -131,6 +149,7 @@ router.post("/:id/submit", async (req: AuthRequest, res: Response) => { const result = await prisma.testResult.create({ data: { testId: id, + studentId: req.user!.id, answers: JSON.stringify(answers), score, total, diff --git a/frontend/src/pages/TestPage.tsx b/frontend/src/pages/TestPage.tsx index 83d976f..a0bf2d0 100644 --- a/frontend/src/pages/TestPage.tsx +++ b/frontend/src/pages/TestPage.tsx @@ -17,6 +17,7 @@ interface Test { questions: string; createdAt: string; results: TestResult[]; + author?: { username: string; displayName: string | null }; } interface TestResult { @@ -248,7 +249,9 @@ export default function TestPage() {

Тестирование

-

Проверьте знания — 10 вопросов с вариантами ответов

+

+ Проверьте знания — 10 вопросов с вариантами ответов. Тесты общие для пары; в истории — только попытки ученика (видны всем). +

@@ -298,8 +301,13 @@ export default function TestPage() { {tests.map((t) => (
-
+
{t.topic} + {t.author && ( + + · автор: {t.author.displayName?.trim() || t.author.username} + + )} {new Date(t.createdAt).toLocaleDateString("ru")}