feat(tests): общие тесты с автором, в истории только результаты ученика (для всех ролей)
Made-with: Cursor
This commit is contained in:
+26
@@ -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;
|
||||||
@@ -26,7 +26,9 @@ model User {
|
|||||||
questions Question[]
|
questions Question[]
|
||||||
chatMessages ChatMessage[]
|
chatMessages ChatMessage[]
|
||||||
textbooks Textbook[]
|
textbooks Textbook[]
|
||||||
tests Test[]
|
testsInScope Test[] @relation("TestStudentScope")
|
||||||
|
testsAuthored Test[] @relation("TestAuthor")
|
||||||
|
testResults TestResult[]
|
||||||
reports Report[]
|
reports Report[]
|
||||||
hallPhotos HallPhoto[]
|
hallPhotos HallPhoto[]
|
||||||
}
|
}
|
||||||
@@ -75,8 +77,11 @@ model Textbook {
|
|||||||
|
|
||||||
model Test {
|
model Test {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
|
/// Контекст пары наставник–ученик (id назначенного ученика); общие тесты для обоих
|
||||||
studentId Int
|
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
|
topic String
|
||||||
questions String
|
questions String
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
@@ -86,11 +91,13 @@ model Test {
|
|||||||
model TestResult {
|
model TestResult {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
testId Int
|
testId Int
|
||||||
|
test Test @relation(fields: [testId], references: [id], onDelete: Cascade)
|
||||||
|
studentId Int
|
||||||
|
student User @relation(fields: [studentId], references: [id], onDelete: Cascade)
|
||||||
answers String
|
answers String
|
||||||
score Int
|
score Int
|
||||||
total Int
|
total Int
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
test Test @relation(fields: [testId], references: [id], onDelete: Cascade)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
model Report {
|
model Report {
|
||||||
|
|||||||
+33
-14
@@ -17,27 +17,41 @@ const DEFAULT_TEST_PROMPT = `Ты — составитель тестов. Сг
|
|||||||
ВАЖНО: Верни ТОЛЬКО JSON-массив, без markdown-обёрток (\`\`\`json), без пояснений, ЧИСТЫЙ JSON:
|
ВАЖНО: Верни ТОЛЬКО JSON-массив, без markdown-обёрток (\`\`\`json), без пояснений, ЧИСТЫЙ JSON:
|
||||||
[{"question":"текст вопроса","options":{"a":"вариант А","b":"вариант Б","c":"вариант В","d":"вариант Г"},"correct":"a"}]`;
|
[{"question":"текст вопроса","options":{"a":"вариант А","b":"вариант Б","c":"вариант В","d":"вариант Г"},"correct":"a"}]`;
|
||||||
|
|
||||||
/** Тесты привязаны к текущему пользователю (ученик — свои; наставник — свои черновые, не к ученику). */
|
/** Контекст пары: id назначенного ученика (общие тесты для наставника и ученика). */
|
||||||
function testOwnerId(req: AuthRequest): number {
|
function pairStudentScopeId(req: AuthRequest): number {
|
||||||
return req.user!.id;
|
return req.studentId!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const authorSelect = { username: true, displayName: true } as const;
|
||||||
|
|
||||||
router.get("/", async (req: AuthRequest, res: Response) => {
|
router.get("/", async (req: AuthRequest, res: Response) => {
|
||||||
const ownerId = testOwnerId(req);
|
const scopeId = pairStudentScopeId(req);
|
||||||
const tests = await prisma.test.findMany({
|
const tests = await prisma.test.findMany({
|
||||||
where: { studentId: ownerId },
|
where: { studentId: scopeId },
|
||||||
orderBy: { createdAt: "desc" },
|
orderBy: { createdAt: "desc" },
|
||||||
include: { results: true },
|
include: {
|
||||||
|
author: { select: authorSelect },
|
||||||
|
results: {
|
||||||
|
where: { studentId: scopeId },
|
||||||
|
orderBy: { createdAt: "asc" },
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
res.json(tests);
|
res.json(tests);
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get("/:id", async (req: AuthRequest, res: Response) => {
|
router.get("/:id", async (req: AuthRequest, res: Response) => {
|
||||||
const id = parseInt(req.params.id as string, 10);
|
const id = parseInt(req.params.id as string, 10);
|
||||||
const ownerId = testOwnerId(req);
|
const scopeId = pairStudentScopeId(req);
|
||||||
const test = await prisma.test.findFirst({
|
const test = await prisma.test.findFirst({
|
||||||
where: { id, studentId: ownerId },
|
where: { id, studentId: scopeId },
|
||||||
include: { results: true },
|
include: {
|
||||||
|
author: { select: authorSelect },
|
||||||
|
results: {
|
||||||
|
where: { studentId: scopeId },
|
||||||
|
orderBy: { createdAt: "asc" },
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!test) {
|
if (!test) {
|
||||||
@@ -50,8 +64,7 @@ router.get("/:id", async (req: AuthRequest, res: Response) => {
|
|||||||
|
|
||||||
router.post("/generate", async (req: AuthRequest, res: Response) => {
|
router.post("/generate", async (req: AuthRequest, res: Response) => {
|
||||||
const { topic, fromQuestions } = req.body;
|
const { topic, fromQuestions } = req.body;
|
||||||
const ownerId = testOwnerId(req);
|
const scopeId = pairStudentScopeId(req);
|
||||||
/** Вопросы «по моим вопросам» берутся из базы назначенного ученика (для наставника — его ученик). */
|
|
||||||
const questionBankStudentId = req.studentId!;
|
const questionBankStudentId = req.studentId!;
|
||||||
|
|
||||||
const client = await getDeepSeekClient();
|
const client = await getDeepSeekClient();
|
||||||
@@ -92,7 +105,12 @@ router.post("/generate", async (req: AuthRequest, res: Response) => {
|
|||||||
data: {
|
data: {
|
||||||
topic: topic || "По прошлым вопросам",
|
topic: topic || "По прошлым вопросам",
|
||||||
questions: questionsJson,
|
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) => {
|
router.post("/:id/submit", async (req: AuthRequest, res: Response) => {
|
||||||
const id = parseInt(req.params.id as string, 10);
|
const id = parseInt(req.params.id as string, 10);
|
||||||
const ownerId = testOwnerId(req);
|
const scopeId = pairStudentScopeId(req);
|
||||||
const { answers } = req.body as { answers: Record<string, string> };
|
const { answers } = req.body as { answers: Record<string, string> };
|
||||||
|
|
||||||
const test = await prisma.test.findFirst({ where: { id, studentId: ownerId } });
|
const test = await prisma.test.findFirst({ where: { id, studentId: scopeId } });
|
||||||
if (!test) {
|
if (!test) {
|
||||||
res.status(404).json({ error: "Test not found" });
|
res.status(404).json({ error: "Test not found" });
|
||||||
return;
|
return;
|
||||||
@@ -131,6 +149,7 @@ router.post("/:id/submit", async (req: AuthRequest, res: Response) => {
|
|||||||
const result = await prisma.testResult.create({
|
const result = await prisma.testResult.create({
|
||||||
data: {
|
data: {
|
||||||
testId: id,
|
testId: id,
|
||||||
|
studentId: req.user!.id,
|
||||||
answers: JSON.stringify(answers),
|
answers: JSON.stringify(answers),
|
||||||
score,
|
score,
|
||||||
total,
|
total,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ interface Test {
|
|||||||
questions: string;
|
questions: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
results: TestResult[];
|
results: TestResult[];
|
||||||
|
author?: { username: string; displayName: string | null };
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TestResult {
|
interface TestResult {
|
||||||
@@ -248,7 +249,9 @@ export default function TestPage() {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-xl font-semibold tracking-tight">Тестирование</h1>
|
<h1 className="text-xl font-semibold tracking-tight">Тестирование</h1>
|
||||||
<p className="text-sm text-muted-foreground mt-1">Проверьте знания — 10 вопросов с вариантами ответов</p>
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
Проверьте знания — 10 вопросов с вариантами ответов. Тесты общие для пары; в истории — только попытки ученика (видны всем).
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
@@ -298,8 +301,13 @@ export default function TestPage() {
|
|||||||
</h2>
|
</h2>
|
||||||
{tests.map((t) => (
|
{tests.map((t) => (
|
||||||
<div key={t.id} className="flex items-center justify-between p-3.5 rounded-xl border text-sm">
|
<div key={t.id} className="flex items-center justify-between p-3.5 rounded-xl border text-sm">
|
||||||
<div>
|
<div className="min-w-0">
|
||||||
<span className="font-medium">{t.topic}</span>
|
<span className="font-medium">{t.topic}</span>
|
||||||
|
{t.author && (
|
||||||
|
<span className="text-xs text-muted-foreground ml-2">
|
||||||
|
· автор: {t.author.displayName?.trim() || t.author.username}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
<span className="text-xs text-muted-foreground ml-2">
|
<span className="text-xs text-muted-foreground ml-2">
|
||||||
{new Date(t.createdAt).toLocaleDateString("ru")}
|
{new Date(t.createdAt).toLocaleDateString("ru")}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user