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
This commit is contained in:
parent
827b016114
commit
7e8c8e2532
3 changed files with 36 additions and 11 deletions
|
|
@ -905,14 +905,19 @@ export class CoursesInstructorService {
|
||||||
const selectedChoiceId = studentAnswer?.choice_id || null;
|
const selectedChoiceId = studentAnswer?.choice_id || null;
|
||||||
const selectedChoice = selectedChoiceId ? question.choices.find(c => c.id === selectedChoiceId) : 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 {
|
return {
|
||||||
question_id: question.id,
|
question_id: question.id,
|
||||||
sort_order: question.sort_order,
|
sort_order: question.sort_order,
|
||||||
question_text: question.question as { th: string; en: string },
|
question_text: question.question as { th: string; en: string },
|
||||||
selected_choice_id: selectedChoiceId,
|
selected_choice_id: selectedChoiceId,
|
||||||
selected_choice_text: selectedChoice ? selectedChoice.text as { th: string; en: string } : null,
|
selected_choice_text: selectedChoice ? selectedChoice.text as { th: string; en: string } : null,
|
||||||
is_correct: studentAnswer?.is_correct || false,
|
is_correct: isCorrect,
|
||||||
score: studentAnswer?.score || 0,
|
score: earnedScore,
|
||||||
question_score: question.score,
|
question_score: question.score,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -486,17 +486,35 @@ export class CoursesStudentService {
|
||||||
// Import additional MinIO functions
|
// Import additional MinIO functions
|
||||||
// Using MinIO functions imported above
|
// 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;
|
let video_url: string | null = null;
|
||||||
try {
|
const videoAttachment = await prisma.lessonAttachment.findFirst({
|
||||||
const videoPrefix = getVideoFolder(chapter_course_id, lesson_id);
|
where: { lesson_id, sort_order: 0 }
|
||||||
const videoFiles = await listObjects(videoPrefix);
|
});
|
||||||
if (videoFiles.length > 0) {
|
|
||||||
// Get presigned URL for the first video file
|
if (videoAttachment) {
|
||||||
video_url = await getPresignedUrl(videoFiles[0].name, 3600);
|
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
|
// Get attachments from MinIO folder
|
||||||
|
|
@ -578,6 +596,7 @@ export class CoursesStudentService {
|
||||||
shuffle_questions: lesson.quiz.shuffle_questions,
|
shuffle_questions: lesson.quiz.shuffle_questions,
|
||||||
shuffle_choices: lesson.quiz.shuffle_choices,
|
shuffle_choices: lesson.quiz.shuffle_choices,
|
||||||
is_skippable: lesson.quiz.is_skippable,
|
is_skippable: lesson.quiz.is_skippable,
|
||||||
|
show_answers_after_completion: lesson.quiz.show_answers_after_completion,
|
||||||
questions: lesson.quiz.questions.map(q => ({
|
questions: lesson.quiz.questions.map(q => ({
|
||||||
id: q.id,
|
id: q.id,
|
||||||
question: q.question as { th: string; en: string },
|
question: q.question as { th: string; en: string },
|
||||||
|
|
|
||||||
|
|
@ -158,6 +158,7 @@ export interface LessonContentData {
|
||||||
shuffle_questions: boolean;
|
shuffle_questions: boolean;
|
||||||
shuffle_choices: boolean;
|
shuffle_choices: boolean;
|
||||||
is_skippable: boolean;
|
is_skippable: boolean;
|
||||||
|
show_answers_after_completion: boolean;
|
||||||
questions: {
|
questions: {
|
||||||
id: number;
|
id: number;
|
||||||
question: MultiLangText;
|
question: MultiLangText;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue