@studious-lms/server 1.4.2 → 1.4.4
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/.coderabbit.yaml +4 -1
- package/.vscode/settings.json +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +68 -29
- package/dist/index.js.map +1 -1
- package/dist/lib/fileUpload.d.ts +10 -2
- package/dist/lib/fileUpload.d.ts.map +1 -1
- package/dist/lib/fileUpload.js +38 -6
- package/dist/lib/fileUpload.js.map +1 -1
- package/dist/middleware/auth.d.ts.map +1 -1
- package/dist/middleware/auth.js +18 -5
- package/dist/middleware/auth.js.map +1 -1
- package/dist/models/auth.d.ts +19 -14
- package/dist/models/auth.d.ts.map +1 -1
- package/dist/models/auth.js +53 -30
- package/dist/models/auth.js.map +1 -1
- package/dist/models/class.d.ts +3 -1
- package/dist/models/class.d.ts.map +1 -1
- package/dist/models/class.js +16 -4
- package/dist/models/class.js.map +1 -1
- package/dist/models/notification.d.ts +11 -10
- package/dist/models/notification.d.ts.map +1 -1
- package/dist/models/notification.js +13 -9
- package/dist/models/notification.js.map +1 -1
- package/dist/models/worksheet.d.ts +379 -7
- package/dist/models/worksheet.d.ts.map +1 -1
- package/dist/models/worksheet.js +168 -2
- package/dist/models/worksheet.js.map +1 -1
- package/dist/pipelines/aiLabChat.d.ts.map +1 -1
- package/dist/pipelines/aiLabChat.js +11 -3
- package/dist/pipelines/aiLabChat.js.map +1 -1
- package/dist/pipelines/aiLabChatContract.d.ts +3 -3
- package/dist/pipelines/aiNewtonChat.d.ts.map +1 -1
- package/dist/pipelines/aiNewtonChat.js +25 -14
- package/dist/pipelines/aiNewtonChat.js.map +1 -1
- package/dist/pipelines/gradeWorksheet.d.ts.map +1 -1
- package/dist/pipelines/gradeWorksheet.js +10 -3
- package/dist/pipelines/gradeWorksheet.js.map +1 -1
- package/dist/pipelines/pdfToWorksheet.d.ts +12 -0
- package/dist/pipelines/pdfToWorksheet.d.ts.map +1 -0
- package/dist/pipelines/pdfToWorksheet.js +155 -0
- package/dist/pipelines/pdfToWorksheet.js.map +1 -0
- package/dist/routers/_app.d.ts +264 -16
- package/dist/routers/_app.d.ts.map +1 -1
- package/dist/routers/class.d.ts +2 -1
- package/dist/routers/class.d.ts.map +1 -1
- package/dist/routers/class.js +3 -3
- package/dist/routers/class.js.map +1 -1
- package/dist/routers/labChat.d.ts +6 -0
- package/dist/routers/labChat.d.ts.map +1 -1
- package/dist/routers/labChat.js +5 -3
- package/dist/routers/labChat.js.map +1 -1
- package/dist/routers/message.d.ts +31 -0
- package/dist/routers/message.d.ts.map +1 -1
- package/dist/routers/message.js +27 -4
- package/dist/routers/message.js.map +1 -1
- package/dist/routers/newtonChat.d.ts +6 -0
- package/dist/routers/newtonChat.d.ts.map +1 -1
- package/dist/routers/newtonChat.js +5 -4
- package/dist/routers/newtonChat.js.map +1 -1
- package/dist/routers/notifications.d.ts +9 -2
- package/dist/routers/notifications.d.ts.map +1 -1
- package/dist/routers/notifications.js +4 -4
- package/dist/routers/notifications.js.map +1 -1
- package/dist/routers/worksheet.d.ts +78 -5
- package/dist/routers/worksheet.d.ts.map +1 -1
- package/dist/routers/worksheet.js +126 -17
- package/dist/routers/worksheet.js.map +1 -1
- package/dist/services/auth.d.ts +1 -1
- package/dist/services/auth.d.ts.map +1 -1
- package/dist/services/auth.js +19 -12
- package/dist/services/auth.js.map +1 -1
- package/dist/services/class.d.ts +1 -1
- package/dist/services/class.d.ts.map +1 -1
- package/dist/services/class.js +6 -3
- package/dist/services/class.js.map +1 -1
- package/dist/services/file.d.ts.map +1 -1
- package/dist/services/file.js +7 -2
- package/dist/services/file.js.map +1 -1
- package/dist/services/labChat.d.ts +6 -0
- package/dist/services/labChat.d.ts.map +1 -1
- package/dist/services/labChat.js +30 -18
- package/dist/services/labChat.js.map +1 -1
- package/dist/services/message.d.ts +30 -0
- package/dist/services/message.d.ts.map +1 -1
- package/dist/services/message.js +97 -21
- package/dist/services/message.js.map +1 -1
- package/dist/services/newtonChat.d.ts +6 -1
- package/dist/services/newtonChat.d.ts.map +1 -1
- package/dist/services/newtonChat.js +36 -18
- package/dist/services/newtonChat.js.map +1 -1
- package/dist/services/notification.d.ts +12 -5
- package/dist/services/notification.d.ts.map +1 -1
- package/dist/services/notification.js +24 -9
- package/dist/services/notification.js.map +1 -1
- package/dist/services/studentProgress.d.ts.map +1 -1
- package/dist/services/studentProgress.js +6 -3
- package/dist/services/studentProgress.js.map +1 -1
- package/dist/services/worksheet.d.ts +36 -15
- package/dist/services/worksheet.d.ts.map +1 -1
- package/dist/services/worksheet.js +186 -54
- package/dist/services/worksheet.js.map +1 -1
- package/dist/trpc.d.ts.map +1 -1
- package/dist/trpc.js +11 -17
- package/dist/trpc.js.map +1 -1
- package/dist/utils/inference.d.ts +10 -1
- package/dist/utils/inference.d.ts.map +1 -1
- package/dist/utils/inference.js +186 -50
- package/dist/utils/inference.js.map +1 -1
- package/package.json +1 -1
- package/prisma/migrations/20260415103500_add_session_type/migration.sql +33 -0
- package/prisma/migrations/20260415133000_add_ai_usage_logs/migration.sql +19 -0
- package/prisma/schema.prisma +27 -1
- package/scripts/test-pre-push.ts +6 -2
- package/src/index.ts +83 -30
- package/src/lib/fileUpload.ts +55 -5
- package/src/middleware/auth.ts +19 -4
- package/src/models/auth.ts +59 -29
- package/src/models/class.ts +15 -4
- package/src/models/notification.ts +13 -7
- package/src/models/worksheet.ts +203 -0
- package/src/pipelines/aiLabChat.ts +9 -1
- package/src/pipelines/aiNewtonChat.ts +24 -14
- package/src/pipelines/gradeWorksheet.ts +8 -0
- package/src/pipelines/pdfToWorksheet.ts +177 -0
- package/src/routers/class.ts +2 -2
- package/src/routers/labChat.ts +3 -1
- package/src/routers/message.ts +38 -1
- package/src/routers/newtonChat.ts +4 -2
- package/src/routers/notifications.ts +2 -2
- package/src/routers/worksheet.ts +174 -19
- package/src/services/auth.ts +26 -12
- package/src/services/class.ts +4 -1
- package/src/services/file.ts +6 -0
- package/src/services/labChat.ts +42 -18
- package/src/services/message.ts +163 -18
- package/src/services/newtonChat.ts +49 -18
- package/src/services/notification.ts +27 -8
- package/src/services/studentProgress.ts +4 -1
- package/src/services/worksheet.ts +252 -51
- package/src/trpc.ts +12 -18
- package/src/utils/inference.ts +280 -56
- package/tests/globalSetup.ts +13 -4
- package/tests/pipelines/pdfToWorksheet.test.ts +176 -0
- package/tests/routers/auth.test.ts +120 -17
- package/tests/routers/class.test.ts +7 -1
- package/tests/routers/message.test.ts +236 -9
- package/tests/routers/newtonChat.test.ts +81 -0
- package/tests/routers/notification.test.ts +12 -0
- package/tests/routers/worksheet.test.ts +194 -0
- package/tests/setup.ts +22 -5
- package/tests/utils/inference.test.ts +255 -0
|
@@ -5,10 +5,13 @@
|
|
|
5
5
|
import { TRPCError } from "@trpc/server";
|
|
6
6
|
import { GenerationStatus, type WorksheetQuestionType } from "@prisma/client";
|
|
7
7
|
import { prisma } from "../lib/prisma.js";
|
|
8
|
+
import { getFileUrl } from "../lib/fileUpload.js";
|
|
8
9
|
import { cancelGradePipeline, regradeWorksheetPipeline } from "../pipelines/gradeWorksheet.js";
|
|
10
|
+
import { pdfToWorksheet } from "../pipelines/pdfToWorksheet.js";
|
|
9
11
|
import {
|
|
10
|
-
findWorksheetById,
|
|
11
12
|
findWorksheetByIdMinimal,
|
|
13
|
+
findWorksheetByIdForClassMember,
|
|
14
|
+
findWorksheetByIdForTeacher,
|
|
12
15
|
findWorksheetsByClassId,
|
|
13
16
|
createWorksheet,
|
|
14
17
|
updateWorksheet,
|
|
@@ -17,30 +20,63 @@ import {
|
|
|
17
20
|
createWorksheetQuestion,
|
|
18
21
|
updateWorksheetQuestion,
|
|
19
22
|
deleteWorksheetQuestion,
|
|
20
|
-
|
|
23
|
+
findSubmissionByIdForWorksheetAccess,
|
|
21
24
|
findOrCreateWorksheetResponse,
|
|
22
|
-
|
|
23
|
-
|
|
25
|
+
findWorksheetResponseByIdForTeacher,
|
|
26
|
+
findWorksheetResponseWithResponsesForStudent,
|
|
27
|
+
findWorksheetQuestionByIdForTeacher,
|
|
28
|
+
findWorksheetQuestionByIdInWorksheet,
|
|
24
29
|
updateStudentQuestionProgress,
|
|
25
30
|
createStudentQuestionProgress,
|
|
26
31
|
findStudentQuestionProgress,
|
|
27
32
|
updateStudentQuestionProgressForGrading,
|
|
28
33
|
createStudentQuestionProgressForGrading,
|
|
29
34
|
createComment,
|
|
35
|
+
findStudentQuestionProgressByIdForTeacher,
|
|
36
|
+
findStudentQuestionProgressByIdForTeacherAndResponse,
|
|
30
37
|
} from "../models/worksheet.js";
|
|
31
38
|
|
|
32
39
|
type MCQOptions = { id: string; text: string; isCorrect: boolean }[];
|
|
33
40
|
|
|
41
|
+
const sanitizeQuestionOptionsForStudent = (options: unknown) => {
|
|
42
|
+
if (!Array.isArray(options)) return options;
|
|
43
|
+
|
|
44
|
+
return options.map((option) => {
|
|
45
|
+
if (!option || typeof option !== "object") return option;
|
|
46
|
+
|
|
47
|
+
const { isCorrect: _isCorrect, ...rest } = option as Record<string, unknown>;
|
|
48
|
+
return rest;
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const sanitizeWorksheetForStudent = <T extends {
|
|
53
|
+
questions: Array<Record<string, unknown>>;
|
|
54
|
+
}>(worksheet: T) => ({
|
|
55
|
+
...worksheet,
|
|
56
|
+
questions: worksheet.questions.map((question) => ({
|
|
57
|
+
...question,
|
|
58
|
+
answer: "",
|
|
59
|
+
markScheme: null,
|
|
60
|
+
options: sanitizeQuestionOptionsForStudent(question.options),
|
|
61
|
+
})),
|
|
62
|
+
});
|
|
63
|
+
|
|
34
64
|
/** Get worksheet with questions and mark schemes. */
|
|
35
|
-
export async function getWorksheet(worksheetId: string) {
|
|
36
|
-
const
|
|
37
|
-
if (
|
|
65
|
+
export async function getWorksheet(worksheetId: string, userId: string) {
|
|
66
|
+
const teacherWorksheet = await findWorksheetByIdForTeacher(worksheetId, userId);
|
|
67
|
+
if (teacherWorksheet) {
|
|
68
|
+
return teacherWorksheet;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const studentWorksheet = await findWorksheetByIdForClassMember(worksheetId, userId);
|
|
72
|
+
if (!studentWorksheet) {
|
|
38
73
|
throw new TRPCError({
|
|
39
|
-
code: "
|
|
40
|
-
message: "
|
|
74
|
+
code: "FORBIDDEN",
|
|
75
|
+
message: "You do not have access to this worksheet",
|
|
41
76
|
});
|
|
42
77
|
}
|
|
43
|
-
|
|
78
|
+
|
|
79
|
+
return sanitizeWorksheetForStudent(studentWorksheet);
|
|
44
80
|
}
|
|
45
81
|
|
|
46
82
|
/** Check if worksheet exists. */
|
|
@@ -59,12 +95,27 @@ export async function listWorksheets(classId: string) {
|
|
|
59
95
|
|
|
60
96
|
export async function updateWorksheetRecord(
|
|
61
97
|
worksheetId: string,
|
|
98
|
+
userId: string,
|
|
62
99
|
data: { name?: string }
|
|
63
100
|
) {
|
|
101
|
+
const worksheet = await findWorksheetByIdForTeacher(worksheetId, userId);
|
|
102
|
+
if (!worksheet) {
|
|
103
|
+
throw new TRPCError({
|
|
104
|
+
code: "FORBIDDEN",
|
|
105
|
+
message: "Only class teachers can update worksheets",
|
|
106
|
+
});
|
|
107
|
+
}
|
|
64
108
|
return updateWorksheet(worksheetId, data);
|
|
65
109
|
}
|
|
66
110
|
|
|
67
|
-
export async function deleteWorksheetRecord(worksheetId: string) {
|
|
111
|
+
export async function deleteWorksheetRecord(worksheetId: string, userId: string) {
|
|
112
|
+
const worksheet = await findWorksheetByIdForTeacher(worksheetId, userId);
|
|
113
|
+
if (!worksheet) {
|
|
114
|
+
throw new TRPCError({
|
|
115
|
+
code: "FORBIDDEN",
|
|
116
|
+
message: "Only class teachers can delete worksheets",
|
|
117
|
+
});
|
|
118
|
+
}
|
|
68
119
|
return deleteWorksheet(worksheetId);
|
|
69
120
|
}
|
|
70
121
|
|
|
@@ -72,8 +123,68 @@ export async function createWorksheetRecord(classId: string, name: string) {
|
|
|
72
123
|
return createWorksheet({ classId, name });
|
|
73
124
|
}
|
|
74
125
|
|
|
126
|
+
/**
|
|
127
|
+
* Create a worksheet by converting a PDF (URL) into structured questions.
|
|
128
|
+
* Uses the `pdfToWorksheet` pipeline to do inference + persistence.
|
|
129
|
+
*/
|
|
130
|
+
export async function createWorksheetFromPdf(
|
|
131
|
+
classId: string,
|
|
132
|
+
url: string
|
|
133
|
+
) {
|
|
134
|
+
const created = await pdfToWorksheet(url, classId);
|
|
135
|
+
if (!created) {
|
|
136
|
+
throw new TRPCError({
|
|
137
|
+
code: "INTERNAL_SERVER_ERROR",
|
|
138
|
+
message: "Failed to create worksheet from PDF",
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return created;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Create a worksheet from a PDF that was uploaded through the direct-upload flow.
|
|
146
|
+
*/
|
|
147
|
+
export async function createWorksheetFromUploadedPdf(
|
|
148
|
+
classId: string,
|
|
149
|
+
fileId: string,
|
|
150
|
+
userId: string
|
|
151
|
+
) {
|
|
152
|
+
const file = await prisma.file.findFirst({
|
|
153
|
+
where: {
|
|
154
|
+
id: fileId,
|
|
155
|
+
userId,
|
|
156
|
+
uploadStatus: "COMPLETED",
|
|
157
|
+
},
|
|
158
|
+
select: {
|
|
159
|
+
id: true,
|
|
160
|
+
path: true,
|
|
161
|
+
type: true,
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
if (!file) {
|
|
166
|
+
throw new TRPCError({
|
|
167
|
+
code: "NOT_FOUND",
|
|
168
|
+
message: "Uploaded file not found or not ready",
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const isPdf =
|
|
173
|
+
file.type === "application/pdf" || file.path.toLowerCase().endsWith(".pdf");
|
|
174
|
+
if (!isPdf) {
|
|
175
|
+
throw new TRPCError({
|
|
176
|
+
code: "BAD_REQUEST",
|
|
177
|
+
message: "Only PDF files can be converted into worksheets",
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const signedUrl = await getFileUrl(file.path);
|
|
182
|
+
return createWorksheetFromPdf(classId, signedUrl);
|
|
183
|
+
}
|
|
184
|
+
|
|
75
185
|
export async function addQuestionToWorksheet(
|
|
76
186
|
worksheetId: string,
|
|
187
|
+
userId: string,
|
|
77
188
|
data: {
|
|
78
189
|
question: string;
|
|
79
190
|
answer: string;
|
|
@@ -83,11 +194,11 @@ export async function addQuestionToWorksheet(
|
|
|
83
194
|
type: WorksheetQuestionType;
|
|
84
195
|
}
|
|
85
196
|
) {
|
|
86
|
-
const worksheet = await
|
|
197
|
+
const worksheet = await findWorksheetByIdForTeacher(worksheetId, userId);
|
|
87
198
|
if (!worksheet) {
|
|
88
199
|
throw new TRPCError({
|
|
89
|
-
code: "
|
|
90
|
-
message: "
|
|
200
|
+
code: "FORBIDDEN",
|
|
201
|
+
message: "Only class teachers can modify worksheet questions",
|
|
91
202
|
});
|
|
92
203
|
}
|
|
93
204
|
return createWorksheetQuestion({
|
|
@@ -98,15 +209,16 @@ export async function addQuestionToWorksheet(
|
|
|
98
209
|
|
|
99
210
|
export async function reorderWorksheetQuestions(
|
|
100
211
|
worksheetId: string,
|
|
212
|
+
userId: string,
|
|
101
213
|
movedId: string,
|
|
102
214
|
position: "before" | "after",
|
|
103
215
|
targetId: string
|
|
104
216
|
) {
|
|
105
|
-
const worksheet = await
|
|
217
|
+
const worksheet = await findWorksheetByIdForTeacher(worksheetId, userId);
|
|
106
218
|
if (!worksheet) {
|
|
107
219
|
throw new TRPCError({
|
|
108
|
-
code: "
|
|
109
|
-
message: "
|
|
220
|
+
code: "FORBIDDEN",
|
|
221
|
+
message: "Only class teachers can reorder worksheet questions",
|
|
110
222
|
});
|
|
111
223
|
}
|
|
112
224
|
|
|
@@ -158,6 +270,7 @@ export async function reorderWorksheetQuestions(
|
|
|
158
270
|
export async function updateWorksheetQuestionRecord(
|
|
159
271
|
worksheetId: string,
|
|
160
272
|
questionId: string,
|
|
273
|
+
userId: string,
|
|
161
274
|
data: {
|
|
162
275
|
question?: string;
|
|
163
276
|
answer?: string;
|
|
@@ -167,11 +280,11 @@ export async function updateWorksheetQuestionRecord(
|
|
|
167
280
|
type?: WorksheetQuestionType;
|
|
168
281
|
}
|
|
169
282
|
) {
|
|
170
|
-
const
|
|
171
|
-
if (!
|
|
283
|
+
const question = await findWorksheetQuestionByIdForTeacher(questionId, userId);
|
|
284
|
+
if (!question || question.worksheetId !== worksheetId) {
|
|
172
285
|
throw new TRPCError({
|
|
173
|
-
code: "
|
|
174
|
-
message: "
|
|
286
|
+
code: "FORBIDDEN",
|
|
287
|
+
message: "Only class teachers can update worksheet questions",
|
|
175
288
|
});
|
|
176
289
|
}
|
|
177
290
|
return updateWorksheetQuestion(questionId, data);
|
|
@@ -179,13 +292,14 @@ export async function updateWorksheetQuestionRecord(
|
|
|
179
292
|
|
|
180
293
|
export async function deleteWorksheetQuestionRecord(
|
|
181
294
|
worksheetId: string,
|
|
182
|
-
questionId: string
|
|
295
|
+
questionId: string,
|
|
296
|
+
userId: string
|
|
183
297
|
) {
|
|
184
|
-
const
|
|
185
|
-
if (!
|
|
298
|
+
const question = await findWorksheetQuestionByIdForTeacher(questionId, userId);
|
|
299
|
+
if (!question || question.worksheetId !== worksheetId) {
|
|
186
300
|
throw new TRPCError({
|
|
187
|
-
code: "
|
|
188
|
-
message: "
|
|
301
|
+
code: "FORBIDDEN",
|
|
302
|
+
message: "Only class teachers can delete worksheet questions",
|
|
189
303
|
});
|
|
190
304
|
}
|
|
191
305
|
return deleteWorksheetQuestion(questionId);
|
|
@@ -193,13 +307,27 @@ export async function deleteWorksheetQuestionRecord(
|
|
|
193
307
|
|
|
194
308
|
export async function getWorksheetSubmission(
|
|
195
309
|
worksheetId: string,
|
|
196
|
-
submissionId: string
|
|
310
|
+
submissionId: string,
|
|
311
|
+
userId: string
|
|
197
312
|
) {
|
|
198
|
-
const
|
|
313
|
+
const worksheet = await findWorksheetByIdForClassMember(worksheetId, userId);
|
|
314
|
+
if (!worksheet) {
|
|
315
|
+
throw new TRPCError({
|
|
316
|
+
code: "FORBIDDEN",
|
|
317
|
+
message: "You do not have access to this worksheet",
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const submission = await findSubmissionByIdForWorksheetAccess(
|
|
322
|
+
submissionId,
|
|
323
|
+
worksheetId,
|
|
324
|
+
worksheet.classId,
|
|
325
|
+
userId
|
|
326
|
+
);
|
|
199
327
|
if (!submission) {
|
|
200
328
|
throw new TRPCError({
|
|
201
|
-
code: "
|
|
202
|
-
message: "
|
|
329
|
+
code: "FORBIDDEN",
|
|
330
|
+
message: "You do not have access to this worksheet submission",
|
|
203
331
|
});
|
|
204
332
|
}
|
|
205
333
|
return findOrCreateWorksheetResponse(
|
|
@@ -215,25 +343,36 @@ export async function answerWorksheetQuestion(
|
|
|
215
343
|
response: string,
|
|
216
344
|
userId: string
|
|
217
345
|
) {
|
|
218
|
-
const worksheetResponse = await
|
|
346
|
+
const worksheetResponse = await findWorksheetResponseWithResponsesForStudent(
|
|
219
347
|
worksheetResponseId,
|
|
348
|
+
userId,
|
|
220
349
|
questionId
|
|
221
350
|
);
|
|
222
351
|
if (!worksheetResponse) {
|
|
352
|
+
throw new TRPCError({
|
|
353
|
+
code: "FORBIDDEN",
|
|
354
|
+
message: "You can only answer your own worksheet responses",
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const question = await findWorksheetQuestionByIdInWorksheet(
|
|
359
|
+
questionId,
|
|
360
|
+
worksheetResponse.worksheetId
|
|
361
|
+
);
|
|
362
|
+
if (!question) {
|
|
223
363
|
throw new TRPCError({
|
|
224
364
|
code: "NOT_FOUND",
|
|
225
|
-
message: "Worksheet
|
|
365
|
+
message: "Worksheet question not found",
|
|
226
366
|
});
|
|
227
367
|
}
|
|
228
368
|
|
|
229
|
-
const question = await findWorksheetQuestionById(questionId);
|
|
230
369
|
const isMarkableByAlgo =
|
|
231
|
-
question
|
|
232
|
-
const marksAwardedIfCorrect = question
|
|
370
|
+
question.type === "MULTIPLE_CHOICE" || question.type === "TRUE_FALSE";
|
|
371
|
+
const marksAwardedIfCorrect = question.points || 0;
|
|
233
372
|
const correctAnswer = isMarkableByAlgo
|
|
234
|
-
? question
|
|
235
|
-
? (question
|
|
236
|
-
: question
|
|
373
|
+
? question.type === "MULTIPLE_CHOICE"
|
|
374
|
+
? (question.options as MCQOptions)?.find((o) => o.isCorrect)?.id
|
|
375
|
+
: question.answer?.toString()
|
|
237
376
|
: null;
|
|
238
377
|
|
|
239
378
|
const existingResponse = worksheetResponse.responses[0];
|
|
@@ -273,21 +412,46 @@ export async function answerWorksheetQuestion(
|
|
|
273
412
|
|
|
274
413
|
export async function cancelGrading(
|
|
275
414
|
worksheetResponseId: string,
|
|
276
|
-
progressId: string
|
|
415
|
+
progressId: string,
|
|
416
|
+
userId: string
|
|
277
417
|
) {
|
|
418
|
+
const progress = await findStudentQuestionProgressByIdForTeacherAndResponse(
|
|
419
|
+
progressId,
|
|
420
|
+
worksheetResponseId,
|
|
421
|
+
userId
|
|
422
|
+
);
|
|
423
|
+
if (!progress) {
|
|
424
|
+
throw new TRPCError({
|
|
425
|
+
code: "FORBIDDEN",
|
|
426
|
+
message: "Only class teachers can cancel worksheet grading",
|
|
427
|
+
});
|
|
428
|
+
}
|
|
278
429
|
return cancelGradePipeline(worksheetResponseId, progressId);
|
|
279
430
|
}
|
|
280
431
|
|
|
281
432
|
export async function regradeQuestion(
|
|
282
433
|
worksheetResponseId: string,
|
|
283
|
-
progressId: string
|
|
434
|
+
progressId: string,
|
|
435
|
+
userId: string
|
|
284
436
|
) {
|
|
437
|
+
const progress = await findStudentQuestionProgressByIdForTeacherAndResponse(
|
|
438
|
+
progressId,
|
|
439
|
+
worksheetResponseId,
|
|
440
|
+
userId
|
|
441
|
+
);
|
|
442
|
+
if (!progress) {
|
|
443
|
+
throw new TRPCError({
|
|
444
|
+
code: "FORBIDDEN",
|
|
445
|
+
message: "Only class teachers can regrade worksheet answers",
|
|
446
|
+
});
|
|
447
|
+
}
|
|
285
448
|
return regradeWorksheetPipeline(worksheetResponseId, progressId);
|
|
286
449
|
}
|
|
287
450
|
|
|
288
451
|
export async function gradeAnswer(
|
|
289
452
|
questionId: string,
|
|
290
453
|
studentWorksheetResponseId: string,
|
|
454
|
+
userId: string,
|
|
291
455
|
data: {
|
|
292
456
|
responseId?: string;
|
|
293
457
|
response?: string;
|
|
@@ -297,7 +461,44 @@ export async function gradeAnswer(
|
|
|
297
461
|
points?: number;
|
|
298
462
|
}
|
|
299
463
|
) {
|
|
464
|
+
const worksheetResponse = await findWorksheetResponseByIdForTeacher(
|
|
465
|
+
studentWorksheetResponseId,
|
|
466
|
+
userId
|
|
467
|
+
);
|
|
468
|
+
if (!worksheetResponse) {
|
|
469
|
+
throw new TRPCError({
|
|
470
|
+
code: "FORBIDDEN",
|
|
471
|
+
message: "Only class teachers can grade worksheet answers",
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const question = await findWorksheetQuestionByIdInWorksheet(
|
|
476
|
+
questionId,
|
|
477
|
+
worksheetResponse.worksheetId
|
|
478
|
+
);
|
|
479
|
+
if (!question) {
|
|
480
|
+
throw new TRPCError({
|
|
481
|
+
code: "NOT_FOUND",
|
|
482
|
+
message: "Worksheet question not found",
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
|
|
300
486
|
if (data.responseId) {
|
|
487
|
+
const existingProgress = await findStudentQuestionProgressByIdForTeacher(
|
|
488
|
+
data.responseId,
|
|
489
|
+
userId
|
|
490
|
+
);
|
|
491
|
+
if (
|
|
492
|
+
!existingProgress ||
|
|
493
|
+
existingProgress.studentWorksheetResponseId !== studentWorksheetResponseId ||
|
|
494
|
+
existingProgress.questionId !== questionId
|
|
495
|
+
) {
|
|
496
|
+
throw new TRPCError({
|
|
497
|
+
code: "FORBIDDEN",
|
|
498
|
+
message: "Only class teachers can grade worksheet answers",
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
|
|
301
502
|
return updateStudentQuestionProgressForGrading(data.responseId, {
|
|
302
503
|
isCorrect: data.isCorrect,
|
|
303
504
|
feedback: data.feedback,
|
|
@@ -306,17 +507,6 @@ export async function gradeAnswer(
|
|
|
306
507
|
});
|
|
307
508
|
}
|
|
308
509
|
|
|
309
|
-
const worksheetResponse = await prisma.studentWorksheetResponse.findUnique({
|
|
310
|
-
where: { id: studentWorksheetResponseId },
|
|
311
|
-
select: { studentId: true },
|
|
312
|
-
});
|
|
313
|
-
if (!worksheetResponse) {
|
|
314
|
-
throw new TRPCError({
|
|
315
|
-
code: "NOT_FOUND",
|
|
316
|
-
message: "Student worksheet response not found",
|
|
317
|
-
});
|
|
318
|
-
}
|
|
319
|
-
|
|
320
510
|
const existing = await findStudentQuestionProgress(
|
|
321
511
|
worksheetResponse.studentId,
|
|
322
512
|
questionId,
|
|
@@ -350,6 +540,17 @@ export async function addCommentToResponse(
|
|
|
350
540
|
comment: string,
|
|
351
541
|
authorId: string
|
|
352
542
|
) {
|
|
543
|
+
const progress = await findStudentQuestionProgressByIdForTeacher(
|
|
544
|
+
responseId,
|
|
545
|
+
authorId
|
|
546
|
+
);
|
|
547
|
+
if (!progress) {
|
|
548
|
+
throw new TRPCError({
|
|
549
|
+
code: "FORBIDDEN",
|
|
550
|
+
message: "Only class teachers can comment on worksheet responses",
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
|
|
353
554
|
return createComment({
|
|
354
555
|
studentQuestionProgressId: responseId,
|
|
355
556
|
content: comment,
|
package/src/trpc.ts
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { initTRPC, TRPCError } from '@trpc/server';
|
|
2
2
|
import { ZodError } from 'zod';
|
|
3
3
|
import { logger } from './utils/logger.js';
|
|
4
|
-
import { prisma } from './lib/prisma.js';
|
|
5
4
|
import { createLoggingMiddleware } from './middleware/logging.js';
|
|
6
5
|
import { createAuthMiddleware } from './middleware/auth.js';
|
|
7
6
|
import { Request, Response } from 'express';
|
|
8
7
|
import { z } from 'zod';
|
|
9
8
|
import { handlePrismaError, PrismaErrorInfo } from './utils/prismaErrorHandler.js';
|
|
10
|
-
import {
|
|
9
|
+
import { findUserBySessionToken } from './models/auth.js';
|
|
11
10
|
|
|
12
11
|
interface CreateContextOptions {
|
|
13
12
|
req: Request;
|
|
@@ -26,22 +25,17 @@ export type Context = {
|
|
|
26
25
|
|
|
27
26
|
export const createTRPCContext = async (opts: CreateContextOptions): Promise<Context> => {
|
|
28
27
|
const { req, res } = opts;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
select: {
|
|
41
|
-
id: true,
|
|
42
|
-
}
|
|
43
|
-
}) : null;
|
|
44
|
-
|
|
28
|
+
|
|
29
|
+
const userHeader = req.headers["x-user"];
|
|
30
|
+
const tokenFromUserHeader = typeof userHeader === "string" ? userHeader : userHeader?.[0];
|
|
31
|
+
const authorizationHeader = req.headers.authorization;
|
|
32
|
+
const tokenFromAuthorization =
|
|
33
|
+
typeof authorizationHeader === "string" && authorizationHeader.startsWith("Bearer ")
|
|
34
|
+
? authorizationHeader.slice(7)
|
|
35
|
+
: null;
|
|
36
|
+
const token = tokenFromUserHeader ?? tokenFromAuthorization;
|
|
37
|
+
const user = token ? await findUserBySessionToken(token) : null;
|
|
38
|
+
|
|
45
39
|
return {
|
|
46
40
|
req,
|
|
47
41
|
res,
|