add check quizz for student
This commit is contained in:
parent
76f8ab120a
commit
c982ab2c05
3 changed files with 232 additions and 0 deletions
|
|
@ -11,6 +11,8 @@ import {
|
||||||
SaveVideoProgressBody,
|
SaveVideoProgressBody,
|
||||||
GetVideoProgressResponse,
|
GetVideoProgressResponse,
|
||||||
CompleteLessonResponse,
|
CompleteLessonResponse,
|
||||||
|
SubmitQuizResponse,
|
||||||
|
SubmitQuizBody,
|
||||||
} from '../types/CoursesStudent.types';
|
} from '../types/CoursesStudent.types';
|
||||||
import { EnrollmentStatus } from '@prisma/client';
|
import { EnrollmentStatus } from '@prisma/client';
|
||||||
|
|
||||||
|
|
@ -202,4 +204,34 @@ export class CoursesStudentController {
|
||||||
}
|
}
|
||||||
return await this.service.completeLesson({ token, lesson_id: lessonId });
|
return await this.service.completeLesson({ token, lesson_id: lessonId });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ส่งคำตอบ Quiz และรับผลคะแนน
|
||||||
|
* Submit quiz answers and get score
|
||||||
|
* @param courseId - รหัสคอร์ส / Course ID
|
||||||
|
* @param lessonId - รหัสบทเรียน / Lesson ID
|
||||||
|
*/
|
||||||
|
@Post('courses/{courseId}/lessons/{lessonId}/quiz/submit')
|
||||||
|
@Security('jwt', ['student'])
|
||||||
|
@SuccessResponse('200', 'Quiz submitted successfully')
|
||||||
|
@Response('401', 'Invalid or expired token')
|
||||||
|
@Response('403', 'Not enrolled in this course')
|
||||||
|
@Response('404', 'Lesson or quiz not found')
|
||||||
|
public async submitQuiz(
|
||||||
|
@Request() request: any,
|
||||||
|
@Path() courseId: number,
|
||||||
|
@Path() lessonId: number,
|
||||||
|
@Body() body: SubmitQuizBody
|
||||||
|
): Promise<SubmitQuizResponse> {
|
||||||
|
const token = request.headers.authorization?.replace('Bearer ', '');
|
||||||
|
if (!token) {
|
||||||
|
throw new ValidationError('No token provided');
|
||||||
|
}
|
||||||
|
return await this.service.submitQuiz({
|
||||||
|
token,
|
||||||
|
course_id: courseId,
|
||||||
|
lesson_id: lessonId,
|
||||||
|
answers: body.answers,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,8 @@ import {
|
||||||
GetVideoProgressResponse,
|
GetVideoProgressResponse,
|
||||||
CompleteLessonInput,
|
CompleteLessonInput,
|
||||||
CompleteLessonResponse,
|
CompleteLessonResponse,
|
||||||
|
SubmitQuizInput,
|
||||||
|
SubmitQuizResponse,
|
||||||
} from "../types/CoursesStudent.types";
|
} from "../types/CoursesStudent.types";
|
||||||
import { getPresignedUrl, listObjects, getVideoFolder, getAttachmentsFolder } from '../config/minio';
|
import { getPresignedUrl, listObjects, getVideoFolder, getAttachmentsFolder } from '../config/minio';
|
||||||
|
|
||||||
|
|
@ -461,6 +463,7 @@ export class CoursesStudentService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async checkAccessLesson(input: CheckLessonAccessInput): Promise<CheckLessonAccessResponse> {
|
async checkAccessLesson(input: CheckLessonAccessInput): Promise<CheckLessonAccessResponse> {
|
||||||
try {
|
try {
|
||||||
const { token, course_id, lesson_id } = input;
|
const { token, course_id, lesson_id } = input;
|
||||||
|
|
@ -993,4 +996,156 @@ export class CoursesStudentService {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ส่งคำตอบ Quiz และคำนวณคะแนน
|
||||||
|
* Submit quiz answers and calculate score
|
||||||
|
*/
|
||||||
|
async submitQuiz(input: SubmitQuizInput): Promise<SubmitQuizResponse> {
|
||||||
|
try {
|
||||||
|
const { token, course_id, lesson_id, answers } = input;
|
||||||
|
const decoded = jwt.verify(token, config.jwt.secret) as { id: number };
|
||||||
|
|
||||||
|
// Check enrollment
|
||||||
|
const enrollment = await prisma.enrollment.findUnique({
|
||||||
|
where: {
|
||||||
|
unique_enrollment: {
|
||||||
|
user_id: decoded.id,
|
||||||
|
course_id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!enrollment) {
|
||||||
|
throw new ForbiddenError('You are not enrolled in this course');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get lesson and verify it's a QUIZ type
|
||||||
|
const lesson = await prisma.lesson.findUnique({
|
||||||
|
where: { id: lesson_id },
|
||||||
|
include: {
|
||||||
|
quiz: {
|
||||||
|
include: {
|
||||||
|
questions: {
|
||||||
|
include: {
|
||||||
|
choices: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
chapter: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!lesson) {
|
||||||
|
throw new NotFoundError('Lesson not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lesson.type !== 'QUIZ') {
|
||||||
|
throw new ValidationError('This lesson is not a quiz');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!lesson.quiz) {
|
||||||
|
throw new NotFoundError('Quiz not found for this lesson');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify lesson belongs to the course
|
||||||
|
if (lesson.chapter.course_id !== course_id) {
|
||||||
|
throw new NotFoundError('Lesson not found in this course');
|
||||||
|
}
|
||||||
|
|
||||||
|
const quiz = lesson.quiz;
|
||||||
|
|
||||||
|
// Get previous attempt count
|
||||||
|
const previousAttempts = await prisma.quizAttempt.count({
|
||||||
|
where: {
|
||||||
|
user_id: decoded.id,
|
||||||
|
quiz_id: quiz.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const attemptNumber = previousAttempts + 1;
|
||||||
|
|
||||||
|
// Calculate score
|
||||||
|
let totalScore = 0;
|
||||||
|
let earnedScore = 0;
|
||||||
|
let correctAnswers = 0;
|
||||||
|
const answersReview: {
|
||||||
|
question_id: number;
|
||||||
|
selected_choice_id: number;
|
||||||
|
correct_choice_id: number;
|
||||||
|
is_correct: boolean;
|
||||||
|
score: number;
|
||||||
|
}[] = [];
|
||||||
|
|
||||||
|
for (const question of quiz.questions) {
|
||||||
|
totalScore += question.score;
|
||||||
|
|
||||||
|
// Find the correct choice for this question
|
||||||
|
const correctChoice = question.choices.find(c => c.is_correct);
|
||||||
|
const correctChoiceId = correctChoice?.id ?? 0;
|
||||||
|
|
||||||
|
// Find student's answer for this question
|
||||||
|
const studentAnswer = answers.find(a => a.question_id === question.id);
|
||||||
|
const selectedChoiceId = studentAnswer?.choice_id ?? 0;
|
||||||
|
|
||||||
|
// Check if answer is correct
|
||||||
|
const isCorrect = selectedChoiceId === correctChoiceId;
|
||||||
|
if (isCorrect) {
|
||||||
|
earnedScore += question.score;
|
||||||
|
correctAnswers++;
|
||||||
|
}
|
||||||
|
|
||||||
|
answersReview.push({
|
||||||
|
question_id: question.id,
|
||||||
|
selected_choice_id: selectedChoiceId,
|
||||||
|
correct_choice_id: correctChoiceId,
|
||||||
|
is_correct: isCorrect,
|
||||||
|
score: isCorrect ? question.score : 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate percentage and check if passed
|
||||||
|
const scorePercentage = totalScore > 0 ? (earnedScore / totalScore) * 100 : 0;
|
||||||
|
const isPassed = scorePercentage >= quiz.passing_score;
|
||||||
|
|
||||||
|
// Create quiz attempt record
|
||||||
|
const now = new Date();
|
||||||
|
const quizAttempt = await prisma.quizAttempt.create({
|
||||||
|
data: {
|
||||||
|
user_id: decoded.id,
|
||||||
|
quiz_id: quiz.id,
|
||||||
|
score: earnedScore,
|
||||||
|
total_questions: quiz.questions.length,
|
||||||
|
correct_answers: correctAnswers,
|
||||||
|
is_passed: isPassed,
|
||||||
|
attempt_number: attemptNumber,
|
||||||
|
answers: answers as any, // Store student answers as JSON
|
||||||
|
started_at: now,
|
||||||
|
completed_at: now,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: isPassed ? 'Quiz passed!' : 'Quiz completed',
|
||||||
|
data: {
|
||||||
|
attempt_id: quizAttempt.id,
|
||||||
|
quiz_id: quiz.id,
|
||||||
|
score: earnedScore,
|
||||||
|
total_score: totalScore,
|
||||||
|
total_questions: quiz.questions.length,
|
||||||
|
correct_answers: correctAnswers,
|
||||||
|
is_passed: isPassed,
|
||||||
|
passing_score: quiz.passing_score,
|
||||||
|
attempt_number: attemptNumber,
|
||||||
|
started_at: quizAttempt.started_at,
|
||||||
|
completed_at: quizAttempt.completed_at!,
|
||||||
|
answers_review: quiz.show_answers_after_completion ? answersReview : undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -319,3 +319,48 @@ export interface CompleteLessonResponse {
|
||||||
certificate_issued?: boolean;
|
certificate_issued?: boolean;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Quiz Submission Types
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
export interface QuizAnswerInput {
|
||||||
|
question_id: number;
|
||||||
|
choice_id: number; // For MULTIPLE_CHOICE and TRUE_FALSE
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubmitQuizInput {
|
||||||
|
token: string;
|
||||||
|
course_id: number;
|
||||||
|
lesson_id: number;
|
||||||
|
answers: QuizAnswerInput[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubmitQuizBody {
|
||||||
|
answers: QuizAnswerInput[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubmitQuizResponse {
|
||||||
|
code: number;
|
||||||
|
message: string;
|
||||||
|
data?: {
|
||||||
|
attempt_id: number;
|
||||||
|
quiz_id: number;
|
||||||
|
score: number;
|
||||||
|
total_score: number;
|
||||||
|
total_questions: number;
|
||||||
|
correct_answers: number;
|
||||||
|
is_passed: boolean;
|
||||||
|
passing_score: number;
|
||||||
|
attempt_number: number;
|
||||||
|
started_at: Date;
|
||||||
|
completed_at: Date;
|
||||||
|
answers_review?: {
|
||||||
|
question_id: number;
|
||||||
|
selected_choice_id: number;
|
||||||
|
correct_choice_id: number;
|
||||||
|
is_correct: boolean;
|
||||||
|
score: number;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue