From fb3490f61c43422f78f786763e3c4957b9fe8088 Mon Sep 17 00:00:00 2001 From: MmagdyhafezZ Date: Thu, 2 Oct 2025 17:03:51 -0600 Subject: [PATCH 1/5] verbosity update --- .../mock.jwt.cookie.auth.guard.ts | 9 +- apps/api-gateway/src/main.ts | 23 +- apps/api/package.json | 3 +- .../migration.sql | 30 ++ apps/api/prisma/schema.prisma | 10 +- .../assignment/attempt/attempt.controller.ts | 43 ++- .../api/assignment/attempt/attempt.service.ts | 358 ++++++++++-------- .../get.assignment.attempt.response.dto.ts | 9 +- .../update.assignment.attempt.response.dto.ts | 3 +- .../dto/get.assignment.response.dto.ts | 5 +- .../dto/update.assignment.request.dto.ts | 8 +- .../dto/update.questions.request.dto.ts | 4 +- .../question/question.controller.ts | 54 ++- .../assignment/question/question.service.ts | 55 +++ .../v2/repositories/assignment.repository.ts | 19 +- .../v2/services/assignment.service.ts | 141 +++---- .../v2/services/version-management.service.ts | 133 ++++--- .../v2/tests/unit/__mocks__/ common-mocks.ts | 112 +++--- .../unit/services/assignment.service.spec.ts | 2 +- .../api/attempt/dto/success-page-data.dto.ts | 80 ++++ .../services/attempt-grading.service.ts | 14 +- .../services/attempt-submission.service.ts | 319 ++++++++++++---- .../src/scripts/create-initial-versions.ts | 180 --------- .../src/test/migration-verification.test.ts | 260 +++++++++++++ apps/web/app/Helpers/checkDiff.ts | 6 +- .../Header/CheckLearnerSideButton.tsx | 6 +- .../app/author/(components)/Header/index.tsx | 8 +- .../StepTwo/AssignmentFeedback.tsx | 20 +- .../(components)/StepTwo/FeedbackSettings.tsx | 112 +++++- .../app/author/[assignmentId]/review/page.tsx | 6 +- .../successPage/[submissionId]/page.tsx | 23 +- .../version-control/VersionTreeView.tsx | 2 +- apps/web/config/types.ts | 14 +- apps/web/lib/api-health.ts | 94 +++++ apps/web/lib/learner.ts | 43 +++ apps/web/lib/talkToBackend.ts | 1 + ...ace_expression_model-weights_manifest.json | 345 +---------------- ..._face_detector_model-weights_manifest.json | 198 +--------- apps/web/next.config.js | 4 + apps/web/stores/assignmentFeedbackConfig.ts | 34 +- package.json | 4 +- scripts/cleanup-ports.sh | 31 ++ turbo.json | 3 +- 43 files changed, 1531 insertions(+), 1297 deletions(-) create mode 100644 apps/api/prisma/migrations/20251002001400_convert_show_correct_answer_to_enum/migration.sql create mode 100644 apps/api/src/api/attempt/dto/success-page-data.dto.ts delete mode 100644 apps/api/src/scripts/create-initial-versions.ts create mode 100644 apps/api/src/test/migration-verification.test.ts create mode 100644 apps/web/lib/api-health.ts create mode 100755 scripts/cleanup-ports.sh diff --git a/apps/api-gateway/src/auth/jwt/cookie-based/mock.jwt.cookie.auth.guard.ts b/apps/api-gateway/src/auth/jwt/cookie-based/mock.jwt.cookie.auth.guard.ts index ee1dc1e1..e85d75e4 100644 --- a/apps/api-gateway/src/auth/jwt/cookie-based/mock.jwt.cookie.auth.guard.ts +++ b/apps/api-gateway/src/auth/jwt/cookie-based/mock.jwt.cookie.auth.guard.ts @@ -22,15 +22,14 @@ export class MockJwtCookieAuthGuard extends AuthGuard("cookie-strategy") { const request: RequestWithUserSession = context.switchToHttp().getRequest(); request.user = { - userId: "dev-user", - role: UserRole.LEARNER, - groupId: "string", - assignmentId: 1888, + userId: "magdy.hafez@ibm.com1", + role: UserRole.AUTHOR, + groupId: "autogen-faculty-v1-course-v1-IND-AI0103EN-v1", + assignmentId: 1, gradingCallbackRequired: false, returnUrl: "https://skills.network", launch_presentation_locale: "en", }; - return true; } } diff --git a/apps/api-gateway/src/main.ts b/apps/api-gateway/src/main.ts index 5bfd6420..347de199 100644 --- a/apps/api-gateway/src/main.ts +++ b/apps/api-gateway/src/main.ts @@ -9,10 +9,13 @@ import { AppModule } from "./app.module"; import { winstonOptions } from "./logger/config"; async function bootstrap() { - const app = await NestFactory.create(AppModule, { - cors: false, - logger: WinstonModule.createLogger(winstonOptions), - }); + const logger = WinstonModule.createLogger(winstonOptions); + + try { + const app = await NestFactory.create(AppModule, { + cors: false, + logger, + }); app.use(json({ limit: "1000mb" })); app.use(urlencoded({ limit: "1000mb", extended: true })); app.setGlobalPrefix("api", { @@ -39,6 +42,16 @@ async function bootstrap() { app.enableShutdownHooks(); - await app.listen(process.env.API_GATEWAY_PORT ?? 3000); + const port = process.env.API_GATEWAY_PORT ?? 3000; + await app.listen(port, '0.0.0.0'); + + logger.log(`🚀 API Gateway is running on port ${port}`); + logger.log(`📚 API Documentation available at http://localhost:${port}/api`); + + } catch (error) { + logger.error('❌ Failed to start API Gateway:', error); + process.exit(1); + } } + void bootstrap(); diff --git a/apps/api/package.json b/apps/api/package.json index 326d1f7f..8c6507a9 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -24,7 +24,8 @@ "test:staged": "jest --bail --findRelatedTests --passWithNoTests --collectCoverage=false", "test:watch": "jest --watch", "create-initial-versions": "dotenv -e ./dev.env -- ts-node src/scripts/create-initial-versions.ts", - "translation-audit": "node dist/src/scripts/translation-audit.js" + "translation-audit": "node dist/src/scripts/translation-audit.js", + "verify-migration": "dotenv -e ./dev.env -- ts-node scripts/verify-migration.ts" }, "prisma": { "seed": "ts-node prisma/seed.ts" diff --git a/apps/api/prisma/migrations/20251002001400_convert_show_correct_answer_to_enum/migration.sql b/apps/api/prisma/migrations/20251002001400_convert_show_correct_answer_to_enum/migration.sql new file mode 100644 index 00000000..9c77aa84 --- /dev/null +++ b/apps/api/prisma/migrations/20251002001400_convert_show_correct_answer_to_enum/migration.sql @@ -0,0 +1,30 @@ +-- CreateEnum for CorrectAnswerVisibility +CREATE TYPE "CorrectAnswerVisibility" AS ENUM ('NEVER', 'ALWAYS', 'ON_PASS'); + +-- Add the new column with ALWAYS as default +ALTER TABLE "Assignment" ADD COLUMN "correctAnswerVisibility" "CorrectAnswerVisibility" DEFAULT 'ALWAYS'; + +-- Convert existing data: if showCorrectAnswer was true, set to ON_PASS, otherwise set to NEVER +UPDATE "Assignment" +SET "correctAnswerVisibility" = CASE + WHEN "showCorrectAnswer" = true THEN 'ON_PASS'::"CorrectAnswerVisibility" + ELSE 'NEVER'::"CorrectAnswerVisibility" +END; + +-- Make the new column NOT NULL after data conversion +ALTER TABLE "Assignment" ALTER COLUMN "correctAnswerVisibility" SET NOT NULL; + +-- Drop the old column +ALTER TABLE "Assignment" DROP COLUMN "showCorrectAnswer"; + +-- Add the new column to AssignmentVersion table +ALTER TABLE "AssignmentVersion" ADD COLUMN "correctAnswerVisibility" "CorrectAnswerVisibility" DEFAULT 'ALWAYS'; + +-- Update existing AssignmentVersion data: copy from parent Assignment +UPDATE "AssignmentVersion" av +SET "correctAnswerVisibility" = a."correctAnswerVisibility" +FROM "Assignment" a +WHERE av."assignmentId" = a.id; + +-- Make the new column NOT NULL after data conversion +ALTER TABLE "AssignmentVersion" ALTER COLUMN "correctAnswerVisibility" SET NOT NULL; \ No newline at end of file diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index fa6a323a..dce89a46 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -24,6 +24,13 @@ enum AssignmentQuestionDisplayOrder { DEFINED /// The assignments are displayed in a predefined order RANDOM /// The assignments are displayed in a random order } + +/// This enum defines when correct answers should be visible to learners +enum CorrectAnswerVisibility { + NEVER /// Correct answers are never shown to learners + ALWAYS /// Correct answers are always shown after submission + ON_PASS /// Correct answers are only shown when learners pass the assignment +} model GradingAudit { id Int @id @default(autoincrement()) questionId Int @@ -231,7 +238,7 @@ model Assignment { showQuestionScore Boolean @default(true) /// Should the question score be shown to the learner after its submission showSubmissionFeedback Boolean @default(true) /// Should the AI provide feedback when the learner submits a question showQuestions Boolean @default(true) /// Should the questions be shown to the learner - showCorrectAnswer Boolean @default(true) /// Should the correct answer be shown to the learner after its submission + correctAnswerVisibility CorrectAnswerVisibility @default(ALWAYS) /// When should correct answers be shown to learners updatedAt DateTime @default(now()) @updatedAt /// The DateTime at which the assignment was last updated languageCode String? /// The language code for the assignment currentVersionId Int? /// The ID of the current active version @@ -279,6 +286,7 @@ model AssignmentVersion { showQuestionScore Boolean @default(true) /// Show question score showSubmissionFeedback Boolean @default(true) /// Show submission feedback showQuestions Boolean @default(true) /// Show questions + correctAnswerVisibility CorrectAnswerVisibility @default(ALWAYS) /// When to show correct answers to learners languageCode String? /// Language code createdBy String /// User who created this version createdAt DateTime @default(now()) /// When version was created diff --git a/apps/api/src/api/assignment/attempt/attempt.controller.ts b/apps/api/src/api/assignment/attempt/attempt.controller.ts index 0434ad1a..ee62a583 100644 --- a/apps/api/src/api/assignment/attempt/attempt.controller.ts +++ b/apps/api/src/api/assignment/attempt/attempt.controller.ts @@ -27,6 +27,7 @@ import { UserSessionRequest, } from "../../../auth/interfaces/user.session.interface"; import { Roles } from "../../../auth/role/roles.global.guard"; +import { SuccessPageDataDto } from "../../attempt/dto/success-page-data.dto"; import { GRADE_SUBMISSION_EXCEPTION, MAX_ATTEMPTS_SUBMISSION_EXCEPTION_MESSAGE, @@ -60,7 +61,7 @@ export class AttemptControllerV1 { private logger; constructor( @Inject(WINSTON_MODULE_PROVIDER) private parentLogger: Logger, - private readonly attemptService: AttemptServiceV1, + private readonly attemptService: AttemptServiceV1 ) { this.logger = parentLogger.child({ context: AttemptControllerV1.name }); } @@ -80,11 +81,11 @@ export class AttemptControllerV1 { @ApiResponse({ status: 403 }) createAssignmentAttempt( @Param("assignmentId") assignmentId: number, - @Req() request: UserSessionRequest, + @Req() request: UserSessionRequest ): Promise { return this.attemptService.createAssignmentAttempt( Number(assignmentId), - request.userSession, + request.userSession ); } @@ -96,11 +97,11 @@ export class AttemptControllerV1 { @ApiResponse({ status: 403 }) listAssignmentAttempts( @Param("assignmentId") assignmentId: number, - @Req() request: UserSessionRequest, + @Req() request: UserSessionRequest ): Promise { return this.attemptService.listAssignmentAttempts( Number(assignmentId), - request.userSession, + request.userSession ); } @@ -112,11 +113,11 @@ export class AttemptControllerV1 { @ApiResponse({ status: 403 }) getAssignmentAttempt( @Param("attemptId") assignmentAttemptId: number, - @Query("lang") lang?: string, + @Query("lang") lang?: string ): Promise { return this.attemptService.getAssignmentAttempt( Number(assignmentAttemptId), - lang, + lang ); } @@ -127,10 +128,10 @@ export class AttemptControllerV1 { @ApiResponse({ status: 200, type: GetAssignmentAttemptResponseDto }) @ApiResponse({ status: 403 }) getLearnerAssignmentAttempt( - @Param("attemptId") assignmentAttemptId: number, + @Param("attemptId") assignmentAttemptId: number ): Promise { return this.attemptService.getLearnerAssignmentAttempt( - Number(assignmentAttemptId), + Number(assignmentAttemptId) ); } @@ -161,7 +162,7 @@ export class AttemptControllerV1 { @Param("assignmentId") assignmentId: number, @Body() learnerUpdateAssignmentAttemptDto: LearnerUpdateAssignmentAttemptRequestDto, - @Req() request: UserSessionRequest, + @Req() request: UserSessionRequest ): Promise { const authCookie: string = request?.cookies?.authentication as string; const gradingCallbackRequired = @@ -173,7 +174,7 @@ export class AttemptControllerV1 { learnerUpdateAssignmentAttemptDto, authCookie, gradingCallbackRequired, - request, + request ); } @@ -188,14 +189,14 @@ export class AttemptControllerV1 { @Param("assignmentId") assignmentId: string, @Param("attemptId") attemptId: string, @Body() body: { feedback: AssignmentFeedbackDto }, - @Req() request: UserSessionRequest, + @Req() request: UserSessionRequest ): Promise { const feedbackDto = body.feedback; return this.attemptService.submitFeedback( Number(assignmentId), Number(attemptId), feedbackDto, - request.userSession, + request.userSession ); } @@ -208,12 +209,12 @@ export class AttemptControllerV1 { getFeedback( @Param("assignmentId") assignmentId: string, @Param("attemptId") attemptId: string, - @Req() request: UserSessionRequest, + @Req() request: UserSessionRequest ): Promise { return this.attemptService.getFeedback( Number(assignmentId), Number(attemptId), - request.userSession, + request.userSession ); } @@ -227,13 +228,13 @@ export class AttemptControllerV1 { @Param("assignmentId") assignmentId: string, @Param("attemptId") attemptId: string, @Body() body: { regradingRequest: RegradingRequestDto }, - @Req() request: UserSessionRequest, + @Req() request: UserSessionRequest ): Promise { return this.attemptService.processRegradingRequest( Number(assignmentId), Number(attemptId), body.regradingRequest, - request.userSession, + request.userSession ); } @@ -246,12 +247,12 @@ export class AttemptControllerV1 { getRegradingStatus( @Param("assignmentId") assignmentId: string, @Param("attemptId") attemptId: string, - @Req() request: UserSessionRequest, + @Req() request: UserSessionRequest ): Promise { return this.attemptService.getRegradingStatus( Number(assignmentId), Number(attemptId), - request.userSession, + request.userSession ); } @@ -274,7 +275,7 @@ export class AttemptControllerV1 { @Param("attemptId") attemptId: number, @Param("assignmentId") assignmentId: number, @Body() body: ReportRequestDTO, - @Req() request: UserSessionRequest, + @Req() request: UserSessionRequest ): Promise<{ message: string }> { const { issueType, description } = body; @@ -297,7 +298,7 @@ export class AttemptControllerV1 { Number(attemptId), issueType, description, - userId, + userId ); return { message: "Report submitted successfully" }; diff --git a/apps/api/src/api/assignment/attempt/attempt.service.ts b/apps/api/src/api/assignment/attempt/attempt.service.ts index 6726f1e9..8438b26e 100644 --- a/apps/api/src/api/assignment/attempt/attempt.service.ts +++ b/apps/api/src/api/assignment/attempt/attempt.service.ts @@ -10,6 +10,7 @@ import { } from "@nestjs/common"; import { Assignment, + CorrectAnswerVisibility, Question, QuestionType, QuestionVariant, @@ -19,6 +20,10 @@ import { } from "@prisma/client"; import { JsonValue } from "@prisma/client/runtime/library"; import { LearnerFileUpload } from "src/api/attempt/common/interfaces/attempt.interface"; +import { + SuccessPageDataDto, + SuccessPageQuestionDto, +} from "src/api/attempt/dto/success-page-data.dto"; import { LlmFacadeService } from "src/api/llm/llm-facade.service"; import { PresentationQuestionEvaluateModel } from "src/api/llm/model/presentation.question.evaluate.model"; import { VideoPresentationQuestionEvaluateModel } from "src/api/llm/model/video-presentation.question.evaluate.model"; @@ -92,14 +97,14 @@ export class AttemptServiceV1 { private readonly llmFacadeService: LlmFacadeService, private readonly questionService: QuestionService, private readonly assignmentService: AssignmentServiceV1, - private readonly httpService: HttpService, + private readonly httpService: HttpService ) {} async submitFeedback( assignmentId: number, attemptId: number, feedbackDto: AssignmentFeedbackDto, - userSession: UserSession, + userSession: UserSession ): Promise { const assignmentAttempt = await this.prisma.assignmentAttempt.findUnique({ where: { id: attemptId }, @@ -107,19 +112,19 @@ export class AttemptServiceV1 { if (!assignmentAttempt) { throw new NotFoundException( - `Assignment attempt with ID ${attemptId} not found.`, + `Assignment attempt with ID ${attemptId} not found.` ); } if (assignmentAttempt.assignmentId !== assignmentId) { throw new BadRequestException( - "Assignment ID does not match the attempt.", + "Assignment ID does not match the attempt." ); } if (assignmentAttempt.userId !== userSession.userId) { throw new ForbiddenException( - "You do not have permission to submit feedback for this attempt.", + "You do not have permission to submit feedback for this attempt." ); } @@ -166,7 +171,7 @@ export class AttemptServiceV1 { async getFeedback( assignmentId: number, attemptId: number, - userSession: UserSession, + userSession: UserSession ): Promise { const feedback = await this.prisma.assignmentFeedback.findFirst({ where: { @@ -194,7 +199,7 @@ export class AttemptServiceV1 { assignmentId: number, attemptId: number, regradingRequestDto: RegradingRequestDto, - userSession: UserSession, + userSession: UserSession ): Promise { const assignmentAttempt = await this.prisma.assignmentAttempt.findUnique({ where: { id: attemptId }, @@ -202,19 +207,19 @@ export class AttemptServiceV1 { if (!assignmentAttempt) { throw new NotFoundException( - `Assignment attempt with ID ${attemptId} not found.`, + `Assignment attempt with ID ${attemptId} not found.` ); } if (assignmentAttempt.assignmentId !== assignmentId) { throw new BadRequestException( - "Assignment ID does not match the attempt.", + "Assignment ID does not match the attempt." ); } if (assignmentAttempt.userId !== userSession.userId) { throw new ForbiddenException( - "You do not have permission to request regrading for this attempt.", + "You do not have permission to request regrading for this attempt." ); } @@ -236,7 +241,7 @@ export class AttemptServiceV1 { regradingStatus: RegradingStatus.PENDING, updatedAt: new Date(), }, - }, + } ); return { @@ -262,7 +267,7 @@ export class AttemptServiceV1 { async getRegradingStatus( assignmentId: number, attemptId: number, - userSession: UserSession, + userSession: UserSession ): Promise { const regradingRequest = await this.prisma.regradingRequest.findFirst({ where: { @@ -274,7 +279,7 @@ export class AttemptServiceV1 { if (!regradingRequest) { throw new NotFoundException( - `Regrading request for assignment ${assignmentId} and attempt ${attemptId} not found.`, + `Regrading request for assignment ${assignmentId} and attempt ${attemptId} not found.` ); } @@ -291,7 +296,7 @@ export class AttemptServiceV1 { */ async listAssignmentAttempts( assignmentId: number, - userSession: UserSession, + userSession: UserSession ): Promise { const { userId, role } = userSession; @@ -331,11 +336,11 @@ export class AttemptServiceV1 { */ async createAssignmentAttempt( assignmentId: number, - userSession: UserSession, + userSession: UserSession ): Promise { const assignment = await this.assignmentService.findOne( assignmentId, - userSession, + userSession ); await this.validateNewAttempt(assignment, userSession); const attemptExpiresAt = this.calculateAttemptExpiresAt(assignment); @@ -371,7 +376,7 @@ export class AttemptServiceV1 { questions.sort( (a, b) => assignment.questionOrder.indexOf(a.id) - - assignment.questionOrder.indexOf(b.id), + assignment.questionOrder.indexOf(b.id) ); } await this.prisma.assignmentAttempt.update({ @@ -384,7 +389,7 @@ export class AttemptServiceV1 { const attemptQuestionVariantsData = questions.map((question) => { const questionAndVariants = [undefined, ...question.variants]; const randomIndex = Math.floor( - Math.random() * questionAndVariants.length, + Math.random() * questionAndVariants.length ); const chosenVariant = questionAndVariants[randomIndex]; @@ -395,12 +400,12 @@ export class AttemptServiceV1 { variantId = chosenVariant.id ?? undefined; randomizedChoices = this.maybeShuffleChoices( chosenVariant.choices as unknown as Choice[], - chosenVariant.randomizedChoices === true, + chosenVariant.randomizedChoices === true ); } else { randomizedChoices = this.maybeShuffleChoices( question.choices as unknown as Choice[], - question.randomizedChoices === true, + question.randomizedChoices === true ); } @@ -472,7 +477,7 @@ export class AttemptServiceV1 { updateAssignmentAttemptDto: LearnerUpdateAssignmentAttemptRequestDto, authCookie: string, gradingCallbackRequired: boolean, - request: UserSessionRequest, + request: UserSessionRequest ): Promise { const { role, userId } = request.userSession; if (role === UserRole.LEARNER) { @@ -489,7 +494,7 @@ export class AttemptServiceV1 { }); if (!assignmentAttempt) { throw new NotFoundException( - `AssignmentAttempt with Id ${assignmentAttemptId} not found.`, + `AssignmentAttempt with Id ${assignmentAttemptId} not found.` ); } const tenSecondsBeforeNow = new Date(Date.now() - 10 * 1000); @@ -515,7 +520,7 @@ export class AttemptServiceV1 { grade: 0, showSubmissionFeedback: false, showQuestions: false, - showCorrectAnswer: false, + correctAnswerVisibility: "NEVER", feedbacksForQuestions: [], message: SUBMISSION_DEADLINE_EXCEPTION_MESSAGE, }; @@ -525,7 +530,7 @@ export class AttemptServiceV1 { for (const response of updateAssignmentAttemptDto.responsesForQuestions) { const questionId: number = response.id; const variantMapping = assignmentAttempt.questionVariants.find( - (qv) => qv.questionId === questionId, + (qv) => qv.questionId === questionId ); let question: QuestionDto; if (variantMapping && variantMapping.questionVariant !== null) { @@ -542,17 +547,17 @@ export class AttemptServiceV1 { scoring: typeof variant.scoring === "string" ? (JSON.parse(variant.scoring) as ScoringDto) - : ((variant.scoring as unknown as ScoringDto) ?? + : (variant.scoring as unknown as ScoringDto) ?? (typeof baseQuestion.scoring === "string" ? (JSON.parse(baseQuestion.scoring) as ScoringDto) - : (baseQuestion.scoring as unknown as ScoringDto))), + : (baseQuestion.scoring as unknown as ScoringDto)), choices: typeof variant.choices === "string" ? (JSON.parse(variant.choices) as Choice[]) - : ((variant.choices as unknown as Choice[]) ?? + : (variant.choices as unknown as Choice[]) ?? (typeof baseQuestion.choices === "string" ? (JSON.parse(baseQuestion.choices) as Choice[]) - : (baseQuestion.choices as unknown as Choice[]))), + : (baseQuestion.choices as unknown as Choice[])), answer: baseQuestion.answer ?? variant.answer, alreadyInBackend: true, totalPoints: baseQuestion.totalPoints, @@ -566,7 +571,7 @@ export class AttemptServiceV1 { question = await this.applyTranslationToQuestion( question, updateAssignmentAttemptDto.language, - variantMapping, + variantMapping ); preTranslatedQuestions.set(questionId, question); } @@ -581,7 +586,11 @@ export class AttemptServiceV1 { showSubmissionFeedback: true, showQuestionScore: true, showQuestions: true, - showCorrectAnswer: true, + currentVersion: { + select: { + correctAnswerVisibility: true, + }, + }, questions: { where: { isDeleted: false }, }, @@ -595,17 +604,17 @@ export class AttemptServiceV1 { updateAssignmentAttemptDto.language, updateAssignmentAttemptDto.authorQuestions, updateAssignmentAttemptDto.authorAssignmentDetails, - updateAssignmentAttemptDto.preTranslatedQuestions, + updateAssignmentAttemptDto.preTranslatedQuestions ); const { grade, totalPointsEarned, totalPossiblePoints } = role === UserRole.LEARNER ? this.calculateGradeForLearner( successfulQuestionResponses, - assignment as unknown as GetAssignmentAttemptResponseDto, + assignment as unknown as GetAssignmentAttemptResponseDto ) : this.calculateGradeForAuthor( successfulQuestionResponses, - updateAssignmentAttemptDto.authorQuestions, + updateAssignmentAttemptDto.authorQuestions ); if (gradingCallbackRequired && role === UserRole.LEARNER) { @@ -639,17 +648,18 @@ export class AttemptServiceV1 { grade: assignment.showAssignmentScore ? grade : undefined, showSubmissionFeedback: assignment.showSubmissionFeedback, showQuestions: assignment.showQuestions, - showCorrectAnswer: assignment.showCorrectAnswer, + correctAnswerVisibility: + assignment.currentVersion?.correctAnswerVisibility || "NEVER", feedbacksForQuestions: this.constructFeedbacksForQuestions( successfulQuestionResponses, - assignment as unknown as LearnerGetAssignmentResponseDto, + assignment as unknown as LearnerGetAssignmentResponseDto ), }; } else { const result = await this.updateAssignmentAttemptInDb( assignmentAttemptId, updateAssignmentAttemptDto, - grade, + grade ); return { id: result.id, @@ -660,10 +670,11 @@ export class AttemptServiceV1 { showQuestions: assignment.showQuestions, grade: assignment.showAssignmentScore ? result.grade : undefined, showSubmissionFeedback: assignment.showSubmissionFeedback, - showCorrectAnswer: assignment.showCorrectAnswer, + correctAnswerVisibility: + assignment.currentVersion?.correctAnswerVisibility || "NEVER", feedbacksForQuestions: this.constructFeedbacksForQuestions( successfulQuestionResponses, - assignment as unknown as LearnerGetAssignmentResponseDto, + assignment as unknown as LearnerGetAssignmentResponseDto ), }; } @@ -709,7 +720,7 @@ export class AttemptServiceV1 { * @throws BadRequestException If stored data formats are invalid */ async getLearnerAssignmentAttempt( - assignmentAttemptId: number, + assignmentAttemptId: number ): Promise { const assignmentAttempt = await this.prisma.assignmentAttempt.findUnique({ where: { id: assignmentAttemptId }, @@ -722,7 +733,7 @@ export class AttemptServiceV1 { }); if (!assignmentAttempt) { throw new NotFoundException( - `AssignmentAttempt with Id ${assignmentAttemptId} not found.`, + `AssignmentAttempt with Id ${assignmentAttemptId} not found.` ); } @@ -731,7 +742,7 @@ export class AttemptServiceV1 { }); if (!questions) { throw new NotFoundException( - `Questions for assignment with Id ${assignmentAttempt.assignmentId} not found.`, + `Questions for assignment with Id ${assignmentAttempt.assignmentId} not found.` ); } @@ -746,7 +757,11 @@ export class AttemptServiceV1 { showSubmissionFeedback: true, showQuestionScore: true, showQuestions: true, - showCorrectAnswer: true, + currentVersion: { + select: { + correctAnswerVisibility: true, + }, + }, }, }); @@ -797,7 +812,7 @@ export class AttemptServiceV1 { }); const questionVariantsMap = new Map( - questionsWithVariants.map((question) => [question.id, question]), + questionsWithVariants.map((question) => [question.id, question]) ); const mergedQuestions = questions.map((originalQ) => { const variantQ = questionVariantsMap.get(originalQ.id); @@ -818,7 +833,7 @@ export class AttemptServiceV1 { liveRecordingConfig: (question.liveRecordingConfig as unknown as JsonValue) ?? undefined, })), - assignmentAttempt.questionResponses as unknown as QuestionResponse[], + assignmentAttempt.questionResponses as unknown as QuestionResponse[] ); const finalQuestions = questionOrder .map((qId) => questionsWithResponses.find((q) => q.id === qId)) @@ -879,7 +894,8 @@ export class AttemptServiceV1 { // Apply visibility settings for correct answers and if learner didnt pass if ( - assignment.showCorrectAnswer === false && + (assignment.currentVersion?.correctAnswerVisibility || "NEVER") === + "NEVER" && assignmentAttempt.grade < assignment.passingGrade ) { for (const question of finalQuestions) { @@ -906,7 +922,8 @@ export class AttemptServiceV1 { showAssignmentScore: assignment.showAssignmentScore, showSubmissionFeedback: assignment.showSubmissionFeedback, showQuestionScore: assignment.showQuestionScore, - showCorrectAnswer: assignment.showCorrectAnswer, + correctAnswerVisibility: + assignment.currentVersion?.correctAnswerVisibility || "NEVER", comments: assignmentAttempt.comments, }; } @@ -933,7 +950,7 @@ export class AttemptServiceV1 { */ async getAssignmentAttempt( assignmentAttemptId: number, - language: string, + language: string ): Promise { if (!language) { language = "en"; @@ -957,7 +974,7 @@ export class AttemptServiceV1 { }); if (!assignmentAttempt) { throw new NotFoundException( - `AssignmentAttempt with Id ${assignmentAttemptId} not found.`, + `AssignmentAttempt with Id ${assignmentAttemptId} not found.` ); } @@ -971,7 +988,11 @@ export class AttemptServiceV1 { showAssignmentScore: true, showSubmissionFeedback: true, showQuestionScore: true, - showCorrectAnswer: true, + currentVersion: { + select: { + correctAnswerVisibility: true, + }, + }, }, })) as LearnerGetAssignmentResponseDto; @@ -1046,7 +1067,7 @@ export class AttemptServiceV1 { if (!Array.isArray(normalizedChoices)) { throw new InternalServerErrorException( - `Malformed choices for question ${originalQ.id}`, + `Malformed choices for question ${originalQ.id}` ); } let finalChoices = normalizedChoices; @@ -1056,7 +1077,7 @@ export class AttemptServiceV1 { if (typeof qv.randomizedChoices === "string") { try { randomizedChoicesArray = JSON.parse( - qv.randomizedChoices, + qv.randomizedChoices ) as Choice[]; } catch { randomizedChoicesArray = []; @@ -1069,11 +1090,11 @@ export class AttemptServiceV1 { return normalizedChoices?.findIndex((bc) => bc.id === rChoice.id); } return normalizedChoices?.findIndex( - (bc) => bc.choice === rChoice.choice, + (bc) => bc.choice === rChoice.choice ); }); const orderedBaseChoices = permutation?.map( - (index) => normalizedChoices[index], + (index) => normalizedChoices[index] ); if (orderedBaseChoices?.length === normalizedChoices?.length) { finalChoices = orderedBaseChoices; @@ -1093,7 +1114,7 @@ export class AttemptServiceV1 { const origTranslatedChoices = translationObject.translatedChoices as Choice[]; const reorderedTranslatedChoices = permutation.map( - (index) => origTranslatedChoices[index], + (index) => origTranslatedChoices[index] ); translationObject.translatedChoices = reorderedTranslatedChoices; } @@ -1123,14 +1144,14 @@ export class AttemptServiceV1 { }); const questionVariantsMap = new Map( - questionsWithVariants.map((question) => [question.id, question]), + questionsWithVariants.map((question) => [question.id, question]) ); const questions: Question[] = await this.prisma.question.findMany({ where: { assignmentId: assignmentAttempt.assignmentId }, }); const nonVariantQuestions = questions.filter( - (originalQ) => !questionVariantsMap.has(originalQ.id), + (originalQ) => !questionVariantsMap.has(originalQ.id) ); const mergedQuestions = [ @@ -1153,9 +1174,9 @@ export class AttemptServiceV1 { if (question.choices) { for (const choice of question.choices) { delete choice.points; - // Only remove correct answer data if showCorrectAnswer is false if ( - assignment.showCorrectAnswer === false && + (assignment.currentVersion?.correctAnswerVisibility || "NEVER") === + "ON_PASS" && assignmentAttempt.grade < assignment.passingGrade ) { delete choice.isCorrect; @@ -1170,9 +1191,9 @@ export class AttemptServiceV1 { if (translationObject?.translatedChoices) { for (const choice of translationObject.translatedChoices) { delete choice.points; - // Only remove correct answer data if showCorrectAnswer is false if ( - assignment.showCorrectAnswer === false && + (assignment.currentVersion?.correctAnswerVisibility || + "NEVER") === "ON_PASS" && assignmentAttempt.grade < assignment.passingGrade ) { delete choice.isCorrect; @@ -1188,13 +1209,13 @@ export class AttemptServiceV1 { typeof question.randomizedChoices === "string" ) { const randomizedArray = JSON.parse( - question.randomizedChoices, + question.randomizedChoices ) as Choice[]; for (const choice of randomizedArray) { delete choice.points; - // Only remove correct answer data if showCorrectAnswer is false if ( - assignment.showCorrectAnswer === false && + (assignment.currentVersion?.correctAnswerVisibility || "NEVER") === + "ON_PASS" && assignmentAttempt.grade < assignment.passingGrade ) { delete choice.isCorrect; @@ -1204,9 +1225,9 @@ export class AttemptServiceV1 { question.randomizedChoices = JSON.stringify(randomizedArray); } - // Only remove the answer field if showCorrectAnswer is false if ( - assignment.showCorrectAnswer === false && + (assignment.currentVersion?.correctAnswerVisibility || "NEVER") === + "ON_PASS" && assignmentAttempt.grade < assignment.passingGrade ) { delete question.answer; @@ -1227,7 +1248,8 @@ export class AttemptServiceV1 { showSubmissionFeedback: assignment.showSubmissionFeedback, showQuestionScore: assignment.showQuestionScore, showQuestions: assignment.showQuestions, - showCorrectAnswer: assignment.showCorrectAnswer, + correctAnswerVisibility: + assignment.currentVersion?.correctAnswerVisibility || "NEVER", }; } @@ -1275,7 +1297,7 @@ export class AttemptServiceV1 { language: string, authorQuestions?: QuestionDto[], assignmentDetails?: authorAssignmentDetailsDTO, - preTranslatedQuestions?: Map, + preTranslatedQuestions?: Map ): Promise { let question: QuestionDto; let assignmentContext: { @@ -1302,11 +1324,11 @@ export class AttemptServiceV1 { }); if (!assignmentAttempt) { throw new NotFoundException( - `AssignmentAttempt with Id ${assignmentAttemptId} not found.`, + `AssignmentAttempt with Id ${assignmentAttemptId} not found.` ); } const variantMapping = assignmentAttempt.questionVariants.find( - (qv) => qv.questionId === questionId, + (qv) => qv.questionId === questionId ); if ( variantMapping && @@ -1327,17 +1349,17 @@ export class AttemptServiceV1 { scoring: typeof variant.scoring === "string" ? (JSON.parse(variant.scoring) as ScoringDto) - : ((variant.scoring as unknown as ScoringDto) ?? + : (variant.scoring as unknown as ScoringDto) ?? (typeof baseQuestion.scoring === "string" ? (JSON.parse(baseQuestion.scoring) as ScoringDto) - : (baseQuestion.scoring as unknown as ScoringDto))), + : (baseQuestion.scoring as unknown as ScoringDto)), choices: typeof variant.choices === "string" ? (JSON.parse(variant.choices) as Choice[]) - : ((variant.choices as unknown as Choice[]) ?? + : (variant.choices as unknown as Choice[]) ?? (typeof baseQuestion.choices === "string" ? (JSON.parse(baseQuestion.choices) as Choice[]) - : (baseQuestion.choices as unknown as Choice[]))), + : (baseQuestion.choices as unknown as Choice[])), answer: baseQuestion.answer ?? variant.answer, alreadyInBackend: true, totalPoints: baseQuestion.totalPoints, @@ -1350,7 +1372,7 @@ export class AttemptServiceV1 { assignmentId, questionId, assignmentAttemptId, - role, + role ); } else if (role === UserRole.AUTHOR) { question = authorQuestions.find((q) => q.id === questionId); @@ -1364,7 +1386,7 @@ export class AttemptServiceV1 { createQuestionResponseAttemptRequestDto, assignmentContext, assignmentId, - language, + language ); const result = await this.prisma.questionResponse.create({ data: { @@ -1421,7 +1443,7 @@ export class AttemptServiceV1 { attemptId: number, issueType: ReportType, description: string, - userId: string, + userId: string ): Promise { const assignmentExists = await this.prisma.assignment.findUnique({ where: { id: assignmentId }, @@ -1449,7 +1471,7 @@ export class AttemptServiceV1 { }); if (reports.length >= 5) { throw new UnprocessableEntityException( - "You have reached the maximum number of reports allowed in a 24-hour period.", + "You have reached the maximum number of reports allowed in a 24-hour period." ); } @@ -1472,7 +1494,7 @@ export class AttemptServiceV1 { */ private async validateNewAttempt( assignment: LearnerGetAssignmentResponseDto, - userSession: UserSession, + userSession: UserSession ): Promise { const timeRangeStartDate = this.calculateTimeRangeStartDate(assignment); @@ -1503,12 +1525,12 @@ export class AttemptServiceV1 { const ongoingAttempts = attempts.filter( (sub) => !sub.submitted && - (sub.expiresAt >= new Date() || sub.expiresAt === null), + (sub.expiresAt >= new Date() || sub.expiresAt === null) ); const attemptsInTimeRange = attempts.filter( (sub) => - sub.createdAt >= timeRangeStartDate && sub.createdAt <= new Date(), + sub.createdAt >= timeRangeStartDate && sub.createdAt <= new Date() ); if (ongoingAttempts.length > 0) { throw new UnprocessableEntityException(IN_PROGRESS_SUBMISSION_EXCEPTION); @@ -1519,18 +1541,18 @@ export class AttemptServiceV1 { attemptsInTimeRange.length >= assignment.attemptsPerTimeRange ) { throw new UnprocessableEntityException( - TIME_RANGE_ATTEMPTS_SUBMISSION_EXCEPTION_MESSAGE, + TIME_RANGE_ATTEMPTS_SUBMISSION_EXCEPTION_MESSAGE ); } if (assignment.numAttempts !== null && assignment.numAttempts !== -1) { const attemptCount = await this.countUserAttempts( userSession.userId, - assignment.id, + assignment.id ); if (attemptCount >= assignment.numAttempts) { throw new UnprocessableEntityException( - MAX_ATTEMPTS_SUBMISSION_EXCEPTION_MESSAGE, + MAX_ATTEMPTS_SUBMISSION_EXCEPTION_MESSAGE ); } } @@ -1542,7 +1564,7 @@ export class AttemptServiceV1 { * @returns The expiration date or null. */ private calculateAttemptExpiresAt( - assignment: LearnerGetAssignmentResponseDto, + assignment: LearnerGetAssignmentResponseDto ): Date | null { if ( assignment.allotedTimeMinutes !== undefined && @@ -1555,7 +1577,7 @@ export class AttemptServiceV1 { private maybeShuffleChoices( choices: Choice[] | string | null | undefined, - shouldShuffle: boolean, + shouldShuffle: boolean ): string | null { if (!choices) return; let parsed: Choice[]; @@ -1596,7 +1618,7 @@ export class AttemptServiceV1 { language: string, authorQuestions?: QuestionDto[], assignmentDetails?: authorAssignmentDetailsDTO, - preTranslatedQuestions?: Map, + preTranslatedQuestions?: Map ): Promise { const questionResponsesPromise = responsesForQuestions.map( async (questionResponse) => { @@ -1610,13 +1632,13 @@ export class AttemptServiceV1 { language, authorQuestions, assignmentDetails, - preTranslatedQuestions, + preTranslatedQuestions ); - }, + } ); const questionResponses = await Promise.allSettled( - questionResponsesPromise, + questionResponsesPromise ); const successfulResponses = questionResponses .filter((response) => response.status === "fulfilled") @@ -1628,7 +1650,7 @@ export class AttemptServiceV1 { if (failedResponses.length > 0) { throw new InternalServerErrorException( - `Failed to submit questions: ${failedResponses.join(", ")}`, + `Failed to submit questions: ${failedResponses.join(", ")}` ); } @@ -1643,20 +1665,20 @@ export class AttemptServiceV1 { */ private calculateGradeForAuthor( successfulQuestionResponses: CreateQuestionResponseAttemptResponseDto[], - authorQuestions: QuestionDto[], + authorQuestions: QuestionDto[] ): { grade: number; totalPointsEarned: number; totalPossiblePoints: number } { if (successfulQuestionResponses.length === 0) { return { grade: 0, totalPointsEarned: 0, totalPossiblePoints: 0 }; } const totalPointsEarned = successfulQuestionResponses.reduce( (accumulator, response) => accumulator + response.totalPoints, - 0, + 0 ); const totalPossiblePoints = authorQuestions.reduce( (accumulator: number, question: QuestionDto) => accumulator + question.totalPoints, - 0, + 0 ); const grade = totalPointsEarned / totalPossiblePoints; @@ -1671,20 +1693,20 @@ export class AttemptServiceV1 { */ private calculateGradeForLearner( successfulQuestionResponses: CreateQuestionResponseAttemptResponseDto[], - assignment: GetAssignmentAttemptResponseDto, + assignment: GetAssignmentAttemptResponseDto ): { grade: number; totalPointsEarned: number; totalPossiblePoints: number } { if (successfulQuestionResponses.length === 0) { return { grade: 0, totalPointsEarned: 0, totalPossiblePoints: 0 }; } const totalPointsEarned = successfulQuestionResponses.reduce( (accumulator, response) => accumulator + response.totalPoints, - 0, + 0 ); const totalPossiblePoints = assignment.questions.reduce( (accumulator: number, question: { totalPoints: number }) => accumulator + question.totalPoints, - 0, + 0 ); const grade = totalPointsEarned / totalPossiblePoints; @@ -1698,7 +1720,7 @@ export class AttemptServiceV1 { */ private async sendGradeToLtiGateway( grade: number, - authCookie: string, + authCookie: string ): Promise { const ltiGatewayResponse = await this.httpService .put( @@ -1708,7 +1730,7 @@ export class AttemptServiceV1 { headers: { Cookie: `authentication=${authCookie}`, }, - }, + } ) .toPromise(); @@ -1727,7 +1749,7 @@ export class AttemptServiceV1 { private async updateAssignmentAttemptInDb( assignmentAttemptId: number, updateAssignmentAttemptDto: LearnerUpdateAssignmentAttemptRequestDto, - grade: number, + grade: number ) { const { // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -1761,7 +1783,7 @@ export class AttemptServiceV1 { */ private constructFeedbacksForQuestions( successfulQuestionResponses: CreateQuestionResponseAttemptResponseDto[], - assignment: LearnerGetAssignmentResponseDto, + assignment: LearnerGetAssignmentResponseDto ) { return successfulQuestionResponses.map((feedbackForQuestion) => { const { totalPoints, feedback, ...otherData } = feedbackForQuestion; @@ -1781,7 +1803,7 @@ export class AttemptServiceV1 { const thirtySecondsBeforeNow = new Date(Date.now() - 30 * 1000); if (expiresAt && thirtySecondsBeforeNow > expiresAt) { throw new UnprocessableEntityException( - SUBMISSION_DEADLINE_EXCEPTION_MESSAGE, + SUBMISSION_DEADLINE_EXCEPTION_MESSAGE ); } } @@ -1801,7 +1823,7 @@ export class AttemptServiceV1 { questionAnswerContext: QuestionAnswerContext[]; }, assignmentId: number, - language: string, + language: string ): Promise<{ responseDto: CreateQuestionResponseAttemptResponseDto; learnerResponse: @@ -1811,7 +1833,7 @@ export class AttemptServiceV1 { }> { if ( Array.isArray( - createQuestionResponseAttemptRequestDto.learnerFileResponse, + createQuestionResponseAttemptRequestDto.learnerFileResponse ) && createQuestionResponseAttemptRequestDto.learnerFileResponse.length === 0 && @@ -1825,7 +1847,7 @@ export class AttemptServiceV1 { (createQuestionResponseAttemptRequestDto.learnerPresentationResponse === undefined || (Array.isArray( - createQuestionResponseAttemptRequestDto.learnerPresentationResponse, + createQuestionResponseAttemptRequestDto.learnerPresentationResponse ) && createQuestionResponseAttemptRequestDto.learnerPresentationResponse .length === 0)) @@ -1848,7 +1870,7 @@ export class AttemptServiceV1 { createQuestionResponseAttemptRequestDto, assignmentContext, assignmentId, - language, + language ); } case QuestionType.LINK_FILE: { @@ -1858,7 +1880,7 @@ export class AttemptServiceV1 { createQuestionResponseAttemptRequestDto, assignmentContext, assignmentId, - language, + language ); } else if ( createQuestionResponseAttemptRequestDto.learnerFileResponse @@ -1868,11 +1890,11 @@ export class AttemptServiceV1 { question.type, createQuestionResponseAttemptRequestDto, assignmentContext, - language, + language ); } else { throw new BadRequestException( - "Expected a file-based response (learnerFileResponse) or URL-based response (learnerUrlResponse), but did not receive one.", + "Expected a file-based response (learnerFileResponse) or URL-based response (learnerUrlResponse), but did not receive one." ); } } @@ -1882,21 +1904,21 @@ export class AttemptServiceV1 { question, createQuestionResponseAttemptRequestDto, assignmentContext, - assignmentId, + assignmentId ); } else if (question.responseType === "PRESENTATION") { return this.handleVideoPresentationQuestionResponse( question, createQuestionResponseAttemptRequestDto, assignmentContext, - assignmentId, + assignmentId ); } else { return this.handleFileUploadQuestionResponse( question, question.type, createQuestionResponseAttemptRequestDto, - assignmentContext, + assignmentContext ); } } @@ -1906,28 +1928,28 @@ export class AttemptServiceV1 { createQuestionResponseAttemptRequestDto, assignmentContext, assignmentId, - language, + language ); } case QuestionType.TRUE_FALSE: { return this.handleTrueFalseQuestionResponse( question, createQuestionResponseAttemptRequestDto, - language, + language ); } case QuestionType.SINGLE_CORRECT: { return this.handleSingleCorrectQuestionResponse( question, createQuestionResponseAttemptRequestDto, - language, + language ); } case QuestionType.MULTIPLE_CORRECT: { return this.handleMultipleCorrectQuestionResponse( question, createQuestionResponseAttemptRequestDto, - language, + language ); } default: { @@ -1943,14 +1965,14 @@ export class AttemptServiceV1 { assignmentInstructions: string; questionAnswerContext: QuestionAnswerContext[]; }, - assignmentId: number, + assignmentId: number ): Promise<{ responseDto: CreateQuestionResponseAttemptResponseDto; learnerResponse: LearnerPresentationResponse; }> { if (!createQuestionResponseAttemptRequestDto.learnerPresentationResponse) { throw new BadRequestException( - "Expected a presentation-based response (learnerPresentationResponse), but did not receive one.", + "Expected a presentation-based response (learnerPresentationResponse), but did not receive one." ); } @@ -1966,12 +1988,12 @@ export class AttemptServiceV1 { question.scoring?.type ?? "", question.scoring, question.type, - question.responseType ?? "OTHER", + question.responseType ?? "OTHER" ); const model = await this.llmFacadeService.gradePresentationQuestion( presentationQuestionEvaluateModel, - assignmentId, + assignmentId ); const responseDto = new CreateQuestionResponseAttemptResponseDto(); @@ -1986,14 +2008,14 @@ export class AttemptServiceV1 { assignmentInstructions: string; questionAnswerContext: QuestionAnswerContext[]; }, - assignmentId: number, + assignmentId: number ): Promise<{ responseDto: CreateQuestionResponseAttemptResponseDto; learnerResponse: LearnerPresentationResponse; }> { if (!createQuestionResponseAttemptRequestDto.learnerPresentationResponse) { throw new BadRequestException( - "Expected a presentation-based response (learnerPresentationResponse), but did not receive one.", + "Expected a presentation-based response (learnerPresentationResponse), but did not receive one." ); } @@ -2010,12 +2032,12 @@ export class AttemptServiceV1 { question.scoring, question.type, question.responseType ?? "OTHER", - question.videoPresentationConfig, + question.videoPresentationConfig ); const model = await this.llmFacadeService.gradeVideoPresentationQuestion( videoPresentationQuestionEvaluateModel, - assignmentId, + assignmentId ); const responseDto = new CreateQuestionResponseAttemptResponseDto(); @@ -2032,14 +2054,14 @@ export class AttemptServiceV1 { assignmentInstructions: string; questionAnswerContext: QuestionAnswerContext[]; }, - language?: string, + language?: string ): Promise<{ responseDto: CreateQuestionResponseAttemptResponseDto; learnerResponse: LearnerFileUpload[]; }> { if (!createQuestionResponseAttemptRequestDto.learnerFileResponse) { throw new BadRequestException( - "Expected a file-based response (learnerFileResponse), but did not receive one.", + "Expected a file-based response (learnerFileResponse), but did not receive one." ); } const learnerResponse = @@ -2053,12 +2075,12 @@ export class AttemptServiceV1 { question.scoring?.type ?? "", question.scoring, questionType, - question.responseType ?? "OTHER", + question.responseType ?? "OTHER" ); const model = await this.llmFacadeService.gradeFileBasedQuestion( fileUploadQuestionEvaluateModel, question.assignmentId, - language, + language ); const responseDto = new CreateQuestionResponseAttemptResponseDto(); @@ -2079,14 +2101,14 @@ export class AttemptServiceV1 { questionAnswerContext: QuestionAnswerContext[]; }, assignmentId: number, - language?: string, + language?: string ): Promise<{ responseDto: CreateQuestionResponseAttemptResponseDto; learnerResponse: string; }> { const learnerResponse = await AttemptHelper.validateAndGetTextResponse( questionType, - createQuestionResponseAttemptRequestDto, + createQuestionResponseAttemptRequestDto ); const textBasedQuestionEvaluateModel = new TextBasedQuestionEvaluateModel( @@ -2097,13 +2119,13 @@ export class AttemptServiceV1 { question.totalPoints, question.scoring?.type ?? "", question.scoring, - question.responseType ?? "OTHER", + question.responseType ?? "OTHER" ); const model = await this.llmFacadeService.gradeTextBasedQuestion( textBasedQuestionEvaluateModel, assignmentId, - language, + language ); const responseDto = new CreateQuestionResponseAttemptResponseDto(); @@ -2123,25 +2145,26 @@ export class AttemptServiceV1 { questionAnswerContext: QuestionAnswerContext[]; }, assignmentId: number, - language?: string, + language?: string ): Promise<{ responseDto: CreateQuestionResponseAttemptResponseDto; learnerResponse: string; }> { if (!createQuestionResponseAttemptRequestDto.learnerUrlResponse) { throw new BadRequestException( - "Expected a URL-based response (learnerUrlResponse), but did not receive one.", + "Expected a URL-based response (learnerUrlResponse), but did not receive one." ); } const learnerResponse = createQuestionResponseAttemptRequestDto.learnerUrlResponse; - const urlFetchResponse = - await AttemptHelper.fetchPlainTextFromUrl(learnerResponse); + const urlFetchResponse = await AttemptHelper.fetchPlainTextFromUrl( + learnerResponse + ); if (!urlFetchResponse.isFunctional) { throw new BadRequestException( - `Unable to extract content from the provided URL: ${learnerResponse}`, + `Unable to extract content from the provided URL: ${learnerResponse}` ); } @@ -2155,13 +2178,13 @@ export class AttemptServiceV1 { question.totalPoints, question.scoring?.type ?? "", question.scoring, - question.responseType ?? "OTHER", + question.responseType ?? "OTHER" ); const model = await this.llmFacadeService.gradeUrlBasedQuestion( urlBasedQuestionEvaluateModel, assignmentId, - language, + language ); const responseDto = new CreateQuestionResponseAttemptResponseDto(); @@ -2170,7 +2193,7 @@ export class AttemptServiceV1 { return { responseDto, learnerResponse }; } private getSafeChoices( - choices: Choice[] | string | null | undefined, + choices: Choice[] | string | null | undefined ): string | null { if (!choices) return; return typeof choices === "string" ? choices : JSON.stringify(choices); @@ -2181,7 +2204,7 @@ export class AttemptServiceV1 { private handleTrueFalseQuestionResponse( question: QuestionDto, createQuestionResponseAttemptRequestDto: CreateQuestionResponseAttemptRequestDto, - language: string, + language: string ): { responseDto: CreateQuestionResponseAttemptResponseDto; learnerResponse: string; @@ -2191,14 +2214,14 @@ export class AttemptServiceV1 { createQuestionResponseAttemptRequestDto.learnerAnswerChoice === undefined ) { throw new BadRequestException( - this.getLocalizedString("expectedTrueFalse", language), + this.getLocalizedString("expectedTrueFalse", language) ); } const learnerChoice = createQuestionResponseAttemptRequestDto.learnerAnswerChoice; if (learnerChoice === null) { throw new BadRequestException( - this.getLocalizedString("invalidTrueFalse", language), + this.getLocalizedString("invalidTrueFalse", language) ); } const correctAnswer = question.choices[0].isCorrect; @@ -2229,7 +2252,7 @@ export class AttemptServiceV1 { private async applyTranslationToQuestion( question: QuestionDto, language: string, - variantMapping?: { questionId: number; questionVariant: QuestionVariant }, + variantMapping?: { questionId: number; questionVariant: QuestionVariant } ): Promise { if (!language || language === "en") return question; @@ -2272,7 +2295,7 @@ export class AttemptServiceV1 { if (typeof translation.translatedChoices === "string") { try { question.choices = JSON.parse( - translation.translatedChoices, + translation.translatedChoices ) as Choice[]; } catch { question.choices = []; @@ -2291,11 +2314,11 @@ export class AttemptServiceV1 { */ private formatFeedback( feedbackTemplate: string, - data: { [key: string]: unknown }, + data: { [key: string]: unknown } ): string { return feedbackTemplate.replaceAll( /\${(.*?)}/g, - (_, g: string) => (data[g] as string) || "", + (_, g: string) => (data[g] as string) || "" ); } /** @@ -2304,7 +2327,7 @@ export class AttemptServiceV1 { private handleSingleCorrectQuestionResponse( question: QuestionDto, createQuestionResponseAttemptRequestDto: CreateQuestionResponseAttemptRequestDto, - language: string, + language: string ): { responseDto: CreateQuestionResponseAttemptResponseDto; learnerResponse: string; @@ -2317,7 +2340,7 @@ export class AttemptServiceV1 { const correctChoice = choices.find((choice) => choice.isCorrect); const selectedChoice = choices.find( - (choice) => this.normalizeText(choice.choice) === normalizedLearnerChoice, + (choice) => this.normalizeText(choice.choice) === normalizedLearnerChoice ); const data = { @@ -2367,7 +2390,7 @@ export class AttemptServiceV1 { private handleMultipleCorrectQuestionResponse( question: QuestionDto, createQuestionResponseAttemptRequestDto: CreateQuestionResponseAttemptRequestDto, - language: string, + language: string ): { responseDto: CreateQuestionResponseAttemptResponseDto; learnerResponse: string; @@ -2393,7 +2416,7 @@ export class AttemptServiceV1 { createQuestionResponseAttemptRequestDto.learnerChoices; const normalizedLearnerChoices = new Set( - learnerChoices.map((choice) => this.normalizeText(choice)), + learnerChoices.map((choice) => this.normalizeText(choice)) ); const choices = this.parseChoices(question.choices); @@ -2404,7 +2427,7 @@ export class AttemptServiceV1 { const correctChoices = choices.filter((choice) => choice.isCorrect) || []; const correctChoiceTexts = correctChoices.map((choice) => - this.normalizeText(choice.choice), + this.normalizeText(choice.choice) ); let totalPoints = 0; @@ -2413,7 +2436,7 @@ export class AttemptServiceV1 { for (const learnerChoice of learnerChoices) { const normalizedLearnerChoice = this.normalizeText(learnerChoice); const matchedChoice = normalizedChoices.find( - (item) => item.normalized === normalizedLearnerChoice, + (item) => item.normalized === normalizedLearnerChoice ); if (matchedChoice) { @@ -2426,7 +2449,7 @@ export class AttemptServiceV1 { if (matchedChoice.original.feedback) { choiceFeedback = this.formatFeedback( matchedChoice.original.feedback, - data, + data ); } else { choiceFeedback = matchedChoice.original.isCorrect @@ -2438,19 +2461,19 @@ export class AttemptServiceV1 { feedbackDetails.push( this.getLocalizedString("invalidSelection", language, { learnerChoice, - }), + }) ); } } const maxPoints = correctChoices.reduce( (accumulator, choice) => accumulator + choice.points, - 0, + 0 ); const finalPoints = Math.max(0, Math.min(totalPoints, maxPoints)); const allCorrectSelected = correctChoiceTexts.every((correctText) => - normalizedLearnerChoices.has(correctText), + normalizedLearnerChoices.has(correctText) ); const feedbackMessage = ` @@ -2490,11 +2513,11 @@ export class AttemptServiceV1 { * @returns The time range start date. */ private calculateTimeRangeStartDate( - assignment: LearnerGetAssignmentResponseDto, + assignment: LearnerGetAssignmentResponseDto ): Date { if (assignment.attemptsTimeRangeHours) { return new Date( - Date.now() - assignment.attemptsTimeRangeHours * 60 * 60 * 1000, + Date.now() - assignment.attemptsTimeRangeHours * 60 * 60 * 1000 ); } return new Date(); @@ -2508,7 +2531,7 @@ export class AttemptServiceV1 { */ private async countUserAttempts( userId: string, - assignmentId: number, + assignmentId: number ): Promise { return this.prisma.assignmentAttempt.count({ where: { @@ -2521,7 +2544,7 @@ export class AttemptServiceV1 { private getLocalizedString( key: string, language: string, - placeholders?: { [key: string]: string | number }, + placeholders?: { [key: string]: string | number } ): string { const translations: Record = { en: { @@ -2822,7 +2845,7 @@ export class AttemptServiceV1 { private parseBooleanResponse( learnerChoice: string, - language: string, + language: string ): boolean | null { const mapping: Record> = { en: { true: true, false: false }, @@ -2865,7 +2888,7 @@ export class AttemptServiceV1 { */ private constructQuestionsWithResponses( questions: Question[], - questionResponses: QuestionResponse[], + questionResponses: QuestionResponse[] ): AssignmentAttemptQuestions[] { return questions.map((question) => { const extendedQuestion = question as ExtendedQuestion; @@ -2916,7 +2939,7 @@ export class AttemptServiceV1 { points: choice.points, feedback: choice.feedback, isCorrect: choice.isCorrect, - }), + }) ); return { @@ -2964,7 +2987,7 @@ export class AttemptServiceV1 { assignmentAttemptId: number, role: UserRole, authorQuestions?: QuestionDto[], - assignmentDetails?: Assignment, + assignmentDetails?: Assignment ): Promise<{ assignmentInstructions: string; questionAnswerContext: QuestionAnswerContext[]; @@ -3038,8 +3061,9 @@ export class AttemptServiceV1 { groupedResponses[contextQuestion.id]?.learnerResponse || ""; if (contextQuestion.type === "URL" && learnerResponse) { - const urlContent = - await AttemptHelper.fetchPlainTextFromUrl(learnerResponse); + const urlContent = await AttemptHelper.fetchPlainTextFromUrl( + learnerResponse + ); learnerResponse = JSON.stringify({ url: learnerResponse, ...urlContent, @@ -3050,7 +3074,7 @@ export class AttemptServiceV1 { question: contextQuestion.question, answer: learnerResponse, }; - }), + }) ); } diff --git a/apps/api/src/api/assignment/attempt/dto/assignment-attempt/get.assignment.attempt.response.dto.ts b/apps/api/src/api/assignment/attempt/dto/assignment-attempt/get.assignment.attempt.response.dto.ts index d3f87a8d..060b5837 100644 --- a/apps/api/src/api/assignment/attempt/dto/assignment-attempt/get.assignment.attempt.response.dto.ts +++ b/apps/api/src/api/assignment/attempt/dto/assignment-attempt/get.assignment.attempt.response.dto.ts @@ -1,6 +1,11 @@ import { Optional } from "@nestjs/common"; import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; -import { QuestionResponse, QuestionType, ResponseType } from "@prisma/client"; +import { + CorrectAnswerVisibility, + QuestionResponse, + QuestionType, + ResponseType, +} from "@prisma/client"; import { Type } from "class-transformer"; import { AttemptQuestionDto } from "src/api/assignment/dto/update.questions.request.dto"; import { Choice } from "../../../question/dto/create.update.question.request.dto"; @@ -91,7 +96,7 @@ export class GetAssignmentAttemptResponseDto extends AssignmentAttemptResponseDt type: Boolean, required: false, }) - showCorrectAnswer: boolean; + correctAnswerVisibility: CorrectAnswerVisibility; @ApiPropertyOptional({ description: "The comments for the question.", diff --git a/apps/api/src/api/assignment/attempt/dto/assignment-attempt/update.assignment.attempt.response.dto.ts b/apps/api/src/api/assignment/attempt/dto/assignment-attempt/update.assignment.attempt.response.dto.ts index 2d899bfc..cf5059f9 100644 --- a/apps/api/src/api/assignment/attempt/dto/assignment-attempt/update.assignment.attempt.response.dto.ts +++ b/apps/api/src/api/assignment/attempt/dto/assignment-attempt/update.assignment.attempt.response.dto.ts @@ -1,4 +1,5 @@ import { ApiProperty } from "@nestjs/swagger"; +import { CorrectAnswerVisibility } from "@prisma/client"; import { IsArray, IsOptional } from "class-validator"; import type { CreateQuestionResponseAttemptResponseDto } from "../question-response/create.question.response.attempt.response.dto"; import { BaseAssignmentAttemptResponseDto } from "./base.assignment.attempt.response.dto"; @@ -47,7 +48,7 @@ export class UpdateAssignmentAttemptResponseDto extends BaseAssignmentAttemptRes type: Boolean, required: false, }) - showCorrectAnswer: boolean; + correctAnswerVisibility: CorrectAnswerVisibility; @ApiProperty({ description: "The total points earned by the learner.", diff --git a/apps/api/src/api/assignment/dto/get.assignment.response.dto.ts b/apps/api/src/api/assignment/dto/get.assignment.response.dto.ts index e66190ff..bc78249e 100644 --- a/apps/api/src/api/assignment/dto/get.assignment.response.dto.ts +++ b/apps/api/src/api/assignment/dto/get.assignment.response.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty, OmitType } from "@nestjs/swagger"; -import { Question } from "@prisma/client"; +import { AssignmentVersion, Question } from "@prisma/client"; import { UpdateAssignmentRequestDto } from "./update.assignment.request.dto"; export class AssignmentResponseDto extends UpdateAssignmentRequestDto { @@ -42,8 +42,9 @@ export class GetAssignmentResponseDto extends AssignmentResponseDto { export class LearnerGetAssignmentResponseDto extends OmitType( GetAssignmentResponseDto, - ["questions", "displayOrder"] as const, + ["questions", "displayOrder"] as const ) { + currentVersion?: AssignmentVersion; questions: Question[]; displayOrder: string; } diff --git a/apps/api/src/api/assignment/dto/update.assignment.request.dto.ts b/apps/api/src/api/assignment/dto/update.assignment.request.dto.ts index 3e98699e..860d2734 100644 --- a/apps/api/src/api/assignment/dto/update.assignment.request.dto.ts +++ b/apps/api/src/api/assignment/dto/update.assignment.request.dto.ts @@ -1,6 +1,7 @@ import { ApiProperty } from "@nestjs/swagger"; import { AssignmentQuestionDisplayOrder, + CorrectAnswerVisibility, QuestionDisplay, } from "@prisma/client"; import { @@ -201,12 +202,9 @@ export class UpdateAssignmentRequestDto { showSubmissionFeedback: boolean; @ApiProperty({ - description: - "Should the correct answer be shown to the learner after its submission", - type: Boolean, + description: "When should correct answers be shown to learners", required: false, }) @IsOptional() - @IsBoolean() - showCorrectAnswer: boolean; + correctAnswerVisibility: CorrectAnswerVisibility; } diff --git a/apps/api/src/api/assignment/dto/update.questions.request.dto.ts b/apps/api/src/api/assignment/dto/update.questions.request.dto.ts index f76fbaf3..130b26b1 100644 --- a/apps/api/src/api/assignment/dto/update.questions.request.dto.ts +++ b/apps/api/src/api/assignment/dto/update.questions.request.dto.ts @@ -1,6 +1,7 @@ import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; import { AssignmentQuestionDisplayOrder, + CorrectAnswerVisibility, QuestionDisplay, QuestionType, ResponseType, @@ -547,8 +548,7 @@ export class UpdateAssignmentQuestionsDto { required: false, }) @IsOptional() - @IsBoolean() - showCorrectAnswer: boolean; + correctAnswerVisibility: CorrectAnswerVisibility; @ApiProperty({ description: "updatedAt", diff --git a/apps/api/src/api/assignment/question/question.controller.ts b/apps/api/src/api/assignment/question/question.controller.ts index e537cb3b..982d2731 100644 --- a/apps/api/src/api/assignment/question/question.controller.ts +++ b/apps/api/src/api/assignment/question/question.controller.ts @@ -44,7 +44,7 @@ export class QuestionController { private logger; constructor( @Inject(WINSTON_MODULE_PROVIDER) private parentLogger: Logger, - private readonly questionService: QuestionService, + private readonly questionService: QuestionService ) { this.logger = parentLogger.child({ context: QuestionController.name }); } @@ -61,11 +61,11 @@ export class QuestionController { @ApiResponse({ status: 403 }) createQuestion( @Param("assignmentId") assignmentId: number, - @Body() createQuestionRequestDto: CreateUpdateQuestionRequestDto, + @Body() createQuestionRequestDto: CreateUpdateQuestionRequestDto ): Promise { return this.questionService.create( Number(assignmentId), - createQuestionRequestDto, + createQuestionRequestDto ); } @@ -92,12 +92,12 @@ export class QuestionController { updateQuestion( @Param("assignmentId") assignmentId: number, @Param("id") id: number, - @Body() updateQuestionRequestDto: CreateUpdateQuestionRequestDto, + @Body() updateQuestionRequestDto: CreateUpdateQuestionRequestDto ): Promise { return this.questionService.update( Number(assignmentId), Number(id), - updateQuestionRequestDto, + updateQuestionRequestDto ); } @@ -114,12 +114,12 @@ export class QuestionController { replaceQuestion( @Param("assignmentId") assignmentId: number, @Param("id") id: number, - @Body() updateQuestionRequestDto: CreateUpdateQuestionRequestDto, + @Body() updateQuestionRequestDto: CreateUpdateQuestionRequestDto ): Promise { return this.questionService.replace( Number(assignmentId), Number(id), - updateQuestionRequestDto, + updateQuestionRequestDto ); } @@ -147,13 +147,13 @@ export class QuestionController { question: QuestionDto; rubricIndex: number; }, - @Req() request: UserSessionRequest, + @Req() request: UserSessionRequest ): Promise { const { question, rubricIndex } = body; return await this.questionService.createMarkingRubric( question, request.userSession.assignmentId, - rubricIndex, + rubricIndex ); } @@ -170,12 +170,12 @@ export class QuestionController { body: { question: QuestionDto; }, - @Req() request: UserSessionRequest, + @Req() request: UserSessionRequest ): Promise { const { question } = body; return await this.questionService.expandMarkingRubric( question, - request.userSession.assignmentId, + request.userSession.assignmentId ); } @@ -194,7 +194,7 @@ export class QuestionController { @Body() body: { liveRecordingData: LearnerLiveRecordingFeedback; - }, + } ): Promise<{ feedback: string }> { const { liveRecordingData } = body; @@ -203,7 +203,7 @@ export class QuestionController { } return this.questionService.getLiveRecordingFeedback( liveRecordingData, - Number(assignmentId), + Number(assignmentId) ); } @Post(":id/translations") @@ -221,21 +221,37 @@ export class QuestionController { @Param("id") questionId: number, @Body() body: { - question: QuestionDto; selectedLanguage: string; selectedLanguageCode: string; }, - ): Promise<{ translatedQuestion: string; translatedChoices?: Choice[] }> { - const { selectedLanguage, selectedLanguageCode, question } = body; - const { assignmentId } = question; + @Req() request: UserSessionRequest + ): Promise<{ + translatedQuestion: string; + translatedChoices?: Choice[] | { id: number | null; choice: string }[]; + }> { + const { selectedLanguage, selectedLanguageCode } = body; + const { userSession } = request; + + const question = await this.questionService.findOneForTranslation( + Number(questionId) + ); + const { translatedQuestion, translatedChoices } = await this.questionService.generateTranslationIfNotExists( - assignmentId, + question.assignmentId, question, selectedLanguageCode, - selectedLanguage, + selectedLanguage ); + if (userSession.role === UserRole.LEARNER) { + const safeChoices = translatedChoices?.map((choice) => ({ + id: choice.id || null, + choice: choice.choice, + })); + return { translatedQuestion, translatedChoices: safeChoices }; + } + return { translatedQuestion, translatedChoices }; } } diff --git a/apps/api/src/api/assignment/question/question.service.ts b/apps/api/src/api/assignment/question/question.service.ts index 9ee7657a..25d9598b 100644 --- a/apps/api/src/api/assignment/question/question.service.ts +++ b/apps/api/src/api/assignment/question/question.service.ts @@ -98,6 +98,61 @@ export class QuestionService { }; } + /** + * Find a question for translation purposes - returns basic question data without sensitive information + */ + async findOneForTranslation(id: number): Promise { + if (!id || Number.isNaN(Number(id))) { + throw new NotFoundException(`Question with ID ${id} not found`); + } + + const result = await this.prisma.question.findUnique({ + where: { id }, + select: { + id: true, + question: true, + type: true, + assignmentId: true, + totalPoints: true, + maxWords: true, + maxCharacters: true, + choices: true, + scoring: true, + gradingContextQuestionIds: true, + responseType: true, + isDeleted: true, + randomizedChoices: true, + videoPresentationConfig: true, + liveRecordingConfig: true, + // Exclude 'answer' field to prevent exposure of correct answers + }, + }); + + if (!result) { + throw new NotFoundException(`Question with Id ${id} not found.`); + } + + return { + ...result, + answer: undefined, // Explicitly exclude answer + scoring: result.scoring + ? (result.scoring as unknown as ScoringDto) + : undefined, + choices: result.choices + ? (result.choices as unknown as Choice[]) + : undefined, + assignmentId: result.assignmentId, + videoPresentationConfig: result.videoPresentationConfig + ? (result.videoPresentationConfig as unknown as VideoPresentationConfig) + : undefined, + liveRecordingConfig: result.liveRecordingConfig + ? (result.liveRecordingConfig as unknown as object) + : undefined, + alreadyInBackend: true, + success: true, + }; + } + async update( assignmentId: number, id: number, diff --git a/apps/api/src/api/assignment/v2/repositories/assignment.repository.ts b/apps/api/src/api/assignment/v2/repositories/assignment.repository.ts index b70ea837..e2fb1107 100644 --- a/apps/api/src/api/assignment/v2/repositories/assignment.repository.ts +++ b/apps/api/src/api/assignment/v2/repositories/assignment.repository.ts @@ -42,7 +42,7 @@ export class AssignmentRepository { async findById( id: number, - userSession?: UserSession, + userSession?: UserSession ): Promise { const isLearner = userSession?.role === UserRole.LEARNER; @@ -88,7 +88,7 @@ export class AssignmentRepository { */ async findAllForUser( - userSession: UserSession, + userSession: UserSession ): Promise { // If user is an author, only show assignments they've authored if (userSession.role === UserRole.AUTHOR) { @@ -130,6 +130,7 @@ export class AssignmentRepository { * @returns Updated assignment */ async update(id: number, data: Partial): Promise { + console.log("Updating assignment with ID:", data); try { return await this.prisma.assignment.update({ where: { id }, @@ -142,7 +143,7 @@ export class AssignmentRepository { error instanceof Error ? error.stack : "No stack trace"; this.logger.error( `Error updating assignment ${id}: ${errorMessage}`, - errorStack, + errorStack ); throw error; } @@ -171,7 +172,7 @@ export class AssignmentRepository { error instanceof Error ? error.stack : "No stack trace"; this.logger.error( `Error replacing assignment ${id}: ${errorMessage}`, - errorStack, + errorStack ); throw error; } @@ -186,10 +187,10 @@ export class AssignmentRepository { private processAssignmentData( rawAssignment: Assignment & { questions: (Question & { variants: QuestionVariant[] })[]; - }, + } ): Assignment & { questions: QuestionDto[] } { const assignment = JSON.parse( - JSON.stringify(rawAssignment), + JSON.stringify(rawAssignment) ) as Assignment & { questions: QuestionDto[] }; const questions = Array.isArray(assignment.questions) @@ -205,7 +206,7 @@ export class AssignmentRepository { scoring: this.parseJsonField(q.scoring), choices: this.parseJsonField(q.choices), videoPresentationConfig: this.parseJsonField( - q.videoPresentationConfig, + q.videoPresentationConfig ), }; @@ -232,7 +233,7 @@ export class AssignmentRepository { filteredQuestions.sort( (a, b) => assignment.questionOrder.indexOf(a.id) - - assignment.questionOrder.indexOf(b.id), + assignment.questionOrder.indexOf(b.id) ); } @@ -257,7 +258,7 @@ export class AssignmentRepository { error instanceof Error ? error.stack : "No stack trace"; this.logger.error( `Error parsing JSON field: ${errorMessage}`, - errorStack, + errorStack ); return undefined; } diff --git a/apps/api/src/api/assignment/v2/services/assignment.service.ts b/apps/api/src/api/assignment/v2/services/assignment.service.ts index 9a7b1cbb..bad2fa20 100644 --- a/apps/api/src/api/assignment/v2/services/assignment.service.ts +++ b/apps/api/src/api/assignment/v2/services/assignment.service.ts @@ -39,7 +39,7 @@ export class AssignmentServiceV2 { private readonly versionManagementService: VersionManagementService, private readonly jobStatusService: JobStatusServiceV2, private readonly prisma: PrismaService, - @Inject(WINSTON_MODULE_PROVIDER) private parentLogger: Logger, + @Inject(WINSTON_MODULE_PROVIDER) private parentLogger: Logger ) { this.logger = parentLogger.child({ context: "AssignmentServiceV2" }); } @@ -56,17 +56,17 @@ export class AssignmentServiceV2 { async getAssignment( assignmentId: number, userSession: UserSession, - languageCode?: string, + languageCode?: string ): Promise { const assignment = await this.assignmentRepository.findById( assignmentId, - userSession, + userSession ); if (languageCode) { await this.translationService.applyTranslationsToAssignment( assignment, - languageCode, + languageCode ); } @@ -81,7 +81,7 @@ export class AssignmentServiceV2 { */ async listAssignments( - userSession: UserSession, + userSession: UserSession ): Promise { return this.assignmentRepository.findAllForUser(userSession); } @@ -96,13 +96,13 @@ export class AssignmentServiceV2 { async updateAssignment( id: number, - updateDto: UpdateAssignmentRequestDto, + updateDto: UpdateAssignmentRequestDto ): Promise { const existingAssignment = await this.assignmentRepository.findById(id); const shouldTranslate = this.shouldTranslateAssignment( existingAssignment, - updateDto, + updateDto ); const result = await this.assignmentRepository.update(id, updateDto); @@ -130,7 +130,7 @@ export class AssignmentServiceV2 { */ async replaceAssignment( id: number, - replaceDto: ReplaceAssignmentRequestDto, + replaceDto: ReplaceAssignmentRequestDto ): Promise { const result = await this.assignmentRepository.replace(id, replaceDto); @@ -161,14 +161,14 @@ export class AssignmentServiceV2 { async publishAssignment( assignmentId: number, updateDto: UpdateAssignmentQuestionsDto, - userId: string, + userId: string ): Promise<{ jobId: number; message: string }> { this.logger.info( - `📦 PUBLISH REQUEST: Received updateDto with versionNumber: ${updateDto.versionNumber}, versionDescription: ${updateDto.versionDescription}`, + `📦 PUBLISH REQUEST: Received updateDto with versionNumber: ${updateDto.versionNumber}, versionDescription: ${updateDto.versionDescription}` ); const job = await this.jobStatusService.createPublishJob( assignmentId, - userId, + userId ); this.startPublishingProcess(job.id, assignmentId, updateDto, userId).catch( @@ -181,7 +181,7 @@ export class AssignmentServiceV2 { status: "Failed", progress: `Error: ${errorMessage}`, }); - }, + } ); return { @@ -193,7 +193,7 @@ export class AssignmentServiceV2 { jobId: number, assignmentId: number, updateDto: UpdateAssignmentQuestionsDto, - userId: string, + userId: string ): Promise { try { // Progress allocation: @@ -209,13 +209,14 @@ export class AssignmentServiceV2 { percentage: 5, }); - const existingAssignment = - await this.assignmentRepository.findById(assignmentId); + const existingAssignment = await this.assignmentRepository.findById( + assignmentId + ); const assignmentTranslatableFieldsChanged = this.haveTranslatableAssignmentFieldsChanged( existingAssignment, - updateDto, + updateDto ); await this.assignmentRepository.update(assignmentId, { @@ -232,7 +233,7 @@ export class AssignmentServiceV2 { showAssignmentScore: updateDto.showAssignmentScore, showQuestionScore: updateDto.showQuestionScore, showSubmissionFeedback: updateDto.showSubmissionFeedback, - showCorrectAnswer: updateDto.showCorrectAnswer, + correctAnswerVisibility: updateDto.correctAnswerVisibility, timeEstimateMinutes: updateDto.timeEstimateMinutes, showQuestions: updateDto.showQuestions, numberOfQuestionsPerAttempt: updateDto.numberOfQuestionsPerAttempt, @@ -263,7 +264,7 @@ export class AssignmentServiceV2 { this.logger.warn( `Failed to store assignment author: ${ error instanceof Error ? error.message : "Unknown error" - }`, + }` ); } @@ -281,7 +282,7 @@ export class AssignmentServiceV2 { questionContentChanged = this.haveQuestionContentsChanged( existingQuestions, - updateDto.questions, + updateDto.questions ); await this.jobStatusService.updateJobStatus(jobId, { @@ -303,7 +304,7 @@ export class AssignmentServiceV2 { progress: `Processing questions: ${childProgress}% complete`, percentage: Math.floor(mappedProgress), }); - }, + } ); } @@ -348,12 +349,12 @@ export class AssignmentServiceV2 { } else { // Only do expensive validation if quick check fails this.logger.warn( - `Quick validation failed for assignment ${assignmentId}, running full validation`, + `Quick validation failed for assignment ${assignmentId}, running full validation` ); const languageValidation = await this.translationService.validateAssignmentLanguageConsistency( - assignmentId, + assignmentId ); if (languageValidation.isConsistent) { @@ -365,8 +366,8 @@ export class AssignmentServiceV2 { } else { this.logger.warn( `Language consistency issues detected for assignment ${assignmentId}: ${languageValidation.mismatchedLanguages.join( - ", ", - )}`, + ", " + )}` ); // Language mismatch detected - force retranslation for affected languages @@ -379,7 +380,7 @@ export class AssignmentServiceV2 { await this.translationService.retranslateAssignmentForLanguages( assignmentId, languageValidation.mismatchedLanguages, - jobId, + jobId ); await this.jobStatusService.updateJobStatus(jobId, { @@ -408,13 +409,13 @@ export class AssignmentServiceV2 { const translationCompleteness = await this.translationService.ensureTranslationCompleteness( - assignmentId, + assignmentId ); if (!translationCompleteness.isComplete) { this.logger.warn( `Missing translations detected for assignment ${assignmentId}. Attempting to fix...`, - { missingTranslations: translationCompleteness.missingTranslations }, + { missingTranslations: translationCompleteness.missingTranslations } ); // Attempt to fix missing translations @@ -427,12 +428,12 @@ export class AssignmentServiceV2 { missing.variantId ? `variant ${missing.variantId}` : `question ${missing.questionId}` - }: ${missing.missingLanguages.join(", ")}`, + }: ${missing.missingLanguages.join(", ")}` ); } catch (error) { this.logger.error( `Failed to fix missing translation for question ${missing.questionId}`, - error, + error ); } } @@ -469,7 +470,7 @@ export class AssignmentServiceV2 { `Found ${updatedQuestions.length} questions after processing for assignment ${assignmentId}`, { questionIds: updatedQuestions.map((q) => q.id), - }, + } ); // Create a new version when publishing - AFTER questions are processed and committed @@ -481,7 +482,7 @@ export class AssignmentServiceV2 { try { this.logger.info( - `Managing version after question processing - found ${updatedQuestions.length} questions`, + `Managing version after question processing - found ${updatedQuestions.length} questions` ); const userSession = { @@ -494,7 +495,7 @@ export class AssignmentServiceV2 { const existingDraft = await this.versionManagementService.getUserLatestDraft( assignmentId, - userSession, + userSession ); // Check for recently created unpublished versions (to prevent duplicates from frontend) @@ -513,7 +514,7 @@ export class AssignmentServiceV2 { const draftVersionId = existingDraft._draftVersionId; this.logger.info( `Found existing draft version, publishing it instead of creating new version`, - { draftVersionId }, + { draftVersionId } ); // Update the existing draft with current content first @@ -533,13 +534,13 @@ export class AssignmentServiceV2 { `Published version - ${new Date().toLocaleDateString()}`, versionNumber: updateDto.versionNumber, }, - userSession, + userSession ); // Then publish the updated draft versionResult = await this.versionManagementService.publishVersion( assignmentId, - draftVersionId, + draftVersionId ); } else if ( latestVersion && @@ -552,20 +553,20 @@ export class AssignmentServiceV2 { { versionId: latestVersion.id, versionNumber: latestVersion.versionNumber, - }, + } ); versionResult = await this.versionManagementService.publishVersion( assignmentId, - latestVersion.id, + latestVersion.id ); } else if (!existingDraft && updateDto.published) { // No existing draft and no unpublished version - create new version directly this.logger.info( - `No existing draft or unpublished version found, creating new version directly`, + `No existing draft or unpublished version found, creating new version directly` ); this.logger.info( - `UpdateDto contains versionNumber: ${updateDto.versionNumber}, versionDescription: ${updateDto.versionDescription}`, + `UpdateDto contains versionNumber: ${updateDto.versionNumber}, versionDescription: ${updateDto.versionDescription}` ); versionResult = await this.versionManagementService.createVersion( @@ -578,7 +579,7 @@ export class AssignmentServiceV2 { isDraft: false, // Create as published directly shouldActivate: true, }, - userSession, + userSession ); } else { // Create or update draft version (not publishing) @@ -600,7 +601,7 @@ export class AssignmentServiceV2 { `Draft - ${new Date().toLocaleDateString()}`, versionNumber: updateDto.versionNumber, }, - userSession, + userSession ); } @@ -611,7 +612,7 @@ export class AssignmentServiceV2 { isDraft: versionResult.isDraft, isActive: versionResult.isActive, published: versionResult.published, - }, + } ); } catch (versionError) { // Log the full error details @@ -627,7 +628,7 @@ export class AssignmentServiceV2 { assignmentId, userId, questionsFound: updatedQuestions.length, - }, + } ); } @@ -646,7 +647,7 @@ export class AssignmentServiceV2 { const errorStack = error instanceof Error ? error.stack : undefined; this.logger.error( `Publishing process failed: ${errorMessage}`, - errorStack, + errorStack ); await this.jobStatusService.updateJobStatus(jobId, { status: "Failed", @@ -658,7 +659,7 @@ export class AssignmentServiceV2 { private safeStringCompare = ( string1: string | null | undefined, - string2: string | null | undefined, + string2: string | null | undefined ): boolean => { const normalizedString1 = string1 === null || string1 === undefined ? "" : String(string1); @@ -674,11 +675,11 @@ export class AssignmentServiceV2 { existingAssignment: | GetAssignmentResponseDto | LearnerGetAssignmentResponseDto, - updateDto: UpdateAssignmentRequestDto | UpdateAssignmentQuestionsDto, + updateDto: UpdateAssignmentRequestDto | UpdateAssignmentQuestionsDto ): boolean { if (existingAssignment.graded !== updateDto.graded) { this.logger.debug( - "Graded status changed, but this doesn't trigger translation", + "Graded status changed, but this doesn't trigger translation" ); } @@ -692,7 +693,7 @@ export class AssignmentServiceV2 { updateDto.instructions !== null && !this.safeStringCompare( existingAssignment.instructions, - updateDto.instructions, + updateDto.instructions ); const introductionChanged = @@ -700,7 +701,7 @@ export class AssignmentServiceV2 { updateDto.introduction !== null && !this.safeStringCompare( existingAssignment.introduction, - updateDto.introduction, + updateDto.introduction ); const gradingCriteriaChanged = @@ -708,7 +709,7 @@ export class AssignmentServiceV2 { updateDto.gradingCriteriaOverview !== null && !this.safeStringCompare( existingAssignment.gradingCriteriaOverview, - updateDto.gradingCriteriaOverview, + updateDto.gradingCriteriaOverview ); if ( nameChanged || @@ -738,17 +739,17 @@ export class AssignmentServiceV2 { */ private haveQuestionContentsChanged( existingQuestions: QuestionDto[], - updatedQuestions: QuestionDto[], + updatedQuestions: QuestionDto[] ): boolean { if (existingQuestions.length !== updatedQuestions.length) { this.logger.debug( - `Question count changed: ${existingQuestions.length} → ${updatedQuestions.length}`, + `Question count changed: ${existingQuestions.length} → ${updatedQuestions.length}` ); return true; } this.logger.debug( - `Comparing ${existingQuestions.length} questions for content changes`, + `Comparing ${existingQuestions.length} questions for content changes` ); const existingQuestionsMap = new Map(); @@ -781,7 +782,7 @@ export class AssignmentServiceV2 { if ( !this.safeStringCompare( updatedQuestion.question, - existingQuestion.question, + existingQuestion.question ) ) { this.logger.debug(`Question #${updatedQuestion.id} text changed`); @@ -790,14 +791,14 @@ export class AssignmentServiceV2 { if (updatedQuestion.type !== existingQuestion.type) { this.logger.debug( - `Question #${updatedQuestion.id} type changed: ${existingQuestion.type} → ${updatedQuestion.type}`, + `Question #${updatedQuestion.id} type changed: ${existingQuestion.type} → ${updatedQuestion.type}` ); return true; } const choicesEqual = this.areChoicesEqual( updatedQuestion.choices, - existingQuestion.choices, + existingQuestion.choices ); if (!choicesEqual) { this.logger.debug(`Question #${updatedQuestion.id} choices changed`); @@ -807,7 +808,7 @@ export class AssignmentServiceV2 { const variantsChanged = this.haveVariantsChanged( existingQuestion.variants, updatedQuestion.variants, - updatedQuestion.id, + updatedQuestion.id ); if (variantsChanged) { @@ -817,13 +818,13 @@ export class AssignmentServiceV2 { if (updatedQuestion.totalPoints !== existingQuestion.totalPoints) { this.logger.debug( - `Question #${updatedQuestion.id} points changed: ${existingQuestion.totalPoints} → ${updatedQuestion.totalPoints} (non-translatable)`, + `Question #${updatedQuestion.id} points changed: ${existingQuestion.totalPoints} → ${updatedQuestion.totalPoints} (non-translatable)` ); } if (updatedQuestion.maxWords !== existingQuestion.maxWords) { this.logger.debug( - `Question #${updatedQuestion.id} maxWords changed: ${existingQuestion.maxWords} → ${updatedQuestion.maxWords} (non-translatable)`, + `Question #${updatedQuestion.id} maxWords changed: ${existingQuestion.maxWords} → ${updatedQuestion.maxWords} (non-translatable)` ); } } @@ -838,7 +839,7 @@ export class AssignmentServiceV2 { private haveVariantsChanged( variants1?: VariantDto[], variants2?: VariantDto[], - questionId?: number, + questionId?: number ): boolean { const logPrefix = questionId ? `Question #${questionId} variants: ` @@ -846,28 +847,28 @@ export class AssignmentServiceV2 { if (!variants1 && !variants2) { this.logger.debug( - `${logPrefix}Both variant arrays are null/undefined (no change)`, + `${logPrefix}Both variant arrays are null/undefined (no change)` ); return false; } if (!variants1 || !variants2) { this.logger.debug( - `${logPrefix}One variant array is null/undefined (change detected)`, + `${logPrefix}One variant array is null/undefined (change detected)` ); return true; } if (variants1.length !== variants2.length) { this.logger.debug( - `${logPrefix}Variant count changed: ${variants1.length} → ${variants2.length}`, + `${logPrefix}Variant count changed: ${variants1.length} → ${variants2.length}` ); return true; } if (variants1.length === 0) { this.logger.debug( - `${logPrefix}Both variant arrays are empty (no change)`, + `${logPrefix}Both variant arrays are empty (no change)` ); return false; } @@ -875,10 +876,10 @@ export class AssignmentServiceV2 { this.logger.debug(`${logPrefix}Comparing ${variants1.length} variants`); const sortedVariants1 = [...variants1].sort( - (a, b) => (a.id || 0) - (b.id || 0), + (a, b) => (a.id || 0) - (b.id || 0) ); const sortedVariants2 = [...variants2].sort( - (a, b) => (a.id || 0) - (b.id || 0), + (a, b) => (a.id || 0) - (b.id || 0) ); for (const [index, v1] of sortedVariants1.entries()) { @@ -887,7 +888,7 @@ export class AssignmentServiceV2 { this.logger.debug(`${logPrefix}Comparing variant #${index + 1}: Content: "${v1.variantContent.slice( 0, - 30, + 30 )}..." → "${v2.variantContent.slice(0, 30)}..." Choices Count: ${v1.choices?.length || 0} → ${v2.choices?.length || 0} `); @@ -916,10 +917,10 @@ export class AssignmentServiceV2 { if (!choices1 || !choices2) return false; if (choices1.length !== choices2.length) return false; const sortedChoices1 = [...choices1].sort( - (a, b) => (a.id || 0) - (b.id || 0), + (a, b) => (a.id || 0) - (b.id || 0) ); const sortedChoices2 = [...choices2].sort( - (a, b) => (a.id || 0) - (b.id || 0), + (a, b) => (a.id || 0) - (b.id || 0) ); for (const [index, c1] of sortedChoices1.entries()) { const c2 = sortedChoices2[index]; @@ -947,7 +948,7 @@ export class AssignmentServiceV2 { existingAssignment: | GetAssignmentResponseDto | LearnerGetAssignmentResponseDto, - updateDto: UpdateAssignmentRequestDto, + updateDto: UpdateAssignmentRequestDto ): boolean { return ( (updateDto.name && updateDto.name !== existingAssignment.name) || diff --git a/apps/api/src/api/assignment/v2/services/version-management.service.ts b/apps/api/src/api/assignment/v2/services/version-management.service.ts index 66c88ecc..7e6d0820 100644 --- a/apps/api/src/api/assignment/v2/services/version-management.service.ts +++ b/apps/api/src/api/assignment/v2/services/version-management.service.ts @@ -91,7 +91,7 @@ export class VersionManagementService { constructor( private readonly prisma: PrismaService, - @Inject(WINSTON_MODULE_PROVIDER) private parentLogger: Logger, + @Inject(WINSTON_MODULE_PROVIDER) private parentLogger: Logger ) { this.logger = parentLogger.child({ context: "VersionManagementService" }); } @@ -136,14 +136,14 @@ export class VersionManagementService { async createVersion( assignmentId: number, createVersionDto: CreateVersionDto, - userSession: UserSession, + userSession: UserSession ): Promise { this.logger.info( `🚀 CREATE VERSION: Starting for assignment ${assignmentId}`, { createVersionDto, userId: userSession.userId, - }, + } ); // Log the key decision factors @@ -177,7 +177,7 @@ export class VersionManagementService { // If no legacy questions found, this might be a new assignment created through version control if (assignment.questions.length === 0) { this.logger.warn( - `No legacy questions found for assignment ${assignmentId}. Creating version with empty questions.`, + `No legacy questions found for assignment ${assignmentId}. Creating version with empty questions.` ); } @@ -189,7 +189,7 @@ export class VersionManagementService { const semanticVersionRegex = /^\d+\.\d+\.\d+(?:-rc\d+)?$/; if (!semanticVersionRegex.test(createVersionDto.versionNumber)) { throw new BadRequestException( - "Version number must follow semantic versioning format (e.g., '1.0.0' or '1.0.0-rc1')", + "Version number must follow semantic versioning format (e.g., '1.0.0' or '1.0.0-rc1')" ); } versionNumber = createVersionDto.versionNumber; @@ -229,13 +229,13 @@ export class VersionManagementService { versionId: createVersionDto.versionId, versionNumber: createVersionDto.versionNumber, versionDescription: createVersionDto.versionDescription, - }, + } ); return await this.updateExistingVersion( assignmentId, createVersionDto.versionId, createVersionDto, - userSession, + userSession ); } @@ -256,13 +256,13 @@ export class VersionManagementService { if (createVersionDto.updateExisting) { // Update existing version instead of creating new one (fallback to version number lookup) this.logger.info( - `Updating existing version ${existingVersion.id} found by version number ${finalVersionNumber}`, + `Updating existing version ${existingVersion.id} found by version number ${finalVersionNumber}` ); return await this.updateExistingVersion( assignmentId, existingVersion.id, createVersionDto, - userSession, + userSession ); } else { // Return a special response indicating the version exists @@ -315,6 +315,7 @@ export class VersionManagementService { showQuestionScore: assignment.showQuestionScore, showSubmissionFeedback: assignment.showSubmissionFeedback, showQuestions: assignment.showQuestions, + correctAnswerVisibility: assignment.correctAnswerVisibility, languageCode: assignment.languageCode, createdBy: userSession.userId, isDraft: createVersionDto.isDraft ?? true, @@ -329,7 +330,7 @@ export class VersionManagementService { // Create question versions this.logger.info( - `Creating ${assignment.questions.length} question versions for assignment version ${assignmentVersion.id}`, + `Creating ${assignment.questions.length} question versions for assignment version ${assignmentVersion.id}` ); for (const [index, question] of assignment.questions.entries()) { @@ -357,12 +358,12 @@ export class VersionManagementService { this.logger.debug( `Created question version ${questionVersion.id} for question ${ question.id - } (${question.question.slice(0, 50)}...)`, + } (${question.question.slice(0, 50)}...)` ); } this.logger.info( - `Successfully created all ${assignment.questions.length} question versions`, + `Successfully created all ${assignment.questions.length} question versions` ); // Update current version if should activate @@ -403,7 +404,7 @@ export class VersionManagementService { originalVersionNumber: wasAutoIncremented ? originalVersionNumber : undefined, - }, + } ); return { @@ -459,7 +460,7 @@ export class VersionManagementService { async getVersion( assignmentId: number, - versionId: number, + versionId: number ): Promise< AssignmentVersion & { questionVersions: any[]; @@ -485,7 +486,7 @@ export class VersionManagementService { questionId: qv.questionId, question: qv.question?.slice(0, 50) + "...", })), - }, + } ); // Transform the response to match the expected format @@ -519,6 +520,7 @@ export class VersionManagementService { showQuestionScore: version.showQuestionScore, showSubmissionFeedback: version.showSubmissionFeedback, showQuestions: version.showQuestions, + correctAnswerVisibility: version.correctAnswerVisibility, languageCode: version.languageCode, // Transform questionVersions to the expected questions format questionVersions: version.questionVersions.map((qv) => ({ @@ -545,7 +547,7 @@ export class VersionManagementService { async saveDraft( assignmentId: number, saveDraftDto: SaveDraftDto, - userSession: UserSession, + userSession: UserSession ): Promise { this.logger.info(`Saving draft for assignment ${assignmentId}`, { userId: userSession.userId, @@ -569,13 +571,13 @@ export class VersionManagementService { async restoreVersion( assignmentId: number, restoreVersionDto: RestoreVersionDto, - userSession: UserSession, + userSession: UserSession ): Promise { this.logger.info( `Restoring version ${restoreVersionDto.versionId} for assignment ${assignmentId}`, { userId: userSession.userId, - }, + } ); const versionToRestore = await this.prisma.assignmentVersion.findUnique({ @@ -592,7 +594,7 @@ export class VersionManagementService { // Create new version from restored data const nextVersionNumber = await this.getNextVersionNumber( assignmentId, - tx, + tx ); const restoredVersion = await tx.assignmentVersion.create({ @@ -621,6 +623,7 @@ export class VersionManagementService { showQuestionScore: versionToRestore.showQuestionScore, showSubmissionFeedback: versionToRestore.showSubmissionFeedback, showQuestions: versionToRestore.showQuestions, + correctAnswerVisibility: versionToRestore.correctAnswerVisibility, languageCode: versionToRestore.languageCode, createdBy: userSession.userId, isDraft: true, // Restored versions start as drafts @@ -689,13 +692,13 @@ export class VersionManagementService { assignmentId, restoreVersionDto.versionId, userSession, - tx, + tx ); } else { // For regular versions, check if published before activation if (!versionToRestore.published) { throw new BadRequestException( - `Version ${versionToRestore.versionNumber} cannot be activated because it has not been published yet. Please publish the version first before activating it.`, + `Version ${versionToRestore.versionNumber} cannot be activated because it has not been published yet. Please publish the version first before activating it.` ); } @@ -744,7 +747,7 @@ export class VersionManagementService { async publishVersion( assignmentId: number, - versionId: number, + versionId: number ): Promise { const version = await this.prisma.assignmentVersion.findUnique({ where: { id: versionId, assignmentId }, @@ -753,13 +756,13 @@ export class VersionManagementService { if (!version) { throw new NotFoundException( - `Version with ID ${versionId} not found for assignment ${assignmentId}`, + `Version with ID ${versionId} not found for assignment ${assignmentId}` ); } if (version.published) { throw new BadRequestException( - `Version ${version.versionNumber} is already published`, + `Version ${version.versionNumber} is already published` ); } @@ -781,7 +784,7 @@ export class VersionManagementService { if (existingPublishedVersion) { // Auto-increment patch version to resolve conflict const versionMatch = publishedVersionNumber.match( - /^(\d+)\.(\d+)\.(\d+)$/, + /^(\d+)\.(\d+)\.(\d+)$/ ); if (versionMatch) { const [, major, minor, patch] = versionMatch; @@ -804,11 +807,11 @@ export class VersionManagementService { publishedVersionNumber = newVersionNumber; this.logger.info( - `Resolved version conflict by incrementing patch: ${version.versionNumber} → ${publishedVersionNumber}`, + `Resolved version conflict by incrementing patch: ${version.versionNumber} → ${publishedVersionNumber}` ); } else { throw new ConflictException( - `Published version ${publishedVersionNumber} already exists and version format is not recognizable.`, + `Published version ${publishedVersionNumber} already exists and version format is not recognizable.` ); } } @@ -838,7 +841,7 @@ export class VersionManagementService { this.logger.info( `Successfully published version: ${originalVersionNumber} → ${publishedVersionNumber}${ wasAutoIncremented ? " (auto-incremented)" : "" - }`, + }` ); return { @@ -861,7 +864,7 @@ export class VersionManagementService { async compareVersions( assignmentId: number, - compareDto: CompareVersionsDto, + compareDto: CompareVersionsDto ): Promise { const [fromVersion, toVersion] = await Promise.all([ this.prisma.assignmentVersion.findUnique({ @@ -880,11 +883,11 @@ export class VersionManagementService { const assignmentChanges = this.compareAssignmentData( fromVersion, - toVersion, + toVersion ); const questionChanges = this.compareQuestionData( fromVersion.questionVersions, - toVersion.questionVersions, + toVersion.questionVersions ); return { @@ -932,7 +935,7 @@ export class VersionManagementService { // Private helper methods private async updateExistingDraft( draftId: number, - saveDraftDto: SaveDraftDto, + saveDraftDto: SaveDraftDto ): Promise { return await this.prisma.$transaction(async (tx) => { const updatedDraft = await tx.assignmentVersion.update({ @@ -1015,7 +1018,7 @@ export class VersionManagementService { assignmentId: number, versionId: number, updateData: CreateVersionDto, - userSession: UserSession, + userSession: UserSession ): Promise { this.logger.info( `🔄 UPDATE EXISTING VERSION: Starting update for version ${versionId} on assignment ${assignmentId}`, @@ -1024,7 +1027,7 @@ export class VersionManagementService { assignmentId, updateData, userId: userSession.userId, - }, + } ); // Get current assignment data @@ -1068,6 +1071,7 @@ export class VersionManagementService { showQuestionScore: assignment.showQuestionScore, showSubmissionFeedback: assignment.showSubmissionFeedback, showQuestions: assignment.showQuestions, + correctAnswerVisibility: assignment.correctAnswerVisibility, languageCode: assignment.languageCode, }, include: { _count: { select: { questionVersions: true } } }, @@ -1129,7 +1133,7 @@ export class VersionManagementService { }); this.logger.info( - `✅ UPDATE EXISTING VERSION: Successfully updated version ${updatedVersion.versionNumber} (ID: ${updatedVersion.id}) for assignment ${assignmentId}`, + `✅ UPDATE EXISTING VERSION: Successfully updated version ${updatedVersion.versionNumber} (ID: ${updatedVersion.id}) for assignment ${assignmentId}` ); return { @@ -1148,7 +1152,7 @@ export class VersionManagementService { private async getNextVersionNumber( assignmentId: number, - tx: any, + tx: any ): Promise { const lastVersion = await tx.assignmentVersion.findFirst({ where: { assignmentId }, @@ -1194,8 +1198,8 @@ export class VersionManagementService { from[field] === null ? "added" : to[field] === null - ? "removed" - : "modified", + ? "removed" + : "modified", }); } } @@ -1207,7 +1211,7 @@ export class VersionManagementService { // Create maps for easier comparison const fromMap = new Map( - fromQuestions.map((q) => [q.questionId || q.id, q]), + fromQuestions.map((q) => [q.questionId || q.id, q]) ); const toMap = new Map(toQuestions.map((q) => [q.questionId || q.id, q])); @@ -1277,7 +1281,7 @@ export class VersionManagementService { private async verifyAssignmentAccess( assignmentId: number, - userSession: UserSession, + userSession: UserSession ) { const assignment = await this.prisma.assignment.findUnique({ where: { id: assignmentId }, @@ -1290,7 +1294,7 @@ export class VersionManagementService { if (userSession.role === UserRole.AUTHOR) { const hasAccess = assignment.AssignmentAuthor.some( - (author) => author.userId === userSession.userId, + (author) => author.userId === userSession.userId ); if (!hasAccess) { throw new NotFoundException("Assignment not found"); @@ -1301,7 +1305,7 @@ export class VersionManagementService { private async createDraftVersion( assignmentId: number, saveDraftDto: SaveDraftDto, - userSession: UserSession, + userSession: UserSession ): Promise { // Get the base assignment for reference const assignment = await this.prisma.assignment.findUnique({ @@ -1328,7 +1332,7 @@ export class VersionManagementService { if (rcMatch) { const baseVersion = lastVersion.versionNumber.replace( /-rc\d+$/, - "", + "" ); const rcNumber = Number.parseInt(rcMatch[1], 10) + 1; nextVersionNumber = `${baseVersion}-rc${rcNumber}`; @@ -1376,6 +1380,7 @@ export class VersionManagementService { showQuestionScore: assignment.showQuestionScore, showSubmissionFeedback: assignment.showSubmissionFeedback, showQuestions: assignment.showQuestions, + correctAnswerVisibility: assignment.correctAnswerVisibility, languageCode: assignment.languageCode, createdBy: userSession.userId, isDraft: true, @@ -1443,7 +1448,7 @@ export class VersionManagementService { async getUserLatestDraft( assignmentId: number, - userSession: UserSession, + userSession: UserSession ): Promise<{ questions: any[]; _isDraftVersion: boolean; @@ -1468,6 +1473,7 @@ export class VersionManagementService { showQuestionScore: boolean; showSubmissionFeedback: boolean; showQuestions: boolean; + correctAnswerVisibility: string; languageCode: string | null; }> { // await this.verifyAssignmentAccess(assignmentId, userSession); @@ -1510,6 +1516,7 @@ export class VersionManagementService { showQuestionScore: latestDraft.showQuestionScore, showSubmissionFeedback: latestDraft.showSubmissionFeedback, showQuestions: latestDraft.showQuestions, + correctAnswerVisibility: latestDraft.correctAnswerVisibility, languageCode: latestDraft.languageCode, questions: latestDraft.questionVersions.map((qv) => ({ id: qv.questionId, @@ -1538,13 +1545,13 @@ export class VersionManagementService { assignmentId: number, versionId: number, questionIds: number[], - userSession: UserSession, + userSession: UserSession ): Promise { this.logger.info( `Restoring deleted questions ${questionIds.join( - ", ", + ", " )} from version ${versionId} for assignment ${assignmentId}`, - { userId: userSession.userId }, + { userId: userSession.userId } ); // await this.verifyAssignmentAccess(assignmentId, userSession); @@ -1581,7 +1588,7 @@ export class VersionManagementService { // Create a new draft version const nextVersionNumber = await this.getNextVersionNumber( assignmentId, - tx, + tx ); targetDraft = await tx.assignmentVersion.create({ data: { @@ -1609,6 +1616,7 @@ export class VersionManagementService { showQuestionScore: sourceVersion.showQuestionScore, showSubmissionFeedback: sourceVersion.showSubmissionFeedback, showQuestions: sourceVersion.showQuestions, + correctAnswerVisibility: sourceVersion.correctAnswerVisibility, languageCode: sourceVersion.languageCode, createdBy: userSession.userId, isDraft: true, @@ -1662,7 +1670,7 @@ export class VersionManagementService { }); this.logger.info( - `Successfully restored ${questionIds.length} questions from version ${versionId} to draft ${targetDraft.id}`, + `Successfully restored ${questionIds.length} questions from version ${versionId} to draft ${targetDraft.id}` ); return { @@ -1683,7 +1691,7 @@ export class VersionManagementService { assignmentId: number, versionId: number, versionDescription: string, - userSession: UserSession, + userSession: UserSession ): Promise { this.logger.info(`Updating version description for version ${versionId}`, { assignmentId, @@ -1713,7 +1721,7 @@ export class VersionManagementService { }); this.logger.info( - `Successfully updated version description for version ${versionId}`, + `Successfully updated version description for version ${versionId}` ); return { @@ -1733,7 +1741,7 @@ export class VersionManagementService { assignmentId: number, versionId: number, versionNumber: string, - userSession: UserSession, + userSession: UserSession ): Promise { this.logger.info(`Updating version number for version ${versionId}`, { assignmentId, @@ -1765,7 +1773,7 @@ export class VersionManagementService { if (existingVersion) { throw new BadRequestException( - `Version number "${versionNumber}" already exists for this assignment`, + `Version number "${versionNumber}" already exists for this assignment` ); } @@ -1779,7 +1787,7 @@ export class VersionManagementService { }); this.logger.info( - `Successfully updated version number for version ${versionId} to ${versionNumber}`, + `Successfully updated version number for version ${versionId} to ${versionNumber}` ); return { @@ -1798,7 +1806,7 @@ export class VersionManagementService { async deleteVersion( assignmentId: number, versionId: number, - userSession: UserSession, + userSession: UserSession ): Promise { this.logger.info( `Deleting version ${versionId} for assignment ${assignmentId}`, @@ -1806,7 +1814,7 @@ export class VersionManagementService { assignmentId, versionId, userId: userSession.userId, - }, + } ); // First, verify the version exists and belongs to the assignment @@ -1856,7 +1864,7 @@ export class VersionManagementService { assignmentId: number, rcVersionId: number, userSession: UserSession, - tx?: Prisma.TransactionClient, + tx?: Prisma.TransactionClient ): Promise { const prisma = tx || this.prisma; @@ -1866,7 +1874,7 @@ export class VersionManagementService { assignmentId, rcVersionId, userId: userSession.userId, - }, + } ); // Get the RC version @@ -1919,7 +1927,7 @@ export class VersionManagementService { finalVersionNumber = newVersionNumber; this.logger.info( - `Resolved version conflict by incrementing patch: ${rcVersion.versionNumber} → ${finalVersionNumber}`, + `Resolved version conflict by incrementing patch: ${rcVersion.versionNumber} → ${finalVersionNumber}` ); } } @@ -2001,7 +2009,7 @@ export class VersionManagementService { assignmentData: Assignment; questionsData?: Question[]; }, - userSession: UserSession, + userSession: UserSession ): Promise { // Verify assignment access const assignment = await this.prisma.assignment.findUnique({ @@ -2023,7 +2031,7 @@ export class VersionManagementService { if (existingVersion) { throw new ConflictException( - `Version ${draftData.versionNumber} already exists for this assignment`, + `Version ${draftData.versionNumber} already exists for this assignment` ); } @@ -2094,6 +2102,9 @@ export class VersionManagementService { draftData.assignmentData.showQuestions ?? assignment.showQuestions ?? true, + correctAnswerVisibility: + draftData.assignmentData.correctAnswerVisibility ?? + assignment.correctAnswerVisibility, languageCode: draftData.assignmentData.languageCode ?? assignment.languageCode, }, diff --git a/apps/api/src/api/assignment/v2/tests/unit/__mocks__/ common-mocks.ts b/apps/api/src/api/assignment/v2/tests/unit/__mocks__/ common-mocks.ts index 7698adb1..f7b515fc 100644 --- a/apps/api/src/api/assignment/v2/tests/unit/__mocks__/ common-mocks.ts +++ b/apps/api/src/api/assignment/v2/tests/unit/__mocks__/ common-mocks.ts @@ -612,7 +612,7 @@ const toJsonValue = (value: unknown): Prisma.JsonValue => { */ export const createMockQuestion = ( overrides: Partial = {}, - questionType: QuestionType = QuestionType.SINGLE_CORRECT, + questionType: QuestionType = QuestionType.SINGLE_CORRECT ): Question => { const baseQuestion: Question = { id: 1, @@ -673,7 +673,7 @@ export const createMockQuestion = ( */ export const createMockQuestionDto = ( overrides: Partial = {}, - questionType: QuestionType = QuestionType.SINGLE_CORRECT, + questionType: QuestionType = QuestionType.SINGLE_CORRECT ): QuestionDto => { const baseQuestionDto: QuestionDto = { id: 1, @@ -735,7 +735,7 @@ export const createMockQuestionDto = ( export const createMockQuestionVariant = ( overrides: Partial = {}, questionId = 1, - variantType: VariantType = VariantType.REWORDED, + variantType: VariantType = VariantType.REWORDED ): QuestionVariant => { const baseVariant: QuestionVariant = { id: 101, @@ -765,7 +765,7 @@ export const createMockQuestionVariant = ( export const createMockVariantDto = ( overrides: Partial = {}, _questionId = 1, - variantType: VariantType = VariantType.REWORDED, + variantType: VariantType = VariantType.REWORDED ): VariantDto => { const baseVariantDto: VariantDto = { id: 101, @@ -792,7 +792,7 @@ export const createMockVariantDto = ( export const createReactQuestionDto = ( type: QuestionType, id: number = Math.floor(Math.random() * 1_000_000_000), - overrides: Partial = {}, + overrides: Partial = {} ): QuestionDto => { const baseQuestion: Partial = { id, @@ -940,7 +940,7 @@ export const createReactQuestionDto = ( * Pass a partial object to override only the fields you care about. */ export const createMockAssignment = ( - overrides: Partial = {}, + overrides: Partial = {} ): Assignment => ({ id: 1, name: "Sample Assignment", @@ -961,7 +961,7 @@ export const createMockAssignment = ( questionOrder: [1, 2], published: false, showAssignmentScore: true, - showCorrectAnswer: true, + correctAnswerVisibility: "ON_PASS", showQuestionScore: true, showSubmissionFeedback: true, showQuestions: true, @@ -979,7 +979,7 @@ export const createMockGetAssignmentResponseDto = ( questions: Question[] = [ createMockQuestion(), createMockQuestion({ id: 2 }, QuestionType.MULTIPLE_CORRECT), - ], + ] ): GetAssignmentResponseDto => { const assignment = createMockAssignment(); @@ -995,7 +995,7 @@ export const createMockGetAssignmentResponseDto = ( * Create a sample LearnerGetAssignmentResponseDto with sane defaults */ export const createMockLearnerGetAssignmentResponseDto = ( - overrides: Partial = {}, + overrides: Partial = {} ): LearnerGetAssignmentResponseDto => { const assignment = createMockAssignment(); @@ -1011,7 +1011,7 @@ export const createMockLearnerGetAssignmentResponseDto = ( * Create a sample AssignmentResponseDto with sane defaults */ export const createMockAssignmentResponseDto = ( - overrides: Partial = {}, + overrides: Partial = {} ): AssignmentResponseDto => { const assignment = createMockAssignment(); @@ -1025,7 +1025,7 @@ export const createMockAssignmentResponseDto = ( * Create a sample BaseAssignmentResponseDto with sane defaults */ export const createMockBaseAssignmentResponseDto = ( - overrides: Partial = {}, + overrides: Partial = {} ): BaseAssignmentResponseDto => { return { id: 1, @@ -1040,7 +1040,7 @@ export const createMockBaseAssignmentResponseDto = ( * but you can override any subset of properties. */ export const createMockUpdateAssignmentDto = ( - overrides: Partial = {}, + overrides: Partial = {} ): UpdateAssignmentRequestDto => ({ name: "Updated Assignment", introduction: "New intro", @@ -1062,7 +1062,7 @@ export const createMockUpdateAssignmentDto = ( showQuestionScore: true, showSubmissionFeedback: true, showQuestions: true, - showCorrectAnswer: true, + correctAnswerVisibility: "ON_PASS", ...overrides, }); @@ -1070,7 +1070,7 @@ export const createMockUpdateAssignmentDto = ( * Create a sample ReplaceAssignmentRequestDto with sane defaults */ export const createMockReplaceAssignmentDto = ( - overrides: Partial = {}, + overrides: Partial = {} ): ReplaceAssignmentRequestDto => ({ introduction: "Completely new introduction", instructions: "Completely new instructions", @@ -1093,7 +1093,7 @@ export const createMockReplaceAssignmentDto = ( */ export const createMockUpdateAssignmentQuestionsDto = ( overrides: Partial = {}, - includeQuestions = true, + includeQuestions = true ): UpdateAssignmentQuestionsDto => { const baseDto: UpdateAssignmentQuestionsDto = { name: "Updated Assignment with Questions", @@ -1116,7 +1116,7 @@ export const createMockUpdateAssignmentQuestionsDto = ( showQuestionScore: true, showSubmissionFeedback: true, showQuestions: true, - showCorrectAnswer: true, + correctAnswerVisibility: "ON_PASS", versionNumber: "0.0.1", versionDescription: "Updated questions version", updatedAt: new Date(), @@ -1139,7 +1139,7 @@ export const createMockUpdateAssignmentQuestionsDto = ( */ export const createMockAssignmentAttempt = ( overrides: Partial = {}, - isCompleted = false, + isCompleted = false ): AssignmentAttempt => { const baseAttempt: AssignmentAttempt = { id: 1, @@ -1167,7 +1167,7 @@ export const createMockAssignmentAttempt = ( export const createMockQuestionResponse = ( overrides: Partial = {}, questionId = 1, - isGraded = true, + isGraded = true ): QuestionResponse => { const baseResponse: QuestionResponse = { id: 1, @@ -1197,7 +1197,7 @@ export const createMockQuestionResponse = ( */ export const createMockJob = ( overrides: Partial = {}, - status = "Pending", + status = "Pending" ): Job => { const baseJob: Job = { id: 1, @@ -1227,7 +1227,7 @@ export const createMockJob = ( */ export const createMockTranslation = ( overrides: Partial = {}, - languageCode = "fr", + languageCode = "fr" ): Translation => { const baseTranslation: Translation = { id: 1, @@ -1238,8 +1238,8 @@ export const createMockTranslation = ( languageCode === "fr" ? "Quelle est la capitale de la France?" : languageCode === "es" - ? "¿Cuál es la capital de Francia?" - : "What is the capital of France?", + ? "¿Cuál es la capital de Francia?" + : "What is the capital of France?", untranslatedText: "What is the capital of France?", translatedChoices: JSON.stringify([ { @@ -1248,8 +1248,8 @@ export const createMockTranslation = ( languageCode === "fr" ? "Paris" : languageCode === "es" - ? "París" - : "Paris", + ? "París" + : "Paris", }, { ...sampleChoiceB, @@ -1257,8 +1257,8 @@ export const createMockTranslation = ( languageCode === "fr" ? "Londres" : languageCode === "es" - ? "Londres" - : "London", + ? "Londres" + : "London", }, { ...sampleChoiceC, @@ -1266,8 +1266,8 @@ export const createMockTranslation = ( languageCode === "fr" ? "Berlin" : languageCode === "es" - ? "Berlín" - : "Berlin", + ? "Berlín" + : "Berlin", }, ]), untranslatedChoices: JSON.stringify([ @@ -1289,7 +1289,7 @@ export const createMockTranslation = ( */ export const createMockAssignmentTranslation = ( overrides: Partial = {}, - languageCode = "fr", + languageCode = "fr" ): AssignmentTranslation => { const baseAssignmentTranslation: AssignmentTranslation = { id: 1, @@ -1303,26 +1303,26 @@ export const createMockAssignmentTranslation = ( languageCode === "fr" ? "Exemple de devoir" : languageCode === "es" - ? "Ejemplo de tarea" - : "Sample Assignment", + ? "Ejemplo de tarea" + : "Sample Assignment", translatedIntroduction: languageCode === "fr" ? "Texte d'introduction" : languageCode === "es" - ? "Texto introductorio" - : "Intro text", + ? "Texto introductorio" + : "Intro text", translatedInstructions: languageCode === "fr" ? "Faites le travail" : languageCode === "es" - ? "Haz el trabajo" - : "Do the work", + ? "Haz el trabajo" + : "Do the work", translatedGradingCriteriaOverview: languageCode === "fr" ? "Chaque question = 1pt" : languageCode === "es" - ? "Cada pregunta = 1pt" - : "Each question = 1pt", + ? "Cada pregunta = 1pt" + : "Each question = 1pt", createdAt: new Date(), updatedAt: new Date(), }; @@ -1338,7 +1338,7 @@ export const createMockAssignmentTranslation = ( */ export const createMockFeedbackTranslation = ( overrides: Partial = {}, - languageCode = "fr", + languageCode = "fr" ): FeedbackTranslation => { const baseFeedbackTranslation: FeedbackTranslation = { id: 1, @@ -1357,16 +1357,16 @@ export const createMockFeedbackTranslation = ( "3": "C'est la capitale de l'Allemagne, pas de la France.", }) : languageCode === "es" - ? JSON.stringify({ - "1": "¡Correcto!", - "2": "Incorrecto. París es la capital de Francia.", - "3": "Esa es la capital de Alemania, no de Francia.", - }) - : JSON.stringify({ - "1": "Correct!", - "2": "Incorrect. Paris is the capital of France.", - "3": "That's the capital of Germany, not France.", - }), + ? JSON.stringify({ + "1": "¡Correcto!", + "2": "Incorrecto. París es la capital de Francia.", + "3": "Esa es la capital de Alemania, no de Francia.", + }) + : JSON.stringify({ + "1": "Correct!", + "2": "Incorrect. Paris is the capital of France.", + "3": "That's the capital of Germany, not France.", + }), createdAt: new Date(), updatedAt: new Date(), }; @@ -1382,7 +1382,7 @@ export const createMockFeedbackTranslation = ( */ export const createMockReport = ( overrides: Partial = {}, - issueType: ReportType = ReportType.BUG, + issueType: ReportType = ReportType.BUG ): Report => { const baseReport: Report = { id: 1, @@ -1415,7 +1415,7 @@ export const createMockReport = ( * Create a sample AssignmentFeedback with sane defaults */ export const createMockAssignmentFeedback = ( - overrides: Partial = {}, + overrides: Partial = {} ): AssignmentFeedback => { const baseFeedback: AssignmentFeedback = { id: 1, @@ -1445,7 +1445,7 @@ export const createMockAssignmentFeedback = ( */ export const createMockRegradingRequest = ( overrides: Partial = {}, - status: RegradingStatus = RegradingStatus.PENDING, + status: RegradingStatus = RegradingStatus.PENDING ): RegradingRequest => { const baseRequest: RegradingRequest = { id: 1, @@ -1468,7 +1468,7 @@ export const createMockRegradingRequest = ( * Create a sample QuestionGenerationPayload with sane defaults */ export const createMockQuestionGenerationPayload = ( - overrides: Partial = {}, + overrides: Partial = {} ): QuestionGenerationPayload => { const basePayload: QuestionGenerationPayload = { assignmentId: 1, @@ -1659,7 +1659,7 @@ export const createMockQuestionRepository = () => ({ mapToQuestionDto: jest .fn() .mockImplementation((question: { id: number }) => - createMockQuestionDto({ id: question.id }), + createMockQuestionDto({ id: question.id }) ), parseJsonField: jest .fn() @@ -1716,7 +1716,7 @@ export const createMockVariantRepository = () => ({ mapToVariantDto: jest .fn() .mockImplementation((variant: { id: number }) => - createMockVariantDto({ id: variant.id }), + createMockVariantDto({ id: variant.id }) ), parseJsonField: jest .fn() @@ -1843,7 +1843,7 @@ export const createMockLlmFacadeService = () => ({ translateText: jest .fn() .mockImplementation((text: string) => - Promise.resolve(`Translated: ${text}`), + Promise.resolve(`Translated: ${text}`) ), generateQuestionRewordings: jest.fn().mockResolvedValue([ { @@ -1866,7 +1866,7 @@ export const createMockLlmFacadeService = () => ({ generateQuestionTranslation: jest .fn() .mockImplementation((_assignmentId: number, text: string) => - Promise.resolve(`Translated: ${text}`), + Promise.resolve(`Translated: ${text}`) ), generateChoicesTranslation: jest.fn().mockResolvedValue([ { ...sampleChoiceA, choice: "Translated choice A" }, diff --git a/apps/api/src/api/assignment/v2/tests/unit/services/assignment.service.spec.ts b/apps/api/src/api/assignment/v2/tests/unit/services/assignment.service.spec.ts index 5d0a367c..1f320ffa 100644 --- a/apps/api/src/api/assignment/v2/tests/unit/services/assignment.service.spec.ts +++ b/apps/api/src/api/assignment/v2/tests/unit/services/assignment.service.spec.ts @@ -363,7 +363,7 @@ describe("AssignmentServiceV2 – full unit-suite", () => { showQuestionScore: false, showSubmissionFeedback: false, showQuestions: false, - showCorrectAnswer: false, + correctAnswerVisibility: "NEVER", }; expect( diff --git a/apps/api/src/api/attempt/dto/success-page-data.dto.ts b/apps/api/src/api/attempt/dto/success-page-data.dto.ts new file mode 100644 index 00000000..a0284370 --- /dev/null +++ b/apps/api/src/api/attempt/dto/success-page-data.dto.ts @@ -0,0 +1,80 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class SuccessPageQuestionDto { + @ApiProperty() + id: number; + + @ApiProperty() + number: number; + + @ApiProperty() + question: string; + + @ApiProperty() + type: string; + + @ApiProperty() + totalPoints: number; + + @ApiProperty() + earnedPoints: number; + + @ApiProperty() + learnerResponse?: any; // The actual response given by the user + + @ApiProperty() + choices?: any[]; // For multiple choice questions + + @ApiProperty() + correctAnswer?: any; // Only included if visibility allows + + @ApiProperty() + feedback?: string; // AI/instructor feedback + + @ApiProperty() + scoring?: any; // Rubric info if needed +} + +export class SuccessPageDataDto { + @ApiProperty() + assignmentId: number; + + @ApiProperty() + assignmentName: string; + + @ApiProperty() + isAuthor: boolean; + + @ApiProperty() + grade: number; // Percentage (0-100) + + @ApiProperty() + totalPointsEarned: number; + + @ApiProperty() + totalPointsPossible: number; + + @ApiProperty() + passingGrade: number; + + @ApiProperty() + passed: boolean; + + @ApiProperty() + showQuestions: boolean; + + @ApiProperty() + showSubmissionFeedback: boolean; + + @ApiProperty() + correctAnswerVisibility: 'NEVER' | 'ALWAYS' | 'ON_PASS'; + + @ApiProperty() + comments?: string; // Instructor comments + + @ApiProperty() + preferredLanguage: string; + + @ApiProperty({ type: [SuccessPageQuestionDto] }) + questions: SuccessPageQuestionDto[]; +} \ No newline at end of file diff --git a/apps/api/src/api/attempt/services/attempt-grading.service.ts b/apps/api/src/api/attempt/services/attempt-grading.service.ts index 9dedfe88..a654c228 100644 --- a/apps/api/src/api/attempt/services/attempt-grading.service.ts +++ b/apps/api/src/api/attempt/services/attempt-grading.service.ts @@ -12,14 +12,14 @@ export class AttemptGradingService { */ calculateGradeForAuthor( successfulQuestionResponses: CreateQuestionResponseAttemptResponseDto[], - totalPossiblePoints: number, + totalPossiblePoints: number ): { grade: number; totalPointsEarned: number } { if (successfulQuestionResponses.length === 0) { return { grade: 0, totalPointsEarned: 0 }; } const totalPointsEarned = this.calculateTotalPointsEarned( - successfulQuestionResponses, + successfulQuestionResponses ); const grade = totalPossiblePoints > 0 ? totalPointsEarned / totalPossiblePoints : 0; @@ -35,13 +35,13 @@ export class AttemptGradingService { */ calculateGradeForLearner( successfulQuestionResponses: CreateQuestionResponseAttemptResponseDto[], - totalPossiblePoints: number, + totalPossiblePoints: number ): { grade: number; totalPointsEarned: number; totalPossiblePoints: number } { if (successfulQuestionResponses.length === 0) { return { grade: 0, totalPointsEarned: 0, totalPossiblePoints: 0 }; } const totalPointsEarned = this.calculateTotalPointsEarned( - successfulQuestionResponses, + successfulQuestionResponses ); const grade = totalPossiblePoints > 0 ? totalPointsEarned / totalPossiblePoints : 0; @@ -57,7 +57,7 @@ export class AttemptGradingService { */ constructFeedbacksForQuestions( successfulQuestionResponses: CreateQuestionResponseAttemptResponseDto[], - assignment: Assignment, + assignment: Assignment ) { return successfulQuestionResponses.map((feedbackForQuestion) => { const { totalPoints, feedback, ...otherData } = feedbackForQuestion; @@ -75,11 +75,11 @@ export class AttemptGradingService { * @returns Total points earned */ private calculateTotalPointsEarned( - responses: CreateQuestionResponseAttemptResponseDto[], + responses: CreateQuestionResponseAttemptResponseDto[] ): number { return responses.reduce( (accumulator, response) => accumulator + response.totalPoints, - 0, + 0 ); } } diff --git a/apps/api/src/api/attempt/services/attempt-submission.service.ts b/apps/api/src/api/attempt/services/attempt-submission.service.ts index f1f53d54..88e66b3d 100644 --- a/apps/api/src/api/attempt/services/attempt-submission.service.ts +++ b/apps/api/src/api/attempt/services/attempt-submission.service.ts @@ -9,6 +9,7 @@ import { import { AssignmentAttempt, AssignmentQuestionDisplayOrder, + CorrectAnswerVisibility, Question, QuestionType, QuestionVariant, @@ -64,18 +65,18 @@ export class AttemptSubmissionService { private readonly questionResponseService: QuestionResponseService, private readonly translationService: TranslationService, private readonly questionVariantService: QuestionVariantService, - private readonly httpService: HttpService, + private readonly httpService: HttpService ) {} /** * Creates a new assignment attempt */ async createAssignmentAttempt( assignmentId: number, - userSession: UserSession, + userSession: UserSession ): Promise { const assignment = await this.assignmentRepository.findById( assignmentId, - userSession, + userSession ); await this.validationService.validateNewAttempt(assignment, userSession); @@ -102,12 +103,12 @@ export class AttemptSubmissionService { }, }, }, - }, + } ); if (!assignmentWithActiveVersion) { throw new NotFoundException( - `Assignment with Id ${assignmentId} not found.`, + `Assignment with Id ${assignmentId} not found.` ); } @@ -148,7 +149,7 @@ export class AttemptSubmissionService { videoPresentationConfig: qv.videoPresentationConfig as unknown as VideoPresentationConfig, liveRecordingConfig: qv.liveRecordingConfig as object, - }), + }) ) : ((assignmentWithActiveVersion?.questions || []).map((q) => ({ ...q, @@ -173,11 +174,11 @@ export class AttemptSubmissionService { const shuffledQuestions = questions.sort(() => Math.random() - 0.5); const selectedQuestions = shuffledQuestions.slice( 0, - assignment.numberOfQuestionsPerAttempt, + assignment.numberOfQuestionsPerAttempt ); if (selectedQuestions.length < assignment.numberOfQuestionsPerAttempt) { throw new NotFoundException( - `Not enough questions available for the assignment with Id ${assignmentId}.`, + `Not enough questions available for the assignment with Id ${assignmentId}.` ); } questions.length = 0; // clear the original questions array @@ -217,16 +218,16 @@ export class AttemptSubmissionService { typeof q.randomizedChoices === "boolean" ? q.randomizedChoices : typeof q.randomizedChoices === "string" - ? q.randomizedChoices === "true" - : false, + ? q.randomizedChoices === "true" + : false, videoPresentationConfig: this.parseJsonValue( q.videoPresentationConfig, - null, + null ), liveRecordingConfig: this.parseJsonValue | null>( q.liveRecordingConfig, - null, + null ), })); @@ -241,7 +242,7 @@ export class AttemptSubmissionService { await this.questionVariantService.createAttemptQuestionVariants( assignmentAttempt.id, - orderedQuestions, + orderedQuestions ); return { @@ -260,7 +261,7 @@ export class AttemptSubmissionService { authCookie: string, gradingCallbackRequired: boolean, request: UserSessionRequest, - progressCallback?: (progress: string, percentage?: number) => Promise, + progressCallback?: (progress: string, percentage?: number) => Promise ): Promise { const { role } = request.userSession; if (role === UserRole.LEARNER) { @@ -271,17 +272,17 @@ export class AttemptSubmissionService { authCookie, gradingCallbackRequired, request, - progressCallback, + progressCallback ); } else if (role === UserRole.AUTHOR) { return this.updateAuthorAttempt( assignmentId, updateDto, - progressCallback, + progressCallback ); } else { throw new NotFoundException( - `User with role ${role} cannot update assignment attempts.`, + `User with role ${role} cannot update assignment attempts.` ); } } @@ -289,7 +290,7 @@ export class AttemptSubmissionService { * Gets a learner assignment attempt with all details needed for display */ async getLearnerAssignmentAttempt( - attemptId: number, + attemptId: number ): Promise { const assignmentAttempt = await this.prisma.assignmentAttempt.findUnique({ where: { id: attemptId }, @@ -308,7 +309,7 @@ export class AttemptSubmissionService { if (!assignmentAttempt) { throw new NotFoundException( - `AssignmentAttempt with Id ${attemptId} not found.`, + `AssignmentAttempt with Id ${attemptId} not found.` ); } @@ -349,13 +350,17 @@ export class AttemptSubmissionService { showSubmissionFeedback: true, showQuestionScore: true, showQuestions: true, - showCorrectAnswer: true, + currentVersion: { + select: { + correctAnswerVisibility: true, + }, + }, }, }); if (!assignment) { throw new NotFoundException( - `Assignment with Id ${assignmentAttempt.assignmentId} not found.`, + `Assignment with Id ${assignmentAttempt.assignmentId} not found.` ); } @@ -366,8 +371,8 @@ export class AttemptSubmissionService { typeof question.answer === "boolean" ? String(question.answer) : question.answer !== null && question.answer !== undefined - ? String(question.answer) - : undefined; + ? String(question.answer) + : undefined; const randomizedChoicesValue: string = typeof question.randomizedChoices === "string" @@ -397,7 +402,7 @@ export class AttemptSubmissionService { videoPresentationConfig: this.parseJsonValue( question.videoPresentationConfig, - null, + null ), liveRecordingConfig: this.parseJsonValue 0 + ? assignmentAttempt.assignmentVersion.questionVersions.map((qv) => ({ + id: qv.questionId || qv.id, + question: qv.question, + type: qv.type, + assignmentId: assignmentAttempt.assignmentId, + totalPoints: qv.totalPoints, + maxWords: qv.maxWords, + maxCharacters: qv.maxCharacters, + choices: qv.choices, + scoring: qv.scoring, + answer: qv.answer, + gradingContextQuestionIds: qv.gradingContextQuestionIds, + responseType: qv.responseType, + isDeleted: false, + randomizedChoices: qv.randomizedChoices, + videoPresentationConfig: qv.videoPresentationConfig, + liveRecordingConfig: qv.liveRecordingConfig, + })) + : await this.prisma.question.findMany({ + where: { assignmentId: assignmentAttempt.assignmentId, isDeleted: false }, + }); + + // Convert to questionDtos format + const allQuestionDtos: EnhancedAttemptQuestionDto[] = allQuestions.map((q) => { + const question = q as Record; + + const answerValue = + typeof question.answer === "boolean" + ? String(question.answer) + : question.answer !== null && question.answer !== undefined + ? String(question.answer) + : undefined; + + const randomizedChoicesValue: string = + typeof question.randomizedChoices === "string" + ? question.randomizedChoices + : JSON.stringify(question.randomizedChoices ?? false); + + return { + id: question.id as number, + question: question.question as string, + type: question.type as QuestionType, + assignmentId: question.assignmentId as number, + totalPoints: question.totalPoints as number, + maxWords: (question.maxWords as number) || undefined, + maxCharacters: (question.maxCharacters as number) || undefined, + choices: this.parseJsonValue(question.choices, []), + scoring: this.parseJsonValue(question.scoring, { + type: ScoringType.CRITERIA_BASED, + showRubricsToLearner: false, + rubrics: [], + }), + answer: answerValue, + gradingContextQuestionIds: + (question.gradingContextQuestionIds as number[]) || [], + responseType: (question.responseType as ResponseType) || undefined, + isDeleted: question.isDeleted as boolean, + randomizedChoices: randomizedChoicesValue, + videoPresentationConfig: + this.parseJsonValue( + question.videoPresentationConfig, + null + ), + liveRecordingConfig: this.parseJsonValue | null>(question.liveRecordingConfig, null), + }; + }); + + questionsToShow = allQuestionDtos; + } + const finalQuestions = await AttemptQuestionsMapper.buildQuestionsWithResponses( formattedAttempt, - questionDtos, + questionsToShow, { id: assignmentAttempt.assignmentId, ...assignment, + // Override questionOrder to include all questions if we should show all + questionOrder: shouldShowAllQuestions ? questionsToShow.map(q => q.id) : assignment.questionOrder, }, this.prisma, - assignmentAttempt.preferredLanguage || undefined, + assignmentAttempt.preferredLanguage || undefined ); this.applyVisibilitySettings(finalQuestions, assignmentAttempt, assignment); @@ -457,7 +551,7 @@ export class AttemptSubmissionService { showSubmissionFeedback: assignment.showSubmissionFeedback, showQuestions: assignment.showQuestions, showQuestionScore: assignment.showQuestionScore, - showCorrectAnswer: assignment.showCorrectAnswer, + correctAnswerVisibility: assignment.currentVersion?.correctAnswerVisibility || "NEVER", comments: assignmentAttempt.comments, }; } @@ -467,7 +561,7 @@ export class AttemptSubmissionService { */ async getAssignmentAttempt( attemptId: number, - language?: string, + language?: string ): Promise { const normalizedLanguage = this.getNormalizedLanguage(language); @@ -494,7 +588,7 @@ export class AttemptSubmissionService { if (!assignmentAttempt) { throw new NotFoundException( - `AssignmentAttempt with Id ${attemptId} not found.`, + `AssignmentAttempt with Id ${attemptId} not found.` ); } @@ -509,7 +603,11 @@ export class AttemptSubmissionService { showSubmissionFeedback: true, showQuestions: true, showQuestionScore: true, - showCorrectAnswer: true, + currentVersion: { + select: { + correctAnswerVisibility: true, + }, + }, }, })) as { questions: Question[]; @@ -520,7 +618,9 @@ export class AttemptSubmissionService { showSubmissionFeedback: boolean; showQuestions: boolean; showQuestionScore: boolean; - showCorrectAnswer: boolean; + currentVersion: { + correctAnswerVisibility: CorrectAnswerVisibility; + } | null; }; // Get version-specific questions for translation if available @@ -552,7 +652,7 @@ export class AttemptSubmissionService { const translations = await this.translationService.getTranslationsForAttempt( assignmentAttempt, - questionsForTranslation, + questionsForTranslation ); const formattedAttempt: AssignmentAttemptWithRelations = { @@ -582,15 +682,34 @@ export class AttemptSubmissionService { })), }; + // Determine if we should show all questions based on correctAnswerVisibility + const shouldShowAllQuestions = this.shouldShowCorrectAnswers( + assignment.currentVersion?.correctAnswerVisibility || "NEVER", + assignmentAttempt.grade || 0, + assignment.passingGrade + ); + + // If we should show all questions, override the assignment questionOrder + const assignmentForTranslation = { + ...assignment, + questionOrder: shouldShowAllQuestions ? questionsForTranslation.map(q => q.id) : assignment.questionOrder, + questions: questionsForTranslation, + } as unknown as UpdateAssignmentQuestionsDto; + const finalQuestions: AttemptQuestionDto[] = await AttemptQuestionsMapper.buildQuestionsWithTranslations( formattedAttempt, - assignment as unknown as UpdateAssignmentQuestionsDto, + assignmentForTranslation, translations, - normalizedLanguage, + normalizedLanguage ); - this.removeSensitiveData(finalQuestions, assignment); + this.removeSensitiveData( + finalQuestions, + { correctAnswerVisibility: assignment.currentVersion?.correctAnswerVisibility || "NEVER" }, + assignmentAttempt.grade || 0, + assignment.passingGrade + ); return { ...assignmentAttempt, @@ -600,7 +719,7 @@ export class AttemptSubmissionService { showSubmissionFeedback: assignment.showSubmissionFeedback, showQuestionScore: assignment.showQuestionScore, showQuestions: assignment.showQuestions, - showCorrectAnswer: assignment.showCorrectAnswer, + correctAnswerVisibility: assignment.currentVersion?.correctAnswerVisibility || "NEVER", }; } @@ -614,7 +733,7 @@ export class AttemptSubmissionService { authCookie: string, gradingCallbackRequired: boolean, request: UserSessionRequest, - progressCallback?: (progress: string, percentage?: number) => Promise, + progressCallback?: (progress: string, percentage?: number) => Promise ): Promise { try { // Report initial progress @@ -636,7 +755,7 @@ export class AttemptSubmissionService { if (!assignmentAttempt) { throw new NotFoundException( - `AssignmentAttempt with Id ${attemptId} not found.`, + `AssignmentAttempt with Id ${attemptId} not found.` ); } @@ -655,7 +774,7 @@ export class AttemptSubmissionService { await this.translationService.preTranslateQuestions( updateDto.responsesForQuestions, assignmentAttempt, - updateDto.language, + updateDto.language ); updateDto.preTranslatedQuestions = preTranslatedQuestions; @@ -666,6 +785,11 @@ export class AttemptSubmissionService { questions: { where: { isDeleted: false }, }, + currentVersion: { + select: { + correctAnswerVisibility: true, + }, + }, }, }); @@ -682,7 +806,7 @@ export class AttemptSubmissionService { updateDto.language, updateDto.authorQuestions, updateDto.authorAssignmentDetails, - updateDto.preTranslatedQuestions, + updateDto.preTranslatedQuestions ); if (progressCallback) { @@ -692,7 +816,7 @@ export class AttemptSubmissionService { let totalPossiblePoints = 0; for (const response of successfulQuestionResponses) { const question = assignment.questions.find( - (q) => q.id === response.questionId, + (q) => q.id === response.questionId ); totalPossiblePoints += question?.totalPoints || 0; } @@ -700,7 +824,7 @@ export class AttemptSubmissionService { const { grade, totalPointsEarned } = this.gradingService.calculateGradeForLearner( successfulQuestionResponses, - totalPossiblePoints, + totalPossiblePoints ); if (gradingCallbackRequired) { @@ -711,7 +835,7 @@ export class AttemptSubmissionService { grade, authCookie, assignmentId, - request.userSession.userId, + request.userSession.userId ); } @@ -722,7 +846,7 @@ export class AttemptSubmissionService { const result = await this.updateAssignmentAttemptInDb( attemptId, updateDto, - grade, + grade ); if (progressCallback) { @@ -738,11 +862,11 @@ export class AttemptSubmissionService { grade: assignment.showAssignmentScore ? result.grade : undefined, showQuestions: assignment.showQuestions, showSubmissionFeedback: assignment.showSubmissionFeedback, - showCorrectAnswer: assignment.showCorrectAnswer, + correctAnswerVisibility: assignment.currentVersion?.correctAnswerVisibility || "NEVER", feedbacksForQuestions: this.gradingService.constructFeedbacksForQuestions( successfulQuestionResponses, - assignment, + assignment ), }; } catch (error) { @@ -761,7 +885,7 @@ export class AttemptSubmissionService { private async updateAuthorAttempt( assignmentId: number, updateDto: LearnerUpdateAssignmentAttemptRequestDto, - progressCallback?: (progress: string, percentage?: number) => Promise, + progressCallback?: (progress: string, percentage?: number) => Promise ): Promise { try { if (progressCallback) { @@ -774,6 +898,11 @@ export class AttemptSubmissionService { questions: { where: { isDeleted: false }, }, + currentVersion: { + select: { + correctAnswerVisibility: true, + }, + }, }, }); @@ -791,7 +920,7 @@ export class AttemptSubmissionService { assignmentId, updateDto.language, updateDto.authorQuestions, - updateDto.authorAssignmentDetails, + updateDto.authorAssignmentDetails ); if (progressCallback) { @@ -801,7 +930,7 @@ export class AttemptSubmissionService { let totalPossiblePoints = 0; for (const response of successfulQuestionResponses) { const question = assignment.questions.find( - (q) => q.id === response.questionId, + (q) => q.id === response.questionId ); totalPossiblePoints += question?.totalPoints || 0; } @@ -809,7 +938,7 @@ export class AttemptSubmissionService { const { grade, totalPointsEarned } = this.gradingService.calculateGradeForAuthor( successfulQuestionResponses, - totalPossiblePoints, + totalPossiblePoints ); if (progressCallback) { @@ -825,11 +954,11 @@ export class AttemptSubmissionService { grade: assignment.showAssignmentScore ? grade : undefined, showQuestions: assignment.showQuestions, showSubmissionFeedback: assignment.showSubmissionFeedback, - showCorrectAnswer: assignment.showCorrectAnswer, + correctAnswerVisibility: assignment.currentVersion?.correctAnswerVisibility || "NEVER", feedbacksForQuestions: this.gradingService.constructFeedbacksForQuestions( successfulQuestionResponses, - assignment, + assignment ), }; } catch (error) { @@ -846,7 +975,7 @@ export class AttemptSubmissionService { * Handle an expired attempt */ private async handleExpiredAttempt( - attemptId: number, + attemptId: number ): Promise { await this.prisma.assignmentAttempt.update({ where: { id: attemptId }, @@ -869,7 +998,7 @@ export class AttemptSubmissionService { feedbacksForQuestions: [], message: "The attempt deadline has passed.", showQuestions: false, - showCorrectAnswer: false, + correctAnswerVisibility: "NEVER", }; } @@ -880,7 +1009,7 @@ export class AttemptSubmissionService { grade: number, authCookie: string, assignmentId: number, - userId: string, + userId: string ): Promise { const userAttempts = await this.prisma.assignmentAttempt.findMany({ where: { @@ -912,7 +1041,7 @@ export class AttemptSubmissionService { private async updateAssignmentAttemptInDb( attemptId: number, updateDto: LearnerUpdateAssignmentAttemptRequestDto, - grade: number, + grade: number ) { const { responsesForQuestions, @@ -939,7 +1068,7 @@ export class AttemptSubmissionService { */ private async sendGradeToLtiGateway( grade: number, - authCookie: string, + authCookie: string ): Promise { try { const ltiGatewayResponse = await this.httpService @@ -950,7 +1079,7 @@ export class AttemptSubmissionService { headers: { Cookie: `authentication=${authCookie}`, }, - }, + } ) .toPromise(); @@ -963,7 +1092,7 @@ export class AttemptSubmissionService { ? error.message : "An unknown error occurred while sending the grade to the LTI gateway."; throw new InternalServerErrorException( - `${GRADE_SUBMISSION_EXCEPTION}: ${errorMessage}`, + `${GRADE_SUBMISSION_EXCEPTION}: ${errorMessage}` ); } } @@ -972,7 +1101,7 @@ export class AttemptSubmissionService { * Calculate the expiration date for an attempt */ private calculateAttemptExpiresAt( - assignment: GetAssignmentResponseDto | LearnerGetAssignmentResponseDto, + assignment: GetAssignmentResponseDto | LearnerGetAssignmentResponseDto ): Date | null { if ( assignment.allotedTimeMinutes !== undefined && @@ -988,7 +1117,7 @@ export class AttemptSubmissionService { */ private getOrderedQuestions( questions: QuestionDto[], - assignment: GetAssignmentResponseDto | LearnerGetAssignmentResponseDto, + assignment: GetAssignmentResponseDto | LearnerGetAssignmentResponseDto ): QuestionDto[] { const orderedQuestions = [...questions]; @@ -1001,7 +1130,7 @@ export class AttemptSubmissionService { orderedQuestions.sort( (a, b) => assignment.questionOrder.indexOf(a.id) - - assignment.questionOrder.indexOf(b.id), + assignment.questionOrder.indexOf(b.id) ); } @@ -1039,16 +1168,16 @@ export class AttemptSubmissionService { typeof q.randomizedChoices === "boolean" ? q.randomizedChoices : typeof q.randomizedChoices === "string" - ? q.randomizedChoices === "true" - : false, + ? q.randomizedChoices === "true" + : false, videoPresentationConfig: this.parseJsonValue( q.videoPresentationConfig, - null, + null ), liveRecordingConfig: this.parseJsonValue | null>( q.liveRecordingConfig, - null, + null ), })); } @@ -1079,7 +1208,7 @@ export class AttemptSubmissionService { showQuestionScore?: boolean; showQuestions?: boolean; showCorrectAnswer?: boolean; - }, + } ): void { if (assignment.showAssignmentScore === false) { assignmentAttempt.grade = null; @@ -1114,9 +1243,9 @@ export class AttemptSubmissionService { */ private removeSensitiveData( questions: AttemptQuestionDto[], - assignment: { - showCorrectAnswer?: boolean; - }, + assignment: { correctAnswerVisibility: CorrectAnswerVisibility }, + grade: number, + passingGrade: number ): void { for (const question of questions) { if (!question.scoring?.showRubricsToLearner) { @@ -1126,7 +1255,13 @@ export class AttemptSubmissionService { if (question.choices) { for (const choice of question.choices) { delete choice.points; - if (assignment.showCorrectAnswer === false) { + if ( + !this.shouldShowCorrectAnswers( + assignment.correctAnswerVisibility || "NEVER", + grade, + passingGrade + ) + ) { delete choice.isCorrect; delete choice.feedback; } @@ -1139,7 +1274,13 @@ export class AttemptSubmissionService { if (translationObject?.translatedChoices) { for (const choice of translationObject.translatedChoices) { delete choice.points; - if (assignment.showCorrectAnswer === false) { + if ( + !this.shouldShowCorrectAnswers( + assignment.correctAnswerVisibility, + grade, + passingGrade + ) + ) { delete choice.isCorrect; delete choice.feedback; } @@ -1153,7 +1294,7 @@ export class AttemptSubmissionService { typeof question.randomizedChoices === "string" ) { const randomizedArray = JSON.parse( - question.randomizedChoices, + question.randomizedChoices ) as Array<{ points?: number; isCorrect?: boolean; @@ -1163,7 +1304,13 @@ export class AttemptSubmissionService { if (Array.isArray(randomizedArray)) { for (const choice of randomizedArray) { delete choice.points; - if (assignment.showCorrectAnswer === false) { + if ( + !this.shouldShowCorrectAnswers( + assignment.correctAnswerVisibility || "NEVER", + grade, + passingGrade + ) + ) { delete choice.isCorrect; delete choice.feedback; } @@ -1208,4 +1355,28 @@ export class AttemptSubmissionService { } return language.toLowerCase().split("-")[0]; } + + /** + * Determine if correct answers should be shown based on visibility setting and grade + */ + private shouldShowCorrectAnswers( + correctAnswerVisibility: CorrectAnswerVisibility, + grade: number, + passingGrade: number + ): boolean { + switch (correctAnswerVisibility) { + case "NEVER": { + return false; + } + case "ALWAYS": { + return true; + } + case "ON_PASS": { + return grade >= passingGrade; + } + default: { + return false; + } + } + } } diff --git a/apps/api/src/scripts/create-initial-versions.ts b/apps/api/src/scripts/create-initial-versions.ts deleted file mode 100644 index 833eaa5e..00000000 --- a/apps/api/src/scripts/create-initial-versions.ts +++ /dev/null @@ -1,180 +0,0 @@ -#!/usr/bin/env ts-node - -/** - * Script to create initial versions (version 1) for all existing assignments - * This should be run once to migrate existing assignments to the new version system - */ -import { PrismaService } from "../prisma.service"; - -async function createInitialVersions() { - const prisma = new PrismaService(); - - try { - console.log( - "Starting to create initial versions for existing assignments...", - ); - - const alreadyMigrated = await prisma.assignmentVersion.count(); - if (alreadyMigrated > 0) { - console.log("🚫 Initial versions already exist. Skipping migration."); - process.exit(0); - } - - // Find all assignments that don't have any versions yet - const assignmentsWithoutVersions = await prisma.assignment.findMany({ - where: { - versions: { - none: {}, - }, - }, - include: { - questions: { - where: { isDeleted: false }, - }, - AssignmentAuthor: true, - }, - }); - - console.log( - `Found ${assignmentsWithoutVersions.length} assignments without versions`, - ); - - let createdCount = 0; - - for (const assignment of assignmentsWithoutVersions) { - try { - await prisma.$transaction(async (tx) => { - console.log( - `Creating version 1 for assignment "${assignment.name}" (ID: ${assignment.id})`, - ); - - // Create the initial version (version 1) - const assignmentVersion = await tx.assignmentVersion.create({ - data: { - assignmentId: assignment.id, - versionNumber: "1.0.0", - name: assignment.name, - introduction: assignment.introduction, - instructions: assignment.instructions, - gradingCriteriaOverview: assignment.gradingCriteriaOverview, - timeEstimateMinutes: assignment.timeEstimateMinutes, - type: assignment.type, - graded: assignment.graded, - numAttempts: assignment.numAttempts, - allotedTimeMinutes: assignment.allotedTimeMinutes, - attemptsPerTimeRange: assignment.attemptsPerTimeRange, - attemptsTimeRangeHours: assignment.attemptsTimeRangeHours, - passingGrade: assignment.passingGrade, - displayOrder: assignment.displayOrder, - questionDisplay: assignment.questionDisplay, - numberOfQuestionsPerAttempt: - assignment.numberOfQuestionsPerAttempt, - questionOrder: assignment.questionOrder, - published: assignment.published, - showAssignmentScore: assignment.showAssignmentScore, - showQuestionScore: assignment.showQuestionScore, - showSubmissionFeedback: assignment.showSubmissionFeedback, - showQuestions: assignment.showQuestions, - languageCode: assignment.languageCode, - createdBy: assignment.AssignmentAuthor[0]?.userId || "system", - isDraft: false, // Existing published assignments become version 1 (non-draft) - versionDescription: - "Initial version created from existing assignment", - isActive: true, // Make this the active version - }, - }); - - // Create question versions for all questions - for (const [index, question] of assignment.questions.entries()) { - await tx.questionVersion.create({ - data: { - assignmentVersionId: assignmentVersion.id, - questionId: question.id, - totalPoints: question.totalPoints, - type: question.type, - responseType: question.responseType, - question: question.question, - maxWords: question.maxWords, - scoring: question.scoring, - choices: question.choices, - randomizedChoices: question.randomizedChoices, - answer: question.answer, - gradingContextQuestionIds: question.gradingContextQuestionIds, - maxCharacters: question.maxCharacters, - videoPresentationConfig: question.videoPresentationConfig, - liveRecordingConfig: question.liveRecordingConfig, - displayOrder: index + 1, - }, - }); - } - - // Set the assignment's currentVersionId to this new version - await tx.assignment.update({ - where: { id: assignment.id }, - data: { currentVersionId: assignmentVersion.id }, - }); - - // Create version history entry - await tx.versionHistory.create({ - data: { - assignmentId: assignment.id, - toVersionId: assignmentVersion.id, - action: "initial_version_created", - description: "Initial version created during migration", - userId: assignment.AssignmentAuthor[0]?.userId || "system", - }, - }); - - createdCount++; - console.log( - `✅ Created version 1 for assignment "${assignment.name}" with ${assignment.questions.length} questions`, - ); - }); - } catch (error) { - console.error( - `❌ Failed to create version for assignment "${assignment.name}" (ID: ${assignment.id}):`, - error, - ); - } - } - - console.log( - `\n🎉 Successfully created initial versions for ${createdCount} assignments`, - ); - - // Verify the results - const totalVersions = await prisma.assignmentVersion.count(); - const assignmentsWithVersions = await prisma.assignment.count({ - where: { - versions: { - some: {}, - }, - }, - }); - - console.log(`\n📊 Summary:`); - console.log(`- Total assignment versions in database: ${totalVersions}`); - console.log(`- Assignments with versions: ${assignmentsWithVersions}`); - } catch (error) { - console.error("❌ Script failed with error:", error); - process.exit(1); - } finally { - await prisma.$disconnect(); - } -} - -// Run the script if called directly -// eslint-disable-next-line unicorn/prefer-module -if (require.main === module) { - createInitialVersions() - .then(() => { - console.log("✅ Script completed successfully"); - process.exit(0); - }) - .catch((error) => { - console.error("❌ Script failed:", error); - process.exit(1); - }); -} - -export { createInitialVersions }; diff --git a/apps/api/src/test/migration-verification.test.ts b/apps/api/src/test/migration-verification.test.ts new file mode 100644 index 00000000..aa57363f --- /dev/null +++ b/apps/api/src/test/migration-verification.test.ts @@ -0,0 +1,260 @@ +import { PrismaClient } from '@prisma/client'; + +describe('Assignment Data Consolidation Migration Verification', () => { + let prisma: PrismaClient; + + beforeAll(async () => { + prisma = new PrismaClient(); + await prisma.$connect(); + }); + + afterAll(async () => { + await prisma.$disconnect(); + }); + + describe('Assignment Table Structure', () => { + it('should have simplified Assignment table with only essential fields', async () => { + // Check that Assignment table has the correct simplified structure + const columns = await prisma.$queryRaw>` + SELECT column_name, is_nullable, data_type + FROM information_schema.columns + WHERE table_name = 'Assignment' AND table_schema = 'public' + ORDER BY column_name; + `; + + const columnNames = columns.map(col => col.column_name); + + // Essential fields that should exist + expect(columnNames).toContain('id'); + expect(columnNames).toContain('createdAt'); + expect(columnNames).toContain('updatedAt'); + expect(columnNames).toContain('currentVersionId'); + + // Content fields that should NOT exist (moved to AssignmentVersion) + expect(columnNames).not.toContain('name'); + expect(columnNames).not.toContain('introduction'); + expect(columnNames).not.toContain('instructions'); + expect(columnNames).not.toContain('type'); + expect(columnNames).not.toContain('published'); + expect(columnNames).not.toContain('correctAnswerVisibility'); + }); + + it('should have currentVersionId as NOT NULL', async () => { + const currentVersionIdColumn = await prisma.$queryRaw>` + SELECT is_nullable + FROM information_schema.columns + WHERE table_name = 'Assignment' + AND column_name = 'currentVersionId' + AND table_schema = 'public'; + `; + + expect(currentVersionIdColumn[0]?.is_nullable).toBe('NO'); + }); + + it('should have foreign key constraint on currentVersionId', async () => { + const foreignKeys = await prisma.$queryRaw>` + SELECT constraint_name + FROM information_schema.table_constraints + WHERE table_name = 'Assignment' + AND constraint_type = 'FOREIGN KEY' + AND table_schema = 'public'; + `; + + const hasForeignKey = foreignKeys.some(fk => + fk.constraint_name.includes('currentVersionId') + ); + expect(hasForeignKey).toBe(true); + }); + }); + + describe('Data Integrity Verification', () => { + it('should ensure all assignments have a valid currentVersionId', async () => { + const assignmentsWithoutVersion = await prisma.assignment.count({ + where: { currentVersionId: null } + }); + + expect(assignmentsWithoutVersion).toBe(0); + }); + + it('should ensure all currentVersionId references point to existing AssignmentVersions', async () => { + const invalidReferences = await prisma.$queryRaw>` + SELECT COUNT(*) as count + FROM "Assignment" a + LEFT JOIN "AssignmentVersion" av ON a."currentVersionId" = av.id + WHERE av.id IS NULL; + `; + + expect(Number(invalidReferences[0]?.count)).toBe(0); + }); + + it('should ensure all assignments have at least one version', async () => { + const assignmentsWithoutVersions = await prisma.$queryRaw>` + SELECT COUNT(*) as count + FROM "Assignment" a + WHERE NOT EXISTS ( + SELECT 1 FROM "AssignmentVersion" av WHERE av."assignmentId" = a.id + ); + `; + + expect(Number(assignmentsWithoutVersions[0]?.count)).toBe(0); + }); + + it('should verify that currentVersion points to an active or most recent version', async () => { + // Get all assignments and verify their currentVersion is either active or most recent + const assignments = await prisma.assignment.findMany({ + include: { + currentVersion: true, + versions: { + orderBy: { createdAt: 'desc' } + } + } + }); + + for (const assignment of assignments) { + expect(assignment.currentVersion).toBeTruthy(); + + // Current version should either be active OR be the most recent version + const isActive = assignment.currentVersion!.isActive; + const isMostRecent = assignment.versions[0]?.id === assignment.currentVersion!.id; + + expect(isActive || isMostRecent).toBe(true); + } + }); + }); + + describe('Version Data Verification', () => { + it('should ensure assignments without prior versions got version 0.0.1', async () => { + // Check if there are any 0.0.1 versions (indicating migration created them) + const initialVersions = await prisma.assignmentVersion.findMany({ + where: { versionNumber: '0.0.1' } + }); + + // If there are 0.0.1 versions, verify they have proper data + if (initialVersions.length > 0) { + for (const version of initialVersions) { + expect(version.name).toBeTruthy(); + expect(version.isActive).toBe(true); + expect(version.isDraft).toBe(false); + expect(version.versionDescription).toContain('Initial version created from assignment data'); + } + } + }); + + it('should ensure question versions exist for 0.0.1 versions', async () => { + const initialVersions = await prisma.assignmentVersion.findMany({ + where: { versionNumber: '0.0.1' }, + include: { + questionVersions: true, + assignment: { + include: { + questions: { + where: { isDeleted: false } + } + } + } + } + }); + + for (const version of initialVersions) { + // If the assignment has questions, the version should have question versions + if (version.assignment.questions.length > 0) { + expect(version.questionVersions.length).toBeGreaterThan(0); + expect(version.questionVersions.length).toBe(version.assignment.questions.length); + } + } + }); + + it('should verify question versions have proper display order', async () => { + const questionVersions = await prisma.questionVersion.findMany({ + include: { + assignmentVersion: true + } + }); + + // Group by assignment version and check display order + const versionGroups = questionVersions.reduce((acc, qv) => { + const versionId = qv.assignmentVersionId; + if (!acc[versionId]) acc[versionId] = []; + acc[versionId].push(qv); + return acc; + }, {} as Record); + + for (const [versionId, questions] of Object.entries(versionGroups)) { + const sortedQuestions = questions.sort((a, b) => a.displayOrder - b.displayOrder); + + // Display order should start from 1 and be sequential + sortedQuestions.forEach((question, index) => { + expect(question.displayOrder).toBe(index + 1); + }); + } + }); + }); + + describe('Data Consistency Verification', () => { + it('should ensure no data was lost during migration', async () => { + // Count total assignments + const assignmentCount = await prisma.assignment.count(); + + // Count assignments with versions + const assignmentsWithVersions = await prisma.$queryRaw>` + SELECT COUNT(DISTINCT "assignmentId") as count + FROM "AssignmentVersion"; + `; + + expect(Number(assignmentsWithVersions[0]?.count)).toBe(assignmentCount); + }); + + it('should verify assignment metadata is preserved in versions', async () => { + const assignments = await prisma.assignment.findMany({ + include: { + currentVersion: true + } + }); + + for (const assignment of assignments) { + const version = assignment.currentVersion!; + + // Basic checks that version has content + expect(version.name).toBeTruthy(); + expect(typeof version.published).toBe('boolean'); + expect(typeof version.graded).toBe('boolean'); + expect(version.correctAnswerVisibility).toMatch(/^(NEVER|ALWAYS|ON_PASS)$/); + } + }); + + it('should ensure foreign key relationships are intact', async () => { + // Test that we can successfully join Assignment -> AssignmentVersion -> QuestionVersion + const testJoin = await prisma.assignment.findMany({ + include: { + currentVersion: { + include: { + questionVersions: true + } + } + }, + take: 5 + }); + + expect(testJoin).toBeTruthy(); + // If we get here without errors, the foreign key relationships are working + }); + }); + + describe('Performance and Index Verification', () => { + it('should ensure currentVersionId has proper indexing', async () => { + const indexes = await prisma.$queryRaw>` + SELECT indexname + FROM pg_indexes + WHERE tablename = 'Assignment' + AND schemaname = 'public'; + `; + + const hasCurrentVersionIndex = indexes.some(idx => + idx.indexname.includes('currentVersionId') + ); + + // This might not exist if not explicitly created, but foreign key creates one + expect(indexes.length).toBeGreaterThan(0); + }); + }); +}); \ No newline at end of file diff --git a/apps/web/app/Helpers/checkDiff.ts b/apps/web/app/Helpers/checkDiff.ts index e5498a8d..ce9d2c9f 100644 --- a/apps/web/app/Helpers/checkDiff.ts +++ b/apps/web/app/Helpers/checkDiff.ts @@ -73,7 +73,7 @@ export function useChangesSummary(): string { showQuestionScore, showAssignmentScore, showQuestions, - showCorrectAnswer, + correctAnswerVisibility, } = useAssignmentFeedbackConfig(); const changesSummary = useMemo(() => { @@ -115,8 +115,8 @@ export function useChangesSummary(): string { if ( !safeCompare( - showCorrectAnswer, - originalAssignment.showCorrectAnswer ?? true, + correctAnswerVisibility, + originalAssignment.correctAnswerVisibility ?? "ALWAYS", ) ) diffs.push("Changed correct answer visibility."); diff --git a/apps/web/app/author/(components)/Header/CheckLearnerSideButton.tsx b/apps/web/app/author/(components)/Header/CheckLearnerSideButton.tsx index 99cdcf53..92dc0223 100644 --- a/apps/web/app/author/(components)/Header/CheckLearnerSideButton.tsx +++ b/apps/web/app/author/(components)/Header/CheckLearnerSideButton.tsx @@ -24,13 +24,13 @@ const CheckLearnerSideButton: FC = (props) => { showQuestionScore, showAssignmentScore, showQuestions, - showCorrectAnswer, + correctAnswerVisibility, ] = useAssignmentFeedbackConfig((state) => [ state.showSubmissionFeedback, state.showQuestionScore, state.showAssignmentScore, state.showQuestions, - state.showCorrectAnswer, + state.correctAnswerVisibility, ]); const assignmentConfig = { questionDisplay: assignmentConfigstate.questionDisplay, @@ -45,7 +45,7 @@ const CheckLearnerSideButton: FC = (props) => { showQuestions: showQuestions, showAssignmentScore: showAssignmentScore, showQuestionScore: showQuestionScore, - showCorrectAnswer: showCorrectAnswer, + correctAnswerVisibility: correctAnswerVisibility, instructions: authorState.instructions ?? "", gradingCriteriaOverview: authorState.gradingCriteriaOverview ?? "", showSubmissionFeedback: showSubmissionFeedback, diff --git a/apps/web/app/author/(components)/Header/index.tsx b/apps/web/app/author/(components)/Header/index.tsx index 633e1f87..3011c1cd 100644 --- a/apps/web/app/author/(components)/Header/index.tsx +++ b/apps/web/app/author/(components)/Header/index.tsx @@ -162,13 +162,13 @@ function AuthorHeader() { showQuestionScore, showAssignmentScore, showQuestions, - showCorrectAnswer, + correctAnswerVisibility, ] = useAssignmentFeedbackConfig((state) => [ state.showSubmissionFeedback, state.showQuestionScore, state.showAssignmentScore, state.showQuestions, - state.showCorrectAnswer, + state.correctAnswerVisibility, ]); const role = useAuthorStore((state) => state.role); @@ -279,7 +279,7 @@ function AuthorHeader() { showSubmissionFeedback: newAssignment.showSubmissionFeedback, showQuestionScore: newAssignment.showQuestionScore, showAssignmentScore: newAssignment.showAssignmentScore, - showCorrectAnswer: newAssignment.showCorrectAnswer, + correctAnswerVisibility: newAssignment.correctAnswerVisibility, }); useAuthorStore.getState().setName(newAssignment.name); @@ -515,7 +515,7 @@ function AuthorHeader() { showQuestions, showQuestionScore, showAssignmentScore, - showCorrectAnswer, + correctAnswerVisibility, numberOfQuestionsPerAttempt, questions: questionsAreDifferent ? processQuestions(clonedCurrentQuestions) diff --git a/apps/web/app/author/(components)/StepTwo/AssignmentFeedback.tsx b/apps/web/app/author/(components)/StepTwo/AssignmentFeedback.tsx index 488b7606..48891a22 100644 --- a/apps/web/app/author/(components)/StepTwo/AssignmentFeedback.tsx +++ b/apps/web/app/author/(components)/StepTwo/AssignmentFeedback.tsx @@ -64,7 +64,7 @@ const Component: FC = () => { setShowSubmissionFeedback, setShowQuestionScore, setShowQuestion, - setShowCorrectAnswer, + setCorrectAnswerVisibility, ] = useAssignmentFeedbackConfig((state) => [ state.verbosityLevel, state.setVerbosityLevel, @@ -72,20 +72,20 @@ const Component: FC = () => { state.setShowSubmissionFeedback, state.setShowQuestionScore, state.setShowQuestion, - state.setShowCorrectAnswer, + state.setCorrectAnswerVisibility, ]); const [ showAssignmentScore, showSubmissionFeedback, showQuestionScore, showQuestions, - showCorrectAnswer, + correctAnswerVisibility, ] = useAssignmentFeedbackConfig((state) => [ state.showAssignmentScore, state.showSubmissionFeedback, state.showQuestionScore, state.showQuestions, - state.showCorrectAnswer, + state.correctAnswerVisibility, ]); const handleButtonClick = (verbosity: VerbosityLevels) => { setVerbosityLevel(verbosity); @@ -95,21 +95,21 @@ const Component: FC = () => { setShowSubmissionFeedback(true); setShowQuestionScore(true); setShowQuestion(true); - setShowCorrectAnswer(true); + setCorrectAnswerVisibility("ALWAYS"); break; case "Custom": setShowAssignmentScore(true); setShowSubmissionFeedback(false); setShowQuestionScore(true); setShowQuestion(true); - setShowCorrectAnswer(true); + setCorrectAnswerVisibility("ALWAYS"); break; case "None": setShowAssignmentScore(false); setShowSubmissionFeedback(false); setShowQuestionScore(false); setShowQuestion(false); - setShowCorrectAnswer(false); + setCorrectAnswerVisibility("NEVER"); break; default: break; @@ -121,7 +121,7 @@ const Component: FC = () => { showSubmissionFeedback && showQuestionScore && showQuestions && - showCorrectAnswer + correctAnswerVisibility === "ALWAYS" ) { setVerbosityLevel("Full"); } else if ( @@ -129,7 +129,7 @@ const Component: FC = () => { !showSubmissionFeedback && !showQuestionScore && !showQuestions && - !showCorrectAnswer + correctAnswerVisibility === "NEVER" ) { setVerbosityLevel("None"); } else { @@ -140,7 +140,7 @@ const Component: FC = () => { showSubmissionFeedback, showQuestionScore, showQuestions, - showCorrectAnswer, + correctAnswerVisibility, ]); return ( diff --git a/apps/web/app/author/(components)/StepTwo/FeedbackSettings.tsx b/apps/web/app/author/(components)/StepTwo/FeedbackSettings.tsx index f26bc591..5e6768dc 100644 --- a/apps/web/app/author/(components)/StepTwo/FeedbackSettings.tsx +++ b/apps/web/app/author/(components)/StepTwo/FeedbackSettings.tsx @@ -2,6 +2,7 @@ import { cn } from "@/lib/strings"; import { useAssignmentFeedbackConfig } from "@/stores/assignmentFeedbackConfig"; +import type { CorrectAnswerVisibility } from "@/config/types"; interface SettingItemProps { title: string; @@ -51,18 +52,109 @@ const SettingItem: React.FC = ({ ); }; +interface CorrectAnswerSettingProps { + title: string; + description: string; + value: CorrectAnswerVisibility; + onChange: (value: CorrectAnswerVisibility) => void; +} + +const CorrectAnswerSetting: React.FC = ({ + title, + description, + value, + onChange, +}) => { + const showCorrectAnswers = value !== "NEVER"; + + const handleToggleChange = () => { + if (showCorrectAnswers) { + onChange("NEVER"); + } else { + onChange("ALWAYS"); // Default to ALWAYS when enabling + } + }; + + const radioOptions = [ + { + value: "ON_PASS" as const, + label: "Show only on pass", + description: "Correct answers will only be visible when learners pass", + }, + { + value: "ALWAYS" as const, + label: "Always show", + description: "Correct answers will always be visible after submission", + }, + ]; + + return ( +
+
+
{title}
+
+ {description} +
+ + {/* Radio buttons for when to show (only visible if toggle is enabled) */} + {showCorrectAnswers && ( +
+
When to show:
+ {radioOptions.map((option) => ( + + ))} +
+ )} +
+ + {/* Toggle for enabling/disabling correct answers - positioned on the right side */} +
+ +
+
+ ); +}; + const SettingsContainer: React.FC = () => { const { toggleShowAssignmentScore, toggleShowSubmissionFeedback, toggleShowQuestionScore, toggleShowQuestions, - toggleShowCorrectAnswer, + setCorrectAnswerVisibility, showAssignmentScore, showSubmissionFeedback, showQuestionScore, showQuestions, - showCorrectAnswer, + correctAnswerVisibility, } = useAssignmentFeedbackConfig(); const settingsData = [ @@ -92,13 +184,6 @@ const SettingsContainer: React.FC = () => { value: showQuestions, toggleValue: toggleShowQuestions, }, - { - title: "Show Correct Answers on Pass", - description: - "The correct answer will be visible to the learner only if they pass the assignment.", - value: showCorrectAnswer, - toggleValue: toggleShowCorrectAnswer, - }, ] as const; return (
@@ -107,11 +192,18 @@ const SettingsContainer: React.FC = () => { key={index} title={setting.title} description={setting.description} - lastItem={index === settingsData.length - 1} + lastItem={false} value={setting.value} toggleValue={setting.toggleValue} /> ))} + +
); }; diff --git a/apps/web/app/author/[assignmentId]/review/page.tsx b/apps/web/app/author/[assignmentId]/review/page.tsx index 57cbacc5..2a36de3c 100644 --- a/apps/web/app/author/[assignmentId]/review/page.tsx +++ b/apps/web/app/author/[assignmentId]/review/page.tsx @@ -796,14 +796,14 @@ function Component() { showQuestionScore, showAssignmentScore, showQuestions, - showCorrectAnswer, + correctAnswerVisibility, ] = useAssignmentFeedbackConfig((state) => [ state.verbosityLevel, state.showSubmissionFeedback, state.showQuestionScore, state.showAssignmentScore, state.showQuestions, - state.showCorrectAnswer, + state.correctAnswerVisibility, ]); const router = useRouter(); @@ -844,7 +844,7 @@ function Component() { showAssignmentScore: filteredChanges.some((c) => c.includes("Changed assignment score visibility"), ), - showCorrectAnswer: filteredChanges.some((c) => + correctAnswerVisibility: filteredChanges.some((c) => c.includes("Changed correct answer visibility"), ), questionOrder: filteredChanges.some((c) => diff --git a/apps/web/app/learner/[assignmentId]/successPage/[submissionId]/page.tsx b/apps/web/app/learner/[assignmentId]/successPage/[submissionId]/page.tsx index f4e90e81..66f23058 100644 --- a/apps/web/app/learner/[assignmentId]/successPage/[submissionId]/page.tsx +++ b/apps/web/app/learner/[assignmentId]/successPage/[submissionId]/page.tsx @@ -12,6 +12,7 @@ import { import { getCompletedAttempt, getFeedback, + getSuccessPageData, getUser, submitFeedback, submitRegradingRequest, @@ -56,7 +57,9 @@ function SuccessPage() { useState(); const [showSubmissionFeedback, setShowSubmissionFeedback] = useState(false); - const [showCorrectAnswer, setShowCorrectAnswer] = useState(true); + const [correctAnswerVisibility, setCorrectAnswerVisibility] = useState< + "NEVER" | "ALWAYS" | "ON_PASS" + >("ALWAYS"); const [zustandShowSubmissionFeedback, zustandShowQuestions] = useAssignmentDetails((state) => [ state.assignmentDetails?.showSubmissionFeedback ?? true, @@ -120,7 +123,9 @@ function SuccessPage() { setShowSubmissionFeedback( submissionDetails.showSubmissionFeedback || false, ); - setShowCorrectAnswer(submissionDetails.showCorrectAnswer ?? true); + setCorrectAnswerVisibility( + submissionDetails.correctAnswerVisibility ?? "ALWAYS", + ); setShowQuestions(submissionDetails.showQuestions); setUserPreferredLanguage(submissionDetails.preferredLanguage); setGrade(submissionDetails.grade * 100); @@ -160,8 +165,8 @@ function SuccessPage() { setTotalPointsEarned(zustandTotalPointsEarned); setTotalPoints(zustandTotalPoints); setAssignmentDetails(zustandAssignmentDetails); - setShowCorrectAnswer( - zustandAssignmentDetails?.showCorrectAnswer ?? true, + setCorrectAnswerVisibility( + zustandAssignmentDetails?.correctAnswerVisibility ?? "ALWAYS", ); setLoading(false); } else { @@ -606,9 +611,13 @@ function SuccessPage() { question={question} language={userPreferredLanguage} showSubmissionFeedback={showSubmissionFeedback} - showCorrectAnswer={ - !showCorrectAnswer && grade >= passingGrade - } + showCorrectAnswer={(() => { + if (correctAnswerVisibility === "NEVER") return false; + if (correctAnswerVisibility === "ALWAYS") return true; + if (correctAnswerVisibility === "ON_PASS") + return grade >= passingGrade; + return false; + })()} /> ))} diff --git a/apps/web/components/version-control/VersionTreeView.tsx b/apps/web/components/version-control/VersionTreeView.tsx index e7a09734..74c1e7d3 100644 --- a/apps/web/components/version-control/VersionTreeView.tsx +++ b/apps/web/components/version-control/VersionTreeView.tsx @@ -867,7 +867,7 @@ export function VersionTreeView({ assignmentId }: Props) { -
+
{/* Header */} diff --git a/apps/web/config/types.ts b/apps/web/config/types.ts index 0355539f..d4892963 100644 --- a/apps/web/config/types.ts +++ b/apps/web/config/types.ts @@ -216,7 +216,7 @@ export type AuthorAssignmentState = { showQuestionScore: boolean; showSubmissionFeedback: boolean; showQuestions: boolean; - showCorrectAnswer: boolean; + correctAnswerVisibility: CorrectAnswerVisibility; updatedAt: number; numberOfQuestionsPerAttempt?: number; }; @@ -560,7 +560,7 @@ export type FeedbackData = { showQuestions: boolean; showAssignmentScore: boolean; - showCorrectAnswer: boolean; + correctAnswerVisibility: CorrectAnswerVisibility; updatedAt: number | undefined; }; @@ -583,7 +583,7 @@ export type ReplaceAssignmentRequest = { showAssignmentScore?: boolean; showQuestionScore?: boolean; showSubmissionFeedback?: boolean; - showCorrectAnswer?: boolean; + correctAnswerVisibility?: CorrectAnswerVisibility; updatedAt: number; questionVariationNumber?: number; versionDescription?: string; @@ -626,7 +626,7 @@ export interface AssignmentAttemptWithQuestions extends AssignmentAttempt { showAssignmentScore?: boolean; showQuestions?: boolean; showQuestionScore?: boolean; - showCorrectAnswer?: boolean; + correctAnswerVisibility?: CorrectAnswerVisibility; comments?: string; preferredLanguage?: string; } @@ -648,7 +648,7 @@ export interface AssignmentDetails { showAssignmentScore?: boolean; showQuestionScore?: boolean; showSubmissionFeedback?: boolean; - showCorrectAnswer?: boolean; + correctAnswerVisibility?: CorrectAnswerVisibility; numberOfQuestionsPerAttempt?: number; } @@ -661,7 +661,7 @@ export interface AssignmentDetailsLocal extends AssignmentDetails { showAssignmentScore: boolean; showQuestionScore: boolean; showSubmissionFeedback: boolean; - showCorrectAnswer: boolean; + correctAnswerVisibility: CorrectAnswerVisibility; } export type BaseBackendResponse = { @@ -690,6 +690,8 @@ export type LearnerAssignmentState = | "completed"; export type VerbosityLevels = "Full" | "Partial" | "None" | "Custom"; + +export type CorrectAnswerVisibility = "NEVER" | "ALWAYS" | "ON_PASS"; export type VerbosityState = { verbosity: VerbosityLevels; loading: boolean; diff --git a/apps/web/lib/api-health.ts b/apps/web/lib/api-health.ts new file mode 100644 index 00000000..db147c5a --- /dev/null +++ b/apps/web/lib/api-health.ts @@ -0,0 +1,94 @@ +/** + * API Health Check and Connection Utilities + */ + +const API_GATEWAY_HOST = process.env.API_GATEWAY_HOST || 'http://localhost:8000'; + +/** + * Check if API Gateway is healthy + */ +export async function checkApiHealth(): Promise { + try { + const response = await fetch(`${API_GATEWAY_HOST}/health`, { + method: 'GET', + signal: AbortSignal.timeout(5000), // 5 second timeout + }); + return response.ok; + } catch (error) { + console.warn('API health check failed:', error); + return false; + } +} + +/** + * Wait for API Gateway to be ready with exponential backoff + */ +export async function waitForApiReady(maxRetries = 30): Promise { + let retries = 0; + let delay = 1000; // Start with 1 second + + while (retries < maxRetries) { + const isHealthy = await checkApiHealth(); + + if (isHealthy) { + console.log('✅ API Gateway is ready'); + return true; + } + + retries++; + console.log(`⏳ API Gateway not ready, retry ${retries}/${maxRetries} in ${delay}ms`); + + await new Promise(resolve => setTimeout(resolve, delay)); + + // Exponential backoff with max 5 seconds + delay = Math.min(delay * 1.2, 5000); + } + + console.error('❌ API Gateway failed to become ready after', maxRetries, 'retries'); + return false; +} + +/** + * Make API request with retry logic + */ +export async function apiRequestWithRetry( + url: string, + options: RequestInit = {}, + maxRetries = 3 +): Promise { + let lastError: Error; + + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + const response = await fetch(url, { + ...options, + signal: AbortSignal.timeout(10000), // 10 second timeout + }); + + // If successful, return immediately + if (response.ok || response.status < 500) { + return response; + } + + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } catch (error) { + lastError = error as Error; + + // Don't retry on certain errors + if (error instanceof TypeError && error.message.includes('Failed to fetch')) { + console.warn(`API request failed on attempt ${attempt}:`, error.message); + + if (attempt < maxRetries) { + const delay = attempt * 1000; // Linear backoff + await new Promise(resolve => setTimeout(resolve, delay)); + continue; + } + } + + // For other errors, throw immediately + throw error; + } + } + + throw lastError!; +} \ No newline at end of file diff --git a/apps/web/lib/learner.ts b/apps/web/lib/learner.ts index 3c2c7813..da01d9a6 100644 --- a/apps/web/lib/learner.ts +++ b/apps/web/lib/learner.ts @@ -121,6 +121,49 @@ export async function getCompletedAttempt( } } +/** + * Gets unified success page data for an attempt (works for both authors and learners). + * @param assignmentId The id of the assignment. + * @param attemptId The id of the attempt. + * @param authorData Optional author data from Zustand stores (for authors only). + * @param cookies Optional cookies for authentication. + * @returns Success page data or undefined if error. + */ +export async function getSuccessPageData( + assignmentId: number, + attemptId: number, + authorData?: { + questions: any[]; + grade: number; + totalPointsEarned: number; + totalPointsPossible: number; + responses: any[]; + }, + cookies?: string, +): Promise { + const endpointURL = `${getApiRoutes().assignments}/${assignmentId}/attempts/${attemptId}/success-page-data`; + + try { + const res = await fetch(endpointURL, { + method: "POST", + headers: { + "Content-Type": "application/json", + ...(cookies ? { Cookie: cookies } : {}), + }, + body: JSON.stringify(authorData || {}), + }); + if (!res.ok) { + const errorBody = (await res.json()) as { message: string }; + throw new Error(errorBody.message || "Failed to get success page data"); + } + const data = (await res.json()) as any; + return data; + } catch (err) { + console.error("Error fetching success page data:", err); + return undefined; + } +} + /** * Submits an answer for a given assignment, attempt, and question. */ diff --git a/apps/web/lib/talkToBackend.ts b/apps/web/lib/talkToBackend.ts index 7d4d7d65..14ba2602 100644 --- a/apps/web/lib/talkToBackend.ts +++ b/apps/web/lib/talkToBackend.ts @@ -70,6 +70,7 @@ export const submitReportAuthor = apiAuthor.submitReportAuthor; export const createAttempt = apiLearner.createAttempt; export const getAttempt = apiLearner.getAttempt; export const getCompletedAttempt = apiLearner.getCompletedAttempt; +export const getSuccessPageData = apiLearner.getSuccessPageData; export const submitQuestion = apiLearner.submitQuestion; export const getLiveRecordingFeedback = apiLearner.getLiveRecordingFeedback; export const submitAssignment = apiLearner.submitAssignment; diff --git a/apps/web/models/face_expression_model/face_expression_model-weights_manifest.json b/apps/web/models/face_expression_model/face_expression_model-weights_manifest.json index 71f1720c..7b74b5ab 100644 --- a/apps/web/models/face_expression_model/face_expression_model-weights_manifest.json +++ b/apps/web/models/face_expression_model/face_expression_model-weights_manifest.json @@ -1,344 +1 @@ -[ - { - "weights": [ - { - "name": "dense0/conv0/filters", - "shape": [3, 3, 3, 32], - "dtype": "float32", - "quantization": { - "dtype": "uint8", - "scale": 0.0057930146946626555, - "min": -0.7125408074435067 - } - }, - { "name": "dense0/conv0/bias", "shape": [32], "dtype": "float32" }, - { - "name": "dense0/conv1/depthwise_filter", - "shape": [3, 3, 32, 1], - "dtype": "float32", - "quantization": { - "dtype": "uint8", - "scale": 0.006473719839956246, - "min": -0.6408982641556684 - } - }, - { - "name": "dense0/conv1/pointwise_filter", - "shape": [1, 1, 32, 32], - "dtype": "float32", - "quantization": { - "dtype": "uint8", - "scale": 0.010509579321917366, - "min": -1.408283629136927 - } - }, - { "name": "dense0/conv1/bias", "shape": [32], "dtype": "float32" }, - { - "name": "dense0/conv2/depthwise_filter", - "shape": [3, 3, 32, 1], - "dtype": "float32", - "quantization": { - "dtype": "uint8", - "scale": 0.005666389652326995, - "min": -0.7252978754978554 - } - }, - { - "name": "dense0/conv2/pointwise_filter", - "shape": [1, 1, 32, 32], - "dtype": "float32", - "quantization": { - "dtype": "uint8", - "scale": 0.010316079270605948, - "min": -1.1760330368490781 - } - }, - { "name": "dense0/conv2/bias", "shape": [32], "dtype": "float32" }, - { - "name": "dense0/conv3/depthwise_filter", - "shape": [3, 3, 32, 1], - "dtype": "float32", - "quantization": { - "dtype": "uint8", - "scale": 0.0063220320963392074, - "min": -0.853474333005793 - } - }, - { - "name": "dense0/conv3/pointwise_filter", - "shape": [1, 1, 32, 32], - "dtype": "float32", - "quantization": { - "dtype": "uint8", - "scale": 0.010322785377502442, - "min": -1.4658355236053466 - } - }, - { "name": "dense0/conv3/bias", "shape": [32], "dtype": "float32" }, - { - "name": "dense1/conv0/depthwise_filter", - "shape": [3, 3, 32, 1], - "dtype": "float32", - "quantization": { - "dtype": "uint8", - "scale": 0.0042531527724920535, - "min": -0.5741756242864272 - } - }, - { - "name": "dense1/conv0/pointwise_filter", - "shape": [1, 1, 32, 64], - "dtype": "float32", - "quantization": { - "dtype": "uint8", - "scale": 0.010653339647779278, - "min": -1.1825207009035 - } - }, - { "name": "dense1/conv0/bias", "shape": [64], "dtype": "float32" }, - { - "name": "dense1/conv1/depthwise_filter", - "shape": [3, 3, 64, 1], - "dtype": "float32", - "quantization": { - "dtype": "uint8", - "scale": 0.005166931012097527, - "min": -0.6355325144879957 - } - }, - { - "name": "dense1/conv1/pointwise_filter", - "shape": [1, 1, 64, 64], - "dtype": "float32", - "quantization": { - "dtype": "uint8", - "scale": 0.011478300188101974, - "min": -1.3888743227603388 - } - }, - { "name": "dense1/conv1/bias", "shape": [64], "dtype": "float32" }, - { - "name": "dense1/conv2/depthwise_filter", - "shape": [3, 3, 64, 1], - "dtype": "float32", - "quantization": { - "dtype": "uint8", - "scale": 0.006144821410085641, - "min": -0.8479853545918185 - } - }, - { - "name": "dense1/conv2/pointwise_filter", - "shape": [1, 1, 64, 64], - "dtype": "float32", - "quantization": { - "dtype": "uint8", - "scale": 0.010541967317169788, - "min": -1.3809977185492421 - } - }, - { "name": "dense1/conv2/bias", "shape": [64], "dtype": "float32" }, - { - "name": "dense1/conv3/depthwise_filter", - "shape": [3, 3, 64, 1], - "dtype": "float32", - "quantization": { - "dtype": "uint8", - "scale": 0.005769844849904378, - "min": -0.686611537138621 - } - }, - { - "name": "dense1/conv3/pointwise_filter", - "shape": [1, 1, 64, 64], - "dtype": "float32", - "quantization": { - "dtype": "uint8", - "scale": 0.010939095534530341, - "min": -1.2689350820055196 - } - }, - { "name": "dense1/conv3/bias", "shape": [64], "dtype": "float32" }, - { - "name": "dense2/conv0/depthwise_filter", - "shape": [3, 3, 64, 1], - "dtype": "float32", - "quantization": { - "dtype": "uint8", - "scale": 0.0037769308277204924, - "min": -0.40790852939381317 - } - }, - { - "name": "dense2/conv0/pointwise_filter", - "shape": [1, 1, 64, 128], - "dtype": "float32", - "quantization": { - "dtype": "uint8", - "scale": 0.01188667194516051, - "min": -1.4382873053644218 - } - }, - { "name": "dense2/conv0/bias", "shape": [128], "dtype": "float32" }, - { - "name": "dense2/conv1/depthwise_filter", - "shape": [3, 3, 128, 1], - "dtype": "float32", - "quantization": { - "dtype": "uint8", - "scale": 0.006497045825509464, - "min": -0.8381189114907208 - } - }, - { - "name": "dense2/conv1/pointwise_filter", - "shape": [1, 1, 128, 128], - "dtype": "float32", - "quantization": { - "dtype": "uint8", - "scale": 0.011632198913424622, - "min": -1.3377028750438316 - } - }, - { "name": "dense2/conv1/bias", "shape": [128], "dtype": "float32" }, - { - "name": "dense2/conv2/depthwise_filter", - "shape": [3, 3, 128, 1], - "dtype": "float32", - "quantization": { - "dtype": "uint8", - "scale": 0.005947182225246056, - "min": -0.7969224181829715 - } - }, - { - "name": "dense2/conv2/pointwise_filter", - "shape": [1, 1, 128, 128], - "dtype": "float32", - "quantization": { - "dtype": "uint8", - "scale": 0.011436844339557722, - "min": -1.4524792311238306 - } - }, - { "name": "dense2/conv2/bias", "shape": [128], "dtype": "float32" }, - { - "name": "dense2/conv3/depthwise_filter", - "shape": [3, 3, 128, 1], - "dtype": "float32", - "quantization": { - "dtype": "uint8", - "scale": 0.006665432686899222, - "min": -0.8998334127313949 - } - }, - { - "name": "dense2/conv3/pointwise_filter", - "shape": [1, 1, 128, 128], - "dtype": "float32", - "quantization": { - "dtype": "uint8", - "scale": 0.01283421422920975, - "min": -1.642779421338848 - } - }, - { "name": "dense2/conv3/bias", "shape": [128], "dtype": "float32" }, - { - "name": "dense3/conv0/depthwise_filter", - "shape": [3, 3, 128, 1], - "dtype": "float32", - "quantization": { - "dtype": "uint8", - "scale": 0.004711699953266218, - "min": -0.6737730933170692 - } - }, - { - "name": "dense3/conv0/pointwise_filter", - "shape": [1, 1, 128, 256], - "dtype": "float32", - "quantization": { - "dtype": "uint8", - "scale": 0.010955964817720302, - "min": -1.3914075318504784 - } - }, - { "name": "dense3/conv0/bias", "shape": [256], "dtype": "float32" }, - { - "name": "dense3/conv1/depthwise_filter", - "shape": [3, 3, 256, 1], - "dtype": "float32", - "quantization": { - "dtype": "uint8", - "scale": 0.00554193468654857, - "min": -0.7149095745647656 - } - }, - { - "name": "dense3/conv1/pointwise_filter", - "shape": [1, 1, 256, 256], - "dtype": "float32", - "quantization": { - "dtype": "uint8", - "scale": 0.016790372250126858, - "min": -2.484975093018775 - } - }, - { "name": "dense3/conv1/bias", "shape": [256], "dtype": "float32" }, - { - "name": "dense3/conv2/depthwise_filter", - "shape": [3, 3, 256, 1], - "dtype": "float32", - "quantization": { - "dtype": "uint8", - "scale": 0.006361540626077091, - "min": -0.8142772001378676 - } - }, - { - "name": "dense3/conv2/pointwise_filter", - "shape": [1, 1, 256, 256], - "dtype": "float32", - "quantization": { - "dtype": "uint8", - "scale": 0.01777329678628959, - "min": -1.7062364914838006 - } - }, - { "name": "dense3/conv2/bias", "shape": [256], "dtype": "float32" }, - { - "name": "dense3/conv3/depthwise_filter", - "shape": [3, 3, 256, 1], - "dtype": "float32", - "quantization": { - "dtype": "uint8", - "scale": 0.006900275922289082, - "min": -0.8625344902861353 - } - }, - { - "name": "dense3/conv3/pointwise_filter", - "shape": [1, 1, 256, 256], - "dtype": "float32", - "quantization": { - "dtype": "uint8", - "scale": 0.015449936717164282, - "min": -1.9003422162112067 - } - }, - { "name": "dense3/conv3/bias", "shape": [256], "dtype": "float32" }, - { - "name": "fc/weights", - "shape": [256, 7], - "dtype": "float32", - "quantization": { - "dtype": "uint8", - "scale": 0.004834276554631252, - "min": -0.7203072066400565 - } - }, - { "name": "fc/bias", "shape": [7], "dtype": "float32" } - ], - "paths": ["face_expression_model-shard1"] - } -] +[{"weights":[{"name":"dense0/conv0/filters","shape":[3,3,3,32],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0057930146946626555,"min":-0.7125408074435067}},{"name":"dense0/conv0/bias","shape":[32],"dtype":"float32"},{"name":"dense0/conv1/depthwise_filter","shape":[3,3,32,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.006473719839956246,"min":-0.6408982641556684}},{"name":"dense0/conv1/pointwise_filter","shape":[1,1,32,32],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.010509579321917366,"min":-1.408283629136927}},{"name":"dense0/conv1/bias","shape":[32],"dtype":"float32"},{"name":"dense0/conv2/depthwise_filter","shape":[3,3,32,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.005666389652326995,"min":-0.7252978754978554}},{"name":"dense0/conv2/pointwise_filter","shape":[1,1,32,32],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.010316079270605948,"min":-1.1760330368490781}},{"name":"dense0/conv2/bias","shape":[32],"dtype":"float32"},{"name":"dense0/conv3/depthwise_filter","shape":[3,3,32,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0063220320963392074,"min":-0.853474333005793}},{"name":"dense0/conv3/pointwise_filter","shape":[1,1,32,32],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.010322785377502442,"min":-1.4658355236053466}},{"name":"dense0/conv3/bias","shape":[32],"dtype":"float32"},{"name":"dense1/conv0/depthwise_filter","shape":[3,3,32,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0042531527724920535,"min":-0.5741756242864272}},{"name":"dense1/conv0/pointwise_filter","shape":[1,1,32,64],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.010653339647779278,"min":-1.1825207009035}},{"name":"dense1/conv0/bias","shape":[64],"dtype":"float32"},{"name":"dense1/conv1/depthwise_filter","shape":[3,3,64,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.005166931012097527,"min":-0.6355325144879957}},{"name":"dense1/conv1/pointwise_filter","shape":[1,1,64,64],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.011478300188101974,"min":-1.3888743227603388}},{"name":"dense1/conv1/bias","shape":[64],"dtype":"float32"},{"name":"dense1/conv2/depthwise_filter","shape":[3,3,64,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.006144821410085641,"min":-0.8479853545918185}},{"name":"dense1/conv2/pointwise_filter","shape":[1,1,64,64],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.010541967317169788,"min":-1.3809977185492421}},{"name":"dense1/conv2/bias","shape":[64],"dtype":"float32"},{"name":"dense1/conv3/depthwise_filter","shape":[3,3,64,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.005769844849904378,"min":-0.686611537138621}},{"name":"dense1/conv3/pointwise_filter","shape":[1,1,64,64],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.010939095534530341,"min":-1.2689350820055196}},{"name":"dense1/conv3/bias","shape":[64],"dtype":"float32"},{"name":"dense2/conv0/depthwise_filter","shape":[3,3,64,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0037769308277204924,"min":-0.40790852939381317}},{"name":"dense2/conv0/pointwise_filter","shape":[1,1,64,128],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.01188667194516051,"min":-1.4382873053644218}},{"name":"dense2/conv0/bias","shape":[128],"dtype":"float32"},{"name":"dense2/conv1/depthwise_filter","shape":[3,3,128,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.006497045825509464,"min":-0.8381189114907208}},{"name":"dense2/conv1/pointwise_filter","shape":[1,1,128,128],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.011632198913424622,"min":-1.3377028750438316}},{"name":"dense2/conv1/bias","shape":[128],"dtype":"float32"},{"name":"dense2/conv2/depthwise_filter","shape":[3,3,128,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.005947182225246056,"min":-0.7969224181829715}},{"name":"dense2/conv2/pointwise_filter","shape":[1,1,128,128],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.011436844339557722,"min":-1.4524792311238306}},{"name":"dense2/conv2/bias","shape":[128],"dtype":"float32"},{"name":"dense2/conv3/depthwise_filter","shape":[3,3,128,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.006665432686899222,"min":-0.8998334127313949}},{"name":"dense2/conv3/pointwise_filter","shape":[1,1,128,128],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.01283421422920975,"min":-1.642779421338848}},{"name":"dense2/conv3/bias","shape":[128],"dtype":"float32"},{"name":"dense3/conv0/depthwise_filter","shape":[3,3,128,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.004711699953266218,"min":-0.6737730933170692}},{"name":"dense3/conv0/pointwise_filter","shape":[1,1,128,256],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.010955964817720302,"min":-1.3914075318504784}},{"name":"dense3/conv0/bias","shape":[256],"dtype":"float32"},{"name":"dense3/conv1/depthwise_filter","shape":[3,3,256,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.00554193468654857,"min":-0.7149095745647656}},{"name":"dense3/conv1/pointwise_filter","shape":[1,1,256,256],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.016790372250126858,"min":-2.484975093018775}},{"name":"dense3/conv1/bias","shape":[256],"dtype":"float32"},{"name":"dense3/conv2/depthwise_filter","shape":[3,3,256,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.006361540626077091,"min":-0.8142772001378676}},{"name":"dense3/conv2/pointwise_filter","shape":[1,1,256,256],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.01777329678628959,"min":-1.7062364914838006}},{"name":"dense3/conv2/bias","shape":[256],"dtype":"float32"},{"name":"dense3/conv3/depthwise_filter","shape":[3,3,256,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.006900275922289082,"min":-0.8625344902861353}},{"name":"dense3/conv3/pointwise_filter","shape":[1,1,256,256],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.015449936717164282,"min":-1.9003422162112067}},{"name":"dense3/conv3/bias","shape":[256],"dtype":"float32"},{"name":"fc/weights","shape":[256,7],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.004834276554631252,"min":-0.7203072066400565}},{"name":"fc/bias","shape":[7],"dtype":"float32"}],"paths":["face_expression_model-shard1"]}] \ No newline at end of file diff --git a/apps/web/models/tiny_face_detector_model/tiny_face_detector_model-weights_manifest.json b/apps/web/models/tiny_face_detector_model/tiny_face_detector_model-weights_manifest.json index f916e9a5..7d3b222d 100644 --- a/apps/web/models/tiny_face_detector_model/tiny_face_detector_model-weights_manifest.json +++ b/apps/web/models/tiny_face_detector_model/tiny_face_detector_model-weights_manifest.json @@ -1,197 +1 @@ -[ - { - "weights": [ - { - "name": "conv0/filters", - "shape": [3, 3, 3, 16], - "dtype": "float32", - "quantization": { - "dtype": "uint8", - "scale": 0.009007044399485869, - "min": -1.2069439495311063 - } - }, - { - "name": "conv0/bias", - "shape": [16], - "dtype": "float32", - "quantization": { - "dtype": "uint8", - "scale": 0.005263455241334205, - "min": -0.9211046672334858 - } - }, - { - "name": "conv1/depthwise_filter", - "shape": [3, 3, 16, 1], - "dtype": "float32", - "quantization": { - "dtype": "uint8", - "scale": 0.004001977630690033, - "min": -0.5042491814669441 - } - }, - { - "name": "conv1/pointwise_filter", - "shape": [1, 1, 16, 32], - "dtype": "float32", - "quantization": { - "dtype": "uint8", - "scale": 0.013836609615999109, - "min": -1.411334180831909 - } - }, - { - "name": "conv1/bias", - "shape": [32], - "dtype": "float32", - "quantization": { - "dtype": "uint8", - "scale": 0.0015159862590771096, - "min": -0.30926119685173037 - } - }, - { - "name": "conv2/depthwise_filter", - "shape": [3, 3, 32, 1], - "dtype": "float32", - "quantization": { - "dtype": "uint8", - "scale": 0.002666276225856706, - "min": -0.317286870876948 - } - }, - { - "name": "conv2/pointwise_filter", - "shape": [1, 1, 32, 64], - "dtype": "float32", - "quantization": { - "dtype": "uint8", - "scale": 0.015265831292844286, - "min": -1.6792414422128714 - } - }, - { - "name": "conv2/bias", - "shape": [64], - "dtype": "float32", - "quantization": { - "dtype": "uint8", - "scale": 0.0020280554598453, - "min": -0.37113414915168985 - } - }, - { - "name": "conv3/depthwise_filter", - "shape": [3, 3, 64, 1], - "dtype": "float32", - "quantization": { - "dtype": "uint8", - "scale": 0.006100742489683862, - "min": -0.8907084034938438 - } - }, - { - "name": "conv3/pointwise_filter", - "shape": [1, 1, 64, 128], - "dtype": "float32", - "quantization": { - "dtype": "uint8", - "scale": 0.016276211832083907, - "min": -2.0508026908425725 - } - }, - { - "name": "conv3/bias", - "shape": [128], - "dtype": "float32", - "quantization": { - "dtype": "uint8", - "scale": 0.003394414279975143, - "min": -0.7637432129944072 - } - }, - { - "name": "conv4/depthwise_filter", - "shape": [3, 3, 128, 1], - "dtype": "float32", - "quantization": { - "dtype": "uint8", - "scale": 0.006716050119961009, - "min": -0.8059260143953211 - } - }, - { - "name": "conv4/pointwise_filter", - "shape": [1, 1, 128, 256], - "dtype": "float32", - "quantization": { - "dtype": "uint8", - "scale": 0.021875603993733724, - "min": -2.8875797271728514 - } - }, - { - "name": "conv4/bias", - "shape": [256], - "dtype": "float32", - "quantization": { - "dtype": "uint8", - "scale": 0.0041141652009066415, - "min": -0.8187188749804216 - } - }, - { - "name": "conv5/depthwise_filter", - "shape": [3, 3, 256, 1], - "dtype": "float32", - "quantization": { - "dtype": "uint8", - "scale": 0.008423839597141042, - "min": -0.9013508368940915 - } - }, - { - "name": "conv5/pointwise_filter", - "shape": [1, 1, 256, 512], - "dtype": "float32", - "quantization": { - "dtype": "uint8", - "scale": 0.030007277283014035, - "min": -3.8709387695088107 - } - }, - { - "name": "conv5/bias", - "shape": [512], - "dtype": "float32", - "quantization": { - "dtype": "uint8", - "scale": 0.008402082966823203, - "min": -1.4871686851277068 - } - }, - { - "name": "conv8/filters", - "shape": [1, 1, 512, 25], - "dtype": "float32", - "quantization": { - "dtype": "uint8", - "scale": 0.028336129469030042, - "min": -4.675461362389957 - } - }, - { - "name": "conv8/bias", - "shape": [25], - "dtype": "float32", - "quantization": { - "dtype": "uint8", - "scale": 0.002268134028303857, - "min": -0.41053225912299807 - } - } - ], - "paths": ["tiny_face_detector_model-shard1"] - } -] +[{"weights":[{"name":"conv0/filters","shape":[3,3,3,16],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.009007044399485869,"min":-1.2069439495311063}},{"name":"conv0/bias","shape":[16],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.005263455241334205,"min":-0.9211046672334858}},{"name":"conv1/depthwise_filter","shape":[3,3,16,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.004001977630690033,"min":-0.5042491814669441}},{"name":"conv1/pointwise_filter","shape":[1,1,16,32],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.013836609615999109,"min":-1.411334180831909}},{"name":"conv1/bias","shape":[32],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0015159862590771096,"min":-0.30926119685173037}},{"name":"conv2/depthwise_filter","shape":[3,3,32,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.002666276225856706,"min":-0.317286870876948}},{"name":"conv2/pointwise_filter","shape":[1,1,32,64],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.015265831292844286,"min":-1.6792414422128714}},{"name":"conv2/bias","shape":[64],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0020280554598453,"min":-0.37113414915168985}},{"name":"conv3/depthwise_filter","shape":[3,3,64,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.006100742489683862,"min":-0.8907084034938438}},{"name":"conv3/pointwise_filter","shape":[1,1,64,128],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.016276211832083907,"min":-2.0508026908425725}},{"name":"conv3/bias","shape":[128],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.003394414279975143,"min":-0.7637432129944072}},{"name":"conv4/depthwise_filter","shape":[3,3,128,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.006716050119961009,"min":-0.8059260143953211}},{"name":"conv4/pointwise_filter","shape":[1,1,128,256],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.021875603993733724,"min":-2.8875797271728514}},{"name":"conv4/bias","shape":[256],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0041141652009066415,"min":-0.8187188749804216}},{"name":"conv5/depthwise_filter","shape":[3,3,256,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.008423839597141042,"min":-0.9013508368940915}},{"name":"conv5/pointwise_filter","shape":[1,1,256,512],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.030007277283014035,"min":-3.8709387695088107}},{"name":"conv5/bias","shape":[512],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.008402082966823203,"min":-1.4871686851277068}},{"name":"conv8/filters","shape":[1,1,512,25],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.028336129469030042,"min":-4.675461362389957}},{"name":"conv8/bias","shape":[25],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.002268134028303857,"min":-0.41053225912299807}}],"paths":["tiny_face_detector_model-shard1"]}] \ No newline at end of file diff --git a/apps/web/next.config.js b/apps/web/next.config.js index 9bd20ec1..9a96d44f 100644 --- a/apps/web/next.config.js +++ b/apps/web/next.config.js @@ -18,6 +18,10 @@ const nextConfig = { }, ]; }, + + experimental: { + useDeploymentId: false, + }, }; module.exports = nextConfig; diff --git a/apps/web/stores/assignmentFeedbackConfig.ts b/apps/web/stores/assignmentFeedbackConfig.ts index c18b850b..b2682c46 100644 --- a/apps/web/stores/assignmentFeedbackConfig.ts +++ b/apps/web/stores/assignmentFeedbackConfig.ts @@ -1,4 +1,8 @@ -import type { FeedbackData, VerbosityLevels } from "../config/types"; +import type { + FeedbackData, + VerbosityLevels, + CorrectAnswerVisibility, +} from "../config/types"; import { withUpdatedAt } from "./middlewares"; import { extractAssignmentId } from "@/lib/strings"; import { createJSONStorage, devtools, persist } from "zustand/middleware"; @@ -11,13 +15,14 @@ type FeedbackDataActions = { toggleShowQuestionScore: () => void; toggleShowAssignmentScore: () => void; toggleShowQuestions: () => void; - toggleShowCorrectAnswer: () => void; setShowSubmissionFeedback: (showSubmissionFeedback: boolean) => void; setShowQuestionScore: (showQuestionScore: boolean) => void; setShowQuestion: (showQuestions: boolean) => void; setShowAssignmentScore: (showAssignmentScore: boolean) => void; - setShowCorrectAnswer: (showCorrectAnswer: boolean) => void; + setCorrectAnswerVisibility: ( + correctAnswerVisibility: CorrectAnswerVisibility, + ) => void; setUpdatedAt: (updatedAt: number) => void; setAssignmentFeedbackConfigStore: (state: Partial) => void; @@ -35,27 +40,19 @@ export const useAssignmentFeedbackConfig = createWithEqualityFn< showQuestionScore: true, showAssignmentScore: true, showQuestions: true, - showCorrectAnswer: true, + correctAnswerVisibility: "ALWAYS", setShowQuestion: (showQuestions: boolean) => set({ showQuestions }), toggleShowQuestions: () => set((state) => ({ showQuestions: !state.showQuestions })), - toggleShowCorrectAnswer: () => - set((state) => { - const newValue = !state.showCorrectAnswer; - // Clear localStorage when showCorrectAnswer changes - if (typeof window !== "undefined") { - localStorage.removeItem("questions"); - localStorage.removeItem("assignmentConfig"); - } - return { showCorrectAnswer: newValue }; - }), - setShowCorrectAnswer: (showCorrectAnswer: boolean) => { - // Clear localStorage when showCorrectAnswer changes + setCorrectAnswerVisibility: ( + correctAnswerVisibility: CorrectAnswerVisibility, + ) => { + // Clear localStorage when correctAnswerVisibility changes if (typeof window !== "undefined") { localStorage.removeItem("questions"); localStorage.removeItem("assignmentConfig"); } - set({ showCorrectAnswer }); + set({ correctAnswerVisibility }); }, updatedAt: Date.now(), setVerbosityLevel: (verbosityLevel) => set({ verbosityLevel }), @@ -82,7 +79,8 @@ export const useAssignmentFeedbackConfig = createWithEqualityFn< showSubmissionFeedback: true, showQuestionScore: true, showAssignmentScore: true, - showCorrectAnswer: true, + showQuestions: true, + correctAnswerVisibility: "ALWAYS", updatedAt: Date.now(), })), })), diff --git a/package.json b/package.json index 7189be0d..5f09d957 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,9 @@ "build:docker": "docker buildx build -f ./apps/api-gateway/Dockerfile -t mark-api-gateway --no-cache --platform linux/amd64 . &> build-gateway.log && docker buildx build -f ./apps/web/Dockerfile -t mark-web --no-cache --platform linux/amd64 . &> build-web.log && docker buildx build -f ./apps/api/Dockerfile -t mark-api --no-cache --platform linux/amd64 . &> build-api.log", "db": "scripts/start-db.sh", "seed": "source dev.env && docker cp seed.sql mark-postgres:/tmp/seed.sql && docker exec -i mark-postgres pg_restore --username=$POSTGRES_USER --dbname=$POSTGRES_DB --no-owner --no-acl --clean --if-exists --no-password /tmp/seed.sql", - "dev": "dotenv -e dev.env -- turbo run dev --parallel", + "cleanup": "./scripts/cleanup-ports.sh", + "dev": "./scripts/cleanup-ports.sh && dotenv -e dev.env -- turbo run dev --parallel", + "dev:clean": "./scripts/cleanup-ports.sh && yarn dev", "format": "prettier --ignore-unknown --write .", "format:staged": "prettier --ignore-unknown --write \"**/*.{js,jsx,ts,tsx,json,css,scss,md,html}\"", "lint": "turbo run lint -- --fix", diff --git a/scripts/cleanup-ports.sh b/scripts/cleanup-ports.sh new file mode 100755 index 00000000..f4770787 --- /dev/null +++ b/scripts/cleanup-ports.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +echo "🧹 Cleaning up development ports..." + +PORTS=(3010 4222 8000) + +for port in "${PORTS[@]}"; do + echo "Checking port $port..." + + PIDS=$(lsof -ti:$port 2>/dev/null || true) + + if [ -n "$PIDS" ]; then + echo " Found processes on port $port: $PIDS" + echo " Terminating processes..." + echo "$PIDS" | xargs kill -TERM 2>/dev/null || true + + sleep 2 + + REMAINING=$(lsof -ti:$port 2>/dev/null || true) + if [ -n "$REMAINING" ]; then + echo " Force killing remaining processes..." + echo "$REMAINING" | xargs kill -9 2>/dev/null || true + fi + + echo " ✅ Port $port cleaned" + else + echo " ✅ Port $port is free" + fi +done + +echo "🎉 Port cleanup complete!" \ No newline at end of file diff --git a/turbo.json b/turbo.json index 3a6849e8..3d6b50ec 100644 --- a/turbo.json +++ b/turbo.json @@ -12,7 +12,8 @@ "seed:update": {}, "lint:staged": {}, "dev": { - "cache": false + "cache": false, + "persistent": true }, "test": { "outputs": ["coverage/**"], From 46d88da7bbe175b14f69ae48a108ce8a81bdfec8 Mon Sep 17 00:00:00 2001 From: MmagdyhafezZ Date: Tue, 7 Oct 2025 02:33:53 -0600 Subject: [PATCH 2/5] final touch --- .secrets.baseline | 4 +- apps/api-gateway/src/main.ts | 54 +-- apps/api/prisma/schema.prisma | 8 +- .../services/attempt-submission.service.ts | 321 +++++++++--------- .../src/test/migration-verification.test.ts | 260 -------------- apps/web/lib/api-health.ts | 39 ++- 6 files changed, 224 insertions(+), 462 deletions(-) delete mode 100644 apps/api/src/test/migration-verification.test.ts diff --git a/.secrets.baseline b/.secrets.baseline index 59dfb9f4..bfd98716 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -3,7 +3,7 @@ "files": null, "lines": null }, - "generated_at": "2025-09-21T22:56:11Z", + "generated_at": "2025-10-07T08:33:49Z", "plugins_used": [ { "name": "AWSKeyDetector" @@ -346,7 +346,7 @@ } ] }, - "version": "0.13.1+ibm.62.dss", + "version": "0.13.1+ibm.64.dss", "word_list": { "file": null, "hash": null diff --git a/apps/api-gateway/src/main.ts b/apps/api-gateway/src/main.ts index 347de199..6fdfb62b 100644 --- a/apps/api-gateway/src/main.ts +++ b/apps/api-gateway/src/main.ts @@ -16,40 +16,42 @@ async function bootstrap() { cors: false, logger, }); - app.use(json({ limit: "1000mb" })); - app.use(urlencoded({ limit: "1000mb", extended: true })); - app.setGlobalPrefix("api", { - exclude: ["health", "health/liveness", "health/readiness"], - }); - - app.enableVersioning({ - type: VersioningType.URI, - }); + app.use(json({ limit: "1000mb" })); + app.use(urlencoded({ limit: "1000mb", extended: true })); + app.setGlobalPrefix("api", { + exclude: ["health", "health/liveness", "health/readiness"], + }); - app.use(helmet()); + app.enableVersioning({ + type: VersioningType.URI, + }); - app.use(cookieParser()); + app.use(helmet()); - const config = new DocumentBuilder() - .setTitle("API") - .setDescription("API Description") - .build(); - const document = SwaggerModule.createDocument(app, config); - SwaggerModule.setup("api", app, document, { - customSiteTitle: "API Docs", - customCss: ".swagger-ui .topbar .topbar-wrapper { display: none; }", - }); + app.use(cookieParser()); - app.enableShutdownHooks(); + const config = new DocumentBuilder() + .setTitle("API") + .setDescription("API Description") + .build(); + const document = SwaggerModule.createDocument(app, config); + SwaggerModule.setup("api", app, document, { + customSiteTitle: "API Docs", + customCss: ".swagger-ui .topbar .topbar-wrapper { display: none; }", + }); - const port = process.env.API_GATEWAY_PORT ?? 3000; - await app.listen(port, '0.0.0.0'); + app.enableShutdownHooks(); - logger.log(`🚀 API Gateway is running on port ${port}`); - logger.log(`📚 API Documentation available at http://localhost:${port}/api`); + const port = process.env.API_GATEWAY_PORT ?? 3000; + await app.listen(port, "0.0.0.0"); + logger.log(`🚀 API Gateway is running on port ${port}`); + logger.log( + `📚 API Documentation available at http://localhost:${port}/api`, + ); } catch (error) { - logger.error('❌ Failed to start API Gateway:', error); + logger.error("❌ Failed to start API Gateway:", error); + // eslint-disable-next-line unicorn/no-process-exit process.exit(1); } } diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index dce89a46..f20ffb85 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -6,7 +6,6 @@ generator client { binaryTargets = ["native", "linux-musl-openssl-3.0.x"] } -// prisma/schema.prisma datasource db { provider = "postgresql" url = env("DATABASE_URL") @@ -90,7 +89,7 @@ model Job { progress String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - result Json? // Add this field to store generated questions + result Json? } model GradingJob { id Int @id @default(autoincrement()) @@ -122,7 +121,7 @@ model publishJob { percentage Int? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - result Json? // Add this field to store generated questions + result Json? } /// This model tracks the AI usage for each assignment and usage type model AIUsage { @@ -196,7 +195,6 @@ enum ChatRole { SYSTEM } -/// The Group model represents a particular group of assignments (having the same groupID) model Group { id String @unique /// Unique ID of the course assignments AssignmentGroup[] /// The list of assignments associated with this group @@ -212,7 +210,6 @@ model AssignmentGroup { @@id([assignmentId, groupId]) /// Unique identifier for the relationship } -/// The Assignment model represents a particular assignment model Assignment { id Int @id @default(autoincrement()) /// Unique identifier for the assignment name String /// Name of the assignment @@ -528,7 +525,6 @@ model Report { similarityScore Float? // Similarity score (0-1) with the parent report } -// Add to schema.prisma model UserNotification { id Int @id @default(autoincrement()) userId String diff --git a/apps/api/src/api/attempt/services/attempt-submission.service.ts b/apps/api/src/api/attempt/services/attempt-submission.service.ts index 88e66b3d..91301432 100644 --- a/apps/api/src/api/attempt/services/attempt-submission.service.ts +++ b/apps/api/src/api/attempt/services/attempt-submission.service.ts @@ -65,18 +65,18 @@ export class AttemptSubmissionService { private readonly questionResponseService: QuestionResponseService, private readonly translationService: TranslationService, private readonly questionVariantService: QuestionVariantService, - private readonly httpService: HttpService + private readonly httpService: HttpService, ) {} /** * Creates a new assignment attempt */ async createAssignmentAttempt( assignmentId: number, - userSession: UserSession + userSession: UserSession, ): Promise { const assignment = await this.assignmentRepository.findById( assignmentId, - userSession + userSession, ); await this.validationService.validateNewAttempt(assignment, userSession); @@ -103,12 +103,12 @@ export class AttemptSubmissionService { }, }, }, - } + }, ); if (!assignmentWithActiveVersion) { throw new NotFoundException( - `Assignment with Id ${assignmentId} not found.` + `Assignment with Id ${assignmentId} not found.`, ); } @@ -149,7 +149,7 @@ export class AttemptSubmissionService { videoPresentationConfig: qv.videoPresentationConfig as unknown as VideoPresentationConfig, liveRecordingConfig: qv.liveRecordingConfig as object, - }) + }), ) : ((assignmentWithActiveVersion?.questions || []).map((q) => ({ ...q, @@ -174,11 +174,11 @@ export class AttemptSubmissionService { const shuffledQuestions = questions.sort(() => Math.random() - 0.5); const selectedQuestions = shuffledQuestions.slice( 0, - assignment.numberOfQuestionsPerAttempt + assignment.numberOfQuestionsPerAttempt, ); if (selectedQuestions.length < assignment.numberOfQuestionsPerAttempt) { throw new NotFoundException( - `Not enough questions available for the assignment with Id ${assignmentId}.` + `Not enough questions available for the assignment with Id ${assignmentId}.`, ); } questions.length = 0; // clear the original questions array @@ -218,16 +218,16 @@ export class AttemptSubmissionService { typeof q.randomizedChoices === "boolean" ? q.randomizedChoices : typeof q.randomizedChoices === "string" - ? q.randomizedChoices === "true" - : false, + ? q.randomizedChoices === "true" + : false, videoPresentationConfig: this.parseJsonValue( q.videoPresentationConfig, - null + null, ), liveRecordingConfig: this.parseJsonValue | null>( q.liveRecordingConfig, - null + null, ), })); @@ -242,7 +242,7 @@ export class AttemptSubmissionService { await this.questionVariantService.createAttemptQuestionVariants( assignmentAttempt.id, - orderedQuestions + orderedQuestions, ); return { @@ -261,7 +261,7 @@ export class AttemptSubmissionService { authCookie: string, gradingCallbackRequired: boolean, request: UserSessionRequest, - progressCallback?: (progress: string, percentage?: number) => Promise + progressCallback?: (progress: string, percentage?: number) => Promise, ): Promise { const { role } = request.userSession; if (role === UserRole.LEARNER) { @@ -272,17 +272,17 @@ export class AttemptSubmissionService { authCookie, gradingCallbackRequired, request, - progressCallback + progressCallback, ); } else if (role === UserRole.AUTHOR) { return this.updateAuthorAttempt( assignmentId, updateDto, - progressCallback + progressCallback, ); } else { throw new NotFoundException( - `User with role ${role} cannot update assignment attempts.` + `User with role ${role} cannot update assignment attempts.`, ); } } @@ -290,7 +290,7 @@ export class AttemptSubmissionService { * Gets a learner assignment attempt with all details needed for display */ async getLearnerAssignmentAttempt( - attemptId: number + attemptId: number, ): Promise { const assignmentAttempt = await this.prisma.assignmentAttempt.findUnique({ where: { id: attemptId }, @@ -309,36 +309,10 @@ export class AttemptSubmissionService { if (!assignmentAttempt) { throw new NotFoundException( - `AssignmentAttempt with Id ${attemptId} not found.` + `AssignmentAttempt with Id ${attemptId} not found.`, ); } - // Use version-specific questions if available, otherwise fallback to legacy questions - const questions: unknown[] = - assignmentAttempt.assignmentVersionId && - assignmentAttempt.assignmentVersion?.questionVersions?.length > 0 - ? assignmentAttempt.assignmentVersion.questionVersions.map((qv) => ({ - id: qv.questionId || qv.id, - question: qv.question, - type: qv.type, - assignmentId: assignmentAttempt.assignmentId, - totalPoints: qv.totalPoints, - maxWords: qv.maxWords, - maxCharacters: qv.maxCharacters, - choices: qv.choices, - scoring: qv.scoring, - answer: qv.answer, - gradingContextQuestionIds: qv.gradingContextQuestionIds, - responseType: qv.responseType, - isDeleted: false, - randomizedChoices: qv.randomizedChoices, - videoPresentationConfig: qv.videoPresentationConfig, - liveRecordingConfig: qv.liveRecordingConfig, - })) - : await this.prisma.question.findMany({ - where: { assignmentId: assignmentAttempt.assignmentId }, - }); - const assignment = await this.prisma.assignment.findUnique({ where: { id: assignmentAttempt.assignmentId }, select: { @@ -360,10 +334,41 @@ export class AttemptSubmissionService { if (!assignment) { throw new NotFoundException( - `Assignment with Id ${assignmentAttempt.assignmentId} not found.` + `Assignment with Id ${assignmentAttempt.assignmentId} not found.`, ); } + const shouldShowCorrectAnswers = this.shouldShowCorrectAnswers( + assignment.currentVersion?.correctAnswerVisibility || "NEVER", + assignmentAttempt.grade || 0, + assignment.passingGrade, + ); + + const questions: unknown[] = + assignmentAttempt.assignmentVersionId && + assignmentAttempt.assignmentVersion?.questionVersions?.length > 0 + ? assignmentAttempt.assignmentVersion.questionVersions.map((qv) => ({ + id: qv.questionId || qv.id, + question: qv.question, + type: qv.type, + assignmentId: assignmentAttempt.assignmentId, + totalPoints: qv.totalPoints, + maxWords: qv.maxWords, + maxCharacters: qv.maxCharacters, + choices: qv.choices, + scoring: qv.scoring, + answer: shouldShowCorrectAnswers ? qv.answer : undefined, + gradingContextQuestionIds: qv.gradingContextQuestionIds, + responseType: qv.responseType, + isDeleted: false, + randomizedChoices: qv.randomizedChoices, + videoPresentationConfig: qv.videoPresentationConfig, + liveRecordingConfig: qv.liveRecordingConfig, + })) + : await this.prisma.question.findMany({ + where: { assignmentId: assignmentAttempt.assignmentId }, + }); + const questionDtos: EnhancedAttemptQuestionDto[] = questions.map((q) => { const question = q as Record; @@ -371,8 +376,8 @@ export class AttemptSubmissionService { typeof question.answer === "boolean" ? String(question.answer) : question.answer !== null && question.answer !== undefined - ? String(question.answer) - : undefined; + ? String(question.answer) + : undefined; const randomizedChoicesValue: string = typeof question.randomizedChoices === "string" @@ -393,7 +398,7 @@ export class AttemptSubmissionService { showRubricsToLearner: false, rubrics: [], }), - answer: answerValue, + answer: shouldShowCorrectAnswers ? answerValue : undefined, gradingContextQuestionIds: (question.gradingContextQuestionIds as number[]) || [], responseType: (question.responseType as ResponseType) || undefined, @@ -402,7 +407,7 @@ export class AttemptSubmissionService { videoPresentationConfig: this.parseJsonValue( question.videoPresentationConfig, - null + null, ), liveRecordingConfig: this.parseJsonValue { - const question = q as Record; - - const answerValue = - typeof question.answer === "boolean" - ? String(question.answer) - : question.answer !== null && question.answer !== undefined - ? String(question.answer) - : undefined; - - const randomizedChoicesValue: string = - typeof question.randomizedChoices === "string" - ? question.randomizedChoices - : JSON.stringify(question.randomizedChoices ?? false); - - return { - id: question.id as number, - question: question.question as string, - type: question.type as QuestionType, - assignmentId: question.assignmentId as number, - totalPoints: question.totalPoints as number, - maxWords: (question.maxWords as number) || undefined, - maxCharacters: (question.maxCharacters as number) || undefined, - choices: this.parseJsonValue(question.choices, []), - scoring: this.parseJsonValue(question.scoring, { - type: ScoringType.CRITERIA_BASED, - showRubricsToLearner: false, - rubrics: [], - }), - answer: answerValue, - gradingContextQuestionIds: - (question.gradingContextQuestionIds as number[]) || [], - responseType: (question.responseType as ResponseType) || undefined, - isDeleted: question.isDeleted as boolean, - randomizedChoices: randomizedChoicesValue, - videoPresentationConfig: - this.parseJsonValue( - question.videoPresentationConfig, - null - ), - liveRecordingConfig: this.parseJsonValue | null>(question.liveRecordingConfig, null), - }; - }); + const allQuestionDtos: EnhancedAttemptQuestionDto[] = allQuestions.map( + (q) => { + const question = q as Record; + + const answerValue = + typeof question.answer === "boolean" + ? String(question.answer) + : question.answer !== null && question.answer !== undefined + ? String(question.answer) + : undefined; + + const randomizedChoicesValue: string = + typeof question.randomizedChoices === "string" + ? question.randomizedChoices + : JSON.stringify(question.randomizedChoices ?? false); + + return { + id: question.id as number, + question: question.question as string, + type: question.type as QuestionType, + assignmentId: question.assignmentId as number, + totalPoints: question.totalPoints as number, + maxWords: (question.maxWords as number) || undefined, + maxCharacters: (question.maxCharacters as number) || undefined, + choices: this.parseJsonValue(question.choices, []), + scoring: this.parseJsonValue(question.scoring, { + type: ScoringType.CRITERIA_BASED, + showRubricsToLearner: false, + rubrics: [], + }), + answer: shouldShowCorrectAnswers ? answerValue : undefined, + gradingContextQuestionIds: + (question.gradingContextQuestionIds as number[]) || [], + responseType: (question.responseType as ResponseType) || undefined, + isDeleted: question.isDeleted as boolean, + randomizedChoices: randomizedChoicesValue, + videoPresentationConfig: + this.parseJsonValue( + question.videoPresentationConfig, + null, + ), + liveRecordingConfig: this.parseJsonValue | null>(question.liveRecordingConfig, null), + }; + }, + ); questionsToShow = allQuestionDtos; } @@ -535,10 +537,10 @@ export class AttemptSubmissionService { id: assignmentAttempt.assignmentId, ...assignment, // Override questionOrder to include all questions if we should show all - questionOrder: shouldShowAllQuestions ? questionsToShow.map(q => q.id) : assignment.questionOrder, + questionOrder: questionsToShow.map((q) => q.id), }, this.prisma, - assignmentAttempt.preferredLanguage || undefined + assignmentAttempt.preferredLanguage || undefined, ); this.applyVisibilitySettings(finalQuestions, assignmentAttempt, assignment); @@ -551,7 +553,8 @@ export class AttemptSubmissionService { showSubmissionFeedback: assignment.showSubmissionFeedback, showQuestions: assignment.showQuestions, showQuestionScore: assignment.showQuestionScore, - correctAnswerVisibility: assignment.currentVersion?.correctAnswerVisibility || "NEVER", + correctAnswerVisibility: + assignment.currentVersion?.correctAnswerVisibility || "NEVER", comments: assignmentAttempt.comments, }; } @@ -561,7 +564,7 @@ export class AttemptSubmissionService { */ async getAssignmentAttempt( attemptId: number, - language?: string + language?: string, ): Promise { const normalizedLanguage = this.getNormalizedLanguage(language); @@ -588,7 +591,7 @@ export class AttemptSubmissionService { if (!assignmentAttempt) { throw new NotFoundException( - `AssignmentAttempt with Id ${attemptId} not found.` + `AssignmentAttempt with Id ${attemptId} not found.`, ); } @@ -652,7 +655,7 @@ export class AttemptSubmissionService { const translations = await this.translationService.getTranslationsForAttempt( assignmentAttempt, - questionsForTranslation + questionsForTranslation, ); const formattedAttempt: AssignmentAttemptWithRelations = { @@ -686,13 +689,15 @@ export class AttemptSubmissionService { const shouldShowAllQuestions = this.shouldShowCorrectAnswers( assignment.currentVersion?.correctAnswerVisibility || "NEVER", assignmentAttempt.grade || 0, - assignment.passingGrade + assignment.passingGrade, ); // If we should show all questions, override the assignment questionOrder const assignmentForTranslation = { ...assignment, - questionOrder: shouldShowAllQuestions ? questionsForTranslation.map(q => q.id) : assignment.questionOrder, + questionOrder: shouldShowAllQuestions + ? questionsForTranslation.map((q) => q.id) + : assignment.questionOrder, questions: questionsForTranslation, } as unknown as UpdateAssignmentQuestionsDto; @@ -701,14 +706,17 @@ export class AttemptSubmissionService { formattedAttempt, assignmentForTranslation, translations, - normalizedLanguage + normalizedLanguage, ); this.removeSensitiveData( finalQuestions, - { correctAnswerVisibility: assignment.currentVersion?.correctAnswerVisibility || "NEVER" }, + { + correctAnswerVisibility: + assignment.currentVersion?.correctAnswerVisibility || "NEVER", + }, assignmentAttempt.grade || 0, - assignment.passingGrade + assignment.passingGrade, ); return { @@ -719,7 +727,8 @@ export class AttemptSubmissionService { showSubmissionFeedback: assignment.showSubmissionFeedback, showQuestionScore: assignment.showQuestionScore, showQuestions: assignment.showQuestions, - correctAnswerVisibility: assignment.currentVersion?.correctAnswerVisibility || "NEVER", + correctAnswerVisibility: + assignment.currentVersion?.correctAnswerVisibility || "NEVER", }; } @@ -733,7 +742,7 @@ export class AttemptSubmissionService { authCookie: string, gradingCallbackRequired: boolean, request: UserSessionRequest, - progressCallback?: (progress: string, percentage?: number) => Promise + progressCallback?: (progress: string, percentage?: number) => Promise, ): Promise { try { // Report initial progress @@ -755,7 +764,7 @@ export class AttemptSubmissionService { if (!assignmentAttempt) { throw new NotFoundException( - `AssignmentAttempt with Id ${attemptId} not found.` + `AssignmentAttempt with Id ${attemptId} not found.`, ); } @@ -774,7 +783,7 @@ export class AttemptSubmissionService { await this.translationService.preTranslateQuestions( updateDto.responsesForQuestions, assignmentAttempt, - updateDto.language + updateDto.language, ); updateDto.preTranslatedQuestions = preTranslatedQuestions; @@ -806,7 +815,7 @@ export class AttemptSubmissionService { updateDto.language, updateDto.authorQuestions, updateDto.authorAssignmentDetails, - updateDto.preTranslatedQuestions + updateDto.preTranslatedQuestions, ); if (progressCallback) { @@ -816,7 +825,7 @@ export class AttemptSubmissionService { let totalPossiblePoints = 0; for (const response of successfulQuestionResponses) { const question = assignment.questions.find( - (q) => q.id === response.questionId + (q) => q.id === response.questionId, ); totalPossiblePoints += question?.totalPoints || 0; } @@ -824,7 +833,7 @@ export class AttemptSubmissionService { const { grade, totalPointsEarned } = this.gradingService.calculateGradeForLearner( successfulQuestionResponses, - totalPossiblePoints + totalPossiblePoints, ); if (gradingCallbackRequired) { @@ -835,7 +844,7 @@ export class AttemptSubmissionService { grade, authCookie, assignmentId, - request.userSession.userId + request.userSession.userId, ); } @@ -846,7 +855,7 @@ export class AttemptSubmissionService { const result = await this.updateAssignmentAttemptInDb( attemptId, updateDto, - grade + grade, ); if (progressCallback) { @@ -862,11 +871,12 @@ export class AttemptSubmissionService { grade: assignment.showAssignmentScore ? result.grade : undefined, showQuestions: assignment.showQuestions, showSubmissionFeedback: assignment.showSubmissionFeedback, - correctAnswerVisibility: assignment.currentVersion?.correctAnswerVisibility || "NEVER", + correctAnswerVisibility: + assignment.currentVersion?.correctAnswerVisibility || "NEVER", feedbacksForQuestions: this.gradingService.constructFeedbacksForQuestions( successfulQuestionResponses, - assignment + assignment, ), }; } catch (error) { @@ -885,7 +895,7 @@ export class AttemptSubmissionService { private async updateAuthorAttempt( assignmentId: number, updateDto: LearnerUpdateAssignmentAttemptRequestDto, - progressCallback?: (progress: string, percentage?: number) => Promise + progressCallback?: (progress: string, percentage?: number) => Promise, ): Promise { try { if (progressCallback) { @@ -920,7 +930,7 @@ export class AttemptSubmissionService { assignmentId, updateDto.language, updateDto.authorQuestions, - updateDto.authorAssignmentDetails + updateDto.authorAssignmentDetails, ); if (progressCallback) { @@ -930,7 +940,7 @@ export class AttemptSubmissionService { let totalPossiblePoints = 0; for (const response of successfulQuestionResponses) { const question = assignment.questions.find( - (q) => q.id === response.questionId + (q) => q.id === response.questionId, ); totalPossiblePoints += question?.totalPoints || 0; } @@ -938,7 +948,7 @@ export class AttemptSubmissionService { const { grade, totalPointsEarned } = this.gradingService.calculateGradeForAuthor( successfulQuestionResponses, - totalPossiblePoints + totalPossiblePoints, ); if (progressCallback) { @@ -954,11 +964,12 @@ export class AttemptSubmissionService { grade: assignment.showAssignmentScore ? grade : undefined, showQuestions: assignment.showQuestions, showSubmissionFeedback: assignment.showSubmissionFeedback, - correctAnswerVisibility: assignment.currentVersion?.correctAnswerVisibility || "NEVER", + correctAnswerVisibility: + assignment.currentVersion?.correctAnswerVisibility || "NEVER", feedbacksForQuestions: this.gradingService.constructFeedbacksForQuestions( successfulQuestionResponses, - assignment + assignment, ), }; } catch (error) { @@ -975,7 +986,7 @@ export class AttemptSubmissionService { * Handle an expired attempt */ private async handleExpiredAttempt( - attemptId: number + attemptId: number, ): Promise { await this.prisma.assignmentAttempt.update({ where: { id: attemptId }, @@ -1009,7 +1020,7 @@ export class AttemptSubmissionService { grade: number, authCookie: string, assignmentId: number, - userId: string + userId: string, ): Promise { const userAttempts = await this.prisma.assignmentAttempt.findMany({ where: { @@ -1041,7 +1052,7 @@ export class AttemptSubmissionService { private async updateAssignmentAttemptInDb( attemptId: number, updateDto: LearnerUpdateAssignmentAttemptRequestDto, - grade: number + grade: number, ) { const { responsesForQuestions, @@ -1068,7 +1079,7 @@ export class AttemptSubmissionService { */ private async sendGradeToLtiGateway( grade: number, - authCookie: string + authCookie: string, ): Promise { try { const ltiGatewayResponse = await this.httpService @@ -1079,7 +1090,7 @@ export class AttemptSubmissionService { headers: { Cookie: `authentication=${authCookie}`, }, - } + }, ) .toPromise(); @@ -1092,7 +1103,7 @@ export class AttemptSubmissionService { ? error.message : "An unknown error occurred while sending the grade to the LTI gateway."; throw new InternalServerErrorException( - `${GRADE_SUBMISSION_EXCEPTION}: ${errorMessage}` + `${GRADE_SUBMISSION_EXCEPTION}: ${errorMessage}`, ); } } @@ -1101,7 +1112,7 @@ export class AttemptSubmissionService { * Calculate the expiration date for an attempt */ private calculateAttemptExpiresAt( - assignment: GetAssignmentResponseDto | LearnerGetAssignmentResponseDto + assignment: GetAssignmentResponseDto | LearnerGetAssignmentResponseDto, ): Date | null { if ( assignment.allotedTimeMinutes !== undefined && @@ -1117,7 +1128,7 @@ export class AttemptSubmissionService { */ private getOrderedQuestions( questions: QuestionDto[], - assignment: GetAssignmentResponseDto | LearnerGetAssignmentResponseDto + assignment: GetAssignmentResponseDto | LearnerGetAssignmentResponseDto, ): QuestionDto[] { const orderedQuestions = [...questions]; @@ -1130,7 +1141,7 @@ export class AttemptSubmissionService { orderedQuestions.sort( (a, b) => assignment.questionOrder.indexOf(a.id) - - assignment.questionOrder.indexOf(b.id) + assignment.questionOrder.indexOf(b.id), ); } @@ -1168,16 +1179,16 @@ export class AttemptSubmissionService { typeof q.randomizedChoices === "boolean" ? q.randomizedChoices : typeof q.randomizedChoices === "string" - ? q.randomizedChoices === "true" - : false, + ? q.randomizedChoices === "true" + : false, videoPresentationConfig: this.parseJsonValue( q.videoPresentationConfig, - null + null, ), liveRecordingConfig: this.parseJsonValue | null>( q.liveRecordingConfig, - null + null, ), })); } @@ -1208,7 +1219,7 @@ export class AttemptSubmissionService { showQuestionScore?: boolean; showQuestions?: boolean; showCorrectAnswer?: boolean; - } + }, ): void { if (assignment.showAssignmentScore === false) { assignmentAttempt.grade = null; @@ -1245,7 +1256,7 @@ export class AttemptSubmissionService { questions: AttemptQuestionDto[], assignment: { correctAnswerVisibility: CorrectAnswerVisibility }, grade: number, - passingGrade: number + passingGrade: number, ): void { for (const question of questions) { if (!question.scoring?.showRubricsToLearner) { @@ -1259,7 +1270,7 @@ export class AttemptSubmissionService { !this.shouldShowCorrectAnswers( assignment.correctAnswerVisibility || "NEVER", grade, - passingGrade + passingGrade, ) ) { delete choice.isCorrect; @@ -1278,7 +1289,7 @@ export class AttemptSubmissionService { !this.shouldShowCorrectAnswers( assignment.correctAnswerVisibility, grade, - passingGrade + passingGrade, ) ) { delete choice.isCorrect; @@ -1294,7 +1305,7 @@ export class AttemptSubmissionService { typeof question.randomizedChoices === "string" ) { const randomizedArray = JSON.parse( - question.randomizedChoices + question.randomizedChoices, ) as Array<{ points?: number; isCorrect?: boolean; @@ -1308,7 +1319,7 @@ export class AttemptSubmissionService { !this.shouldShowCorrectAnswers( assignment.correctAnswerVisibility || "NEVER", grade, - passingGrade + passingGrade, ) ) { delete choice.isCorrect; @@ -1362,7 +1373,7 @@ export class AttemptSubmissionService { private shouldShowCorrectAnswers( correctAnswerVisibility: CorrectAnswerVisibility, grade: number, - passingGrade: number + passingGrade: number, ): boolean { switch (correctAnswerVisibility) { case "NEVER": { diff --git a/apps/api/src/test/migration-verification.test.ts b/apps/api/src/test/migration-verification.test.ts deleted file mode 100644 index aa57363f..00000000 --- a/apps/api/src/test/migration-verification.test.ts +++ /dev/null @@ -1,260 +0,0 @@ -import { PrismaClient } from '@prisma/client'; - -describe('Assignment Data Consolidation Migration Verification', () => { - let prisma: PrismaClient; - - beforeAll(async () => { - prisma = new PrismaClient(); - await prisma.$connect(); - }); - - afterAll(async () => { - await prisma.$disconnect(); - }); - - describe('Assignment Table Structure', () => { - it('should have simplified Assignment table with only essential fields', async () => { - // Check that Assignment table has the correct simplified structure - const columns = await prisma.$queryRaw>` - SELECT column_name, is_nullable, data_type - FROM information_schema.columns - WHERE table_name = 'Assignment' AND table_schema = 'public' - ORDER BY column_name; - `; - - const columnNames = columns.map(col => col.column_name); - - // Essential fields that should exist - expect(columnNames).toContain('id'); - expect(columnNames).toContain('createdAt'); - expect(columnNames).toContain('updatedAt'); - expect(columnNames).toContain('currentVersionId'); - - // Content fields that should NOT exist (moved to AssignmentVersion) - expect(columnNames).not.toContain('name'); - expect(columnNames).not.toContain('introduction'); - expect(columnNames).not.toContain('instructions'); - expect(columnNames).not.toContain('type'); - expect(columnNames).not.toContain('published'); - expect(columnNames).not.toContain('correctAnswerVisibility'); - }); - - it('should have currentVersionId as NOT NULL', async () => { - const currentVersionIdColumn = await prisma.$queryRaw>` - SELECT is_nullable - FROM information_schema.columns - WHERE table_name = 'Assignment' - AND column_name = 'currentVersionId' - AND table_schema = 'public'; - `; - - expect(currentVersionIdColumn[0]?.is_nullable).toBe('NO'); - }); - - it('should have foreign key constraint on currentVersionId', async () => { - const foreignKeys = await prisma.$queryRaw>` - SELECT constraint_name - FROM information_schema.table_constraints - WHERE table_name = 'Assignment' - AND constraint_type = 'FOREIGN KEY' - AND table_schema = 'public'; - `; - - const hasForeignKey = foreignKeys.some(fk => - fk.constraint_name.includes('currentVersionId') - ); - expect(hasForeignKey).toBe(true); - }); - }); - - describe('Data Integrity Verification', () => { - it('should ensure all assignments have a valid currentVersionId', async () => { - const assignmentsWithoutVersion = await prisma.assignment.count({ - where: { currentVersionId: null } - }); - - expect(assignmentsWithoutVersion).toBe(0); - }); - - it('should ensure all currentVersionId references point to existing AssignmentVersions', async () => { - const invalidReferences = await prisma.$queryRaw>` - SELECT COUNT(*) as count - FROM "Assignment" a - LEFT JOIN "AssignmentVersion" av ON a."currentVersionId" = av.id - WHERE av.id IS NULL; - `; - - expect(Number(invalidReferences[0]?.count)).toBe(0); - }); - - it('should ensure all assignments have at least one version', async () => { - const assignmentsWithoutVersions = await prisma.$queryRaw>` - SELECT COUNT(*) as count - FROM "Assignment" a - WHERE NOT EXISTS ( - SELECT 1 FROM "AssignmentVersion" av WHERE av."assignmentId" = a.id - ); - `; - - expect(Number(assignmentsWithoutVersions[0]?.count)).toBe(0); - }); - - it('should verify that currentVersion points to an active or most recent version', async () => { - // Get all assignments and verify their currentVersion is either active or most recent - const assignments = await prisma.assignment.findMany({ - include: { - currentVersion: true, - versions: { - orderBy: { createdAt: 'desc' } - } - } - }); - - for (const assignment of assignments) { - expect(assignment.currentVersion).toBeTruthy(); - - // Current version should either be active OR be the most recent version - const isActive = assignment.currentVersion!.isActive; - const isMostRecent = assignment.versions[0]?.id === assignment.currentVersion!.id; - - expect(isActive || isMostRecent).toBe(true); - } - }); - }); - - describe('Version Data Verification', () => { - it('should ensure assignments without prior versions got version 0.0.1', async () => { - // Check if there are any 0.0.1 versions (indicating migration created them) - const initialVersions = await prisma.assignmentVersion.findMany({ - where: { versionNumber: '0.0.1' } - }); - - // If there are 0.0.1 versions, verify they have proper data - if (initialVersions.length > 0) { - for (const version of initialVersions) { - expect(version.name).toBeTruthy(); - expect(version.isActive).toBe(true); - expect(version.isDraft).toBe(false); - expect(version.versionDescription).toContain('Initial version created from assignment data'); - } - } - }); - - it('should ensure question versions exist for 0.0.1 versions', async () => { - const initialVersions = await prisma.assignmentVersion.findMany({ - where: { versionNumber: '0.0.1' }, - include: { - questionVersions: true, - assignment: { - include: { - questions: { - where: { isDeleted: false } - } - } - } - } - }); - - for (const version of initialVersions) { - // If the assignment has questions, the version should have question versions - if (version.assignment.questions.length > 0) { - expect(version.questionVersions.length).toBeGreaterThan(0); - expect(version.questionVersions.length).toBe(version.assignment.questions.length); - } - } - }); - - it('should verify question versions have proper display order', async () => { - const questionVersions = await prisma.questionVersion.findMany({ - include: { - assignmentVersion: true - } - }); - - // Group by assignment version and check display order - const versionGroups = questionVersions.reduce((acc, qv) => { - const versionId = qv.assignmentVersionId; - if (!acc[versionId]) acc[versionId] = []; - acc[versionId].push(qv); - return acc; - }, {} as Record); - - for (const [versionId, questions] of Object.entries(versionGroups)) { - const sortedQuestions = questions.sort((a, b) => a.displayOrder - b.displayOrder); - - // Display order should start from 1 and be sequential - sortedQuestions.forEach((question, index) => { - expect(question.displayOrder).toBe(index + 1); - }); - } - }); - }); - - describe('Data Consistency Verification', () => { - it('should ensure no data was lost during migration', async () => { - // Count total assignments - const assignmentCount = await prisma.assignment.count(); - - // Count assignments with versions - const assignmentsWithVersions = await prisma.$queryRaw>` - SELECT COUNT(DISTINCT "assignmentId") as count - FROM "AssignmentVersion"; - `; - - expect(Number(assignmentsWithVersions[0]?.count)).toBe(assignmentCount); - }); - - it('should verify assignment metadata is preserved in versions', async () => { - const assignments = await prisma.assignment.findMany({ - include: { - currentVersion: true - } - }); - - for (const assignment of assignments) { - const version = assignment.currentVersion!; - - // Basic checks that version has content - expect(version.name).toBeTruthy(); - expect(typeof version.published).toBe('boolean'); - expect(typeof version.graded).toBe('boolean'); - expect(version.correctAnswerVisibility).toMatch(/^(NEVER|ALWAYS|ON_PASS)$/); - } - }); - - it('should ensure foreign key relationships are intact', async () => { - // Test that we can successfully join Assignment -> AssignmentVersion -> QuestionVersion - const testJoin = await prisma.assignment.findMany({ - include: { - currentVersion: { - include: { - questionVersions: true - } - } - }, - take: 5 - }); - - expect(testJoin).toBeTruthy(); - // If we get here without errors, the foreign key relationships are working - }); - }); - - describe('Performance and Index Verification', () => { - it('should ensure currentVersionId has proper indexing', async () => { - const indexes = await prisma.$queryRaw>` - SELECT indexname - FROM pg_indexes - WHERE tablename = 'Assignment' - AND schemaname = 'public'; - `; - - const hasCurrentVersionIndex = indexes.some(idx => - idx.indexname.includes('currentVersionId') - ); - - // This might not exist if not explicitly created, but foreign key creates one - expect(indexes.length).toBeGreaterThan(0); - }); - }); -}); \ No newline at end of file diff --git a/apps/web/lib/api-health.ts b/apps/web/lib/api-health.ts index db147c5a..12e9fbe3 100644 --- a/apps/web/lib/api-health.ts +++ b/apps/web/lib/api-health.ts @@ -2,7 +2,8 @@ * API Health Check and Connection Utilities */ -const API_GATEWAY_HOST = process.env.API_GATEWAY_HOST || 'http://localhost:8000'; +const API_GATEWAY_HOST = + process.env.API_GATEWAY_HOST || "http://localhost:8000"; /** * Check if API Gateway is healthy @@ -10,12 +11,12 @@ const API_GATEWAY_HOST = process.env.API_GATEWAY_HOST || 'http://localhost:8000' export async function checkApiHealth(): Promise { try { const response = await fetch(`${API_GATEWAY_HOST}/health`, { - method: 'GET', + method: "GET", signal: AbortSignal.timeout(5000), // 5 second timeout }); return response.ok; } catch (error) { - console.warn('API health check failed:', error); + console.warn("API health check failed:", error); return false; } } @@ -31,20 +32,26 @@ export async function waitForApiReady(maxRetries = 30): Promise { const isHealthy = await checkApiHealth(); if (isHealthy) { - console.log('✅ API Gateway is ready'); + console.log("✅ API Gateway is ready"); return true; } retries++; - console.log(`⏳ API Gateway not ready, retry ${retries}/${maxRetries} in ${delay}ms`); + console.log( + `⏳ API Gateway not ready, retry ${retries}/${maxRetries} in ${delay}ms`, + ); - await new Promise(resolve => setTimeout(resolve, delay)); + await new Promise((resolve) => setTimeout(resolve, delay)); // Exponential backoff with max 5 seconds delay = Math.min(delay * 1.2, 5000); } - console.error('❌ API Gateway failed to become ready after', maxRetries, 'retries'); + console.error( + "❌ API Gateway failed to become ready after", + maxRetries, + "retries", + ); return false; } @@ -54,7 +61,7 @@ export async function waitForApiReady(maxRetries = 30): Promise { export async function apiRequestWithRetry( url: string, options: RequestInit = {}, - maxRetries = 3 + maxRetries = 3, ): Promise { let lastError: Error; @@ -75,12 +82,18 @@ export async function apiRequestWithRetry( lastError = error as Error; // Don't retry on certain errors - if (error instanceof TypeError && error.message.includes('Failed to fetch')) { - console.warn(`API request failed on attempt ${attempt}:`, error.message); + if ( + error instanceof TypeError && + error.message.includes("Failed to fetch") + ) { + console.warn( + `API request failed on attempt ${attempt}:`, + error.message, + ); if (attempt < maxRetries) { const delay = attempt * 1000; // Linear backoff - await new Promise(resolve => setTimeout(resolve, delay)); + await new Promise((resolve) => setTimeout(resolve, delay)); continue; } } @@ -90,5 +103,5 @@ export async function apiRequestWithRetry( } } - throw lastError!; -} \ No newline at end of file + throw lastError; +} From 3b4cc37b957f383ac816b4c4bb495766b6723b82 Mon Sep 17 00:00:00 2001 From: MmagdyhafezZ Date: Thu, 9 Oct 2025 05:09:26 -0600 Subject: [PATCH 3/5] adjusting schema.prisma back --- apps/api/prisma/schema.prisma | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index 32fe0be3..fab58ec1 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -235,8 +235,8 @@ model Assignment { showAssignmentScore Boolean @default(true) /// Should the assignment score be shown to the learner after its submission showQuestionScore Boolean @default(true) /// Should the question score be shown to the learner after its submission showSubmissionFeedback Boolean @default(true) /// Should the AI provide feedback when the learner submits a question - showQuestions Boolean @default(true) /// Should the questions be shown to the learner - showCorrectAnswer Boolean @default(true) /// Should the correct answer be shown to the learner after its submission + showQuestions Boolean @default(true) /// Should the questions be shown to the learner + correctAnswerVisibility CorrectAnswerVisibility @default(ALWAYS) /// When should correct answers be shown to learners updatedAt DateTime @default(now()) @updatedAt /// The DateTime at which the assignment was last updated languageCode String? /// The language code for the assignment currentVersionId Int? /// The ID of the current active version From 1a73834476a84cc7bce5db34587a62f95f12965f Mon Sep 17 00:00:00 2001 From: MmagdyhafezZ Date: Thu, 9 Oct 2025 07:24:17 -0600 Subject: [PATCH 4/5] changes and adjustments --- .secrets.baseline | 8 +- .../v2/repositories/assignment.repository.ts | 19 ++- .../utils/attempt-questions-mapper.util.ts | 2 - apps/web/app/Helpers/checkDiff.ts | 24 ++-- .../AuthorQuestionsPage/index.tsx | 21 ++- .../app/author/[assignmentId]/review/page.tsx | 136 ++++++++++-------- .../web/app/chatbot/store/useAuthorContext.ts | 2 +- apps/web/app/learner/(components)/Header.tsx | 6 +- .../[assignmentId]/successPage/Question.tsx | 102 ++++++++----- .../successPage/[submissionId]/page.tsx | 1 + .../version-control/VersionTreeView.tsx | 23 +++ apps/web/hooks/useUserBehaviorMonitor.ts | 2 +- apps/web/stores/author.ts | 7 + 13 files changed, 227 insertions(+), 126 deletions(-) diff --git a/.secrets.baseline b/.secrets.baseline index 49373ff5..0063518f 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -3,7 +3,7 @@ "files": null, "lines": null }, - "generated_at": "2025-10-09T10:36:06Z", + "generated_at": "2025-10-09T13:24:14Z", "plugins_used": [ { "name": "AWSKeyDetector" @@ -283,21 +283,21 @@ { "hashed_secret": "e1166e6dd837019ab04f130ab34c425e04161645", "is_verified": false, - "line_number": 382, + "line_number": 384, "type": "Secret Keyword", "verified_result": null }, { "hashed_secret": "f0c5bc5473fd2f959bdac630e625aa33346fd12a", "is_verified": false, - "line_number": 429, + "line_number": 431, "type": "Secret Keyword", "verified_result": null }, { "hashed_secret": "29080f1c58f9859ddaa6aeda7d2c410c12e222dc", "is_verified": false, - "line_number": 461, + "line_number": 463, "type": "Secret Keyword", "verified_result": null } diff --git a/apps/api/src/api/assignment/v2/repositories/assignment.repository.ts b/apps/api/src/api/assignment/v2/repositories/assignment.repository.ts index 8ad78854..30cdc959 100644 --- a/apps/api/src/api/assignment/v2/repositories/assignment.repository.ts +++ b/apps/api/src/api/assignment/v2/repositories/assignment.repository.ts @@ -89,7 +89,7 @@ export class AssignmentRepository { async findById( id: number, - userSession?: UserSession + userSession?: UserSession, ): Promise { const isLearner = userSession?.role === UserRole.LEARNER; @@ -202,7 +202,7 @@ export class AssignmentRepository { */ async findAllForUser( - userSession: UserSession + userSession: UserSession, ): Promise { // If user is an author, only show assignments they've authored if (userSession.role === UserRole.AUTHOR) { @@ -244,7 +244,6 @@ export class AssignmentRepository { * @returns Updated assignment */ async update(id: number, data: Partial): Promise { - console.log("Updating assignment with ID:", data); try { return await this.prisma.assignment.update({ where: { id }, @@ -257,7 +256,7 @@ export class AssignmentRepository { error instanceof Error ? error.stack : "No stack trace"; this.logger.error( `Error updating assignment ${id}: ${errorMessage}`, - errorStack + errorStack, ); throw error; } @@ -286,7 +285,7 @@ export class AssignmentRepository { error instanceof Error ? error.stack : "No stack trace"; this.logger.error( `Error replacing assignment ${id}: ${errorMessage}`, - errorStack + errorStack, ); throw error; } @@ -301,10 +300,10 @@ export class AssignmentRepository { private processAssignmentData( rawAssignment: Assignment & { questions: (Question & { variants: QuestionVariant[] })[]; - } + }, ): Assignment & { questions: QuestionDto[] } { const assignment = JSON.parse( - JSON.stringify(rawAssignment) + JSON.stringify(rawAssignment), ) as Assignment & { questions: QuestionDto[] }; const questions = Array.isArray(assignment.questions) @@ -320,7 +319,7 @@ export class AssignmentRepository { scoring: this.parseJsonField(q.scoring), choices: this.parseJsonField(q.choices), videoPresentationConfig: this.parseJsonField( - q.videoPresentationConfig + q.videoPresentationConfig, ), }; @@ -347,7 +346,7 @@ export class AssignmentRepository { filteredQuestions.sort( (a, b) => assignment.questionOrder.indexOf(a.id) - - assignment.questionOrder.indexOf(b.id) + assignment.questionOrder.indexOf(b.id), ); } @@ -372,7 +371,7 @@ export class AssignmentRepository { error instanceof Error ? error.stack : "No stack trace"; this.logger.error( `Error parsing JSON field: ${errorMessage}`, - errorStack + errorStack, ); return undefined; } diff --git a/apps/api/src/api/attempt/common/utils/attempt-questions-mapper.util.ts b/apps/api/src/api/attempt/common/utils/attempt-questions-mapper.util.ts index b2eb7a4e..d661dcf1 100644 --- a/apps/api/src/api/attempt/common/utils/attempt-questions-mapper.util.ts +++ b/apps/api/src/api/attempt/common/utils/attempt-questions-mapper.util.ts @@ -231,8 +231,6 @@ export class AttemptQuestionsMapper { ? translations.get(variantKey) || {} : {}; - console.log("variantTranslations", variantTranslations); - const questionTranslations = translations.has(questionKey) ? translations.get(questionKey) || {} : {}; diff --git a/apps/web/app/Helpers/checkDiff.ts b/apps/web/app/Helpers/checkDiff.ts index 4564cea0..b1b232d3 100644 --- a/apps/web/app/Helpers/checkDiff.ts +++ b/apps/web/app/Helpers/checkDiff.ts @@ -24,22 +24,30 @@ function safeArrayCompare( b: T[] | null | undefined, compareFn?: (itemA: T, itemB: T) => boolean, ): boolean { - if (a == null && b == null) return true; - if (a == null || b == null) return false; + // Normalize null/undefined to empty arrays for comparison + const normalizeArray = (arr: T[] | null | undefined): T[] => { + if (arr == null) return []; + return arr; + }; - if (a.length !== b.length) return false; + const normalizedA = normalizeArray(a); + const normalizedB = normalizeArray(b); - if (a.length === 0 && b.length === 0) return true; + if (normalizedA.length !== normalizedB.length) return false; + + if (normalizedA.length === 0 && normalizedB.length === 0) return true; if (compareFn) { - for (let i = 0; i < a.length; i++) { - const matchFound = b.some((bItem) => compareFn(a[i], bItem)); + for (let i = 0; i < normalizedA.length; i++) { + const matchFound = normalizedB.some((bItem) => + compareFn(normalizedA[i], bItem), + ); if (!matchFound) return false; } return true; } - return JSON.stringify(a) === JSON.stringify(b); + return JSON.stringify(normalizedA) === JSON.stringify(normalizedB); } export function useChangesSummary(): string { @@ -129,7 +137,7 @@ export function useChangesSummary(): string { } const originalQuestions = originalAssignment.questions || []; - const currentQuestions = questions || []; + const currentQuestions = questions instanceof Array ? questions : []; const addedQuestions = currentQuestions?.filter( (question) => diff --git a/apps/web/app/author/(components)/AuthorQuestionsPage/index.tsx b/apps/web/app/author/(components)/AuthorQuestionsPage/index.tsx index 2b30f00e..7a1a5e08 100644 --- a/apps/web/app/author/(components)/AuthorQuestionsPage/index.tsx +++ b/apps/web/app/author/(components)/AuthorQuestionsPage/index.tsx @@ -419,12 +419,15 @@ const AuthorQuestionsPage: FC = ({ useEffect(() => { const currentQuestionOrder = useAuthorStore.getState().questionOrder; - const newQuestionOrder = questions.map((q) => q.id); + const questionIds = questions.map((q) => q.id); if ( - JSON.stringify(currentQuestionOrder) !== JSON.stringify(newQuestionOrder) + !currentQuestionOrder || + currentQuestionOrder.length === 0 || + currentQuestionOrder.some((id) => !questionIds.includes(id)) || + questionIds.some((id) => !currentQuestionOrder.includes(id)) ) { - useAuthorStore.getState().setQuestionOrder(newQuestionOrder); + useAuthorStore.getState().setQuestionOrder(questionIds); } }, [questions]); @@ -1021,11 +1024,17 @@ const AuthorQuestionsPage: FC = ({ settingsUpdated = true; } if (assignmentData.config.attemptsBeforeCoolDown !== undefined) { - configStore.setAttemptsBeforeCoolDown(assignmentData.config.attemptsBeforeCoolDown); + configStore.setAttemptsBeforeCoolDown( + assignmentData.config.attemptsBeforeCoolDown, + ); settingsUpdated = true; } - if (assignmentData.config.retakeAttemptCoolDownMinutes !== undefined) { - configStore.setRetakeAttemptCoolDownMinutes(assignmentData.config.retakeAttemptCoolDownMinutes); + if ( + assignmentData.config.retakeAttemptCoolDownMinutes !== undefined + ) { + configStore.setRetakeAttemptCoolDownMinutes( + assignmentData.config.retakeAttemptCoolDownMinutes, + ); settingsUpdated = true; } if (assignmentData.config.allotedTimeMinutes !== undefined) { diff --git a/apps/web/app/author/[assignmentId]/review/page.tsx b/apps/web/app/author/[assignmentId]/review/page.tsx index 28bea0d5..0baf04d4 100644 --- a/apps/web/app/author/[assignmentId]/review/page.tsx +++ b/apps/web/app/author/[assignmentId]/review/page.tsx @@ -603,67 +603,91 @@ const QuestionChanges = ({ // Check for choices changes (for multiple choice questions) if (changeDetails.some((d) => d.includes("Modified choices"))) { - changes.push( -
-
Choices
-
-
-
- - Before -
-
- {originalQuestion.choices?.map((choice: any, idx: number) => ( -
-
- Choice {idx + 1}: - {choice.choice || "(empty)"} - {choice.isCorrect && ( - - Correct - - )} - - Points: {choice.points} - + // Double-check if there are actually meaningful differences + const originalChoices = originalQuestion.choices || []; + const currentChoices = currentQuestion.choices || []; + + // Only show if there are actual differences in non-empty arrays or meaningful changes + const hasActualChanges = + originalChoices.length > 0 || currentChoices.length > 0; + + if (hasActualChanges) { + changes.push( +
+
Choices
+
+
+
+ + Before +
+
+ {originalChoices.length > 0 ? ( + originalChoices.map((choice: any, idx: number) => ( +
+
+ Choice {idx + 1}: + {choice.choice || "(empty)"} + {choice.isCorrect && ( + + Correct + + )} + + Points: {choice.points} + +
+
+ )) + ) : ( +
+ No choices defined
-
- ))} -
-
-
-
- - After + )} +
-
- {currentQuestion.choices?.map((choice: any, idx: number) => ( -
-
- Choice {idx + 1}: - {choice.choice || "(empty)"} - {choice.isCorrect && ( - - Correct - - )} - - Points: {choice.points} - +
+
+ + + After + +
+
+ {currentChoices.length > 0 ? ( + currentChoices.map((choice: any, idx: number) => ( +
+
+ Choice {idx + 1}: + {choice.choice || "(empty)"} + {choice.isCorrect && ( + + Correct + + )} + + Points: {choice.points} + +
+
+ )) + ) : ( +
+ No choices defined
-
- ))} + )} +
-
-
, - ); +
, + ); + } } // Check for rubric changes diff --git a/apps/web/app/chatbot/store/useAuthorContext.ts b/apps/web/app/chatbot/store/useAuthorContext.ts index e276adee..99e369a6 100644 --- a/apps/web/app/chatbot/store/useAuthorContext.ts +++ b/apps/web/app/chatbot/store/useAuthorContext.ts @@ -78,7 +78,7 @@ export const useAuthorContext = (): UseAuthorContextInterface => { }, [setUpdatedAt]); useEffect(() => { - if (questions && focusedQuestionId) { + if (questions && focusedQuestionId && questions instanceof Array) { const focusedQuestion = questions.find((q) => q.id === focusedQuestionId); if (focusedQuestion) { setCurrentQuestion(focusedQuestion); diff --git a/apps/web/app/learner/(components)/Header.tsx b/apps/web/app/learner/(components)/Header.tsx index 57e65cb4..cb16af40 100644 --- a/apps/web/app/learner/(components)/Header.tsx +++ b/apps/web/app/learner/(components)/Header.tsx @@ -187,7 +187,7 @@ function LearnerHeader() { role === "author" ? q.choices ?.map((choice, index) => - q.learnerChoices?.find((c) => String(c) === String(index)) + q.learnerChoices?.includes(String(index)) ? choice.choice : undefined, ) @@ -291,7 +291,9 @@ function LearnerHeader() { }); } clearGithubStore(); - clearLearnerAnswers(); // Clear all learner responses after successful submission + if (role === "learner") { + clearLearnerAnswers(); + } useLearnerStore.getState().setActiveQuestionNumber(null); router.push(`/learner/${assignmentId}/successPage/${res.id}`); diff --git a/apps/web/app/learner/[assignmentId]/successPage/Question.tsx b/apps/web/app/learner/[assignmentId]/successPage/Question.tsx index 16aba0ce..1cc8d120 100644 --- a/apps/web/app/learner/[assignmentId]/successPage/Question.tsx +++ b/apps/web/app/learner/[assignmentId]/successPage/Question.tsx @@ -33,6 +33,7 @@ interface Props { language: string; showSubmissionFeedback?: boolean; showCorrectAnswer?: boolean; + correctAnswerVisibility?: "NEVER" | "ALWAYS" | "ON_PASS"; } interface HighestScoreResponseType { @@ -106,6 +107,7 @@ const Question: FC = ({ language = "en", showSubmissionFeedback, showCorrectAnswer, + correctAnswerVisibility, }) => { const { question: questionText, @@ -483,6 +485,8 @@ const Question: FC = ({ const questionResponse = questionResponses?.[0]; + const shouldShowHighlighting = correctAnswerVisibility !== "NEVER"; + const learnerResponse: LearnerResponseType = (learnerChoices && learnerChoices.length > 0 ? learnerChoices @@ -504,11 +508,13 @@ const Question: FC = ({ return (

0 - ? "bg-yellow-50 border border-yellow-500 rounded p-2" - : "bg-red-50 border border-red-700 rounded p-2" + shouldShowHighlighting + ? highestScoreResponse?.points === totalPoints + ? "bg-green-50 border border-green-500 rounded p-2" + : highestScoreResponse?.points > 0 + ? "bg-yellow-50 border border-yellow-500 rounded p-2" + : "bg-red-50 border border-red-700 rounded p-2" + : "bg-gray-50 border border-gray-300 rounded p-2" }`} > @@ -541,11 +547,15 @@ const Question: FC = ({ (Array.isArray(learnerResponse) && learnerResponse.length === 0) ) { return ( -

+

No answer was provided by the learner. - + {shouldShowHighlighting && ( + + )}

); } @@ -564,12 +574,16 @@ const Question: FC = ({
  • @@ -613,10 +627,12 @@ const Question: FC = ({
  • - {isCorrect && showCorrectAnswer && ( - - )} - {!isCorrect && isSelected && ( + {shouldShowHighlighting && + isCorrect && + showCorrectAnswer && ( + + )} + {shouldShowHighlighting && !isCorrect && isSelected && ( )}
    @@ -630,11 +646,13 @@ const Question: FC = ({ return (

    0 - ? "bg-yellow-50 border border-yellow-500 rounded p-2" - : "bg-red-50 border border-red-700 rounded p-2" + shouldShowHighlighting + ? highestScoreResponse?.points === totalPoints + ? "bg-green-50 border border-green-500 rounded p-2" + : highestScoreResponse?.points > 0 + ? "bg-yellow-50 border border-yellow-500 rounded p-2" + : "bg-red-50 border border-red-700 rounded p-2" + : "bg-gray-50 border border-gray-300 rounded p-2" }`} > {learnerResponse @@ -656,11 +674,13 @@ const Question: FC = ({ return (

    0 - ? "bg-yellow-50 border border-yellow-500 rounded p-2" - : "bg-red-50 border border-red-700 rounded p-2" + shouldShowHighlighting + ? highestScoreResponse?.points === totalPoints + ? "bg-green-50 border border-green-500 rounded p-2" + : highestScoreResponse?.points > 0 + ? "bg-yellow-50 border border-yellow-500 rounded p-2" + : "bg-red-50 border border-red-700 rounded p-2" + : "bg-gray-50 border border-gray-300 rounded p-2" }`} > Transcript: {transcript} @@ -722,11 +742,13 @@ const Question: FC = ({ return (

    0 - ? "bg-yellow-50 border border-yellow-500 rounded p-2" - : "bg-red-50 border border-red-700 rounded p-2" + shouldShowHighlighting + ? highestScoreResponse?.points === totalPoints + ? "bg-green-50 border border-green-500 rounded p-2" + : highestScoreResponse?.points > 0 + ? "bg-yellow-50 border border-yellow-500 rounded p-2" + : "bg-red-50 border border-red-700 rounded p-2" + : "bg-gray-50 border border-gray-300 rounded p-2" }`} > {learnerResponse} @@ -734,19 +756,27 @@ const Question: FC = ({ ); } else { return ( -

    +

    No answer was provided by the learner. - + {shouldShowHighlighting && ( + + )}

    ); } } else { return ( -

    +

    No answer was provided by the learner. - + {shouldShowHighlighting && ( + + )}

    ); } diff --git a/apps/web/app/learner/[assignmentId]/successPage/[submissionId]/page.tsx b/apps/web/app/learner/[assignmentId]/successPage/[submissionId]/page.tsx index 66f23058..f2559a23 100644 --- a/apps/web/app/learner/[assignmentId]/successPage/[submissionId]/page.tsx +++ b/apps/web/app/learner/[assignmentId]/successPage/[submissionId]/page.tsx @@ -611,6 +611,7 @@ function SuccessPage() { question={question} language={userPreferredLanguage} showSubmissionFeedback={showSubmissionFeedback} + correctAnswerVisibility={correctAnswerVisibility} showCorrectAnswer={(() => { if (correctAnswerVisibility === "NEVER") return false; if (correctAnswerVisibility === "ALWAYS") return true; diff --git a/apps/web/components/version-control/VersionTreeView.tsx b/apps/web/components/version-control/VersionTreeView.tsx index b0edfdb1..d60fccb9 100644 --- a/apps/web/components/version-control/VersionTreeView.tsx +++ b/apps/web/components/version-control/VersionTreeView.tsx @@ -1561,6 +1561,29 @@ export function VersionTreeView({ assignmentId }: Props) { : "Hidden"}
    +
    + + Correct Answer Visibility: + + + { + (selectedVersionDetails || selectedVersion) + .correctAnswerVisibility + } + +
    diff --git a/apps/web/hooks/useUserBehaviorMonitor.ts b/apps/web/hooks/useUserBehaviorMonitor.ts index 237d615c..9d16af50 100644 --- a/apps/web/hooks/useUserBehaviorMonitor.ts +++ b/apps/web/hooks/useUserBehaviorMonitor.ts @@ -110,7 +110,7 @@ export const useUserBehaviorMonitor = ( context = `working on "${assignmentName}"`; } - if (focusedQuestion && totalQuestions > 0) { + if (focusedQuestion && totalQuestions > 0 && contextData.questions) { const questionIndex = contextData.questions?.findIndex( (q) => q.id === focusedQuestion || q.questionId === focusedQuestion, ); diff --git a/apps/web/stores/author.ts b/apps/web/stores/author.ts index 5c552db8..449df68a 100644 --- a/apps/web/stores/author.ts +++ b/apps/web/stores/author.ts @@ -1915,6 +1915,10 @@ export const useAuthorStore = createWithEqualityFn< useAssignmentFeedbackConfig .getState() .setAssignmentFeedbackConfigStore({ + correctAnswerVisibility: + versionData.correctAnswerVisibility !== undefined + ? versionData.correctAnswerVisibility + : feedbackConfigState.correctAnswerVisibility, showAssignmentScore: versionData.showAssignmentScore !== undefined ? versionData.showAssignmentScore @@ -1994,6 +1998,9 @@ export const useAuthorStore = createWithEqualityFn< gradingCriteriaOverview: decodedVersionData.gradingCriteriaOverview, questions: processedQuestions, + questionOrder: + decodedVersionData.questionOrder || + processedQuestions.map((q) => q.id), checkedOutVersion: versionToCheckout, hasUnsavedChanges: false, }); From a7f8682dacf92936bb9a7299bfc6e12f0e2ee80a Mon Sep 17 00:00:00 2001 From: MmagdyhafezZ Date: Thu, 9 Oct 2025 07:24:49 -0600 Subject: [PATCH 5/5] formatting --- .../assignment/attempt/attempt.controller.ts | 42 +-- .../api/assignment/attempt/attempt.service.ts | 302 +++++++++--------- .../dto/get.assignment.response.dto.ts | 2 +- .../question/question.controller.ts | 32 +- .../v2/services/assignment.service.ts | 139 ++++---- .../v2/services/draft-management.service.ts | 26 +- .../v2/services/version-management.service.ts | 122 +++---- .../v2/tests/unit/__mocks__/ common-mocks.ts | 106 +++--- .../api/attempt/dto/success-page-data.dto.ts | 6 +- .../services/attempt-grading.service.ts | 14 +- .../(components)/StepTwo/FeedbackSettings.tsx | 16 +- 11 files changed, 406 insertions(+), 401 deletions(-) diff --git a/apps/api/src/api/assignment/attempt/attempt.controller.ts b/apps/api/src/api/assignment/attempt/attempt.controller.ts index 33edeb1c..871d9748 100644 --- a/apps/api/src/api/assignment/attempt/attempt.controller.ts +++ b/apps/api/src/api/assignment/attempt/attempt.controller.ts @@ -62,7 +62,7 @@ export class AttemptControllerV1 { private logger; constructor( @Inject(WINSTON_MODULE_PROVIDER) private parentLogger: Logger, - private readonly attemptService: AttemptServiceV1 + private readonly attemptService: AttemptServiceV1, ) { this.logger = parentLogger.child({ context: AttemptControllerV1.name }); } @@ -87,11 +87,11 @@ export class AttemptControllerV1 { @ApiResponse({ status: 403 }) createAssignmentAttempt( @Param("assignmentId") assignmentId: number, - @Req() request: UserSessionRequest + @Req() request: UserSessionRequest, ): Promise { return this.attemptService.createAssignmentAttempt( Number(assignmentId), - request.userSession + request.userSession, ); } @@ -103,11 +103,11 @@ export class AttemptControllerV1 { @ApiResponse({ status: 403 }) listAssignmentAttempts( @Param("assignmentId") assignmentId: number, - @Req() request: UserSessionRequest + @Req() request: UserSessionRequest, ): Promise { return this.attemptService.listAssignmentAttempts( Number(assignmentId), - request.userSession + request.userSession, ); } @@ -119,11 +119,11 @@ export class AttemptControllerV1 { @ApiResponse({ status: 403 }) getAssignmentAttempt( @Param("attemptId") assignmentAttemptId: number, - @Query("lang") lang?: string + @Query("lang") lang?: string, ): Promise { return this.attemptService.getAssignmentAttempt( Number(assignmentAttemptId), - lang + lang, ); } @@ -134,10 +134,10 @@ export class AttemptControllerV1 { @ApiResponse({ status: 200, type: GetAssignmentAttemptResponseDto }) @ApiResponse({ status: 403 }) getLearnerAssignmentAttempt( - @Param("attemptId") assignmentAttemptId: number + @Param("attemptId") assignmentAttemptId: number, ): Promise { return this.attemptService.getLearnerAssignmentAttempt( - Number(assignmentAttemptId) + Number(assignmentAttemptId), ); } @@ -173,7 +173,7 @@ export class AttemptControllerV1 { @Param("assignmentId") assignmentId: number, @Body() learnerUpdateAssignmentAttemptDto: LearnerUpdateAssignmentAttemptRequestDto, - @Req() request: UserSessionRequest + @Req() request: UserSessionRequest, ): Promise { const authCookie: string = request?.cookies?.authentication as string; const gradingCallbackRequired = @@ -185,7 +185,7 @@ export class AttemptControllerV1 { learnerUpdateAssignmentAttemptDto, authCookie, gradingCallbackRequired, - request + request, ); } @@ -200,14 +200,14 @@ export class AttemptControllerV1 { @Param("assignmentId") assignmentId: string, @Param("attemptId") attemptId: string, @Body() body: { feedback: AssignmentFeedbackDto }, - @Req() request: UserSessionRequest + @Req() request: UserSessionRequest, ): Promise { const feedbackDto = body.feedback; return this.attemptService.submitFeedback( Number(assignmentId), Number(attemptId), feedbackDto, - request.userSession + request.userSession, ); } @@ -220,12 +220,12 @@ export class AttemptControllerV1 { getFeedback( @Param("assignmentId") assignmentId: string, @Param("attemptId") attemptId: string, - @Req() request: UserSessionRequest + @Req() request: UserSessionRequest, ): Promise { return this.attemptService.getFeedback( Number(assignmentId), Number(attemptId), - request.userSession + request.userSession, ); } @@ -239,13 +239,13 @@ export class AttemptControllerV1 { @Param("assignmentId") assignmentId: string, @Param("attemptId") attemptId: string, @Body() body: { regradingRequest: RegradingRequestDto }, - @Req() request: UserSessionRequest + @Req() request: UserSessionRequest, ): Promise { return this.attemptService.processRegradingRequest( Number(assignmentId), Number(attemptId), body.regradingRequest, - request.userSession + request.userSession, ); } @@ -258,12 +258,12 @@ export class AttemptControllerV1 { getRegradingStatus( @Param("assignmentId") assignmentId: string, @Param("attemptId") attemptId: string, - @Req() request: UserSessionRequest + @Req() request: UserSessionRequest, ): Promise { return this.attemptService.getRegradingStatus( Number(assignmentId), Number(attemptId), - request.userSession + request.userSession, ); } @@ -286,7 +286,7 @@ export class AttemptControllerV1 { @Param("attemptId") attemptId: number, @Param("assignmentId") assignmentId: number, @Body() body: ReportRequestDTO, - @Req() request: UserSessionRequest + @Req() request: UserSessionRequest, ): Promise<{ message: string }> { const { issueType, description } = body; @@ -309,7 +309,7 @@ export class AttemptControllerV1 { Number(attemptId), issueType, description, - userId + userId, ); return { message: "Report submitted successfully" }; diff --git a/apps/api/src/api/assignment/attempt/attempt.service.ts b/apps/api/src/api/assignment/attempt/attempt.service.ts index 1811f909..0256c0e4 100644 --- a/apps/api/src/api/assignment/attempt/attempt.service.ts +++ b/apps/api/src/api/assignment/attempt/attempt.service.ts @@ -98,14 +98,14 @@ export class AttemptServiceV1 { private readonly llmFacadeService: LlmFacadeService, private readonly questionService: QuestionService, private readonly assignmentService: AssignmentServiceV1, - private readonly httpService: HttpService + private readonly httpService: HttpService, ) {} async submitFeedback( assignmentId: number, attemptId: number, feedbackDto: AssignmentFeedbackDto, - userSession: UserSession + userSession: UserSession, ): Promise { const assignmentAttempt = await this.prisma.assignmentAttempt.findUnique({ where: { id: attemptId }, @@ -113,19 +113,19 @@ export class AttemptServiceV1 { if (!assignmentAttempt) { throw new NotFoundException( - `Assignment attempt with ID ${attemptId} not found.` + `Assignment attempt with ID ${attemptId} not found.`, ); } if (assignmentAttempt.assignmentId !== assignmentId) { throw new BadRequestException( - "Assignment ID does not match the attempt." + "Assignment ID does not match the attempt.", ); } if (assignmentAttempt.userId !== userSession.userId) { throw new ForbiddenException( - "You do not have permission to submit feedback for this attempt." + "You do not have permission to submit feedback for this attempt.", ); } @@ -172,7 +172,7 @@ export class AttemptServiceV1 { async getFeedback( assignmentId: number, attemptId: number, - userSession: UserSession + userSession: UserSession, ): Promise { const feedback = await this.prisma.assignmentFeedback.findFirst({ where: { @@ -200,7 +200,7 @@ export class AttemptServiceV1 { assignmentId: number, attemptId: number, regradingRequestDto: RegradingRequestDto, - userSession: UserSession + userSession: UserSession, ): Promise { const assignmentAttempt = await this.prisma.assignmentAttempt.findUnique({ where: { id: attemptId }, @@ -208,19 +208,19 @@ export class AttemptServiceV1 { if (!assignmentAttempt) { throw new NotFoundException( - `Assignment attempt with ID ${attemptId} not found.` + `Assignment attempt with ID ${attemptId} not found.`, ); } if (assignmentAttempt.assignmentId !== assignmentId) { throw new BadRequestException( - "Assignment ID does not match the attempt." + "Assignment ID does not match the attempt.", ); } if (assignmentAttempt.userId !== userSession.userId) { throw new ForbiddenException( - "You do not have permission to request regrading for this attempt." + "You do not have permission to request regrading for this attempt.", ); } @@ -242,7 +242,7 @@ export class AttemptServiceV1 { regradingStatus: RegradingStatus.PENDING, updatedAt: new Date(), }, - } + }, ); return { @@ -268,7 +268,7 @@ export class AttemptServiceV1 { async getRegradingStatus( assignmentId: number, attemptId: number, - userSession: UserSession + userSession: UserSession, ): Promise { const regradingRequest = await this.prisma.regradingRequest.findFirst({ where: { @@ -280,7 +280,7 @@ export class AttemptServiceV1 { if (!regradingRequest) { throw new NotFoundException( - `Regrading request for assignment ${assignmentId} and attempt ${attemptId} not found.` + `Regrading request for assignment ${assignmentId} and attempt ${attemptId} not found.`, ); } @@ -297,7 +297,7 @@ export class AttemptServiceV1 { */ async listAssignmentAttempts( assignmentId: number, - userSession: UserSession + userSession: UserSession, ): Promise { const { userId, role } = userSession; @@ -337,11 +337,11 @@ export class AttemptServiceV1 { */ async createAssignmentAttempt( assignmentId: number, - userSession: UserSession + userSession: UserSession, ): Promise { const assignment = await this.assignmentService.findOne( assignmentId, - userSession + userSession, ); await this.validateNewAttempt(assignment, userSession); const attemptExpiresAt = this.calculateAttemptExpiresAt(assignment); @@ -377,7 +377,7 @@ export class AttemptServiceV1 { questions.sort( (a, b) => assignment.questionOrder.indexOf(a.id) - - assignment.questionOrder.indexOf(b.id) + assignment.questionOrder.indexOf(b.id), ); } await this.prisma.assignmentAttempt.update({ @@ -390,7 +390,7 @@ export class AttemptServiceV1 { const attemptQuestionVariantsData = questions.map((question) => { const questionAndVariants = [undefined, ...question.variants]; const randomIndex = Math.floor( - Math.random() * questionAndVariants.length + Math.random() * questionAndVariants.length, ); const chosenVariant = questionAndVariants[randomIndex]; @@ -401,12 +401,12 @@ export class AttemptServiceV1 { variantId = chosenVariant.id ?? undefined; randomizedChoices = this.maybeShuffleChoices( chosenVariant.choices as unknown as Choice[], - chosenVariant.randomizedChoices === true + chosenVariant.randomizedChoices === true, ); } else { randomizedChoices = this.maybeShuffleChoices( question.choices as unknown as Choice[], - question.randomizedChoices === true + question.randomizedChoices === true, ); } @@ -478,7 +478,7 @@ export class AttemptServiceV1 { updateAssignmentAttemptDto: LearnerUpdateAssignmentAttemptRequestDto, authCookie: string, gradingCallbackRequired: boolean, - request: UserSessionRequest + request: UserSessionRequest, ): Promise { const { role, userId } = request.userSession; if (role === UserRole.LEARNER) { @@ -495,7 +495,7 @@ export class AttemptServiceV1 { }); if (!assignmentAttempt) { throw new NotFoundException( - `AssignmentAttempt with Id ${assignmentAttemptId} not found.` + `AssignmentAttempt with Id ${assignmentAttemptId} not found.`, ); } const tenSecondsBeforeNow = new Date(Date.now() - 10 * 1000); @@ -531,7 +531,7 @@ export class AttemptServiceV1 { for (const response of updateAssignmentAttemptDto.responsesForQuestions) { const questionId: number = response.id; const variantMapping = assignmentAttempt.questionVariants.find( - (qv) => qv.questionId === questionId + (qv) => qv.questionId === questionId, ); let question: QuestionDto; if (variantMapping && variantMapping.questionVariant !== null) { @@ -548,17 +548,17 @@ export class AttemptServiceV1 { scoring: typeof variant.scoring === "string" ? (JSON.parse(variant.scoring) as ScoringDto) - : (variant.scoring as unknown as ScoringDto) ?? + : ((variant.scoring as unknown as ScoringDto) ?? (typeof baseQuestion.scoring === "string" ? (JSON.parse(baseQuestion.scoring) as ScoringDto) - : (baseQuestion.scoring as unknown as ScoringDto)), + : (baseQuestion.scoring as unknown as ScoringDto))), choices: typeof variant.choices === "string" ? (JSON.parse(variant.choices) as Choice[]) - : (variant.choices as unknown as Choice[]) ?? + : ((variant.choices as unknown as Choice[]) ?? (typeof baseQuestion.choices === "string" ? (JSON.parse(baseQuestion.choices) as Choice[]) - : (baseQuestion.choices as unknown as Choice[])), + : (baseQuestion.choices as unknown as Choice[]))), answer: baseQuestion.answer ?? variant.answer, alreadyInBackend: true, totalPoints: baseQuestion.totalPoints, @@ -572,7 +572,7 @@ export class AttemptServiceV1 { question = await this.applyTranslationToQuestion( question, updateAssignmentAttemptDto.language, - variantMapping + variantMapping, ); preTranslatedQuestions.set(questionId, question); } @@ -605,17 +605,17 @@ export class AttemptServiceV1 { updateAssignmentAttemptDto.language, updateAssignmentAttemptDto.authorQuestions, updateAssignmentAttemptDto.authorAssignmentDetails, - updateAssignmentAttemptDto.preTranslatedQuestions + updateAssignmentAttemptDto.preTranslatedQuestions, ); const { grade, totalPointsEarned, totalPossiblePoints } = role === UserRole.LEARNER ? this.calculateGradeForLearner( successfulQuestionResponses, - assignment as unknown as GetAssignmentAttemptResponseDto + assignment as unknown as GetAssignmentAttemptResponseDto, ) : this.calculateGradeForAuthor( successfulQuestionResponses, - updateAssignmentAttemptDto.authorQuestions + updateAssignmentAttemptDto.authorQuestions, ); if (gradingCallbackRequired && role === UserRole.LEARNER) { @@ -653,14 +653,14 @@ export class AttemptServiceV1 { assignment.currentVersion?.correctAnswerVisibility || "NEVER", feedbacksForQuestions: this.constructFeedbacksForQuestions( successfulQuestionResponses, - assignment as unknown as LearnerGetAssignmentResponseDto + assignment as unknown as LearnerGetAssignmentResponseDto, ), }; } else { const result = await this.updateAssignmentAttemptInDb( assignmentAttemptId, updateAssignmentAttemptDto, - grade + grade, ); return { id: result.id, @@ -675,7 +675,7 @@ export class AttemptServiceV1 { assignment.currentVersion?.correctAnswerVisibility || "NEVER", feedbacksForQuestions: this.constructFeedbacksForQuestions( successfulQuestionResponses, - assignment as unknown as LearnerGetAssignmentResponseDto + assignment as unknown as LearnerGetAssignmentResponseDto, ), }; } @@ -721,7 +721,7 @@ export class AttemptServiceV1 { * @throws BadRequestException If stored data formats are invalid */ async getLearnerAssignmentAttempt( - assignmentAttemptId: number + assignmentAttemptId: number, ): Promise { const assignmentAttempt = await this.prisma.assignmentAttempt.findUnique({ where: { id: assignmentAttemptId }, @@ -734,7 +734,7 @@ export class AttemptServiceV1 { }); if (!assignmentAttempt) { throw new NotFoundException( - `AssignmentAttempt with Id ${assignmentAttemptId} not found.` + `AssignmentAttempt with Id ${assignmentAttemptId} not found.`, ); } @@ -743,7 +743,7 @@ export class AttemptServiceV1 { }); if (!questions) { throw new NotFoundException( - `Questions for assignment with Id ${assignmentAttempt.assignmentId} not found.` + `Questions for assignment with Id ${assignmentAttempt.assignmentId} not found.`, ); } @@ -813,7 +813,7 @@ export class AttemptServiceV1 { }); const questionVariantsMap = new Map( - questionsWithVariants.map((question) => [question.id, question]) + questionsWithVariants.map((question) => [question.id, question]), ); const mergedQuestions = questions.map((originalQ) => { const variantQ = questionVariantsMap.get(originalQ.id); @@ -834,7 +834,7 @@ export class AttemptServiceV1 { liveRecordingConfig: (question.liveRecordingConfig as unknown as JsonValue) ?? undefined, })), - assignmentAttempt.questionResponses as unknown as QuestionResponse[] + assignmentAttempt.questionResponses as unknown as QuestionResponse[], ); const finalQuestions = questionOrder .map((qId) => questionsWithResponses.find((q) => q.id === qId)) @@ -951,7 +951,7 @@ export class AttemptServiceV1 { */ async getAssignmentAttempt( assignmentAttemptId: number, - language: string + language: string, ): Promise { if (!language) { language = "en"; @@ -975,7 +975,7 @@ export class AttemptServiceV1 { }); if (!assignmentAttempt) { throw new NotFoundException( - `AssignmentAttempt with Id ${assignmentAttemptId} not found.` + `AssignmentAttempt with Id ${assignmentAttemptId} not found.`, ); } @@ -1068,7 +1068,7 @@ export class AttemptServiceV1 { if (!Array.isArray(normalizedChoices)) { throw new InternalServerErrorException( - `Malformed choices for question ${originalQ.id}` + `Malformed choices for question ${originalQ.id}`, ); } let finalChoices = normalizedChoices; @@ -1078,7 +1078,7 @@ export class AttemptServiceV1 { if (typeof qv.randomizedChoices === "string") { try { randomizedChoicesArray = JSON.parse( - qv.randomizedChoices + qv.randomizedChoices, ) as Choice[]; } catch { randomizedChoicesArray = []; @@ -1091,11 +1091,11 @@ export class AttemptServiceV1 { return normalizedChoices?.findIndex((bc) => bc.id === rChoice.id); } return normalizedChoices?.findIndex( - (bc) => bc.choice === rChoice.choice + (bc) => bc.choice === rChoice.choice, ); }); const orderedBaseChoices = permutation?.map( - (index) => normalizedChoices[index] + (index) => normalizedChoices[index], ); if (orderedBaseChoices?.length === normalizedChoices?.length) { finalChoices = orderedBaseChoices; @@ -1115,7 +1115,7 @@ export class AttemptServiceV1 { const origTranslatedChoices = translationObject.translatedChoices as Choice[]; const reorderedTranslatedChoices = permutation.map( - (index) => origTranslatedChoices[index] + (index) => origTranslatedChoices[index], ); translationObject.translatedChoices = reorderedTranslatedChoices; } @@ -1145,14 +1145,14 @@ export class AttemptServiceV1 { }); const questionVariantsMap = new Map( - questionsWithVariants.map((question) => [question.id, question]) + questionsWithVariants.map((question) => [question.id, question]), ); const questions: Question[] = await this.prisma.question.findMany({ where: { assignmentId: assignmentAttempt.assignmentId }, }); const nonVariantQuestions = questions.filter( - (originalQ) => !questionVariantsMap.has(originalQ.id) + (originalQ) => !questionVariantsMap.has(originalQ.id), ); const mergedQuestions = [ @@ -1210,7 +1210,7 @@ export class AttemptServiceV1 { typeof question.randomizedChoices === "string" ) { const randomizedArray = JSON.parse( - question.randomizedChoices + question.randomizedChoices, ) as Choice[]; for (const choice of randomizedArray) { delete choice.points; @@ -1298,7 +1298,7 @@ export class AttemptServiceV1 { language: string, authorQuestions?: QuestionDto[], assignmentDetails?: authorAssignmentDetailsDTO, - preTranslatedQuestions?: Map + preTranslatedQuestions?: Map, ): Promise { let question: QuestionDto; let assignmentContext: { @@ -1325,11 +1325,11 @@ export class AttemptServiceV1 { }); if (!assignmentAttempt) { throw new NotFoundException( - `AssignmentAttempt with Id ${assignmentAttemptId} not found.` + `AssignmentAttempt with Id ${assignmentAttemptId} not found.`, ); } const variantMapping = assignmentAttempt.questionVariants.find( - (qv) => qv.questionId === questionId + (qv) => qv.questionId === questionId, ); if ( variantMapping && @@ -1350,17 +1350,17 @@ export class AttemptServiceV1 { scoring: typeof variant.scoring === "string" ? (JSON.parse(variant.scoring) as ScoringDto) - : (variant.scoring as unknown as ScoringDto) ?? + : ((variant.scoring as unknown as ScoringDto) ?? (typeof baseQuestion.scoring === "string" ? (JSON.parse(baseQuestion.scoring) as ScoringDto) - : (baseQuestion.scoring as unknown as ScoringDto)), + : (baseQuestion.scoring as unknown as ScoringDto))), choices: typeof variant.choices === "string" ? (JSON.parse(variant.choices) as Choice[]) - : (variant.choices as unknown as Choice[]) ?? + : ((variant.choices as unknown as Choice[]) ?? (typeof baseQuestion.choices === "string" ? (JSON.parse(baseQuestion.choices) as Choice[]) - : (baseQuestion.choices as unknown as Choice[])), + : (baseQuestion.choices as unknown as Choice[]))), answer: baseQuestion.answer ?? variant.answer, alreadyInBackend: true, totalPoints: baseQuestion.totalPoints, @@ -1373,7 +1373,7 @@ export class AttemptServiceV1 { assignmentId, questionId, assignmentAttemptId, - role + role, ); } else if (role === UserRole.AUTHOR) { question = authorQuestions.find((q) => q.id === questionId); @@ -1387,7 +1387,7 @@ export class AttemptServiceV1 { createQuestionResponseAttemptRequestDto, assignmentContext, assignmentId, - language + language, ); const result = await this.prisma.questionResponse.create({ data: { @@ -1444,7 +1444,7 @@ export class AttemptServiceV1 { attemptId: number, issueType: ReportType, description: string, - userId: string + userId: string, ): Promise { const assignmentExists = await this.prisma.assignment.findUnique({ where: { id: assignmentId }, @@ -1472,7 +1472,7 @@ export class AttemptServiceV1 { }); if (reports.length >= 5) { throw new UnprocessableEntityException( - "You have reached the maximum number of reports allowed in a 24-hour period." + "You have reached the maximum number of reports allowed in a 24-hour period.", ); } @@ -1495,7 +1495,7 @@ export class AttemptServiceV1 { */ private async validateNewAttempt( assignment: LearnerGetAssignmentResponseDto, - userSession: UserSession + userSession: UserSession, ): Promise { const now = new Date(); const timeRangeStartDate = this.calculateTimeRangeStartDate(assignment); @@ -1521,13 +1521,13 @@ export class AttemptServiceV1 { orderBy: { createdAt: "desc" }, }); const ongoingAttempts = attempts.filter( - (sub) => !sub.submitted && (!sub.expiresAt || sub.expiresAt >= now) + (sub) => !sub.submitted && (!sub.expiresAt || sub.expiresAt >= now), ); if (ongoingAttempts.length > 0) { throw new UnprocessableEntityException(IN_PROGRESS_SUBMISSION_EXCEPTION); } const attemptsInTimeRange = attempts.filter( - (sub) => sub.createdAt >= timeRangeStartDate && sub.createdAt <= now + (sub) => sub.createdAt >= timeRangeStartDate && sub.createdAt <= now, ); if ( @@ -1535,18 +1535,18 @@ export class AttemptServiceV1 { attemptsInTimeRange.length >= assignment.attemptsPerTimeRange ) { throw new UnprocessableEntityException( - TIME_RANGE_ATTEMPTS_SUBMISSION_EXCEPTION_MESSAGE + TIME_RANGE_ATTEMPTS_SUBMISSION_EXCEPTION_MESSAGE, ); } if (assignment.numAttempts !== null && assignment.numAttempts !== -1) { const totalAttempts = await this.countUserAttempts( userSession.userId, - assignment.id + assignment.id, ); if (totalAttempts >= assignment.numAttempts) { throw new UnprocessableEntityException( - MAX_ATTEMPTS_SUBMISSION_EXCEPTION_MESSAGE + MAX_ATTEMPTS_SUBMISSION_EXCEPTION_MESSAGE, ); } @@ -1576,7 +1576,7 @@ export class AttemptServiceV1 { * @returns The expiration date or null. */ private calculateAttemptExpiresAt( - assignment: LearnerGetAssignmentResponseDto + assignment: LearnerGetAssignmentResponseDto, ): Date | null { if ( assignment.allotedTimeMinutes !== undefined && @@ -1589,7 +1589,7 @@ export class AttemptServiceV1 { private maybeShuffleChoices( choices: Choice[] | string | null | undefined, - shouldShuffle: boolean + shouldShuffle: boolean, ): string | null { if (!choices) return; let parsed: Choice[]; @@ -1630,7 +1630,7 @@ export class AttemptServiceV1 { language: string, authorQuestions?: QuestionDto[], assignmentDetails?: authorAssignmentDetailsDTO, - preTranslatedQuestions?: Map + preTranslatedQuestions?: Map, ): Promise { const questionResponsesPromise = responsesForQuestions.map( async (questionResponse) => { @@ -1644,13 +1644,13 @@ export class AttemptServiceV1 { language, authorQuestions, assignmentDetails, - preTranslatedQuestions + preTranslatedQuestions, ); - } + }, ); const questionResponses = await Promise.allSettled( - questionResponsesPromise + questionResponsesPromise, ); const successfulResponses = questionResponses .filter((response) => response.status === "fulfilled") @@ -1662,7 +1662,7 @@ export class AttemptServiceV1 { if (failedResponses.length > 0) { throw new InternalServerErrorException( - `Failed to submit questions: ${failedResponses.join(", ")}` + `Failed to submit questions: ${failedResponses.join(", ")}`, ); } @@ -1677,20 +1677,20 @@ export class AttemptServiceV1 { */ private calculateGradeForAuthor( successfulQuestionResponses: CreateQuestionResponseAttemptResponseDto[], - authorQuestions: QuestionDto[] + authorQuestions: QuestionDto[], ): { grade: number; totalPointsEarned: number; totalPossiblePoints: number } { if (successfulQuestionResponses.length === 0) { return { grade: 0, totalPointsEarned: 0, totalPossiblePoints: 0 }; } const totalPointsEarned = successfulQuestionResponses.reduce( (accumulator, response) => accumulator + response.totalPoints, - 0 + 0, ); const totalPossiblePoints = authorQuestions.reduce( (accumulator: number, question: QuestionDto) => accumulator + question.totalPoints, - 0 + 0, ); const grade = totalPointsEarned / totalPossiblePoints; @@ -1705,20 +1705,20 @@ export class AttemptServiceV1 { */ private calculateGradeForLearner( successfulQuestionResponses: CreateQuestionResponseAttemptResponseDto[], - assignment: GetAssignmentAttemptResponseDto + assignment: GetAssignmentAttemptResponseDto, ): { grade: number; totalPointsEarned: number; totalPossiblePoints: number } { if (successfulQuestionResponses.length === 0) { return { grade: 0, totalPointsEarned: 0, totalPossiblePoints: 0 }; } const totalPointsEarned = successfulQuestionResponses.reduce( (accumulator, response) => accumulator + response.totalPoints, - 0 + 0, ); const totalPossiblePoints = assignment.questions.reduce( (accumulator: number, question: { totalPoints: number }) => accumulator + question.totalPoints, - 0 + 0, ); const grade = totalPointsEarned / totalPossiblePoints; @@ -1732,7 +1732,7 @@ export class AttemptServiceV1 { */ private async sendGradeToLtiGateway( grade: number, - authCookie: string + authCookie: string, ): Promise { const ltiGatewayResponse = await this.httpService .put( @@ -1742,7 +1742,7 @@ export class AttemptServiceV1 { headers: { Cookie: `authentication=${authCookie}`, }, - } + }, ) .toPromise(); @@ -1761,7 +1761,7 @@ export class AttemptServiceV1 { private async updateAssignmentAttemptInDb( assignmentAttemptId: number, updateAssignmentAttemptDto: LearnerUpdateAssignmentAttemptRequestDto, - grade: number + grade: number, ) { const { // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -1795,7 +1795,7 @@ export class AttemptServiceV1 { */ private constructFeedbacksForQuestions( successfulQuestionResponses: CreateQuestionResponseAttemptResponseDto[], - assignment: LearnerGetAssignmentResponseDto + assignment: LearnerGetAssignmentResponseDto, ) { return successfulQuestionResponses.map((feedbackForQuestion) => { const { totalPoints, feedback, ...otherData } = feedbackForQuestion; @@ -1815,7 +1815,7 @@ export class AttemptServiceV1 { const thirtySecondsBeforeNow = new Date(Date.now() - 30 * 1000); if (expiresAt && thirtySecondsBeforeNow > expiresAt) { throw new UnprocessableEntityException( - SUBMISSION_DEADLINE_EXCEPTION_MESSAGE + SUBMISSION_DEADLINE_EXCEPTION_MESSAGE, ); } } @@ -1835,7 +1835,7 @@ export class AttemptServiceV1 { questionAnswerContext: QuestionAnswerContext[]; }, assignmentId: number, - language: string + language: string, ): Promise<{ responseDto: CreateQuestionResponseAttemptResponseDto; learnerResponse: @@ -1845,7 +1845,7 @@ export class AttemptServiceV1 { }> { if ( Array.isArray( - createQuestionResponseAttemptRequestDto.learnerFileResponse + createQuestionResponseAttemptRequestDto.learnerFileResponse, ) && createQuestionResponseAttemptRequestDto.learnerFileResponse.length === 0 && @@ -1859,7 +1859,7 @@ export class AttemptServiceV1 { (createQuestionResponseAttemptRequestDto.learnerPresentationResponse === undefined || (Array.isArray( - createQuestionResponseAttemptRequestDto.learnerPresentationResponse + createQuestionResponseAttemptRequestDto.learnerPresentationResponse, ) && createQuestionResponseAttemptRequestDto.learnerPresentationResponse .length === 0)) @@ -1882,7 +1882,7 @@ export class AttemptServiceV1 { createQuestionResponseAttemptRequestDto, assignmentContext, assignmentId, - language + language, ); } case QuestionType.LINK_FILE: { @@ -1892,7 +1892,7 @@ export class AttemptServiceV1 { createQuestionResponseAttemptRequestDto, assignmentContext, assignmentId, - language + language, ); } else if ( createQuestionResponseAttemptRequestDto.learnerFileResponse @@ -1902,11 +1902,11 @@ export class AttemptServiceV1 { question.type, createQuestionResponseAttemptRequestDto, assignmentContext, - language + language, ); } else { throw new BadRequestException( - "Expected a file-based response (learnerFileResponse) or URL-based response (learnerUrlResponse), but did not receive one." + "Expected a file-based response (learnerFileResponse) or URL-based response (learnerUrlResponse), but did not receive one.", ); } } @@ -1916,21 +1916,21 @@ export class AttemptServiceV1 { question, createQuestionResponseAttemptRequestDto, assignmentContext, - assignmentId + assignmentId, ); } else if (question.responseType === "PRESENTATION") { return this.handleVideoPresentationQuestionResponse( question, createQuestionResponseAttemptRequestDto, assignmentContext, - assignmentId + assignmentId, ); } else { return this.handleFileUploadQuestionResponse( question, question.type, createQuestionResponseAttemptRequestDto, - assignmentContext + assignmentContext, ); } } @@ -1940,28 +1940,28 @@ export class AttemptServiceV1 { createQuestionResponseAttemptRequestDto, assignmentContext, assignmentId, - language + language, ); } case QuestionType.TRUE_FALSE: { return this.handleTrueFalseQuestionResponse( question, createQuestionResponseAttemptRequestDto, - language + language, ); } case QuestionType.SINGLE_CORRECT: { return this.handleSingleCorrectQuestionResponse( question, createQuestionResponseAttemptRequestDto, - language + language, ); } case QuestionType.MULTIPLE_CORRECT: { return this.handleMultipleCorrectQuestionResponse( question, createQuestionResponseAttemptRequestDto, - language + language, ); } default: { @@ -1977,14 +1977,14 @@ export class AttemptServiceV1 { assignmentInstructions: string; questionAnswerContext: QuestionAnswerContext[]; }, - assignmentId: number + assignmentId: number, ): Promise<{ responseDto: CreateQuestionResponseAttemptResponseDto; learnerResponse: LearnerPresentationResponse; }> { if (!createQuestionResponseAttemptRequestDto.learnerPresentationResponse) { throw new BadRequestException( - "Expected a presentation-based response (learnerPresentationResponse), but did not receive one." + "Expected a presentation-based response (learnerPresentationResponse), but did not receive one.", ); } @@ -2000,12 +2000,12 @@ export class AttemptServiceV1 { question.scoring?.type ?? "", question.scoring, question.type, - question.responseType ?? "OTHER" + question.responseType ?? "OTHER", ); const model = await this.llmFacadeService.gradePresentationQuestion( presentationQuestionEvaluateModel, - assignmentId + assignmentId, ); const responseDto = new CreateQuestionResponseAttemptResponseDto(); @@ -2020,14 +2020,14 @@ export class AttemptServiceV1 { assignmentInstructions: string; questionAnswerContext: QuestionAnswerContext[]; }, - assignmentId: number + assignmentId: number, ): Promise<{ responseDto: CreateQuestionResponseAttemptResponseDto; learnerResponse: LearnerPresentationResponse; }> { if (!createQuestionResponseAttemptRequestDto.learnerPresentationResponse) { throw new BadRequestException( - "Expected a presentation-based response (learnerPresentationResponse), but did not receive one." + "Expected a presentation-based response (learnerPresentationResponse), but did not receive one.", ); } @@ -2044,12 +2044,12 @@ export class AttemptServiceV1 { question.scoring, question.type, question.responseType ?? "OTHER", - question.videoPresentationConfig + question.videoPresentationConfig, ); const model = await this.llmFacadeService.gradeVideoPresentationQuestion( videoPresentationQuestionEvaluateModel, - assignmentId + assignmentId, ); const responseDto = new CreateQuestionResponseAttemptResponseDto(); @@ -2066,14 +2066,14 @@ export class AttemptServiceV1 { assignmentInstructions: string; questionAnswerContext: QuestionAnswerContext[]; }, - language?: string + language?: string, ): Promise<{ responseDto: CreateQuestionResponseAttemptResponseDto; learnerResponse: LearnerFileUpload[]; }> { if (!createQuestionResponseAttemptRequestDto.learnerFileResponse) { throw new BadRequestException( - "Expected a file-based response (learnerFileResponse), but did not receive one." + "Expected a file-based response (learnerFileResponse), but did not receive one.", ); } const learnerResponse = @@ -2087,12 +2087,12 @@ export class AttemptServiceV1 { question.scoring?.type ?? "", question.scoring, questionType, - question.responseType ?? "OTHER" + question.responseType ?? "OTHER", ); const model = await this.llmFacadeService.gradeFileBasedQuestion( fileUploadQuestionEvaluateModel, question.assignmentId, - language + language, ); const responseDto = new CreateQuestionResponseAttemptResponseDto(); @@ -2113,14 +2113,14 @@ export class AttemptServiceV1 { questionAnswerContext: QuestionAnswerContext[]; }, assignmentId: number, - language?: string + language?: string, ): Promise<{ responseDto: CreateQuestionResponseAttemptResponseDto; learnerResponse: string; }> { const learnerResponse = await AttemptHelper.validateAndGetTextResponse( questionType, - createQuestionResponseAttemptRequestDto + createQuestionResponseAttemptRequestDto, ); const textBasedQuestionEvaluateModel = new TextBasedQuestionEvaluateModel( @@ -2131,13 +2131,13 @@ export class AttemptServiceV1 { question.totalPoints, question.scoring?.type ?? "", question.scoring, - question.responseType ?? "OTHER" + question.responseType ?? "OTHER", ); const model = await this.llmFacadeService.gradeTextBasedQuestion( textBasedQuestionEvaluateModel, assignmentId, - language + language, ); const responseDto = new CreateQuestionResponseAttemptResponseDto(); @@ -2157,26 +2157,25 @@ export class AttemptServiceV1 { questionAnswerContext: QuestionAnswerContext[]; }, assignmentId: number, - language?: string + language?: string, ): Promise<{ responseDto: CreateQuestionResponseAttemptResponseDto; learnerResponse: string; }> { if (!createQuestionResponseAttemptRequestDto.learnerUrlResponse) { throw new BadRequestException( - "Expected a URL-based response (learnerUrlResponse), but did not receive one." + "Expected a URL-based response (learnerUrlResponse), but did not receive one.", ); } const learnerResponse = createQuestionResponseAttemptRequestDto.learnerUrlResponse; - const urlFetchResponse = await AttemptHelper.fetchPlainTextFromUrl( - learnerResponse - ); + const urlFetchResponse = + await AttemptHelper.fetchPlainTextFromUrl(learnerResponse); if (!urlFetchResponse.isFunctional) { throw new BadRequestException( - `Unable to extract content from the provided URL: ${learnerResponse}` + `Unable to extract content from the provided URL: ${learnerResponse}`, ); } @@ -2190,13 +2189,13 @@ export class AttemptServiceV1 { question.totalPoints, question.scoring?.type ?? "", question.scoring, - question.responseType ?? "OTHER" + question.responseType ?? "OTHER", ); const model = await this.llmFacadeService.gradeUrlBasedQuestion( urlBasedQuestionEvaluateModel, assignmentId, - language + language, ); const responseDto = new CreateQuestionResponseAttemptResponseDto(); @@ -2205,7 +2204,7 @@ export class AttemptServiceV1 { return { responseDto, learnerResponse }; } private getSafeChoices( - choices: Choice[] | string | null | undefined + choices: Choice[] | string | null | undefined, ): string | null { if (!choices) return; return typeof choices === "string" ? choices : JSON.stringify(choices); @@ -2216,7 +2215,7 @@ export class AttemptServiceV1 { private handleTrueFalseQuestionResponse( question: QuestionDto, createQuestionResponseAttemptRequestDto: CreateQuestionResponseAttemptRequestDto, - language: string + language: string, ): { responseDto: CreateQuestionResponseAttemptResponseDto; learnerResponse: string; @@ -2226,14 +2225,14 @@ export class AttemptServiceV1 { createQuestionResponseAttemptRequestDto.learnerAnswerChoice === undefined ) { throw new BadRequestException( - this.getLocalizedString("expectedTrueFalse", language) + this.getLocalizedString("expectedTrueFalse", language), ); } const learnerChoice = createQuestionResponseAttemptRequestDto.learnerAnswerChoice; if (learnerChoice === null) { throw new BadRequestException( - this.getLocalizedString("invalidTrueFalse", language) + this.getLocalizedString("invalidTrueFalse", language), ); } const correctAnswer = question.choices[0].isCorrect; @@ -2264,7 +2263,7 @@ export class AttemptServiceV1 { private async applyTranslationToQuestion( question: QuestionDto, language: string, - variantMapping?: { questionId: number; questionVariant: QuestionVariant } + variantMapping?: { questionId: number; questionVariant: QuestionVariant }, ): Promise { if (!language || language === "en") return question; @@ -2307,7 +2306,7 @@ export class AttemptServiceV1 { if (typeof translation.translatedChoices === "string") { try { question.choices = JSON.parse( - translation.translatedChoices + translation.translatedChoices, ) as Choice[]; } catch { question.choices = []; @@ -2326,11 +2325,11 @@ export class AttemptServiceV1 { */ private formatFeedback( feedbackTemplate: string, - data: { [key: string]: unknown } + data: { [key: string]: unknown }, ): string { return feedbackTemplate.replaceAll( /\${(.*?)}/g, - (_, g: string) => (data[g] as string) || "" + (_, g: string) => (data[g] as string) || "", ); } /** @@ -2339,7 +2338,7 @@ export class AttemptServiceV1 { private handleSingleCorrectQuestionResponse( question: QuestionDto, createQuestionResponseAttemptRequestDto: CreateQuestionResponseAttemptRequestDto, - language: string + language: string, ): { responseDto: CreateQuestionResponseAttemptResponseDto; learnerResponse: string; @@ -2352,7 +2351,7 @@ export class AttemptServiceV1 { const correctChoice = choices.find((choice) => choice.isCorrect); const selectedChoice = choices.find( - (choice) => this.normalizeText(choice.choice) === normalizedLearnerChoice + (choice) => this.normalizeText(choice.choice) === normalizedLearnerChoice, ); const data = { @@ -2402,7 +2401,7 @@ export class AttemptServiceV1 { private handleMultipleCorrectQuestionResponse( question: QuestionDto, createQuestionResponseAttemptRequestDto: CreateQuestionResponseAttemptRequestDto, - language: string + language: string, ): { responseDto: CreateQuestionResponseAttemptResponseDto; learnerResponse: string; @@ -2428,7 +2427,7 @@ export class AttemptServiceV1 { createQuestionResponseAttemptRequestDto.learnerChoices; const normalizedLearnerChoices = new Set( - learnerChoices.map((choice) => this.normalizeText(choice)) + learnerChoices.map((choice) => this.normalizeText(choice)), ); const choices = this.parseChoices(question.choices); @@ -2439,7 +2438,7 @@ export class AttemptServiceV1 { const correctChoices = choices.filter((choice) => choice.isCorrect) || []; const correctChoiceTexts = correctChoices.map((choice) => - this.normalizeText(choice.choice) + this.normalizeText(choice.choice), ); let totalPoints = 0; @@ -2448,7 +2447,7 @@ export class AttemptServiceV1 { for (const learnerChoice of learnerChoices) { const normalizedLearnerChoice = this.normalizeText(learnerChoice); const matchedChoice = normalizedChoices.find( - (item) => item.normalized === normalizedLearnerChoice + (item) => item.normalized === normalizedLearnerChoice, ); if (matchedChoice) { @@ -2461,7 +2460,7 @@ export class AttemptServiceV1 { if (matchedChoice.original.feedback) { choiceFeedback = this.formatFeedback( matchedChoice.original.feedback, - data + data, ); } else { choiceFeedback = matchedChoice.original.isCorrect @@ -2473,19 +2472,19 @@ export class AttemptServiceV1 { feedbackDetails.push( this.getLocalizedString("invalidSelection", language, { learnerChoice, - }) + }), ); } } const maxPoints = correctChoices.reduce( (accumulator, choice) => accumulator + choice.points, - 0 + 0, ); const finalPoints = Math.max(0, Math.min(totalPoints, maxPoints)); const allCorrectSelected = correctChoiceTexts.every((correctText) => - normalizedLearnerChoices.has(correctText) + normalizedLearnerChoices.has(correctText), ); const feedbackMessage = ` @@ -2525,11 +2524,11 @@ export class AttemptServiceV1 { * @returns The time range start date. */ private calculateTimeRangeStartDate( - assignment: LearnerGetAssignmentResponseDto + assignment: LearnerGetAssignmentResponseDto, ): Date { if (assignment.attemptsTimeRangeHours) { return new Date( - Date.now() - assignment.attemptsTimeRangeHours * 60 * 60 * 1000 + Date.now() - assignment.attemptsTimeRangeHours * 60 * 60 * 1000, ); } return new Date(); @@ -2543,7 +2542,7 @@ export class AttemptServiceV1 { */ private async countUserAttempts( userId: string, - assignmentId: number + assignmentId: number, ): Promise { return this.prisma.assignmentAttempt.count({ where: { @@ -2556,7 +2555,7 @@ export class AttemptServiceV1 { private getLocalizedString( key: string, language: string, - placeholders?: { [key: string]: string | number } + placeholders?: { [key: string]: string | number }, ): string { const translations: Record = { en: { @@ -2857,7 +2856,7 @@ export class AttemptServiceV1 { private parseBooleanResponse( learnerChoice: string, - language: string + language: string, ): boolean | null { const mapping: Record> = { en: { true: true, false: false }, @@ -2900,7 +2899,7 @@ export class AttemptServiceV1 { */ private constructQuestionsWithResponses( questions: Question[], - questionResponses: QuestionResponse[] + questionResponses: QuestionResponse[], ): AssignmentAttemptQuestions[] { return questions.map((question) => { const extendedQuestion = question as ExtendedQuestion; @@ -2951,7 +2950,7 @@ export class AttemptServiceV1 { points: choice.points, feedback: choice.feedback, isCorrect: choice.isCorrect, - }) + }), ); return { @@ -2999,7 +2998,7 @@ export class AttemptServiceV1 { assignmentAttemptId: number, role: UserRole, authorQuestions?: QuestionDto[], - assignmentDetails?: Assignment + assignmentDetails?: Assignment, ): Promise<{ assignmentInstructions: string; questionAnswerContext: QuestionAnswerContext[]; @@ -3073,9 +3072,8 @@ export class AttemptServiceV1 { groupedResponses[contextQuestion.id]?.learnerResponse || ""; if (contextQuestion.type === "URL" && learnerResponse) { - const urlContent = await AttemptHelper.fetchPlainTextFromUrl( - learnerResponse - ); + const urlContent = + await AttemptHelper.fetchPlainTextFromUrl(learnerResponse); learnerResponse = JSON.stringify({ url: learnerResponse, ...urlContent, @@ -3086,7 +3084,7 @@ export class AttemptServiceV1 { question: contextQuestion.question, answer: learnerResponse, }; - }) + }), ); } diff --git a/apps/api/src/api/assignment/dto/get.assignment.response.dto.ts b/apps/api/src/api/assignment/dto/get.assignment.response.dto.ts index bc78249e..7d290e81 100644 --- a/apps/api/src/api/assignment/dto/get.assignment.response.dto.ts +++ b/apps/api/src/api/assignment/dto/get.assignment.response.dto.ts @@ -42,7 +42,7 @@ export class GetAssignmentResponseDto extends AssignmentResponseDto { export class LearnerGetAssignmentResponseDto extends OmitType( GetAssignmentResponseDto, - ["questions", "displayOrder"] as const + ["questions", "displayOrder"] as const, ) { currentVersion?: AssignmentVersion; questions: Question[]; diff --git a/apps/api/src/api/assignment/question/question.controller.ts b/apps/api/src/api/assignment/question/question.controller.ts index 982d2731..6d8adb68 100644 --- a/apps/api/src/api/assignment/question/question.controller.ts +++ b/apps/api/src/api/assignment/question/question.controller.ts @@ -44,7 +44,7 @@ export class QuestionController { private logger; constructor( @Inject(WINSTON_MODULE_PROVIDER) private parentLogger: Logger, - private readonly questionService: QuestionService + private readonly questionService: QuestionService, ) { this.logger = parentLogger.child({ context: QuestionController.name }); } @@ -61,11 +61,11 @@ export class QuestionController { @ApiResponse({ status: 403 }) createQuestion( @Param("assignmentId") assignmentId: number, - @Body() createQuestionRequestDto: CreateUpdateQuestionRequestDto + @Body() createQuestionRequestDto: CreateUpdateQuestionRequestDto, ): Promise { return this.questionService.create( Number(assignmentId), - createQuestionRequestDto + createQuestionRequestDto, ); } @@ -92,12 +92,12 @@ export class QuestionController { updateQuestion( @Param("assignmentId") assignmentId: number, @Param("id") id: number, - @Body() updateQuestionRequestDto: CreateUpdateQuestionRequestDto + @Body() updateQuestionRequestDto: CreateUpdateQuestionRequestDto, ): Promise { return this.questionService.update( Number(assignmentId), Number(id), - updateQuestionRequestDto + updateQuestionRequestDto, ); } @@ -114,12 +114,12 @@ export class QuestionController { replaceQuestion( @Param("assignmentId") assignmentId: number, @Param("id") id: number, - @Body() updateQuestionRequestDto: CreateUpdateQuestionRequestDto + @Body() updateQuestionRequestDto: CreateUpdateQuestionRequestDto, ): Promise { return this.questionService.replace( Number(assignmentId), Number(id), - updateQuestionRequestDto + updateQuestionRequestDto, ); } @@ -147,13 +147,13 @@ export class QuestionController { question: QuestionDto; rubricIndex: number; }, - @Req() request: UserSessionRequest + @Req() request: UserSessionRequest, ): Promise { const { question, rubricIndex } = body; return await this.questionService.createMarkingRubric( question, request.userSession.assignmentId, - rubricIndex + rubricIndex, ); } @@ -170,12 +170,12 @@ export class QuestionController { body: { question: QuestionDto; }, - @Req() request: UserSessionRequest + @Req() request: UserSessionRequest, ): Promise { const { question } = body; return await this.questionService.expandMarkingRubric( question, - request.userSession.assignmentId + request.userSession.assignmentId, ); } @@ -194,7 +194,7 @@ export class QuestionController { @Body() body: { liveRecordingData: LearnerLiveRecordingFeedback; - } + }, ): Promise<{ feedback: string }> { const { liveRecordingData } = body; @@ -203,7 +203,7 @@ export class QuestionController { } return this.questionService.getLiveRecordingFeedback( liveRecordingData, - Number(assignmentId) + Number(assignmentId), ); } @Post(":id/translations") @@ -224,7 +224,7 @@ export class QuestionController { selectedLanguage: string; selectedLanguageCode: string; }, - @Req() request: UserSessionRequest + @Req() request: UserSessionRequest, ): Promise<{ translatedQuestion: string; translatedChoices?: Choice[] | { id: number | null; choice: string }[]; @@ -233,7 +233,7 @@ export class QuestionController { const { userSession } = request; const question = await this.questionService.findOneForTranslation( - Number(questionId) + Number(questionId), ); const { translatedQuestion, translatedChoices } = @@ -241,7 +241,7 @@ export class QuestionController { question.assignmentId, question, selectedLanguageCode, - selectedLanguage + selectedLanguage, ); if (userSession.role === UserRole.LEARNER) { diff --git a/apps/api/src/api/assignment/v2/services/assignment.service.ts b/apps/api/src/api/assignment/v2/services/assignment.service.ts index 9df03797..8bd40ccf 100644 --- a/apps/api/src/api/assignment/v2/services/assignment.service.ts +++ b/apps/api/src/api/assignment/v2/services/assignment.service.ts @@ -39,7 +39,7 @@ export class AssignmentServiceV2 { private readonly versionManagementService: VersionManagementService, private readonly jobStatusService: JobStatusServiceV2, private readonly prisma: PrismaService, - @Inject(WINSTON_MODULE_PROVIDER) private parentLogger: Logger + @Inject(WINSTON_MODULE_PROVIDER) private parentLogger: Logger, ) { this.logger = parentLogger.child({ context: "AssignmentServiceV2" }); } @@ -56,17 +56,17 @@ export class AssignmentServiceV2 { async getAssignment( assignmentId: number, userSession: UserSession, - languageCode?: string + languageCode?: string, ): Promise { const assignment = await this.assignmentRepository.findById( assignmentId, - userSession + userSession, ); if (languageCode) { await this.translationService.applyTranslationsToAssignment( assignment, - languageCode + languageCode, ); } @@ -81,7 +81,7 @@ export class AssignmentServiceV2 { */ async listAssignments( - userSession: UserSession + userSession: UserSession, ): Promise { return this.assignmentRepository.findAllForUser(userSession); } @@ -96,13 +96,13 @@ export class AssignmentServiceV2 { async updateAssignment( id: number, - updateDto: UpdateAssignmentRequestDto + updateDto: UpdateAssignmentRequestDto, ): Promise { const existingAssignment = await this.assignmentRepository.findById(id); const shouldTranslate = this.shouldTranslateAssignment( existingAssignment, - updateDto + updateDto, ); const result = await this.assignmentRepository.update(id, updateDto); @@ -130,7 +130,7 @@ export class AssignmentServiceV2 { */ async replaceAssignment( id: number, - replaceDto: ReplaceAssignmentRequestDto + replaceDto: ReplaceAssignmentRequestDto, ): Promise { const result = await this.assignmentRepository.replace(id, replaceDto); @@ -161,14 +161,14 @@ export class AssignmentServiceV2 { async publishAssignment( assignmentId: number, updateDto: UpdateAssignmentQuestionsDto, - userId: string + userId: string, ): Promise<{ jobId: number; message: string }> { this.logger.info( - `📦 PUBLISH REQUEST: Received updateDto with versionNumber: ${updateDto.versionNumber}, versionDescription: ${updateDto.versionDescription}` + `📦 PUBLISH REQUEST: Received updateDto with versionNumber: ${updateDto.versionNumber}, versionDescription: ${updateDto.versionDescription}`, ); const job = await this.jobStatusService.createPublishJob( assignmentId, - userId + userId, ); this.startPublishingProcess(job.id, assignmentId, updateDto, userId).catch( @@ -181,7 +181,7 @@ export class AssignmentServiceV2 { status: "Failed", progress: `Error: ${errorMessage}`, }); - } + }, ); return { @@ -193,7 +193,7 @@ export class AssignmentServiceV2 { jobId: number, assignmentId: number, updateDto: UpdateAssignmentQuestionsDto, - userId: string + userId: string, ): Promise { try { // Progress allocation: @@ -209,14 +209,13 @@ export class AssignmentServiceV2 { percentage: 5, }); - const existingAssignment = await this.assignmentRepository.findById( - assignmentId - ); + const existingAssignment = + await this.assignmentRepository.findById(assignmentId); const assignmentTranslatableFieldsChanged = this.haveTranslatableAssignmentFieldsChanged( existingAssignment, - updateDto + updateDto, ); await this.assignmentRepository.update(assignmentId, { @@ -266,7 +265,7 @@ export class AssignmentServiceV2 { this.logger.warn( `Failed to store assignment author: ${ error instanceof Error ? error.message : "Unknown error" - }` + }`, ); } @@ -284,7 +283,7 @@ export class AssignmentServiceV2 { questionContentChanged = this.haveQuestionContentsChanged( existingQuestions, - updateDto.questions + updateDto.questions, ); await this.jobStatusService.updateJobStatus(jobId, { @@ -306,7 +305,7 @@ export class AssignmentServiceV2 { progress: `Processing questions: ${childProgress}% complete`, percentage: Math.floor(mappedProgress), }); - } + }, ); } @@ -351,12 +350,12 @@ export class AssignmentServiceV2 { } else { // Only do expensive validation if quick check fails this.logger.warn( - `Quick validation failed for assignment ${assignmentId}, running full validation` + `Quick validation failed for assignment ${assignmentId}, running full validation`, ); const languageValidation = await this.translationService.validateAssignmentLanguageConsistency( - assignmentId + assignmentId, ); if (languageValidation.isConsistent) { @@ -368,8 +367,8 @@ export class AssignmentServiceV2 { } else { this.logger.warn( `Language consistency issues detected for assignment ${assignmentId}: ${languageValidation.mismatchedLanguages.join( - ", " - )}` + ", ", + )}`, ); // Language mismatch detected - force retranslation for affected languages @@ -382,7 +381,7 @@ export class AssignmentServiceV2 { await this.translationService.retranslateAssignmentForLanguages( assignmentId, languageValidation.mismatchedLanguages, - jobId + jobId, ); await this.jobStatusService.updateJobStatus(jobId, { @@ -411,13 +410,13 @@ export class AssignmentServiceV2 { const translationCompleteness = await this.translationService.ensureTranslationCompleteness( - assignmentId + assignmentId, ); if (!translationCompleteness.isComplete) { this.logger.warn( `Missing translations detected for assignment ${assignmentId}. Attempting to fix...`, - { missingTranslations: translationCompleteness.missingTranslations } + { missingTranslations: translationCompleteness.missingTranslations }, ); // Attempt to fix missing translations @@ -430,12 +429,12 @@ export class AssignmentServiceV2 { missing.variantId ? `variant ${missing.variantId}` : `question ${missing.questionId}` - }: ${missing.missingLanguages.join(", ")}` + }: ${missing.missingLanguages.join(", ")}`, ); } catch (error) { this.logger.error( `Failed to fix missing translation for question ${missing.questionId}`, - error + error, ); } } @@ -472,7 +471,7 @@ export class AssignmentServiceV2 { `Found ${updatedQuestions.length} questions after processing for assignment ${assignmentId}`, { questionIds: updatedQuestions.map((q) => q.id), - } + }, ); // Create a new version when publishing - AFTER questions are processed and committed @@ -484,7 +483,7 @@ export class AssignmentServiceV2 { try { this.logger.info( - `Managing version after question processing - found ${updatedQuestions.length} questions` + `Managing version after question processing - found ${updatedQuestions.length} questions`, ); const userSession = { @@ -497,7 +496,7 @@ export class AssignmentServiceV2 { const existingDraft = await this.versionManagementService.getUserLatestDraft( assignmentId, - userSession + userSession, ); // Check for recently created unpublished versions (to prevent duplicates from frontend) @@ -516,7 +515,7 @@ export class AssignmentServiceV2 { const draftVersionId = existingDraft._draftVersionId; this.logger.info( `Found existing draft version, publishing it instead of creating new version`, - { draftVersionId } + { draftVersionId }, ); // Update the existing draft with current content first @@ -536,13 +535,13 @@ export class AssignmentServiceV2 { `Published version - ${new Date().toLocaleDateString()}`, versionNumber: updateDto.versionNumber, }, - userSession + userSession, ); // Then publish the updated draft versionResult = await this.versionManagementService.publishVersion( assignmentId, - draftVersionId + draftVersionId, ); } else if ( latestVersion && @@ -555,20 +554,20 @@ export class AssignmentServiceV2 { { versionId: latestVersion.id, versionNumber: latestVersion.versionNumber, - } + }, ); versionResult = await this.versionManagementService.publishVersion( assignmentId, - latestVersion.id + latestVersion.id, ); } else if (!existingDraft && updateDto.published) { // No existing draft and no unpublished version - create new version directly this.logger.info( - `No existing draft or unpublished version found, creating new version directly` + `No existing draft or unpublished version found, creating new version directly`, ); this.logger.info( - `UpdateDto contains versionNumber: ${updateDto.versionNumber}, versionDescription: ${updateDto.versionDescription}` + `UpdateDto contains versionNumber: ${updateDto.versionNumber}, versionDescription: ${updateDto.versionDescription}`, ); versionResult = await this.versionManagementService.createVersion( @@ -581,7 +580,7 @@ export class AssignmentServiceV2 { isDraft: false, // Create as published directly shouldActivate: true, }, - userSession + userSession, ); } else { // Create or update draft version (not publishing) @@ -603,7 +602,7 @@ export class AssignmentServiceV2 { `Draft - ${new Date().toLocaleDateString()}`, versionNumber: updateDto.versionNumber, }, - userSession + userSession, ); } @@ -614,7 +613,7 @@ export class AssignmentServiceV2 { isDraft: versionResult.isDraft, isActive: versionResult.isActive, published: versionResult.published, - } + }, ); } catch (versionError) { // Log the full error details @@ -630,7 +629,7 @@ export class AssignmentServiceV2 { assignmentId, userId, questionsFound: updatedQuestions.length, - } + }, ); } @@ -649,7 +648,7 @@ export class AssignmentServiceV2 { const errorStack = error instanceof Error ? error.stack : undefined; this.logger.error( `Publishing process failed: ${errorMessage}`, - errorStack + errorStack, ); await this.jobStatusService.updateJobStatus(jobId, { status: "Failed", @@ -661,7 +660,7 @@ export class AssignmentServiceV2 { private safeStringCompare = ( string1: string | null | undefined, - string2: string | null | undefined + string2: string | null | undefined, ): boolean => { const normalizedString1 = string1 === null || string1 === undefined ? "" : String(string1); @@ -677,11 +676,11 @@ export class AssignmentServiceV2 { existingAssignment: | GetAssignmentResponseDto | LearnerGetAssignmentResponseDto, - updateDto: UpdateAssignmentRequestDto | UpdateAssignmentQuestionsDto + updateDto: UpdateAssignmentRequestDto | UpdateAssignmentQuestionsDto, ): boolean { if (existingAssignment.graded !== updateDto.graded) { this.logger.debug( - "Graded status changed, but this doesn't trigger translation" + "Graded status changed, but this doesn't trigger translation", ); } @@ -695,7 +694,7 @@ export class AssignmentServiceV2 { updateDto.instructions !== null && !this.safeStringCompare( existingAssignment.instructions, - updateDto.instructions + updateDto.instructions, ); const introductionChanged = @@ -703,7 +702,7 @@ export class AssignmentServiceV2 { updateDto.introduction !== null && !this.safeStringCompare( existingAssignment.introduction, - updateDto.introduction + updateDto.introduction, ); const gradingCriteriaChanged = @@ -711,7 +710,7 @@ export class AssignmentServiceV2 { updateDto.gradingCriteriaOverview !== null && !this.safeStringCompare( existingAssignment.gradingCriteriaOverview, - updateDto.gradingCriteriaOverview + updateDto.gradingCriteriaOverview, ); if ( nameChanged || @@ -741,17 +740,17 @@ export class AssignmentServiceV2 { */ private haveQuestionContentsChanged( existingQuestions: QuestionDto[], - updatedQuestions: QuestionDto[] + updatedQuestions: QuestionDto[], ): boolean { if (existingQuestions.length !== updatedQuestions.length) { this.logger.debug( - `Question count changed: ${existingQuestions.length} → ${updatedQuestions.length}` + `Question count changed: ${existingQuestions.length} → ${updatedQuestions.length}`, ); return true; } this.logger.debug( - `Comparing ${existingQuestions.length} questions for content changes` + `Comparing ${existingQuestions.length} questions for content changes`, ); const existingQuestionsMap = new Map(); @@ -784,7 +783,7 @@ export class AssignmentServiceV2 { if ( !this.safeStringCompare( updatedQuestion.question, - existingQuestion.question + existingQuestion.question, ) ) { this.logger.debug(`Question #${updatedQuestion.id} text changed`); @@ -793,14 +792,14 @@ export class AssignmentServiceV2 { if (updatedQuestion.type !== existingQuestion.type) { this.logger.debug( - `Question #${updatedQuestion.id} type changed: ${existingQuestion.type} → ${updatedQuestion.type}` + `Question #${updatedQuestion.id} type changed: ${existingQuestion.type} → ${updatedQuestion.type}`, ); return true; } const choicesEqual = this.areChoicesEqual( updatedQuestion.choices, - existingQuestion.choices + existingQuestion.choices, ); if (!choicesEqual) { this.logger.debug(`Question #${updatedQuestion.id} choices changed`); @@ -810,7 +809,7 @@ export class AssignmentServiceV2 { const variantsChanged = this.haveVariantsChanged( existingQuestion.variants, updatedQuestion.variants, - updatedQuestion.id + updatedQuestion.id, ); if (variantsChanged) { @@ -820,13 +819,13 @@ export class AssignmentServiceV2 { if (updatedQuestion.totalPoints !== existingQuestion.totalPoints) { this.logger.debug( - `Question #${updatedQuestion.id} points changed: ${existingQuestion.totalPoints} → ${updatedQuestion.totalPoints} (non-translatable)` + `Question #${updatedQuestion.id} points changed: ${existingQuestion.totalPoints} → ${updatedQuestion.totalPoints} (non-translatable)`, ); } if (updatedQuestion.maxWords !== existingQuestion.maxWords) { this.logger.debug( - `Question #${updatedQuestion.id} maxWords changed: ${existingQuestion.maxWords} → ${updatedQuestion.maxWords} (non-translatable)` + `Question #${updatedQuestion.id} maxWords changed: ${existingQuestion.maxWords} → ${updatedQuestion.maxWords} (non-translatable)`, ); } } @@ -841,7 +840,7 @@ export class AssignmentServiceV2 { private haveVariantsChanged( variants1?: VariantDto[], variants2?: VariantDto[], - questionId?: number + questionId?: number, ): boolean { const logPrefix = questionId ? `Question #${questionId} variants: ` @@ -849,28 +848,28 @@ export class AssignmentServiceV2 { if (!variants1 && !variants2) { this.logger.debug( - `${logPrefix}Both variant arrays are null/undefined (no change)` + `${logPrefix}Both variant arrays are null/undefined (no change)`, ); return false; } if (!variants1 || !variants2) { this.logger.debug( - `${logPrefix}One variant array is null/undefined (change detected)` + `${logPrefix}One variant array is null/undefined (change detected)`, ); return true; } if (variants1.length !== variants2.length) { this.logger.debug( - `${logPrefix}Variant count changed: ${variants1.length} → ${variants2.length}` + `${logPrefix}Variant count changed: ${variants1.length} → ${variants2.length}`, ); return true; } if (variants1.length === 0) { this.logger.debug( - `${logPrefix}Both variant arrays are empty (no change)` + `${logPrefix}Both variant arrays are empty (no change)`, ); return false; } @@ -878,10 +877,10 @@ export class AssignmentServiceV2 { this.logger.debug(`${logPrefix}Comparing ${variants1.length} variants`); const sortedVariants1 = [...variants1].sort( - (a, b) => (a.id || 0) - (b.id || 0) + (a, b) => (a.id || 0) - (b.id || 0), ); const sortedVariants2 = [...variants2].sort( - (a, b) => (a.id || 0) - (b.id || 0) + (a, b) => (a.id || 0) - (b.id || 0), ); for (const [index, v1] of sortedVariants1.entries()) { @@ -890,7 +889,7 @@ export class AssignmentServiceV2 { this.logger.debug(`${logPrefix}Comparing variant #${index + 1}: Content: "${v1.variantContent.slice( 0, - 30 + 30, )}..." → "${v2.variantContent.slice(0, 30)}..." Choices Count: ${v1.choices?.length || 0} → ${v2.choices?.length || 0} `); @@ -919,10 +918,10 @@ export class AssignmentServiceV2 { if (!choices1 || !choices2) return false; if (choices1.length !== choices2.length) return false; const sortedChoices1 = [...choices1].sort( - (a, b) => (a.id || 0) - (b.id || 0) + (a, b) => (a.id || 0) - (b.id || 0), ); const sortedChoices2 = [...choices2].sort( - (a, b) => (a.id || 0) - (b.id || 0) + (a, b) => (a.id || 0) - (b.id || 0), ); for (const [index, c1] of sortedChoices1.entries()) { const c2 = sortedChoices2[index]; @@ -950,7 +949,7 @@ export class AssignmentServiceV2 { existingAssignment: | GetAssignmentResponseDto | LearnerGetAssignmentResponseDto, - updateDto: UpdateAssignmentRequestDto + updateDto: UpdateAssignmentRequestDto, ): boolean { return ( (updateDto.name && updateDto.name !== existingAssignment.name) || diff --git a/apps/api/src/api/assignment/v2/services/draft-management.service.ts b/apps/api/src/api/assignment/v2/services/draft-management.service.ts index 11e6c794..04cd2152 100644 --- a/apps/api/src/api/assignment/v2/services/draft-management.service.ts +++ b/apps/api/src/api/assignment/v2/services/draft-management.service.ts @@ -63,13 +63,13 @@ export class DraftManagementService { constructor( private readonly prisma: PrismaService, - @Inject(WINSTON_MODULE_PROVIDER) private parentLogger: Logger + @Inject(WINSTON_MODULE_PROVIDER) private parentLogger: Logger, ) { this.logger = parentLogger.child({ context: "DraftManagementService" }); } private parseDisplayOrder( - value: any + value: any, ): AssignmentQuestionDisplayOrder | undefined { if (value === "DEFINED" || value === "RANDOM") { return value as AssignmentQuestionDisplayOrder; @@ -87,7 +87,7 @@ export class DraftManagementService { async saveDraft( assignmentId: number, saveDraftDto: SaveDraftDto, - userSession: UserSession + userSession: UserSession, ): Promise { this.logger.info(`Saving draft for assignment ${assignmentId}`, { userId: userSession.userId, @@ -157,7 +157,7 @@ export class DraftManagementService { assignment.displayOrder, questionDisplay: this.parseQuestionDisplay( - saveDraftDto.assignmentData?.questionDisplay + saveDraftDto.assignmentData?.questionDisplay, ) ?? assignment.questionDisplay, numberOfQuestionsPerAttempt: saveDraftDto.assignmentData?.numberOfQuestionsPerAttempt ?? @@ -192,7 +192,7 @@ export class DraftManagementService { { draftId: assignmentDraft.id, userId: userSession.userId, - } + }, ); return { @@ -210,7 +210,7 @@ export class DraftManagementService { async updateDraft( draftId: number, saveDraftDto: SaveDraftDto, - userSession: UserSession + userSession: UserSession, ): Promise { this.logger.info(`Updating draft ${draftId}`, { userId: userSession.userId, @@ -283,12 +283,12 @@ export class DraftManagementService { }), ...(saveDraftDto.assignmentData?.displayOrder !== undefined && { displayOrder: this.parseDisplayOrder( - saveDraftDto.assignmentData.displayOrder + saveDraftDto.assignmentData.displayOrder, ), }), ...(saveDraftDto.assignmentData?.questionDisplay !== undefined && { questionDisplay: this.parseQuestionDisplay( - saveDraftDto.assignmentData.questionDisplay + saveDraftDto.assignmentData.questionDisplay, ), }), ...(saveDraftDto.assignmentData?.numberOfQuestionsPerAttempt !== @@ -335,7 +335,7 @@ export class DraftManagementService { async listUserDrafts( assignmentId: number, - userSession: UserSession + userSession: UserSession, ): Promise { // Verify assignment access // await this.verifyAssignmentAccess(assignmentId, userSession); @@ -362,7 +362,7 @@ export class DraftManagementService { async getDraft( draftId: number, - userSession: UserSession + userSession: UserSession, ): Promise<{ id: number; name: string; @@ -467,7 +467,7 @@ export class DraftManagementService { async getLatestDraft( assignmentId: number, - userSession: UserSession + userSession: UserSession, ): Promise<{ id: number; name: string; @@ -520,7 +520,7 @@ export class DraftManagementService { private async verifyAssignmentAccess( assignmentId: number, - userSession: UserSession + userSession: UserSession, ) { const assignment = await this.prisma.assignment.findUnique({ where: { id: assignmentId }, @@ -533,7 +533,7 @@ export class DraftManagementService { if (userSession.role === UserRole.AUTHOR) { const hasAccess = assignment.AssignmentAuthor.some( - (author) => author.userId === userSession.userId + (author) => author.userId === userSession.userId, ); if (!hasAccess) { throw new NotFoundException("Assignment not found"); diff --git a/apps/api/src/api/assignment/v2/services/version-management.service.ts b/apps/api/src/api/assignment/v2/services/version-management.service.ts index 07c012f7..8e5dfd62 100644 --- a/apps/api/src/api/assignment/v2/services/version-management.service.ts +++ b/apps/api/src/api/assignment/v2/services/version-management.service.ts @@ -92,7 +92,7 @@ export class VersionManagementService { constructor( private readonly prisma: PrismaService, - @Inject(WINSTON_MODULE_PROVIDER) private parentLogger: Logger + @Inject(WINSTON_MODULE_PROVIDER) private parentLogger: Logger, ) { this.logger = parentLogger.child({ context: "VersionManagementService" }); } @@ -137,14 +137,14 @@ export class VersionManagementService { async createVersion( assignmentId: number, createVersionDto: CreateVersionDto, - userSession: UserSession + userSession: UserSession, ): Promise { this.logger.info( `🚀 CREATE VERSION: Starting for assignment ${assignmentId}`, { createVersionDto, userId: userSession.userId, - } + }, ); // Log the key decision factors @@ -178,7 +178,7 @@ export class VersionManagementService { // If no legacy questions found, this might be a new assignment created through version control if (assignment.questions.length === 0) { this.logger.warn( - `No legacy questions found for assignment ${assignmentId}. Creating version with empty questions.` + `No legacy questions found for assignment ${assignmentId}. Creating version with empty questions.`, ); } @@ -190,7 +190,7 @@ export class VersionManagementService { const semanticVersionRegex = /^\d+\.\d+\.\d+(?:-rc\d+)?$/; if (!semanticVersionRegex.test(createVersionDto.versionNumber)) { throw new BadRequestException( - "Version number must follow semantic versioning format (e.g., '1.0.0' or '1.0.0-rc1')" + "Version number must follow semantic versioning format (e.g., '1.0.0' or '1.0.0-rc1')", ); } versionNumber = createVersionDto.versionNumber; @@ -230,13 +230,13 @@ export class VersionManagementService { versionId: createVersionDto.versionId, versionNumber: createVersionDto.versionNumber, versionDescription: createVersionDto.versionDescription, - } + }, ); return await this.updateExistingVersion( assignmentId, createVersionDto.versionId, createVersionDto, - userSession + userSession, ); } @@ -257,13 +257,13 @@ export class VersionManagementService { if (createVersionDto.updateExisting) { // Update existing version instead of creating new one (fallback to version number lookup) this.logger.info( - `Updating existing version ${existingVersion.id} found by version number ${finalVersionNumber}` + `Updating existing version ${existingVersion.id} found by version number ${finalVersionNumber}`, ); return await this.updateExistingVersion( assignmentId, existingVersion.id, createVersionDto, - userSession + userSession, ); } else { // Return a special response indicating the version exists @@ -333,7 +333,7 @@ export class VersionManagementService { // Create question versions this.logger.info( - `Creating ${assignment.questions.length} question versions for assignment version ${assignmentVersion.id}` + `Creating ${assignment.questions.length} question versions for assignment version ${assignmentVersion.id}`, ); for (const [index, question] of assignment.questions.entries()) { @@ -361,12 +361,12 @@ export class VersionManagementService { this.logger.debug( `Created question version ${questionVersion.id} for question ${ question.id - } (${question.question.slice(0, 50)}...)` + } (${question.question.slice(0, 50)}...)`, ); } this.logger.info( - `Successfully created all ${assignment.questions.length} question versions` + `Successfully created all ${assignment.questions.length} question versions`, ); // Update current version if should activate @@ -407,7 +407,7 @@ export class VersionManagementService { originalVersionNumber: wasAutoIncremented ? originalVersionNumber : undefined, - } + }, ); return { @@ -463,7 +463,7 @@ export class VersionManagementService { async getVersion( assignmentId: number, - versionId: number + versionId: number, ): Promise< AssignmentVersion & { questionVersions: any[]; @@ -489,7 +489,7 @@ export class VersionManagementService { questionId: qv.questionId, question: qv.question?.slice(0, 50) + "...", })), - } + }, ); // Fetch variants for each question that has a questionId @@ -584,7 +584,7 @@ export class VersionManagementService { async saveDraft( assignmentId: number, saveDraftDto: SaveDraftDto, - userSession: UserSession + userSession: UserSession, ): Promise { this.logger.info(`Saving draft for assignment ${assignmentId}`, { userId: userSession.userId, @@ -608,13 +608,13 @@ export class VersionManagementService { async restoreVersion( assignmentId: number, restoreVersionDto: RestoreVersionDto, - userSession: UserSession + userSession: UserSession, ): Promise { this.logger.info( `Restoring version ${restoreVersionDto.versionId} for assignment ${assignmentId}`, { userId: userSession.userId, - } + }, ); const versionToRestore = await this.prisma.assignmentVersion.findUnique({ @@ -631,7 +631,7 @@ export class VersionManagementService { // Create new version from restored data const nextVersionNumber = await this.getNextVersionNumber( assignmentId, - tx + tx, ); const restoredVersion = await tx.assignmentVersion.create({ @@ -732,13 +732,13 @@ export class VersionManagementService { assignmentId, restoreVersionDto.versionId, userSession, - tx + tx, ); } else { // For regular versions, check if published before activation if (!versionToRestore.published) { throw new BadRequestException( - `Version ${versionToRestore.versionNumber} cannot be activated because it has not been published yet. Please publish the version first before activating it.` + `Version ${versionToRestore.versionNumber} cannot be activated because it has not been published yet. Please publish the version first before activating it.`, ); } @@ -787,7 +787,7 @@ export class VersionManagementService { async publishVersion( assignmentId: number, - versionId: number + versionId: number, ): Promise { const version = await this.prisma.assignmentVersion.findUnique({ where: { id: versionId, assignmentId }, @@ -796,13 +796,13 @@ export class VersionManagementService { if (!version) { throw new NotFoundException( - `Version with ID ${versionId} not found for assignment ${assignmentId}` + `Version with ID ${versionId} not found for assignment ${assignmentId}`, ); } if (version.published) { throw new BadRequestException( - `Version ${version.versionNumber} is already published` + `Version ${version.versionNumber} is already published`, ); } @@ -824,7 +824,7 @@ export class VersionManagementService { if (existingPublishedVersion) { // Auto-increment patch version to resolve conflict const versionMatch = publishedVersionNumber.match( - /^(\d+)\.(\d+)\.(\d+)$/ + /^(\d+)\.(\d+)\.(\d+)$/, ); if (versionMatch) { const [, major, minor, patch] = versionMatch; @@ -847,11 +847,11 @@ export class VersionManagementService { publishedVersionNumber = newVersionNumber; this.logger.info( - `Resolved version conflict by incrementing patch: ${version.versionNumber} → ${publishedVersionNumber}` + `Resolved version conflict by incrementing patch: ${version.versionNumber} → ${publishedVersionNumber}`, ); } else { throw new ConflictException( - `Published version ${publishedVersionNumber} already exists and version format is not recognizable.` + `Published version ${publishedVersionNumber} already exists and version format is not recognizable.`, ); } } @@ -881,7 +881,7 @@ export class VersionManagementService { this.logger.info( `Successfully published version: ${originalVersionNumber} → ${publishedVersionNumber}${ wasAutoIncremented ? " (auto-incremented)" : "" - }` + }`, ); return { @@ -904,7 +904,7 @@ export class VersionManagementService { async compareVersions( assignmentId: number, - compareDto: CompareVersionsDto + compareDto: CompareVersionsDto, ): Promise { const [fromVersion, toVersion] = await Promise.all([ this.prisma.assignmentVersion.findUnique({ @@ -923,11 +923,11 @@ export class VersionManagementService { const assignmentChanges = this.compareAssignmentData( fromVersion, - toVersion + toVersion, ); const questionChanges = this.compareQuestionData( fromVersion.questionVersions, - toVersion.questionVersions + toVersion.questionVersions, ); return { @@ -975,7 +975,7 @@ export class VersionManagementService { // Private helper methods private async updateExistingDraft( draftId: number, - saveDraftDto: SaveDraftDto + saveDraftDto: SaveDraftDto, ): Promise { return await this.prisma.$transaction(async (tx) => { const updatedDraft = await tx.assignmentVersion.update({ @@ -1058,7 +1058,7 @@ export class VersionManagementService { assignmentId: number, versionId: number, updateData: CreateVersionDto, - userSession: UserSession + userSession: UserSession, ): Promise { this.logger.info( `🔄 UPDATE EXISTING VERSION: Starting update for version ${versionId} on assignment ${assignmentId}`, @@ -1067,7 +1067,7 @@ export class VersionManagementService { assignmentId, updateData, userId: userSession.userId, - } + }, ); // Get current assignment data @@ -1175,7 +1175,7 @@ export class VersionManagementService { }); this.logger.info( - `✅ UPDATE EXISTING VERSION: Successfully updated version ${updatedVersion.versionNumber} (ID: ${updatedVersion.id}) for assignment ${assignmentId}` + `✅ UPDATE EXISTING VERSION: Successfully updated version ${updatedVersion.versionNumber} (ID: ${updatedVersion.id}) for assignment ${assignmentId}`, ); return { @@ -1194,7 +1194,7 @@ export class VersionManagementService { private async getNextVersionNumber( assignmentId: number, - tx: any + tx: any, ): Promise { const lastVersion = await tx.assignmentVersion.findFirst({ where: { assignmentId }, @@ -1240,8 +1240,8 @@ export class VersionManagementService { from[field] === null ? "added" : to[field] === null - ? "removed" - : "modified", + ? "removed" + : "modified", }); } } @@ -1253,7 +1253,7 @@ export class VersionManagementService { // Create maps for easier comparison const fromMap = new Map( - fromQuestions.map((q) => [q.questionId || q.id, q]) + fromQuestions.map((q) => [q.questionId || q.id, q]), ); const toMap = new Map(toQuestions.map((q) => [q.questionId || q.id, q])); @@ -1323,7 +1323,7 @@ export class VersionManagementService { private async verifyAssignmentAccess( assignmentId: number, - userSession: UserSession + userSession: UserSession, ) { const assignment = await this.prisma.assignment.findUnique({ where: { id: assignmentId }, @@ -1336,7 +1336,7 @@ export class VersionManagementService { if (userSession.role === UserRole.AUTHOR) { const hasAccess = assignment.AssignmentAuthor.some( - (author) => author.userId === userSession.userId + (author) => author.userId === userSession.userId, ); if (!hasAccess) { throw new NotFoundException("Assignment not found"); @@ -1347,7 +1347,7 @@ export class VersionManagementService { private async createDraftVersion( assignmentId: number, saveDraftDto: SaveDraftDto, - userSession: UserSession + userSession: UserSession, ): Promise { // Get the base assignment for reference const assignment = await this.prisma.assignment.findUnique({ @@ -1374,7 +1374,7 @@ export class VersionManagementService { if (rcMatch) { const baseVersion = lastVersion.versionNumber.replace( /-rc\d+$/, - "" + "", ); const rcNumber = Number.parseInt(rcMatch[1], 10) + 1; nextVersionNumber = `${baseVersion}-rc${rcNumber}`; @@ -1492,7 +1492,7 @@ export class VersionManagementService { async getUserLatestDraft( assignmentId: number, - userSession: UserSession + userSession: UserSession, ): Promise<{ questions: any[]; _isDraftVersion: boolean; @@ -1593,13 +1593,13 @@ export class VersionManagementService { assignmentId: number, versionId: number, questionIds: number[], - userSession: UserSession + userSession: UserSession, ): Promise { this.logger.info( `Restoring deleted questions ${questionIds.join( - ", " + ", ", )} from version ${versionId} for assignment ${assignmentId}`, - { userId: userSession.userId } + { userId: userSession.userId }, ); // await this.verifyAssignmentAccess(assignmentId, userSession); @@ -1636,7 +1636,7 @@ export class VersionManagementService { // Create a new draft version const nextVersionNumber = await this.getNextVersionNumber( assignmentId, - tx + tx, ); targetDraft = await tx.assignmentVersion.create({ data: { @@ -1721,7 +1721,7 @@ export class VersionManagementService { }); this.logger.info( - `Successfully restored ${questionIds.length} questions from version ${versionId} to draft ${targetDraft.id}` + `Successfully restored ${questionIds.length} questions from version ${versionId} to draft ${targetDraft.id}`, ); return { @@ -1742,7 +1742,7 @@ export class VersionManagementService { assignmentId: number, versionId: number, versionDescription: string, - userSession: UserSession + userSession: UserSession, ): Promise { this.logger.info(`Updating version description for version ${versionId}`, { assignmentId, @@ -1772,7 +1772,7 @@ export class VersionManagementService { }); this.logger.info( - `Successfully updated version description for version ${versionId}` + `Successfully updated version description for version ${versionId}`, ); return { @@ -1792,7 +1792,7 @@ export class VersionManagementService { assignmentId: number, versionId: number, versionNumber: string, - userSession: UserSession + userSession: UserSession, ): Promise { this.logger.info(`Updating version number for version ${versionId}`, { assignmentId, @@ -1824,7 +1824,7 @@ export class VersionManagementService { if (existingVersion) { throw new BadRequestException( - `Version number "${versionNumber}" already exists for this assignment` + `Version number "${versionNumber}" already exists for this assignment`, ); } @@ -1838,7 +1838,7 @@ export class VersionManagementService { }); this.logger.info( - `Successfully updated version number for version ${versionId} to ${versionNumber}` + `Successfully updated version number for version ${versionId} to ${versionNumber}`, ); return { @@ -1857,7 +1857,7 @@ export class VersionManagementService { async deleteVersion( assignmentId: number, versionId: number, - userSession: UserSession + userSession: UserSession, ): Promise { this.logger.info( `Deleting version ${versionId} for assignment ${assignmentId}`, @@ -1865,7 +1865,7 @@ export class VersionManagementService { assignmentId, versionId, userId: userSession.userId, - } + }, ); // First, verify the version exists and belongs to the assignment @@ -1915,7 +1915,7 @@ export class VersionManagementService { assignmentId: number, rcVersionId: number, userSession: UserSession, - tx?: Prisma.TransactionClient + tx?: Prisma.TransactionClient, ): Promise { const prisma = tx || this.prisma; @@ -1925,7 +1925,7 @@ export class VersionManagementService { assignmentId, rcVersionId, userId: userSession.userId, - } + }, ); // Get the RC version @@ -1978,7 +1978,7 @@ export class VersionManagementService { finalVersionNumber = newVersionNumber; this.logger.info( - `Resolved version conflict by incrementing patch: ${rcVersion.versionNumber} → ${finalVersionNumber}` + `Resolved version conflict by incrementing patch: ${rcVersion.versionNumber} → ${finalVersionNumber}`, ); } } @@ -2060,7 +2060,7 @@ export class VersionManagementService { assignmentData: Assignment; questionsData?: Question[]; }, - userSession: UserSession + userSession: UserSession, ): Promise { // Verify assignment access const assignment = await this.prisma.assignment.findUnique({ @@ -2082,7 +2082,7 @@ export class VersionManagementService { if (existingVersion) { throw new ConflictException( - `Version ${draftData.versionNumber} already exists for this assignment` + `Version ${draftData.versionNumber} already exists for this assignment`, ); } diff --git a/apps/api/src/api/assignment/v2/tests/unit/__mocks__/ common-mocks.ts b/apps/api/src/api/assignment/v2/tests/unit/__mocks__/ common-mocks.ts index 550342d4..a4146eb5 100644 --- a/apps/api/src/api/assignment/v2/tests/unit/__mocks__/ common-mocks.ts +++ b/apps/api/src/api/assignment/v2/tests/unit/__mocks__/ common-mocks.ts @@ -612,7 +612,7 @@ const toJsonValue = (value: unknown): Prisma.JsonValue => { */ export const createMockQuestion = ( overrides: Partial = {}, - questionType: QuestionType = QuestionType.SINGLE_CORRECT + questionType: QuestionType = QuestionType.SINGLE_CORRECT, ): Question => { const baseQuestion: Question = { id: 1, @@ -673,7 +673,7 @@ export const createMockQuestion = ( */ export const createMockQuestionDto = ( overrides: Partial = {}, - questionType: QuestionType = QuestionType.SINGLE_CORRECT + questionType: QuestionType = QuestionType.SINGLE_CORRECT, ): QuestionDto => { const baseQuestionDto: QuestionDto = { id: 1, @@ -735,7 +735,7 @@ export const createMockQuestionDto = ( export const createMockQuestionVariant = ( overrides: Partial = {}, questionId = 1, - variantType: VariantType = VariantType.REWORDED + variantType: VariantType = VariantType.REWORDED, ): QuestionVariant => { const baseVariant: QuestionVariant = { id: 101, @@ -765,7 +765,7 @@ export const createMockQuestionVariant = ( export const createMockVariantDto = ( overrides: Partial = {}, _questionId = 1, - variantType: VariantType = VariantType.REWORDED + variantType: VariantType = VariantType.REWORDED, ): VariantDto => { const baseVariantDto: VariantDto = { id: 101, @@ -792,7 +792,7 @@ export const createMockVariantDto = ( export const createReactQuestionDto = ( type: QuestionType, id: number = Math.floor(Math.random() * 1_000_000_000), - overrides: Partial = {} + overrides: Partial = {}, ): QuestionDto => { const baseQuestion: Partial = { id, @@ -940,7 +940,7 @@ export const createReactQuestionDto = ( * Pass a partial object to override only the fields you care about. */ export const createMockAssignment = ( - overrides: Partial = {} + overrides: Partial = {}, ): Assignment => ({ id: 1, name: "Sample Assignment", @@ -981,7 +981,7 @@ export const createMockGetAssignmentResponseDto = ( questions: Question[] = [ createMockQuestion(), createMockQuestion({ id: 2 }, QuestionType.MULTIPLE_CORRECT), - ] + ], ): GetAssignmentResponseDto => { const assignment = createMockAssignment(); @@ -997,7 +997,7 @@ export const createMockGetAssignmentResponseDto = ( * Create a sample LearnerGetAssignmentResponseDto with sane defaults */ export const createMockLearnerGetAssignmentResponseDto = ( - overrides: Partial = {} + overrides: Partial = {}, ): LearnerGetAssignmentResponseDto => { const assignment = createMockAssignment(); @@ -1013,7 +1013,7 @@ export const createMockLearnerGetAssignmentResponseDto = ( * Create a sample AssignmentResponseDto with sane defaults */ export const createMockAssignmentResponseDto = ( - overrides: Partial = {} + overrides: Partial = {}, ): AssignmentResponseDto => { const assignment = createMockAssignment(); @@ -1027,7 +1027,7 @@ export const createMockAssignmentResponseDto = ( * Create a sample BaseAssignmentResponseDto with sane defaults */ export const createMockBaseAssignmentResponseDto = ( - overrides: Partial = {} + overrides: Partial = {}, ): BaseAssignmentResponseDto => { return { id: 1, @@ -1042,7 +1042,7 @@ export const createMockBaseAssignmentResponseDto = ( * but you can override any subset of properties. */ export const createMockUpdateAssignmentDto = ( - overrides: Partial = {} + overrides: Partial = {}, ): UpdateAssignmentRequestDto => ({ name: "Updated Assignment", introduction: "New intro", @@ -1074,7 +1074,7 @@ export const createMockUpdateAssignmentDto = ( * Create a sample ReplaceAssignmentRequestDto with sane defaults */ export const createMockReplaceAssignmentDto = ( - overrides: Partial = {} + overrides: Partial = {}, ): ReplaceAssignmentRequestDto => ({ introduction: "Completely new introduction", instructions: "Completely new instructions", @@ -1099,7 +1099,7 @@ export const createMockReplaceAssignmentDto = ( */ export const createMockUpdateAssignmentQuestionsDto = ( overrides: Partial = {}, - includeQuestions = true + includeQuestions = true, ): UpdateAssignmentQuestionsDto => { const baseDto: UpdateAssignmentQuestionsDto = { name: "Updated Assignment with Questions", @@ -1147,7 +1147,7 @@ export const createMockUpdateAssignmentQuestionsDto = ( */ export const createMockAssignmentAttempt = ( overrides: Partial = {}, - isCompleted = false + isCompleted = false, ): AssignmentAttempt => { const baseAttempt: AssignmentAttempt = { id: 1, @@ -1175,7 +1175,7 @@ export const createMockAssignmentAttempt = ( export const createMockQuestionResponse = ( overrides: Partial = {}, questionId = 1, - isGraded = true + isGraded = true, ): QuestionResponse => { const baseResponse: QuestionResponse = { id: 1, @@ -1205,7 +1205,7 @@ export const createMockQuestionResponse = ( */ export const createMockJob = ( overrides: Partial = {}, - status = "Pending" + status = "Pending", ): Job => { const baseJob: Job = { id: 1, @@ -1235,7 +1235,7 @@ export const createMockJob = ( */ export const createMockTranslation = ( overrides: Partial = {}, - languageCode = "fr" + languageCode = "fr", ): Translation => { const baseTranslation: Translation = { id: 1, @@ -1246,8 +1246,8 @@ export const createMockTranslation = ( languageCode === "fr" ? "Quelle est la capitale de la France?" : languageCode === "es" - ? "¿Cuál es la capital de Francia?" - : "What is the capital of France?", + ? "¿Cuál es la capital de Francia?" + : "What is the capital of France?", untranslatedText: "What is the capital of France?", translatedChoices: JSON.stringify([ { @@ -1256,8 +1256,8 @@ export const createMockTranslation = ( languageCode === "fr" ? "Paris" : languageCode === "es" - ? "París" - : "Paris", + ? "París" + : "Paris", }, { ...sampleChoiceB, @@ -1265,8 +1265,8 @@ export const createMockTranslation = ( languageCode === "fr" ? "Londres" : languageCode === "es" - ? "Londres" - : "London", + ? "Londres" + : "London", }, { ...sampleChoiceC, @@ -1274,8 +1274,8 @@ export const createMockTranslation = ( languageCode === "fr" ? "Berlin" : languageCode === "es" - ? "Berlín" - : "Berlin", + ? "Berlín" + : "Berlin", }, ]), untranslatedChoices: JSON.stringify([ @@ -1297,7 +1297,7 @@ export const createMockTranslation = ( */ export const createMockAssignmentTranslation = ( overrides: Partial = {}, - languageCode = "fr" + languageCode = "fr", ): AssignmentTranslation => { const baseAssignmentTranslation: AssignmentTranslation = { id: 1, @@ -1311,26 +1311,26 @@ export const createMockAssignmentTranslation = ( languageCode === "fr" ? "Exemple de devoir" : languageCode === "es" - ? "Ejemplo de tarea" - : "Sample Assignment", + ? "Ejemplo de tarea" + : "Sample Assignment", translatedIntroduction: languageCode === "fr" ? "Texte d'introduction" : languageCode === "es" - ? "Texto introductorio" - : "Intro text", + ? "Texto introductorio" + : "Intro text", translatedInstructions: languageCode === "fr" ? "Faites le travail" : languageCode === "es" - ? "Haz el trabajo" - : "Do the work", + ? "Haz el trabajo" + : "Do the work", translatedGradingCriteriaOverview: languageCode === "fr" ? "Chaque question = 1pt" : languageCode === "es" - ? "Cada pregunta = 1pt" - : "Each question = 1pt", + ? "Cada pregunta = 1pt" + : "Each question = 1pt", createdAt: new Date(), updatedAt: new Date(), }; @@ -1346,7 +1346,7 @@ export const createMockAssignmentTranslation = ( */ export const createMockFeedbackTranslation = ( overrides: Partial = {}, - languageCode = "fr" + languageCode = "fr", ): FeedbackTranslation => { const baseFeedbackTranslation: FeedbackTranslation = { id: 1, @@ -1365,16 +1365,16 @@ export const createMockFeedbackTranslation = ( "3": "C'est la capitale de l'Allemagne, pas de la France.", }) : languageCode === "es" - ? JSON.stringify({ - "1": "¡Correcto!", - "2": "Incorrecto. París es la capital de Francia.", - "3": "Esa es la capital de Alemania, no de Francia.", - }) - : JSON.stringify({ - "1": "Correct!", - "2": "Incorrect. Paris is the capital of France.", - "3": "That's the capital of Germany, not France.", - }), + ? JSON.stringify({ + "1": "¡Correcto!", + "2": "Incorrecto. París es la capital de Francia.", + "3": "Esa es la capital de Alemania, no de Francia.", + }) + : JSON.stringify({ + "1": "Correct!", + "2": "Incorrect. Paris is the capital of France.", + "3": "That's the capital of Germany, not France.", + }), createdAt: new Date(), updatedAt: new Date(), }; @@ -1390,7 +1390,7 @@ export const createMockFeedbackTranslation = ( */ export const createMockReport = ( overrides: Partial = {}, - issueType: ReportType = ReportType.BUG + issueType: ReportType = ReportType.BUG, ): Report => { const baseReport: Report = { id: 1, @@ -1423,7 +1423,7 @@ export const createMockReport = ( * Create a sample AssignmentFeedback with sane defaults */ export const createMockAssignmentFeedback = ( - overrides: Partial = {} + overrides: Partial = {}, ): AssignmentFeedback => { const baseFeedback: AssignmentFeedback = { id: 1, @@ -1453,7 +1453,7 @@ export const createMockAssignmentFeedback = ( */ export const createMockRegradingRequest = ( overrides: Partial = {}, - status: RegradingStatus = RegradingStatus.PENDING + status: RegradingStatus = RegradingStatus.PENDING, ): RegradingRequest => { const baseRequest: RegradingRequest = { id: 1, @@ -1476,7 +1476,7 @@ export const createMockRegradingRequest = ( * Create a sample QuestionGenerationPayload with sane defaults */ export const createMockQuestionGenerationPayload = ( - overrides: Partial = {} + overrides: Partial = {}, ): QuestionGenerationPayload => { const basePayload: QuestionGenerationPayload = { assignmentId: 1, @@ -1669,7 +1669,7 @@ export const createMockQuestionRepository = () => ({ mapToQuestionDto: jest .fn() .mockImplementation((question: { id: number }) => - createMockQuestionDto({ id: question.id }) + createMockQuestionDto({ id: question.id }), ), parseJsonField: jest .fn() @@ -1726,7 +1726,7 @@ export const createMockVariantRepository = () => ({ mapToVariantDto: jest .fn() .mockImplementation((variant: { id: number }) => - createMockVariantDto({ id: variant.id }) + createMockVariantDto({ id: variant.id }), ), parseJsonField: jest .fn() @@ -1853,7 +1853,7 @@ export const createMockLlmFacadeService = () => ({ translateText: jest .fn() .mockImplementation((text: string) => - Promise.resolve(`Translated: ${text}`) + Promise.resolve(`Translated: ${text}`), ), generateQuestionRewordings: jest.fn().mockResolvedValue([ { @@ -1876,7 +1876,7 @@ export const createMockLlmFacadeService = () => ({ generateQuestionTranslation: jest .fn() .mockImplementation((_assignmentId: number, text: string) => - Promise.resolve(`Translated: ${text}`) + Promise.resolve(`Translated: ${text}`), ), generateChoicesTranslation: jest.fn().mockResolvedValue([ { ...sampleChoiceA, choice: "Translated choice A" }, diff --git a/apps/api/src/api/attempt/dto/success-page-data.dto.ts b/apps/api/src/api/attempt/dto/success-page-data.dto.ts index a0284370..6d051d1e 100644 --- a/apps/api/src/api/attempt/dto/success-page-data.dto.ts +++ b/apps/api/src/api/attempt/dto/success-page-data.dto.ts @@ -1,4 +1,4 @@ -import { ApiProperty } from '@nestjs/swagger'; +import { ApiProperty } from "@nestjs/swagger"; export class SuccessPageQuestionDto { @ApiProperty() @@ -67,7 +67,7 @@ export class SuccessPageDataDto { showSubmissionFeedback: boolean; @ApiProperty() - correctAnswerVisibility: 'NEVER' | 'ALWAYS' | 'ON_PASS'; + correctAnswerVisibility: "NEVER" | "ALWAYS" | "ON_PASS"; @ApiProperty() comments?: string; // Instructor comments @@ -77,4 +77,4 @@ export class SuccessPageDataDto { @ApiProperty({ type: [SuccessPageQuestionDto] }) questions: SuccessPageQuestionDto[]; -} \ No newline at end of file +} diff --git a/apps/api/src/api/attempt/services/attempt-grading.service.ts b/apps/api/src/api/attempt/services/attempt-grading.service.ts index a654c228..9dedfe88 100644 --- a/apps/api/src/api/attempt/services/attempt-grading.service.ts +++ b/apps/api/src/api/attempt/services/attempt-grading.service.ts @@ -12,14 +12,14 @@ export class AttemptGradingService { */ calculateGradeForAuthor( successfulQuestionResponses: CreateQuestionResponseAttemptResponseDto[], - totalPossiblePoints: number + totalPossiblePoints: number, ): { grade: number; totalPointsEarned: number } { if (successfulQuestionResponses.length === 0) { return { grade: 0, totalPointsEarned: 0 }; } const totalPointsEarned = this.calculateTotalPointsEarned( - successfulQuestionResponses + successfulQuestionResponses, ); const grade = totalPossiblePoints > 0 ? totalPointsEarned / totalPossiblePoints : 0; @@ -35,13 +35,13 @@ export class AttemptGradingService { */ calculateGradeForLearner( successfulQuestionResponses: CreateQuestionResponseAttemptResponseDto[], - totalPossiblePoints: number + totalPossiblePoints: number, ): { grade: number; totalPointsEarned: number; totalPossiblePoints: number } { if (successfulQuestionResponses.length === 0) { return { grade: 0, totalPointsEarned: 0, totalPossiblePoints: 0 }; } const totalPointsEarned = this.calculateTotalPointsEarned( - successfulQuestionResponses + successfulQuestionResponses, ); const grade = totalPossiblePoints > 0 ? totalPointsEarned / totalPossiblePoints : 0; @@ -57,7 +57,7 @@ export class AttemptGradingService { */ constructFeedbacksForQuestions( successfulQuestionResponses: CreateQuestionResponseAttemptResponseDto[], - assignment: Assignment + assignment: Assignment, ) { return successfulQuestionResponses.map((feedbackForQuestion) => { const { totalPoints, feedback, ...otherData } = feedbackForQuestion; @@ -75,11 +75,11 @@ export class AttemptGradingService { * @returns Total points earned */ private calculateTotalPointsEarned( - responses: CreateQuestionResponseAttemptResponseDto[] + responses: CreateQuestionResponseAttemptResponseDto[], ): number { return responses.reduce( (accumulator, response) => accumulator + response.totalPoints, - 0 + 0, ); } } diff --git a/apps/web/app/author/(components)/StepTwo/FeedbackSettings.tsx b/apps/web/app/author/(components)/StepTwo/FeedbackSettings.tsx index 5e6768dc..bed1d63c 100644 --- a/apps/web/app/author/(components)/StepTwo/FeedbackSettings.tsx +++ b/apps/web/app/author/(components)/StepTwo/FeedbackSettings.tsx @@ -99,7 +99,9 @@ const CorrectAnswerSetting: React.FC = ({ {/* Radio buttons for when to show (only visible if toggle is enabled) */} {showCorrectAnswers && (
    -
    When to show:
    +
    + When to show: +
    {radioOptions.map((option) => (