@studious-lms/server 1.2.31 → 1.2.33
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/routers/_app.d.ts +124 -84
- package/dist/routers/_app.d.ts.map +1 -1
- package/dist/routers/assignment.d.ts +36 -16
- package/dist/routers/assignment.d.ts.map +1 -1
- package/dist/routers/assignment.js +355 -209
- package/dist/routers/class.d.ts +7 -7
- package/dist/routers/class.d.ts.map +1 -1
- package/dist/routers/class.js +18 -2
- package/dist/routers/event.d.ts +2 -2
- package/dist/routers/worksheet.d.ts +17 -17
- package/dist/routers/worksheet.d.ts.map +1 -1
- package/dist/routers/worksheet.js +30 -7
- package/package.json +1 -1
- package/prisma/schema.prisma +4 -3
- package/src/routers/assignment.ts +451 -290
- package/src/routers/class.ts +18 -2
- package/src/routers/worksheet.ts +33 -7
|
@@ -22,10 +22,13 @@ const createAssignmentSchema = z.object({
|
|
|
22
22
|
dueDate: z.string(),
|
|
23
23
|
files: z.array(directFileSchema).optional(), // Use direct file schema
|
|
24
24
|
existingFileIds: z.array(z.string()).optional(),
|
|
25
|
+
aiPolicyLevel: z.number().default(0),
|
|
25
26
|
acceptFiles: z.boolean().optional(),
|
|
26
27
|
acceptExtendedResponse: z.boolean().optional(),
|
|
27
28
|
acceptWorksheet: z.boolean().optional(),
|
|
29
|
+
worksheetIds: z.array(z.string()).optional(),
|
|
28
30
|
gradeWithAI: z.boolean().optional(),
|
|
31
|
+
studentIds: z.array(z.string()).optional(),
|
|
29
32
|
maxGrade: z.number().optional(),
|
|
30
33
|
graded: z.boolean().optional(),
|
|
31
34
|
weight: z.number().optional(),
|
|
@@ -42,10 +45,13 @@ const updateAssignmentSchema = z.object({
|
|
|
42
45
|
instructions: z.string().optional(),
|
|
43
46
|
dueDate: z.string().optional(),
|
|
44
47
|
files: z.array(directFileSchema).optional(), // Use direct file schema
|
|
48
|
+
aiPolicyLevel: z.number().default(0),
|
|
45
49
|
acceptFiles: z.boolean().optional(),
|
|
46
50
|
acceptExtendedResponse: z.boolean().optional(),
|
|
47
51
|
acceptWorksheet: z.boolean().optional(),
|
|
52
|
+
worksheetIds: z.array(z.string()).optional(),
|
|
48
53
|
gradeWithAI: z.boolean().optional(),
|
|
54
|
+
studentIds: z.array(z.string()).optional(),
|
|
49
55
|
existingFileIds: z.array(z.string()).optional(),
|
|
50
56
|
removedAttachments: z.array(z.string()).optional(),
|
|
51
57
|
maxGrade: z.number().optional(),
|
|
@@ -69,6 +75,7 @@ const submissionSchema = z.object({
|
|
|
69
75
|
submissionId: z.string(),
|
|
70
76
|
submit: z.boolean().optional(),
|
|
71
77
|
newAttachments: z.array(directFileSchema).optional(), // Use direct file schema
|
|
78
|
+
extendedResponse: z.string().optional(),
|
|
72
79
|
existingFileIds: z.array(z.string()).optional(),
|
|
73
80
|
removedAttachments: z.array(z.string()).optional(),
|
|
74
81
|
});
|
|
@@ -144,15 +151,38 @@ async function getUnifiedList(tx, classId) {
|
|
|
144
151
|
return unified;
|
|
145
152
|
}
|
|
146
153
|
// Helper function to normalize unified list to 1..n
|
|
154
|
+
// Updated to batch updates to prevent timeouts with large lists
|
|
147
155
|
async function normalizeUnifiedList(tx, classId, orderedItems) {
|
|
148
|
-
|
|
156
|
+
const BATCH_SIZE = 10; // Process 10 items at a time to avoid overwhelming the transaction
|
|
157
|
+
// Group items by type for more efficient updates
|
|
158
|
+
const sections = [];
|
|
159
|
+
const assignments = [];
|
|
160
|
+
orderedItems.forEach((item, index) => {
|
|
161
|
+
const orderData = { id: item.id, order: index + 1 };
|
|
149
162
|
if (item.type === 'section') {
|
|
150
|
-
|
|
163
|
+
sections.push(orderData);
|
|
151
164
|
}
|
|
152
165
|
else {
|
|
153
|
-
|
|
166
|
+
assignments.push(orderData);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
// Process updates in batches
|
|
170
|
+
const processBatch = async (items, type) => {
|
|
171
|
+
for (let i = 0; i < items.length; i += BATCH_SIZE) {
|
|
172
|
+
const batch = items.slice(i, i + BATCH_SIZE);
|
|
173
|
+
await Promise.all(batch.map(item => {
|
|
174
|
+
if (type === 'section') {
|
|
175
|
+
return tx.section.update({ where: { id: item.id }, data: { order: item.order } });
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
return tx.assignment.update({ where: { id: item.id }, data: { order: item.order } });
|
|
179
|
+
}
|
|
180
|
+
}));
|
|
154
181
|
}
|
|
155
|
-
}
|
|
182
|
+
};
|
|
183
|
+
// Process sections and assignments sequentially to avoid transaction overload
|
|
184
|
+
await processBatch(sections, 'section');
|
|
185
|
+
await processBatch(assignments, 'assignment');
|
|
156
186
|
}
|
|
157
187
|
export const assignmentRouter = createTRPCRouter({
|
|
158
188
|
// Reorder an assignment within the unified list (sections + assignments)
|
|
@@ -215,6 +245,9 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
215
245
|
// Normalize to 1..n
|
|
216
246
|
await normalizeUnifiedList(tx, classId, next);
|
|
217
247
|
return tx.assignment.findUnique({ where: { id: movedId } });
|
|
248
|
+
}, {
|
|
249
|
+
maxWait: 10000, // 10 seconds max wait time
|
|
250
|
+
timeout: 30000, // 30 seconds timeout for reordering operations
|
|
218
251
|
});
|
|
219
252
|
return result;
|
|
220
253
|
}),
|
|
@@ -240,6 +273,9 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
240
273
|
const unified = await getUnifiedList(tx, current.classId);
|
|
241
274
|
await normalizeUnifiedList(tx, current.classId, unified.map(item => ({ id: item.id, type: item.type })));
|
|
242
275
|
return tx.assignment.findUnique({ where: { id } });
|
|
276
|
+
}, {
|
|
277
|
+
maxWait: 10000, // 10 seconds max wait time
|
|
278
|
+
timeout: 30000, // 30 seconds timeout for reordering operations
|
|
243
279
|
});
|
|
244
280
|
return updated;
|
|
245
281
|
}),
|
|
@@ -266,53 +302,67 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
266
302
|
// No need to reorder since we're keeping the same position in the unified list
|
|
267
303
|
// If frontend wants to change position, they should call reorder after move
|
|
268
304
|
return tx.assignment.findUnique({ where: { id } });
|
|
305
|
+
}, {
|
|
306
|
+
maxWait: 5000, // 5 seconds max wait time
|
|
307
|
+
timeout: 10000, // 10 seconds timeout
|
|
269
308
|
});
|
|
270
309
|
return updated;
|
|
271
310
|
}),
|
|
272
311
|
create: protectedProcedure
|
|
273
312
|
.input(createAssignmentSchema)
|
|
274
313
|
.mutation(async ({ ctx, input }) => {
|
|
275
|
-
const { classId, title, instructions, dueDate, files, existingFileIds, acceptFiles, acceptExtendedResponse, acceptWorksheet, gradeWithAI, maxGrade, graded, weight, sectionId, type, markSchemeId, gradingBoundaryId, inProgress } = input;
|
|
314
|
+
const { classId, title, instructions, dueDate, files, existingFileIds, aiPolicyLevel, acceptFiles, acceptExtendedResponse, acceptWorksheet, worksheetIds, gradeWithAI, studentIds, maxGrade, graded, weight, sectionId, type, markSchemeId, gradingBoundaryId, inProgress } = input;
|
|
276
315
|
if (!ctx.user) {
|
|
277
316
|
throw new TRPCError({
|
|
278
317
|
code: "UNAUTHORIZED",
|
|
279
318
|
message: "User must be authenticated",
|
|
280
319
|
});
|
|
281
320
|
}
|
|
282
|
-
//
|
|
283
|
-
const classData = await
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
321
|
+
// Pre-fetch data needed for the transaction (outside transaction scope)
|
|
322
|
+
const [classData, rubricData] = await Promise.all([
|
|
323
|
+
prisma.class.findUnique({
|
|
324
|
+
where: { id: classId },
|
|
325
|
+
include: {
|
|
326
|
+
students: {
|
|
327
|
+
select: { id: true }
|
|
328
|
+
}
|
|
288
329
|
}
|
|
289
|
-
}
|
|
290
|
-
|
|
330
|
+
}),
|
|
331
|
+
markSchemeId ? prisma.markScheme.findUnique({
|
|
332
|
+
where: { id: markSchemeId },
|
|
333
|
+
select: {
|
|
334
|
+
structured: true,
|
|
335
|
+
}
|
|
336
|
+
}) : null
|
|
337
|
+
]);
|
|
291
338
|
if (!classData) {
|
|
292
339
|
throw new TRPCError({
|
|
293
340
|
code: "NOT_FOUND",
|
|
294
341
|
message: "Class not found",
|
|
295
342
|
});
|
|
296
343
|
}
|
|
344
|
+
// Calculate max grade outside transaction
|
|
297
345
|
let computedMaxGrade = maxGrade;
|
|
298
|
-
if (markSchemeId) {
|
|
299
|
-
const
|
|
300
|
-
where: { id: markSchemeId },
|
|
301
|
-
select: {
|
|
302
|
-
structured: true,
|
|
303
|
-
}
|
|
304
|
-
});
|
|
305
|
-
const parsedRubric = JSON.parse(rubric?.structured || "{}");
|
|
306
|
-
// Calculate max grade from rubric criteria levels
|
|
346
|
+
if (markSchemeId && rubricData) {
|
|
347
|
+
const parsedRubric = JSON.parse(rubricData.structured || "{}");
|
|
307
348
|
computedMaxGrade = parsedRubric.criteria.reduce((acc, criterion) => {
|
|
308
349
|
const maxPoints = Math.max(...criterion.levels.map((level) => level.points));
|
|
309
350
|
return acc + maxPoints;
|
|
310
351
|
}, 0);
|
|
311
352
|
}
|
|
312
|
-
console.log(
|
|
313
|
-
//
|
|
353
|
+
console.log(studentIds);
|
|
354
|
+
// Prepare submission data outside transaction
|
|
355
|
+
const submissionData = studentIds && studentIds.length > 0
|
|
356
|
+
? studentIds.map((studentId) => ({
|
|
357
|
+
student: { connect: { id: studentId } }
|
|
358
|
+
}))
|
|
359
|
+
: classData.students.map((student) => ({
|
|
360
|
+
student: { connect: { id: student.id } }
|
|
361
|
+
}));
|
|
314
362
|
const teacherId = ctx.user.id;
|
|
363
|
+
// Minimal transaction - only for critical operations
|
|
315
364
|
const assignment = await prisma.$transaction(async (tx) => {
|
|
365
|
+
// Create assignment with order 0 (will be at top)
|
|
316
366
|
const created = await tx.assignment.create({
|
|
317
367
|
data: {
|
|
318
368
|
title,
|
|
@@ -322,11 +372,22 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
322
372
|
graded,
|
|
323
373
|
weight,
|
|
324
374
|
type,
|
|
375
|
+
...(aiPolicyLevel && { aiPolicyLevel }),
|
|
325
376
|
acceptFiles,
|
|
326
377
|
acceptExtendedResponse,
|
|
327
378
|
acceptWorksheet,
|
|
379
|
+
...(worksheetIds && {
|
|
380
|
+
worksheets: {
|
|
381
|
+
connect: worksheetIds.map(id => ({ id }))
|
|
382
|
+
}
|
|
383
|
+
}),
|
|
328
384
|
gradeWithAI,
|
|
329
|
-
|
|
385
|
+
...(studentIds && {
|
|
386
|
+
assignedTo: {
|
|
387
|
+
connect: studentIds.map(id => ({ id }))
|
|
388
|
+
}
|
|
389
|
+
}),
|
|
390
|
+
order: 0, // Set to 0 to place at top
|
|
330
391
|
inProgress: inProgress || false,
|
|
331
392
|
class: {
|
|
332
393
|
connect: { id: classId }
|
|
@@ -347,11 +408,7 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
347
408
|
}
|
|
348
409
|
}),
|
|
349
410
|
submissions: {
|
|
350
|
-
create:
|
|
351
|
-
student: {
|
|
352
|
-
connect: { id: student.id }
|
|
353
|
-
}
|
|
354
|
-
}))
|
|
411
|
+
create: submissionData
|
|
355
412
|
},
|
|
356
413
|
teacher: {
|
|
357
414
|
connect: { id: teacherId }
|
|
@@ -393,241 +450,311 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
393
450
|
}
|
|
394
451
|
}
|
|
395
452
|
});
|
|
396
|
-
//
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
453
|
+
// Optimized reordering - only increment existing items by 1
|
|
454
|
+
// This is much faster than reordering everything
|
|
455
|
+
await tx.assignment.updateMany({
|
|
456
|
+
where: {
|
|
457
|
+
classId: classId,
|
|
458
|
+
id: { not: created.id },
|
|
459
|
+
},
|
|
460
|
+
data: {
|
|
461
|
+
order: { increment: 1 }
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
await tx.section.updateMany({
|
|
465
|
+
where: {
|
|
466
|
+
classId: classId,
|
|
467
|
+
},
|
|
468
|
+
data: {
|
|
469
|
+
order: { increment: 1 }
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
// Update the created assignment to have order 1
|
|
473
|
+
await tx.assignment.update({
|
|
474
|
+
where: { id: created.id },
|
|
475
|
+
data: { order: 1 }
|
|
476
|
+
});
|
|
401
477
|
return created;
|
|
478
|
+
}, {
|
|
479
|
+
maxWait: 10000, // Increased to 10 seconds max wait time
|
|
480
|
+
timeout: 20000, // Increased to 20 seconds timeout for safety
|
|
402
481
|
});
|
|
403
|
-
//
|
|
404
|
-
|
|
405
|
-
//
|
|
406
|
-
let uploadedFiles = [];
|
|
482
|
+
// Handle file operations outside transaction
|
|
483
|
+
const fileOperations = [];
|
|
484
|
+
// Process direct upload files
|
|
407
485
|
if (files && files.length > 0) {
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
connect: { id: file.thumbnailId }
|
|
425
|
-
}
|
|
426
|
-
})
|
|
427
|
-
}))
|
|
428
|
-
}
|
|
486
|
+
fileOperations.push(createDirectUploadFiles(files, ctx.user.id, undefined, assignment.id)
|
|
487
|
+
.then(uploadedFiles => {
|
|
488
|
+
if (uploadedFiles.length > 0) {
|
|
489
|
+
return prisma.assignment.update({
|
|
490
|
+
where: { id: assignment.id },
|
|
491
|
+
data: {
|
|
492
|
+
attachments: {
|
|
493
|
+
create: uploadedFiles.map(file => ({
|
|
494
|
+
name: file.name,
|
|
495
|
+
type: file.type,
|
|
496
|
+
size: file.size,
|
|
497
|
+
path: file.path,
|
|
498
|
+
}))
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
});
|
|
429
502
|
}
|
|
430
|
-
});
|
|
503
|
+
}));
|
|
431
504
|
}
|
|
432
|
-
// Connect existing files
|
|
505
|
+
// Connect existing files
|
|
433
506
|
if (existingFileIds && existingFileIds.length > 0) {
|
|
434
|
-
|
|
507
|
+
fileOperations.push(prisma.assignment.update({
|
|
435
508
|
where: { id: assignment.id },
|
|
436
509
|
data: {
|
|
437
510
|
attachments: {
|
|
438
511
|
connect: existingFileIds.map(fileId => ({ id: fileId }))
|
|
439
512
|
}
|
|
440
513
|
}
|
|
441
|
-
});
|
|
514
|
+
}));
|
|
442
515
|
}
|
|
516
|
+
// Execute file operations in parallel
|
|
517
|
+
await Promise.all(fileOperations);
|
|
518
|
+
// Send notifications asynchronously (non-blocking)
|
|
443
519
|
sendNotifications(classData.students.map(student => student.id), {
|
|
444
520
|
title: `🔔 New assignment for ${classData.name}`,
|
|
445
521
|
content: `The assignment "${title}" has been created in ${classData.name}.\n
|
|
446
522
|
Due date: ${new Date(dueDate).toLocaleDateString()}.
|
|
447
523
|
[Link to assignment](/class/${classId}/assignments/${assignment.id})`
|
|
448
524
|
}).catch(error => {
|
|
449
|
-
logger.error('Failed to send assignment notifications:');
|
|
525
|
+
logger.error('Failed to send assignment notifications:', error);
|
|
450
526
|
});
|
|
451
527
|
return assignment;
|
|
452
528
|
}),
|
|
453
529
|
update: protectedProcedure
|
|
454
530
|
.input(updateAssignmentSchema)
|
|
455
531
|
.mutation(async ({ ctx, input }) => {
|
|
456
|
-
const { id, title, instructions, dueDate, files, existingFileIds, maxGrade, graded, weight, sectionId, type, inProgress, acceptFiles, acceptExtendedResponse, acceptWorksheet, gradeWithAI } = input;
|
|
532
|
+
const { id, title, instructions, dueDate, files, existingFileIds, worksheetIds, aiPolicyLevel, maxGrade, graded, weight, sectionId, type, inProgress, acceptFiles, acceptExtendedResponse, acceptWorksheet, gradeWithAI, studentIds } = input;
|
|
457
533
|
if (!ctx.user) {
|
|
458
534
|
throw new TRPCError({
|
|
459
535
|
code: "UNAUTHORIZED",
|
|
460
536
|
message: "User must be authenticated",
|
|
461
537
|
});
|
|
462
538
|
}
|
|
463
|
-
//
|
|
464
|
-
const assignment = await
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
539
|
+
// Pre-fetch all necessary data outside transaction
|
|
540
|
+
const [assignment, classData] = await Promise.all([
|
|
541
|
+
prisma.assignment.findFirst({
|
|
542
|
+
where: {
|
|
543
|
+
id,
|
|
544
|
+
teacherId: ctx.user.id,
|
|
545
|
+
},
|
|
546
|
+
include: {
|
|
547
|
+
attachments: {
|
|
548
|
+
select: {
|
|
549
|
+
id: true,
|
|
550
|
+
name: true,
|
|
551
|
+
type: true,
|
|
552
|
+
path: true,
|
|
553
|
+
size: true,
|
|
554
|
+
uploadStatus: true,
|
|
555
|
+
thumbnail: {
|
|
556
|
+
select: {
|
|
557
|
+
path: true
|
|
558
|
+
}
|
|
481
559
|
}
|
|
560
|
+
},
|
|
561
|
+
},
|
|
562
|
+
class: {
|
|
563
|
+
select: {
|
|
564
|
+
id: true,
|
|
565
|
+
name: true
|
|
482
566
|
}
|
|
483
567
|
},
|
|
568
|
+
markScheme: true,
|
|
484
569
|
},
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
570
|
+
}),
|
|
571
|
+
prisma.class.findFirst({
|
|
572
|
+
where: {
|
|
573
|
+
assignments: {
|
|
574
|
+
some: { id }
|
|
489
575
|
}
|
|
490
576
|
},
|
|
491
|
-
|
|
492
|
-
|
|
577
|
+
include: {
|
|
578
|
+
students: {
|
|
579
|
+
select: { id: true }
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
})
|
|
583
|
+
]);
|
|
493
584
|
if (!assignment) {
|
|
494
585
|
throw new TRPCError({
|
|
495
586
|
code: "NOT_FOUND",
|
|
496
587
|
message: "Assignment not found",
|
|
497
588
|
});
|
|
498
589
|
}
|
|
499
|
-
//
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
590
|
+
// Prepare submission data outside transaction if needed
|
|
591
|
+
const submissionData = studentIds && studentIds.length > 0
|
|
592
|
+
? studentIds.map((studentId) => ({
|
|
593
|
+
student: { connect: { id: studentId } }
|
|
594
|
+
}))
|
|
595
|
+
: classData?.students.map((student) => ({
|
|
596
|
+
student: { connect: { id: student.id } }
|
|
597
|
+
}));
|
|
598
|
+
// Handle file deletion operations outside transaction
|
|
599
|
+
const fileDeletionPromises = [];
|
|
506
600
|
if (input.removedAttachments && input.removedAttachments.length > 0) {
|
|
507
601
|
const filesToDelete = assignment.attachments.filter((file) => input.removedAttachments.includes(file.id));
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
await deleteFile(file.thumbnail.path);
|
|
518
|
-
}
|
|
602
|
+
filesToDelete.forEach((file) => {
|
|
603
|
+
if (file.uploadStatus === 'COMPLETED') {
|
|
604
|
+
fileDeletionPromises.push(deleteFile(file.path).catch(error => {
|
|
605
|
+
console.warn(`Failed to delete file ${file.path}:`, error);
|
|
606
|
+
}));
|
|
607
|
+
if (file.thumbnail?.path) {
|
|
608
|
+
fileDeletionPromises.push(deleteFile(file.thumbnail.path).catch(error => {
|
|
609
|
+
console.warn(`Failed to delete thumbnail ${file.thumbnail.path}:`, error);
|
|
610
|
+
}));
|
|
519
611
|
}
|
|
520
612
|
}
|
|
521
|
-
|
|
522
|
-
console.warn(`Failed to delete file ${file.path}:`, error);
|
|
523
|
-
}
|
|
524
|
-
}));
|
|
613
|
+
});
|
|
525
614
|
}
|
|
526
|
-
//
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
size: file.size,
|
|
555
|
-
path: file.path,
|
|
556
|
-
...(file.thumbnailId && {
|
|
557
|
-
thumbnail: {
|
|
558
|
-
connect: { id: file.thumbnailId }
|
|
559
|
-
}
|
|
560
|
-
})
|
|
561
|
-
}))
|
|
562
|
-
}
|
|
563
|
-
}),
|
|
564
|
-
...(existingFileIds && existingFileIds.length > 0 && {
|
|
565
|
-
attachments: {
|
|
566
|
-
connect: existingFileIds.map(fileId => ({ id: fileId }))
|
|
567
|
-
}
|
|
568
|
-
}),
|
|
569
|
-
...(input.removedAttachments && input.removedAttachments.length > 0 && {
|
|
570
|
-
attachments: {
|
|
571
|
-
deleteMany: {
|
|
572
|
-
id: { in: input.removedAttachments }
|
|
615
|
+
// Execute file deletions in parallel
|
|
616
|
+
await Promise.all(fileDeletionPromises);
|
|
617
|
+
// Prepare file upload operations
|
|
618
|
+
let uploadedFiles = [];
|
|
619
|
+
if (files && files.length > 0) {
|
|
620
|
+
uploadedFiles = await createDirectUploadFiles(files, ctx.user.id, undefined, input.id);
|
|
621
|
+
}
|
|
622
|
+
// Minimal transaction for database update
|
|
623
|
+
const updatedAssignment = await prisma.$transaction(async (tx) => {
|
|
624
|
+
return tx.assignment.update({
|
|
625
|
+
where: { id },
|
|
626
|
+
data: {
|
|
627
|
+
...(title && { title }),
|
|
628
|
+
...(instructions && { instructions }),
|
|
629
|
+
...(dueDate && { dueDate: new Date(dueDate) }),
|
|
630
|
+
...(maxGrade && { maxGrade }),
|
|
631
|
+
...(graded !== undefined && { graded }),
|
|
632
|
+
...(weight && { weight }),
|
|
633
|
+
...(type && { type }),
|
|
634
|
+
...(inProgress !== undefined && { inProgress }),
|
|
635
|
+
...(acceptFiles !== undefined && { acceptFiles }),
|
|
636
|
+
...(acceptExtendedResponse !== undefined && { acceptExtendedResponse }),
|
|
637
|
+
...(acceptWorksheet !== undefined && { acceptWorksheet }),
|
|
638
|
+
...(gradeWithAI !== undefined && { gradeWithAI }),
|
|
639
|
+
...(studentIds && {
|
|
640
|
+
assignedTo: {
|
|
641
|
+
set: [], // Clear existing
|
|
642
|
+
connect: studentIds.map(id => ({ id }))
|
|
573
643
|
}
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
644
|
+
}),
|
|
645
|
+
...(submissionData && {
|
|
646
|
+
submissions: {
|
|
647
|
+
deleteMany: {}, // Clear existing submissions
|
|
648
|
+
create: submissionData
|
|
649
|
+
}
|
|
650
|
+
}),
|
|
651
|
+
...(aiPolicyLevel !== undefined && { aiPolicyLevel }),
|
|
652
|
+
...(sectionId !== undefined && {
|
|
653
|
+
section: sectionId ? {
|
|
654
|
+
connect: { id: sectionId }
|
|
655
|
+
} : {
|
|
656
|
+
disconnect: true
|
|
657
|
+
}
|
|
658
|
+
}),
|
|
659
|
+
...(worksheetIds && {
|
|
660
|
+
worksheets: {
|
|
661
|
+
set: [], // Clear existing
|
|
662
|
+
connect: worksheetIds.map(id => ({ id }))
|
|
663
|
+
}
|
|
664
|
+
}),
|
|
665
|
+
...(uploadedFiles.length > 0 && {
|
|
666
|
+
attachments: {
|
|
667
|
+
create: uploadedFiles.map(file => ({
|
|
668
|
+
name: file.name,
|
|
669
|
+
type: file.type,
|
|
670
|
+
size: file.size,
|
|
671
|
+
path: file.path,
|
|
672
|
+
...(file.thumbnailId && {
|
|
673
|
+
thumbnail: {
|
|
674
|
+
connect: { id: file.thumbnailId }
|
|
675
|
+
}
|
|
676
|
+
})
|
|
677
|
+
}))
|
|
678
|
+
}
|
|
679
|
+
}),
|
|
680
|
+
...(existingFileIds && existingFileIds.length > 0 && {
|
|
681
|
+
attachments: {
|
|
682
|
+
connect: existingFileIds.map(fileId => ({ id: fileId }))
|
|
683
|
+
}
|
|
684
|
+
}),
|
|
685
|
+
...(input.removedAttachments && input.removedAttachments.length > 0 && {
|
|
686
|
+
attachments: {
|
|
687
|
+
deleteMany: {
|
|
688
|
+
id: { in: input.removedAttachments }
|
|
593
689
|
}
|
|
594
690
|
}
|
|
595
|
-
}
|
|
596
|
-
},
|
|
597
|
-
attachments: {
|
|
598
|
-
select: {
|
|
599
|
-
id: true,
|
|
600
|
-
name: true,
|
|
601
|
-
type: true,
|
|
602
|
-
thumbnail: true,
|
|
603
|
-
size: true,
|
|
604
|
-
path: true,
|
|
605
|
-
uploadedAt: true,
|
|
606
|
-
thumbnailId: true,
|
|
607
|
-
}
|
|
691
|
+
}),
|
|
608
692
|
},
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
693
|
+
select: {
|
|
694
|
+
id: true,
|
|
695
|
+
title: true,
|
|
696
|
+
instructions: true,
|
|
697
|
+
dueDate: true,
|
|
698
|
+
maxGrade: true,
|
|
699
|
+
graded: true,
|
|
700
|
+
weight: true,
|
|
701
|
+
type: true,
|
|
702
|
+
createdAt: true,
|
|
703
|
+
markSchemeId: true,
|
|
704
|
+
submissions: {
|
|
705
|
+
select: {
|
|
706
|
+
student: {
|
|
707
|
+
select: {
|
|
708
|
+
id: true,
|
|
709
|
+
username: true
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
},
|
|
714
|
+
attachments: {
|
|
715
|
+
select: {
|
|
716
|
+
id: true,
|
|
717
|
+
name: true,
|
|
718
|
+
type: true,
|
|
719
|
+
thumbnail: true,
|
|
720
|
+
size: true,
|
|
721
|
+
path: true,
|
|
722
|
+
uploadedAt: true,
|
|
723
|
+
thumbnailId: true,
|
|
724
|
+
}
|
|
725
|
+
},
|
|
726
|
+
section: true,
|
|
727
|
+
teacher: true,
|
|
728
|
+
class: true
|
|
729
|
+
}
|
|
730
|
+
});
|
|
731
|
+
}, {
|
|
732
|
+
maxWait: 5000, // 5 seconds max wait time
|
|
733
|
+
timeout: 10000, // 10 seconds timeout
|
|
613
734
|
});
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
735
|
+
// Handle rubric max grade calculation outside transaction
|
|
736
|
+
if (updatedAssignment.markSchemeId) {
|
|
737
|
+
prisma.markScheme.findUnique({
|
|
738
|
+
where: { id: updatedAssignment.markSchemeId },
|
|
617
739
|
select: {
|
|
618
740
|
structured: true,
|
|
619
741
|
}
|
|
620
|
-
})
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
742
|
+
}).then(rubric => {
|
|
743
|
+
if (rubric) {
|
|
744
|
+
const parsedRubric = JSON.parse(rubric.structured || "{}");
|
|
745
|
+
const computedMaxGrade = parsedRubric.criteria?.reduce((acc, criterion) => {
|
|
746
|
+
const maxPoints = Math.max(...criterion.levels.map((level) => level.points));
|
|
747
|
+
return acc + maxPoints;
|
|
748
|
+
}, 0) || 0;
|
|
749
|
+
return prisma.assignment.update({
|
|
750
|
+
where: { id },
|
|
751
|
+
data: {
|
|
752
|
+
maxGrade: computedMaxGrade,
|
|
753
|
+
}
|
|
754
|
+
});
|
|
630
755
|
}
|
|
756
|
+
}).catch(error => {
|
|
757
|
+
logger.error('Failed to update max grade from rubric:', error);
|
|
631
758
|
});
|
|
632
759
|
}
|
|
633
760
|
return updatedAssignment;
|
|
@@ -755,6 +882,12 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
755
882
|
username: true
|
|
756
883
|
}
|
|
757
884
|
},
|
|
885
|
+
worksheets: {
|
|
886
|
+
select: {
|
|
887
|
+
id: true,
|
|
888
|
+
name: true,
|
|
889
|
+
}
|
|
890
|
+
},
|
|
758
891
|
class: {
|
|
759
892
|
select: {
|
|
760
893
|
id: true,
|
|
@@ -838,6 +971,12 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
838
971
|
structured: true,
|
|
839
972
|
}
|
|
840
973
|
},
|
|
974
|
+
worksheets: {
|
|
975
|
+
select: {
|
|
976
|
+
id: true,
|
|
977
|
+
name: true,
|
|
978
|
+
}
|
|
979
|
+
},
|
|
841
980
|
gradingBoundary: {
|
|
842
981
|
select: {
|
|
843
982
|
id: true,
|
|
@@ -945,6 +1084,12 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
945
1084
|
id: true,
|
|
946
1085
|
structured: true,
|
|
947
1086
|
}
|
|
1087
|
+
},
|
|
1088
|
+
worksheets: {
|
|
1089
|
+
select: {
|
|
1090
|
+
id: true,
|
|
1091
|
+
name: true,
|
|
1092
|
+
}
|
|
948
1093
|
}
|
|
949
1094
|
},
|
|
950
1095
|
},
|
|
@@ -970,7 +1115,7 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
970
1115
|
message: "User must be authenticated",
|
|
971
1116
|
});
|
|
972
1117
|
}
|
|
973
|
-
const { submissionId, submit, newAttachments, existingFileIds, removedAttachments } = input;
|
|
1118
|
+
const { submissionId, submit, newAttachments, existingFileIds, removedAttachments, extendedResponse } = input;
|
|
974
1119
|
const submission = await prisma.submission.findFirst({
|
|
975
1120
|
where: {
|
|
976
1121
|
id: submissionId,
|
|
@@ -1129,6 +1274,7 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
1129
1274
|
},
|
|
1130
1275
|
},
|
|
1131
1276
|
}),
|
|
1277
|
+
...(extendedResponse !== undefined && { extendedResponse }),
|
|
1132
1278
|
},
|
|
1133
1279
|
include: {
|
|
1134
1280
|
attachments: {
|