@studious-lms/server 1.2.32 → 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 +116 -88
- package/dist/routers/_app.d.ts.map +1 -1
- package/dist/routers/assignment.d.ts +32 -18
- package/dist/routers/assignment.d.ts.map +1 -1
- package/dist/routers/assignment.js +337 -219
- 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 +443 -310
- package/src/routers/class.ts +18 -2
- package/src/routers/worksheet.ts +33 -7
|
@@ -82,6 +82,7 @@ const submissionSchema = z.object({
|
|
|
82
82
|
submissionId: z.string(),
|
|
83
83
|
submit: z.boolean().optional(),
|
|
84
84
|
newAttachments: z.array(directFileSchema).optional(), // Use direct file schema
|
|
85
|
+
extendedResponse: z.string().optional(),
|
|
85
86
|
existingFileIds: z.array(z.string()).optional(),
|
|
86
87
|
removedAttachments: z.array(z.string()).optional(),
|
|
87
88
|
});
|
|
@@ -169,16 +170,42 @@ async function getUnifiedList(tx: any, classId: string) {
|
|
|
169
170
|
}
|
|
170
171
|
|
|
171
172
|
// Helper function to normalize unified list to 1..n
|
|
173
|
+
// Updated to batch updates to prevent timeouts with large lists
|
|
172
174
|
async function normalizeUnifiedList(tx: any, classId: string, orderedItems: Array<{ id: string; type: 'section' | 'assignment' }>) {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
}
|
|
181
|
-
|
|
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');
|
|
182
209
|
}
|
|
183
210
|
|
|
184
211
|
export const assignmentRouter = createTRPCRouter({
|
|
@@ -249,6 +276,9 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
249
276
|
await normalizeUnifiedList(tx, classId, next);
|
|
250
277
|
|
|
251
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
|
|
252
282
|
});
|
|
253
283
|
|
|
254
284
|
return result;
|
|
@@ -280,6 +310,9 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
280
310
|
await normalizeUnifiedList(tx, current.classId, unified.map(item => ({ id: item.id, type: item.type })));
|
|
281
311
|
|
|
282
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
|
|
283
316
|
});
|
|
284
317
|
|
|
285
318
|
return updated;
|
|
@@ -313,6 +346,9 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
313
346
|
// If frontend wants to change position, they should call reorder after move
|
|
314
347
|
|
|
315
348
|
return tx.assignment.findUnique({ where: { id } });
|
|
349
|
+
}, {
|
|
350
|
+
maxWait: 5000, // 5 seconds max wait time
|
|
351
|
+
timeout: 10000, // 10 seconds timeout
|
|
316
352
|
});
|
|
317
353
|
|
|
318
354
|
return updated;
|
|
@@ -330,15 +366,23 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
330
366
|
});
|
|
331
367
|
}
|
|
332
368
|
|
|
333
|
-
//
|
|
334
|
-
const classData = await
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
+
}
|
|
339
377
|
}
|
|
340
|
-
}
|
|
341
|
-
|
|
378
|
+
}),
|
|
379
|
+
markSchemeId ? prisma.markScheme.findUnique({
|
|
380
|
+
where: { id: markSchemeId },
|
|
381
|
+
select: {
|
|
382
|
+
structured: true,
|
|
383
|
+
}
|
|
384
|
+
}) : null
|
|
385
|
+
]);
|
|
342
386
|
|
|
343
387
|
if (!classData) {
|
|
344
388
|
throw new TRPCError({
|
|
@@ -347,172 +391,198 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
347
391
|
});
|
|
348
392
|
}
|
|
349
393
|
|
|
394
|
+
// Calculate max grade outside transaction
|
|
350
395
|
let computedMaxGrade = maxGrade;
|
|
351
|
-
if (markSchemeId) {
|
|
352
|
-
const
|
|
353
|
-
where: { id: markSchemeId },
|
|
354
|
-
select: {
|
|
355
|
-
structured: true,
|
|
356
|
-
}
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
const parsedRubric = JSON.parse(rubric?.structured || "{}");
|
|
360
|
-
|
|
361
|
-
// Calculate max grade from rubric criteria levels
|
|
396
|
+
if (markSchemeId && rubricData) {
|
|
397
|
+
const parsedRubric = JSON.parse(rubricData.structured || "{}");
|
|
362
398
|
computedMaxGrade = parsedRubric.criteria.reduce((acc: number, criterion: any) => {
|
|
363
399
|
const maxPoints = Math.max(...criterion.levels.map((level: any) => level.points));
|
|
364
400
|
return acc + maxPoints;
|
|
365
401
|
}, 0);
|
|
366
402
|
}
|
|
367
|
-
console.log(markSchemeId, gradingBoundaryId);
|
|
368
403
|
|
|
369
|
-
|
|
370
|
-
|
|
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
|
|
371
418
|
const assignment = await prisma.$transaction(async (tx) => {
|
|
419
|
+
// Create assignment with order 0 (will be at top)
|
|
372
420
|
const created = await tx.assignment.create({
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
}
|
|
389
|
-
}),
|
|
390
|
-
gradeWithAI,
|
|
391
|
-
...(studentIds && {
|
|
392
|
-
assignedTo: {
|
|
393
|
-
connect: studentIds.map(id => ({ id }))
|
|
394
|
-
}
|
|
395
|
-
}),
|
|
396
|
-
order: 1,
|
|
397
|
-
inProgress: inProgress || false,
|
|
398
|
-
class: {
|
|
399
|
-
connect: { id: classId }
|
|
400
|
-
},
|
|
401
|
-
...(sectionId && {
|
|
402
|
-
section: {
|
|
403
|
-
connect: { id: sectionId }
|
|
404
|
-
}
|
|
405
|
-
}),
|
|
406
|
-
...(markSchemeId && {
|
|
407
|
-
markScheme: {
|
|
408
|
-
connect: { id: markSchemeId }
|
|
409
|
-
}
|
|
410
|
-
}),
|
|
411
|
-
...(gradingBoundaryId && {
|
|
412
|
-
gradingBoundary: {
|
|
413
|
-
connect: { id: gradingBoundaryId }
|
|
414
|
-
}
|
|
415
|
-
}),
|
|
416
|
-
submissions: {
|
|
417
|
-
create: classData.students.map((student) => ({
|
|
418
|
-
student: {
|
|
419
|
-
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 }))
|
|
420
436
|
}
|
|
421
|
-
})
|
|
422
|
-
|
|
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
|
+
},
|
|
423
467
|
teacher: {
|
|
424
468
|
connect: { id: teacherId }
|
|
425
469
|
}
|
|
426
|
-
},
|
|
427
|
-
select: {
|
|
428
|
-
id: true,
|
|
429
|
-
title: true,
|
|
430
|
-
instructions: true,
|
|
431
|
-
dueDate: true,
|
|
432
|
-
maxGrade: true,
|
|
433
|
-
graded: true,
|
|
434
|
-
weight: true,
|
|
435
|
-
type: true,
|
|
436
|
-
attachments: {
|
|
437
|
-
select: {
|
|
438
|
-
id: true,
|
|
439
|
-
name: true,
|
|
440
|
-
type: true,
|
|
441
|
-
}
|
|
442
|
-
},
|
|
443
|
-
section: {
|
|
444
|
-
select: {
|
|
445
|
-
id: true,
|
|
446
|
-
name: true
|
|
447
|
-
}
|
|
448
470
|
},
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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
|
+
}
|
|
453
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 },
|
|
454
514
|
},
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
id: true,
|
|
458
|
-
name: true
|
|
459
|
-
}
|
|
515
|
+
data: {
|
|
516
|
+
order: { increment: 1 }
|
|
460
517
|
}
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
await tx.section.updateMany({
|
|
521
|
+
where: {
|
|
522
|
+
classId: classId,
|
|
523
|
+
},
|
|
524
|
+
data: {
|
|
525
|
+
order: { increment: 1 }
|
|
461
526
|
}
|
|
462
527
|
});
|
|
463
528
|
|
|
464
|
-
//
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
529
|
+
// Update the created assignment to have order 1
|
|
530
|
+
await tx.assignment.update({
|
|
531
|
+
where: { id: created.id },
|
|
532
|
+
data: { order: 1 }
|
|
533
|
+
});
|
|
469
534
|
|
|
470
535
|
return created;
|
|
536
|
+
}, {
|
|
537
|
+
maxWait: 10000, // Increased to 10 seconds max wait time
|
|
538
|
+
timeout: 20000, // Increased to 20 seconds timeout for safety
|
|
471
539
|
});
|
|
472
540
|
|
|
473
|
-
//
|
|
474
|
-
|
|
475
|
-
// Actual file uploads should use getAssignmentUploadUrls endpoint
|
|
476
|
-
let uploadedFiles: UploadedFile[] = [];
|
|
477
|
-
if (files && files.length > 0) {
|
|
478
|
-
// Create direct upload files instead of processing base64
|
|
479
|
-
uploadedFiles = await createDirectUploadFiles(files, ctx.user.id, undefined, assignment.id);
|
|
480
|
-
}
|
|
541
|
+
// Handle file operations outside transaction
|
|
542
|
+
const fileOperations: Promise<any>[] = [];
|
|
481
543
|
|
|
482
|
-
//
|
|
483
|
-
if (
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
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
|
+
}
|
|
496
561
|
}
|
|
497
|
-
})
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
});
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
})
|
|
565
|
+
);
|
|
502
566
|
}
|
|
503
567
|
|
|
504
|
-
// Connect existing files
|
|
568
|
+
// Connect existing files
|
|
505
569
|
if (existingFileIds && existingFileIds.length > 0) {
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
570
|
+
fileOperations.push(
|
|
571
|
+
prisma.assignment.update({
|
|
572
|
+
where: { id: assignment.id },
|
|
573
|
+
data: {
|
|
574
|
+
attachments: {
|
|
575
|
+
connect: existingFileIds.map(fileId => ({ id: fileId }))
|
|
576
|
+
}
|
|
511
577
|
}
|
|
512
|
-
}
|
|
513
|
-
|
|
578
|
+
})
|
|
579
|
+
);
|
|
514
580
|
}
|
|
581
|
+
|
|
582
|
+
// Execute file operations in parallel
|
|
583
|
+
await Promise.all(fileOperations);
|
|
515
584
|
|
|
585
|
+
// Send notifications asynchronously (non-blocking)
|
|
516
586
|
sendNotifications(classData.students.map(student => student.id), {
|
|
517
587
|
title: `🔔 New assignment for ${classData.name}`,
|
|
518
588
|
content:
|
|
@@ -520,7 +590,7 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
520
590
|
Due date: ${new Date(dueDate).toLocaleDateString()}.
|
|
521
591
|
[Link to assignment](/class/${classId}/assignments/${assignment.id})`
|
|
522
592
|
}).catch(error => {
|
|
523
|
-
logger.error('Failed to send assignment notifications:');
|
|
593
|
+
logger.error('Failed to send assignment notifications:', error);
|
|
524
594
|
});
|
|
525
595
|
|
|
526
596
|
return assignment;
|
|
@@ -537,36 +607,51 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
537
607
|
});
|
|
538
608
|
}
|
|
539
609
|
|
|
540
|
-
//
|
|
541
|
-
const assignment = await
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
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
|
+
}
|
|
558
630
|
}
|
|
631
|
+
},
|
|
632
|
+
},
|
|
633
|
+
class: {
|
|
634
|
+
select: {
|
|
635
|
+
id: true,
|
|
636
|
+
name: true
|
|
559
637
|
}
|
|
560
638
|
},
|
|
639
|
+
markScheme: true,
|
|
561
640
|
},
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
641
|
+
}),
|
|
642
|
+
prisma.class.findFirst({
|
|
643
|
+
where: {
|
|
644
|
+
assignments: {
|
|
645
|
+
some: { id }
|
|
646
|
+
}
|
|
567
647
|
},
|
|
568
|
-
|
|
569
|
-
|
|
648
|
+
include: {
|
|
649
|
+
students: {
|
|
650
|
+
select: { id: true }
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
})
|
|
654
|
+
]);
|
|
570
655
|
|
|
571
656
|
if (!assignment) {
|
|
572
657
|
throw new TRPCError({
|
|
@@ -575,161 +660,190 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
575
660
|
});
|
|
576
661
|
}
|
|
577
662
|
|
|
578
|
-
//
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
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
|
+
}));
|
|
584
671
|
|
|
585
|
-
//
|
|
672
|
+
// Handle file deletion operations outside transaction
|
|
673
|
+
const fileDeletionPromises: Promise<void>[] = [];
|
|
586
674
|
if (input.removedAttachments && input.removedAttachments.length > 0) {
|
|
587
675
|
const filesToDelete = assignment.attachments.filter((file) =>
|
|
588
676
|
input.removedAttachments!.includes(file.id)
|
|
589
677
|
);
|
|
590
678
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
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
|
+
);
|
|
603
692
|
}
|
|
604
|
-
} catch (error) {
|
|
605
|
-
console.warn(`Failed to delete file ${file.path}:`, error);
|
|
606
693
|
}
|
|
607
|
-
})
|
|
694
|
+
});
|
|
608
695
|
}
|
|
609
696
|
|
|
610
|
-
//
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
worksheets: {
|
|
641
|
-
connect: worksheetIds.map(id => ({ id }))
|
|
642
|
-
}
|
|
643
|
-
}),
|
|
644
|
-
...(uploadedFiles.length > 0 && {
|
|
645
|
-
attachments: {
|
|
646
|
-
create: uploadedFiles.map(file => ({
|
|
647
|
-
name: file.name,
|
|
648
|
-
type: file.type,
|
|
649
|
-
size: file.size,
|
|
650
|
-
path: file.path,
|
|
651
|
-
...(file.thumbnailId && {
|
|
652
|
-
thumbnail: {
|
|
653
|
-
connect: { id: file.thumbnailId }
|
|
654
|
-
}
|
|
655
|
-
})
|
|
656
|
-
}))
|
|
657
|
-
}
|
|
658
|
-
}),
|
|
659
|
-
...(existingFileIds && existingFileIds.length > 0 && {
|
|
660
|
-
attachments: {
|
|
661
|
-
connect: existingFileIds.map(fileId => ({ id: fileId }))
|
|
662
|
-
}
|
|
663
|
-
}),
|
|
664
|
-
...(input.removedAttachments && input.removedAttachments.length > 0 && {
|
|
665
|
-
attachments: {
|
|
666
|
-
deleteMany: {
|
|
667
|
-
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 }))
|
|
668
727
|
}
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
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 }
|
|
688
773
|
}
|
|
689
774
|
}
|
|
690
|
-
}
|
|
691
|
-
},
|
|
692
|
-
attachments: {
|
|
693
|
-
select: {
|
|
694
|
-
id: true,
|
|
695
|
-
name: true,
|
|
696
|
-
type: true,
|
|
697
|
-
thumbnail: true,
|
|
698
|
-
size: true,
|
|
699
|
-
path: true,
|
|
700
|
-
uploadedAt: true,
|
|
701
|
-
thumbnailId: true,
|
|
702
|
-
}
|
|
775
|
+
}),
|
|
703
776
|
},
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
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
|
|
708
818
|
});
|
|
709
819
|
|
|
710
|
-
|
|
711
|
-
if (
|
|
712
|
-
|
|
713
|
-
where: { id:
|
|
820
|
+
// Handle rubric max grade calculation outside transaction
|
|
821
|
+
if (updatedAssignment.markSchemeId) {
|
|
822
|
+
prisma.markScheme.findUnique({
|
|
823
|
+
where: { id: updatedAssignment.markSchemeId },
|
|
714
824
|
select: {
|
|
715
825
|
structured: true,
|
|
716
826
|
}
|
|
717
|
-
})
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
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
|
+
});
|
|
728
841
|
}
|
|
842
|
+
}).catch(error => {
|
|
843
|
+
logger.error('Failed to update max grade from rubric:', error);
|
|
729
844
|
});
|
|
730
845
|
}
|
|
731
846
|
|
|
732
|
-
|
|
733
847
|
return updatedAssignment;
|
|
734
848
|
}),
|
|
735
849
|
|
|
@@ -866,6 +980,12 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
866
980
|
username: true
|
|
867
981
|
}
|
|
868
982
|
},
|
|
983
|
+
worksheets: {
|
|
984
|
+
select: {
|
|
985
|
+
id: true,
|
|
986
|
+
name: true,
|
|
987
|
+
}
|
|
988
|
+
},
|
|
869
989
|
class: {
|
|
870
990
|
select: {
|
|
871
991
|
id: true,
|
|
@@ -955,6 +1075,12 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
955
1075
|
structured: true,
|
|
956
1076
|
}
|
|
957
1077
|
},
|
|
1078
|
+
worksheets: {
|
|
1079
|
+
select: {
|
|
1080
|
+
id: true,
|
|
1081
|
+
name: true,
|
|
1082
|
+
}
|
|
1083
|
+
},
|
|
958
1084
|
gradingBoundary: {
|
|
959
1085
|
select: {
|
|
960
1086
|
id: true,
|
|
@@ -1067,6 +1193,12 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
1067
1193
|
id: true,
|
|
1068
1194
|
structured: true,
|
|
1069
1195
|
}
|
|
1196
|
+
},
|
|
1197
|
+
worksheets: {
|
|
1198
|
+
select: {
|
|
1199
|
+
id: true,
|
|
1200
|
+
name: true,
|
|
1201
|
+
}
|
|
1070
1202
|
}
|
|
1071
1203
|
},
|
|
1072
1204
|
},
|
|
@@ -1096,7 +1228,7 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
1096
1228
|
});
|
|
1097
1229
|
}
|
|
1098
1230
|
|
|
1099
|
-
const { submissionId, submit, newAttachments, existingFileIds, removedAttachments } = input;
|
|
1231
|
+
const { submissionId, submit, newAttachments, existingFileIds, removedAttachments, extendedResponse } = input;
|
|
1100
1232
|
|
|
1101
1233
|
const submission = await prisma.submission.findFirst({
|
|
1102
1234
|
where: {
|
|
@@ -1266,6 +1398,7 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
1266
1398
|
},
|
|
1267
1399
|
},
|
|
1268
1400
|
}),
|
|
1401
|
+
...(extendedResponse !== undefined && { extendedResponse }),
|
|
1269
1402
|
},
|
|
1270
1403
|
include: {
|
|
1271
1404
|
attachments: {
|