@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
|
@@ -75,6 +75,7 @@ const submissionSchema = z.object({
|
|
|
75
75
|
submissionId: z.string(),
|
|
76
76
|
submit: z.boolean().optional(),
|
|
77
77
|
newAttachments: z.array(directFileSchema).optional(), // Use direct file schema
|
|
78
|
+
extendedResponse: z.string().optional(),
|
|
78
79
|
existingFileIds: z.array(z.string()).optional(),
|
|
79
80
|
removedAttachments: z.array(z.string()).optional(),
|
|
80
81
|
});
|
|
@@ -150,15 +151,38 @@ async function getUnifiedList(tx, classId) {
|
|
|
150
151
|
return unified;
|
|
151
152
|
}
|
|
152
153
|
// Helper function to normalize unified list to 1..n
|
|
154
|
+
// Updated to batch updates to prevent timeouts with large lists
|
|
153
155
|
async function normalizeUnifiedList(tx, classId, orderedItems) {
|
|
154
|
-
|
|
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 };
|
|
155
162
|
if (item.type === 'section') {
|
|
156
|
-
|
|
163
|
+
sections.push(orderData);
|
|
157
164
|
}
|
|
158
165
|
else {
|
|
159
|
-
|
|
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
|
+
}));
|
|
160
181
|
}
|
|
161
|
-
}
|
|
182
|
+
};
|
|
183
|
+
// Process sections and assignments sequentially to avoid transaction overload
|
|
184
|
+
await processBatch(sections, 'section');
|
|
185
|
+
await processBatch(assignments, 'assignment');
|
|
162
186
|
}
|
|
163
187
|
export const assignmentRouter = createTRPCRouter({
|
|
164
188
|
// Reorder an assignment within the unified list (sections + assignments)
|
|
@@ -221,6 +245,9 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
221
245
|
// Normalize to 1..n
|
|
222
246
|
await normalizeUnifiedList(tx, classId, next);
|
|
223
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
|
|
224
251
|
});
|
|
225
252
|
return result;
|
|
226
253
|
}),
|
|
@@ -246,6 +273,9 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
246
273
|
const unified = await getUnifiedList(tx, current.classId);
|
|
247
274
|
await normalizeUnifiedList(tx, current.classId, unified.map(item => ({ id: item.id, type: item.type })));
|
|
248
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
|
|
249
279
|
});
|
|
250
280
|
return updated;
|
|
251
281
|
}),
|
|
@@ -272,6 +302,9 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
272
302
|
// No need to reorder since we're keeping the same position in the unified list
|
|
273
303
|
// If frontend wants to change position, they should call reorder after move
|
|
274
304
|
return tx.assignment.findUnique({ where: { id } });
|
|
305
|
+
}, {
|
|
306
|
+
maxWait: 5000, // 5 seconds max wait time
|
|
307
|
+
timeout: 10000, // 10 seconds timeout
|
|
275
308
|
});
|
|
276
309
|
return updated;
|
|
277
310
|
}),
|
|
@@ -285,40 +318,51 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
285
318
|
message: "User must be authenticated",
|
|
286
319
|
});
|
|
287
320
|
}
|
|
288
|
-
//
|
|
289
|
-
const classData = await
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
+
}
|
|
294
329
|
}
|
|
295
|
-
}
|
|
296
|
-
|
|
330
|
+
}),
|
|
331
|
+
markSchemeId ? prisma.markScheme.findUnique({
|
|
332
|
+
where: { id: markSchemeId },
|
|
333
|
+
select: {
|
|
334
|
+
structured: true,
|
|
335
|
+
}
|
|
336
|
+
}) : null
|
|
337
|
+
]);
|
|
297
338
|
if (!classData) {
|
|
298
339
|
throw new TRPCError({
|
|
299
340
|
code: "NOT_FOUND",
|
|
300
341
|
message: "Class not found",
|
|
301
342
|
});
|
|
302
343
|
}
|
|
344
|
+
// Calculate max grade outside transaction
|
|
303
345
|
let computedMaxGrade = maxGrade;
|
|
304
|
-
if (markSchemeId) {
|
|
305
|
-
const
|
|
306
|
-
where: { id: markSchemeId },
|
|
307
|
-
select: {
|
|
308
|
-
structured: true,
|
|
309
|
-
}
|
|
310
|
-
});
|
|
311
|
-
const parsedRubric = JSON.parse(rubric?.structured || "{}");
|
|
312
|
-
// Calculate max grade from rubric criteria levels
|
|
346
|
+
if (markSchemeId && rubricData) {
|
|
347
|
+
const parsedRubric = JSON.parse(rubricData.structured || "{}");
|
|
313
348
|
computedMaxGrade = parsedRubric.criteria.reduce((acc, criterion) => {
|
|
314
349
|
const maxPoints = Math.max(...criterion.levels.map((level) => level.points));
|
|
315
350
|
return acc + maxPoints;
|
|
316
351
|
}, 0);
|
|
317
352
|
}
|
|
318
|
-
console.log(
|
|
319
|
-
//
|
|
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
|
+
}));
|
|
320
362
|
const teacherId = ctx.user.id;
|
|
363
|
+
// Minimal transaction - only for critical operations
|
|
321
364
|
const assignment = await prisma.$transaction(async (tx) => {
|
|
365
|
+
// Create assignment with order 0 (will be at top)
|
|
322
366
|
const created = await tx.assignment.create({
|
|
323
367
|
data: {
|
|
324
368
|
title,
|
|
@@ -328,7 +372,7 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
328
372
|
graded,
|
|
329
373
|
weight,
|
|
330
374
|
type,
|
|
331
|
-
...(aiPolicyLevel
|
|
375
|
+
...(aiPolicyLevel && { aiPolicyLevel }),
|
|
332
376
|
acceptFiles,
|
|
333
377
|
acceptExtendedResponse,
|
|
334
378
|
acceptWorksheet,
|
|
@@ -343,7 +387,7 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
343
387
|
connect: studentIds.map(id => ({ id }))
|
|
344
388
|
}
|
|
345
389
|
}),
|
|
346
|
-
order:
|
|
390
|
+
order: 0, // Set to 0 to place at top
|
|
347
391
|
inProgress: inProgress || false,
|
|
348
392
|
class: {
|
|
349
393
|
connect: { id: classId }
|
|
@@ -364,11 +408,7 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
364
408
|
}
|
|
365
409
|
}),
|
|
366
410
|
submissions: {
|
|
367
|
-
create:
|
|
368
|
-
student: {
|
|
369
|
-
connect: { id: student.id }
|
|
370
|
-
}
|
|
371
|
-
}))
|
|
411
|
+
create: submissionData
|
|
372
412
|
},
|
|
373
413
|
teacher: {
|
|
374
414
|
connect: { id: teacherId }
|
|
@@ -410,60 +450,79 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
410
450
|
}
|
|
411
451
|
}
|
|
412
452
|
});
|
|
413
|
-
//
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
+
});
|
|
418
477
|
return created;
|
|
478
|
+
}, {
|
|
479
|
+
maxWait: 10000, // Increased to 10 seconds max wait time
|
|
480
|
+
timeout: 20000, // Increased to 20 seconds timeout for safety
|
|
419
481
|
});
|
|
420
|
-
//
|
|
421
|
-
|
|
422
|
-
//
|
|
423
|
-
let uploadedFiles = [];
|
|
482
|
+
// Handle file operations outside transaction
|
|
483
|
+
const fileOperations = [];
|
|
484
|
+
// Process direct upload files
|
|
424
485
|
if (files && files.length > 0) {
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
connect: { id: file.thumbnailId }
|
|
442
|
-
}
|
|
443
|
-
})
|
|
444
|
-
}))
|
|
445
|
-
}
|
|
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
|
+
});
|
|
446
502
|
}
|
|
447
|
-
});
|
|
503
|
+
}));
|
|
448
504
|
}
|
|
449
|
-
// Connect existing files
|
|
505
|
+
// Connect existing files
|
|
450
506
|
if (existingFileIds && existingFileIds.length > 0) {
|
|
451
|
-
|
|
507
|
+
fileOperations.push(prisma.assignment.update({
|
|
452
508
|
where: { id: assignment.id },
|
|
453
509
|
data: {
|
|
454
510
|
attachments: {
|
|
455
511
|
connect: existingFileIds.map(fileId => ({ id: fileId }))
|
|
456
512
|
}
|
|
457
513
|
}
|
|
458
|
-
});
|
|
514
|
+
}));
|
|
459
515
|
}
|
|
516
|
+
// Execute file operations in parallel
|
|
517
|
+
await Promise.all(fileOperations);
|
|
518
|
+
// Send notifications asynchronously (non-blocking)
|
|
460
519
|
sendNotifications(classData.students.map(student => student.id), {
|
|
461
520
|
title: `🔔 New assignment for ${classData.name}`,
|
|
462
521
|
content: `The assignment "${title}" has been created in ${classData.name}.\n
|
|
463
522
|
Due date: ${new Date(dueDate).toLocaleDateString()}.
|
|
464
523
|
[Link to assignment](/class/${classId}/assignments/${assignment.id})`
|
|
465
524
|
}).catch(error => {
|
|
466
|
-
logger.error('Failed to send assignment notifications:');
|
|
525
|
+
logger.error('Failed to send assignment notifications:', error);
|
|
467
526
|
});
|
|
468
527
|
return assignment;
|
|
469
528
|
}),
|
|
@@ -477,185 +536,225 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
477
536
|
message: "User must be authenticated",
|
|
478
537
|
});
|
|
479
538
|
}
|
|
480
|
-
//
|
|
481
|
-
const assignment = await
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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
|
+
}
|
|
498
559
|
}
|
|
560
|
+
},
|
|
561
|
+
},
|
|
562
|
+
class: {
|
|
563
|
+
select: {
|
|
564
|
+
id: true,
|
|
565
|
+
name: true
|
|
499
566
|
}
|
|
500
567
|
},
|
|
568
|
+
markScheme: true,
|
|
501
569
|
},
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
570
|
+
}),
|
|
571
|
+
prisma.class.findFirst({
|
|
572
|
+
where: {
|
|
573
|
+
assignments: {
|
|
574
|
+
some: { id }
|
|
506
575
|
}
|
|
507
576
|
},
|
|
508
|
-
|
|
509
|
-
|
|
577
|
+
include: {
|
|
578
|
+
students: {
|
|
579
|
+
select: { id: true }
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
})
|
|
583
|
+
]);
|
|
510
584
|
if (!assignment) {
|
|
511
585
|
throw new TRPCError({
|
|
512
586
|
code: "NOT_FOUND",
|
|
513
587
|
message: "Assignment not found",
|
|
514
588
|
});
|
|
515
589
|
}
|
|
516
|
-
//
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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 = [];
|
|
523
600
|
if (input.removedAttachments && input.removedAttachments.length > 0) {
|
|
524
601
|
const filesToDelete = assignment.attachments.filter((file) => input.removedAttachments.includes(file.id));
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
await deleteFile(file.thumbnail.path);
|
|
535
|
-
}
|
|
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
|
+
}));
|
|
536
611
|
}
|
|
537
612
|
}
|
|
538
|
-
|
|
539
|
-
console.warn(`Failed to delete file ${file.path}:`, error);
|
|
540
|
-
}
|
|
541
|
-
}));
|
|
613
|
+
});
|
|
542
614
|
}
|
|
543
|
-
//
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
}),
|
|
572
|
-
...(worksheetIds && {
|
|
573
|
-
worksheets: {
|
|
574
|
-
connect: worksheetIds.map(id => ({ id }))
|
|
575
|
-
}
|
|
576
|
-
}),
|
|
577
|
-
...(uploadedFiles.length > 0 && {
|
|
578
|
-
attachments: {
|
|
579
|
-
create: uploadedFiles.map(file => ({
|
|
580
|
-
name: file.name,
|
|
581
|
-
type: file.type,
|
|
582
|
-
size: file.size,
|
|
583
|
-
path: file.path,
|
|
584
|
-
...(file.thumbnailId && {
|
|
585
|
-
thumbnail: {
|
|
586
|
-
connect: { id: file.thumbnailId }
|
|
587
|
-
}
|
|
588
|
-
})
|
|
589
|
-
}))
|
|
590
|
-
}
|
|
591
|
-
}),
|
|
592
|
-
...(existingFileIds && existingFileIds.length > 0 && {
|
|
593
|
-
attachments: {
|
|
594
|
-
connect: existingFileIds.map(fileId => ({ id: fileId }))
|
|
595
|
-
}
|
|
596
|
-
}),
|
|
597
|
-
...(input.removedAttachments && input.removedAttachments.length > 0 && {
|
|
598
|
-
attachments: {
|
|
599
|
-
deleteMany: {
|
|
600
|
-
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 }))
|
|
601
643
|
}
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
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 }
|
|
621
689
|
}
|
|
622
690
|
}
|
|
623
|
-
}
|
|
624
|
-
},
|
|
625
|
-
attachments: {
|
|
626
|
-
select: {
|
|
627
|
-
id: true,
|
|
628
|
-
name: true,
|
|
629
|
-
type: true,
|
|
630
|
-
thumbnail: true,
|
|
631
|
-
size: true,
|
|
632
|
-
path: true,
|
|
633
|
-
uploadedAt: true,
|
|
634
|
-
thumbnailId: true,
|
|
635
|
-
}
|
|
691
|
+
}),
|
|
636
692
|
},
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
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
|
|
641
734
|
});
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
735
|
+
// Handle rubric max grade calculation outside transaction
|
|
736
|
+
if (updatedAssignment.markSchemeId) {
|
|
737
|
+
prisma.markScheme.findUnique({
|
|
738
|
+
where: { id: updatedAssignment.markSchemeId },
|
|
645
739
|
select: {
|
|
646
740
|
structured: true,
|
|
647
741
|
}
|
|
648
|
-
})
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
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
|
+
});
|
|
658
755
|
}
|
|
756
|
+
}).catch(error => {
|
|
757
|
+
logger.error('Failed to update max grade from rubric:', error);
|
|
659
758
|
});
|
|
660
759
|
}
|
|
661
760
|
return updatedAssignment;
|
|
@@ -783,6 +882,12 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
783
882
|
username: true
|
|
784
883
|
}
|
|
785
884
|
},
|
|
885
|
+
worksheets: {
|
|
886
|
+
select: {
|
|
887
|
+
id: true,
|
|
888
|
+
name: true,
|
|
889
|
+
}
|
|
890
|
+
},
|
|
786
891
|
class: {
|
|
787
892
|
select: {
|
|
788
893
|
id: true,
|
|
@@ -866,6 +971,12 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
866
971
|
structured: true,
|
|
867
972
|
}
|
|
868
973
|
},
|
|
974
|
+
worksheets: {
|
|
975
|
+
select: {
|
|
976
|
+
id: true,
|
|
977
|
+
name: true,
|
|
978
|
+
}
|
|
979
|
+
},
|
|
869
980
|
gradingBoundary: {
|
|
870
981
|
select: {
|
|
871
982
|
id: true,
|
|
@@ -973,6 +1084,12 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
973
1084
|
id: true,
|
|
974
1085
|
structured: true,
|
|
975
1086
|
}
|
|
1087
|
+
},
|
|
1088
|
+
worksheets: {
|
|
1089
|
+
select: {
|
|
1090
|
+
id: true,
|
|
1091
|
+
name: true,
|
|
1092
|
+
}
|
|
976
1093
|
}
|
|
977
1094
|
},
|
|
978
1095
|
},
|
|
@@ -998,7 +1115,7 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
998
1115
|
message: "User must be authenticated",
|
|
999
1116
|
});
|
|
1000
1117
|
}
|
|
1001
|
-
const { submissionId, submit, newAttachments, existingFileIds, removedAttachments } = input;
|
|
1118
|
+
const { submissionId, submit, newAttachments, existingFileIds, removedAttachments, extendedResponse } = input;
|
|
1002
1119
|
const submission = await prisma.submission.findFirst({
|
|
1003
1120
|
where: {
|
|
1004
1121
|
id: submissionId,
|
|
@@ -1157,6 +1274,7 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
1157
1274
|
},
|
|
1158
1275
|
},
|
|
1159
1276
|
}),
|
|
1277
|
+
...(extendedResponse !== undefined && { extendedResponse }),
|
|
1160
1278
|
},
|
|
1161
1279
|
include: {
|
|
1162
1280
|
attachments: {
|