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,
|
||||
GetVideoProgressResponse,
|
||||
CompleteLessonResponse,
|
||||
SubmitQuizResponse,
|
||||
SubmitQuizBody,
|
||||
} from '../types/CoursesStudent.types';
|
||||
import { EnrollmentStatus } from '@prisma/client';
|
||||
|
||||
|
|
@ -202,4 +204,34 @@ export class CoursesStudentController {
|
|||
}
|
||||
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,
|
||||
CompleteLessonInput,
|
||||
CompleteLessonResponse,
|
||||
SubmitQuizInput,
|
||||
SubmitQuizResponse,
|
||||
} from "../types/CoursesStudent.types";
|
||||
import { getPresignedUrl, listObjects, getVideoFolder, getAttachmentsFolder } from '../config/minio';
|
||||
|
||||
|
|
@ -461,6 +463,7 @@ export class CoursesStudentService {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
async checkAccessLesson(input: CheckLessonAccessInput): Promise<CheckLessonAccessResponse> {
|
||||
try {
|
||||
const { token, course_id, lesson_id } = input;
|
||||
|
|
@ -993,4 +996,156 @@ export class CoursesStudentService {
|
|||
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;
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 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