@studious-lms/server 1.12.0 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/models/class.d.ts +10 -0
- package/dist/models/class.d.ts.map +1 -1
- package/dist/models/class.js +9 -3
- package/dist/models/class.js.map +1 -1
- package/dist/models/comment.d.ts +7 -0
- package/dist/models/comment.d.ts.map +1 -1
- package/dist/models/comment.js +3 -2
- package/dist/models/comment.js.map +1 -1
- package/dist/models/conversation.d.ts +17 -0
- package/dist/models/conversation.d.ts.map +1 -1
- package/dist/models/conversation.js +13 -2
- package/dist/models/conversation.js.map +1 -1
- package/dist/models/worksheet.d.ts +4 -0
- package/dist/models/worksheet.d.ts.map +1 -1
- package/dist/models/worksheet.js +13 -7
- package/dist/models/worksheet.js.map +1 -1
- package/dist/pipelines/gradeWorksheet.d.ts.map +1 -1
- package/dist/pipelines/gradeWorksheet.js +59 -14
- package/dist/pipelines/gradeWorksheet.js.map +1 -1
- package/dist/routers/_app.d.ts +124 -0
- package/dist/routers/_app.d.ts.map +1 -1
- package/dist/routers/announcement.d.ts +4 -0
- package/dist/routers/announcement.d.ts.map +1 -1
- package/dist/routers/announcement.js +6 -5
- package/dist/routers/announcement.js.map +1 -1
- package/dist/routers/assignment.d.ts +8 -0
- package/dist/routers/assignment.d.ts.map +1 -1
- package/dist/routers/assignment.js +4 -4
- package/dist/routers/assignment.js.map +1 -1
- package/dist/routers/class.d.ts +23 -0
- package/dist/routers/class.d.ts.map +1 -1
- package/dist/routers/comment.d.ts +24 -0
- package/dist/routers/comment.d.ts.map +1 -1
- package/dist/routers/comment.js +6 -3
- package/dist/routers/comment.js.map +1 -1
- package/dist/routers/worksheet.d.ts +3 -0
- package/dist/routers/worksheet.d.ts.map +1 -1
- package/dist/routers/worksheet.js +4 -2
- package/dist/routers/worksheet.js.map +1 -1
- package/dist/services/assignment.d.ts.map +1 -1
- package/dist/services/assignment.js +10 -8
- package/dist/services/assignment.js.map +1 -1
- package/dist/services/class.d.ts +23 -0
- package/dist/services/class.d.ts.map +1 -1
- package/dist/services/class.js +8 -3
- package/dist/services/class.js.map +1 -1
- package/dist/services/comment.d.ts +18 -0
- package/dist/services/comment.d.ts.map +1 -1
- package/dist/services/comment.js +23 -2
- package/dist/services/comment.js.map +1 -1
- package/dist/services/conversation.d.ts.map +1 -1
- package/dist/services/conversation.js +9 -7
- package/dist/services/conversation.js.map +1 -1
- package/dist/services/worksheet.d.ts +3 -0
- package/dist/services/worksheet.d.ts.map +1 -1
- package/dist/services/worksheet.js +2 -2
- package/dist/services/worksheet.js.map +1 -1
- package/package.json +1 -1
- package/prisma/schema.prisma +1 -0
- package/src/models/class.ts +7 -1
- package/src/models/comment.ts +1 -0
- package/src/models/conversation.ts +12 -0
- package/src/models/worksheet.ts +11 -5
- package/src/pipelines/gradeWorksheet.ts +66 -15
- package/src/routers/announcement.ts +4 -3
- package/src/routers/assignment.ts +2 -2
- package/src/routers/comment.ts +5 -0
- package/src/routers/worksheet.ts +2 -0
- package/src/services/assignment.ts +8 -7
- package/src/services/class.ts +10 -1
- package/src/services/comment.ts +22 -0
- package/src/services/conversation.ts +9 -4
- package/src/services/worksheet.ts +1 -0
- package/tests/integration/routers/conversation.test.ts +9 -7
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"worksheet.js","sources":["services/worksheet.ts"],"sourceRoot":"/","sourcesContent":["/**\n * Worksheet service – CRUD for worksheets, questions, submissions, and AI grading.\n * Integrates with gradeWorksheet pipeline for auto-grading.\n */\nimport { TRPCError } from \"@trpc/server\";\nimport { JobStatus, type WorksheetQuestionType } from \"@prisma/client\";\nimport { prisma } from \"../lib/prisma.js\";\nimport { getFileUrl } from \"../lib/fileUpload.js\";\nimport { cancelGradePipeline, regradeWorksheetPipeline } from \"../pipelines/gradeWorksheet.js\";\nimport { enqueueRunJob } from \"../workers/queues.js\";\nimport { logger } from \"../utils/logger.js\";\nimport {\n findWorksheetByIdMinimal,\n findWorksheetByIdForClassMember,\n findWorksheetByIdForTeacher,\n findWorksheetsByClassId,\n createWorksheet,\n duplicateWorksheet,\n updateWorksheet,\n deleteWorksheet,\n findQuestionsByWorksheetId,\n createWorksheetQuestion,\n updateWorksheetQuestion,\n deleteWorksheetQuestion,\n findSubmissionByIdForWorksheetAccess,\n findOrCreateWorksheetResponse,\n findWorksheetResponseByIdForTeacher,\n findWorksheetResponseWithResponsesForStudent,\n findWorksheetQuestionByIdForTeacher,\n findWorksheetQuestionByIdInWorksheet,\n updateStudentQuestionProgress,\n createStudentQuestionProgress,\n findStudentQuestionProgress,\n updateStudentQuestionProgressForGrading,\n createStudentQuestionProgressForGrading,\n createComment,\n findStudentQuestionProgressByIdForTeacher,\n findStudentQuestionProgressByIdForStudentOrTeacher,\n findStudentQuestionProgressByIdForTeacherAndResponse,\n} from \"../models/worksheet.js\";\n\ntype MCQOptions = { id: string; text: string; isCorrect: boolean }[];\n\nconst sanitizeQuestionOptionsForStudent = (options: unknown) => {\n if (!Array.isArray(options)) return options;\n\n return options.map((option) => {\n if (!option || typeof option !== \"object\") return option;\n\n const { isCorrect: _isCorrect, ...rest } = option as Record<string, unknown>;\n return rest;\n });\n};\n\nconst sanitizeWorksheetForStudent = <T extends {\n questions: Array<Record<string, unknown>>;\n}>(worksheet: T) => ({\n ...worksheet,\n questions: worksheet.questions.map((question) => ({\n ...question,\n answer: \"\",\n markScheme: null,\n options: sanitizeQuestionOptionsForStudent(question.options),\n })),\n});\n\n/** Get worksheet with questions and mark schemes. */\nexport async function getWorksheet(worksheetId: string, userId: string) {\n const teacherWorksheet = await findWorksheetByIdForTeacher(worksheetId, userId);\n if (teacherWorksheet) {\n return teacherWorksheet;\n }\n\n const studentWorksheet = await findWorksheetByIdForClassMember(worksheetId, userId);\n if (!studentWorksheet) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"You do not have access to this worksheet\",\n });\n }\n\n return sanitizeWorksheetForStudent(studentWorksheet);\n}\n\n/** Check if worksheet exists. */\nexport async function worksheetExists(id: string) {\n const worksheet = await findWorksheetByIdMinimal(id);\n return !!worksheet;\n}\n\nexport async function listWorksheets(classId: string) {\n const worksheets = await findWorksheetsByClassId(classId);\n return worksheets.map((w) => ({\n ...w,\n questionCount: w.questions.length,\n }));\n}\n\nexport async function updateWorksheetRecord(\n worksheetId: string,\n userId: string,\n data: { name?: string }\n) {\n const worksheet = await findWorksheetByIdForTeacher(worksheetId, userId);\n if (!worksheet) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"Only class teachers can update worksheets\",\n });\n }\n return updateWorksheet(worksheetId, data);\n}\n\nexport async function deleteWorksheetRecord(worksheetId: string, userId: string) {\n const worksheet = await findWorksheetByIdForTeacher(worksheetId, userId);\n if (!worksheet) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"Only class teachers can delete worksheets\",\n });\n }\n return deleteWorksheet(worksheetId);\n}\n\nexport async function createWorksheetRecord(classId: string, name: string) {\n return createWorksheet({ classId, name });\n}\n\n/**\n * Duplicate a worksheet into another class. Caller must teach both the source worksheet's class\n * and the target class.\n */\nexport async function duplicateWorksheetRecord(\n worksheetId: string,\n classId: string,\n name: string,\n userId: string\n) {\n const source = await findWorksheetByIdForTeacher(worksheetId, userId);\n if (!source) {\n throw new TRPCError({\n code: \"NOT_FOUND\",\n message:\n \"Worksheet not found or you are not a teacher of the class it belongs to\",\n });\n }\n\n const targetOk = await prisma.class.findFirst({\n where: {\n id: classId,\n teachers: { some: { id: userId } },\n },\n select: { id: true },\n });\n\n if (!targetOk) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"You are not a teacher of the target class\",\n });\n }\n\n const created = await duplicateWorksheet(worksheetId, classId, name);\n if (!created) {\n throw new TRPCError({\n code: \"NOT_FOUND\",\n message: \"Worksheet not found\",\n });\n }\n\n return created;\n}\n\n/**\n * Kick off PDF-to-worksheet conversion asynchronously.\n *\n * We synchronously create a placeholder Worksheet row (status PENDING) so the\n * caller has an id to navigate to immediately, then enqueue a BullMQ job that\n * fills in the real title and questions. Progress is broadcast on the\n * per-worksheet job Pusher channel; clients should subscribe there to drive\n * loading UI.\n */\nexport async function createWorksheetFromPdf(\n classId: string,\n url: string,\n userId?: string,\n options: { name?: string } = {},\n) {\n const placeholder = await prisma.worksheet.create({\n data: {\n classId,\n name: options.name?.trim() || \"Converting PDF…\",\n status: JobStatus.PENDING,\n },\n });\n\n try {\n const { runJobId } = await enqueueRunJob(\n \"PDF_TO_WORKSHEET\",\n { gcsUrl: url, classId, worksheetId: placeholder.id },\n { classId, userId: userId ?? null },\n );\n\n await prisma.worksheet\n .update({\n where: { id: placeholder.id },\n data: { runJobId },\n })\n .catch((linkError) => {\n logger.error(\n \"createWorksheetFromPdf: failed to link RunJob to Worksheet\",\n {\n worksheetId: placeholder.id,\n runJobId,\n error:\n linkError instanceof Error\n ? linkError.message\n : String(linkError),\n },\n );\n });\n\n return {\n ...placeholder,\n runJobId,\n };\n } catch (error) {\n await prisma.worksheet\n .update({\n where: { id: placeholder.id },\n data: { status: JobStatus.FAILED },\n })\n .catch(() => undefined);\n\n logger.error(\"createWorksheetFromPdf: failed to enqueue conversion\", {\n worksheetId: placeholder.id,\n classId,\n error: error instanceof Error ? error.message : String(error),\n });\n\n throw new TRPCError({\n code: \"INTERNAL_SERVER_ERROR\",\n message: \"Failed to start worksheet conversion\",\n });\n }\n}\n\n/**\n * Create a worksheet from a PDF that was uploaded through the direct-upload flow.\n */\nexport async function createWorksheetFromUploadedPdf(\n classId: string,\n fileId: string,\n userId: string,\n options: { name?: string } = {},\n) {\n const file = await prisma.file.findFirst({\n where: {\n id: fileId,\n userId,\n uploadStatus: \"COMPLETED\",\n },\n select: {\n id: true,\n path: true,\n type: true,\n name: true,\n },\n });\n\n if (!file) {\n throw new TRPCError({\n code: \"NOT_FOUND\",\n message: \"Uploaded file not found or not ready\",\n });\n }\n\n const isPdf =\n file.type === \"application/pdf\" || file.path.toLowerCase().endsWith(\".pdf\");\n if (!isPdf) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: \"Only PDF files can be converted into worksheets\",\n });\n }\n\n const signedUrl = await getFileUrl(file.path);\n return createWorksheetFromPdf(classId, signedUrl, userId, {\n name: options.name ?? file.name ?? undefined,\n });\n}\n\nexport async function addQuestionToWorksheet(\n worksheetId: string,\n userId: string,\n data: {\n question: string;\n answer: string;\n points?: number;\n options?: object;\n markScheme?: object;\n type: WorksheetQuestionType;\n }\n) {\n const worksheet = await findWorksheetByIdForTeacher(worksheetId, userId);\n if (!worksheet) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"Only class teachers can modify worksheet questions\",\n });\n }\n return createWorksheetQuestion({\n worksheetId,\n ...data,\n });\n}\n\nexport async function reorderWorksheetQuestions(\n worksheetId: string,\n userId: string,\n movedId: string,\n position: \"before\" | \"after\",\n targetId: string\n) {\n const worksheet = await findWorksheetByIdForTeacher(worksheetId, userId);\n if (!worksheet) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"Only class teachers can reorder worksheet questions\",\n });\n }\n\n const questions = await findQuestionsByWorksheetId(worksheetId);\n const movedIdx = questions.findIndex((q) => q.id === movedId);\n if (movedIdx === -1) {\n throw new TRPCError({\n code: \"NOT_FOUND\",\n message: \"Moved question not found\",\n });\n }\n const targetIdx = questions.findIndex((q) => q.id === targetId);\n if (targetIdx === -1) {\n throw new TRPCError({\n code: \"NOT_FOUND\",\n message: \"Target question not found\",\n });\n }\n if (movedId === targetId) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: \"Moved question and target question must be different\",\n });\n }\n\n const withoutMoved = questions.filter((q) => q.id !== movedId);\n const targetIdxWithoutMoved = withoutMoved.findIndex((q) => q.id === targetId);\n const insertIdx =\n position === \"before\" ? targetIdxWithoutMoved : targetIdxWithoutMoved + 1;\n const next = [\n ...withoutMoved.slice(0, insertIdx).map((item) => ({ id: item.id })),\n { id: movedId },\n ...withoutMoved.slice(insertIdx).map((item) => ({ id: item.id })),\n ];\n\n await prisma.$transaction(\n next.map((item, index) =>\n prisma.worksheetQuestion.update({\n where: { id: item.id },\n data: { order: index },\n })\n )\n );\n return next;\n}\n\nexport async function updateWorksheetQuestionRecord(\n worksheetId: string,\n questionId: string,\n userId: string,\n data: {\n question?: string;\n answer?: string;\n points?: number;\n options?: object;\n markScheme?: object;\n type?: WorksheetQuestionType;\n }\n) {\n const question = await findWorksheetQuestionByIdForTeacher(questionId, userId);\n if (!question || question.worksheetId !== worksheetId) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"Only class teachers can update worksheet questions\",\n });\n }\n return updateWorksheetQuestion(questionId, data);\n}\n\nexport async function deleteWorksheetQuestionRecord(\n worksheetId: string,\n questionId: string,\n userId: string\n) {\n const question = await findWorksheetQuestionByIdForTeacher(questionId, userId);\n if (!question || question.worksheetId !== worksheetId) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"Only class teachers can delete worksheet questions\",\n });\n }\n return deleteWorksheetQuestion(questionId);\n}\n\nexport async function getWorksheetSubmission(\n worksheetId: string,\n submissionId: string,\n userId: string\n) {\n const worksheet = await findWorksheetByIdForClassMember(worksheetId, userId);\n if (!worksheet) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"You do not have access to this worksheet\",\n });\n }\n\n const submission = await findSubmissionByIdForWorksheetAccess(\n submissionId,\n worksheetId,\n worksheet.classId,\n userId\n );\n if (!submission) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"You do not have access to this worksheet submission\",\n });\n }\n return findOrCreateWorksheetResponse(\n submissionId,\n worksheetId,\n submission.studentId\n );\n}\n\nexport async function answerWorksheetQuestion(\n worksheetResponseId: string,\n questionId: string,\n response: string,\n userId: string\n) {\n const worksheetResponse = await findWorksheetResponseWithResponsesForStudent(\n worksheetResponseId,\n userId,\n questionId\n );\n if (!worksheetResponse) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"You can only answer your own worksheet responses\",\n });\n }\n\n const question = await findWorksheetQuestionByIdInWorksheet(\n questionId,\n worksheetResponse.worksheetId\n );\n if (!question) {\n throw new TRPCError({\n code: \"NOT_FOUND\",\n message: \"Worksheet question not found\",\n });\n }\n\n const isMarkableByAlgo =\n question.type === \"MULTIPLE_CHOICE\" || question.type === \"TRUE_FALSE\";\n const marksAwardedIfCorrect = question.points || 0;\n const correctAnswer = isMarkableByAlgo\n ? question.type === \"MULTIPLE_CHOICE\"\n ? (question.options as MCQOptions)?.find((o) => o.isCorrect)?.id\n : question.answer?.toString()\n : null;\n\n const existingResponse = worksheetResponse.responses[0];\n\n if (existingResponse) {\n await updateStudentQuestionProgress(existingResponse.id, {\n response,\n ...(isMarkableByAlgo && {\n isCorrect: response === correctAnswer,\n }),\n ...(isMarkableByAlgo && {\n points: response === correctAnswer ? marksAwardedIfCorrect : 0,\n }),\n status: JobStatus.NOT_STARTED,\n });\n } else {\n await createStudentQuestionProgress({\n studentId: worksheetResponse.studentId,\n questionId,\n response,\n studentWorksheetResponseId: worksheetResponseId,\n ...(isMarkableByAlgo && {\n isCorrect: response === correctAnswer,\n }),\n ...(isMarkableByAlgo && {\n points: response === correctAnswer ? marksAwardedIfCorrect : 0,\n }),\n });\n }\n\n const updated = await prisma.studentWorksheetResponse.findUnique({\n where: { id: worksheetResponseId },\n include: { responses: true },\n });\n return updated!;\n}\n\nexport async function cancelGrading(\n worksheetResponseId: string,\n progressId: string,\n userId: string\n) {\n const progress = await findStudentQuestionProgressByIdForTeacherAndResponse(\n progressId,\n worksheetResponseId,\n userId\n );\n if (!progress) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"Only class teachers can cancel worksheet grading\",\n });\n }\n return cancelGradePipeline(worksheetResponseId, progressId);\n}\n\nexport async function regradeQuestion(\n worksheetResponseId: string,\n progressId: string,\n userId: string\n) {\n const progress = await findStudentQuestionProgressByIdForTeacherAndResponse(\n progressId,\n worksheetResponseId,\n userId\n );\n if (!progress) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"Only class teachers can regrade worksheet answers\",\n });\n }\n return regradeWorksheetPipeline(worksheetResponseId, progressId);\n}\n\nexport async function gradeAnswer(\n questionId: string,\n studentWorksheetResponseId: string,\n userId: string,\n data: {\n responseId?: string;\n response?: string;\n isCorrect: boolean;\n feedback?: string;\n markschemeState?: object;\n points?: number;\n }\n) {\n const worksheetResponse = await findWorksheetResponseByIdForTeacher(\n studentWorksheetResponseId,\n userId\n );\n if (!worksheetResponse) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"Only class teachers can grade worksheet answers\",\n });\n }\n\n const question = await findWorksheetQuestionByIdInWorksheet(\n questionId,\n worksheetResponse.worksheetId\n );\n if (!question) {\n throw new TRPCError({\n code: \"NOT_FOUND\",\n message: \"Worksheet question not found\",\n });\n }\n\n if (data.responseId) {\n const existingProgress = await findStudentQuestionProgressByIdForTeacher(\n data.responseId,\n userId\n );\n if (\n !existingProgress ||\n existingProgress.studentWorksheetResponseId !== studentWorksheetResponseId ||\n existingProgress.questionId !== questionId\n ) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"Only class teachers can grade worksheet answers\",\n });\n }\n\n return updateStudentQuestionProgressForGrading(data.responseId, {\n isCorrect: data.isCorrect,\n feedback: data.feedback,\n markschemeState: data.markschemeState,\n points: data.points,\n });\n }\n\n const existing = await findStudentQuestionProgress(\n worksheetResponse.studentId,\n questionId,\n studentWorksheetResponseId\n );\n\n if (existing) {\n return updateStudentQuestionProgressForGrading(existing.id, {\n isCorrect: data.isCorrect,\n response: data.response,\n feedback: data.feedback,\n markschemeState: data.markschemeState,\n points: data.points,\n });\n }\n\n return createStudentQuestionProgressForGrading({\n studentId: worksheetResponse.studentId,\n questionId,\n studentWorksheetResponseId,\n response: data.response || \"\",\n isCorrect: data.isCorrect,\n feedback: data.feedback,\n markschemeState: data.markschemeState,\n points: data.points || 0,\n });\n}\n\nexport async function addCommentToResponse(\n responseId: string,\n comment: string,\n authorId: string\n) {\n const progress = await findStudentQuestionProgressByIdForStudentOrTeacher(\n responseId,\n authorId\n );\n if (!progress) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message:\n \"You can only comment on your own worksheet answers or on answers for classes you teach\",\n });\n }\n\n return createComment({\n studentQuestionProgressId: responseId,\n content: comment,\n authorId,\n });\n}\n"],"names":[],"mappings":"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","debug_id":"dbf4168b-2ccd-58fc-80da-0cb74d76cf44"}
|
|
1
|
+
{"version":3,"file":"worksheet.js","sources":["services/worksheet.ts"],"sourceRoot":"/","sourcesContent":["/**\n * Worksheet service – CRUD for worksheets, questions, submissions, and AI grading.\n * Integrates with gradeWorksheet pipeline for auto-grading.\n */\nimport { TRPCError } from \"@trpc/server\";\nimport { JobStatus, type WorksheetQuestionType } from \"@prisma/client\";\nimport { prisma } from \"../lib/prisma.js\";\nimport { getFileUrl } from \"../lib/fileUpload.js\";\nimport { cancelGradePipeline, regradeWorksheetPipeline } from \"../pipelines/gradeWorksheet.js\";\nimport { enqueueRunJob } from \"../workers/queues.js\";\nimport { logger } from \"../utils/logger.js\";\nimport {\n findWorksheetByIdMinimal,\n findWorksheetByIdForClassMember,\n findWorksheetByIdForTeacher,\n findWorksheetsByClassId,\n createWorksheet,\n duplicateWorksheet,\n updateWorksheet,\n deleteWorksheet,\n findQuestionsByWorksheetId,\n createWorksheetQuestion,\n updateWorksheetQuestion,\n deleteWorksheetQuestion,\n findSubmissionByIdForWorksheetAccess,\n findOrCreateWorksheetResponse,\n findWorksheetResponseByIdForTeacher,\n findWorksheetResponseWithResponsesForStudent,\n findWorksheetQuestionByIdForTeacher,\n findWorksheetQuestionByIdInWorksheet,\n updateStudentQuestionProgress,\n createStudentQuestionProgress,\n findStudentQuestionProgress,\n updateStudentQuestionProgressForGrading,\n createStudentQuestionProgressForGrading,\n createComment,\n findStudentQuestionProgressByIdForTeacher,\n findStudentQuestionProgressByIdForStudentOrTeacher,\n findStudentQuestionProgressByIdForTeacherAndResponse,\n} from \"../models/worksheet.js\";\n\ntype MCQOptions = { id: string; text: string; isCorrect: boolean }[];\n\nconst sanitizeQuestionOptionsForStudent = (options: unknown) => {\n if (!Array.isArray(options)) return options;\n\n return options.map((option) => {\n if (!option || typeof option !== \"object\") return option;\n\n const { isCorrect: _isCorrect, ...rest } = option as Record<string, unknown>;\n return rest;\n });\n};\n\nconst sanitizeWorksheetForStudent = <T extends {\n questions: Array<Record<string, unknown>>;\n}>(worksheet: T) => ({\n ...worksheet,\n questions: worksheet.questions.map((question) => ({\n ...question,\n answer: \"\",\n markScheme: null,\n options: sanitizeQuestionOptionsForStudent(question.options),\n })),\n});\n\n/** Get worksheet with questions and mark schemes. */\nexport async function getWorksheet(worksheetId: string, userId: string) {\n const teacherWorksheet = await findWorksheetByIdForTeacher(worksheetId, userId);\n if (teacherWorksheet) {\n return teacherWorksheet;\n }\n\n const studentWorksheet = await findWorksheetByIdForClassMember(worksheetId, userId);\n if (!studentWorksheet) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"You do not have access to this worksheet\",\n });\n }\n\n return sanitizeWorksheetForStudent(studentWorksheet);\n}\n\n/** Check if worksheet exists. */\nexport async function worksheetExists(id: string) {\n const worksheet = await findWorksheetByIdMinimal(id);\n return !!worksheet;\n}\n\nexport async function listWorksheets(classId: string) {\n const worksheets = await findWorksheetsByClassId(classId);\n return worksheets.map((w) => ({\n ...w,\n questionCount: w.questions.length,\n }));\n}\n\nexport async function updateWorksheetRecord(\n worksheetId: string,\n userId: string,\n data: { name?: string }\n) {\n const worksheet = await findWorksheetByIdForTeacher(worksheetId, userId);\n if (!worksheet) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"Only class teachers can update worksheets\",\n });\n }\n return updateWorksheet(worksheetId, data);\n}\n\nexport async function deleteWorksheetRecord(worksheetId: string, userId: string) {\n const worksheet = await findWorksheetByIdForTeacher(worksheetId, userId);\n if (!worksheet) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"Only class teachers can delete worksheets\",\n });\n }\n return deleteWorksheet(worksheetId);\n}\n\nexport async function createWorksheetRecord(classId: string, name: string) {\n return createWorksheet({ classId, name });\n}\n\n/**\n * Duplicate a worksheet into another class. Caller must teach both the source worksheet's class\n * and the target class.\n */\nexport async function duplicateWorksheetRecord(\n worksheetId: string,\n classId: string,\n name: string,\n userId: string\n) {\n const source = await findWorksheetByIdForTeacher(worksheetId, userId);\n if (!source) {\n throw new TRPCError({\n code: \"NOT_FOUND\",\n message:\n \"Worksheet not found or you are not a teacher of the class it belongs to\",\n });\n }\n\n const targetOk = await prisma.class.findFirst({\n where: {\n id: classId,\n teachers: { some: { id: userId } },\n },\n select: { id: true },\n });\n\n if (!targetOk) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"You are not a teacher of the target class\",\n });\n }\n\n const created = await duplicateWorksheet(worksheetId, classId, name);\n if (!created) {\n throw new TRPCError({\n code: \"NOT_FOUND\",\n message: \"Worksheet not found\",\n });\n }\n\n return created;\n}\n\n/**\n * Kick off PDF-to-worksheet conversion asynchronously.\n *\n * We synchronously create a placeholder Worksheet row (status PENDING) so the\n * caller has an id to navigate to immediately, then enqueue a BullMQ job that\n * fills in the real title and questions. Progress is broadcast on the\n * per-worksheet job Pusher channel; clients should subscribe there to drive\n * loading UI.\n */\nexport async function createWorksheetFromPdf(\n classId: string,\n url: string,\n userId?: string,\n options: { name?: string } = {},\n) {\n const placeholder = await prisma.worksheet.create({\n data: {\n classId,\n name: options.name?.trim() || \"Converting PDF…\",\n status: JobStatus.PENDING,\n },\n });\n\n try {\n const { runJobId } = await enqueueRunJob(\n \"PDF_TO_WORKSHEET\",\n { gcsUrl: url, classId, worksheetId: placeholder.id },\n { classId, userId: userId ?? null },\n );\n\n await prisma.worksheet\n .update({\n where: { id: placeholder.id },\n data: { runJobId },\n })\n .catch((linkError) => {\n logger.error(\n \"createWorksheetFromPdf: failed to link RunJob to Worksheet\",\n {\n worksheetId: placeholder.id,\n runJobId,\n error:\n linkError instanceof Error\n ? linkError.message\n : String(linkError),\n },\n );\n });\n\n return {\n ...placeholder,\n runJobId,\n };\n } catch (error) {\n await prisma.worksheet\n .update({\n where: { id: placeholder.id },\n data: { status: JobStatus.FAILED },\n })\n .catch(() => undefined);\n\n logger.error(\"createWorksheetFromPdf: failed to enqueue conversion\", {\n worksheetId: placeholder.id,\n classId,\n error: error instanceof Error ? error.message : String(error),\n });\n\n throw new TRPCError({\n code: \"INTERNAL_SERVER_ERROR\",\n message: \"Failed to start worksheet conversion\",\n });\n }\n}\n\n/**\n * Create a worksheet from a PDF that was uploaded through the direct-upload flow.\n */\nexport async function createWorksheetFromUploadedPdf(\n classId: string,\n fileId: string,\n userId: string,\n options: { name?: string } = {},\n) {\n const file = await prisma.file.findFirst({\n where: {\n id: fileId,\n userId,\n uploadStatus: \"COMPLETED\",\n },\n select: {\n id: true,\n path: true,\n type: true,\n name: true,\n },\n });\n\n if (!file) {\n throw new TRPCError({\n code: \"NOT_FOUND\",\n message: \"Uploaded file not found or not ready\",\n });\n }\n\n const isPdf =\n file.type === \"application/pdf\" || file.path.toLowerCase().endsWith(\".pdf\");\n if (!isPdf) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: \"Only PDF files can be converted into worksheets\",\n });\n }\n\n const signedUrl = await getFileUrl(file.path);\n return createWorksheetFromPdf(classId, signedUrl, userId, {\n name: options.name ?? file.name ?? undefined,\n });\n}\n\nexport async function addQuestionToWorksheet(\n worksheetId: string,\n userId: string,\n data: {\n question: string;\n answer: string;\n points?: number;\n options?: object;\n markScheme?: object;\n type: WorksheetQuestionType;\n order?: number;\n }\n) {\n const worksheet = await findWorksheetByIdForTeacher(worksheetId, userId);\n if (!worksheet) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"Only class teachers can modify worksheet questions\",\n });\n }\n return createWorksheetQuestion({\n worksheetId,\n ...data,\n });\n}\n\nexport async function reorderWorksheetQuestions(\n worksheetId: string,\n userId: string,\n movedId: string,\n position: \"before\" | \"after\",\n targetId: string\n) {\n const worksheet = await findWorksheetByIdForTeacher(worksheetId, userId);\n if (!worksheet) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"Only class teachers can reorder worksheet questions\",\n });\n }\n\n const questions = await findQuestionsByWorksheetId(worksheetId);\n const movedIdx = questions.findIndex((q) => q.id === movedId);\n if (movedIdx === -1) {\n throw new TRPCError({\n code: \"NOT_FOUND\",\n message: \"Moved question not found\",\n });\n }\n const targetIdx = questions.findIndex((q) => q.id === targetId);\n if (targetIdx === -1) {\n throw new TRPCError({\n code: \"NOT_FOUND\",\n message: \"Target question not found\",\n });\n }\n if (movedId === targetId) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: \"Moved question and target question must be different\",\n });\n }\n\n const withoutMoved = questions.filter((q) => q.id !== movedId);\n const targetIdxWithoutMoved = withoutMoved.findIndex((q) => q.id === targetId);\n const insertIdx =\n position === \"before\" ? targetIdxWithoutMoved : targetIdxWithoutMoved + 1;\n const next = [\n ...withoutMoved.slice(0, insertIdx).map((item) => ({ id: item.id })),\n { id: movedId },\n ...withoutMoved.slice(insertIdx).map((item) => ({ id: item.id })),\n ];\n\n await prisma.$transaction(\n next.map((item, index) =>\n prisma.worksheetQuestion.update({\n where: { id: item.id },\n data: { order: index },\n })\n )\n );\n return next;\n}\n\nexport async function updateWorksheetQuestionRecord(\n worksheetId: string,\n questionId: string,\n userId: string,\n data: {\n question?: string;\n answer?: string;\n points?: number;\n options?: object;\n markScheme?: object;\n type?: WorksheetQuestionType;\n }\n) {\n const question = await findWorksheetQuestionByIdForTeacher(questionId, userId);\n if (!question || question.worksheetId !== worksheetId) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"Only class teachers can update worksheet questions\",\n });\n }\n return updateWorksheetQuestion(questionId, data);\n}\n\nexport async function deleteWorksheetQuestionRecord(\n worksheetId: string,\n questionId: string,\n userId: string\n) {\n const question = await findWorksheetQuestionByIdForTeacher(questionId, userId);\n if (!question || question.worksheetId !== worksheetId) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"Only class teachers can delete worksheet questions\",\n });\n }\n return deleteWorksheetQuestion(questionId);\n}\n\nexport async function getWorksheetSubmission(\n worksheetId: string,\n submissionId: string,\n userId: string\n) {\n const worksheet = await findWorksheetByIdForClassMember(worksheetId, userId);\n if (!worksheet) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"You do not have access to this worksheet\",\n });\n }\n\n const submission = await findSubmissionByIdForWorksheetAccess(\n submissionId,\n worksheetId,\n worksheet.classId,\n userId\n );\n if (!submission) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"You do not have access to this worksheet submission\",\n });\n }\n return findOrCreateWorksheetResponse(\n submissionId,\n worksheetId,\n submission.studentId\n );\n}\n\nexport async function answerWorksheetQuestion(\n worksheetResponseId: string,\n questionId: string,\n response: string,\n userId: string\n) {\n const worksheetResponse = await findWorksheetResponseWithResponsesForStudent(\n worksheetResponseId,\n userId,\n questionId\n );\n if (!worksheetResponse) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"You can only answer your own worksheet responses\",\n });\n }\n\n const question = await findWorksheetQuestionByIdInWorksheet(\n questionId,\n worksheetResponse.worksheetId\n );\n if (!question) {\n throw new TRPCError({\n code: \"NOT_FOUND\",\n message: \"Worksheet question not found\",\n });\n }\n\n const isMarkableByAlgo =\n question.type === \"MULTIPLE_CHOICE\" || question.type === \"TRUE_FALSE\";\n const marksAwardedIfCorrect = question.points || 0;\n const correctAnswer = isMarkableByAlgo\n ? question.type === \"MULTIPLE_CHOICE\"\n ? (question.options as MCQOptions)?.find((o) => o.isCorrect)?.id\n : question.answer?.toString()\n : null;\n\n const existingResponse = worksheetResponse.responses[0];\n\n if (existingResponse) {\n await updateStudentQuestionProgress(existingResponse.id, {\n response,\n ...(isMarkableByAlgo && {\n isCorrect: response === correctAnswer,\n }),\n ...(isMarkableByAlgo && {\n points: response === correctAnswer ? marksAwardedIfCorrect : 0,\n }),\n status: JobStatus.NOT_STARTED,\n });\n } else {\n await createStudentQuestionProgress({\n studentId: worksheetResponse.studentId,\n questionId,\n response,\n studentWorksheetResponseId: worksheetResponseId,\n ...(isMarkableByAlgo && {\n isCorrect: response === correctAnswer,\n }),\n ...(isMarkableByAlgo && {\n points: response === correctAnswer ? marksAwardedIfCorrect : 0,\n }),\n });\n }\n\n const updated = await prisma.studentWorksheetResponse.findUnique({\n where: { id: worksheetResponseId },\n include: { responses: true },\n });\n return updated!;\n}\n\nexport async function cancelGrading(\n worksheetResponseId: string,\n progressId: string,\n userId: string\n) {\n const progress = await findStudentQuestionProgressByIdForTeacherAndResponse(\n progressId,\n worksheetResponseId,\n userId\n );\n if (!progress) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"Only class teachers can cancel worksheet grading\",\n });\n }\n return cancelGradePipeline(worksheetResponseId, progressId);\n}\n\nexport async function regradeQuestion(\n worksheetResponseId: string,\n progressId: string,\n userId: string\n) {\n const progress = await findStudentQuestionProgressByIdForTeacherAndResponse(\n progressId,\n worksheetResponseId,\n userId\n );\n if (!progress) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"Only class teachers can regrade worksheet answers\",\n });\n }\n return regradeWorksheetPipeline(worksheetResponseId, progressId);\n}\n\nexport async function gradeAnswer(\n questionId: string,\n studentWorksheetResponseId: string,\n userId: string,\n data: {\n responseId?: string;\n response?: string;\n isCorrect: boolean;\n feedback?: string;\n markschemeState?: object;\n points?: number;\n }\n) {\n const worksheetResponse = await findWorksheetResponseByIdForTeacher(\n studentWorksheetResponseId,\n userId\n );\n if (!worksheetResponse) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"Only class teachers can grade worksheet answers\",\n });\n }\n\n const question = await findWorksheetQuestionByIdInWorksheet(\n questionId,\n worksheetResponse.worksheetId\n );\n if (!question) {\n throw new TRPCError({\n code: \"NOT_FOUND\",\n message: \"Worksheet question not found\",\n });\n }\n\n if (data.responseId) {\n const existingProgress = await findStudentQuestionProgressByIdForTeacher(\n data.responseId,\n userId\n );\n if (\n !existingProgress ||\n existingProgress.studentWorksheetResponseId !== studentWorksheetResponseId ||\n existingProgress.questionId !== questionId\n ) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"Only class teachers can grade worksheet answers\",\n });\n }\n\n return updateStudentQuestionProgressForGrading(data.responseId, {\n isCorrect: data.isCorrect,\n feedback: data.feedback,\n markschemeState: data.markschemeState,\n points: data.points,\n });\n }\n\n const existing = await findStudentQuestionProgress(\n worksheetResponse.studentId,\n questionId,\n studentWorksheetResponseId\n );\n\n if (existing) {\n return updateStudentQuestionProgressForGrading(existing.id, {\n isCorrect: data.isCorrect,\n response: data.response,\n feedback: data.feedback,\n markschemeState: data.markschemeState,\n points: data.points,\n });\n }\n\n return createStudentQuestionProgressForGrading({\n studentId: worksheetResponse.studentId,\n questionId,\n studentWorksheetResponseId,\n response: data.response || \"\",\n isCorrect: data.isCorrect,\n feedback: data.feedback,\n markschemeState: data.markschemeState,\n points: data.points || 0,\n });\n}\n\nexport async function addCommentToResponse(\n responseId: string,\n comment: string,\n authorId: string\n) {\n const progress = await findStudentQuestionProgressByIdForStudentOrTeacher(\n responseId,\n authorId\n );\n if (!progress) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message:\n \"You can only comment on your own worksheet answers or on answers for classes you teach\",\n });\n }\n\n return createComment({\n studentQuestionProgressId: responseId,\n content: comment,\n authorId,\n });\n}\n"],"names":[],"mappings":"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","debug_id":"763e1691-86f4-53c9-8832-39f71889d16c"}
|
package/package.json
CHANGED
package/prisma/schema.prisma
CHANGED
|
@@ -519,6 +519,7 @@ model Comment {
|
|
|
519
519
|
parentCommentId String?
|
|
520
520
|
replies Comment[] @relation("CommentReplies")
|
|
521
521
|
reactions Reaction[]
|
|
522
|
+
isPinned Boolean @default(false)
|
|
522
523
|
createdAt DateTime @default(now())
|
|
523
524
|
modifiedAt DateTime? @updatedAt
|
|
524
525
|
studentQuestionProgress StudentQuestionProgress? @relation("StudentQuestionProgressComments", fields: [studentQuestionProgressId], references: [id], onDelete: Cascade)
|
package/src/models/class.ts
CHANGED
|
@@ -18,7 +18,6 @@ import {
|
|
|
18
18
|
const classIncludeForGetAll = {
|
|
19
19
|
assignments: {
|
|
20
20
|
where: {
|
|
21
|
-
dueDate: { lte: new Date(new Date().setHours(23, 59, 59, 999)) },
|
|
22
21
|
template: false,
|
|
23
22
|
},
|
|
24
23
|
select: {
|
|
@@ -26,6 +25,13 @@ const classIncludeForGetAll = {
|
|
|
26
25
|
title: true,
|
|
27
26
|
type: true,
|
|
28
27
|
dueDate: true,
|
|
28
|
+
submissions: {
|
|
29
|
+
select: {
|
|
30
|
+
studentId: true,
|
|
31
|
+
submitted: true,
|
|
32
|
+
returned: true,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
29
35
|
},
|
|
30
36
|
},
|
|
31
37
|
students: {
|
package/src/models/comment.ts
CHANGED
|
@@ -256,6 +256,18 @@ export function hideConversationMember(userId: string, conversationId: string) {
|
|
|
256
256
|
});
|
|
257
257
|
}
|
|
258
258
|
|
|
259
|
+
/** Unhide a conversation for a single member. */
|
|
260
|
+
export function unhideConversationMember(userId: string, conversationId: string) {
|
|
261
|
+
return prisma.conversationMember.update({
|
|
262
|
+
where: {
|
|
263
|
+
userId_conversationId: { userId, conversationId },
|
|
264
|
+
},
|
|
265
|
+
data: {
|
|
266
|
+
hiddenAt: null,
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
259
271
|
/** Remove a member from a conversation. */
|
|
260
272
|
export function deleteConversationMember(userId: string, conversationId: string) {
|
|
261
273
|
return prisma.conversationMember.delete({
|
package/src/models/worksheet.ts
CHANGED
|
@@ -22,7 +22,7 @@ export function findWorksheetById(id: string) {
|
|
|
22
22
|
return prisma.worksheet.findUnique({
|
|
23
23
|
where: { id },
|
|
24
24
|
include: {
|
|
25
|
-
questions: { orderBy: {
|
|
25
|
+
questions: { orderBy: { order: "asc" as const } },
|
|
26
26
|
class: true,
|
|
27
27
|
},
|
|
28
28
|
});
|
|
@@ -43,7 +43,7 @@ export function findWorksheetByIdForClassMember(id: string, userId: string) {
|
|
|
43
43
|
...worksheetClassMemberWhere(userId),
|
|
44
44
|
},
|
|
45
45
|
include: {
|
|
46
|
-
questions: { orderBy: {
|
|
46
|
+
questions: { orderBy: { order: "asc" as const } },
|
|
47
47
|
class: true,
|
|
48
48
|
},
|
|
49
49
|
});
|
|
@@ -57,7 +57,7 @@ export function findWorksheetByIdForTeacher(id: string, userId: string) {
|
|
|
57
57
|
...worksheetTeacherWhere(userId),
|
|
58
58
|
},
|
|
59
59
|
include: {
|
|
60
|
-
questions: { orderBy: {
|
|
60
|
+
questions: { orderBy: { order: "asc" as const } },
|
|
61
61
|
class: true,
|
|
62
62
|
},
|
|
63
63
|
});
|
|
@@ -273,7 +273,10 @@ export function findSubmissionByIdForWorksheetAccess(
|
|
|
273
273
|
const worksheetResponseInclude = {
|
|
274
274
|
responses: {
|
|
275
275
|
include: {
|
|
276
|
-
comments: {
|
|
276
|
+
comments: {
|
|
277
|
+
select: { id: true, isPinned: true },
|
|
278
|
+
orderBy: [{ isPinned: "desc" as const }, { createdAt: "asc" as const }],
|
|
279
|
+
},
|
|
277
280
|
},
|
|
278
281
|
},
|
|
279
282
|
};
|
|
@@ -319,7 +322,10 @@ export function createWorksheetResponse(data: {
|
|
|
319
322
|
include: {
|
|
320
323
|
responses: {
|
|
321
324
|
include: {
|
|
322
|
-
comments: {
|
|
325
|
+
comments: {
|
|
326
|
+
select: { id: true, isPinned: true },
|
|
327
|
+
orderBy: [{ isPinned: "desc" as const }, { createdAt: "asc" as const }],
|
|
328
|
+
},
|
|
323
329
|
},
|
|
324
330
|
},
|
|
325
331
|
},
|
|
@@ -22,6 +22,8 @@ const removeAllPreviousAIComments = async (worksheetQuestionProgressId: string)
|
|
|
22
22
|
|
|
23
23
|
const gradeWorksheetQuestion = async (worksheetResponseId: string, worksheetQuestionProgressId: string) => {
|
|
24
24
|
|
|
25
|
+
await removeAllPreviousAIComments(worksheetQuestionProgressId);
|
|
26
|
+
|
|
25
27
|
const worksheetResponse = await prisma.studentWorksheetResponse.findUnique({
|
|
26
28
|
where: { id: worksheetResponseId },
|
|
27
29
|
include: {
|
|
@@ -45,17 +47,8 @@ const gradeWorksheetQuestion = async (worksheetResponseId: string, worksheetQues
|
|
|
45
47
|
});
|
|
46
48
|
|
|
47
49
|
if (!studentQuestionProgress) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
studentId: worksheetResponse.studentId,
|
|
51
|
-
questionId: worksheetQuestionProgressId,
|
|
52
|
-
response: '',
|
|
53
|
-
isCorrect: false,
|
|
54
|
-
markschemeState: {},
|
|
55
|
-
},
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
return updatedStudentQuestionProgress;
|
|
50
|
+
logger.error('Student question progress not found', { worksheetQuestionProgressId });
|
|
51
|
+
throw new Error('Student question progress not found');
|
|
59
52
|
}
|
|
60
53
|
|
|
61
54
|
pusher.trigger(worksheetChannel(worksheetResponse.id), `set-pending`, {
|
|
@@ -63,9 +56,34 @@ const gradeWorksheetQuestion = async (worksheetResponseId: string, worksheetQues
|
|
|
63
56
|
});
|
|
64
57
|
|
|
65
58
|
const question = studentQuestionProgress.question;
|
|
66
|
-
const comments = studentQuestionProgress.comments;
|
|
67
59
|
const responseText = studentQuestionProgress.response;
|
|
68
60
|
|
|
61
|
+
// Short-circuit for empty responses: award 0 points without calling inference
|
|
62
|
+
if (!responseText || responseText.trim().length === 0) {
|
|
63
|
+
const updatedProgress = await prisma.studentQuestionProgress.update({
|
|
64
|
+
where: { id: studentQuestionProgress.id, status: {
|
|
65
|
+
not: { in: [JobStatus.CANCELLED] },
|
|
66
|
+
} },
|
|
67
|
+
data: {
|
|
68
|
+
status: JobStatus.COMPLETED,
|
|
69
|
+
isCorrect: false,
|
|
70
|
+
points: 0,
|
|
71
|
+
markschemeState: {},
|
|
72
|
+
comments: {
|
|
73
|
+
create: [{
|
|
74
|
+
content: 'No answer provided.',
|
|
75
|
+
authorId: getAIUserId(),
|
|
76
|
+
}],
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
pusher.trigger(worksheetChannel(worksheetResponse.id), `set-completed`, {
|
|
81
|
+
id: updatedProgress.id,
|
|
82
|
+
});
|
|
83
|
+
return updatedProgress;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const comments = studentQuestionProgress.comments;
|
|
69
87
|
|
|
70
88
|
try {
|
|
71
89
|
const apiResponse = await inference(
|
|
@@ -206,7 +224,42 @@ export const gradeWorksheetPipeline = async (worksheetResponseId: string) => {
|
|
|
206
224
|
throw new Error('Worksheet response not found');
|
|
207
225
|
}
|
|
208
226
|
|
|
209
|
-
//
|
|
227
|
+
// Find worksheet questions that have no StudentQuestionProgress for this response
|
|
228
|
+
// and create empty progress records so they get graded (with 0 points).
|
|
229
|
+
const allQuestions = await prisma.worksheetQuestion.findMany({
|
|
230
|
+
where: {
|
|
231
|
+
worksheetId: worksheetResponse.worksheetId,
|
|
232
|
+
type: {
|
|
233
|
+
notIn: [WorksheetQuestionType.MULTIPLE_CHOICE, WorksheetQuestionType.TRUE_FALSE],
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
select: { id: true },
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
const existingQuestionIds = new Set(
|
|
240
|
+
worksheetResponse.responses.map((r) => r.questionId),
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
for (const q of allQuestions) {
|
|
244
|
+
if (existingQuestionIds.has(q.id)) continue;
|
|
245
|
+
|
|
246
|
+
const created = await prisma.studentQuestionProgress.create({
|
|
247
|
+
data: {
|
|
248
|
+
studentId: worksheetResponse.studentId,
|
|
249
|
+
questionId: q.id,
|
|
250
|
+
studentWorksheetResponseId: worksheetResponseId,
|
|
251
|
+
response: '',
|
|
252
|
+
isCorrect: false,
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
worksheetResponse.responses.push({
|
|
257
|
+
...created,
|
|
258
|
+
question: { id: q.id } as any,
|
|
259
|
+
comments: [],
|
|
260
|
+
} as any);
|
|
261
|
+
}
|
|
262
|
+
|
|
210
263
|
for (const response of worksheetResponse.responses) {
|
|
211
264
|
logger.info('Grading question', { questionId: response.questionId });
|
|
212
265
|
|
|
@@ -293,8 +346,6 @@ export const regradeWorksheetPipeline = async (worksheetResponseId: string, work
|
|
|
293
346
|
where: { id: worksheetQuestionProgressId },
|
|
294
347
|
});
|
|
295
348
|
|
|
296
|
-
await removeAllPreviousAIComments(worksheetQuestionProgressId);
|
|
297
|
-
|
|
298
349
|
await gradeWorksheetQuestion(worksheetResponseId, worksheetQuestionProgressId);
|
|
299
350
|
|
|
300
351
|
return updatedStudentQuestionProgress;
|
|
@@ -453,9 +453,10 @@ export const announcementRouter = createTRPCRouter({
|
|
|
453
453
|
},
|
|
454
454
|
},
|
|
455
455
|
},
|
|
456
|
-
orderBy:
|
|
457
|
-
|
|
458
|
-
|
|
456
|
+
orderBy: [
|
|
457
|
+
{ isPinned: 'desc' as const },
|
|
458
|
+
{ createdAt: 'asc' as const },
|
|
459
|
+
],
|
|
459
460
|
});
|
|
460
461
|
|
|
461
462
|
return { comments };
|
|
@@ -366,7 +366,7 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
366
366
|
parentCommentId: null,
|
|
367
367
|
},
|
|
368
368
|
include: commentInclude,
|
|
369
|
-
orderBy: { createdAt: "asc" },
|
|
369
|
+
orderBy: [{ isPinned: "desc" }, { createdAt: "asc" }],
|
|
370
370
|
});
|
|
371
371
|
|
|
372
372
|
return { comments };
|
|
@@ -476,7 +476,7 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
476
476
|
parentCommentId: null,
|
|
477
477
|
},
|
|
478
478
|
include: commentInclude,
|
|
479
|
-
orderBy: { createdAt: "asc" },
|
|
479
|
+
orderBy: [{ isPinned: "desc" }, { createdAt: "asc" }],
|
|
480
480
|
});
|
|
481
481
|
|
|
482
482
|
return { comments };
|
package/src/routers/comment.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
replyToComment,
|
|
8
8
|
updateComment,
|
|
9
9
|
deleteComment,
|
|
10
|
+
pinComment,
|
|
10
11
|
addReaction,
|
|
11
12
|
removeReaction,
|
|
12
13
|
getReactions,
|
|
@@ -49,6 +50,10 @@ export const commentRouter = createTRPCRouter({
|
|
|
49
50
|
.input(z.object({ id: z.string() }))
|
|
50
51
|
.mutation(({ ctx, input }) => deleteComment(ctx.user!.id, input.id)),
|
|
51
52
|
|
|
53
|
+
pin: protectedProcedure
|
|
54
|
+
.input(z.object({ id: z.string(), isPinned: z.boolean() }))
|
|
55
|
+
.mutation(({ ctx, input }) => pinComment(ctx.user!.id, input.id, input.isPinned)),
|
|
56
|
+
|
|
52
57
|
addReaction: protectedProcedure
|
|
53
58
|
.input(
|
|
54
59
|
z.object({
|
package/src/routers/worksheet.ts
CHANGED
|
@@ -233,6 +233,7 @@ export const worksheetRouter = createTRPCRouter({
|
|
|
233
233
|
"MATH_EXPRESSION",
|
|
234
234
|
"ESSAY",
|
|
235
235
|
]),
|
|
236
|
+
order: z.number().int().min(0).optional(),
|
|
236
237
|
})
|
|
237
238
|
)
|
|
238
239
|
.mutation(({ ctx, input }) =>
|
|
@@ -243,6 +244,7 @@ export const worksheetRouter = createTRPCRouter({
|
|
|
243
244
|
options: input.options,
|
|
244
245
|
markScheme: input.markScheme,
|
|
245
246
|
type: input.type,
|
|
247
|
+
order: input.order,
|
|
246
248
|
})
|
|
247
249
|
),
|
|
248
250
|
|
|
@@ -425,12 +425,13 @@ export async function updateAssignmentRecord(
|
|
|
425
425
|
});
|
|
426
426
|
}
|
|
427
427
|
|
|
428
|
-
const submissionData =
|
|
429
|
-
input.studentIds
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
428
|
+
const submissionData = input.studentIds
|
|
429
|
+
? (input.studentIds.length > 0
|
|
430
|
+
? input.studentIds.map((studentId) => ({ student: { connect: { id: studentId } } }))
|
|
431
|
+
: classData?.students.map((student) => ({
|
|
432
|
+
student: { connect: { id: student.id } },
|
|
433
|
+
})))
|
|
434
|
+
: undefined;
|
|
434
435
|
|
|
435
436
|
if (input.removedAttachments?.length) {
|
|
436
437
|
const filesToDelete = assignment.attachments.filter((f) =>
|
|
@@ -481,7 +482,7 @@ export async function updateAssignmentRecord(
|
|
|
481
482
|
connect: input.studentIds.map((id) => ({ id })),
|
|
482
483
|
},
|
|
483
484
|
}),
|
|
484
|
-
...(submissionData && {
|
|
485
|
+
...(input.studentIds && submissionData && {
|
|
485
486
|
submissions: {
|
|
486
487
|
deleteMany: {},
|
|
487
488
|
create: submissionData,
|
package/src/services/class.ts
CHANGED
|
@@ -85,7 +85,16 @@ export async function getAllClasses(userId: string) {
|
|
|
85
85
|
section: cls.section,
|
|
86
86
|
subject: cls.subject,
|
|
87
87
|
dueToday: cls.assignments,
|
|
88
|
-
assignments: cls.assignments
|
|
88
|
+
assignments: cls.assignments.map((assignment) => ({
|
|
89
|
+
...assignment,
|
|
90
|
+
late: new Date(assignment.dueDate) < new Date(),
|
|
91
|
+
submitted: assignment.submissions.find(
|
|
92
|
+
(s) => s.studentId === userId,
|
|
93
|
+
)?.submitted ?? false,
|
|
94
|
+
returned: assignment.submissions.find(
|
|
95
|
+
(s) => s.studentId === userId,
|
|
96
|
+
)?.returned ?? false,
|
|
97
|
+
})),
|
|
89
98
|
members: [...cls.students, ...cls.teachers],
|
|
90
99
|
color: cls.color,
|
|
91
100
|
schoolId: cls.schoolId ?? null,
|
package/src/services/comment.ts
CHANGED
|
@@ -158,6 +158,28 @@ export async function deleteComment(userId: string, id: string) {
|
|
|
158
158
|
return { success: true };
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
+
/** Pin or unpin a comment. Only teachers can pin comments. */
|
|
162
|
+
export async function pinComment(userId: string, commentId: string, isPinned: boolean) {
|
|
163
|
+
const comment = await requireCommentAccess(commentId, userId);
|
|
164
|
+
if (comment.parentCommentId) {
|
|
165
|
+
throw new TRPCError({
|
|
166
|
+
code: "BAD_REQUEST",
|
|
167
|
+
message: "Only top-level comments can be pinned",
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
if (!isClassTeacher(comment, userId)) {
|
|
171
|
+
throw new TRPCError({
|
|
172
|
+
code: "FORBIDDEN",
|
|
173
|
+
message: "Only teachers can pin comments",
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
const updated = await prisma.comment.update({
|
|
177
|
+
where: { id: commentId },
|
|
178
|
+
data: { isPinned },
|
|
179
|
+
});
|
|
180
|
+
return updated;
|
|
181
|
+
}
|
|
182
|
+
|
|
161
183
|
/** Add or update a reaction (emoji) on a comment. */
|
|
162
184
|
export async function addReaction(
|
|
163
185
|
userId: string,
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
findConversationByIdAndMember,
|
|
16
16
|
createConversationMember,
|
|
17
17
|
hideConversationMember,
|
|
18
|
+
unhideConversationMember,
|
|
18
19
|
deleteConversationMember,
|
|
19
20
|
} from "../models/conversation.js";
|
|
20
21
|
|
|
@@ -160,10 +161,14 @@ export async function createConversationRecord(
|
|
|
160
161
|
});
|
|
161
162
|
|
|
162
163
|
if (existingDM) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
164
|
+
const userMembership = existingDM.members.find(
|
|
165
|
+
(m) => m.userId === userId
|
|
166
|
+
);
|
|
167
|
+
if (userMembership) {
|
|
168
|
+
await unhideConversationMember(userId, existingDM.id);
|
|
169
|
+
}
|
|
170
|
+
const conversation = await findConversationByIdAndMember(existingDM.id, userId);
|
|
171
|
+
return conversation!;
|
|
167
172
|
}
|
|
168
173
|
}
|
|
169
174
|
|
|
@@ -64,13 +64,15 @@ describe('Conversation Router', () => {
|
|
|
64
64
|
expect(conversation.type).toBe('GROUP');
|
|
65
65
|
});
|
|
66
66
|
|
|
67
|
-
test('should
|
|
68
|
-
await
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
).
|
|
67
|
+
test('should return existing DM instead of creating duplicate', async () => {
|
|
68
|
+
const existingConversation = await user1Caller.conversation.create({
|
|
69
|
+
type: 'DM',
|
|
70
|
+
memberIds: ['testuser2'],
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
expect(existingConversation).toBeDefined();
|
|
74
|
+
expect(existingConversation.id).toBe(dmConversation.id);
|
|
75
|
+
expect(existingConversation.members.length).toBe(2);
|
|
74
76
|
});
|
|
75
77
|
|
|
76
78
|
test('should fail to create GROUP without name', async () => {
|