From 7e8c8e2532e39cfe2849cd6bacc294042ac6d269 Mon Sep 17 00:00:00 2001 From: JakkrapartXD Date: Wed, 4 Feb 2026 14:19:17 +0700 Subject: [PATCH] feat: fix quiz scoring to use Choice model correctness, add YouTube video support to lesson content, and include show_answers_after_completion in quiz data Update quiz attempt detail scoring to determine correctness from Choice.is_correct instead of StudentAnswer.is_correct and calculate earned score based on correctness. Add YouTube video support to getLessonContent endpoint by checking LessonAttachment for YouTube videos (mime_type 'video/youtube') and building YouTube URLs from video IDs. Ad --- .../src/services/CoursesInstructor.service.ts | 9 ++++- .../src/services/CoursesStudent.service.ts | 37 ++++++++++++++----- Backend/src/types/CoursesStudent.types.ts | 1 + 3 files changed, 36 insertions(+), 11 deletions(-) diff --git a/Backend/src/services/CoursesInstructor.service.ts b/Backend/src/services/CoursesInstructor.service.ts index 116fadc8..2caacada 100644 --- a/Backend/src/services/CoursesInstructor.service.ts +++ b/Backend/src/services/CoursesInstructor.service.ts @@ -905,14 +905,19 @@ export class CoursesInstructorService { const selectedChoiceId = studentAnswer?.choice_id || null; const selectedChoice = selectedChoiceId ? question.choices.find(c => c.id === selectedChoiceId) : null; + // Check if selected choice is correct from Choice model + const isCorrect = selectedChoice?.is_correct || false; + // Score is question.score if correct, otherwise 0 + const earnedScore = isCorrect ? question.score : 0; + return { question_id: question.id, sort_order: question.sort_order, question_text: question.question as { th: string; en: string }, selected_choice_id: selectedChoiceId, selected_choice_text: selectedChoice ? selectedChoice.text as { th: string; en: string } : null, - is_correct: studentAnswer?.is_correct || false, - score: studentAnswer?.score || 0, + is_correct: isCorrect, + score: earnedScore, question_score: question.score, }; }); diff --git a/Backend/src/services/CoursesStudent.service.ts b/Backend/src/services/CoursesStudent.service.ts index bd739055..34b34395 100644 --- a/Backend/src/services/CoursesStudent.service.ts +++ b/Backend/src/services/CoursesStudent.service.ts @@ -486,17 +486,35 @@ export class CoursesStudentService { // Import additional MinIO functions // Using MinIO functions imported above - // Get video URL from video folder (first file) + // Get video URL - check for YouTube or MinIO let video_url: string | null = null; - try { - const videoPrefix = getVideoFolder(chapter_course_id, lesson_id); - const videoFiles = await listObjects(videoPrefix); - if (videoFiles.length > 0) { - // Get presigned URL for the first video file - video_url = await getPresignedUrl(videoFiles[0].name, 3600); + const videoAttachment = await prisma.lessonAttachment.findFirst({ + where: { lesson_id, sort_order: 0 } + }); + + if (videoAttachment) { + if (videoAttachment.mime_type === 'video/youtube') { + // YouTube video - build URL from video ID stored in file_path + video_url = `https://www.youtube.com/watch?v=${videoAttachment.file_path}`; + } else { + // MinIO video - get presigned URL + try { + video_url = await getPresignedUrl(videoAttachment.file_path, 3600); + } catch (err) { + logger.error(`Failed to get video from MinIO: ${err}`); + } + } + } else { + // Fallback: try to get video from MinIO folder (legacy support) + try { + const videoPrefix = getVideoFolder(chapter_course_id, lesson_id); + const videoFiles = await listObjects(videoPrefix); + if (videoFiles.length > 0) { + video_url = await getPresignedUrl(videoFiles[0].name, 3600); + } + } catch (err) { + logger.error(`Failed to get video from MinIO: ${err}`); } - } catch (err) { - logger.error(`Failed to get video from MinIO: ${err}`); } // Get attachments from MinIO folder @@ -578,6 +596,7 @@ export class CoursesStudentService { shuffle_questions: lesson.quiz.shuffle_questions, shuffle_choices: lesson.quiz.shuffle_choices, is_skippable: lesson.quiz.is_skippable, + show_answers_after_completion: lesson.quiz.show_answers_after_completion, questions: lesson.quiz.questions.map(q => ({ id: q.id, question: q.question as { th: string; en: string }, diff --git a/Backend/src/types/CoursesStudent.types.ts b/Backend/src/types/CoursesStudent.types.ts index 50c410a9..13051a8b 100644 --- a/Backend/src/types/CoursesStudent.types.ts +++ b/Backend/src/types/CoursesStudent.types.ts @@ -158,6 +158,7 @@ export interface LessonContentData { shuffle_questions: boolean; shuffle_choices: boolean; is_skippable: boolean; + show_answers_after_completion: boolean; questions: { id: number; question: MultiLangText;