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