@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
|
@@ -25,10 +25,13 @@ const createAssignmentSchema = z.object({
|
|
|
25
25
|
dueDate: z.string(),
|
|
26
26
|
files: z.array(directFileSchema).optional(), // Use direct file schema
|
|
27
27
|
existingFileIds: z.array(z.string()).optional(),
|
|
28
|
+
aiPolicyLevel: z.number().default(0),
|
|
28
29
|
acceptFiles: z.boolean().optional(),
|
|
29
30
|
acceptExtendedResponse: z.boolean().optional(),
|
|
30
31
|
acceptWorksheet: z.boolean().optional(),
|
|
32
|
+
worksheetIds: z.array(z.string()).optional(),
|
|
31
33
|
gradeWithAI: z.boolean().optional(),
|
|
34
|
+
studentIds: z.array(z.string()).optional(),
|
|
32
35
|
maxGrade: z.number().optional(),
|
|
33
36
|
graded: z.boolean().optional(),
|
|
34
37
|
weight: z.number().optional(),
|
|
@@ -46,10 +49,13 @@ const updateAssignmentSchema = z.object({
|
|
|
46
49
|
instructions: z.string().optional(),
|
|
47
50
|
dueDate: z.string().optional(),
|
|
48
51
|
files: z.array(directFileSchema).optional(), // Use direct file schema
|
|
52
|
+
aiPolicyLevel: z.number().default(0),
|
|
49
53
|
acceptFiles: z.boolean().optional(),
|
|
50
54
|
acceptExtendedResponse: z.boolean().optional(),
|
|
51
55
|
acceptWorksheet: z.boolean().optional(),
|
|
56
|
+
worksheetIds: z.array(z.string()).optional(),
|
|
52
57
|
gradeWithAI: z.boolean().optional(),
|
|
58
|
+
studentIds: z.array(z.string()).optional(),
|
|
53
59
|
existingFileIds: z.array(z.string()).optional(),
|
|
54
60
|
removedAttachments: z.array(z.string()).optional(),
|
|
55
61
|
maxGrade: z.number().optional(),
|
|
@@ -76,6 +82,7 @@ const submissionSchema = z.object({
|
|
|
76
82
|
submissionId: z.string(),
|
|
77
83
|
submit: z.boolean().optional(),
|
|
78
84
|
newAttachments: z.array(directFileSchema).optional(), // Use direct file schema
|
|
85
|
+
extendedResponse: z.string().optional(),
|
|
79
86
|
existingFileIds: z.array(z.string()).optional(),
|
|
80
87
|
removedAttachments: z.array(z.string()).optional(),
|
|
81
88
|
});
|
|
@@ -163,16 +170,42 @@ async function getUnifiedList(tx: any, classId: string) {
|
|
|
163
170
|
}
|
|
164
171
|
|
|
165
172
|
// Helper function to normalize unified list to 1..n
|
|
173
|
+
// Updated to batch updates to prevent timeouts with large lists
|
|
166
174
|
async function normalizeUnifiedList(tx: any, classId: string, orderedItems: Array<{ id: string; type: 'section' | 'assignment' }>) {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
}
|
|
175
|
-
|
|
175
|
+
const BATCH_SIZE = 10; // Process 10 items at a time to avoid overwhelming the transaction
|
|
176
|
+
|
|
177
|
+
// Group items by type for more efficient updates
|
|
178
|
+
const sections: Array<{ id: string; order: number }> = [];
|
|
179
|
+
const assignments: Array<{ id: string; order: number }> = [];
|
|
180
|
+
|
|
181
|
+
orderedItems.forEach((item, index) => {
|
|
182
|
+
const orderData = { id: item.id, order: index + 1 };
|
|
183
|
+
if (item.type === 'section') {
|
|
184
|
+
sections.push(orderData);
|
|
185
|
+
} else {
|
|
186
|
+
assignments.push(orderData);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// Process updates in batches
|
|
191
|
+
const processBatch = async (items: Array<{ id: string; order: number }>, type: 'section' | 'assignment') => {
|
|
192
|
+
for (let i = 0; i < items.length; i += BATCH_SIZE) {
|
|
193
|
+
const batch = items.slice(i, i + BATCH_SIZE);
|
|
194
|
+
await Promise.all(
|
|
195
|
+
batch.map(item => {
|
|
196
|
+
if (type === 'section') {
|
|
197
|
+
return tx.section.update({ where: { id: item.id }, data: { order: item.order } });
|
|
198
|
+
} else {
|
|
199
|
+
return tx.assignment.update({ where: { id: item.id }, data: { order: item.order } });
|
|
200
|
+
}
|
|
201
|
+
})
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// Process sections and assignments sequentially to avoid transaction overload
|
|
207
|
+
await processBatch(sections, 'section');
|
|
208
|
+
await processBatch(assignments, 'assignment');
|
|
176
209
|
}
|
|
177
210
|
|
|
178
211
|
export const assignmentRouter = createTRPCRouter({
|
|
@@ -243,6 +276,9 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
243
276
|
await normalizeUnifiedList(tx, classId, next);
|
|
244
277
|
|
|
245
278
|
return tx.assignment.findUnique({ where: { id: movedId } });
|
|
279
|
+
}, {
|
|
280
|
+
maxWait: 10000, // 10 seconds max wait time
|
|
281
|
+
timeout: 30000, // 30 seconds timeout for reordering operations
|
|
246
282
|
});
|
|
247
283
|
|
|
248
284
|
return result;
|
|
@@ -274,6 +310,9 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
274
310
|
await normalizeUnifiedList(tx, current.classId, unified.map(item => ({ id: item.id, type: item.type })));
|
|
275
311
|
|
|
276
312
|
return tx.assignment.findUnique({ where: { id } });
|
|
313
|
+
}, {
|
|
314
|
+
maxWait: 10000, // 10 seconds max wait time
|
|
315
|
+
timeout: 30000, // 30 seconds timeout for reordering operations
|
|
277
316
|
});
|
|
278
317
|
|
|
279
318
|
return updated;
|
|
@@ -307,6 +346,9 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
307
346
|
// If frontend wants to change position, they should call reorder after move
|
|
308
347
|
|
|
309
348
|
return tx.assignment.findUnique({ where: { id } });
|
|
349
|
+
}, {
|
|
350
|
+
maxWait: 5000, // 5 seconds max wait time
|
|
351
|
+
timeout: 10000, // 10 seconds timeout
|
|
310
352
|
});
|
|
311
353
|
|
|
312
354
|
return updated;
|
|
@@ -315,7 +357,7 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
315
357
|
create: protectedProcedure
|
|
316
358
|
.input(createAssignmentSchema)
|
|
317
359
|
.mutation(async ({ ctx, input }) => {
|
|
318
|
-
const { classId, title, instructions, dueDate, files, existingFileIds, acceptFiles, acceptExtendedResponse, acceptWorksheet, gradeWithAI, maxGrade, graded, weight, sectionId, type, markSchemeId, gradingBoundaryId, inProgress } = input;
|
|
360
|
+
const { classId, title, instructions, dueDate, files, existingFileIds, aiPolicyLevel, acceptFiles, acceptExtendedResponse, acceptWorksheet, worksheetIds, gradeWithAI, studentIds, maxGrade, graded, weight, sectionId, type, markSchemeId, gradingBoundaryId, inProgress } = input;
|
|
319
361
|
|
|
320
362
|
if (!ctx.user) {
|
|
321
363
|
throw new TRPCError({
|
|
@@ -324,15 +366,23 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
324
366
|
});
|
|
325
367
|
}
|
|
326
368
|
|
|
327
|
-
//
|
|
328
|
-
const classData = await
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
369
|
+
// Pre-fetch data needed for the transaction (outside transaction scope)
|
|
370
|
+
const [classData, rubricData] = await Promise.all([
|
|
371
|
+
prisma.class.findUnique({
|
|
372
|
+
where: { id: classId },
|
|
373
|
+
include: {
|
|
374
|
+
students: {
|
|
375
|
+
select: { id: true }
|
|
376
|
+
}
|
|
333
377
|
}
|
|
334
|
-
}
|
|
335
|
-
|
|
378
|
+
}),
|
|
379
|
+
markSchemeId ? prisma.markScheme.findUnique({
|
|
380
|
+
where: { id: markSchemeId },
|
|
381
|
+
select: {
|
|
382
|
+
structured: true,
|
|
383
|
+
}
|
|
384
|
+
}) : null
|
|
385
|
+
]);
|
|
336
386
|
|
|
337
387
|
if (!classData) {
|
|
338
388
|
throw new TRPCError({
|
|
@@ -341,161 +391,198 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
341
391
|
});
|
|
342
392
|
}
|
|
343
393
|
|
|
394
|
+
// Calculate max grade outside transaction
|
|
344
395
|
let computedMaxGrade = maxGrade;
|
|
345
|
-
if (markSchemeId) {
|
|
346
|
-
const
|
|
347
|
-
where: { id: markSchemeId },
|
|
348
|
-
select: {
|
|
349
|
-
structured: true,
|
|
350
|
-
}
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
const parsedRubric = JSON.parse(rubric?.structured || "{}");
|
|
354
|
-
|
|
355
|
-
// Calculate max grade from rubric criteria levels
|
|
396
|
+
if (markSchemeId && rubricData) {
|
|
397
|
+
const parsedRubric = JSON.parse(rubricData.structured || "{}");
|
|
356
398
|
computedMaxGrade = parsedRubric.criteria.reduce((acc: number, criterion: any) => {
|
|
357
399
|
const maxPoints = Math.max(...criterion.levels.map((level: any) => level.points));
|
|
358
400
|
return acc + maxPoints;
|
|
359
401
|
}, 0);
|
|
360
402
|
}
|
|
361
|
-
console.log(markSchemeId, gradingBoundaryId);
|
|
362
403
|
|
|
363
|
-
|
|
364
|
-
|
|
404
|
+
console.log(studentIds);
|
|
405
|
+
|
|
406
|
+
// Prepare submission data outside transaction
|
|
407
|
+
const submissionData = studentIds && studentIds.length > 0
|
|
408
|
+
? studentIds.map((studentId) => ({
|
|
409
|
+
student: { connect: { id: studentId } }
|
|
410
|
+
}))
|
|
411
|
+
: classData.students.map((student) => ({
|
|
412
|
+
student: { connect: { id: student.id } }
|
|
413
|
+
}));
|
|
414
|
+
|
|
415
|
+
const teacherId = ctx.user.id;
|
|
416
|
+
|
|
417
|
+
// Minimal transaction - only for critical operations
|
|
365
418
|
const assignment = await prisma.$transaction(async (tx) => {
|
|
419
|
+
// Create assignment with order 0 (will be at top)
|
|
366
420
|
const created = await tx.assignment.create({
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
connect: { id: classId }
|
|
383
|
-
},
|
|
384
|
-
...(sectionId && {
|
|
385
|
-
section: {
|
|
386
|
-
connect: { id: sectionId }
|
|
387
|
-
}
|
|
388
|
-
}),
|
|
389
|
-
...(markSchemeId && {
|
|
390
|
-
markScheme: {
|
|
391
|
-
connect: { id: markSchemeId }
|
|
392
|
-
}
|
|
393
|
-
}),
|
|
394
|
-
...(gradingBoundaryId && {
|
|
395
|
-
gradingBoundary: {
|
|
396
|
-
connect: { id: gradingBoundaryId }
|
|
397
|
-
}
|
|
398
|
-
}),
|
|
399
|
-
submissions: {
|
|
400
|
-
create: classData.students.map((student) => ({
|
|
401
|
-
student: {
|
|
402
|
-
connect: { id: student.id }
|
|
421
|
+
data: {
|
|
422
|
+
title,
|
|
423
|
+
instructions,
|
|
424
|
+
dueDate: new Date(dueDate),
|
|
425
|
+
maxGrade: markSchemeId ? computedMaxGrade : maxGrade,
|
|
426
|
+
graded,
|
|
427
|
+
weight,
|
|
428
|
+
type,
|
|
429
|
+
...(aiPolicyLevel && { aiPolicyLevel }),
|
|
430
|
+
acceptFiles,
|
|
431
|
+
acceptExtendedResponse,
|
|
432
|
+
acceptWorksheet,
|
|
433
|
+
...(worksheetIds && {
|
|
434
|
+
worksheets: {
|
|
435
|
+
connect: worksheetIds.map(id => ({ id }))
|
|
403
436
|
}
|
|
404
|
-
})
|
|
405
|
-
|
|
437
|
+
}),
|
|
438
|
+
gradeWithAI,
|
|
439
|
+
...(studentIds && {
|
|
440
|
+
assignedTo: {
|
|
441
|
+
connect: studentIds.map(id => ({ id }))
|
|
442
|
+
}
|
|
443
|
+
}),
|
|
444
|
+
order: 0, // Set to 0 to place at top
|
|
445
|
+
inProgress: inProgress || false,
|
|
446
|
+
class: {
|
|
447
|
+
connect: { id: classId }
|
|
448
|
+
},
|
|
449
|
+
...(sectionId && {
|
|
450
|
+
section: {
|
|
451
|
+
connect: { id: sectionId }
|
|
452
|
+
}
|
|
453
|
+
}),
|
|
454
|
+
...(markSchemeId && {
|
|
455
|
+
markScheme: {
|
|
456
|
+
connect: { id: markSchemeId }
|
|
457
|
+
}
|
|
458
|
+
}),
|
|
459
|
+
...(gradingBoundaryId && {
|
|
460
|
+
gradingBoundary: {
|
|
461
|
+
connect: { id: gradingBoundaryId }
|
|
462
|
+
}
|
|
463
|
+
}),
|
|
464
|
+
submissions: {
|
|
465
|
+
create: submissionData
|
|
466
|
+
},
|
|
406
467
|
teacher: {
|
|
407
468
|
connect: { id: teacherId }
|
|
408
469
|
}
|
|
409
|
-
},
|
|
410
|
-
select: {
|
|
411
|
-
id: true,
|
|
412
|
-
title: true,
|
|
413
|
-
instructions: true,
|
|
414
|
-
dueDate: true,
|
|
415
|
-
maxGrade: true,
|
|
416
|
-
graded: true,
|
|
417
|
-
weight: true,
|
|
418
|
-
type: true,
|
|
419
|
-
attachments: {
|
|
420
|
-
select: {
|
|
421
|
-
id: true,
|
|
422
|
-
name: true,
|
|
423
|
-
type: true,
|
|
424
|
-
}
|
|
425
|
-
},
|
|
426
|
-
section: {
|
|
427
|
-
select: {
|
|
428
|
-
id: true,
|
|
429
|
-
name: true
|
|
430
|
-
}
|
|
431
470
|
},
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
471
|
+
select: {
|
|
472
|
+
id: true,
|
|
473
|
+
title: true,
|
|
474
|
+
instructions: true,
|
|
475
|
+
dueDate: true,
|
|
476
|
+
maxGrade: true,
|
|
477
|
+
graded: true,
|
|
478
|
+
weight: true,
|
|
479
|
+
type: true,
|
|
480
|
+
attachments: {
|
|
481
|
+
select: {
|
|
482
|
+
id: true,
|
|
483
|
+
name: true,
|
|
484
|
+
type: true,
|
|
485
|
+
}
|
|
486
|
+
},
|
|
487
|
+
section: {
|
|
488
|
+
select: {
|
|
489
|
+
id: true,
|
|
490
|
+
name: true
|
|
491
|
+
}
|
|
492
|
+
},
|
|
493
|
+
teacher: {
|
|
494
|
+
select: {
|
|
495
|
+
id: true,
|
|
496
|
+
username: true
|
|
497
|
+
}
|
|
498
|
+
},
|
|
499
|
+
class: {
|
|
500
|
+
select: {
|
|
501
|
+
id: true,
|
|
502
|
+
name: true
|
|
503
|
+
}
|
|
436
504
|
}
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
// Optimized reordering - only increment existing items by 1
|
|
509
|
+
// This is much faster than reordering everything
|
|
510
|
+
await tx.assignment.updateMany({
|
|
511
|
+
where: {
|
|
512
|
+
classId: classId,
|
|
513
|
+
id: { not: created.id },
|
|
437
514
|
},
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
id: true,
|
|
441
|
-
name: true
|
|
442
|
-
}
|
|
515
|
+
data: {
|
|
516
|
+
order: { increment: 1 }
|
|
443
517
|
}
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
await tx.section.updateMany({
|
|
521
|
+
where: {
|
|
522
|
+
classId: classId,
|
|
523
|
+
},
|
|
524
|
+
data: {
|
|
525
|
+
order: { increment: 1 }
|
|
444
526
|
}
|
|
445
527
|
});
|
|
446
528
|
|
|
447
|
-
//
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
529
|
+
// Update the created assignment to have order 1
|
|
530
|
+
await tx.assignment.update({
|
|
531
|
+
where: { id: created.id },
|
|
532
|
+
data: { order: 1 }
|
|
533
|
+
});
|
|
452
534
|
|
|
453
535
|
return created;
|
|
536
|
+
}, {
|
|
537
|
+
maxWait: 10000, // Increased to 10 seconds max wait time
|
|
538
|
+
timeout: 20000, // Increased to 20 seconds timeout for safety
|
|
454
539
|
});
|
|
455
540
|
|
|
456
|
-
//
|
|
457
|
-
|
|
458
|
-
// Actual file uploads should use getAssignmentUploadUrls endpoint
|
|
459
|
-
let uploadedFiles: UploadedFile[] = [];
|
|
460
|
-
if (files && files.length > 0) {
|
|
461
|
-
// Create direct upload files instead of processing base64
|
|
462
|
-
uploadedFiles = await createDirectUploadFiles(files, ctx.user.id, undefined, assignment.id);
|
|
463
|
-
}
|
|
541
|
+
// Handle file operations outside transaction
|
|
542
|
+
const fileOperations: Promise<any>[] = [];
|
|
464
543
|
|
|
465
|
-
//
|
|
466
|
-
if (
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
544
|
+
// Process direct upload files
|
|
545
|
+
if (files && files.length > 0) {
|
|
546
|
+
fileOperations.push(
|
|
547
|
+
createDirectUploadFiles(files, ctx.user.id, undefined, assignment.id)
|
|
548
|
+
.then(uploadedFiles => {
|
|
549
|
+
if (uploadedFiles.length > 0) {
|
|
550
|
+
return prisma.assignment.update({
|
|
551
|
+
where: { id: assignment.id },
|
|
552
|
+
data: {
|
|
553
|
+
attachments: {
|
|
554
|
+
create: uploadedFiles.map(file => ({
|
|
555
|
+
name: file.name,
|
|
556
|
+
type: file.type,
|
|
557
|
+
size: file.size,
|
|
558
|
+
path: file.path,
|
|
559
|
+
}))
|
|
560
|
+
}
|
|
479
561
|
}
|
|
480
|
-
})
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
});
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
})
|
|
565
|
+
);
|
|
485
566
|
}
|
|
486
567
|
|
|
487
|
-
// Connect existing files
|
|
568
|
+
// Connect existing files
|
|
488
569
|
if (existingFileIds && existingFileIds.length > 0) {
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
570
|
+
fileOperations.push(
|
|
571
|
+
prisma.assignment.update({
|
|
572
|
+
where: { id: assignment.id },
|
|
573
|
+
data: {
|
|
574
|
+
attachments: {
|
|
575
|
+
connect: existingFileIds.map(fileId => ({ id: fileId }))
|
|
576
|
+
}
|
|
494
577
|
}
|
|
495
|
-
}
|
|
496
|
-
|
|
578
|
+
})
|
|
579
|
+
);
|
|
497
580
|
}
|
|
581
|
+
|
|
582
|
+
// Execute file operations in parallel
|
|
583
|
+
await Promise.all(fileOperations);
|
|
498
584
|
|
|
585
|
+
// Send notifications asynchronously (non-blocking)
|
|
499
586
|
sendNotifications(classData.students.map(student => student.id), {
|
|
500
587
|
title: `🔔 New assignment for ${classData.name}`,
|
|
501
588
|
content:
|
|
@@ -503,7 +590,7 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
503
590
|
Due date: ${new Date(dueDate).toLocaleDateString()}.
|
|
504
591
|
[Link to assignment](/class/${classId}/assignments/${assignment.id})`
|
|
505
592
|
}).catch(error => {
|
|
506
|
-
logger.error('Failed to send assignment notifications:');
|
|
593
|
+
logger.error('Failed to send assignment notifications:', error);
|
|
507
594
|
});
|
|
508
595
|
|
|
509
596
|
return assignment;
|
|
@@ -511,7 +598,7 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
511
598
|
update: protectedProcedure
|
|
512
599
|
.input(updateAssignmentSchema)
|
|
513
600
|
.mutation(async ({ ctx, input }) => {
|
|
514
|
-
const { id, title, instructions, dueDate, files, existingFileIds, maxGrade, graded, weight, sectionId, type, inProgress, acceptFiles, acceptExtendedResponse, acceptWorksheet, gradeWithAI } = input;
|
|
601
|
+
const { id, title, instructions, dueDate, files, existingFileIds, worksheetIds, aiPolicyLevel, maxGrade, graded, weight, sectionId, type, inProgress, acceptFiles, acceptExtendedResponse, acceptWorksheet, gradeWithAI, studentIds } = input;
|
|
515
602
|
|
|
516
603
|
if (!ctx.user) {
|
|
517
604
|
throw new TRPCError({
|
|
@@ -520,36 +607,51 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
520
607
|
});
|
|
521
608
|
}
|
|
522
609
|
|
|
523
|
-
//
|
|
524
|
-
const assignment = await
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
610
|
+
// Pre-fetch all necessary data outside transaction
|
|
611
|
+
const [assignment, classData] = await Promise.all([
|
|
612
|
+
prisma.assignment.findFirst({
|
|
613
|
+
where: {
|
|
614
|
+
id,
|
|
615
|
+
teacherId: ctx.user.id,
|
|
616
|
+
},
|
|
617
|
+
include: {
|
|
618
|
+
attachments: {
|
|
619
|
+
select: {
|
|
620
|
+
id: true,
|
|
621
|
+
name: true,
|
|
622
|
+
type: true,
|
|
623
|
+
path: true,
|
|
624
|
+
size: true,
|
|
625
|
+
uploadStatus: true,
|
|
626
|
+
thumbnail: {
|
|
627
|
+
select: {
|
|
628
|
+
path: true
|
|
629
|
+
}
|
|
541
630
|
}
|
|
631
|
+
},
|
|
632
|
+
},
|
|
633
|
+
class: {
|
|
634
|
+
select: {
|
|
635
|
+
id: true,
|
|
636
|
+
name: true
|
|
542
637
|
}
|
|
543
638
|
},
|
|
639
|
+
markScheme: true,
|
|
544
640
|
},
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
641
|
+
}),
|
|
642
|
+
prisma.class.findFirst({
|
|
643
|
+
where: {
|
|
644
|
+
assignments: {
|
|
645
|
+
some: { id }
|
|
646
|
+
}
|
|
550
647
|
},
|
|
551
|
-
|
|
552
|
-
|
|
648
|
+
include: {
|
|
649
|
+
students: {
|
|
650
|
+
select: { id: true }
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
})
|
|
654
|
+
]);
|
|
553
655
|
|
|
554
656
|
if (!assignment) {
|
|
555
657
|
throw new TRPCError({
|
|
@@ -558,150 +660,190 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
558
660
|
});
|
|
559
661
|
}
|
|
560
662
|
|
|
561
|
-
//
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
663
|
+
// Prepare submission data outside transaction if needed
|
|
664
|
+
const submissionData = studentIds && studentIds.length > 0
|
|
665
|
+
? studentIds.map((studentId) => ({
|
|
666
|
+
student: { connect: { id: studentId } }
|
|
667
|
+
}))
|
|
668
|
+
: classData?.students.map((student) => ({
|
|
669
|
+
student: { connect: { id: student.id } }
|
|
670
|
+
}));
|
|
567
671
|
|
|
568
|
-
//
|
|
672
|
+
// Handle file deletion operations outside transaction
|
|
673
|
+
const fileDeletionPromises: Promise<void>[] = [];
|
|
569
674
|
if (input.removedAttachments && input.removedAttachments.length > 0) {
|
|
570
675
|
const filesToDelete = assignment.attachments.filter((file) =>
|
|
571
676
|
input.removedAttachments!.includes(file.id)
|
|
572
677
|
);
|
|
573
678
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
679
|
+
filesToDelete.forEach((file) => {
|
|
680
|
+
if (file.uploadStatus === 'COMPLETED') {
|
|
681
|
+
fileDeletionPromises.push(
|
|
682
|
+
deleteFile(file.path).catch(error => {
|
|
683
|
+
console.warn(`Failed to delete file ${file.path}:`, error);
|
|
684
|
+
})
|
|
685
|
+
);
|
|
686
|
+
if (file.thumbnail?.path) {
|
|
687
|
+
fileDeletionPromises.push(
|
|
688
|
+
deleteFile(file.thumbnail.path).catch(error => {
|
|
689
|
+
console.warn(`Failed to delete thumbnail ${file.thumbnail!.path}:`, error);
|
|
690
|
+
})
|
|
691
|
+
);
|
|
586
692
|
}
|
|
587
|
-
} catch (error) {
|
|
588
|
-
console.warn(`Failed to delete file ${file.path}:`, error);
|
|
589
693
|
}
|
|
590
|
-
})
|
|
694
|
+
});
|
|
591
695
|
}
|
|
592
696
|
|
|
593
|
-
//
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
...(file.thumbnailId && {
|
|
624
|
-
thumbnail: {
|
|
625
|
-
connect: { id: file.thumbnailId }
|
|
626
|
-
}
|
|
627
|
-
})
|
|
628
|
-
}))
|
|
629
|
-
}
|
|
630
|
-
}),
|
|
631
|
-
...(existingFileIds && existingFileIds.length > 0 && {
|
|
632
|
-
attachments: {
|
|
633
|
-
connect: existingFileIds.map(fileId => ({ id: fileId }))
|
|
634
|
-
}
|
|
635
|
-
}),
|
|
636
|
-
...(input.removedAttachments && input.removedAttachments.length > 0 && {
|
|
637
|
-
attachments: {
|
|
638
|
-
deleteMany: {
|
|
639
|
-
id: { in: input.removedAttachments }
|
|
697
|
+
// Execute file deletions in parallel
|
|
698
|
+
await Promise.all(fileDeletionPromises);
|
|
699
|
+
|
|
700
|
+
// Prepare file upload operations
|
|
701
|
+
let uploadedFiles: UploadedFile[] = [];
|
|
702
|
+
if (files && files.length > 0) {
|
|
703
|
+
uploadedFiles = await createDirectUploadFiles(files, ctx.user.id, undefined, input.id);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Minimal transaction for database update
|
|
707
|
+
const updatedAssignment = await prisma.$transaction(async (tx) => {
|
|
708
|
+
return tx.assignment.update({
|
|
709
|
+
where: { id },
|
|
710
|
+
data: {
|
|
711
|
+
...(title && { title }),
|
|
712
|
+
...(instructions && { instructions }),
|
|
713
|
+
...(dueDate && { dueDate: new Date(dueDate) }),
|
|
714
|
+
...(maxGrade && { maxGrade }),
|
|
715
|
+
...(graded !== undefined && { graded }),
|
|
716
|
+
...(weight && { weight }),
|
|
717
|
+
...(type && { type }),
|
|
718
|
+
...(inProgress !== undefined && { inProgress }),
|
|
719
|
+
...(acceptFiles !== undefined && { acceptFiles }),
|
|
720
|
+
...(acceptExtendedResponse !== undefined && { acceptExtendedResponse }),
|
|
721
|
+
...(acceptWorksheet !== undefined && { acceptWorksheet }),
|
|
722
|
+
...(gradeWithAI !== undefined && { gradeWithAI }),
|
|
723
|
+
...(studentIds && {
|
|
724
|
+
assignedTo: {
|
|
725
|
+
set: [], // Clear existing
|
|
726
|
+
connect: studentIds.map(id => ({ id }))
|
|
640
727
|
}
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
728
|
+
}),
|
|
729
|
+
...(submissionData && {
|
|
730
|
+
submissions: {
|
|
731
|
+
deleteMany: {}, // Clear existing submissions
|
|
732
|
+
create: submissionData
|
|
733
|
+
}
|
|
734
|
+
}),
|
|
735
|
+
...(aiPolicyLevel !== undefined && { aiPolicyLevel }),
|
|
736
|
+
...(sectionId !== undefined && {
|
|
737
|
+
section: sectionId ? {
|
|
738
|
+
connect: { id: sectionId }
|
|
739
|
+
} : {
|
|
740
|
+
disconnect: true
|
|
741
|
+
}
|
|
742
|
+
}),
|
|
743
|
+
...(worksheetIds && {
|
|
744
|
+
worksheets: {
|
|
745
|
+
set: [], // Clear existing
|
|
746
|
+
connect: worksheetIds.map(id => ({ id }))
|
|
747
|
+
}
|
|
748
|
+
}),
|
|
749
|
+
...(uploadedFiles.length > 0 && {
|
|
750
|
+
attachments: {
|
|
751
|
+
create: uploadedFiles.map(file => ({
|
|
752
|
+
name: file.name,
|
|
753
|
+
type: file.type,
|
|
754
|
+
size: file.size,
|
|
755
|
+
path: file.path,
|
|
756
|
+
...(file.thumbnailId && {
|
|
757
|
+
thumbnail: {
|
|
758
|
+
connect: { id: file.thumbnailId }
|
|
759
|
+
}
|
|
760
|
+
})
|
|
761
|
+
}))
|
|
762
|
+
}
|
|
763
|
+
}),
|
|
764
|
+
...(existingFileIds && existingFileIds.length > 0 && {
|
|
765
|
+
attachments: {
|
|
766
|
+
connect: existingFileIds.map(fileId => ({ id: fileId }))
|
|
767
|
+
}
|
|
768
|
+
}),
|
|
769
|
+
...(input.removedAttachments && input.removedAttachments.length > 0 && {
|
|
770
|
+
attachments: {
|
|
771
|
+
deleteMany: {
|
|
772
|
+
id: { in: input.removedAttachments }
|
|
660
773
|
}
|
|
661
774
|
}
|
|
662
|
-
}
|
|
775
|
+
}),
|
|
663
776
|
},
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
777
|
+
select: {
|
|
778
|
+
id: true,
|
|
779
|
+
title: true,
|
|
780
|
+
instructions: true,
|
|
781
|
+
dueDate: true,
|
|
782
|
+
maxGrade: true,
|
|
783
|
+
graded: true,
|
|
784
|
+
weight: true,
|
|
785
|
+
type: true,
|
|
786
|
+
createdAt: true,
|
|
787
|
+
markSchemeId: true,
|
|
788
|
+
submissions: {
|
|
789
|
+
select: {
|
|
790
|
+
student: {
|
|
791
|
+
select: {
|
|
792
|
+
id: true,
|
|
793
|
+
username: true
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
},
|
|
798
|
+
attachments: {
|
|
799
|
+
select: {
|
|
800
|
+
id: true,
|
|
801
|
+
name: true,
|
|
802
|
+
type: true,
|
|
803
|
+
thumbnail: true,
|
|
804
|
+
size: true,
|
|
805
|
+
path: true,
|
|
806
|
+
uploadedAt: true,
|
|
807
|
+
thumbnailId: true,
|
|
808
|
+
}
|
|
809
|
+
},
|
|
810
|
+
section: true,
|
|
811
|
+
teacher: true,
|
|
812
|
+
class: true
|
|
813
|
+
}
|
|
814
|
+
});
|
|
815
|
+
}, {
|
|
816
|
+
maxWait: 5000, // 5 seconds max wait time
|
|
817
|
+
timeout: 10000, // 10 seconds timeout
|
|
680
818
|
});
|
|
681
819
|
|
|
682
|
-
|
|
683
|
-
if (
|
|
684
|
-
|
|
685
|
-
where: { id:
|
|
820
|
+
// Handle rubric max grade calculation outside transaction
|
|
821
|
+
if (updatedAssignment.markSchemeId) {
|
|
822
|
+
prisma.markScheme.findUnique({
|
|
823
|
+
where: { id: updatedAssignment.markSchemeId },
|
|
686
824
|
select: {
|
|
687
825
|
structured: true,
|
|
688
826
|
}
|
|
689
|
-
})
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
827
|
+
}).then(rubric => {
|
|
828
|
+
if (rubric) {
|
|
829
|
+
const parsedRubric = JSON.parse(rubric.structured || "{}");
|
|
830
|
+
const computedMaxGrade = parsedRubric.criteria?.reduce((acc: number, criterion: any) => {
|
|
831
|
+
const maxPoints = Math.max(...criterion.levels.map((level: any) => level.points));
|
|
832
|
+
return acc + maxPoints;
|
|
833
|
+
}, 0) || 0;
|
|
834
|
+
|
|
835
|
+
return prisma.assignment.update({
|
|
836
|
+
where: { id },
|
|
837
|
+
data: {
|
|
838
|
+
maxGrade: computedMaxGrade,
|
|
839
|
+
}
|
|
840
|
+
});
|
|
700
841
|
}
|
|
842
|
+
}).catch(error => {
|
|
843
|
+
logger.error('Failed to update max grade from rubric:', error);
|
|
701
844
|
});
|
|
702
845
|
}
|
|
703
846
|
|
|
704
|
-
|
|
705
847
|
return updatedAssignment;
|
|
706
848
|
}),
|
|
707
849
|
|
|
@@ -838,6 +980,12 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
838
980
|
username: true
|
|
839
981
|
}
|
|
840
982
|
},
|
|
983
|
+
worksheets: {
|
|
984
|
+
select: {
|
|
985
|
+
id: true,
|
|
986
|
+
name: true,
|
|
987
|
+
}
|
|
988
|
+
},
|
|
841
989
|
class: {
|
|
842
990
|
select: {
|
|
843
991
|
id: true,
|
|
@@ -927,6 +1075,12 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
927
1075
|
structured: true,
|
|
928
1076
|
}
|
|
929
1077
|
},
|
|
1078
|
+
worksheets: {
|
|
1079
|
+
select: {
|
|
1080
|
+
id: true,
|
|
1081
|
+
name: true,
|
|
1082
|
+
}
|
|
1083
|
+
},
|
|
930
1084
|
gradingBoundary: {
|
|
931
1085
|
select: {
|
|
932
1086
|
id: true,
|
|
@@ -1039,6 +1193,12 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
1039
1193
|
id: true,
|
|
1040
1194
|
structured: true,
|
|
1041
1195
|
}
|
|
1196
|
+
},
|
|
1197
|
+
worksheets: {
|
|
1198
|
+
select: {
|
|
1199
|
+
id: true,
|
|
1200
|
+
name: true,
|
|
1201
|
+
}
|
|
1042
1202
|
}
|
|
1043
1203
|
},
|
|
1044
1204
|
},
|
|
@@ -1068,7 +1228,7 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
1068
1228
|
});
|
|
1069
1229
|
}
|
|
1070
1230
|
|
|
1071
|
-
const { submissionId, submit, newAttachments, existingFileIds, removedAttachments } = input;
|
|
1231
|
+
const { submissionId, submit, newAttachments, existingFileIds, removedAttachments, extendedResponse } = input;
|
|
1072
1232
|
|
|
1073
1233
|
const submission = await prisma.submission.findFirst({
|
|
1074
1234
|
where: {
|
|
@@ -1238,6 +1398,7 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
1238
1398
|
},
|
|
1239
1399
|
},
|
|
1240
1400
|
}),
|
|
1401
|
+
...(extendedResponse !== undefined && { extendedResponse }),
|
|
1241
1402
|
},
|
|
1242
1403
|
include: {
|
|
1243
1404
|
attachments: {
|