@studious-lms/server 1.0.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/LICENSE.txt +8 -0
- package/README.md +143 -0
- package/dist/exportType.d.ts +9 -0
- package/dist/exportType.d.ts.map +1 -0
- package/dist/exportType.js +7 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +82 -0
- package/dist/lib/fileUpload.d.ts +38 -0
- package/dist/lib/fileUpload.d.ts.map +1 -0
- package/dist/lib/fileUpload.js +136 -0
- package/dist/lib/googleCloudStorage.d.ts +20 -0
- package/dist/lib/googleCloudStorage.d.ts.map +1 -0
- package/dist/lib/googleCloudStorage.js +88 -0
- package/dist/lib/prisma.d.ts +8 -0
- package/dist/lib/prisma.d.ts.map +1 -0
- package/dist/lib/prisma.js +11 -0
- package/dist/lib/thumbnailGenerator.d.ts +23 -0
- package/dist/lib/thumbnailGenerator.d.ts.map +1 -0
- package/dist/lib/thumbnailGenerator.js +180 -0
- package/dist/logger.d.ts +26 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +135 -0
- package/dist/middleware/auth.d.ts +7 -0
- package/dist/middleware/auth.d.ts.map +1 -0
- package/dist/middleware/auth.js +170 -0
- package/dist/middleware/logging.d.ts +2 -0
- package/dist/middleware/logging.d.ts.map +1 -0
- package/dist/middleware/logging.js +51 -0
- package/dist/routers/_app.d.ts +4369 -0
- package/dist/routers/_app.d.ts.map +1 -0
- package/dist/routers/_app.js +29 -0
- package/dist/routers/agenda.d.ts +66 -0
- package/dist/routers/agenda.d.ts.map +1 -0
- package/dist/routers/agenda.js +78 -0
- package/dist/routers/announcement.d.ts +79 -0
- package/dist/routers/announcement.d.ts.map +1 -0
- package/dist/routers/announcement.js +122 -0
- package/dist/routers/assignment.d.ts +1051 -0
- package/dist/routers/assignment.d.ts.map +1 -0
- package/dist/routers/assignment.js +1497 -0
- package/dist/routers/attendance.d.ts +99 -0
- package/dist/routers/attendance.d.ts.map +1 -0
- package/dist/routers/attendance.js +269 -0
- package/dist/routers/auth.d.ts +87 -0
- package/dist/routers/auth.d.ts.map +1 -0
- package/dist/routers/auth.js +255 -0
- package/dist/routers/class.d.ts +427 -0
- package/dist/routers/class.d.ts.map +1 -0
- package/dist/routers/class.js +693 -0
- package/dist/routers/event.d.ts +249 -0
- package/dist/routers/event.d.ts.map +1 -0
- package/dist/routers/event.js +467 -0
- package/dist/routers/file.d.ts +27 -0
- package/dist/routers/file.d.ts.map +1 -0
- package/dist/routers/file.js +88 -0
- package/dist/routers/section.d.ts +51 -0
- package/dist/routers/section.d.ts.map +1 -0
- package/dist/routers/section.js +123 -0
- package/dist/routers/user.d.ts +49 -0
- package/dist/routers/user.d.ts.map +1 -0
- package/dist/routers/user.js +74 -0
- package/dist/socket/handlers.d.ts +3 -0
- package/dist/socket/handlers.d.ts.map +1 -0
- package/dist/socket/handlers.js +130 -0
- package/dist/trpc.d.ts +147 -0
- package/dist/trpc.d.ts.map +1 -0
- package/dist/trpc.js +67 -0
- package/dist/types/trpc.d.ts +16 -0
- package/dist/types/trpc.d.ts.map +1 -0
- package/dist/types/trpc.js +2 -0
- package/dist/utils/email.d.ts +3 -0
- package/dist/utils/email.d.ts.map +1 -0
- package/dist/utils/email.js +16 -0
- package/dist/utils/generateInviteCode.d.ts +6 -0
- package/dist/utils/generateInviteCode.d.ts.map +1 -0
- package/dist/utils/generateInviteCode.js +11 -0
- package/dist/utils/logger.d.ts +27 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +124 -0
- package/generated/prisma/client.d.ts +1 -0
- package/generated/prisma/client.js +4 -0
- package/generated/prisma/default.d.ts +1 -0
- package/generated/prisma/default.js +4 -0
- package/generated/prisma/edge.d.ts +1 -0
- package/generated/prisma/edge.js +389 -0
- package/generated/prisma/index-browser.js +375 -0
- package/generated/prisma/index.d.ts +34865 -0
- package/generated/prisma/index.js +410 -0
- package/generated/prisma/libquery_engine-darwin-arm64.dylib.node +0 -0
- package/generated/prisma/package.json +140 -0
- package/generated/prisma/runtime/edge-esm.js +34 -0
- package/generated/prisma/runtime/edge.js +34 -0
- package/generated/prisma/runtime/index-browser.d.ts +370 -0
- package/generated/prisma/runtime/index-browser.js +16 -0
- package/generated/prisma/runtime/library.d.ts +3647 -0
- package/generated/prisma/runtime/library.js +146 -0
- package/generated/prisma/runtime/react-native.js +83 -0
- package/generated/prisma/runtime/wasm.js +35 -0
- package/generated/prisma/schema.prisma +304 -0
- package/generated/prisma/wasm.d.ts +1 -0
- package/generated/prisma/wasm.js +375 -0
- package/package.json +46 -0
|
@@ -0,0 +1,1497 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.assignmentRouter = void 0;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
const trpc_1 = require("../trpc");
|
|
6
|
+
const server_1 = require("@trpc/server");
|
|
7
|
+
const prisma_1 = require("@lib/prisma");
|
|
8
|
+
const fileUpload_1 = require("@lib/fileUpload");
|
|
9
|
+
const googleCloudStorage_1 = require("@lib/googleCloudStorage");
|
|
10
|
+
const fileSchema = zod_1.z.object({
|
|
11
|
+
name: zod_1.z.string(),
|
|
12
|
+
type: zod_1.z.string(),
|
|
13
|
+
size: zod_1.z.number(),
|
|
14
|
+
data: zod_1.z.string(), // base64 encoded file data
|
|
15
|
+
});
|
|
16
|
+
const createAssignmentSchema = zod_1.z.object({
|
|
17
|
+
classId: zod_1.z.string(),
|
|
18
|
+
title: zod_1.z.string(),
|
|
19
|
+
instructions: zod_1.z.string(),
|
|
20
|
+
dueDate: zod_1.z.string(),
|
|
21
|
+
files: zod_1.z.array(fileSchema).optional(),
|
|
22
|
+
maxGrade: zod_1.z.number().optional(),
|
|
23
|
+
graded: zod_1.z.boolean().optional(),
|
|
24
|
+
weight: zod_1.z.number().optional(),
|
|
25
|
+
sectionId: zod_1.z.string().optional(),
|
|
26
|
+
type: zod_1.z.enum(['HOMEWORK', 'QUIZ', 'TEST', 'PROJECT', 'ESSAY', 'DISCUSSION', 'PRESENTATION', 'LAB', 'OTHER']).optional(),
|
|
27
|
+
markSchemeId: zod_1.z.string().optional(),
|
|
28
|
+
gradingBoundaryId: zod_1.z.string().optional(),
|
|
29
|
+
});
|
|
30
|
+
const updateAssignmentSchema = zod_1.z.object({
|
|
31
|
+
classId: zod_1.z.string(),
|
|
32
|
+
id: zod_1.z.string(),
|
|
33
|
+
title: zod_1.z.string().optional(),
|
|
34
|
+
instructions: zod_1.z.string().optional(),
|
|
35
|
+
dueDate: zod_1.z.string().optional(),
|
|
36
|
+
files: zod_1.z.array(fileSchema).optional(),
|
|
37
|
+
removedAttachments: zod_1.z.array(zod_1.z.string()).optional(),
|
|
38
|
+
maxGrade: zod_1.z.number().optional(),
|
|
39
|
+
graded: zod_1.z.boolean().optional(),
|
|
40
|
+
weight: zod_1.z.number().optional(),
|
|
41
|
+
sectionId: zod_1.z.string().nullable().optional(),
|
|
42
|
+
type: zod_1.z.enum(['HOMEWORK', 'QUIZ', 'TEST', 'PROJECT', 'ESSAY', 'DISCUSSION', 'PRESENTATION', 'LAB', 'OTHER']).optional(),
|
|
43
|
+
});
|
|
44
|
+
const deleteAssignmentSchema = zod_1.z.object({
|
|
45
|
+
id: zod_1.z.string(),
|
|
46
|
+
classId: zod_1.z.string(),
|
|
47
|
+
});
|
|
48
|
+
const getAssignmentSchema = zod_1.z.object({
|
|
49
|
+
id: zod_1.z.string(),
|
|
50
|
+
classId: zod_1.z.string(),
|
|
51
|
+
});
|
|
52
|
+
const submissionSchema = zod_1.z.object({
|
|
53
|
+
assignmentId: zod_1.z.string(),
|
|
54
|
+
classId: zod_1.z.string(),
|
|
55
|
+
submissionId: zod_1.z.string(),
|
|
56
|
+
submit: zod_1.z.boolean().optional(),
|
|
57
|
+
newAttachments: zod_1.z.array(fileSchema).optional(),
|
|
58
|
+
removedAttachments: zod_1.z.array(zod_1.z.string()).optional(),
|
|
59
|
+
});
|
|
60
|
+
const updateSubmissionSchema = zod_1.z.object({
|
|
61
|
+
assignmentId: zod_1.z.string(),
|
|
62
|
+
classId: zod_1.z.string(),
|
|
63
|
+
submissionId: zod_1.z.string(),
|
|
64
|
+
return: zod_1.z.boolean().optional(),
|
|
65
|
+
gradeReceived: zod_1.z.number().nullable().optional(),
|
|
66
|
+
newAttachments: zod_1.z.array(fileSchema).optional(),
|
|
67
|
+
removedAttachments: zod_1.z.array(zod_1.z.string()).optional(),
|
|
68
|
+
rubricGrades: zod_1.z.array(zod_1.z.object({
|
|
69
|
+
criteriaId: zod_1.z.string(),
|
|
70
|
+
selectedLevelId: zod_1.z.string(),
|
|
71
|
+
points: zod_1.z.number(),
|
|
72
|
+
comments: zod_1.z.string(),
|
|
73
|
+
})).optional(),
|
|
74
|
+
});
|
|
75
|
+
exports.assignmentRouter = (0, trpc_1.createTRPCRouter)({
|
|
76
|
+
create: trpc_1.protectedProcedure
|
|
77
|
+
.input(createAssignmentSchema)
|
|
78
|
+
.mutation(async ({ ctx, input }) => {
|
|
79
|
+
const { classId, title, instructions, dueDate, files, maxGrade, graded, weight, sectionId, type, markSchemeId, gradingBoundaryId } = input;
|
|
80
|
+
if (!ctx.user) {
|
|
81
|
+
throw new server_1.TRPCError({
|
|
82
|
+
code: "UNAUTHORIZED",
|
|
83
|
+
message: "User must be authenticated",
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
// Get all students in the class
|
|
87
|
+
const classData = await prisma_1.prisma.class.findUnique({
|
|
88
|
+
where: { id: classId },
|
|
89
|
+
include: {
|
|
90
|
+
students: {
|
|
91
|
+
select: { id: true }
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
if (!classData) {
|
|
96
|
+
throw new server_1.TRPCError({
|
|
97
|
+
code: "NOT_FOUND",
|
|
98
|
+
message: "Class not found",
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
const rubric = await prisma_1.prisma.markScheme.findUnique({
|
|
102
|
+
where: { id: markSchemeId },
|
|
103
|
+
select: {
|
|
104
|
+
structured: true,
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
const parsedRubric = JSON.parse(rubric?.structured || "{}");
|
|
108
|
+
// Calculate max grade from rubric criteria levels
|
|
109
|
+
const computedMaxGrade = parsedRubric.criteria.reduce((acc, criterion) => {
|
|
110
|
+
const maxPoints = Math.max(...criterion.levels.map((level) => level.points));
|
|
111
|
+
return acc + maxPoints;
|
|
112
|
+
}, 0);
|
|
113
|
+
// Create assignment with submissions for all students
|
|
114
|
+
const assignment = await prisma_1.prisma.assignment.create({
|
|
115
|
+
data: {
|
|
116
|
+
title,
|
|
117
|
+
instructions,
|
|
118
|
+
dueDate: new Date(dueDate),
|
|
119
|
+
maxGrade: markSchemeId ? computedMaxGrade : maxGrade,
|
|
120
|
+
graded,
|
|
121
|
+
weight,
|
|
122
|
+
type,
|
|
123
|
+
class: {
|
|
124
|
+
connect: { id: classId }
|
|
125
|
+
},
|
|
126
|
+
...(sectionId && {
|
|
127
|
+
section: {
|
|
128
|
+
connect: { id: sectionId }
|
|
129
|
+
}
|
|
130
|
+
}),
|
|
131
|
+
...(markSchemeId && {
|
|
132
|
+
markScheme: {
|
|
133
|
+
connect: { id: markSchemeId }
|
|
134
|
+
}
|
|
135
|
+
}),
|
|
136
|
+
...(gradingBoundaryId && {
|
|
137
|
+
gradingBoundary: {
|
|
138
|
+
connect: { id: gradingBoundaryId }
|
|
139
|
+
}
|
|
140
|
+
}),
|
|
141
|
+
submissions: {
|
|
142
|
+
create: classData.students.map((student) => ({
|
|
143
|
+
student: {
|
|
144
|
+
connect: { id: student.id }
|
|
145
|
+
}
|
|
146
|
+
}))
|
|
147
|
+
},
|
|
148
|
+
teacher: {
|
|
149
|
+
connect: { id: ctx.user.id }
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
select: {
|
|
153
|
+
id: true,
|
|
154
|
+
title: true,
|
|
155
|
+
instructions: true,
|
|
156
|
+
dueDate: true,
|
|
157
|
+
maxGrade: true,
|
|
158
|
+
graded: true,
|
|
159
|
+
weight: true,
|
|
160
|
+
type: true,
|
|
161
|
+
attachments: {
|
|
162
|
+
select: {
|
|
163
|
+
id: true,
|
|
164
|
+
name: true,
|
|
165
|
+
type: true,
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
section: {
|
|
169
|
+
select: {
|
|
170
|
+
id: true,
|
|
171
|
+
name: true
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
teacher: {
|
|
175
|
+
select: {
|
|
176
|
+
id: true,
|
|
177
|
+
username: true
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
class: {
|
|
181
|
+
select: {
|
|
182
|
+
id: true,
|
|
183
|
+
name: true
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
// Upload files if provided
|
|
189
|
+
let uploadedFiles = [];
|
|
190
|
+
if (files && files.length > 0) {
|
|
191
|
+
// Store files in a class and assignment specific directory
|
|
192
|
+
uploadedFiles = await (0, fileUpload_1.uploadFiles)(files, ctx.user.id, `class/${classId}/assignment/${assignment.id}`);
|
|
193
|
+
}
|
|
194
|
+
// Update assignment with new file attachments
|
|
195
|
+
if (uploadedFiles.length > 0) {
|
|
196
|
+
await prisma_1.prisma.assignment.update({
|
|
197
|
+
where: { id: assignment.id },
|
|
198
|
+
data: {
|
|
199
|
+
attachments: {
|
|
200
|
+
create: uploadedFiles.map(file => ({
|
|
201
|
+
name: file.name,
|
|
202
|
+
type: file.type,
|
|
203
|
+
size: file.size,
|
|
204
|
+
path: file.path,
|
|
205
|
+
...(file.thumbnailId && {
|
|
206
|
+
thumbnail: {
|
|
207
|
+
connect: { id: file.thumbnailId }
|
|
208
|
+
}
|
|
209
|
+
})
|
|
210
|
+
}))
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
return assignment;
|
|
216
|
+
}),
|
|
217
|
+
update: trpc_1.protectedProcedure
|
|
218
|
+
.input(updateAssignmentSchema)
|
|
219
|
+
.mutation(async ({ ctx, input }) => {
|
|
220
|
+
const { id, title, instructions, dueDate, files, maxGrade, graded, weight, sectionId, type } = input;
|
|
221
|
+
if (!ctx.user) {
|
|
222
|
+
throw new server_1.TRPCError({
|
|
223
|
+
code: "UNAUTHORIZED",
|
|
224
|
+
message: "User must be authenticated",
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
// Get the assignment with current attachments
|
|
228
|
+
const assignment = await prisma_1.prisma.assignment.findFirst({
|
|
229
|
+
where: {
|
|
230
|
+
id,
|
|
231
|
+
teacherId: ctx.user.id,
|
|
232
|
+
},
|
|
233
|
+
include: {
|
|
234
|
+
attachments: {
|
|
235
|
+
select: {
|
|
236
|
+
id: true,
|
|
237
|
+
name: true,
|
|
238
|
+
type: true,
|
|
239
|
+
thumbnail: {
|
|
240
|
+
select: {
|
|
241
|
+
path: true
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
class: {
|
|
247
|
+
select: {
|
|
248
|
+
id: true,
|
|
249
|
+
name: true
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
if (!assignment) {
|
|
255
|
+
throw new server_1.TRPCError({
|
|
256
|
+
code: "NOT_FOUND",
|
|
257
|
+
message: "Assignment not found",
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
// Upload new files if provided
|
|
261
|
+
let uploadedFiles = [];
|
|
262
|
+
if (files && files.length > 0) {
|
|
263
|
+
// Store files in a class and assignment specific directory
|
|
264
|
+
uploadedFiles = await (0, fileUpload_1.uploadFiles)(files, ctx.user.id, `class/${assignment.classId}/assignment/${id}`);
|
|
265
|
+
}
|
|
266
|
+
// Update assignment
|
|
267
|
+
const updatedAssignment = await prisma_1.prisma.assignment.update({
|
|
268
|
+
where: { id },
|
|
269
|
+
data: {
|
|
270
|
+
...(title && { title }),
|
|
271
|
+
...(instructions && { instructions }),
|
|
272
|
+
...(dueDate && { dueDate: new Date(dueDate) }),
|
|
273
|
+
...(maxGrade && { maxGrade }),
|
|
274
|
+
...(graded !== undefined && { graded }),
|
|
275
|
+
...(weight && { weight }),
|
|
276
|
+
...(type && { type }),
|
|
277
|
+
...(sectionId !== undefined && {
|
|
278
|
+
section: sectionId ? {
|
|
279
|
+
connect: { id: sectionId }
|
|
280
|
+
} : {
|
|
281
|
+
disconnect: true
|
|
282
|
+
}
|
|
283
|
+
}),
|
|
284
|
+
...(uploadedFiles.length > 0 && {
|
|
285
|
+
attachments: {
|
|
286
|
+
create: uploadedFiles.map(file => ({
|
|
287
|
+
name: file.name,
|
|
288
|
+
type: file.type,
|
|
289
|
+
size: file.size,
|
|
290
|
+
path: file.path,
|
|
291
|
+
...(file.thumbnailId && {
|
|
292
|
+
thumbnail: {
|
|
293
|
+
connect: { id: file.thumbnailId }
|
|
294
|
+
}
|
|
295
|
+
})
|
|
296
|
+
}))
|
|
297
|
+
}
|
|
298
|
+
}),
|
|
299
|
+
...(input.removedAttachments && input.removedAttachments.length > 0 && {
|
|
300
|
+
attachments: {
|
|
301
|
+
deleteMany: {
|
|
302
|
+
id: { in: input.removedAttachments }
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}),
|
|
306
|
+
},
|
|
307
|
+
select: {
|
|
308
|
+
id: true,
|
|
309
|
+
title: true,
|
|
310
|
+
instructions: true,
|
|
311
|
+
dueDate: true,
|
|
312
|
+
maxGrade: true,
|
|
313
|
+
graded: true,
|
|
314
|
+
weight: true,
|
|
315
|
+
type: true,
|
|
316
|
+
createdAt: true,
|
|
317
|
+
submissions: {
|
|
318
|
+
select: {
|
|
319
|
+
student: {
|
|
320
|
+
select: {
|
|
321
|
+
id: true,
|
|
322
|
+
username: true
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
attachments: {
|
|
328
|
+
select: {
|
|
329
|
+
id: true,
|
|
330
|
+
name: true,
|
|
331
|
+
type: true,
|
|
332
|
+
thumbnail: true
|
|
333
|
+
}
|
|
334
|
+
},
|
|
335
|
+
section: true,
|
|
336
|
+
teacher: true,
|
|
337
|
+
class: true
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
if (assignment.markSchemeId) {
|
|
341
|
+
const rubric = await prisma_1.prisma.markScheme.findUnique({
|
|
342
|
+
where: { id: assignment.markSchemeId },
|
|
343
|
+
select: {
|
|
344
|
+
structured: true,
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
const parsedRubric = JSON.parse(rubric?.structured || "{}");
|
|
348
|
+
const computedMaxGrade = parsedRubric.criteria.reduce((acc, criterion) => {
|
|
349
|
+
const maxPoints = Math.max(...criterion.levels.map((level) => level.points));
|
|
350
|
+
return acc + maxPoints;
|
|
351
|
+
}, 0);
|
|
352
|
+
await prisma_1.prisma.assignment.update({
|
|
353
|
+
where: { id },
|
|
354
|
+
data: {
|
|
355
|
+
maxGrade: computedMaxGrade,
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
return updatedAssignment;
|
|
360
|
+
}),
|
|
361
|
+
delete: trpc_1.protectedProcedure
|
|
362
|
+
.input(deleteAssignmentSchema)
|
|
363
|
+
.mutation(async ({ ctx, input }) => {
|
|
364
|
+
const { id, classId } = input;
|
|
365
|
+
if (!ctx.user) {
|
|
366
|
+
throw new server_1.TRPCError({
|
|
367
|
+
code: "UNAUTHORIZED",
|
|
368
|
+
message: "User must be authenticated",
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
// Get the assignment with all related files
|
|
372
|
+
const assignment = await prisma_1.prisma.assignment.findFirst({
|
|
373
|
+
where: {
|
|
374
|
+
id,
|
|
375
|
+
teacherId: ctx.user.id,
|
|
376
|
+
},
|
|
377
|
+
include: {
|
|
378
|
+
attachments: {
|
|
379
|
+
include: {
|
|
380
|
+
thumbnail: true
|
|
381
|
+
}
|
|
382
|
+
},
|
|
383
|
+
submissions: {
|
|
384
|
+
include: {
|
|
385
|
+
attachments: {
|
|
386
|
+
include: {
|
|
387
|
+
thumbnail: true
|
|
388
|
+
}
|
|
389
|
+
},
|
|
390
|
+
annotations: {
|
|
391
|
+
include: {
|
|
392
|
+
thumbnail: true
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
if (!assignment) {
|
|
400
|
+
throw new server_1.TRPCError({
|
|
401
|
+
code: "NOT_FOUND",
|
|
402
|
+
message: "Assignment not found",
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
// Delete all files from storage
|
|
406
|
+
const filesToDelete = [
|
|
407
|
+
...assignment.attachments,
|
|
408
|
+
...assignment.submissions.flatMap(sub => [...sub.attachments, ...sub.annotations])
|
|
409
|
+
];
|
|
410
|
+
// Delete files from storage
|
|
411
|
+
await Promise.all(filesToDelete.map(async (file) => {
|
|
412
|
+
try {
|
|
413
|
+
// Delete the main file
|
|
414
|
+
await (0, googleCloudStorage_1.deleteFile)(file.path);
|
|
415
|
+
// Delete thumbnail if it exists
|
|
416
|
+
if (file.thumbnail) {
|
|
417
|
+
await (0, googleCloudStorage_1.deleteFile)(file.thumbnail.path);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
catch (error) {
|
|
421
|
+
console.warn(`Failed to delete file ${file.path}:`, error);
|
|
422
|
+
}
|
|
423
|
+
}));
|
|
424
|
+
// Delete the assignment (this will cascade delete all related records)
|
|
425
|
+
await prisma_1.prisma.assignment.delete({
|
|
426
|
+
where: { id },
|
|
427
|
+
});
|
|
428
|
+
return {
|
|
429
|
+
id,
|
|
430
|
+
};
|
|
431
|
+
}),
|
|
432
|
+
get: trpc_1.protectedProcedure
|
|
433
|
+
.input(getAssignmentSchema)
|
|
434
|
+
.query(async ({ ctx, input }) => {
|
|
435
|
+
const { id, classId } = input;
|
|
436
|
+
if (!ctx.user) {
|
|
437
|
+
throw new server_1.TRPCError({
|
|
438
|
+
code: "UNAUTHORIZED",
|
|
439
|
+
message: "User must be authenticated",
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
const assignment = await prisma_1.prisma.assignment.findUnique({
|
|
443
|
+
where: {
|
|
444
|
+
id,
|
|
445
|
+
// classId,
|
|
446
|
+
},
|
|
447
|
+
include: {
|
|
448
|
+
submissions: {
|
|
449
|
+
select: {
|
|
450
|
+
student: {
|
|
451
|
+
select: {
|
|
452
|
+
id: true,
|
|
453
|
+
username: true
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
},
|
|
458
|
+
attachments: {
|
|
459
|
+
select: {
|
|
460
|
+
id: true,
|
|
461
|
+
name: true,
|
|
462
|
+
type: true,
|
|
463
|
+
size: true,
|
|
464
|
+
path: true,
|
|
465
|
+
thumbnailId: true,
|
|
466
|
+
}
|
|
467
|
+
},
|
|
468
|
+
section: {
|
|
469
|
+
select: {
|
|
470
|
+
id: true,
|
|
471
|
+
name: true,
|
|
472
|
+
}
|
|
473
|
+
},
|
|
474
|
+
teacher: {
|
|
475
|
+
select: {
|
|
476
|
+
id: true,
|
|
477
|
+
username: true
|
|
478
|
+
}
|
|
479
|
+
},
|
|
480
|
+
class: {
|
|
481
|
+
select: {
|
|
482
|
+
id: true,
|
|
483
|
+
name: true
|
|
484
|
+
}
|
|
485
|
+
},
|
|
486
|
+
eventAttached: {
|
|
487
|
+
select: {
|
|
488
|
+
id: true,
|
|
489
|
+
name: true,
|
|
490
|
+
startTime: true,
|
|
491
|
+
endTime: true,
|
|
492
|
+
location: true,
|
|
493
|
+
remarks: true,
|
|
494
|
+
}
|
|
495
|
+
},
|
|
496
|
+
markScheme: {
|
|
497
|
+
select: {
|
|
498
|
+
id: true,
|
|
499
|
+
structured: true,
|
|
500
|
+
}
|
|
501
|
+
},
|
|
502
|
+
gradingBoundary: {
|
|
503
|
+
select: {
|
|
504
|
+
id: true,
|
|
505
|
+
structured: true,
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
if (!assignment) {
|
|
511
|
+
throw new server_1.TRPCError({
|
|
512
|
+
code: "NOT_FOUND",
|
|
513
|
+
message: "Assignment not found",
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
const sections = await prisma_1.prisma.section.findMany({
|
|
517
|
+
where: {
|
|
518
|
+
classId: assignment.classId,
|
|
519
|
+
},
|
|
520
|
+
select: {
|
|
521
|
+
id: true,
|
|
522
|
+
name: true,
|
|
523
|
+
},
|
|
524
|
+
});
|
|
525
|
+
return { ...assignment, sections };
|
|
526
|
+
}),
|
|
527
|
+
getSubmission: trpc_1.protectedClassMemberProcedure
|
|
528
|
+
.input(zod_1.z.object({
|
|
529
|
+
assignmentId: zod_1.z.string(),
|
|
530
|
+
classId: zod_1.z.string(),
|
|
531
|
+
}))
|
|
532
|
+
.query(async ({ ctx, input }) => {
|
|
533
|
+
if (!ctx.user) {
|
|
534
|
+
throw new server_1.TRPCError({
|
|
535
|
+
code: "UNAUTHORIZED",
|
|
536
|
+
message: "User must be authenticated",
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
const { assignmentId } = input;
|
|
540
|
+
const submission = await prisma_1.prisma.submission.findFirst({
|
|
541
|
+
where: {
|
|
542
|
+
assignmentId,
|
|
543
|
+
studentId: ctx.user.id,
|
|
544
|
+
},
|
|
545
|
+
include: {
|
|
546
|
+
attachments: true,
|
|
547
|
+
student: {
|
|
548
|
+
select: {
|
|
549
|
+
id: true,
|
|
550
|
+
username: true,
|
|
551
|
+
},
|
|
552
|
+
},
|
|
553
|
+
assignment: {
|
|
554
|
+
include: {
|
|
555
|
+
class: true,
|
|
556
|
+
markScheme: {
|
|
557
|
+
select: {
|
|
558
|
+
id: true,
|
|
559
|
+
structured: true,
|
|
560
|
+
}
|
|
561
|
+
},
|
|
562
|
+
gradingBoundary: {
|
|
563
|
+
select: {
|
|
564
|
+
id: true,
|
|
565
|
+
structured: true,
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
},
|
|
569
|
+
},
|
|
570
|
+
annotations: true,
|
|
571
|
+
},
|
|
572
|
+
});
|
|
573
|
+
if (!submission) {
|
|
574
|
+
// Create a new submission if it doesn't exist
|
|
575
|
+
return await prisma_1.prisma.submission.create({
|
|
576
|
+
data: {
|
|
577
|
+
assignment: {
|
|
578
|
+
connect: { id: assignmentId },
|
|
579
|
+
},
|
|
580
|
+
student: {
|
|
581
|
+
connect: { id: ctx.user.id },
|
|
582
|
+
},
|
|
583
|
+
},
|
|
584
|
+
include: {
|
|
585
|
+
attachments: true,
|
|
586
|
+
annotations: true,
|
|
587
|
+
student: {
|
|
588
|
+
select: {
|
|
589
|
+
id: true,
|
|
590
|
+
username: true,
|
|
591
|
+
},
|
|
592
|
+
},
|
|
593
|
+
assignment: {
|
|
594
|
+
include: {
|
|
595
|
+
class: true,
|
|
596
|
+
markScheme: {
|
|
597
|
+
select: {
|
|
598
|
+
id: true,
|
|
599
|
+
structured: true,
|
|
600
|
+
}
|
|
601
|
+
},
|
|
602
|
+
gradingBoundary: {
|
|
603
|
+
select: {
|
|
604
|
+
id: true,
|
|
605
|
+
structured: true,
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
},
|
|
609
|
+
},
|
|
610
|
+
},
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
return {
|
|
614
|
+
...submission,
|
|
615
|
+
late: submission.assignment.dueDate < new Date(),
|
|
616
|
+
};
|
|
617
|
+
}),
|
|
618
|
+
getSubmissionById: trpc_1.protectedTeacherProcedure
|
|
619
|
+
.input(zod_1.z.object({
|
|
620
|
+
submissionId: zod_1.z.string(),
|
|
621
|
+
classId: zod_1.z.string(),
|
|
622
|
+
}))
|
|
623
|
+
.query(async ({ ctx, input }) => {
|
|
624
|
+
if (!ctx.user) {
|
|
625
|
+
throw new server_1.TRPCError({
|
|
626
|
+
code: "UNAUTHORIZED",
|
|
627
|
+
message: "User must be authenticated",
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
const { submissionId, classId } = input;
|
|
631
|
+
const submission = await prisma_1.prisma.submission.findFirst({
|
|
632
|
+
where: {
|
|
633
|
+
id: submissionId,
|
|
634
|
+
assignment: {
|
|
635
|
+
classId,
|
|
636
|
+
class: {
|
|
637
|
+
teachers: {
|
|
638
|
+
some: {
|
|
639
|
+
id: ctx.user.id
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
},
|
|
644
|
+
},
|
|
645
|
+
include: {
|
|
646
|
+
attachments: true,
|
|
647
|
+
annotations: true,
|
|
648
|
+
student: {
|
|
649
|
+
select: {
|
|
650
|
+
id: true,
|
|
651
|
+
username: true,
|
|
652
|
+
},
|
|
653
|
+
},
|
|
654
|
+
assignment: {
|
|
655
|
+
include: {
|
|
656
|
+
class: true,
|
|
657
|
+
markScheme: {
|
|
658
|
+
select: {
|
|
659
|
+
id: true,
|
|
660
|
+
structured: true,
|
|
661
|
+
}
|
|
662
|
+
},
|
|
663
|
+
gradingBoundary: {
|
|
664
|
+
select: {
|
|
665
|
+
id: true,
|
|
666
|
+
structured: true,
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
},
|
|
670
|
+
},
|
|
671
|
+
},
|
|
672
|
+
});
|
|
673
|
+
if (!submission) {
|
|
674
|
+
throw new server_1.TRPCError({
|
|
675
|
+
code: "NOT_FOUND",
|
|
676
|
+
message: "Submission not found",
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
return {
|
|
680
|
+
...submission,
|
|
681
|
+
late: submission.assignment.dueDate < new Date(),
|
|
682
|
+
};
|
|
683
|
+
}),
|
|
684
|
+
updateSubmission: trpc_1.protectedClassMemberProcedure
|
|
685
|
+
.input(submissionSchema)
|
|
686
|
+
.mutation(async ({ ctx, input }) => {
|
|
687
|
+
if (!ctx.user) {
|
|
688
|
+
throw new server_1.TRPCError({
|
|
689
|
+
code: "UNAUTHORIZED",
|
|
690
|
+
message: "User must be authenticated",
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
const { submissionId, submit, newAttachments, removedAttachments } = input;
|
|
694
|
+
const submission = await prisma_1.prisma.submission.findFirst({
|
|
695
|
+
where: {
|
|
696
|
+
id: submissionId,
|
|
697
|
+
OR: [
|
|
698
|
+
{
|
|
699
|
+
student: {
|
|
700
|
+
id: ctx.user.id,
|
|
701
|
+
},
|
|
702
|
+
},
|
|
703
|
+
{
|
|
704
|
+
assignment: {
|
|
705
|
+
class: {
|
|
706
|
+
teachers: {
|
|
707
|
+
some: {
|
|
708
|
+
id: ctx.user.id,
|
|
709
|
+
},
|
|
710
|
+
},
|
|
711
|
+
},
|
|
712
|
+
},
|
|
713
|
+
},
|
|
714
|
+
],
|
|
715
|
+
},
|
|
716
|
+
include: {
|
|
717
|
+
attachments: {
|
|
718
|
+
include: {
|
|
719
|
+
thumbnail: true
|
|
720
|
+
}
|
|
721
|
+
},
|
|
722
|
+
assignment: {
|
|
723
|
+
include: {
|
|
724
|
+
class: true,
|
|
725
|
+
markScheme: {
|
|
726
|
+
select: {
|
|
727
|
+
id: true,
|
|
728
|
+
structured: true,
|
|
729
|
+
}
|
|
730
|
+
},
|
|
731
|
+
gradingBoundary: {
|
|
732
|
+
select: {
|
|
733
|
+
id: true,
|
|
734
|
+
structured: true,
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
},
|
|
738
|
+
},
|
|
739
|
+
},
|
|
740
|
+
});
|
|
741
|
+
if (!submission) {
|
|
742
|
+
throw new server_1.TRPCError({
|
|
743
|
+
code: "NOT_FOUND",
|
|
744
|
+
message: "Submission not found",
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
if (submit !== undefined) {
|
|
748
|
+
// Toggle submission status
|
|
749
|
+
return await prisma_1.prisma.submission.update({
|
|
750
|
+
where: { id: submission.id },
|
|
751
|
+
data: {
|
|
752
|
+
submitted: !submission.submitted,
|
|
753
|
+
submittedAt: new Date(),
|
|
754
|
+
},
|
|
755
|
+
include: {
|
|
756
|
+
attachments: true,
|
|
757
|
+
student: {
|
|
758
|
+
select: {
|
|
759
|
+
id: true,
|
|
760
|
+
username: true,
|
|
761
|
+
},
|
|
762
|
+
},
|
|
763
|
+
assignment: {
|
|
764
|
+
include: {
|
|
765
|
+
class: true,
|
|
766
|
+
markScheme: {
|
|
767
|
+
select: {
|
|
768
|
+
id: true,
|
|
769
|
+
structured: true,
|
|
770
|
+
}
|
|
771
|
+
},
|
|
772
|
+
gradingBoundary: {
|
|
773
|
+
select: {
|
|
774
|
+
id: true,
|
|
775
|
+
structured: true,
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
},
|
|
779
|
+
},
|
|
780
|
+
},
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
let uploadedFiles = [];
|
|
784
|
+
if (newAttachments && newAttachments.length > 0) {
|
|
785
|
+
// Store files in a class and assignment specific directory
|
|
786
|
+
uploadedFiles = await (0, fileUpload_1.uploadFiles)(newAttachments, ctx.user.id, `class/${submission.assignment.classId}/assignment/${submission.assignmentId}/submission/${submission.id}`);
|
|
787
|
+
}
|
|
788
|
+
// Update submission with new file attachments
|
|
789
|
+
if (uploadedFiles.length > 0) {
|
|
790
|
+
await prisma_1.prisma.submission.update({
|
|
791
|
+
where: { id: submission.id },
|
|
792
|
+
data: {
|
|
793
|
+
attachments: {
|
|
794
|
+
create: uploadedFiles.map(file => ({
|
|
795
|
+
name: file.name,
|
|
796
|
+
type: file.type,
|
|
797
|
+
size: file.size,
|
|
798
|
+
path: file.path,
|
|
799
|
+
...(file.thumbnailId && {
|
|
800
|
+
thumbnail: {
|
|
801
|
+
connect: { id: file.thumbnailId }
|
|
802
|
+
}
|
|
803
|
+
})
|
|
804
|
+
}))
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
// Delete removed attachments if any
|
|
810
|
+
if (removedAttachments && removedAttachments.length > 0) {
|
|
811
|
+
const filesToDelete = submission.attachments.filter((file) => removedAttachments.includes(file.id));
|
|
812
|
+
// Delete files from storage
|
|
813
|
+
await Promise.all(filesToDelete.map(async (file) => {
|
|
814
|
+
try {
|
|
815
|
+
// Delete the main file
|
|
816
|
+
await (0, googleCloudStorage_1.deleteFile)(file.path);
|
|
817
|
+
// Delete thumbnail if it exists
|
|
818
|
+
if (file.thumbnail?.path) {
|
|
819
|
+
await (0, googleCloudStorage_1.deleteFile)(file.thumbnail.path);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
catch (error) {
|
|
823
|
+
console.warn(`Failed to delete file ${file.path}:`, error);
|
|
824
|
+
}
|
|
825
|
+
}));
|
|
826
|
+
}
|
|
827
|
+
// Update submission with attachments
|
|
828
|
+
return await prisma_1.prisma.submission.update({
|
|
829
|
+
where: { id: submission.id },
|
|
830
|
+
data: {
|
|
831
|
+
...(removedAttachments && removedAttachments.length > 0 && {
|
|
832
|
+
attachments: {
|
|
833
|
+
deleteMany: {
|
|
834
|
+
id: { in: removedAttachments },
|
|
835
|
+
},
|
|
836
|
+
},
|
|
837
|
+
}),
|
|
838
|
+
},
|
|
839
|
+
include: {
|
|
840
|
+
attachments: {
|
|
841
|
+
include: {
|
|
842
|
+
thumbnail: true
|
|
843
|
+
}
|
|
844
|
+
},
|
|
845
|
+
student: {
|
|
846
|
+
select: {
|
|
847
|
+
id: true,
|
|
848
|
+
username: true,
|
|
849
|
+
},
|
|
850
|
+
},
|
|
851
|
+
assignment: {
|
|
852
|
+
include: {
|
|
853
|
+
class: true,
|
|
854
|
+
markScheme: {
|
|
855
|
+
select: {
|
|
856
|
+
id: true,
|
|
857
|
+
structured: true,
|
|
858
|
+
}
|
|
859
|
+
},
|
|
860
|
+
gradingBoundary: {
|
|
861
|
+
select: {
|
|
862
|
+
id: true,
|
|
863
|
+
structured: true,
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
},
|
|
867
|
+
},
|
|
868
|
+
},
|
|
869
|
+
});
|
|
870
|
+
}),
|
|
871
|
+
getSubmissions: trpc_1.protectedTeacherProcedure
|
|
872
|
+
.input(zod_1.z.object({
|
|
873
|
+
assignmentId: zod_1.z.string(),
|
|
874
|
+
classId: zod_1.z.string(),
|
|
875
|
+
}))
|
|
876
|
+
.query(async ({ ctx, input }) => {
|
|
877
|
+
if (!ctx.user) {
|
|
878
|
+
throw new server_1.TRPCError({
|
|
879
|
+
code: "UNAUTHORIZED",
|
|
880
|
+
message: "User must be authenticated",
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
const { assignmentId } = input;
|
|
884
|
+
const submissions = await prisma_1.prisma.submission.findMany({
|
|
885
|
+
where: {
|
|
886
|
+
assignment: {
|
|
887
|
+
id: assignmentId,
|
|
888
|
+
class: {
|
|
889
|
+
teachers: {
|
|
890
|
+
some: { id: ctx.user.id },
|
|
891
|
+
},
|
|
892
|
+
},
|
|
893
|
+
},
|
|
894
|
+
},
|
|
895
|
+
include: {
|
|
896
|
+
attachments: {
|
|
897
|
+
include: {
|
|
898
|
+
thumbnail: true
|
|
899
|
+
}
|
|
900
|
+
},
|
|
901
|
+
student: {
|
|
902
|
+
select: {
|
|
903
|
+
id: true,
|
|
904
|
+
username: true,
|
|
905
|
+
},
|
|
906
|
+
},
|
|
907
|
+
assignment: {
|
|
908
|
+
include: {
|
|
909
|
+
class: true,
|
|
910
|
+
markScheme: {
|
|
911
|
+
select: {
|
|
912
|
+
id: true,
|
|
913
|
+
structured: true,
|
|
914
|
+
}
|
|
915
|
+
},
|
|
916
|
+
gradingBoundary: {
|
|
917
|
+
select: {
|
|
918
|
+
id: true,
|
|
919
|
+
structured: true,
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
},
|
|
923
|
+
},
|
|
924
|
+
},
|
|
925
|
+
});
|
|
926
|
+
return submissions.map(submission => ({
|
|
927
|
+
...submission,
|
|
928
|
+
late: submission.assignment.dueDate < new Date(),
|
|
929
|
+
}));
|
|
930
|
+
}),
|
|
931
|
+
updateSubmissionAsTeacher: trpc_1.protectedTeacherProcedure
|
|
932
|
+
.input(updateSubmissionSchema)
|
|
933
|
+
.mutation(async ({ ctx, input }) => {
|
|
934
|
+
if (!ctx.user) {
|
|
935
|
+
throw new server_1.TRPCError({
|
|
936
|
+
code: "UNAUTHORIZED",
|
|
937
|
+
message: "User must be authenticated",
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
const { submissionId, return: returnSubmission, gradeReceived, newAttachments, removedAttachments, rubricGrades } = input;
|
|
941
|
+
const submission = await prisma_1.prisma.submission.findFirst({
|
|
942
|
+
where: {
|
|
943
|
+
id: submissionId,
|
|
944
|
+
assignment: {
|
|
945
|
+
class: {
|
|
946
|
+
teachers: {
|
|
947
|
+
some: { id: ctx.user.id },
|
|
948
|
+
},
|
|
949
|
+
},
|
|
950
|
+
},
|
|
951
|
+
},
|
|
952
|
+
include: {
|
|
953
|
+
attachments: {
|
|
954
|
+
include: {
|
|
955
|
+
thumbnail: true
|
|
956
|
+
}
|
|
957
|
+
},
|
|
958
|
+
annotations: {
|
|
959
|
+
include: {
|
|
960
|
+
thumbnail: true
|
|
961
|
+
}
|
|
962
|
+
},
|
|
963
|
+
assignment: {
|
|
964
|
+
include: {
|
|
965
|
+
class: true,
|
|
966
|
+
markScheme: {
|
|
967
|
+
select: {
|
|
968
|
+
id: true,
|
|
969
|
+
structured: true,
|
|
970
|
+
}
|
|
971
|
+
},
|
|
972
|
+
gradingBoundary: {
|
|
973
|
+
select: {
|
|
974
|
+
id: true,
|
|
975
|
+
structured: true,
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
},
|
|
979
|
+
},
|
|
980
|
+
},
|
|
981
|
+
});
|
|
982
|
+
if (!submission) {
|
|
983
|
+
throw new server_1.TRPCError({
|
|
984
|
+
code: "NOT_FOUND",
|
|
985
|
+
message: "Submission not found",
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
if (returnSubmission !== undefined) {
|
|
989
|
+
// Toggle return status
|
|
990
|
+
return await prisma_1.prisma.submission.update({
|
|
991
|
+
where: { id: submissionId },
|
|
992
|
+
data: {
|
|
993
|
+
returned: !submission.returned,
|
|
994
|
+
},
|
|
995
|
+
include: {
|
|
996
|
+
attachments: true,
|
|
997
|
+
student: {
|
|
998
|
+
select: {
|
|
999
|
+
id: true,
|
|
1000
|
+
username: true,
|
|
1001
|
+
},
|
|
1002
|
+
},
|
|
1003
|
+
assignment: {
|
|
1004
|
+
include: {
|
|
1005
|
+
class: true,
|
|
1006
|
+
markScheme: {
|
|
1007
|
+
select: {
|
|
1008
|
+
id: true,
|
|
1009
|
+
structured: true,
|
|
1010
|
+
}
|
|
1011
|
+
},
|
|
1012
|
+
gradingBoundary: {
|
|
1013
|
+
select: {
|
|
1014
|
+
id: true,
|
|
1015
|
+
structured: true,
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
},
|
|
1019
|
+
},
|
|
1020
|
+
},
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
let uploadedFiles = [];
|
|
1024
|
+
if (newAttachments && newAttachments.length > 0) {
|
|
1025
|
+
// Store files in a class and assignment specific directory
|
|
1026
|
+
uploadedFiles = await (0, fileUpload_1.uploadFiles)(newAttachments, ctx.user.id, `class/${submission.assignment.classId}/assignment/${submission.assignmentId}/submission/${submission.id}/annotations`);
|
|
1027
|
+
}
|
|
1028
|
+
// Update submission with new file attachments
|
|
1029
|
+
if (uploadedFiles.length > 0) {
|
|
1030
|
+
await prisma_1.prisma.submission.update({
|
|
1031
|
+
where: { id: submission.id },
|
|
1032
|
+
data: {
|
|
1033
|
+
annotations: {
|
|
1034
|
+
create: uploadedFiles.map(file => ({
|
|
1035
|
+
name: file.name,
|
|
1036
|
+
type: file.type,
|
|
1037
|
+
size: file.size,
|
|
1038
|
+
path: file.path,
|
|
1039
|
+
...(file.thumbnailId && {
|
|
1040
|
+
thumbnail: {
|
|
1041
|
+
connect: { id: file.thumbnailId }
|
|
1042
|
+
}
|
|
1043
|
+
})
|
|
1044
|
+
}))
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
// Delete removed attachments if any
|
|
1050
|
+
if (removedAttachments && removedAttachments.length > 0) {
|
|
1051
|
+
const filesToDelete = submission.annotations.filter((file) => removedAttachments.includes(file.id));
|
|
1052
|
+
// Delete files from storage
|
|
1053
|
+
await Promise.all(filesToDelete.map(async (file) => {
|
|
1054
|
+
try {
|
|
1055
|
+
// Delete the main file
|
|
1056
|
+
await (0, googleCloudStorage_1.deleteFile)(file.path);
|
|
1057
|
+
// Delete thumbnail if it exists
|
|
1058
|
+
if (file.thumbnail?.path) {
|
|
1059
|
+
await (0, googleCloudStorage_1.deleteFile)(file.thumbnail.path);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
catch (error) {
|
|
1063
|
+
console.warn(`Failed to delete file ${file.path}:`, error);
|
|
1064
|
+
}
|
|
1065
|
+
}));
|
|
1066
|
+
}
|
|
1067
|
+
// Update submission with grade and attachments
|
|
1068
|
+
return await prisma_1.prisma.submission.update({
|
|
1069
|
+
where: { id: submissionId },
|
|
1070
|
+
data: {
|
|
1071
|
+
...(gradeReceived !== undefined && { gradeReceived }),
|
|
1072
|
+
...(rubricGrades && { rubricState: JSON.stringify(rubricGrades) }),
|
|
1073
|
+
...(removedAttachments && removedAttachments.length > 0 && {
|
|
1074
|
+
annotations: {
|
|
1075
|
+
deleteMany: {
|
|
1076
|
+
id: { in: removedAttachments },
|
|
1077
|
+
},
|
|
1078
|
+
},
|
|
1079
|
+
}),
|
|
1080
|
+
},
|
|
1081
|
+
include: {
|
|
1082
|
+
attachments: {
|
|
1083
|
+
include: {
|
|
1084
|
+
thumbnail: true
|
|
1085
|
+
}
|
|
1086
|
+
},
|
|
1087
|
+
annotations: {
|
|
1088
|
+
include: {
|
|
1089
|
+
thumbnail: true
|
|
1090
|
+
}
|
|
1091
|
+
},
|
|
1092
|
+
student: {
|
|
1093
|
+
select: {
|
|
1094
|
+
id: true,
|
|
1095
|
+
username: true,
|
|
1096
|
+
},
|
|
1097
|
+
},
|
|
1098
|
+
assignment: {
|
|
1099
|
+
include: {
|
|
1100
|
+
class: true,
|
|
1101
|
+
markScheme: {
|
|
1102
|
+
select: {
|
|
1103
|
+
id: true,
|
|
1104
|
+
structured: true,
|
|
1105
|
+
}
|
|
1106
|
+
},
|
|
1107
|
+
gradingBoundary: {
|
|
1108
|
+
select: {
|
|
1109
|
+
id: true,
|
|
1110
|
+
structured: true,
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
},
|
|
1114
|
+
},
|
|
1115
|
+
},
|
|
1116
|
+
});
|
|
1117
|
+
}),
|
|
1118
|
+
attachToEvent: trpc_1.protectedTeacherProcedure
|
|
1119
|
+
.input(zod_1.z.object({
|
|
1120
|
+
assignmentId: zod_1.z.string(),
|
|
1121
|
+
eventId: zod_1.z.string(),
|
|
1122
|
+
}))
|
|
1123
|
+
.mutation(async ({ ctx, input }) => {
|
|
1124
|
+
if (!ctx.user) {
|
|
1125
|
+
throw new server_1.TRPCError({
|
|
1126
|
+
code: "UNAUTHORIZED",
|
|
1127
|
+
message: "User must be authenticated",
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
const { assignmentId, eventId } = input;
|
|
1131
|
+
// Check if assignment exists and user is a teacher of the class
|
|
1132
|
+
const assignment = await prisma_1.prisma.assignment.findFirst({
|
|
1133
|
+
where: {
|
|
1134
|
+
id: assignmentId,
|
|
1135
|
+
class: {
|
|
1136
|
+
teachers: {
|
|
1137
|
+
some: { id: ctx.user.id },
|
|
1138
|
+
},
|
|
1139
|
+
},
|
|
1140
|
+
},
|
|
1141
|
+
include: {
|
|
1142
|
+
class: true,
|
|
1143
|
+
},
|
|
1144
|
+
});
|
|
1145
|
+
if (!assignment) {
|
|
1146
|
+
throw new server_1.TRPCError({
|
|
1147
|
+
code: "NOT_FOUND",
|
|
1148
|
+
message: "Assignment not found or you are not authorized",
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
// Check if event exists and belongs to the same class
|
|
1152
|
+
const event = await prisma_1.prisma.event.findFirst({
|
|
1153
|
+
where: {
|
|
1154
|
+
id: eventId,
|
|
1155
|
+
classId: assignment.classId,
|
|
1156
|
+
},
|
|
1157
|
+
});
|
|
1158
|
+
if (!event) {
|
|
1159
|
+
throw new server_1.TRPCError({
|
|
1160
|
+
code: "NOT_FOUND",
|
|
1161
|
+
message: "Event not found or does not belong to the same class",
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
// Attach assignment to event
|
|
1165
|
+
const updatedAssignment = await prisma_1.prisma.assignment.update({
|
|
1166
|
+
where: { id: assignmentId },
|
|
1167
|
+
data: {
|
|
1168
|
+
eventAttached: {
|
|
1169
|
+
connect: { id: eventId }
|
|
1170
|
+
}
|
|
1171
|
+
},
|
|
1172
|
+
include: {
|
|
1173
|
+
attachments: {
|
|
1174
|
+
select: {
|
|
1175
|
+
id: true,
|
|
1176
|
+
name: true,
|
|
1177
|
+
type: true,
|
|
1178
|
+
}
|
|
1179
|
+
},
|
|
1180
|
+
section: {
|
|
1181
|
+
select: {
|
|
1182
|
+
id: true,
|
|
1183
|
+
name: true
|
|
1184
|
+
}
|
|
1185
|
+
},
|
|
1186
|
+
teacher: {
|
|
1187
|
+
select: {
|
|
1188
|
+
id: true,
|
|
1189
|
+
username: true
|
|
1190
|
+
}
|
|
1191
|
+
},
|
|
1192
|
+
eventAttached: {
|
|
1193
|
+
select: {
|
|
1194
|
+
id: true,
|
|
1195
|
+
name: true,
|
|
1196
|
+
startTime: true,
|
|
1197
|
+
endTime: true,
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
});
|
|
1202
|
+
return { assignment: updatedAssignment };
|
|
1203
|
+
}),
|
|
1204
|
+
detachEvent: trpc_1.protectedTeacherProcedure
|
|
1205
|
+
.input(zod_1.z.object({
|
|
1206
|
+
assignmentId: zod_1.z.string(),
|
|
1207
|
+
}))
|
|
1208
|
+
.mutation(async ({ ctx, input }) => {
|
|
1209
|
+
if (!ctx.user) {
|
|
1210
|
+
throw new server_1.TRPCError({
|
|
1211
|
+
code: "UNAUTHORIZED",
|
|
1212
|
+
message: "User must be authenticated",
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
const { assignmentId } = input;
|
|
1216
|
+
// Check if assignment exists and user is a teacher of the class
|
|
1217
|
+
const assignment = await prisma_1.prisma.assignment.findFirst({
|
|
1218
|
+
where: {
|
|
1219
|
+
id: assignmentId,
|
|
1220
|
+
class: {
|
|
1221
|
+
teachers: {
|
|
1222
|
+
some: { id: ctx.user.id },
|
|
1223
|
+
},
|
|
1224
|
+
},
|
|
1225
|
+
},
|
|
1226
|
+
});
|
|
1227
|
+
if (!assignment) {
|
|
1228
|
+
throw new server_1.TRPCError({
|
|
1229
|
+
code: "NOT_FOUND",
|
|
1230
|
+
message: "Assignment not found or you are not authorized",
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1233
|
+
// Detach assignment from event
|
|
1234
|
+
const updatedAssignment = await prisma_1.prisma.assignment.update({
|
|
1235
|
+
where: { id: assignmentId },
|
|
1236
|
+
data: {
|
|
1237
|
+
eventAttached: {
|
|
1238
|
+
disconnect: true
|
|
1239
|
+
}
|
|
1240
|
+
},
|
|
1241
|
+
include: {
|
|
1242
|
+
attachments: {
|
|
1243
|
+
select: {
|
|
1244
|
+
id: true,
|
|
1245
|
+
name: true,
|
|
1246
|
+
type: true,
|
|
1247
|
+
}
|
|
1248
|
+
},
|
|
1249
|
+
section: {
|
|
1250
|
+
select: {
|
|
1251
|
+
id: true,
|
|
1252
|
+
name: true
|
|
1253
|
+
}
|
|
1254
|
+
},
|
|
1255
|
+
teacher: {
|
|
1256
|
+
select: {
|
|
1257
|
+
id: true,
|
|
1258
|
+
username: true
|
|
1259
|
+
}
|
|
1260
|
+
},
|
|
1261
|
+
eventAttached: {
|
|
1262
|
+
select: {
|
|
1263
|
+
id: true,
|
|
1264
|
+
name: true,
|
|
1265
|
+
startTime: true,
|
|
1266
|
+
endTime: true,
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
});
|
|
1271
|
+
return { assignment: updatedAssignment };
|
|
1272
|
+
}),
|
|
1273
|
+
getAvailableEvents: trpc_1.protectedTeacherProcedure
|
|
1274
|
+
.input(zod_1.z.object({
|
|
1275
|
+
assignmentId: zod_1.z.string(),
|
|
1276
|
+
}))
|
|
1277
|
+
.query(async ({ ctx, input }) => {
|
|
1278
|
+
if (!ctx.user) {
|
|
1279
|
+
throw new server_1.TRPCError({
|
|
1280
|
+
code: "UNAUTHORIZED",
|
|
1281
|
+
message: "User must be authenticated",
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1284
|
+
const { assignmentId } = input;
|
|
1285
|
+
// Get the assignment to find the class
|
|
1286
|
+
const assignment = await prisma_1.prisma.assignment.findFirst({
|
|
1287
|
+
where: {
|
|
1288
|
+
id: assignmentId,
|
|
1289
|
+
class: {
|
|
1290
|
+
teachers: {
|
|
1291
|
+
some: { id: ctx.user.id },
|
|
1292
|
+
},
|
|
1293
|
+
},
|
|
1294
|
+
},
|
|
1295
|
+
select: { classId: true }
|
|
1296
|
+
});
|
|
1297
|
+
if (!assignment) {
|
|
1298
|
+
throw new server_1.TRPCError({
|
|
1299
|
+
code: "NOT_FOUND",
|
|
1300
|
+
message: "Assignment not found or you are not authorized",
|
|
1301
|
+
});
|
|
1302
|
+
}
|
|
1303
|
+
// Get all events for the class that don't already have this assignment attached
|
|
1304
|
+
const events = await prisma_1.prisma.event.findMany({
|
|
1305
|
+
where: {
|
|
1306
|
+
classId: assignment.classId,
|
|
1307
|
+
assignmentsAttached: {
|
|
1308
|
+
none: {
|
|
1309
|
+
id: assignmentId
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
},
|
|
1313
|
+
select: {
|
|
1314
|
+
id: true,
|
|
1315
|
+
name: true,
|
|
1316
|
+
startTime: true,
|
|
1317
|
+
endTime: true,
|
|
1318
|
+
location: true,
|
|
1319
|
+
remarks: true,
|
|
1320
|
+
},
|
|
1321
|
+
orderBy: {
|
|
1322
|
+
startTime: 'asc'
|
|
1323
|
+
}
|
|
1324
|
+
});
|
|
1325
|
+
return { events };
|
|
1326
|
+
}),
|
|
1327
|
+
dueToday: trpc_1.protectedProcedure
|
|
1328
|
+
.query(async ({ ctx }) => {
|
|
1329
|
+
if (!ctx.user) {
|
|
1330
|
+
throw new server_1.TRPCError({
|
|
1331
|
+
code: "UNAUTHORIZED",
|
|
1332
|
+
message: "User must be authenticated",
|
|
1333
|
+
});
|
|
1334
|
+
}
|
|
1335
|
+
const assignments = await prisma_1.prisma.assignment.findMany({
|
|
1336
|
+
where: {
|
|
1337
|
+
dueDate: {
|
|
1338
|
+
equals: new Date(),
|
|
1339
|
+
},
|
|
1340
|
+
},
|
|
1341
|
+
select: {
|
|
1342
|
+
id: true,
|
|
1343
|
+
title: true,
|
|
1344
|
+
dueDate: true,
|
|
1345
|
+
type: true,
|
|
1346
|
+
graded: true,
|
|
1347
|
+
maxGrade: true,
|
|
1348
|
+
class: {
|
|
1349
|
+
select: {
|
|
1350
|
+
id: true,
|
|
1351
|
+
name: true,
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
});
|
|
1356
|
+
return assignments.map(assignment => ({
|
|
1357
|
+
...assignment,
|
|
1358
|
+
dueDate: assignment.dueDate.toISOString(),
|
|
1359
|
+
}));
|
|
1360
|
+
}),
|
|
1361
|
+
attachMarkScheme: trpc_1.protectedTeacherProcedure
|
|
1362
|
+
.input(zod_1.z.object({
|
|
1363
|
+
assignmentId: zod_1.z.string(),
|
|
1364
|
+
markSchemeId: zod_1.z.string().nullable(),
|
|
1365
|
+
}))
|
|
1366
|
+
.mutation(async ({ ctx, input }) => {
|
|
1367
|
+
const { assignmentId, markSchemeId } = input;
|
|
1368
|
+
const assignment = await prisma_1.prisma.assignment.findFirst({
|
|
1369
|
+
where: {
|
|
1370
|
+
id: assignmentId,
|
|
1371
|
+
},
|
|
1372
|
+
});
|
|
1373
|
+
if (!assignment) {
|
|
1374
|
+
throw new server_1.TRPCError({
|
|
1375
|
+
code: "NOT_FOUND",
|
|
1376
|
+
message: "Assignment not found",
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
1379
|
+
// If markSchemeId is provided, verify it exists
|
|
1380
|
+
if (markSchemeId) {
|
|
1381
|
+
const markScheme = await prisma_1.prisma.markScheme.findFirst({
|
|
1382
|
+
where: {
|
|
1383
|
+
id: markSchemeId,
|
|
1384
|
+
},
|
|
1385
|
+
});
|
|
1386
|
+
if (!markScheme) {
|
|
1387
|
+
throw new server_1.TRPCError({
|
|
1388
|
+
code: "NOT_FOUND",
|
|
1389
|
+
message: "Mark scheme not found",
|
|
1390
|
+
});
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
const updatedAssignment = await prisma_1.prisma.assignment.update({
|
|
1394
|
+
where: { id: assignmentId },
|
|
1395
|
+
data: {
|
|
1396
|
+
markScheme: markSchemeId ? {
|
|
1397
|
+
connect: { id: markSchemeId },
|
|
1398
|
+
} : {
|
|
1399
|
+
disconnect: true,
|
|
1400
|
+
},
|
|
1401
|
+
},
|
|
1402
|
+
include: {
|
|
1403
|
+
attachments: true,
|
|
1404
|
+
section: true,
|
|
1405
|
+
teacher: true,
|
|
1406
|
+
eventAttached: true,
|
|
1407
|
+
markScheme: true,
|
|
1408
|
+
},
|
|
1409
|
+
});
|
|
1410
|
+
return updatedAssignment;
|
|
1411
|
+
}),
|
|
1412
|
+
detachMarkScheme: trpc_1.protectedTeacherProcedure
|
|
1413
|
+
.input(zod_1.z.object({
|
|
1414
|
+
assignmentId: zod_1.z.string(),
|
|
1415
|
+
}))
|
|
1416
|
+
.mutation(async ({ ctx, input }) => {
|
|
1417
|
+
const { assignmentId } = input;
|
|
1418
|
+
const assignment = await prisma_1.prisma.assignment.findFirst({
|
|
1419
|
+
where: {
|
|
1420
|
+
id: assignmentId,
|
|
1421
|
+
},
|
|
1422
|
+
});
|
|
1423
|
+
if (!assignment) {
|
|
1424
|
+
throw new server_1.TRPCError({
|
|
1425
|
+
code: "NOT_FOUND",
|
|
1426
|
+
message: "Assignment not found",
|
|
1427
|
+
});
|
|
1428
|
+
}
|
|
1429
|
+
const updatedAssignment = await prisma_1.prisma.assignment.update({
|
|
1430
|
+
where: { id: assignmentId },
|
|
1431
|
+
data: {
|
|
1432
|
+
markScheme: {
|
|
1433
|
+
disconnect: true,
|
|
1434
|
+
},
|
|
1435
|
+
},
|
|
1436
|
+
include: {
|
|
1437
|
+
attachments: true,
|
|
1438
|
+
section: true,
|
|
1439
|
+
teacher: true,
|
|
1440
|
+
eventAttached: true,
|
|
1441
|
+
markScheme: true,
|
|
1442
|
+
},
|
|
1443
|
+
});
|
|
1444
|
+
return updatedAssignment;
|
|
1445
|
+
}),
|
|
1446
|
+
attachGradingBoundary: trpc_1.protectedTeacherProcedure
|
|
1447
|
+
.input(zod_1.z.object({
|
|
1448
|
+
assignmentId: zod_1.z.string(),
|
|
1449
|
+
gradingBoundaryId: zod_1.z.string().nullable(),
|
|
1450
|
+
}))
|
|
1451
|
+
.mutation(async ({ ctx, input }) => {
|
|
1452
|
+
const { assignmentId, gradingBoundaryId } = input;
|
|
1453
|
+
const assignment = await prisma_1.prisma.assignment.findFirst({
|
|
1454
|
+
where: {
|
|
1455
|
+
id: assignmentId,
|
|
1456
|
+
},
|
|
1457
|
+
});
|
|
1458
|
+
if (!assignment) {
|
|
1459
|
+
throw new server_1.TRPCError({
|
|
1460
|
+
code: "NOT_FOUND",
|
|
1461
|
+
message: "Assignment not found",
|
|
1462
|
+
});
|
|
1463
|
+
}
|
|
1464
|
+
// If gradingBoundaryId is provided, verify it exists
|
|
1465
|
+
if (gradingBoundaryId) {
|
|
1466
|
+
const gradingBoundary = await prisma_1.prisma.gradingBoundary.findFirst({
|
|
1467
|
+
where: {
|
|
1468
|
+
id: gradingBoundaryId,
|
|
1469
|
+
},
|
|
1470
|
+
});
|
|
1471
|
+
if (!gradingBoundary) {
|
|
1472
|
+
throw new server_1.TRPCError({
|
|
1473
|
+
code: "NOT_FOUND",
|
|
1474
|
+
message: "Grading boundary not found",
|
|
1475
|
+
});
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
const updatedAssignment = await prisma_1.prisma.assignment.update({
|
|
1479
|
+
where: { id: assignmentId },
|
|
1480
|
+
data: {
|
|
1481
|
+
gradingBoundary: gradingBoundaryId ? {
|
|
1482
|
+
connect: { id: gradingBoundaryId },
|
|
1483
|
+
} : {
|
|
1484
|
+
disconnect: true,
|
|
1485
|
+
},
|
|
1486
|
+
},
|
|
1487
|
+
include: {
|
|
1488
|
+
attachments: true,
|
|
1489
|
+
section: true,
|
|
1490
|
+
teacher: true,
|
|
1491
|
+
eventAttached: true,
|
|
1492
|
+
gradingBoundary: true,
|
|
1493
|
+
},
|
|
1494
|
+
});
|
|
1495
|
+
return updatedAssignment;
|
|
1496
|
+
}),
|
|
1497
|
+
});
|