@studious-lms/server 1.2.53 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.coderabbit.yaml +9 -0
- package/.env.example +9 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +102 -8
- package/dist/index.js.map +1 -1
- package/dist/lib/config/env.d.ts +21 -0
- package/dist/lib/config/env.d.ts.map +1 -1
- package/dist/lib/config/env.js +8 -2
- package/dist/lib/config/env.js.map +1 -1
- package/dist/lib/fileUpload.d.ts.map +1 -1
- package/dist/lib/fileUpload.js +2 -2
- package/dist/lib/fileUpload.js.map +1 -1
- package/dist/lib/googleCloudStorage.d.ts +6 -0
- package/dist/lib/googleCloudStorage.d.ts.map +1 -1
- package/dist/lib/googleCloudStorage.js +19 -2
- package/dist/lib/googleCloudStorage.js.map +1 -1
- package/dist/lib/pusher.d.ts +4 -1
- package/dist/lib/pusher.d.ts.map +1 -1
- package/dist/lib/pusher.js +6 -3
- package/dist/lib/pusher.js.map +1 -1
- package/dist/lib/redis.d.ts +5 -0
- package/dist/lib/redis.d.ts.map +1 -0
- package/dist/lib/redis.js +53 -0
- package/dist/lib/redis.js.map +1 -0
- package/dist/lib/thumbnailGenerator.d.ts +0 -21
- package/dist/lib/thumbnailGenerator.d.ts.map +1 -1
- package/dist/lib/thumbnailGenerator.js +157 -160
- package/dist/lib/thumbnailGenerator.js.map +1 -1
- package/dist/middleware/auth.d.ts.map +1 -1
- package/dist/middleware/auth.js +33 -95
- package/dist/middleware/auth.js.map +1 -1
- package/dist/models/agenda.d.ts +97 -0
- package/dist/models/agenda.d.ts.map +1 -0
- package/dist/models/agenda.js +40 -0
- package/dist/models/agenda.js.map +1 -0
- package/dist/models/announcement.d.ts +223 -0
- package/dist/models/announcement.d.ts.map +1 -0
- package/dist/models/announcement.js +120 -0
- package/dist/models/announcement.js.map +1 -0
- package/dist/models/assignment.d.ts +1292 -0
- package/dist/models/assignment.d.ts.map +1 -0
- package/dist/models/assignment.js +309 -0
- package/dist/models/assignment.js.map +1 -0
- package/dist/models/attendance.d.ts +180 -0
- package/dist/models/attendance.d.ts.map +1 -0
- package/dist/models/attendance.js +188 -0
- package/dist/models/attendance.js.map +1 -0
- package/dist/models/auth.d.ts +153 -0
- package/dist/models/auth.d.ts.map +1 -0
- package/dist/models/auth.js +217 -0
- package/dist/models/auth.js.map +1 -0
- package/dist/models/class.d.ts +461 -0
- package/dist/models/class.d.ts.map +1 -0
- package/dist/models/class.js +645 -0
- package/dist/models/class.js.map +1 -0
- package/dist/models/comment.d.ts +171 -0
- package/dist/models/comment.d.ts.map +1 -0
- package/dist/models/comment.js +138 -0
- package/dist/models/comment.js.map +1 -0
- package/dist/models/conversation.d.ts +164 -0
- package/dist/models/conversation.d.ts.map +1 -0
- package/dist/models/conversation.js +175 -0
- package/dist/models/conversation.js.map +1 -0
- package/dist/models/event.d.ts +295 -0
- package/dist/models/event.d.ts.map +1 -0
- package/dist/models/event.js +145 -0
- package/dist/models/event.js.map +1 -0
- package/dist/models/file.d.ts +536 -0
- package/dist/models/file.d.ts.map +1 -0
- package/dist/models/file.js +126 -0
- package/dist/models/file.js.map +1 -0
- package/dist/models/folder.d.ts +295 -0
- package/dist/models/folder.d.ts.map +1 -0
- package/dist/models/folder.js +202 -0
- package/dist/models/folder.js.map +1 -0
- package/dist/models/labChat.d.ts +243 -0
- package/dist/models/labChat.d.ts.map +1 -0
- package/dist/models/labChat.js +204 -0
- package/dist/models/labChat.js.map +1 -0
- package/dist/models/marketing.d.ts +72 -0
- package/dist/models/marketing.d.ts.map +1 -0
- package/dist/models/marketing.js +26 -0
- package/dist/models/marketing.js.map +1 -0
- package/dist/models/message.d.ts +100 -0
- package/dist/models/message.d.ts.map +1 -0
- package/dist/models/message.js +131 -0
- package/dist/models/message.js.map +1 -0
- package/dist/models/newtonChat.d.ts +72 -0
- package/dist/models/newtonChat.d.ts.map +1 -0
- package/dist/models/newtonChat.js +61 -0
- package/dist/models/newtonChat.js.map +1 -0
- package/dist/models/notification.d.ts +65 -0
- package/dist/models/notification.d.ts.map +1 -0
- package/dist/models/notification.js +46 -0
- package/dist/models/notification.js.map +1 -0
- package/dist/models/section.d.ts +102 -0
- package/dist/models/section.d.ts.map +1 -0
- package/dist/models/section.js +83 -0
- package/dist/models/section.js.map +1 -0
- package/dist/models/user.d.ts +39 -0
- package/dist/models/user.d.ts.map +1 -0
- package/dist/models/user.js +38 -0
- package/dist/models/user.js.map +1 -0
- package/dist/models/worksheet.d.ts +460 -0
- package/dist/models/worksheet.d.ts.map +1 -0
- package/dist/models/worksheet.js +200 -0
- package/dist/models/worksheet.js.map +1 -0
- package/dist/pipelines/aiLabChat.d.ts +76 -0
- package/dist/pipelines/aiLabChat.d.ts.map +1 -0
- package/dist/pipelines/aiLabChat.js +599 -0
- package/dist/pipelines/aiLabChat.js.map +1 -0
- package/dist/pipelines/aiNewtonChat.d.ts +30 -0
- package/dist/pipelines/aiNewtonChat.d.ts.map +1 -0
- package/dist/pipelines/aiNewtonChat.js +289 -0
- package/dist/pipelines/aiNewtonChat.js.map +1 -0
- package/dist/pipelines/gradeWorksheet.d.ts +30 -0
- package/dist/pipelines/gradeWorksheet.d.ts.map +1 -0
- package/dist/pipelines/gradeWorksheet.js +252 -0
- package/dist/pipelines/gradeWorksheet.js.map +1 -0
- package/dist/routers/_app.d.ts +1523 -1315
- package/dist/routers/_app.d.ts.map +1 -1
- package/dist/routers/agenda.d.ts +22 -22
- package/dist/routers/agenda.d.ts.map +1 -1
- package/dist/routers/agenda.js +4 -65
- package/dist/routers/agenda.js.map +1 -1
- package/dist/routers/announcement.d.ts +16 -16
- package/dist/routers/announcement.d.ts.map +1 -1
- package/dist/routers/announcement.js +37 -446
- package/dist/routers/announcement.js.map +1 -1
- package/dist/routers/assignment.d.ts +300 -378
- package/dist/routers/assignment.d.ts.map +1 -1
- package/dist/routers/assignment.js +78 -1868
- package/dist/routers/assignment.js.map +1 -1
- package/dist/routers/attendance.d.ts +19 -9
- package/dist/routers/attendance.d.ts.map +1 -1
- package/dist/routers/attendance.js +7 -264
- package/dist/routers/attendance.js.map +1 -1
- package/dist/routers/auth.d.ts +2 -2
- package/dist/routers/auth.d.ts.map +1 -1
- package/dist/routers/auth.js +29 -354
- package/dist/routers/auth.js.map +1 -1
- package/dist/routers/class.d.ts +160 -68
- package/dist/routers/class.d.ts.map +1 -1
- package/dist/routers/class.js +82 -1052
- package/dist/routers/class.js.map +1 -1
- package/dist/routers/comment.d.ts +6 -42
- package/dist/routers/comment.d.ts.map +1 -1
- package/dist/routers/comment.js +24 -244
- package/dist/routers/comment.js.map +1 -1
- package/dist/routers/conversation.d.ts +45 -7
- package/dist/routers/conversation.d.ts.map +1 -1
- package/dist/routers/conversation.js +19 -327
- package/dist/routers/conversation.js.map +1 -1
- package/dist/routers/event.d.ts +36 -36
- package/dist/routers/event.d.ts.map +1 -1
- package/dist/routers/event.js +13 -433
- package/dist/routers/event.js.map +1 -1
- package/dist/routers/file.d.ts +2 -2
- package/dist/routers/file.d.ts.map +1 -1
- package/dist/routers/file.js +9 -323
- package/dist/routers/file.js.map +1 -1
- package/dist/routers/folder.d.ts +21 -14
- package/dist/routers/folder.d.ts.map +1 -1
- package/dist/routers/folder.js +34 -745
- package/dist/routers/folder.js.map +1 -1
- package/dist/routers/labChat.d.ts +21 -11
- package/dist/routers/labChat.d.ts.map +1 -1
- package/dist/routers/labChat.js +22 -570
- package/dist/routers/labChat.js.map +1 -1
- package/dist/routers/marketing.d.ts +1 -1
- package/dist/routers/marketing.d.ts.map +1 -1
- package/dist/routers/marketing.js +7 -56
- package/dist/routers/marketing.js.map +1 -1
- package/dist/routers/message.d.ts +13 -2
- package/dist/routers/message.d.ts.map +1 -1
- package/dist/routers/message.js +32 -520
- package/dist/routers/message.js.map +1 -1
- package/dist/routers/newtonChat.d.ts +1 -1
- package/dist/routers/newtonChat.d.ts.map +1 -1
- package/dist/routers/newtonChat.js +7 -246
- package/dist/routers/newtonChat.js.map +1 -1
- package/dist/routers/notifications.d.ts +4 -4
- package/dist/routers/notifications.d.ts.map +1 -1
- package/dist/routers/notifications.js +18 -83
- package/dist/routers/notifications.js.map +1 -1
- package/dist/routers/section.d.ts +4 -4
- package/dist/routers/section.d.ts.map +1 -1
- package/dist/routers/section.js +14 -286
- package/dist/routers/section.js.map +1 -1
- package/dist/routers/user.d.ts +1 -1
- package/dist/routers/user.d.ts.map +1 -1
- package/dist/routers/user.js +32 -207
- package/dist/routers/user.js.map +1 -1
- package/dist/routers/worksheet.d.ts +68 -55
- package/dist/routers/worksheet.d.ts.map +1 -1
- package/dist/routers/worksheet.js +79 -394
- package/dist/routers/worksheet.js.map +1 -1
- package/dist/seedDatabase.d.ts +1 -1
- package/dist/server/pipelines/gradeWorksheet.d.ts +6 -6
- package/dist/server/pipelines/gradeWorksheet.d.ts.map +1 -1
- package/dist/server/pipelines/gradeWorksheet.js +12 -5
- package/dist/server/pipelines/gradeWorksheet.js.map +1 -1
- package/dist/services/agenda.d.ts +100 -0
- package/dist/services/agenda.d.ts.map +1 -0
- package/dist/services/agenda.js +21 -0
- package/dist/services/agenda.js.map +1 -0
- package/dist/services/announcement.d.ts +135 -0
- package/dist/services/announcement.d.ts.map +1 -0
- package/dist/services/announcement.js +223 -0
- package/dist/services/announcement.js.map +1 -0
- package/dist/services/assignment.d.ts +1462 -0
- package/dist/services/assignment.d.ts.map +1 -0
- package/dist/services/assignment.js +898 -0
- package/dist/services/assignment.js.map +1 -0
- package/dist/services/attendance.d.ts +93 -0
- package/dist/services/attendance.d.ts.map +1 -0
- package/dist/services/attendance.js +61 -0
- package/dist/services/attendance.js.map +1 -0
- package/dist/services/auth.d.ts +68 -0
- package/dist/services/auth.d.ts.map +1 -0
- package/dist/services/auth.js +218 -0
- package/dist/services/auth.js.map +1 -0
- package/dist/services/class.d.ts +643 -0
- package/dist/services/class.d.ts.map +1 -0
- package/dist/services/class.js +486 -0
- package/dist/services/class.js.map +1 -0
- package/dist/services/comment.d.ts +100 -0
- package/dist/services/comment.d.ts.map +1 -0
- package/dist/services/comment.js +83 -0
- package/dist/services/comment.js.map +1 -0
- package/dist/services/conversation.d.ts +159 -0
- package/dist/services/conversation.d.ts.map +1 -0
- package/dist/services/conversation.js +138 -0
- package/dist/services/conversation.js.map +1 -0
- package/dist/services/event.d.ts +216 -0
- package/dist/services/event.d.ts.map +1 -0
- package/dist/services/event.js +168 -0
- package/dist/services/event.js.map +1 -0
- package/dist/services/file.d.ts +74 -0
- package/dist/services/file.d.ts.map +1 -0
- package/dist/services/file.js +133 -0
- package/dist/services/file.js.map +1 -0
- package/dist/services/folder.d.ts +239 -0
- package/dist/services/folder.d.ts.map +1 -0
- package/dist/services/folder.js +248 -0
- package/dist/services/folder.js.map +1 -0
- package/dist/services/labChat.d.ts +169 -0
- package/dist/services/labChat.d.ts.map +1 -0
- package/dist/services/labChat.js +381 -0
- package/dist/services/labChat.js.map +1 -0
- package/dist/services/marketing.d.ts +50 -0
- package/dist/services/marketing.d.ts.map +1 -0
- package/dist/services/marketing.js +32 -0
- package/dist/services/marketing.js.map +1 -0
- package/dist/services/message.d.ts +103 -0
- package/dist/services/message.d.ts.map +1 -0
- package/dist/services/message.js +422 -0
- package/dist/services/message.js.map +1 -0
- package/dist/services/newtonChat.d.ts +22 -0
- package/dist/services/newtonChat.d.ts.map +1 -0
- package/dist/services/newtonChat.js +174 -0
- package/dist/services/newtonChat.js.map +1 -0
- package/dist/services/notification.d.ts +65 -0
- package/dist/services/notification.d.ts.map +1 -0
- package/dist/services/notification.js +33 -0
- package/dist/services/notification.js.map +1 -0
- package/dist/services/section.d.ts +53 -0
- package/dist/services/section.d.ts.map +1 -0
- package/dist/services/section.js +199 -0
- package/dist/services/section.js.map +1 -0
- package/dist/services/user.d.ts +48 -0
- package/dist/services/user.d.ts.map +1 -0
- package/dist/services/user.js +141 -0
- package/dist/services/user.js.map +1 -0
- package/dist/services/worksheet.d.ts +239 -0
- package/dist/services/worksheet.d.ts.map +1 -0
- package/dist/services/worksheet.js +235 -0
- package/dist/services/worksheet.js.map +1 -0
- package/dist/utils/aiUser.d.ts +1 -3
- package/dist/utils/aiUser.d.ts.map +1 -1
- package/dist/utils/aiUser.js +6 -5
- package/dist/utils/aiUser.js.map +1 -1
- package/dist/utils/email.d.ts +3 -0
- package/dist/utils/email.d.ts.map +1 -1
- package/dist/utils/email.js +7 -4
- package/dist/utils/email.js.map +1 -1
- package/dist/utils/generateInviteCode.d.ts +1 -2
- package/dist/utils/generateInviteCode.d.ts.map +1 -1
- package/dist/utils/generateInviteCode.js +3 -4
- package/dist/utils/generateInviteCode.js.map +1 -1
- package/dist/utils/inference.d.ts +3 -0
- package/dist/utils/inference.d.ts.map +1 -1
- package/dist/utils/inference.js +7 -4
- package/dist/utils/inference.js.map +1 -1
- package/dist/utils/logger.d.ts +3 -0
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +5 -2
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/prismaErrorHandler.d.ts.map +1 -1
- package/dist/utils/prismaErrorHandler.js +5 -2
- package/dist/utils/prismaErrorHandler.js.map +1 -1
- package/dist/utils/prismaWrapper.d.ts +1 -0
- package/dist/utils/prismaWrapper.d.ts.map +1 -1
- package/dist/utils/prismaWrapper.js +6 -2
- package/dist/utils/prismaWrapper.js.map +1 -1
- package/docker-compose.yml +5 -0
- package/package.json +4 -3
- package/prisma/schema.prisma +1 -1
- package/src/index.ts +119 -12
- package/src/lib/config/env.ts +6 -0
- package/src/lib/fileUpload.ts +0 -1
- package/src/lib/googleCloudStorage.ts +17 -0
- package/src/lib/pusher.ts +5 -1
- package/src/lib/redis.ts +56 -0
- package/src/lib/thumbnailGenerator.ts +170 -168
- package/src/middleware/auth.ts +80 -137
- package/src/models/agenda.ts +46 -0
- package/src/models/announcement.ts +134 -0
- package/src/models/assignment.ts +322 -0
- package/src/models/attendance.ts +208 -0
- package/src/models/auth.ts +247 -0
- package/src/models/class.ts +703 -0
- package/src/models/comment.ts +152 -0
- package/src/models/conversation.ts +200 -0
- package/src/models/event.ts +177 -0
- package/src/models/file.ts +129 -0
- package/src/models/folder.ts +225 -0
- package/src/models/labChat.ts +213 -0
- package/src/models/marketing.ts +45 -0
- package/src/models/message.ts +153 -0
- package/src/models/newtonChat.ts +70 -0
- package/src/models/notification.ts +54 -0
- package/src/models/section.ts +98 -0
- package/src/models/user.ts +47 -0
- package/src/models/worksheet.ts +294 -0
- package/src/pipelines/aiLabChat.ts +684 -0
- package/src/{server/pipelines → pipelines}/aiNewtonChat.ts +9 -5
- package/src/{server/pipelines → pipelines}/gradeWorksheet.ts +25 -14
- package/src/routers/agenda.ts +3 -66
- package/src/routers/announcement.ts +54 -495
- package/src/routers/assignment.ts +126 -2018
- package/src/routers/attendance.ts +15 -276
- package/src/routers/auth.ts +79 -442
- package/src/routers/class.ts +263 -1187
- package/src/routers/comment.ts +61 -288
- package/src/routers/conversation.ts +51 -360
- package/src/routers/event.ts +50 -481
- package/src/routers/file.ts +45 -368
- package/src/routers/folder.ts +107 -836
- package/src/routers/labChat.ts +35 -604
- package/src/routers/marketing.ts +35 -77
- package/src/routers/message.ts +54 -567
- package/src/routers/newtonChat.ts +17 -278
- package/src/routers/notifications.ts +32 -82
- package/src/routers/section.ts +46 -330
- package/src/routers/user.ts +49 -227
- package/src/routers/worksheet.ts +215 -503
- package/src/services/agenda.ts +21 -0
- package/src/services/announcement.ts +290 -0
- package/src/services/assignment.ts +1198 -0
- package/src/services/attendance.ts +85 -0
- package/src/services/auth.ts +277 -0
- package/src/services/class.ts +629 -0
- package/src/services/comment.ts +106 -0
- package/src/services/conversation.ts +213 -0
- package/src/services/event.ts +231 -0
- package/src/services/file.ts +167 -0
- package/src/services/folder.ts +316 -0
- package/src/services/labChat.ts +458 -0
- package/src/services/marketing.ts +57 -0
- package/src/services/message.ts +554 -0
- package/src/services/newtonChat.ts +222 -0
- package/src/services/notification.ts +50 -0
- package/src/services/section.ts +283 -0
- package/src/services/user.ts +172 -0
- package/src/services/worksheet.ts +358 -0
- package/src/utils/aiUser.ts +4 -3
- package/src/utils/email.ts +5 -3
- package/src/utils/generateInviteCode.ts +1 -3
- package/src/utils/inference.ts +5 -2
- package/src/utils/logger.ts +3 -1
- package/src/utils/prismaErrorHandler.ts +3 -0
- package/src/utils/prismaWrapper.ts +4 -0
- package/tests/globalSetup.ts +62 -0
- package/tests/helpers.ts +22 -0
- package/tests/middleware/security.test.ts +42 -0
- package/tests/routers/agenda.test.ts +138 -0
- package/tests/routers/announcement.test.ts +490 -0
- package/tests/routers/assignment.test.ts +837 -0
- package/tests/{attendance.test.ts → routers/attendance.test.ts} +6 -14
- package/tests/routers/auth.test.ts +171 -0
- package/tests/{class.test.ts → routers/class.test.ts} +131 -85
- package/tests/routers/comment.test.ts +126 -0
- package/tests/routers/conversation.test.ts +145 -0
- package/tests/{event.test.ts → routers/event.test.ts} +93 -32
- package/tests/routers/folder.test.ts +178 -0
- package/tests/routers/labChat.test.ts +115 -0
- package/tests/routers/marketing.test.ts +59 -0
- package/tests/routers/message.test.ts +123 -0
- package/tests/routers/notification.test.ts +69 -0
- package/tests/{section.test.ts → routers/section.test.ts} +5 -13
- package/tests/server/rateLimit.test.ts +73 -0
- package/tests/setup.ts +18 -92
- package/tests/user.test.ts +9 -31
- package/tests/utils/aiUser.test.ts +22 -0
- package/tests/utils/generateInviteCode.test.ts +24 -0
- package/tests/utils/logger.test.ts +74 -0
- package/tests/utils/prismaErrorHandler.test.ts +101 -0
- package/tests/utils/prismaWrapper.test.ts +82 -0
- package/tests/worksheet.test.ts +181 -0
- package/vitest.config.ts +6 -3
- package/vitest.unit.config.ts +21 -0
- package/TODO.md +0 -2
- package/coverage/base.css +0 -224
- package/coverage/block-navigation.js +0 -87
- package/coverage/clover.xml +0 -12110
- package/coverage/coverage-final.json +0 -44
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +0 -221
- package/coverage/prettify.css +0 -1
- package/coverage/prettify.js +0 -2
- package/coverage/server/index.html +0 -116
- package/coverage/server/src/exportType.ts.html +0 -109
- package/coverage/server/src/index.html +0 -161
- package/coverage/server/src/index.ts.html +0 -1702
- package/coverage/server/src/instrument.ts.html +0 -130
- package/coverage/server/src/lib/config/env.ts.html +0 -448
- package/coverage/server/src/lib/config/index.html +0 -116
- package/coverage/server/src/lib/fileUpload.ts.html +0 -1138
- package/coverage/server/src/lib/googleCloudStorage.ts.html +0 -334
- package/coverage/server/src/lib/index.html +0 -206
- package/coverage/server/src/lib/jsonConversion.ts.html +0 -2323
- package/coverage/server/src/lib/jsonStyles.ts.html +0 -193
- package/coverage/server/src/lib/notificationHandler.ts.html +0 -193
- package/coverage/server/src/lib/pusher.ts.html +0 -121
- package/coverage/server/src/lib/thumbnailGenerator.ts.html +0 -592
- package/coverage/server/src/middleware/auth.ts.html +0 -646
- package/coverage/server/src/middleware/index.html +0 -146
- package/coverage/server/src/middleware/logging.ts.html +0 -244
- package/coverage/server/src/middleware/security.ts.html +0 -271
- package/coverage/server/src/routers/_app.ts.html +0 -232
- package/coverage/server/src/routers/agenda.ts.html +0 -319
- package/coverage/server/src/routers/announcement.ts.html +0 -3481
- package/coverage/server/src/routers/assignment.ts.html +0 -7633
- package/coverage/server/src/routers/attendance.ts.html +0 -1030
- package/coverage/server/src/routers/auth.ts.html +0 -1081
- package/coverage/server/src/routers/class.ts.html +0 -3535
- package/coverage/server/src/routers/comment.ts.html +0 -991
- package/coverage/server/src/routers/conversation.ts.html +0 -982
- package/coverage/server/src/routers/event.ts.html +0 -1609
- package/coverage/server/src/routers/file.ts.html +0 -1144
- package/coverage/server/src/routers/folder.ts.html +0 -2797
- package/coverage/server/src/routers/index.html +0 -386
- package/coverage/server/src/routers/labChat.ts.html +0 -3073
- package/coverage/server/src/routers/marketing.ts.html +0 -340
- package/coverage/server/src/routers/message.ts.html +0 -1912
- package/coverage/server/src/routers/notifications.ts.html +0 -364
- package/coverage/server/src/routers/section.ts.html +0 -1120
- package/coverage/server/src/routers/user.ts.html +0 -862
- package/coverage/server/src/routers/worksheet.ts.html +0 -1729
- package/coverage/server/src/trpc.ts.html +0 -397
- package/coverage/server/src/types/index.html +0 -116
- package/coverage/server/src/types/trpc.ts.html +0 -127
- package/coverage/server/src/utils/aiUser.ts.html +0 -280
- package/coverage/server/src/utils/email.ts.html +0 -121
- package/coverage/server/src/utils/generateInviteCode.ts.html +0 -106
- package/coverage/server/src/utils/index.html +0 -206
- package/coverage/server/src/utils/inference.ts.html +0 -709
- package/coverage/server/src/utils/logger.ts.html +0 -664
- package/coverage/server/src/utils/prismaErrorHandler.ts.html +0 -907
- package/coverage/server/src/utils/prismaWrapper.ts.html +0 -355
- package/coverage/server/vitest.config.ts.html +0 -196
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +0 -210
- package/src/lib/notificationHandler.ts +0 -36
- package/src/server/pipelines/aiLabChat.ts +0 -507
- package/tests/announcement.test.ts +0 -164
- package/tests/assignment.test.ts +0 -296
- package/tests/auth.test.ts +0 -48
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
|
|
2
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
2
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="6ca6683e-2a89-5dab-a4c5-bf0292654572")}catch(e){}}();
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { createTRPCRouter, protectedProcedure, protectedClassMemberProcedure, protectedTeacherProcedure } from "../trpc.js";
|
|
5
5
|
import { TRPCError } from "@trpc/server";
|
|
6
6
|
import { prisma } from "../lib/prisma.js";
|
|
7
7
|
import { createDirectUploadFiles, confirmDirectUpload, updateUploadProgress } from "../lib/fileUpload.js";
|
|
8
|
-
import {
|
|
9
|
-
import { sendNotifications } from "../lib/notificationHandler.js";
|
|
10
|
-
import { logger } from "../utils/logger.js";
|
|
11
|
-
import { gradeWorksheetPipeline } from "../server/pipelines/gradeWorksheet.js";
|
|
8
|
+
import { assignmentExists, getDueToday, getAssignment, getSubmission, getSubmissionById, getSubmissions, createAssignmentRecord, updateAssignmentRecord, deleteAssignmentRecord, updateSubmissionRecord, updateSubmissionAsTeacherRecord, attachAssignmentToEventRecord, detachAssignmentFromEventRecord, getAvailableEventsForAssignment, attachMarkSchemeRecord, detachMarkSchemeRecord, attachGradingBoundaryRecord, detachGradingBoundaryRecord, reorderAssignmentRecord, moveAssignmentRecord, } from "../services/assignment.js";
|
|
12
9
|
// DEPRECATED: This schema is no longer used - files are uploaded directly to GCS
|
|
13
10
|
// Use directFileSchema instead
|
|
14
11
|
// New schema for direct file uploads (no base64 data)
|
|
@@ -135,1897 +132,110 @@ const updateUploadProgressSchema = z.object({
|
|
|
135
132
|
fileId: z.string(),
|
|
136
133
|
progress: z.number().min(0).max(100),
|
|
137
134
|
});
|
|
138
|
-
// Helper function to get unified list of sections and assignments for a class
|
|
139
|
-
async function getUnifiedList(tx, classId) {
|
|
140
|
-
const [sections, assignments] = await Promise.all([
|
|
141
|
-
tx.section.findMany({
|
|
142
|
-
where: { classId },
|
|
143
|
-
select: { id: true, order: true },
|
|
144
|
-
}),
|
|
145
|
-
tx.assignment.findMany({
|
|
146
|
-
where: { classId },
|
|
147
|
-
select: { id: true, order: true },
|
|
148
|
-
}),
|
|
149
|
-
]);
|
|
150
|
-
// Combine and sort by order
|
|
151
|
-
const unified = [
|
|
152
|
-
...sections.map((s) => ({ id: s.id, order: s.order, type: 'section' })),
|
|
153
|
-
...assignments.map((a) => ({ id: a.id, order: a.order, type: 'assignment' })),
|
|
154
|
-
].sort((a, b) => (a.order ?? Number.MAX_SAFE_INTEGER) - (b.order ?? Number.MAX_SAFE_INTEGER));
|
|
155
|
-
return unified;
|
|
156
|
-
}
|
|
157
|
-
// Helper function to normalize unified list to 1..n
|
|
158
|
-
// Updated to batch updates to prevent timeouts with large lists
|
|
159
|
-
async function normalizeUnifiedList(tx, classId, orderedItems) {
|
|
160
|
-
const BATCH_SIZE = 10; // Process 10 items at a time to avoid overwhelming the transaction
|
|
161
|
-
// Group items by type for more efficient updates
|
|
162
|
-
const sections = [];
|
|
163
|
-
const assignments = [];
|
|
164
|
-
orderedItems.forEach((item, index) => {
|
|
165
|
-
const orderData = { id: item.id, order: index + 1 };
|
|
166
|
-
if (item.type === 'section') {
|
|
167
|
-
sections.push(orderData);
|
|
168
|
-
}
|
|
169
|
-
else {
|
|
170
|
-
assignments.push(orderData);
|
|
171
|
-
}
|
|
172
|
-
});
|
|
173
|
-
// Process updates in batches
|
|
174
|
-
const processBatch = async (items, type) => {
|
|
175
|
-
for (let i = 0; i < items.length; i += BATCH_SIZE) {
|
|
176
|
-
const batch = items.slice(i, i + BATCH_SIZE);
|
|
177
|
-
await Promise.all(batch.map(item => {
|
|
178
|
-
if (type === 'section') {
|
|
179
|
-
return tx.section.update({ where: { id: item.id }, data: { order: item.order } });
|
|
180
|
-
}
|
|
181
|
-
else {
|
|
182
|
-
return tx.assignment.update({ where: { id: item.id }, data: { order: item.order } });
|
|
183
|
-
}
|
|
184
|
-
}));
|
|
185
|
-
}
|
|
186
|
-
};
|
|
187
|
-
// Process sections and assignments sequentially to avoid transaction overload
|
|
188
|
-
await processBatch(sections, 'section');
|
|
189
|
-
await processBatch(assignments, 'assignment');
|
|
190
|
-
}
|
|
191
135
|
export const assignmentRouter = createTRPCRouter({
|
|
192
|
-
// Reorder an assignment within the unified list (sections + assignments)
|
|
193
136
|
reorder: protectedTeacherProcedure
|
|
194
137
|
.input(z.object({
|
|
195
138
|
classId: z.string(),
|
|
196
139
|
movedId: z.string(),
|
|
197
|
-
// One of: place at start/end of unified list, or relative to targetId (can be section or assignment)
|
|
198
140
|
position: z.enum(['start', 'end', 'before', 'after']),
|
|
199
|
-
targetId: z.string().optional(),
|
|
200
|
-
}))
|
|
201
|
-
.mutation(async ({ ctx, input }) => {
|
|
202
|
-
const { classId, movedId, position, targetId } = input;
|
|
203
|
-
const moved = await prisma.assignment.findFirst({
|
|
204
|
-
where: { id: movedId, classId },
|
|
205
|
-
select: { id: true, classId: true },
|
|
206
|
-
});
|
|
207
|
-
if (!moved) {
|
|
208
|
-
throw new TRPCError({ code: 'NOT_FOUND', message: 'Assignment not found' });
|
|
209
|
-
}
|
|
210
|
-
if ((position === 'before' || position === 'after') && !targetId) {
|
|
211
|
-
throw new TRPCError({ code: 'BAD_REQUEST', message: 'targetId required for before/after' });
|
|
212
|
-
}
|
|
213
|
-
const result = await prisma.$transaction(async (tx) => {
|
|
214
|
-
const unified = await getUnifiedList(tx, classId);
|
|
215
|
-
// Find moved item and target in unified list
|
|
216
|
-
const movedIdx = unified.findIndex(item => item.id === movedId && item.type === 'assignment');
|
|
217
|
-
if (movedIdx === -1) {
|
|
218
|
-
throw new TRPCError({ code: 'NOT_FOUND', message: 'Assignment not found in unified list' });
|
|
219
|
-
}
|
|
220
|
-
// Build list without moved item
|
|
221
|
-
const withoutMoved = unified.filter(item => !(item.id === movedId && item.type === 'assignment'));
|
|
222
|
-
let next = [];
|
|
223
|
-
if (position === 'start') {
|
|
224
|
-
next = [{ id: movedId, type: 'assignment' }, ...withoutMoved.map(item => ({ id: item.id, type: item.type }))];
|
|
225
|
-
}
|
|
226
|
-
else if (position === 'end') {
|
|
227
|
-
next = [...withoutMoved.map(item => ({ id: item.id, type: item.type })), { id: movedId, type: 'assignment' }];
|
|
228
|
-
}
|
|
229
|
-
else {
|
|
230
|
-
const targetIdx = withoutMoved.findIndex(item => item.id === targetId);
|
|
231
|
-
if (targetIdx === -1) {
|
|
232
|
-
throw new TRPCError({ code: 'BAD_REQUEST', message: 'targetId not found in unified list' });
|
|
233
|
-
}
|
|
234
|
-
if (position === 'before') {
|
|
235
|
-
next = [
|
|
236
|
-
...withoutMoved.slice(0, targetIdx).map(item => ({ id: item.id, type: item.type })),
|
|
237
|
-
{ id: movedId, type: 'assignment' },
|
|
238
|
-
...withoutMoved.slice(targetIdx).map(item => ({ id: item.id, type: item.type })),
|
|
239
|
-
];
|
|
240
|
-
}
|
|
241
|
-
else {
|
|
242
|
-
next = [
|
|
243
|
-
...withoutMoved.slice(0, targetIdx + 1).map(item => ({ id: item.id, type: item.type })),
|
|
244
|
-
{ id: movedId, type: 'assignment' },
|
|
245
|
-
...withoutMoved.slice(targetIdx + 1).map(item => ({ id: item.id, type: item.type })),
|
|
246
|
-
];
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
// Normalize to 1..n
|
|
250
|
-
await normalizeUnifiedList(tx, classId, next);
|
|
251
|
-
return tx.assignment.findUnique({ where: { id: movedId } });
|
|
252
|
-
}, {
|
|
253
|
-
maxWait: 10000, // 10 seconds max wait time
|
|
254
|
-
timeout: 30000, // 30 seconds timeout for reordering operations
|
|
255
|
-
});
|
|
256
|
-
return result;
|
|
257
|
-
}),
|
|
258
|
-
order: protectedTeacherProcedure
|
|
259
|
-
.input(z.object({
|
|
260
|
-
id: z.string(),
|
|
261
|
-
classId: z.string(),
|
|
262
|
-
order: z.number(),
|
|
141
|
+
targetId: z.string().optional(),
|
|
263
142
|
}))
|
|
264
|
-
.mutation(
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
});
|
|
271
|
-
if (!current) {
|
|
272
|
-
throw new TRPCError({ code: 'NOT_FOUND', message: 'Assignment not found' });
|
|
273
|
-
}
|
|
274
|
-
const updated = await prisma.$transaction(async (tx) => {
|
|
275
|
-
await tx.assignment.update({ where: { id }, data: { order } });
|
|
276
|
-
// Normalize entire unified list
|
|
277
|
-
const unified = await getUnifiedList(tx, current.classId);
|
|
278
|
-
await normalizeUnifiedList(tx, current.classId, unified.map(item => ({ id: item.id, type: item.type })));
|
|
279
|
-
return tx.assignment.findUnique({ where: { id } });
|
|
280
|
-
}, {
|
|
281
|
-
maxWait: 10000, // 10 seconds max wait time
|
|
282
|
-
timeout: 30000, // 30 seconds timeout for reordering operations
|
|
283
|
-
});
|
|
284
|
-
return updated;
|
|
285
|
-
}),
|
|
143
|
+
.mutation(({ ctx, input }) => reorderAssignmentRecord(ctx.user.id, {
|
|
144
|
+
classId: input.classId,
|
|
145
|
+
movedId: input.movedId,
|
|
146
|
+
position: input.position,
|
|
147
|
+
targetId: input.targetId,
|
|
148
|
+
})),
|
|
286
149
|
exists: protectedClassMemberProcedure
|
|
287
|
-
.input(z.object({
|
|
288
|
-
|
|
289
|
-
}))
|
|
290
|
-
.query(async ({ ctx, input }) => {
|
|
291
|
-
if (!ctx.user) {
|
|
292
|
-
throw new TRPCError({ code: 'UNAUTHORIZED', message: 'User must be authenticated' });
|
|
293
|
-
}
|
|
294
|
-
const assignment = await prisma.assignment.findUnique({
|
|
295
|
-
where: { id: input.id },
|
|
296
|
-
});
|
|
297
|
-
return assignment ? true : false;
|
|
298
|
-
}),
|
|
150
|
+
.input(z.object({ id: z.string() }))
|
|
151
|
+
.query(({ input }) => assignmentExists(input.id)),
|
|
299
152
|
move: protectedTeacherProcedure
|
|
300
153
|
.input(z.object({
|
|
301
154
|
id: z.string(),
|
|
302
155
|
classId: z.string(),
|
|
303
156
|
targetSectionId: z.string().nullable().optional(),
|
|
304
157
|
}))
|
|
305
|
-
.mutation(
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
select: { id: true, classId: true, sectionId: true },
|
|
311
|
-
});
|
|
312
|
-
if (!moved) {
|
|
313
|
-
throw new TRPCError({ code: 'NOT_FOUND', message: 'Assignment not found' });
|
|
314
|
-
}
|
|
315
|
-
const updated = await prisma.$transaction(async (tx) => {
|
|
316
|
-
// Update sectionId first
|
|
317
|
-
await tx.assignment.update({ where: { id }, data: { sectionId: targetSectionId } });
|
|
318
|
-
// The unified list ordering remains the same, just the assignment's sectionId changed
|
|
319
|
-
// No need to reorder since we're keeping the same position in the unified list
|
|
320
|
-
// If frontend wants to change position, they should call reorder after move
|
|
321
|
-
return tx.assignment.findUnique({ where: { id } });
|
|
322
|
-
}, {
|
|
323
|
-
maxWait: 5000, // 5 seconds max wait time
|
|
324
|
-
timeout: 10000, // 10 seconds timeout
|
|
325
|
-
});
|
|
326
|
-
return updated;
|
|
327
|
-
}),
|
|
158
|
+
.mutation(({ ctx, input }) => moveAssignmentRecord(ctx.user.id, {
|
|
159
|
+
id: input.id,
|
|
160
|
+
classId: input.classId,
|
|
161
|
+
targetSectionId: (input.targetSectionId ?? null) || null,
|
|
162
|
+
})),
|
|
328
163
|
create: protectedTeacherProcedure
|
|
329
164
|
.input(createAssignmentSchema)
|
|
330
|
-
.mutation(
|
|
331
|
-
const { classId, id, title, instructions, dueDate, files, existingFileIds, aiPolicyLevel, acceptFiles, acceptExtendedResponse, acceptWorksheet, worksheetIds, gradeWithAI, studentIds, maxGrade, graded, weight, sectionId, type, markSchemeId, gradingBoundaryId, inProgress } = input;
|
|
332
|
-
if (!ctx.user) {
|
|
333
|
-
throw new TRPCError({
|
|
334
|
-
code: "UNAUTHORIZED",
|
|
335
|
-
message: "User must be authenticated",
|
|
336
|
-
});
|
|
337
|
-
}
|
|
338
|
-
// Pre-fetch data needed for the transaction (outside transaction scope)
|
|
339
|
-
const [classData, rubricData] = await Promise.all([
|
|
340
|
-
prisma.class.findUnique({
|
|
341
|
-
where: { id: classId },
|
|
342
|
-
include: {
|
|
343
|
-
students: {
|
|
344
|
-
select: { id: true }
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
}),
|
|
348
|
-
markSchemeId ? prisma.markScheme.findUnique({
|
|
349
|
-
where: { id: markSchemeId },
|
|
350
|
-
select: {
|
|
351
|
-
structured: true,
|
|
352
|
-
}
|
|
353
|
-
}) : null
|
|
354
|
-
]);
|
|
355
|
-
if (!classData) {
|
|
356
|
-
throw new TRPCError({
|
|
357
|
-
code: "NOT_FOUND",
|
|
358
|
-
message: "Class not found",
|
|
359
|
-
});
|
|
360
|
-
}
|
|
361
|
-
// Calculate max grade outside transaction
|
|
362
|
-
let computedMaxGrade = maxGrade;
|
|
363
|
-
if (markSchemeId && rubricData) {
|
|
364
|
-
const parsedRubric = JSON.parse(rubricData.structured || "{}");
|
|
365
|
-
computedMaxGrade = parsedRubric.criteria.reduce((acc, criterion) => {
|
|
366
|
-
const maxPoints = Math.max(...criterion.levels.map((level) => level.points));
|
|
367
|
-
return acc + maxPoints;
|
|
368
|
-
}, 0);
|
|
369
|
-
}
|
|
370
|
-
// Prepare submission data outside transaction
|
|
371
|
-
const submissionData = studentIds && studentIds.length > 0
|
|
372
|
-
? studentIds.map((studentId) => ({
|
|
373
|
-
student: { connect: { id: studentId } }
|
|
374
|
-
}))
|
|
375
|
-
: classData.students.map((student) => ({
|
|
376
|
-
student: { connect: { id: student.id } }
|
|
377
|
-
}));
|
|
378
|
-
const teacherId = ctx.user.id;
|
|
379
|
-
// Minimal transaction - only for critical operations
|
|
380
|
-
const assignment = await prisma.$transaction(async (tx) => {
|
|
381
|
-
// Create assignment with order 0 (will be at top)
|
|
382
|
-
const created = await tx.assignment.create({
|
|
383
|
-
data: {
|
|
384
|
-
...(id && { id }),
|
|
385
|
-
title,
|
|
386
|
-
instructions,
|
|
387
|
-
dueDate: new Date(dueDate),
|
|
388
|
-
maxGrade: markSchemeId ? computedMaxGrade : maxGrade,
|
|
389
|
-
graded,
|
|
390
|
-
weight,
|
|
391
|
-
type,
|
|
392
|
-
...(aiPolicyLevel && { aiPolicyLevel }),
|
|
393
|
-
acceptFiles,
|
|
394
|
-
acceptExtendedResponse,
|
|
395
|
-
acceptWorksheet,
|
|
396
|
-
...(worksheetIds && {
|
|
397
|
-
worksheets: {
|
|
398
|
-
connect: worksheetIds.map(id => ({ id }))
|
|
399
|
-
}
|
|
400
|
-
}),
|
|
401
|
-
gradeWithAI,
|
|
402
|
-
...(studentIds && {
|
|
403
|
-
assignedTo: {
|
|
404
|
-
connect: studentIds.map(id => ({ id }))
|
|
405
|
-
}
|
|
406
|
-
}),
|
|
407
|
-
order: 0, // Set to 0 to place at top
|
|
408
|
-
inProgress: inProgress || false,
|
|
409
|
-
class: {
|
|
410
|
-
connect: { id: classId }
|
|
411
|
-
},
|
|
412
|
-
...(sectionId && {
|
|
413
|
-
section: {
|
|
414
|
-
connect: { id: sectionId }
|
|
415
|
-
}
|
|
416
|
-
}),
|
|
417
|
-
...(markSchemeId && {
|
|
418
|
-
markScheme: {
|
|
419
|
-
connect: { id: markSchemeId }
|
|
420
|
-
}
|
|
421
|
-
}),
|
|
422
|
-
...(gradingBoundaryId && {
|
|
423
|
-
gradingBoundary: {
|
|
424
|
-
connect: { id: gradingBoundaryId }
|
|
425
|
-
}
|
|
426
|
-
}),
|
|
427
|
-
submissions: {
|
|
428
|
-
create: submissionData
|
|
429
|
-
},
|
|
430
|
-
teacher: {
|
|
431
|
-
connect: { id: teacherId }
|
|
432
|
-
}
|
|
433
|
-
},
|
|
434
|
-
select: {
|
|
435
|
-
id: true,
|
|
436
|
-
title: true,
|
|
437
|
-
instructions: true,
|
|
438
|
-
dueDate: true,
|
|
439
|
-
maxGrade: true,
|
|
440
|
-
graded: true,
|
|
441
|
-
weight: true,
|
|
442
|
-
type: true,
|
|
443
|
-
attachments: {
|
|
444
|
-
select: {
|
|
445
|
-
id: true,
|
|
446
|
-
name: true,
|
|
447
|
-
type: true,
|
|
448
|
-
}
|
|
449
|
-
},
|
|
450
|
-
section: {
|
|
451
|
-
select: {
|
|
452
|
-
id: true,
|
|
453
|
-
name: true
|
|
454
|
-
}
|
|
455
|
-
},
|
|
456
|
-
teacher: {
|
|
457
|
-
select: {
|
|
458
|
-
id: true,
|
|
459
|
-
username: true
|
|
460
|
-
}
|
|
461
|
-
},
|
|
462
|
-
class: {
|
|
463
|
-
select: {
|
|
464
|
-
id: true,
|
|
465
|
-
name: true
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
});
|
|
470
|
-
// Optimized reordering - only increment existing items by 1
|
|
471
|
-
// This is much faster than reordering everything
|
|
472
|
-
await tx.assignment.updateMany({
|
|
473
|
-
where: {
|
|
474
|
-
classId: classId,
|
|
475
|
-
id: { not: created.id },
|
|
476
|
-
},
|
|
477
|
-
data: {
|
|
478
|
-
order: { increment: 1 }
|
|
479
|
-
}
|
|
480
|
-
});
|
|
481
|
-
await tx.section.updateMany({
|
|
482
|
-
where: {
|
|
483
|
-
classId: classId,
|
|
484
|
-
},
|
|
485
|
-
data: {
|
|
486
|
-
order: { increment: 1 }
|
|
487
|
-
}
|
|
488
|
-
});
|
|
489
|
-
// Update the created assignment to have order 1
|
|
490
|
-
await tx.assignment.update({
|
|
491
|
-
where: { id: created.id },
|
|
492
|
-
data: { order: 1 }
|
|
493
|
-
});
|
|
494
|
-
return created;
|
|
495
|
-
}, {
|
|
496
|
-
maxWait: 10000, // Increased to 10 seconds max wait time
|
|
497
|
-
timeout: 20000, // Increased to 20 seconds timeout for safety
|
|
498
|
-
});
|
|
499
|
-
// Handle file operations outside transaction
|
|
500
|
-
const fileOperations = [];
|
|
501
|
-
// Process direct upload files
|
|
502
|
-
if (files && files.length > 0) {
|
|
503
|
-
fileOperations.push(createDirectUploadFiles(files, ctx.user.id, undefined, assignment.id)
|
|
504
|
-
.then(uploadedFiles => {
|
|
505
|
-
if (uploadedFiles.length > 0) {
|
|
506
|
-
return prisma.assignment.update({
|
|
507
|
-
where: { id: assignment.id },
|
|
508
|
-
data: {
|
|
509
|
-
attachments: {
|
|
510
|
-
create: uploadedFiles.map(file => ({
|
|
511
|
-
name: file.name,
|
|
512
|
-
type: file.type,
|
|
513
|
-
size: file.size,
|
|
514
|
-
path: file.path,
|
|
515
|
-
}))
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
});
|
|
519
|
-
}
|
|
520
|
-
}));
|
|
521
|
-
}
|
|
522
|
-
// Connect existing files
|
|
523
|
-
if (existingFileIds && existingFileIds.length > 0) {
|
|
524
|
-
fileOperations.push(prisma.assignment.update({
|
|
525
|
-
where: { id: assignment.id },
|
|
526
|
-
data: {
|
|
527
|
-
attachments: {
|
|
528
|
-
connect: existingFileIds.map(fileId => ({ id: fileId }))
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
}));
|
|
532
|
-
}
|
|
533
|
-
// Execute file operations in parallel
|
|
534
|
-
await Promise.all(fileOperations);
|
|
535
|
-
// Send notifications asynchronously (non-blocking)
|
|
536
|
-
sendNotifications(classData.students.map(student => student.id), {
|
|
537
|
-
title: `🔔 New assignment for ${classData.name}`,
|
|
538
|
-
content: `The assignment "${title}" has been created in ${classData.name}.\n
|
|
539
|
-
Due date: ${new Date(dueDate).toLocaleDateString()}.
|
|
540
|
-
[Link to assignment](/class/${classId}/assignments/${assignment.id})`
|
|
541
|
-
}).catch(error => {
|
|
542
|
-
logger.error('Failed to send assignment notifications:', error);
|
|
543
|
-
});
|
|
544
|
-
return assignment;
|
|
545
|
-
}),
|
|
165
|
+
.mutation(({ ctx, input }) => createAssignmentRecord(ctx.user.id, input)),
|
|
546
166
|
update: protectedTeacherProcedure
|
|
547
167
|
.input(updateAssignmentSchema)
|
|
548
|
-
.mutation(
|
|
549
|
-
const { id, title, instructions, dueDate, files, existingFileIds, worksheetIds, aiPolicyLevel, maxGrade, graded, weight, sectionId, type, inProgress, acceptFiles, acceptExtendedResponse, acceptWorksheet, gradeWithAI, studentIds } = input;
|
|
550
|
-
if (!ctx.user) {
|
|
551
|
-
throw new TRPCError({
|
|
552
|
-
code: "UNAUTHORIZED",
|
|
553
|
-
message: "User must be authenticated",
|
|
554
|
-
});
|
|
555
|
-
}
|
|
556
|
-
// Pre-fetch all necessary data outside transaction
|
|
557
|
-
const [assignment, classData] = await Promise.all([
|
|
558
|
-
prisma.assignment.findFirst({
|
|
559
|
-
where: {
|
|
560
|
-
id,
|
|
561
|
-
teacherId: ctx.user.id,
|
|
562
|
-
},
|
|
563
|
-
include: {
|
|
564
|
-
attachments: {
|
|
565
|
-
select: {
|
|
566
|
-
id: true,
|
|
567
|
-
name: true,
|
|
568
|
-
type: true,
|
|
569
|
-
path: true,
|
|
570
|
-
size: true,
|
|
571
|
-
uploadStatus: true,
|
|
572
|
-
thumbnail: {
|
|
573
|
-
select: {
|
|
574
|
-
path: true
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
},
|
|
578
|
-
},
|
|
579
|
-
class: {
|
|
580
|
-
select: {
|
|
581
|
-
id: true,
|
|
582
|
-
name: true
|
|
583
|
-
}
|
|
584
|
-
},
|
|
585
|
-
markScheme: true,
|
|
586
|
-
},
|
|
587
|
-
}),
|
|
588
|
-
prisma.class.findFirst({
|
|
589
|
-
where: {
|
|
590
|
-
assignments: {
|
|
591
|
-
some: { id }
|
|
592
|
-
}
|
|
593
|
-
},
|
|
594
|
-
include: {
|
|
595
|
-
students: {
|
|
596
|
-
select: { id: true }
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
})
|
|
600
|
-
]);
|
|
601
|
-
if (!assignment) {
|
|
602
|
-
throw new TRPCError({
|
|
603
|
-
code: "NOT_FOUND",
|
|
604
|
-
message: "Assignment not found",
|
|
605
|
-
});
|
|
606
|
-
}
|
|
607
|
-
// Prepare submission data outside transaction if needed
|
|
608
|
-
const submissionData = studentIds && studentIds.length > 0
|
|
609
|
-
? studentIds.map((studentId) => ({
|
|
610
|
-
student: { connect: { id: studentId } }
|
|
611
|
-
}))
|
|
612
|
-
: classData?.students.map((student) => ({
|
|
613
|
-
student: { connect: { id: student.id } }
|
|
614
|
-
}));
|
|
615
|
-
// Handle file deletion operations outside transaction
|
|
616
|
-
const fileDeletionPromises = [];
|
|
617
|
-
if (input.removedAttachments && input.removedAttachments.length > 0) {
|
|
618
|
-
const filesToDelete = assignment.attachments.filter((file) => input.removedAttachments.includes(file.id));
|
|
619
|
-
filesToDelete.forEach((file) => {
|
|
620
|
-
if (file.uploadStatus === 'COMPLETED') {
|
|
621
|
-
fileDeletionPromises.push(deleteFile(file.path).catch(error => {
|
|
622
|
-
console.warn(`Failed to delete file ${file.path}:`, error);
|
|
623
|
-
}));
|
|
624
|
-
if (file.thumbnail?.path) {
|
|
625
|
-
fileDeletionPromises.push(deleteFile(file.thumbnail.path).catch(error => {
|
|
626
|
-
console.warn(`Failed to delete thumbnail ${file.thumbnail.path}:`, error);
|
|
627
|
-
}));
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
});
|
|
631
|
-
}
|
|
632
|
-
// Execute file deletions in parallel
|
|
633
|
-
await Promise.all(fileDeletionPromises);
|
|
634
|
-
// Prepare file upload operations
|
|
635
|
-
let uploadedFiles = [];
|
|
636
|
-
if (files && files.length > 0) {
|
|
637
|
-
uploadedFiles = await createDirectUploadFiles(files, ctx.user.id, undefined, input.id);
|
|
638
|
-
}
|
|
639
|
-
// Minimal transaction for database update
|
|
640
|
-
const updatedAssignment = await prisma.$transaction(async (tx) => {
|
|
641
|
-
return tx.assignment.update({
|
|
642
|
-
where: { id },
|
|
643
|
-
data: {
|
|
644
|
-
...(title && { title }),
|
|
645
|
-
...(instructions && { instructions }),
|
|
646
|
-
...(dueDate && { dueDate: new Date(dueDate) }),
|
|
647
|
-
...(maxGrade && { maxGrade }),
|
|
648
|
-
...(graded !== undefined && { graded }),
|
|
649
|
-
...(weight && { weight }),
|
|
650
|
-
...(type && { type }),
|
|
651
|
-
...(inProgress !== undefined && { inProgress }),
|
|
652
|
-
...(acceptFiles !== undefined && { acceptFiles }),
|
|
653
|
-
...(acceptExtendedResponse !== undefined && { acceptExtendedResponse }),
|
|
654
|
-
...(acceptWorksheet !== undefined && { acceptWorksheet }),
|
|
655
|
-
...(gradeWithAI !== undefined && { gradeWithAI }),
|
|
656
|
-
...(studentIds && {
|
|
657
|
-
assignedTo: {
|
|
658
|
-
set: [], // Clear existing
|
|
659
|
-
connect: studentIds.map(id => ({ id }))
|
|
660
|
-
}
|
|
661
|
-
}),
|
|
662
|
-
...(submissionData && {
|
|
663
|
-
submissions: {
|
|
664
|
-
deleteMany: {}, // Clear existing submissions
|
|
665
|
-
create: submissionData
|
|
666
|
-
}
|
|
667
|
-
}),
|
|
668
|
-
...(aiPolicyLevel !== undefined && { aiPolicyLevel }),
|
|
669
|
-
...(sectionId !== undefined && {
|
|
670
|
-
section: sectionId ? {
|
|
671
|
-
connect: { id: sectionId }
|
|
672
|
-
} : {
|
|
673
|
-
disconnect: true
|
|
674
|
-
}
|
|
675
|
-
}),
|
|
676
|
-
...(worksheetIds && {
|
|
677
|
-
worksheets: {
|
|
678
|
-
set: [], // Clear existing
|
|
679
|
-
connect: worksheetIds.map(id => ({ id }))
|
|
680
|
-
}
|
|
681
|
-
}),
|
|
682
|
-
...(uploadedFiles.length > 0 && {
|
|
683
|
-
attachments: {
|
|
684
|
-
create: uploadedFiles.map(file => ({
|
|
685
|
-
name: file.name,
|
|
686
|
-
type: file.type,
|
|
687
|
-
size: file.size,
|
|
688
|
-
path: file.path,
|
|
689
|
-
...(file.thumbnailId && {
|
|
690
|
-
thumbnail: {
|
|
691
|
-
connect: { id: file.thumbnailId }
|
|
692
|
-
}
|
|
693
|
-
})
|
|
694
|
-
}))
|
|
695
|
-
}
|
|
696
|
-
}),
|
|
697
|
-
...(existingFileIds && existingFileIds.length > 0 && {
|
|
698
|
-
attachments: {
|
|
699
|
-
connect: existingFileIds.map(fileId => ({ id: fileId }))
|
|
700
|
-
}
|
|
701
|
-
}),
|
|
702
|
-
...(input.removedAttachments && input.removedAttachments.length > 0 && {
|
|
703
|
-
attachments: {
|
|
704
|
-
deleteMany: {
|
|
705
|
-
id: { in: input.removedAttachments }
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
}),
|
|
709
|
-
},
|
|
710
|
-
select: {
|
|
711
|
-
id: true,
|
|
712
|
-
title: true,
|
|
713
|
-
instructions: true,
|
|
714
|
-
dueDate: true,
|
|
715
|
-
maxGrade: true,
|
|
716
|
-
graded: true,
|
|
717
|
-
weight: true,
|
|
718
|
-
type: true,
|
|
719
|
-
createdAt: true,
|
|
720
|
-
markSchemeId: true,
|
|
721
|
-
submissions: {
|
|
722
|
-
select: {
|
|
723
|
-
student: {
|
|
724
|
-
select: {
|
|
725
|
-
id: true,
|
|
726
|
-
username: true
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
},
|
|
731
|
-
attachments: {
|
|
732
|
-
select: {
|
|
733
|
-
id: true,
|
|
734
|
-
name: true,
|
|
735
|
-
type: true,
|
|
736
|
-
thumbnail: true,
|
|
737
|
-
size: true,
|
|
738
|
-
path: true,
|
|
739
|
-
uploadedAt: true,
|
|
740
|
-
thumbnailId: true,
|
|
741
|
-
}
|
|
742
|
-
},
|
|
743
|
-
section: true,
|
|
744
|
-
teacher: true,
|
|
745
|
-
class: true
|
|
746
|
-
}
|
|
747
|
-
});
|
|
748
|
-
}, {
|
|
749
|
-
maxWait: 5000, // 5 seconds max wait time
|
|
750
|
-
timeout: 10000, // 10 seconds timeout
|
|
751
|
-
});
|
|
752
|
-
// Handle rubric max grade calculation outside transaction
|
|
753
|
-
if (updatedAssignment.markSchemeId) {
|
|
754
|
-
prisma.markScheme.findUnique({
|
|
755
|
-
where: { id: updatedAssignment.markSchemeId },
|
|
756
|
-
select: {
|
|
757
|
-
structured: true,
|
|
758
|
-
}
|
|
759
|
-
}).then(rubric => {
|
|
760
|
-
if (rubric) {
|
|
761
|
-
const parsedRubric = JSON.parse(rubric.structured || "{}");
|
|
762
|
-
const computedMaxGrade = parsedRubric.criteria?.reduce((acc, criterion) => {
|
|
763
|
-
const maxPoints = Math.max(...criterion.levels.map((level) => level.points));
|
|
764
|
-
return acc + maxPoints;
|
|
765
|
-
}, 0) || 0;
|
|
766
|
-
return prisma.assignment.update({
|
|
767
|
-
where: { id },
|
|
768
|
-
data: {
|
|
769
|
-
maxGrade: computedMaxGrade,
|
|
770
|
-
}
|
|
771
|
-
});
|
|
772
|
-
}
|
|
773
|
-
}).catch(error => {
|
|
774
|
-
logger.error('Failed to update max grade from rubric:', error);
|
|
775
|
-
});
|
|
776
|
-
}
|
|
777
|
-
return updatedAssignment;
|
|
778
|
-
}),
|
|
168
|
+
.mutation(({ ctx, input }) => updateAssignmentRecord(ctx.user.id, input)),
|
|
779
169
|
delete: protectedProcedure
|
|
780
170
|
.input(deleteAssignmentSchema)
|
|
781
|
-
.mutation(
|
|
782
|
-
const { id, classId } = input;
|
|
783
|
-
if (!ctx.user) {
|
|
784
|
-
throw new TRPCError({
|
|
785
|
-
code: "UNAUTHORIZED",
|
|
786
|
-
message: "User must be authenticated",
|
|
787
|
-
});
|
|
788
|
-
}
|
|
789
|
-
// Get the assignment with all related files
|
|
790
|
-
const assignment = await prisma.assignment.findFirst({
|
|
791
|
-
where: {
|
|
792
|
-
id,
|
|
793
|
-
teacherId: ctx.user.id,
|
|
794
|
-
},
|
|
795
|
-
include: {
|
|
796
|
-
attachments: {
|
|
797
|
-
include: {
|
|
798
|
-
thumbnail: true
|
|
799
|
-
}
|
|
800
|
-
},
|
|
801
|
-
submissions: {
|
|
802
|
-
include: {
|
|
803
|
-
attachments: {
|
|
804
|
-
include: {
|
|
805
|
-
thumbnail: true
|
|
806
|
-
}
|
|
807
|
-
},
|
|
808
|
-
annotations: {
|
|
809
|
-
include: {
|
|
810
|
-
thumbnail: true
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
});
|
|
817
|
-
if (!assignment) {
|
|
818
|
-
throw new TRPCError({
|
|
819
|
-
code: "NOT_FOUND",
|
|
820
|
-
message: "Assignment not found",
|
|
821
|
-
});
|
|
822
|
-
}
|
|
823
|
-
// Delete all files from storage
|
|
824
|
-
const filesToDelete = [
|
|
825
|
-
...assignment.attachments,
|
|
826
|
-
...assignment.submissions.flatMap(sub => [...sub.attachments, ...sub.annotations])
|
|
827
|
-
];
|
|
828
|
-
// Delete files from storage (only if they were actually uploaded)
|
|
829
|
-
await Promise.all(filesToDelete.map(async (file) => {
|
|
830
|
-
try {
|
|
831
|
-
// Only delete from GCS if the file was successfully uploaded
|
|
832
|
-
if (file.uploadStatus === 'COMPLETED') {
|
|
833
|
-
// Delete the main file
|
|
834
|
-
await deleteFile(file.path);
|
|
835
|
-
// Delete thumbnail if it exists
|
|
836
|
-
if (file.thumbnail) {
|
|
837
|
-
await deleteFile(file.thumbnail.path);
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
catch (error) {
|
|
842
|
-
console.warn(`Failed to delete file ${file.path}:`, error);
|
|
843
|
-
}
|
|
844
|
-
}));
|
|
845
|
-
// Delete the assignment (this will cascade delete all related records)
|
|
846
|
-
await prisma.assignment.delete({
|
|
847
|
-
where: { id },
|
|
848
|
-
});
|
|
849
|
-
return {
|
|
850
|
-
id,
|
|
851
|
-
};
|
|
852
|
-
}),
|
|
171
|
+
.mutation(({ ctx, input }) => deleteAssignmentRecord(ctx.user.id, input.id, input.classId)),
|
|
853
172
|
get: protectedClassMemberProcedure
|
|
854
173
|
.input(getAssignmentSchema)
|
|
855
|
-
.query(
|
|
856
|
-
const { id, classId } = input;
|
|
857
|
-
if (!ctx.user) {
|
|
858
|
-
throw new TRPCError({
|
|
859
|
-
code: "UNAUTHORIZED",
|
|
860
|
-
message: "User must be authenticated",
|
|
861
|
-
});
|
|
862
|
-
}
|
|
863
|
-
const assignment = await prisma.assignment.findUnique({
|
|
864
|
-
where: {
|
|
865
|
-
id,
|
|
866
|
-
// classId,
|
|
867
|
-
},
|
|
868
|
-
include: {
|
|
869
|
-
submissions: {
|
|
870
|
-
select: {
|
|
871
|
-
student: {
|
|
872
|
-
select: {
|
|
873
|
-
id: true,
|
|
874
|
-
username: true
|
|
875
|
-
}
|
|
876
|
-
}
|
|
877
|
-
}
|
|
878
|
-
},
|
|
879
|
-
attachments: {
|
|
880
|
-
select: {
|
|
881
|
-
id: true,
|
|
882
|
-
name: true,
|
|
883
|
-
type: true,
|
|
884
|
-
size: true,
|
|
885
|
-
path: true,
|
|
886
|
-
uploadedAt: true,
|
|
887
|
-
thumbnailId: true,
|
|
888
|
-
}
|
|
889
|
-
},
|
|
890
|
-
section: {
|
|
891
|
-
select: {
|
|
892
|
-
id: true,
|
|
893
|
-
name: true,
|
|
894
|
-
}
|
|
895
|
-
},
|
|
896
|
-
teacher: {
|
|
897
|
-
select: {
|
|
898
|
-
id: true,
|
|
899
|
-
username: true
|
|
900
|
-
}
|
|
901
|
-
},
|
|
902
|
-
worksheets: {
|
|
903
|
-
select: {
|
|
904
|
-
id: true,
|
|
905
|
-
name: true,
|
|
906
|
-
}
|
|
907
|
-
},
|
|
908
|
-
class: {
|
|
909
|
-
select: {
|
|
910
|
-
id: true,
|
|
911
|
-
name: true
|
|
912
|
-
}
|
|
913
|
-
},
|
|
914
|
-
eventAttached: {
|
|
915
|
-
select: {
|
|
916
|
-
id: true,
|
|
917
|
-
name: true,
|
|
918
|
-
startTime: true,
|
|
919
|
-
endTime: true,
|
|
920
|
-
location: true,
|
|
921
|
-
remarks: true,
|
|
922
|
-
color: true,
|
|
923
|
-
}
|
|
924
|
-
},
|
|
925
|
-
markScheme: {
|
|
926
|
-
select: {
|
|
927
|
-
id: true,
|
|
928
|
-
structured: true,
|
|
929
|
-
}
|
|
930
|
-
},
|
|
931
|
-
gradingBoundary: {
|
|
932
|
-
select: {
|
|
933
|
-
id: true,
|
|
934
|
-
structured: true,
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
}
|
|
938
|
-
});
|
|
939
|
-
if (!assignment) {
|
|
940
|
-
throw new TRPCError({
|
|
941
|
-
code: "NOT_FOUND",
|
|
942
|
-
message: "Assignment not found",
|
|
943
|
-
});
|
|
944
|
-
}
|
|
945
|
-
const sections = await prisma.section.findMany({
|
|
946
|
-
where: {
|
|
947
|
-
classId: assignment.classId,
|
|
948
|
-
},
|
|
949
|
-
select: {
|
|
950
|
-
id: true,
|
|
951
|
-
name: true,
|
|
952
|
-
},
|
|
953
|
-
});
|
|
954
|
-
return { ...assignment, sections };
|
|
955
|
-
}),
|
|
174
|
+
.query(({ input }) => getAssignment(input.id, input.classId)),
|
|
956
175
|
getSubmission: protectedClassMemberProcedure
|
|
176
|
+
.input(z.object({ assignmentId: z.string(), classId: z.string() }))
|
|
177
|
+
.query(({ ctx, input }) => getSubmission(input.assignmentId, ctx.user.id)),
|
|
178
|
+
getSubmissionById: protectedClassMemberProcedure
|
|
179
|
+
.input(z.object({ classId: z.string(), submissionId: z.string() }))
|
|
180
|
+
.query(({ ctx, input }) => getSubmissionById(input.submissionId, input.classId, ctx.user.id)),
|
|
181
|
+
updateSubmission: protectedClassMemberProcedure
|
|
182
|
+
.input(submissionSchema)
|
|
183
|
+
.mutation(({ ctx, input }) => updateSubmissionRecord(ctx.user.id, {
|
|
184
|
+
submissionId: input.submissionId,
|
|
185
|
+
assignmentId: input.assignmentId,
|
|
186
|
+
classId: input.classId,
|
|
187
|
+
submit: input.submit,
|
|
188
|
+
newAttachments: input.newAttachments,
|
|
189
|
+
extendedResponse: input.extendedResponse,
|
|
190
|
+
existingFileIds: input.existingFileIds,
|
|
191
|
+
removedAttachments: input.removedAttachments,
|
|
192
|
+
})),
|
|
193
|
+
getSubmissions: protectedTeacherProcedure
|
|
194
|
+
.input(z.object({ assignmentId: z.string(), classId: z.string() }))
|
|
195
|
+
.query(({ ctx, input }) => getSubmissions(input.assignmentId, ctx.user.id)),
|
|
196
|
+
updateSubmissionAsTeacher: protectedTeacherProcedure
|
|
197
|
+
.input(updateSubmissionSchema)
|
|
198
|
+
.mutation(({ ctx, input }) => updateSubmissionAsTeacherRecord(ctx.user.id, {
|
|
199
|
+
submissionId: input.submissionId,
|
|
200
|
+
assignmentId: input.assignmentId,
|
|
201
|
+
classId: input.classId,
|
|
202
|
+
return: input.return,
|
|
203
|
+
gradeReceived: input.gradeReceived,
|
|
204
|
+
newAttachments: input.newAttachments,
|
|
205
|
+
existingFileIds: input.existingFileIds,
|
|
206
|
+
removedAttachments: input.removedAttachments,
|
|
207
|
+
rubricGrades: input.rubricGrades,
|
|
208
|
+
feedback: input.feedback,
|
|
209
|
+
})),
|
|
210
|
+
attachToEvent: protectedTeacherProcedure
|
|
211
|
+
.input(z.object({ assignmentId: z.string(), eventId: z.string() }))
|
|
212
|
+
.mutation(({ ctx, input }) => attachAssignmentToEventRecord(ctx.user.id, input.assignmentId, input.eventId)),
|
|
213
|
+
detachEvent: protectedTeacherProcedure
|
|
214
|
+
.input(z.object({ assignmentId: z.string() }))
|
|
215
|
+
.mutation(({ ctx, input }) => detachAssignmentFromEventRecord(ctx.user.id, input.assignmentId)),
|
|
216
|
+
getAvailableEvents: protectedTeacherProcedure
|
|
217
|
+
.input(z.object({ assignmentId: z.string() }))
|
|
218
|
+
.query(({ ctx, input }) => getAvailableEventsForAssignment(ctx.user.id, input.assignmentId)),
|
|
219
|
+
dueToday: protectedProcedure
|
|
220
|
+
.query(() => getDueToday()),
|
|
221
|
+
attachMarkScheme: protectedTeacherProcedure
|
|
957
222
|
.input(z.object({
|
|
958
223
|
assignmentId: z.string(),
|
|
959
|
-
|
|
224
|
+
markSchemeId: z.string().nullable(),
|
|
960
225
|
}))
|
|
961
|
-
.
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
});
|
|
967
|
-
}
|
|
968
|
-
const { assignmentId } = input;
|
|
969
|
-
const submission = await prisma.submission.findFirst({
|
|
970
|
-
where: {
|
|
971
|
-
assignmentId,
|
|
972
|
-
studentId: ctx.user.id,
|
|
973
|
-
},
|
|
974
|
-
include: {
|
|
975
|
-
attachments: true,
|
|
976
|
-
student: {
|
|
977
|
-
select: {
|
|
978
|
-
id: true,
|
|
979
|
-
username: true,
|
|
980
|
-
profile: true,
|
|
981
|
-
},
|
|
982
|
-
},
|
|
983
|
-
assignment: {
|
|
984
|
-
include: {
|
|
985
|
-
class: true,
|
|
986
|
-
markScheme: {
|
|
987
|
-
select: {
|
|
988
|
-
id: true,
|
|
989
|
-
structured: true,
|
|
990
|
-
}
|
|
991
|
-
},
|
|
992
|
-
worksheets: {
|
|
993
|
-
select: {
|
|
994
|
-
id: true,
|
|
995
|
-
name: true,
|
|
996
|
-
}
|
|
997
|
-
},
|
|
998
|
-
gradingBoundary: {
|
|
999
|
-
select: {
|
|
1000
|
-
id: true,
|
|
1001
|
-
structured: true,
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
},
|
|
1005
|
-
},
|
|
1006
|
-
annotations: true,
|
|
1007
|
-
},
|
|
1008
|
-
});
|
|
1009
|
-
if (!submission) {
|
|
1010
|
-
// Create a new submission if it doesn't exist
|
|
1011
|
-
return await prisma.submission.create({
|
|
1012
|
-
data: {
|
|
1013
|
-
assignment: {
|
|
1014
|
-
connect: { id: assignmentId },
|
|
1015
|
-
},
|
|
1016
|
-
student: {
|
|
1017
|
-
connect: { id: ctx.user.id },
|
|
1018
|
-
},
|
|
1019
|
-
},
|
|
1020
|
-
include: {
|
|
1021
|
-
attachments: true,
|
|
1022
|
-
annotations: true,
|
|
1023
|
-
student: {
|
|
1024
|
-
select: {
|
|
1025
|
-
id: true,
|
|
1026
|
-
username: true,
|
|
1027
|
-
},
|
|
1028
|
-
},
|
|
1029
|
-
assignment: {
|
|
1030
|
-
include: {
|
|
1031
|
-
class: true,
|
|
1032
|
-
markScheme: {
|
|
1033
|
-
select: {
|
|
1034
|
-
id: true,
|
|
1035
|
-
structured: true,
|
|
1036
|
-
}
|
|
1037
|
-
},
|
|
1038
|
-
gradingBoundary: {
|
|
1039
|
-
select: {
|
|
1040
|
-
id: true,
|
|
1041
|
-
structured: true,
|
|
1042
|
-
}
|
|
1043
|
-
}
|
|
1044
|
-
},
|
|
1045
|
-
},
|
|
1046
|
-
},
|
|
1047
|
-
});
|
|
1048
|
-
}
|
|
1049
|
-
return {
|
|
1050
|
-
...submission,
|
|
1051
|
-
late: submission.assignment.dueDate < new Date(),
|
|
1052
|
-
};
|
|
1053
|
-
}),
|
|
1054
|
-
getSubmissionById: protectedClassMemberProcedure
|
|
226
|
+
.mutation(({ ctx, input }) => attachMarkSchemeRecord(ctx.user.id, input.assignmentId, input.markSchemeId)),
|
|
227
|
+
detachMarkScheme: protectedTeacherProcedure
|
|
228
|
+
.input(z.object({ assignmentId: z.string() }))
|
|
229
|
+
.mutation(({ ctx, input }) => detachMarkSchemeRecord(ctx.user.id, input.assignmentId)),
|
|
230
|
+
attachGradingBoundary: protectedTeacherProcedure
|
|
1055
231
|
.input(z.object({
|
|
1056
|
-
|
|
1057
|
-
|
|
232
|
+
assignmentId: z.string(),
|
|
233
|
+
gradingBoundaryId: z.string().nullable(),
|
|
1058
234
|
}))
|
|
1059
|
-
.
|
|
1060
|
-
const { submissionId, classId } = input;
|
|
1061
|
-
const submission = await prisma.submission.findFirst({
|
|
1062
|
-
where: {
|
|
1063
|
-
id: submissionId,
|
|
1064
|
-
assignment: {
|
|
1065
|
-
classId,
|
|
1066
|
-
class: {
|
|
1067
|
-
OR: [
|
|
1068
|
-
{
|
|
1069
|
-
teachers: {
|
|
1070
|
-
some: {
|
|
1071
|
-
id: ctx.user?.id
|
|
1072
|
-
}
|
|
1073
|
-
}
|
|
1074
|
-
},
|
|
1075
|
-
{
|
|
1076
|
-
students: {
|
|
1077
|
-
some: {
|
|
1078
|
-
id: ctx.user?.id
|
|
1079
|
-
}
|
|
1080
|
-
}
|
|
1081
|
-
}
|
|
1082
|
-
],
|
|
1083
|
-
}
|
|
1084
|
-
},
|
|
1085
|
-
},
|
|
1086
|
-
include: {
|
|
1087
|
-
attachments: true,
|
|
1088
|
-
annotations: true,
|
|
1089
|
-
student: {
|
|
1090
|
-
select: {
|
|
1091
|
-
id: true,
|
|
1092
|
-
username: true,
|
|
1093
|
-
profile: true,
|
|
1094
|
-
},
|
|
1095
|
-
},
|
|
1096
|
-
assignment: {
|
|
1097
|
-
include: {
|
|
1098
|
-
class: true,
|
|
1099
|
-
markScheme: {
|
|
1100
|
-
select: {
|
|
1101
|
-
id: true,
|
|
1102
|
-
structured: true,
|
|
1103
|
-
}
|
|
1104
|
-
},
|
|
1105
|
-
gradingBoundary: {
|
|
1106
|
-
select: {
|
|
1107
|
-
id: true,
|
|
1108
|
-
structured: true,
|
|
1109
|
-
}
|
|
1110
|
-
},
|
|
1111
|
-
worksheets: {
|
|
1112
|
-
select: {
|
|
1113
|
-
id: true,
|
|
1114
|
-
name: true,
|
|
1115
|
-
}
|
|
1116
|
-
}
|
|
1117
|
-
},
|
|
1118
|
-
},
|
|
1119
|
-
},
|
|
1120
|
-
});
|
|
1121
|
-
if (!submission) {
|
|
1122
|
-
throw new TRPCError({
|
|
1123
|
-
code: "NOT_FOUND",
|
|
1124
|
-
message: "Submission not found",
|
|
1125
|
-
});
|
|
1126
|
-
}
|
|
1127
|
-
return {
|
|
1128
|
-
...submission,
|
|
1129
|
-
late: submission.assignment.dueDate < new Date(),
|
|
1130
|
-
};
|
|
1131
|
-
}),
|
|
1132
|
-
updateSubmission: protectedClassMemberProcedure
|
|
1133
|
-
.input(submissionSchema)
|
|
1134
|
-
.mutation(async ({ ctx, input }) => {
|
|
1135
|
-
if (!ctx.user) {
|
|
1136
|
-
throw new TRPCError({
|
|
1137
|
-
code: "UNAUTHORIZED",
|
|
1138
|
-
message: "User must be authenticated",
|
|
1139
|
-
});
|
|
1140
|
-
}
|
|
1141
|
-
const { submissionId, submit, newAttachments, existingFileIds, removedAttachments, extendedResponse } = input;
|
|
1142
|
-
const submission = await prisma.submission.findFirst({
|
|
1143
|
-
where: {
|
|
1144
|
-
id: submissionId,
|
|
1145
|
-
OR: [
|
|
1146
|
-
{
|
|
1147
|
-
student: {
|
|
1148
|
-
id: ctx.user.id,
|
|
1149
|
-
},
|
|
1150
|
-
},
|
|
1151
|
-
{
|
|
1152
|
-
assignment: {
|
|
1153
|
-
class: {
|
|
1154
|
-
teachers: {
|
|
1155
|
-
some: {
|
|
1156
|
-
id: ctx.user.id,
|
|
1157
|
-
},
|
|
1158
|
-
},
|
|
1159
|
-
},
|
|
1160
|
-
},
|
|
1161
|
-
},
|
|
1162
|
-
],
|
|
1163
|
-
},
|
|
1164
|
-
include: {
|
|
1165
|
-
attachments: {
|
|
1166
|
-
include: {
|
|
1167
|
-
thumbnail: true
|
|
1168
|
-
}
|
|
1169
|
-
},
|
|
1170
|
-
assignment: {
|
|
1171
|
-
include: {
|
|
1172
|
-
class: true,
|
|
1173
|
-
markScheme: {
|
|
1174
|
-
select: {
|
|
1175
|
-
id: true,
|
|
1176
|
-
structured: true,
|
|
1177
|
-
}
|
|
1178
|
-
},
|
|
1179
|
-
gradingBoundary: {
|
|
1180
|
-
select: {
|
|
1181
|
-
id: true,
|
|
1182
|
-
structured: true,
|
|
1183
|
-
}
|
|
1184
|
-
}
|
|
1185
|
-
},
|
|
1186
|
-
},
|
|
1187
|
-
},
|
|
1188
|
-
});
|
|
1189
|
-
if (!submission) {
|
|
1190
|
-
throw new TRPCError({
|
|
1191
|
-
code: "NOT_FOUND",
|
|
1192
|
-
message: "Submission not found",
|
|
1193
|
-
});
|
|
1194
|
-
}
|
|
1195
|
-
if (submit !== undefined) {
|
|
1196
|
-
// Toggle submission status
|
|
1197
|
-
if (submission.assignment.acceptWorksheet && submission.assignment.gradeWithAI) {
|
|
1198
|
-
// Grade the submission with AI
|
|
1199
|
-
const worksheetResponses = await prisma.studentWorksheetResponse.findMany({
|
|
1200
|
-
where: {
|
|
1201
|
-
submissionId: submission.id,
|
|
1202
|
-
},
|
|
1203
|
-
});
|
|
1204
|
-
for (const worksheetResponse of worksheetResponses) {
|
|
1205
|
-
// Run it in the background, non-blocking
|
|
1206
|
-
gradeWorksheetPipeline(worksheetResponse.id);
|
|
1207
|
-
}
|
|
1208
|
-
}
|
|
1209
|
-
return await prisma.submission.update({
|
|
1210
|
-
where: { id: submission.id },
|
|
1211
|
-
data: {
|
|
1212
|
-
submitted: !submission.submitted,
|
|
1213
|
-
submittedAt: new Date(),
|
|
1214
|
-
},
|
|
1215
|
-
include: {
|
|
1216
|
-
attachments: true,
|
|
1217
|
-
student: {
|
|
1218
|
-
select: {
|
|
1219
|
-
id: true,
|
|
1220
|
-
username: true,
|
|
1221
|
-
},
|
|
1222
|
-
},
|
|
1223
|
-
assignment: {
|
|
1224
|
-
include: {
|
|
1225
|
-
class: true,
|
|
1226
|
-
markScheme: {
|
|
1227
|
-
select: {
|
|
1228
|
-
id: true,
|
|
1229
|
-
structured: true,
|
|
1230
|
-
}
|
|
1231
|
-
},
|
|
1232
|
-
gradingBoundary: {
|
|
1233
|
-
select: {
|
|
1234
|
-
id: true,
|
|
1235
|
-
structured: true,
|
|
1236
|
-
}
|
|
1237
|
-
}
|
|
1238
|
-
},
|
|
1239
|
-
},
|
|
1240
|
-
},
|
|
1241
|
-
});
|
|
1242
|
-
}
|
|
1243
|
-
let uploadedFiles = [];
|
|
1244
|
-
if (newAttachments && newAttachments.length > 0) {
|
|
1245
|
-
// Store files in a class and assignment specific directory
|
|
1246
|
-
uploadedFiles = await createDirectUploadFiles(newAttachments, ctx.user.id, undefined, undefined, submission.id);
|
|
1247
|
-
}
|
|
1248
|
-
// Update submission with new file attachments
|
|
1249
|
-
if (uploadedFiles.length > 0) {
|
|
1250
|
-
await prisma.submission.update({
|
|
1251
|
-
where: { id: submission.id },
|
|
1252
|
-
data: {
|
|
1253
|
-
attachments: {
|
|
1254
|
-
create: uploadedFiles.map(file => ({
|
|
1255
|
-
name: file.name,
|
|
1256
|
-
type: file.type,
|
|
1257
|
-
size: file.size,
|
|
1258
|
-
path: file.path,
|
|
1259
|
-
...(file.thumbnailId && {
|
|
1260
|
-
thumbnail: {
|
|
1261
|
-
connect: { id: file.thumbnailId }
|
|
1262
|
-
}
|
|
1263
|
-
})
|
|
1264
|
-
}))
|
|
1265
|
-
}
|
|
1266
|
-
}
|
|
1267
|
-
});
|
|
1268
|
-
}
|
|
1269
|
-
// Connect existing files if provided
|
|
1270
|
-
if (existingFileIds && existingFileIds.length > 0) {
|
|
1271
|
-
await prisma.submission.update({
|
|
1272
|
-
where: { id: submission.id },
|
|
1273
|
-
data: {
|
|
1274
|
-
attachments: {
|
|
1275
|
-
connect: existingFileIds.map(fileId => ({ id: fileId }))
|
|
1276
|
-
}
|
|
1277
|
-
}
|
|
1278
|
-
});
|
|
1279
|
-
}
|
|
1280
|
-
// Delete removed attachments if any
|
|
1281
|
-
if (removedAttachments && removedAttachments.length > 0) {
|
|
1282
|
-
const filesToDelete = submission.attachments.filter((file) => removedAttachments.includes(file.id));
|
|
1283
|
-
// Delete files from storage (only if they were actually uploaded)
|
|
1284
|
-
await Promise.all(filesToDelete.map(async (file) => {
|
|
1285
|
-
try {
|
|
1286
|
-
// Only delete from GCS if the file was successfully uploaded
|
|
1287
|
-
if (file.uploadStatus === 'COMPLETED') {
|
|
1288
|
-
// Delete the main file
|
|
1289
|
-
await deleteFile(file.path);
|
|
1290
|
-
// Delete thumbnail if it exists
|
|
1291
|
-
if (file.thumbnail?.path) {
|
|
1292
|
-
await deleteFile(file.thumbnail.path);
|
|
1293
|
-
}
|
|
1294
|
-
}
|
|
1295
|
-
}
|
|
1296
|
-
catch (error) {
|
|
1297
|
-
console.warn(`Failed to delete file ${file.path}:`, error);
|
|
1298
|
-
}
|
|
1299
|
-
}));
|
|
1300
|
-
}
|
|
1301
|
-
// Update submission with attachments
|
|
1302
|
-
return await prisma.submission.update({
|
|
1303
|
-
where: { id: submission.id },
|
|
1304
|
-
data: {
|
|
1305
|
-
...(removedAttachments && removedAttachments.length > 0 && {
|
|
1306
|
-
attachments: {
|
|
1307
|
-
deleteMany: {
|
|
1308
|
-
id: { in: removedAttachments },
|
|
1309
|
-
},
|
|
1310
|
-
},
|
|
1311
|
-
}),
|
|
1312
|
-
...(extendedResponse !== undefined && { extendedResponse }),
|
|
1313
|
-
},
|
|
1314
|
-
include: {
|
|
1315
|
-
attachments: {
|
|
1316
|
-
include: {
|
|
1317
|
-
thumbnail: true
|
|
1318
|
-
}
|
|
1319
|
-
},
|
|
1320
|
-
student: {
|
|
1321
|
-
select: {
|
|
1322
|
-
id: true,
|
|
1323
|
-
username: true,
|
|
1324
|
-
},
|
|
1325
|
-
},
|
|
1326
|
-
assignment: {
|
|
1327
|
-
include: {
|
|
1328
|
-
class: true,
|
|
1329
|
-
markScheme: {
|
|
1330
|
-
select: {
|
|
1331
|
-
id: true,
|
|
1332
|
-
structured: true,
|
|
1333
|
-
}
|
|
1334
|
-
},
|
|
1335
|
-
gradingBoundary: {
|
|
1336
|
-
select: {
|
|
1337
|
-
id: true,
|
|
1338
|
-
structured: true,
|
|
1339
|
-
}
|
|
1340
|
-
}
|
|
1341
|
-
},
|
|
1342
|
-
},
|
|
1343
|
-
},
|
|
1344
|
-
});
|
|
1345
|
-
}),
|
|
1346
|
-
getSubmissions: protectedTeacherProcedure
|
|
1347
|
-
.input(z.object({
|
|
1348
|
-
assignmentId: z.string(),
|
|
1349
|
-
classId: z.string(),
|
|
1350
|
-
}))
|
|
1351
|
-
.query(async ({ ctx, input }) => {
|
|
1352
|
-
if (!ctx.user) {
|
|
1353
|
-
throw new TRPCError({
|
|
1354
|
-
code: "UNAUTHORIZED",
|
|
1355
|
-
message: "User must be authenticated",
|
|
1356
|
-
});
|
|
1357
|
-
}
|
|
1358
|
-
const { assignmentId } = input;
|
|
1359
|
-
const submissions = await prisma.submission.findMany({
|
|
1360
|
-
where: {
|
|
1361
|
-
assignment: {
|
|
1362
|
-
id: assignmentId,
|
|
1363
|
-
class: {
|
|
1364
|
-
teachers: {
|
|
1365
|
-
some: { id: ctx.user.id },
|
|
1366
|
-
},
|
|
1367
|
-
},
|
|
1368
|
-
},
|
|
1369
|
-
},
|
|
1370
|
-
include: {
|
|
1371
|
-
attachments: {
|
|
1372
|
-
include: {
|
|
1373
|
-
thumbnail: true
|
|
1374
|
-
}
|
|
1375
|
-
},
|
|
1376
|
-
student: {
|
|
1377
|
-
select: {
|
|
1378
|
-
id: true,
|
|
1379
|
-
username: true,
|
|
1380
|
-
profile: {
|
|
1381
|
-
select: {
|
|
1382
|
-
displayName: true,
|
|
1383
|
-
profilePicture: true,
|
|
1384
|
-
profilePictureThumbnail: true,
|
|
1385
|
-
},
|
|
1386
|
-
},
|
|
1387
|
-
},
|
|
1388
|
-
},
|
|
1389
|
-
assignment: {
|
|
1390
|
-
include: {
|
|
1391
|
-
class: true,
|
|
1392
|
-
markScheme: {
|
|
1393
|
-
select: {
|
|
1394
|
-
id: true,
|
|
1395
|
-
structured: true,
|
|
1396
|
-
}
|
|
1397
|
-
},
|
|
1398
|
-
gradingBoundary: {
|
|
1399
|
-
select: {
|
|
1400
|
-
id: true,
|
|
1401
|
-
structured: true,
|
|
1402
|
-
}
|
|
1403
|
-
}
|
|
1404
|
-
},
|
|
1405
|
-
},
|
|
1406
|
-
},
|
|
1407
|
-
});
|
|
1408
|
-
return submissions.map(submission => ({
|
|
1409
|
-
...submission,
|
|
1410
|
-
late: submission.assignment.dueDate < new Date(),
|
|
1411
|
-
}));
|
|
1412
|
-
}),
|
|
1413
|
-
updateSubmissionAsTeacher: protectedTeacherProcedure
|
|
1414
|
-
.input(updateSubmissionSchema)
|
|
1415
|
-
.mutation(async ({ ctx, input }) => {
|
|
1416
|
-
if (!ctx.user) {
|
|
1417
|
-
throw new TRPCError({
|
|
1418
|
-
code: "UNAUTHORIZED",
|
|
1419
|
-
message: "User must be authenticated",
|
|
1420
|
-
});
|
|
1421
|
-
}
|
|
1422
|
-
const { submissionId, return: returnSubmission, gradeReceived, newAttachments, existingFileIds, removedAttachments, rubricGrades, feedback } = input;
|
|
1423
|
-
const submission = await prisma.submission.findFirst({
|
|
1424
|
-
where: {
|
|
1425
|
-
id: submissionId,
|
|
1426
|
-
assignment: {
|
|
1427
|
-
class: {
|
|
1428
|
-
teachers: {
|
|
1429
|
-
some: { id: ctx.user.id },
|
|
1430
|
-
},
|
|
1431
|
-
},
|
|
1432
|
-
},
|
|
1433
|
-
},
|
|
1434
|
-
include: {
|
|
1435
|
-
attachments: {
|
|
1436
|
-
include: {
|
|
1437
|
-
thumbnail: true
|
|
1438
|
-
}
|
|
1439
|
-
},
|
|
1440
|
-
annotations: {
|
|
1441
|
-
include: {
|
|
1442
|
-
thumbnail: true
|
|
1443
|
-
}
|
|
1444
|
-
},
|
|
1445
|
-
assignment: {
|
|
1446
|
-
include: {
|
|
1447
|
-
class: true,
|
|
1448
|
-
markScheme: {
|
|
1449
|
-
select: {
|
|
1450
|
-
id: true,
|
|
1451
|
-
structured: true,
|
|
1452
|
-
}
|
|
1453
|
-
},
|
|
1454
|
-
gradingBoundary: {
|
|
1455
|
-
select: {
|
|
1456
|
-
id: true,
|
|
1457
|
-
structured: true,
|
|
1458
|
-
}
|
|
1459
|
-
}
|
|
1460
|
-
},
|
|
1461
|
-
},
|
|
1462
|
-
},
|
|
1463
|
-
});
|
|
1464
|
-
if (!submission) {
|
|
1465
|
-
throw new TRPCError({
|
|
1466
|
-
code: "NOT_FOUND",
|
|
1467
|
-
message: "Submission not found",
|
|
1468
|
-
});
|
|
1469
|
-
}
|
|
1470
|
-
if (returnSubmission !== undefined) {
|
|
1471
|
-
// Toggle return status
|
|
1472
|
-
return await prisma.submission.update({
|
|
1473
|
-
where: { id: submissionId },
|
|
1474
|
-
data: {
|
|
1475
|
-
returned: !submission.returned,
|
|
1476
|
-
},
|
|
1477
|
-
include: {
|
|
1478
|
-
attachments: true,
|
|
1479
|
-
student: {
|
|
1480
|
-
select: {
|
|
1481
|
-
id: true,
|
|
1482
|
-
username: true,
|
|
1483
|
-
profile: {
|
|
1484
|
-
select: {
|
|
1485
|
-
displayName: true,
|
|
1486
|
-
profilePicture: true,
|
|
1487
|
-
profilePictureThumbnail: true,
|
|
1488
|
-
},
|
|
1489
|
-
},
|
|
1490
|
-
},
|
|
1491
|
-
},
|
|
1492
|
-
assignment: {
|
|
1493
|
-
include: {
|
|
1494
|
-
class: true,
|
|
1495
|
-
markScheme: {
|
|
1496
|
-
select: {
|
|
1497
|
-
id: true,
|
|
1498
|
-
structured: true,
|
|
1499
|
-
}
|
|
1500
|
-
},
|
|
1501
|
-
gradingBoundary: {
|
|
1502
|
-
select: {
|
|
1503
|
-
id: true,
|
|
1504
|
-
structured: true,
|
|
1505
|
-
}
|
|
1506
|
-
}
|
|
1507
|
-
},
|
|
1508
|
-
},
|
|
1509
|
-
},
|
|
1510
|
-
});
|
|
1511
|
-
}
|
|
1512
|
-
// NOTE: Teacher annotation files are now handled via direct upload endpoints
|
|
1513
|
-
// Use getAnnotationUploadUrls and confirmAnnotationUpload endpoints instead
|
|
1514
|
-
// The newAttachments field is deprecated for annotations
|
|
1515
|
-
if (newAttachments && newAttachments.length > 0) {
|
|
1516
|
-
throw new TRPCError({
|
|
1517
|
-
code: "BAD_REQUEST",
|
|
1518
|
-
message: "Direct file upload is deprecated. Use getAnnotationUploadUrls endpoint instead.",
|
|
1519
|
-
});
|
|
1520
|
-
}
|
|
1521
|
-
// Connect existing files if provided
|
|
1522
|
-
if (existingFileIds && existingFileIds.length > 0) {
|
|
1523
|
-
await prisma.submission.update({
|
|
1524
|
-
where: { id: submission.id },
|
|
1525
|
-
data: {
|
|
1526
|
-
annotations: {
|
|
1527
|
-
connect: existingFileIds.map(fileId => ({ id: fileId }))
|
|
1528
|
-
}
|
|
1529
|
-
}
|
|
1530
|
-
});
|
|
1531
|
-
}
|
|
1532
|
-
// Delete removed attachments if any
|
|
1533
|
-
if (removedAttachments && removedAttachments.length > 0) {
|
|
1534
|
-
const filesToDelete = submission.annotations.filter((file) => removedAttachments.includes(file.id));
|
|
1535
|
-
// Delete files from storage (only if they were actually uploaded)
|
|
1536
|
-
await Promise.all(filesToDelete.map(async (file) => {
|
|
1537
|
-
try {
|
|
1538
|
-
// Only delete from GCS if the file was successfully uploaded
|
|
1539
|
-
if (file.uploadStatus === 'COMPLETED') {
|
|
1540
|
-
// Delete the main file
|
|
1541
|
-
await deleteFile(file.path);
|
|
1542
|
-
// Delete thumbnail if it exists
|
|
1543
|
-
if (file.thumbnail?.path) {
|
|
1544
|
-
await deleteFile(file.thumbnail.path);
|
|
1545
|
-
}
|
|
1546
|
-
}
|
|
1547
|
-
}
|
|
1548
|
-
catch (error) {
|
|
1549
|
-
console.warn(`Failed to delete file ${file.path}:`, error);
|
|
1550
|
-
}
|
|
1551
|
-
}));
|
|
1552
|
-
}
|
|
1553
|
-
// Update submission with grade and attachments
|
|
1554
|
-
return await prisma.submission.update({
|
|
1555
|
-
where: { id: submissionId },
|
|
1556
|
-
data: {
|
|
1557
|
-
...(gradeReceived !== undefined && { gradeReceived }),
|
|
1558
|
-
...(rubricGrades && { rubricState: JSON.stringify(rubricGrades) }),
|
|
1559
|
-
...(feedback && { teacherComments: feedback }),
|
|
1560
|
-
...(removedAttachments && removedAttachments.length > 0 && {
|
|
1561
|
-
annotations: {
|
|
1562
|
-
deleteMany: {
|
|
1563
|
-
id: { in: removedAttachments },
|
|
1564
|
-
},
|
|
1565
|
-
},
|
|
1566
|
-
}),
|
|
1567
|
-
...(returnSubmission && { returned: returnSubmission }),
|
|
1568
|
-
},
|
|
1569
|
-
include: {
|
|
1570
|
-
attachments: {
|
|
1571
|
-
include: {
|
|
1572
|
-
thumbnail: true
|
|
1573
|
-
}
|
|
1574
|
-
},
|
|
1575
|
-
annotations: {
|
|
1576
|
-
include: {
|
|
1577
|
-
thumbnail: true
|
|
1578
|
-
}
|
|
1579
|
-
},
|
|
1580
|
-
student: {
|
|
1581
|
-
select: {
|
|
1582
|
-
id: true,
|
|
1583
|
-
username: true,
|
|
1584
|
-
profile: {
|
|
1585
|
-
select: {
|
|
1586
|
-
displayName: true,
|
|
1587
|
-
profilePicture: true,
|
|
1588
|
-
profilePictureThumbnail: true,
|
|
1589
|
-
},
|
|
1590
|
-
},
|
|
1591
|
-
},
|
|
1592
|
-
},
|
|
1593
|
-
assignment: {
|
|
1594
|
-
include: {
|
|
1595
|
-
class: true,
|
|
1596
|
-
markScheme: {
|
|
1597
|
-
select: {
|
|
1598
|
-
id: true,
|
|
1599
|
-
structured: true,
|
|
1600
|
-
}
|
|
1601
|
-
},
|
|
1602
|
-
gradingBoundary: {
|
|
1603
|
-
select: {
|
|
1604
|
-
id: true,
|
|
1605
|
-
structured: true,
|
|
1606
|
-
}
|
|
1607
|
-
}
|
|
1608
|
-
},
|
|
1609
|
-
},
|
|
1610
|
-
},
|
|
1611
|
-
});
|
|
1612
|
-
}),
|
|
1613
|
-
attachToEvent: protectedTeacherProcedure
|
|
1614
|
-
.input(z.object({
|
|
1615
|
-
assignmentId: z.string(),
|
|
1616
|
-
eventId: z.string(),
|
|
1617
|
-
}))
|
|
1618
|
-
.mutation(async ({ ctx, input }) => {
|
|
1619
|
-
if (!ctx.user) {
|
|
1620
|
-
throw new TRPCError({
|
|
1621
|
-
code: "UNAUTHORIZED",
|
|
1622
|
-
message: "User must be authenticated",
|
|
1623
|
-
});
|
|
1624
|
-
}
|
|
1625
|
-
const { assignmentId, eventId } = input;
|
|
1626
|
-
// Check if assignment exists and user is a teacher of the class
|
|
1627
|
-
const assignment = await prisma.assignment.findFirst({
|
|
1628
|
-
where: {
|
|
1629
|
-
id: assignmentId,
|
|
1630
|
-
class: {
|
|
1631
|
-
teachers: {
|
|
1632
|
-
some: { id: ctx.user.id },
|
|
1633
|
-
},
|
|
1634
|
-
},
|
|
1635
|
-
},
|
|
1636
|
-
include: {
|
|
1637
|
-
class: true,
|
|
1638
|
-
},
|
|
1639
|
-
});
|
|
1640
|
-
if (!assignment) {
|
|
1641
|
-
throw new TRPCError({
|
|
1642
|
-
code: "NOT_FOUND",
|
|
1643
|
-
message: "Assignment not found or you are not authorized",
|
|
1644
|
-
});
|
|
1645
|
-
}
|
|
1646
|
-
// Check if event exists and belongs to the same class
|
|
1647
|
-
const event = await prisma.event.findFirst({
|
|
1648
|
-
where: {
|
|
1649
|
-
id: eventId,
|
|
1650
|
-
classId: assignment.classId,
|
|
1651
|
-
},
|
|
1652
|
-
});
|
|
1653
|
-
if (!event) {
|
|
1654
|
-
throw new TRPCError({
|
|
1655
|
-
code: "NOT_FOUND",
|
|
1656
|
-
message: "Event not found or does not belong to the same class",
|
|
1657
|
-
});
|
|
1658
|
-
}
|
|
1659
|
-
// Attach assignment to event
|
|
1660
|
-
const updatedAssignment = await prisma.assignment.update({
|
|
1661
|
-
where: { id: assignmentId },
|
|
1662
|
-
data: {
|
|
1663
|
-
eventAttached: {
|
|
1664
|
-
connect: { id: eventId }
|
|
1665
|
-
}
|
|
1666
|
-
},
|
|
1667
|
-
include: {
|
|
1668
|
-
attachments: {
|
|
1669
|
-
select: {
|
|
1670
|
-
id: true,
|
|
1671
|
-
name: true,
|
|
1672
|
-
type: true,
|
|
1673
|
-
}
|
|
1674
|
-
},
|
|
1675
|
-
section: {
|
|
1676
|
-
select: {
|
|
1677
|
-
id: true,
|
|
1678
|
-
name: true
|
|
1679
|
-
}
|
|
1680
|
-
},
|
|
1681
|
-
teacher: {
|
|
1682
|
-
select: {
|
|
1683
|
-
id: true,
|
|
1684
|
-
username: true
|
|
1685
|
-
}
|
|
1686
|
-
},
|
|
1687
|
-
eventAttached: {
|
|
1688
|
-
select: {
|
|
1689
|
-
id: true,
|
|
1690
|
-
name: true,
|
|
1691
|
-
startTime: true,
|
|
1692
|
-
endTime: true,
|
|
1693
|
-
color: true,
|
|
1694
|
-
}
|
|
1695
|
-
}
|
|
1696
|
-
}
|
|
1697
|
-
});
|
|
1698
|
-
return { assignment: updatedAssignment };
|
|
1699
|
-
}),
|
|
1700
|
-
detachEvent: protectedTeacherProcedure
|
|
1701
|
-
.input(z.object({
|
|
1702
|
-
assignmentId: z.string(),
|
|
1703
|
-
}))
|
|
1704
|
-
.mutation(async ({ ctx, input }) => {
|
|
1705
|
-
if (!ctx.user) {
|
|
1706
|
-
throw new TRPCError({
|
|
1707
|
-
code: "UNAUTHORIZED",
|
|
1708
|
-
message: "User must be authenticated",
|
|
1709
|
-
});
|
|
1710
|
-
}
|
|
1711
|
-
const { assignmentId } = input;
|
|
1712
|
-
// Check if assignment exists and user is a teacher of the class
|
|
1713
|
-
const assignment = await prisma.assignment.findFirst({
|
|
1714
|
-
where: {
|
|
1715
|
-
id: assignmentId,
|
|
1716
|
-
class: {
|
|
1717
|
-
teachers: {
|
|
1718
|
-
some: { id: ctx.user.id },
|
|
1719
|
-
},
|
|
1720
|
-
},
|
|
1721
|
-
},
|
|
1722
|
-
});
|
|
1723
|
-
if (!assignment) {
|
|
1724
|
-
throw new TRPCError({
|
|
1725
|
-
code: "NOT_FOUND",
|
|
1726
|
-
message: "Assignment not found or you are not authorized",
|
|
1727
|
-
});
|
|
1728
|
-
}
|
|
1729
|
-
// Detach assignment from event
|
|
1730
|
-
const updatedAssignment = await prisma.assignment.update({
|
|
1731
|
-
where: { id: assignmentId },
|
|
1732
|
-
data: {
|
|
1733
|
-
eventAttached: {
|
|
1734
|
-
disconnect: true
|
|
1735
|
-
}
|
|
1736
|
-
},
|
|
1737
|
-
include: {
|
|
1738
|
-
attachments: {
|
|
1739
|
-
select: {
|
|
1740
|
-
id: true,
|
|
1741
|
-
name: true,
|
|
1742
|
-
type: true,
|
|
1743
|
-
}
|
|
1744
|
-
},
|
|
1745
|
-
section: {
|
|
1746
|
-
select: {
|
|
1747
|
-
id: true,
|
|
1748
|
-
name: true
|
|
1749
|
-
}
|
|
1750
|
-
},
|
|
1751
|
-
teacher: {
|
|
1752
|
-
select: {
|
|
1753
|
-
id: true,
|
|
1754
|
-
username: true
|
|
1755
|
-
}
|
|
1756
|
-
},
|
|
1757
|
-
eventAttached: {
|
|
1758
|
-
select: {
|
|
1759
|
-
id: true,
|
|
1760
|
-
name: true,
|
|
1761
|
-
startTime: true,
|
|
1762
|
-
endTime: true,
|
|
1763
|
-
}
|
|
1764
|
-
}
|
|
1765
|
-
}
|
|
1766
|
-
});
|
|
1767
|
-
return { assignment: updatedAssignment };
|
|
1768
|
-
}),
|
|
1769
|
-
getAvailableEvents: protectedTeacherProcedure
|
|
1770
|
-
.input(z.object({
|
|
1771
|
-
assignmentId: z.string(),
|
|
1772
|
-
}))
|
|
1773
|
-
.query(async ({ ctx, input }) => {
|
|
1774
|
-
if (!ctx.user) {
|
|
1775
|
-
throw new TRPCError({
|
|
1776
|
-
code: "UNAUTHORIZED",
|
|
1777
|
-
message: "User must be authenticated",
|
|
1778
|
-
});
|
|
1779
|
-
}
|
|
1780
|
-
const { assignmentId } = input;
|
|
1781
|
-
// Get the assignment to find the class
|
|
1782
|
-
const assignment = await prisma.assignment.findFirst({
|
|
1783
|
-
where: {
|
|
1784
|
-
id: assignmentId,
|
|
1785
|
-
class: {
|
|
1786
|
-
teachers: {
|
|
1787
|
-
some: { id: ctx.user.id },
|
|
1788
|
-
},
|
|
1789
|
-
},
|
|
1790
|
-
},
|
|
1791
|
-
select: { classId: true }
|
|
1792
|
-
});
|
|
1793
|
-
if (!assignment) {
|
|
1794
|
-
throw new TRPCError({
|
|
1795
|
-
code: "NOT_FOUND",
|
|
1796
|
-
message: "Assignment not found or you are not authorized",
|
|
1797
|
-
});
|
|
1798
|
-
}
|
|
1799
|
-
// Get all events for the class that don't already have this assignment attached
|
|
1800
|
-
const events = await prisma.event.findMany({
|
|
1801
|
-
where: {
|
|
1802
|
-
classId: assignment.classId,
|
|
1803
|
-
assignmentsAttached: {
|
|
1804
|
-
none: {
|
|
1805
|
-
id: assignmentId
|
|
1806
|
-
}
|
|
1807
|
-
}
|
|
1808
|
-
},
|
|
1809
|
-
select: {
|
|
1810
|
-
id: true,
|
|
1811
|
-
name: true,
|
|
1812
|
-
startTime: true,
|
|
1813
|
-
endTime: true,
|
|
1814
|
-
location: true,
|
|
1815
|
-
remarks: true,
|
|
1816
|
-
color: true,
|
|
1817
|
-
},
|
|
1818
|
-
orderBy: {
|
|
1819
|
-
startTime: 'asc'
|
|
1820
|
-
}
|
|
1821
|
-
});
|
|
1822
|
-
return { events };
|
|
1823
|
-
}),
|
|
1824
|
-
dueToday: protectedProcedure
|
|
1825
|
-
.query(async ({ ctx }) => {
|
|
1826
|
-
if (!ctx.user) {
|
|
1827
|
-
throw new TRPCError({
|
|
1828
|
-
code: "UNAUTHORIZED",
|
|
1829
|
-
message: "User must be authenticated",
|
|
1830
|
-
});
|
|
1831
|
-
}
|
|
1832
|
-
const assignments = await prisma.assignment.findMany({
|
|
1833
|
-
where: {
|
|
1834
|
-
dueDate: {
|
|
1835
|
-
equals: new Date(),
|
|
1836
|
-
},
|
|
1837
|
-
},
|
|
1838
|
-
select: {
|
|
1839
|
-
id: true,
|
|
1840
|
-
title: true,
|
|
1841
|
-
dueDate: true,
|
|
1842
|
-
type: true,
|
|
1843
|
-
graded: true,
|
|
1844
|
-
maxGrade: true,
|
|
1845
|
-
class: {
|
|
1846
|
-
select: {
|
|
1847
|
-
id: true,
|
|
1848
|
-
name: true,
|
|
1849
|
-
}
|
|
1850
|
-
}
|
|
1851
|
-
}
|
|
1852
|
-
});
|
|
1853
|
-
return assignments.map(assignment => ({
|
|
1854
|
-
...assignment,
|
|
1855
|
-
dueDate: assignment.dueDate.toISOString(),
|
|
1856
|
-
}));
|
|
1857
|
-
}),
|
|
1858
|
-
attachMarkScheme: protectedTeacherProcedure
|
|
1859
|
-
.input(z.object({
|
|
1860
|
-
assignmentId: z.string(),
|
|
1861
|
-
markSchemeId: z.string().nullable(),
|
|
1862
|
-
}))
|
|
1863
|
-
.mutation(async ({ ctx, input }) => {
|
|
1864
|
-
const { assignmentId, markSchemeId } = input;
|
|
1865
|
-
const assignment = await prisma.assignment.findFirst({
|
|
1866
|
-
where: {
|
|
1867
|
-
id: assignmentId,
|
|
1868
|
-
},
|
|
1869
|
-
});
|
|
1870
|
-
if (!assignment) {
|
|
1871
|
-
throw new TRPCError({
|
|
1872
|
-
code: "NOT_FOUND",
|
|
1873
|
-
message: "Assignment not found",
|
|
1874
|
-
});
|
|
1875
|
-
}
|
|
1876
|
-
// If markSchemeId is provided, verify it exists
|
|
1877
|
-
if (markSchemeId) {
|
|
1878
|
-
const markScheme = await prisma.markScheme.findFirst({
|
|
1879
|
-
where: {
|
|
1880
|
-
id: markSchemeId,
|
|
1881
|
-
},
|
|
1882
|
-
});
|
|
1883
|
-
if (!markScheme) {
|
|
1884
|
-
throw new TRPCError({
|
|
1885
|
-
code: "NOT_FOUND",
|
|
1886
|
-
message: "Mark scheme not found",
|
|
1887
|
-
});
|
|
1888
|
-
}
|
|
1889
|
-
}
|
|
1890
|
-
const updatedAssignment = await prisma.assignment.update({
|
|
1891
|
-
where: { id: assignmentId },
|
|
1892
|
-
data: {
|
|
1893
|
-
markScheme: markSchemeId ? {
|
|
1894
|
-
connect: { id: markSchemeId },
|
|
1895
|
-
} : {
|
|
1896
|
-
disconnect: true,
|
|
1897
|
-
},
|
|
1898
|
-
},
|
|
1899
|
-
include: {
|
|
1900
|
-
attachments: true,
|
|
1901
|
-
section: true,
|
|
1902
|
-
teacher: true,
|
|
1903
|
-
eventAttached: true,
|
|
1904
|
-
markScheme: true,
|
|
1905
|
-
},
|
|
1906
|
-
});
|
|
1907
|
-
return updatedAssignment;
|
|
1908
|
-
}),
|
|
1909
|
-
detachMarkScheme: protectedTeacherProcedure
|
|
1910
|
-
.input(z.object({
|
|
1911
|
-
assignmentId: z.string(),
|
|
1912
|
-
}))
|
|
1913
|
-
.mutation(async ({ ctx, input }) => {
|
|
1914
|
-
const { assignmentId } = input;
|
|
1915
|
-
const assignment = await prisma.assignment.findFirst({
|
|
1916
|
-
where: {
|
|
1917
|
-
id: assignmentId,
|
|
1918
|
-
},
|
|
1919
|
-
});
|
|
1920
|
-
if (!assignment) {
|
|
1921
|
-
throw new TRPCError({
|
|
1922
|
-
code: "NOT_FOUND",
|
|
1923
|
-
message: "Assignment not found",
|
|
1924
|
-
});
|
|
1925
|
-
}
|
|
1926
|
-
const updatedAssignment = await prisma.assignment.update({
|
|
1927
|
-
where: { id: assignmentId },
|
|
1928
|
-
data: {
|
|
1929
|
-
markScheme: {
|
|
1930
|
-
disconnect: true,
|
|
1931
|
-
},
|
|
1932
|
-
},
|
|
1933
|
-
include: {
|
|
1934
|
-
attachments: true,
|
|
1935
|
-
section: true,
|
|
1936
|
-
teacher: true,
|
|
1937
|
-
eventAttached: true,
|
|
1938
|
-
markScheme: true,
|
|
1939
|
-
},
|
|
1940
|
-
});
|
|
1941
|
-
return updatedAssignment;
|
|
1942
|
-
}),
|
|
1943
|
-
attachGradingBoundary: protectedTeacherProcedure
|
|
1944
|
-
.input(z.object({
|
|
1945
|
-
assignmentId: z.string(),
|
|
1946
|
-
gradingBoundaryId: z.string().nullable(),
|
|
1947
|
-
}))
|
|
1948
|
-
.mutation(async ({ ctx, input }) => {
|
|
1949
|
-
const { assignmentId, gradingBoundaryId } = input;
|
|
1950
|
-
const assignment = await prisma.assignment.findFirst({
|
|
1951
|
-
where: {
|
|
1952
|
-
id: assignmentId,
|
|
1953
|
-
},
|
|
1954
|
-
});
|
|
1955
|
-
if (!assignment) {
|
|
1956
|
-
throw new TRPCError({
|
|
1957
|
-
code: "NOT_FOUND",
|
|
1958
|
-
message: "Assignment not found",
|
|
1959
|
-
});
|
|
1960
|
-
}
|
|
1961
|
-
// If gradingBoundaryId is provided, verify it exists
|
|
1962
|
-
if (gradingBoundaryId) {
|
|
1963
|
-
const gradingBoundary = await prisma.gradingBoundary.findFirst({
|
|
1964
|
-
where: {
|
|
1965
|
-
id: gradingBoundaryId,
|
|
1966
|
-
},
|
|
1967
|
-
});
|
|
1968
|
-
if (!gradingBoundary) {
|
|
1969
|
-
throw new TRPCError({
|
|
1970
|
-
code: "NOT_FOUND",
|
|
1971
|
-
message: "Grading boundary not found",
|
|
1972
|
-
});
|
|
1973
|
-
}
|
|
1974
|
-
}
|
|
1975
|
-
const updatedAssignment = await prisma.assignment.update({
|
|
1976
|
-
where: { id: assignmentId },
|
|
1977
|
-
data: {
|
|
1978
|
-
gradingBoundary: gradingBoundaryId ? {
|
|
1979
|
-
connect: { id: gradingBoundaryId },
|
|
1980
|
-
} : {
|
|
1981
|
-
disconnect: true,
|
|
1982
|
-
},
|
|
1983
|
-
},
|
|
1984
|
-
include: {
|
|
1985
|
-
attachments: true,
|
|
1986
|
-
section: true,
|
|
1987
|
-
teacher: true,
|
|
1988
|
-
eventAttached: true,
|
|
1989
|
-
gradingBoundary: true,
|
|
1990
|
-
},
|
|
1991
|
-
});
|
|
1992
|
-
return updatedAssignment;
|
|
1993
|
-
}),
|
|
235
|
+
.mutation(({ ctx, input }) => attachGradingBoundaryRecord(ctx.user.id, input.assignmentId, input.gradingBoundaryId)),
|
|
1994
236
|
detachGradingBoundary: protectedTeacherProcedure
|
|
1995
|
-
.input(z.object({
|
|
1996
|
-
|
|
1997
|
-
assignmentId: z.string(),
|
|
1998
|
-
}))
|
|
1999
|
-
.mutation(async ({ ctx, input }) => {
|
|
2000
|
-
const { assignmentId } = input;
|
|
2001
|
-
const assignment = await prisma.assignment.findFirst({
|
|
2002
|
-
where: {
|
|
2003
|
-
id: assignmentId,
|
|
2004
|
-
},
|
|
2005
|
-
});
|
|
2006
|
-
if (!assignment) {
|
|
2007
|
-
throw new TRPCError({
|
|
2008
|
-
code: "NOT_FOUND",
|
|
2009
|
-
message: "Assignment not found",
|
|
2010
|
-
});
|
|
2011
|
-
}
|
|
2012
|
-
const updatedAssignment = await prisma.assignment.update({
|
|
2013
|
-
where: { id: assignmentId },
|
|
2014
|
-
data: {
|
|
2015
|
-
gradingBoundary: {
|
|
2016
|
-
disconnect: true,
|
|
2017
|
-
},
|
|
2018
|
-
},
|
|
2019
|
-
include: {
|
|
2020
|
-
attachments: true,
|
|
2021
|
-
section: true,
|
|
2022
|
-
teacher: true,
|
|
2023
|
-
eventAttached: true,
|
|
2024
|
-
gradingBoundary: true,
|
|
2025
|
-
},
|
|
2026
|
-
});
|
|
2027
|
-
return updatedAssignment;
|
|
2028
|
-
}),
|
|
237
|
+
.input(z.object({ classId: z.string(), assignmentId: z.string() }))
|
|
238
|
+
.mutation(({ ctx, input }) => detachGradingBoundaryRecord(ctx.user.id, input.assignmentId)),
|
|
2029
239
|
// New direct upload endpoints
|
|
2030
240
|
getAssignmentUploadUrls: protectedTeacherProcedure
|
|
2031
241
|
.input(getAssignmentUploadUrlsSchema)
|
|
@@ -2308,4 +518,4 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
2308
518
|
}),
|
|
2309
519
|
});
|
|
2310
520
|
//# sourceMappingURL=assignment.js.map
|
|
2311
|
-
//# debugId=
|
|
521
|
+
//# debugId=6ca6683e-2a89-5dab-a4c5-bf0292654572
|