feat: Implement granular API for video upload/update and attachment management with dedicated types and endpoints.
This commit is contained in:
parent
e082c77946
commit
be7348c74d
4 changed files with 580 additions and 215 deletions
|
|
@ -28,8 +28,6 @@ import {
|
|||
UpdateLessonResponse,
|
||||
DeleteLessonResponse,
|
||||
ReorderLessonsResponse,
|
||||
AddVideoToLessonInput,
|
||||
UpdateVideoLessonInput,
|
||||
AddQuestionInput,
|
||||
AddQuestionResponse,
|
||||
UpdateQuestionInput,
|
||||
|
|
@ -39,6 +37,17 @@ import {
|
|||
QuizQuestionData,
|
||||
ReorderQuestionInput,
|
||||
ReorderQuestionResponse,
|
||||
UploadVideoInput,
|
||||
UpdateVideoInput,
|
||||
UploadAttachmentInput,
|
||||
DeleteAttachmentInput,
|
||||
VideoOperationResponse,
|
||||
AttachmentOperationResponse,
|
||||
DeleteAttachmentResponse,
|
||||
LessonAttachmentData,
|
||||
UpdateQuizInput,
|
||||
UpdateQuizResponse,
|
||||
QuizData,
|
||||
} from "../types/ChaptersLesson.typs";
|
||||
import { CoursesInstructorService } from './CoursesInstructor.service';
|
||||
|
||||
|
|
@ -583,43 +592,47 @@ export class ChaptersLessonService {
|
|||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Separate Video/Attachment APIs
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* เพิ่มวิดีโอและไฟล์แนบให้บทเรียนประเภท VIDEO
|
||||
* Add video and attachments to an existing VIDEO type lesson
|
||||
* อัพโหลดวิดีโอใหม่ให้บทเรียน (ครั้งแรก)
|
||||
* Upload video to lesson (first time)
|
||||
*/
|
||||
async addVideoLesson(request: AddVideoToLessonInput): Promise<CreateLessonResponse> {
|
||||
async uploadVideo(request: UploadVideoInput): Promise<VideoOperationResponse> {
|
||||
try {
|
||||
const { token, course_id, lesson_id, video, attachments } = request;
|
||||
const { token, course_id, lesson_id, video } = request;
|
||||
const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number };
|
||||
await CoursesInstructorService.validateCourseStatus(course_id);
|
||||
|
||||
const user = await prisma.user.findUnique({ where: { id: decodedToken.id } });
|
||||
if (!user) {
|
||||
throw new UnauthorizedError('Invalid token');
|
||||
}
|
||||
if (!user) throw new UnauthorizedError('Invalid token');
|
||||
|
||||
const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id);
|
||||
if (!courseInstructor) {
|
||||
throw new ForbiddenError('You are not permitted to modify this lesson');
|
||||
}
|
||||
if (!courseInstructor) throw new ForbiddenError('You are not permitted to modify this lesson');
|
||||
|
||||
// Verify lesson exists and is VIDEO type
|
||||
const lesson = await prisma.lesson.findUnique({ where: { id: lesson_id } });
|
||||
if (!lesson) {
|
||||
throw new NotFoundError('Lesson not found');
|
||||
}
|
||||
if (lesson.type !== 'VIDEO') {
|
||||
throw new ValidationError('Cannot add video to non-VIDEO type lesson');
|
||||
if (!lesson) throw new NotFoundError('Lesson not found');
|
||||
if (lesson.type !== 'VIDEO') throw new ValidationError('Cannot add video to non-VIDEO type lesson');
|
||||
|
||||
// Check if video already exists
|
||||
const existingVideo = await prisma.lessonAttachment.findFirst({
|
||||
where: { lesson_id, sort_order: 0 }
|
||||
});
|
||||
if (existingVideo) {
|
||||
throw new ValidationError('Video already exists. Use update video API instead.');
|
||||
}
|
||||
|
||||
// Upload video to MinIO
|
||||
const videoPath = generateFilePath(course_id, lesson_id, 'video', video.originalname);
|
||||
await uploadFile(videoPath, video.buffer, video.mimetype);
|
||||
|
||||
// Save video as attachment
|
||||
// Save video as attachment (sort_order = 0 for video)
|
||||
await prisma.lessonAttachment.create({
|
||||
data: {
|
||||
lesson_id: lesson_id,
|
||||
lesson_id,
|
||||
file_name: video.originalname,
|
||||
file_size: video.size,
|
||||
mime_type: video.mimetype,
|
||||
|
|
@ -628,144 +641,220 @@ export class ChaptersLessonService {
|
|||
}
|
||||
});
|
||||
|
||||
// Handle additional attachments (PDFs, documents, etc.)
|
||||
if (attachments && attachments.length > 0) {
|
||||
for (let i = 0; i < attachments.length; i++) {
|
||||
const attachment = attachments[i];
|
||||
const attachmentPath = generateFilePath(course_id, lesson_id, 'attachment', attachment.originalname);
|
||||
await uploadFile(attachmentPath, attachment.buffer, attachment.mimetype);
|
||||
// Get presigned URL
|
||||
const video_url = await getPresignedUrl(videoPath, 3600);
|
||||
|
||||
await prisma.lessonAttachment.create({
|
||||
data: {
|
||||
lesson_id: lesson_id,
|
||||
file_name: attachment.originalname,
|
||||
file_size: attachment.size,
|
||||
mime_type: attachment.mimetype,
|
||||
file_path: attachmentPath,
|
||||
sort_order: i + 1,
|
||||
}
|
||||
});
|
||||
return {
|
||||
code: 200,
|
||||
message: 'Video uploaded successfully',
|
||||
data: {
|
||||
lesson_id,
|
||||
video_url,
|
||||
file_name: video.originalname,
|
||||
file_size: video.size,
|
||||
mime_type: video.mimetype,
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch the complete lesson with attachments
|
||||
const completeLesson = await prisma.lesson.findUnique({
|
||||
where: { id: lesson_id },
|
||||
include: { attachments: { orderBy: { sort_order: 'asc' } } }
|
||||
});
|
||||
|
||||
return { code: 200, message: 'Video added to lesson successfully', data: completeLesson as LessonData };
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(`Error adding video to lesson: ${error}`);
|
||||
logger.error(`Error uploading video: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async updateVideoLesson(request: UpdateVideoLessonInput): Promise<UpdateLessonResponse> {
|
||||
/**
|
||||
* อัพเดต (เปลี่ยน) วิดีโอของบทเรียน
|
||||
* Update (replace) video in lesson
|
||||
*/
|
||||
async updateVideo(request: UpdateVideoInput): Promise<VideoOperationResponse> {
|
||||
try {
|
||||
const { token, course_id, lesson_id, video, attachments } = request;
|
||||
const { token, course_id, lesson_id, video } = request;
|
||||
const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number };
|
||||
await CoursesInstructorService.validateCourseStatus(course_id);
|
||||
|
||||
const user = await prisma.user.findUnique({ where: { id: decodedToken.id } });
|
||||
logger.info(`User: ${user}`);
|
||||
if (!user) throw new UnauthorizedError('Invalid token');
|
||||
|
||||
const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id);
|
||||
if (!courseInstructor) throw new ForbiddenError('You are not permitted to modify this lesson');
|
||||
|
||||
// Verify lesson exists and is VIDEO type
|
||||
const lesson = await prisma.lesson.findUnique({ where: { id: lesson_id } });
|
||||
if (!lesson) throw new NotFoundError('Lesson not found');
|
||||
if (lesson.type !== 'VIDEO') throw new ValidationError('Cannot update video for non-VIDEO type lesson');
|
||||
|
||||
// Update video if provided
|
||||
if (video) {
|
||||
// Find existing video attachment (sort_order = 0 is video)
|
||||
const existingVideo = await prisma.lessonAttachment.findFirst({
|
||||
where: { lesson_id: lesson_id, sort_order: 0 }
|
||||
});
|
||||
|
||||
if (existingVideo) {
|
||||
// Delete old video from MinIO
|
||||
await deleteFile(existingVideo.file_path);
|
||||
|
||||
// Upload new video to MinIO
|
||||
const newVideoPath = generateFilePath(course_id, lesson_id, 'video', video.originalname);
|
||||
await uploadFile(newVideoPath, video.buffer, video.mimetype);
|
||||
|
||||
// Update lessonAttachment with new video info
|
||||
await prisma.lessonAttachment.update({
|
||||
where: { id: existingVideo.id },
|
||||
data: {
|
||||
file_name: video.originalname,
|
||||
file_size: video.size,
|
||||
mime_type: video.mimetype,
|
||||
file_path: newVideoPath,
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// No existing video, create new one
|
||||
const videoPath = generateFilePath(course_id, lesson_id, 'video', video.originalname);
|
||||
await uploadFile(videoPath, video.buffer, video.mimetype);
|
||||
|
||||
await prisma.lessonAttachment.create({
|
||||
data: {
|
||||
lesson_id: lesson_id,
|
||||
file_name: video.originalname,
|
||||
file_size: video.size,
|
||||
mime_type: video.mimetype,
|
||||
file_path: videoPath,
|
||||
sort_order: 0,
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Update attachments if provided
|
||||
if (attachments && attachments.length > 0) {
|
||||
// Find and delete existing attachments (sort_order > 0)
|
||||
const existingAttachments = await prisma.lessonAttachment.findMany({
|
||||
where: { lesson_id: lesson_id, sort_order: { gt: 0 } }
|
||||
});
|
||||
|
||||
// Delete old attachment files from MinIO
|
||||
for (const att of existingAttachments) {
|
||||
await deleteFile(att.file_path);
|
||||
}
|
||||
|
||||
// Delete old attachment records from database
|
||||
await prisma.lessonAttachment.deleteMany({
|
||||
where: { lesson_id: lesson_id, sort_order: { gt: 0 } }
|
||||
});
|
||||
|
||||
// Upload new attachments
|
||||
for (let i = 0; i < attachments.length; i++) {
|
||||
const attachment = attachments[i];
|
||||
const attachmentPath = generateFilePath(course_id, lesson_id, 'attachment', attachment.originalname);
|
||||
await uploadFile(attachmentPath, attachment.buffer, attachment.mimetype);
|
||||
|
||||
await prisma.lessonAttachment.create({
|
||||
data: {
|
||||
lesson_id: lesson_id,
|
||||
file_name: attachment.originalname,
|
||||
file_size: attachment.size,
|
||||
mime_type: attachment.mimetype,
|
||||
file_path: attachmentPath,
|
||||
sort_order: i + 1,
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch updated lesson with attachments
|
||||
const updatedLesson = await prisma.lesson.findUnique({
|
||||
where: { id: lesson_id },
|
||||
include: { attachments: { orderBy: { sort_order: 'asc' } } }
|
||||
// Find existing video
|
||||
const existingVideo = await prisma.lessonAttachment.findFirst({
|
||||
where: { lesson_id, sort_order: 0 }
|
||||
});
|
||||
|
||||
return { code: 200, message: 'Video lesson updated successfully', data: updatedLesson as LessonData };
|
||||
// Upload new video to MinIO
|
||||
const newVideoPath = generateFilePath(course_id, lesson_id, 'video', video.originalname);
|
||||
await uploadFile(newVideoPath, video.buffer, video.mimetype);
|
||||
|
||||
if (existingVideo) {
|
||||
// Delete old video from MinIO
|
||||
try {
|
||||
await deleteFile(existingVideo.file_path);
|
||||
} catch (err) {
|
||||
logger.warn(`Failed to delete old video: ${existingVideo.file_path}`);
|
||||
}
|
||||
|
||||
// Update lessonAttachment with new video info
|
||||
await prisma.lessonAttachment.update({
|
||||
where: { id: existingVideo.id },
|
||||
data: {
|
||||
file_name: video.originalname,
|
||||
file_size: video.size,
|
||||
mime_type: video.mimetype,
|
||||
file_path: newVideoPath,
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// No existing video, create new one
|
||||
await prisma.lessonAttachment.create({
|
||||
data: {
|
||||
lesson_id,
|
||||
file_name: video.originalname,
|
||||
file_size: video.size,
|
||||
mime_type: video.mimetype,
|
||||
file_path: newVideoPath,
|
||||
sort_order: 0,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Get presigned URL
|
||||
const video_url = await getPresignedUrl(newVideoPath, 3600);
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: 'Video updated successfully',
|
||||
data: {
|
||||
lesson_id,
|
||||
video_url,
|
||||
file_name: video.originalname,
|
||||
file_size: video.size,
|
||||
mime_type: video.mimetype,
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(`Error updating video lesson: ${error}`);
|
||||
logger.error(`Error updating video: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* อัพโหลดไฟล์แนบทีละไฟล์
|
||||
* Upload a single attachment to lesson
|
||||
*/
|
||||
async uploadAttachment(request: UploadAttachmentInput): Promise<AttachmentOperationResponse> {
|
||||
try {
|
||||
const { token, course_id, lesson_id, attachment } = request;
|
||||
const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number };
|
||||
await CoursesInstructorService.validateCourseStatus(course_id);
|
||||
|
||||
const user = await prisma.user.findUnique({ where: { id: decodedToken.id } });
|
||||
if (!user) throw new UnauthorizedError('Invalid token');
|
||||
|
||||
const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id);
|
||||
if (!courseInstructor) throw new ForbiddenError('You are not permitted to modify this lesson');
|
||||
|
||||
// Verify lesson exists
|
||||
const lesson = await prisma.lesson.findUnique({ where: { id: lesson_id } });
|
||||
if (!lesson) throw new NotFoundError('Lesson not found');
|
||||
|
||||
// Get current max sort_order for attachments (excluding video at sort_order 0)
|
||||
const maxSortOrder = await prisma.lessonAttachment.aggregate({
|
||||
where: { lesson_id, sort_order: { gt: 0 } },
|
||||
_max: { sort_order: true }
|
||||
});
|
||||
const nextSortOrder = (maxSortOrder._max.sort_order ?? 0) + 1;
|
||||
|
||||
// Upload attachment to MinIO
|
||||
const attachmentPath = generateFilePath(course_id, lesson_id, 'attachment', attachment.originalname);
|
||||
await uploadFile(attachmentPath, attachment.buffer, attachment.mimetype);
|
||||
|
||||
// Save attachment to database
|
||||
const newAttachment = await prisma.lessonAttachment.create({
|
||||
data: {
|
||||
lesson_id,
|
||||
file_name: attachment.originalname,
|
||||
file_size: attachment.size,
|
||||
mime_type: attachment.mimetype,
|
||||
file_path: attachmentPath,
|
||||
sort_order: nextSortOrder,
|
||||
}
|
||||
});
|
||||
|
||||
// Get presigned URL
|
||||
const presigned_url = await getPresignedUrl(attachmentPath, 3600);
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: 'Attachment uploaded successfully',
|
||||
data: {
|
||||
id: newAttachment.id,
|
||||
lesson_id: newAttachment.lesson_id,
|
||||
file_name: newAttachment.file_name,
|
||||
file_path: newAttachment.file_path,
|
||||
file_size: newAttachment.file_size,
|
||||
mime_type: newAttachment.mime_type,
|
||||
sort_order: newAttachment.sort_order,
|
||||
created_at: newAttachment.created_at,
|
||||
presigned_url,
|
||||
} as LessonAttachmentData
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(`Error uploading attachment: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ลบไฟล์แนบทีละไฟล์
|
||||
* Delete a single attachment from lesson
|
||||
*/
|
||||
async deleteAttachment(request: DeleteAttachmentInput): Promise<DeleteAttachmentResponse> {
|
||||
try {
|
||||
const { token, course_id, lesson_id, attachment_id } = request;
|
||||
const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number };
|
||||
await CoursesInstructorService.validateCourseStatus(course_id);
|
||||
|
||||
const user = await prisma.user.findUnique({ where: { id: decodedToken.id } });
|
||||
if (!user) throw new UnauthorizedError('Invalid token');
|
||||
|
||||
const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id);
|
||||
if (!courseInstructor) throw new ForbiddenError('You are not permitted to modify this lesson');
|
||||
|
||||
// Verify lesson exists
|
||||
const lesson = await prisma.lesson.findUnique({ where: { id: lesson_id } });
|
||||
if (!lesson) throw new NotFoundError('Lesson not found');
|
||||
|
||||
// Find attachment
|
||||
const attachment = await prisma.lessonAttachment.findUnique({
|
||||
where: { id: attachment_id }
|
||||
});
|
||||
if (!attachment) throw new NotFoundError('Attachment not found');
|
||||
if (attachment.lesson_id !== lesson_id) throw new NotFoundError('Attachment not found in this lesson');
|
||||
|
||||
// Don't allow deleting video (sort_order = 0)
|
||||
if (attachment.sort_order === 0) {
|
||||
throw new ValidationError('Cannot delete video using this API. Use delete video API instead.');
|
||||
}
|
||||
|
||||
// Delete file from MinIO
|
||||
try {
|
||||
await deleteFile(attachment.file_path);
|
||||
} catch (err) {
|
||||
logger.warn(`Failed to delete file from MinIO: ${attachment.file_path}`);
|
||||
}
|
||||
|
||||
// Delete attachment record from database
|
||||
await prisma.lessonAttachment.delete({ where: { id: attachment_id } });
|
||||
|
||||
return { code: 200, message: 'Attachment deleted successfully' };
|
||||
} catch (error) {
|
||||
logger.error(`Error deleting attachment: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
|
@ -773,10 +862,11 @@ export class ChaptersLessonService {
|
|||
/**
|
||||
* เพิ่มคำถามทีละข้อให้ Quiz Lesson
|
||||
* Add a single question with choices to an existing QUIZ lesson
|
||||
* คะแนนจะถูกคำนวณอัตโนมัติ (100 คะแนน / จำนวนข้อ)
|
||||
*/
|
||||
async addQuestion(request: AddQuestionInput): Promise<AddQuestionResponse> {
|
||||
try {
|
||||
const { token, course_id, lesson_id, question, explanation, question_type, score, sort_order, choices } = request;
|
||||
const { token, course_id, lesson_id, question, explanation, question_type, sort_order, choices } = request;
|
||||
const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number };
|
||||
await CoursesInstructorService.validateCourseStatus(course_id);
|
||||
|
||||
|
|
@ -802,14 +892,14 @@ export class ChaptersLessonService {
|
|||
});
|
||||
const nextSortOrder = sort_order ?? ((maxSortOrder._max.sort_order ?? -1) + 1);
|
||||
|
||||
// Create the question
|
||||
// Create the question with temporary score (will be recalculated)
|
||||
const newQuestion = await prisma.question.create({
|
||||
data: {
|
||||
quiz_id: lesson.quiz.id,
|
||||
question: question,
|
||||
explanation: explanation,
|
||||
question_type: question_type,
|
||||
score: score ?? 1,
|
||||
score: 1, // Temporary, will be recalculated
|
||||
sort_order: nextSortOrder,
|
||||
}
|
||||
});
|
||||
|
|
@ -829,7 +919,10 @@ export class ChaptersLessonService {
|
|||
}
|
||||
}
|
||||
|
||||
// Fetch complete question with choices
|
||||
// Recalculate scores for all questions (100 points total)
|
||||
await this.recalculateQuestionScores(lesson.quiz.id);
|
||||
|
||||
// Fetch complete question with choices (with updated score)
|
||||
const completeQuestion = await prisma.question.findUnique({
|
||||
where: { id: newQuestion.id },
|
||||
include: { choices: { orderBy: { sort_order: 'asc' } } }
|
||||
|
|
@ -845,10 +938,11 @@ export class ChaptersLessonService {
|
|||
/**
|
||||
* อัปเดตคำถาม
|
||||
* Update a question and optionally replace all choices
|
||||
* หมายเหตุ: คะแนนจะถูกคำนวณอัตโนมัติ ไม่สามารถเปลี่ยนได้
|
||||
*/
|
||||
async updateQuestion(request: UpdateQuestionInput): Promise<UpdateQuestionResponse> {
|
||||
try {
|
||||
const { token, course_id, lesson_id, question_id, question, explanation, question_type, score, sort_order, choices } = request;
|
||||
const { token, course_id, lesson_id, question_id, question, explanation, question_type, sort_order, choices } = request;
|
||||
const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number };
|
||||
await CoursesInstructorService.validateCourseStatus(course_id);
|
||||
|
||||
|
|
@ -873,14 +967,13 @@ export class ChaptersLessonService {
|
|||
throw new NotFoundError('Question not found in this quiz');
|
||||
}
|
||||
|
||||
// Update the question
|
||||
// Update the question (score is NOT updated - it's auto-calculated)
|
||||
await prisma.question.update({
|
||||
where: { id: question_id },
|
||||
data: {
|
||||
question: (question ?? existingQuestion.question) as Prisma.InputJsonValue,
|
||||
explanation: (explanation ?? existingQuestion.explanation) as Prisma.InputJsonValue,
|
||||
question_type: question_type ?? existingQuestion.question_type,
|
||||
score: score ?? existingQuestion.score,
|
||||
sort_order: sort_order ?? existingQuestion.sort_order,
|
||||
}
|
||||
});
|
||||
|
|
@ -1001,6 +1094,7 @@ export class ChaptersLessonService {
|
|||
/**
|
||||
* ลบคำถาม
|
||||
* Delete a question and all its choices
|
||||
* คะแนนจะถูกคำนวณใหม่หลังจากลบ
|
||||
*/
|
||||
async deleteQuestion(request: DeleteQuestionInput): Promise<DeleteQuestionResponse> {
|
||||
try {
|
||||
|
|
@ -1032,6 +1126,9 @@ export class ChaptersLessonService {
|
|||
// Delete the question (CASCADE will delete choices)
|
||||
await prisma.question.delete({ where: { id: question_id } });
|
||||
|
||||
// Recalculate scores for remaining questions
|
||||
await this.recalculateQuestionScores(lesson.quiz.id);
|
||||
|
||||
return { code: 200, message: 'Question deleted successfully' };
|
||||
} catch (error) {
|
||||
logger.error(`Error deleting question: ${error}`);
|
||||
|
|
@ -1039,4 +1136,100 @@ export class ChaptersLessonService {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* คำนวณคะแนนใหม่สำหรับทุกคำถามใน Quiz
|
||||
* Recalculate scores for all questions in a quiz (100 points total)
|
||||
* Distributes points as integers, handling remainders to ensure exact 100 total.
|
||||
*
|
||||
* @param quizId Quiz ID to recalculate
|
||||
*/
|
||||
private async recalculateQuestionScores(quizId: number): Promise<void> {
|
||||
// Get all questions in this quiz
|
||||
const questions = await prisma.question.findMany({
|
||||
where: { quiz_id: quizId },
|
||||
orderBy: { sort_order: 'asc' },
|
||||
select: { id: true }
|
||||
});
|
||||
|
||||
const questionCount = questions.length;
|
||||
if (questionCount === 0) return;
|
||||
|
||||
// Calculate base score and remainder
|
||||
const baseScore = Math.floor(100 / questionCount);
|
||||
const remainder = 100 % questionCount;
|
||||
|
||||
// Prepare updates
|
||||
const updates: any[] = [];
|
||||
|
||||
for (let i = 0; i < questionCount; i++) {
|
||||
// Distribute remainder to the first 'remainder' questions
|
||||
const score = i < remainder ? baseScore + 1 : baseScore;
|
||||
|
||||
updates.push(
|
||||
prisma.question.update({
|
||||
where: { id: questions[i].id },
|
||||
data: { score: score }
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Execute all updates in a transaction
|
||||
if (updates.length > 0) {
|
||||
await prisma.$transaction(updates);
|
||||
}
|
||||
|
||||
logger.info(`Recalculated quiz ${quizId}: ${questionCount} questions. Base: ${baseScore}, Remainder: ${remainder}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* อัพเดตการตั้งค่า Quiz
|
||||
* Update quiz settings (title, passing_score, time_limit, etc.)
|
||||
*/
|
||||
async updateQuiz(request: UpdateQuizInput): Promise<UpdateQuizResponse> {
|
||||
try {
|
||||
const { token, course_id, lesson_id, title, description, passing_score, time_limit, shuffle_questions, shuffle_choices, show_answers_after_completion } = request;
|
||||
const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number };
|
||||
await CoursesInstructorService.validateCourseStatus(course_id);
|
||||
|
||||
const user = await prisma.user.findUnique({ where: { id: decodedToken.id } });
|
||||
if (!user) throw new UnauthorizedError('Invalid token');
|
||||
|
||||
const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id);
|
||||
if (!courseInstructor) throw new ForbiddenError('You are not permitted to modify this lesson');
|
||||
|
||||
// Verify lesson exists and is QUIZ type
|
||||
const lesson = await prisma.lesson.findUnique({
|
||||
where: { id: lesson_id },
|
||||
include: { quiz: { include: { questions: { include: { choices: { orderBy: { sort_order: 'asc' } } }, orderBy: { sort_order: 'asc' } } } } }
|
||||
});
|
||||
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');
|
||||
|
||||
// Build update data (only include provided fields)
|
||||
const updateData: any = {
|
||||
updated_by: user.id,
|
||||
};
|
||||
if (title !== undefined) updateData.title = title;
|
||||
if (description !== undefined) updateData.description = description;
|
||||
if (passing_score !== undefined) updateData.passing_score = passing_score;
|
||||
if (time_limit !== undefined) updateData.time_limit = time_limit;
|
||||
if (shuffle_questions !== undefined) updateData.shuffle_questions = shuffle_questions;
|
||||
if (shuffle_choices !== undefined) updateData.shuffle_choices = shuffle_choices;
|
||||
if (show_answers_after_completion !== undefined) updateData.show_answers_after_completion = show_answers_after_completion;
|
||||
|
||||
// Update the quiz
|
||||
const updatedQuiz = await prisma.quiz.update({
|
||||
where: { id: lesson.quiz.id },
|
||||
data: updateData,
|
||||
include: { questions: { include: { choices: { orderBy: { sort_order: 'asc' } } }, orderBy: { sort_order: 'asc' } } }
|
||||
});
|
||||
|
||||
return { code: 200, message: 'Quiz updated successfully', data: updatedQuiz as unknown as QuizData };
|
||||
} catch (error) {
|
||||
logger.error(`Error updating quiz: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue