fix(cbt): comprehensive overhaul of CBT & DPP test pipelines, JSON schemas, active states, and exam pattern filters
This commit is contained in:
@@ -14,23 +14,88 @@ import { z } from "zod"
|
||||
|
||||
import { useActiveUser } from "@/hooks/useActiveUser"
|
||||
|
||||
// Strict Zod schema for client-side validation
|
||||
const QuestionSchema = z.object({
|
||||
// Permissive, auto-normalizing Zod schema for client-side question parsing
|
||||
const QuestionSchema = z.preprocess((val: any) => {
|
||||
if (typeof val !== "object" || val === null) return val;
|
||||
const q = { ...val };
|
||||
|
||||
// Normalize question text
|
||||
if (!q.question_text && q.question) {
|
||||
q.question_text = q.question;
|
||||
}
|
||||
|
||||
// Normalize solution text
|
||||
if (!q.solution_text && (q.solution || q.explanation)) {
|
||||
q.solution_text = q.solution || q.explanation;
|
||||
}
|
||||
if (!q.solution_text) {
|
||||
q.solution_text = "Detailed solution will be available after review.";
|
||||
}
|
||||
|
||||
// Normalize topic / subtopic / subject / difficulty
|
||||
if (!q.subject) q.subject = "General";
|
||||
if (!q.topic) q.topic = q.subject || "General";
|
||||
if (!q.subtopic) q.subtopic = q.topic || "General";
|
||||
if (!q.difficulty) q.difficulty = "Medium";
|
||||
if (!q.section) q.section = q.subject || "General";
|
||||
|
||||
// Normalize question type
|
||||
const typeStr = String(q.type || "").trim();
|
||||
if (["Single_Choice", "MCQ", "Single", "MCQ_Single"].includes(typeStr)) {
|
||||
q.type = "MCQ_Single";
|
||||
} else if (["Multiple_Choice", "Multiple", "MCQ_Multiple"].includes(typeStr)) {
|
||||
q.type = "MCQ_Multiple";
|
||||
} else if (["Numerical", "Subjective", "Integer"].includes(typeStr)) {
|
||||
q.type = "Numerical";
|
||||
} else {
|
||||
q.type = "MCQ_Single";
|
||||
}
|
||||
|
||||
// Normalize language
|
||||
const langStr = String(q.language || "").trim().toLowerCase();
|
||||
if (langStr === "hindi") q.language = "Hindi";
|
||||
else if (langStr === "bilingual") q.language = "Bilingual";
|
||||
else q.language = "English";
|
||||
|
||||
// Normalize options if passed as an array
|
||||
if (Array.isArray(q.options)) {
|
||||
const optsObj: Record<string, string> = {};
|
||||
q.options.forEach((opt: any, idx: number) => {
|
||||
const letter = String.fromCharCode(65 + idx);
|
||||
optsObj[letter] = String(opt);
|
||||
});
|
||||
q.options = optsObj;
|
||||
}
|
||||
|
||||
// Normalize correct_answer format
|
||||
if (typeof q.correct_answer === "string" || typeof q.correct_answer === "number") {
|
||||
q.correct_answer = { value: String(q.correct_answer) };
|
||||
} else if (Array.isArray(q.correct_answer)) {
|
||||
q.correct_answer = { value: q.correct_answer.map(String) };
|
||||
} else if (typeof q.correct_answer === "object" && q.correct_answer !== null) {
|
||||
const val = q.correct_answer.value ?? q.correct_answer.correct ?? q.correct_answer.answer ?? "A";
|
||||
q.correct_answer = { value: val };
|
||||
} else {
|
||||
q.correct_answer = { value: "A" };
|
||||
}
|
||||
|
||||
return q;
|
||||
}, z.object({
|
||||
id: z.string().optional(),
|
||||
section: z.string().default("General"),
|
||||
subject: z.string(),
|
||||
topic: z.string(),
|
||||
subtopic: z.string(),
|
||||
difficulty: z.string(),
|
||||
subject: z.string().default("General"),
|
||||
topic: z.string().default("General"),
|
||||
subtopic: z.string().default("General"),
|
||||
difficulty: z.string().default("Medium"),
|
||||
type: z.enum(["MCQ_Single", "MCQ_Multiple", "Numerical"]),
|
||||
question_text: z.string(),
|
||||
question_text: z.string().min(1, "Question text is required"),
|
||||
options: z.record(z.string(), z.any()).nullable().optional(),
|
||||
correct_answer: z.object({
|
||||
value: z.any()
|
||||
}),
|
||||
solution_text: z.string(),
|
||||
language: z.enum(["English", "Hindi", "Bilingual"])
|
||||
}).passthrough();
|
||||
}).passthrough());
|
||||
|
||||
const TestSchema = z.array(QuestionSchema);
|
||||
|
||||
@@ -173,13 +238,14 @@ export default function AdminTestsPage() {
|
||||
const handleUpload = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
const effectiveAdminId = userId && userId !== "admin" ? userId : "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
|
||||
|
||||
if (testType === 'DPP_PDF') {
|
||||
const file = fileInputRef.current?.files?.[0]
|
||||
if (!file) {
|
||||
alert("Please select a PDF file to upload to Minio.")
|
||||
return
|
||||
}
|
||||
if (!userId) return
|
||||
setUploading(true)
|
||||
try {
|
||||
const formData = new FormData()
|
||||
@@ -187,7 +253,7 @@ export default function AdminTestsPage() {
|
||||
formData.append("title", title)
|
||||
formData.append("exam_pattern", examPattern)
|
||||
formData.append("time_limit", timeLimit)
|
||||
formData.append("admin_id", userId === "admin" ? "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" : (userId as string))
|
||||
formData.append("admin_id", effectiveAdminId)
|
||||
if (scheduledDate) formData.append("scheduled_date", scheduledDate.toISOString())
|
||||
|
||||
const res = await fetchWithAuth("/api/admin/tests/upload", {
|
||||
@@ -215,7 +281,6 @@ export default function AdminTestsPage() {
|
||||
alert("Please upload a valid JSON file with questions first.")
|
||||
return
|
||||
}
|
||||
if (!userId) return
|
||||
|
||||
setUploading(true)
|
||||
|
||||
@@ -224,7 +289,7 @@ export default function AdminTestsPage() {
|
||||
type: testType,
|
||||
exam_pattern: examPattern,
|
||||
time_limit: parseInt(timeLimit),
|
||||
admin_id: userId === "admin" ? "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" : userId,
|
||||
admin_id: effectiveAdminId,
|
||||
questions_json: validQuestions,
|
||||
scheduled_date: scheduledDate ? scheduledDate.toISOString() : null,
|
||||
end_date: endDate ? endDate.toISOString() : null,
|
||||
@@ -449,7 +514,7 @@ export default function AdminTestsPage() {
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="font-semibold">{test.title}</h3>
|
||||
<Badge variant="outline">{test.exam_pattern.replace("_", " ")}</Badge>
|
||||
<Badge variant="outline">{test.exam_pattern?.replace("_", " ") || "All"}</Badge>
|
||||
<Badge variant={test.is_active ? "default" : "secondary"}>
|
||||
{test.is_active ? "Active" : "Inactive / Processing"}
|
||||
</Badge>
|
||||
|
||||
Reference in New Issue
Block a user