feat: Introduce dedicated instructor and student controllers for chapter and lesson management, including quiz questions.
This commit is contained in:
parent
9a7eb50d17
commit
fc3e2820cc
3 changed files with 426 additions and 18 deletions
344
Backend/src/controllers/ChaptersLessonInstructorController.ts
Normal file
344
Backend/src/controllers/ChaptersLessonInstructorController.ts
Normal file
|
|
@ -0,0 +1,344 @@
|
|||
import { Body, Delete, Get, Path, Post, Put, Request, Response, Route, Security, SuccessResponse, Tags } from 'tsoa';
|
||||
import { ValidationError } from '../middleware/errorHandler';
|
||||
import { ChaptersLessonService } from '../services/ChaptersLesson.service';
|
||||
import {
|
||||
ListChaptersResponse,
|
||||
CreateChapterResponse,
|
||||
UpdateChapterResponse,
|
||||
DeleteChapterResponse,
|
||||
ReorderChapterResponse,
|
||||
GetLessonResponse,
|
||||
CreateLessonResponse,
|
||||
UpdateLessonResponse,
|
||||
DeleteLessonResponse,
|
||||
ReorderLessonsResponse,
|
||||
AddQuestionResponse,
|
||||
UpdateQuestionResponse,
|
||||
DeleteQuestionResponse,
|
||||
CreateChapterBody,
|
||||
UpdateChapterBody,
|
||||
ReorderChapterBody,
|
||||
CreateLessonBody,
|
||||
UpdateLessonBody,
|
||||
ReorderLessonsBody,
|
||||
AddQuestionBody,
|
||||
UpdateQuestionBody,
|
||||
} from '../types/ChaptersLesson.typs';
|
||||
|
||||
const chaptersLessonService = new ChaptersLessonService();
|
||||
|
||||
@Route('api/instructors/courses/{courseId}')
|
||||
@Tags('ChaptersLessons - Instructor')
|
||||
export class ChaptersLessonInstructorController {
|
||||
|
||||
// ============================================
|
||||
// Chapter Endpoints
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* ดึง chapters ทั้งหมดของ course (พร้อม lessons)
|
||||
* Get all chapters of a course with lessons
|
||||
*/
|
||||
@Get('chapters')
|
||||
@Security('jwt', ['instructor'])
|
||||
@SuccessResponse('200', 'Chapters retrieved successfully')
|
||||
@Response('401', 'Unauthorized')
|
||||
public async listChapters(@Request() request: any, @Path() courseId: number): Promise<ListChaptersResponse> {
|
||||
const token = request.headers.authorization?.replace('Bearer ', '');
|
||||
if (!token) throw new ValidationError('No token provided');
|
||||
return await chaptersLessonService.listChapters({ token, course_id: courseId });
|
||||
}
|
||||
|
||||
/**
|
||||
* สร้าง chapter ใหม่
|
||||
* Create a new chapter
|
||||
*/
|
||||
@Post('chapters')
|
||||
@Security('jwt', ['instructor'])
|
||||
@SuccessResponse('201', 'Chapter created successfully')
|
||||
@Response('401', 'Unauthorized')
|
||||
@Response('403', 'Forbidden')
|
||||
public async createChapter(
|
||||
@Request() request: any,
|
||||
@Path() courseId: number,
|
||||
@Body() body: CreateChapterBody
|
||||
): Promise<CreateChapterResponse> {
|
||||
const token = request.headers.authorization?.replace('Bearer ', '');
|
||||
if (!token) throw new ValidationError('No token provided');
|
||||
return await chaptersLessonService.createChapter({
|
||||
token,
|
||||
course_id: courseId,
|
||||
title: body.title,
|
||||
description: body.description,
|
||||
sort_order: body.sort_order,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* อัปเดต chapter
|
||||
* Update a chapter
|
||||
*/
|
||||
@Put('chapters/{chapterId}')
|
||||
@Security('jwt', ['instructor'])
|
||||
@SuccessResponse('200', 'Chapter updated successfully')
|
||||
@Response('401', 'Unauthorized')
|
||||
@Response('403', 'Forbidden')
|
||||
@Response('404', 'Chapter not found')
|
||||
public async updateChapter(
|
||||
@Request() request: any,
|
||||
@Path() courseId: number,
|
||||
@Path() chapterId: number,
|
||||
@Body() body: UpdateChapterBody
|
||||
): Promise<UpdateChapterResponse> {
|
||||
const token = request.headers.authorization?.replace('Bearer ', '');
|
||||
if (!token) throw new ValidationError('No token provided');
|
||||
return await chaptersLessonService.updateChapter({
|
||||
token,
|
||||
course_id: courseId,
|
||||
chapter_id: chapterId,
|
||||
...body,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* ลบ chapter
|
||||
* Delete a chapter
|
||||
*/
|
||||
@Delete('chapters/{chapterId}')
|
||||
@Security('jwt', ['instructor'])
|
||||
@SuccessResponse('200', 'Chapter deleted successfully')
|
||||
@Response('401', 'Unauthorized')
|
||||
@Response('403', 'Forbidden')
|
||||
@Response('404', 'Chapter not found')
|
||||
public async deleteChapter(
|
||||
@Request() request: any,
|
||||
@Path() courseId: number,
|
||||
@Path() chapterId: number
|
||||
): Promise<DeleteChapterResponse> {
|
||||
const token = request.headers.authorization?.replace('Bearer ', '');
|
||||
if (!token) throw new ValidationError('No token provided');
|
||||
return await chaptersLessonService.deleteChapter({ token, course_id: courseId, chapter_id: chapterId });
|
||||
}
|
||||
|
||||
/**
|
||||
* เรียงลำดับ chapter ใหม่
|
||||
* Reorder chapter
|
||||
*/
|
||||
@Put('chapters/{chapterId}/reorder')
|
||||
@Security('jwt', ['instructor'])
|
||||
@SuccessResponse('200', 'Chapter reordered successfully')
|
||||
public async reorderChapter(
|
||||
@Request() request: any,
|
||||
@Path() courseId: number,
|
||||
@Path() chapterId: number,
|
||||
@Body() body: ReorderChapterBody
|
||||
): Promise<ReorderChapterResponse> {
|
||||
const token = request.headers.authorization?.replace('Bearer ', '');
|
||||
if (!token) throw new ValidationError('No token provided');
|
||||
return await chaptersLessonService.reorderChapter({
|
||||
token,
|
||||
course_id: courseId,
|
||||
chapter_id: chapterId,
|
||||
sort_order: body.sort_order,
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Lesson Endpoints
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* ดึงข้อมูล lesson พร้อม attachments และ quiz
|
||||
* Get lesson with attachments and quiz
|
||||
*/
|
||||
@Get('chapters/{chapterId}/lessons/{lessonId}')
|
||||
@Security('jwt', ['instructor'])
|
||||
@SuccessResponse('200', 'Lesson retrieved successfully')
|
||||
public async getLesson(
|
||||
@Request() request: any,
|
||||
@Path() courseId: number,
|
||||
@Path() chapterId: number,
|
||||
@Path() lessonId: number
|
||||
): Promise<GetLessonResponse> {
|
||||
const token = request.headers.authorization?.replace('Bearer ', '');
|
||||
if (!token) throw new ValidationError('No token provided');
|
||||
return await chaptersLessonService.getLesson({ token, course_id: courseId, chapter_id: chapterId, lesson_id: lessonId });
|
||||
}
|
||||
|
||||
/**
|
||||
* สร้าง lesson ใหม่ (สำหรับ QUIZ จะสร้าง quiz shell อัตโนมัติ)
|
||||
* Create a new lesson (for QUIZ type, quiz shell is created automatically)
|
||||
*/
|
||||
@Post('chapters/{chapterId}/lessons')
|
||||
@Security('jwt', ['instructor'])
|
||||
@SuccessResponse('201', 'Lesson created successfully')
|
||||
public async createLesson(
|
||||
@Request() request: any,
|
||||
@Path() courseId: number,
|
||||
@Path() chapterId: number,
|
||||
@Body() body: CreateLessonBody
|
||||
): Promise<CreateLessonResponse> {
|
||||
const token = request.headers.authorization?.replace('Bearer ', '');
|
||||
if (!token) throw new ValidationError('No token provided');
|
||||
return await chaptersLessonService.createLesson({
|
||||
token,
|
||||
course_id: courseId,
|
||||
chapter_id: chapterId,
|
||||
title: body.title,
|
||||
content: body.content,
|
||||
type: body.type,
|
||||
sort_order: body.sort_order,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* อัปเดต lesson
|
||||
* Update a lesson
|
||||
*/
|
||||
@Put('chapters/{chapterId}/lessons/{lessonId}')
|
||||
@Security('jwt', ['instructor'])
|
||||
@SuccessResponse('200', 'Lesson updated successfully')
|
||||
public async updateLesson(
|
||||
@Request() request: any,
|
||||
@Path() courseId: number,
|
||||
@Path() chapterId: number,
|
||||
@Path() lessonId: number,
|
||||
@Body() body: UpdateLessonBody
|
||||
): Promise<UpdateLessonResponse> {
|
||||
const token = request.headers.authorization?.replace('Bearer ', '');
|
||||
if (!token) throw new ValidationError('No token provided');
|
||||
return await chaptersLessonService.updateLesson({
|
||||
token,
|
||||
course_id: courseId,
|
||||
chapter_id: chapterId,
|
||||
lesson_id: lessonId,
|
||||
data: body,
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Video Upload Endpoints (multipart/form-data)
|
||||
// Note: These endpoints should be registered with express router
|
||||
// using multer middleware for actual file upload handling.
|
||||
// See: src/routes/lessons.routes.ts (to be created)
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* ลบ lesson
|
||||
* Delete a lesson (cannot delete if published)
|
||||
*/
|
||||
@Delete('chapters/{chapterId}/lessons/{lessonId}')
|
||||
@Security('jwt', ['instructor'])
|
||||
@SuccessResponse('200', 'Lesson deleted successfully')
|
||||
public async deleteLesson(
|
||||
@Request() request: any,
|
||||
@Path() courseId: number,
|
||||
@Path() chapterId: number,
|
||||
@Path() lessonId: number
|
||||
): Promise<DeleteLessonResponse> {
|
||||
const token = request.headers.authorization?.replace('Bearer ', '');
|
||||
if (!token) throw new ValidationError('No token provided');
|
||||
return await chaptersLessonService.deleteLesson({ token, course_id: courseId, chapter_id: chapterId, lesson_id: lessonId });
|
||||
}
|
||||
|
||||
/**
|
||||
* เรียงลำดับ lessons ใน chapter
|
||||
* Reorder lessons within a chapter
|
||||
*/
|
||||
@Put('chapters/{chapterId}/lessons/reorder')
|
||||
@Security('jwt', ['instructor'])
|
||||
@SuccessResponse('200', 'Lessons reordered successfully')
|
||||
public async reorderLessons(
|
||||
@Request() request: any,
|
||||
@Path() courseId: number,
|
||||
@Path() chapterId: number,
|
||||
@Body() body: ReorderLessonsBody
|
||||
): Promise<ReorderLessonsResponse> {
|
||||
const token = request.headers.authorization?.replace('Bearer ', '');
|
||||
if (!token) throw new ValidationError('No token provided');
|
||||
return await chaptersLessonService.reorderLessons({
|
||||
token,
|
||||
course_id: courseId,
|
||||
chapter_id: chapterId,
|
||||
lesson_ids: body.lesson_ids,
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Quiz Question Endpoints
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* เพิ่มคำถามให้ quiz lesson
|
||||
* Add a question to a quiz lesson
|
||||
*/
|
||||
@Post('chapters/{chapterId}/lessons/{lessonId}/questions')
|
||||
@Security('jwt', ['instructor'])
|
||||
@SuccessResponse('201', 'Question added successfully')
|
||||
public async addQuestion(
|
||||
@Request() request: any,
|
||||
@Path() courseId: number,
|
||||
@Path() chapterId: number,
|
||||
@Path() lessonId: number,
|
||||
@Body() body: AddQuestionBody
|
||||
): Promise<AddQuestionResponse> {
|
||||
const token = request.headers.authorization?.replace('Bearer ', '');
|
||||
if (!token) throw new ValidationError('No token provided');
|
||||
return await chaptersLessonService.addQuestion({
|
||||
token,
|
||||
course_id: courseId,
|
||||
lesson_id: lessonId,
|
||||
...body,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* อัปเดตคำถาม
|
||||
* Update a question
|
||||
*/
|
||||
@Put('chapters/{chapterId}/lessons/{lessonId}/questions/{questionId}')
|
||||
@Security('jwt', ['instructor'])
|
||||
@SuccessResponse('200', 'Question updated successfully')
|
||||
public async updateQuestion(
|
||||
@Request() request: any,
|
||||
@Path() courseId: number,
|
||||
@Path() chapterId: number,
|
||||
@Path() lessonId: number,
|
||||
@Path() questionId: number,
|
||||
@Body() body: UpdateQuestionBody
|
||||
): Promise<UpdateQuestionResponse> {
|
||||
const token = request.headers.authorization?.replace('Bearer ', '');
|
||||
if (!token) throw new ValidationError('No token provided');
|
||||
return await chaptersLessonService.updateQuestion({
|
||||
token,
|
||||
course_id: courseId,
|
||||
lesson_id: lessonId,
|
||||
question_id: questionId,
|
||||
...body,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* ลบคำถาม
|
||||
* Delete a question
|
||||
*/
|
||||
@Delete('chapters/{chapterId}/lessons/{lessonId}/questions/{questionId}')
|
||||
@Security('jwt', ['instructor'])
|
||||
@SuccessResponse('200', 'Question deleted successfully')
|
||||
public async deleteQuestion(
|
||||
@Request() request: any,
|
||||
@Path() courseId: number,
|
||||
@Path() chapterId: number,
|
||||
@Path() lessonId: number,
|
||||
@Path() questionId: number
|
||||
): Promise<DeleteQuestionResponse> {
|
||||
const token = request.headers.authorization?.replace('Bearer ', '');
|
||||
if (!token) throw new ValidationError('No token provided');
|
||||
return await chaptersLessonService.deleteQuestion({
|
||||
token,
|
||||
course_id: courseId,
|
||||
lesson_id: lessonId,
|
||||
question_id: questionId,
|
||||
});
|
||||
}
|
||||
}
|
||||
59
Backend/src/controllers/ChaptersLessonStudentController.ts
Normal file
59
Backend/src/controllers/ChaptersLessonStudentController.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import { Get, Path, Request, Response, Route, Security, SuccessResponse, Tags } from 'tsoa';
|
||||
import { ValidationError } from '../middleware/errorHandler';
|
||||
import { ChaptersLessonService } from '../services/ChaptersLesson.service';
|
||||
import {
|
||||
ListChaptersResponse,
|
||||
GetLessonResponse,
|
||||
} from '../types/ChaptersLesson.typs';
|
||||
|
||||
const chaptersLessonService = new ChaptersLessonService();
|
||||
|
||||
@Route('api/students/courses/{courseId}')
|
||||
@Tags('ChaptersLessons - Student')
|
||||
export class ChaptersLessonStudentController {
|
||||
|
||||
// ============================================
|
||||
// Chapter Endpoints (Read-only for students)
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* ดึง chapters ทั้งหมดของ course (พร้อม lessons) - สำหรับนักเรียนที่ลงทะเบียนแล้ว
|
||||
* Get all chapters of a course with lessons - for enrolled students
|
||||
*/
|
||||
@Get('chapters')
|
||||
@Security('jwt', ['student'])
|
||||
@SuccessResponse('200', 'Chapters retrieved successfully')
|
||||
@Response('401', 'Unauthorized')
|
||||
@Response('403', 'Not enrolled in this course')
|
||||
public async listChapters(@Request() request: any, @Path() courseId: number): Promise<ListChaptersResponse> {
|
||||
const token = request.headers.authorization?.replace('Bearer ', '');
|
||||
if (!token) throw new ValidationError('No token provided');
|
||||
return await chaptersLessonService.listChapters({ token, course_id: courseId });
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Lesson Endpoints (Read-only for students)
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* ดึงข้อมูล lesson พร้อม attachments และ quiz - สำหรับนักเรียนที่ลงทะเบียนแล้ว
|
||||
* Get lesson with attachments and quiz - for enrolled students
|
||||
* หมายเหตุ: จะดูได้เฉพาะ lesson ที่ is_published = true
|
||||
*/
|
||||
@Get('chapters/{chapterId}/lessons/{lessonId}')
|
||||
@Security('jwt', ['student'])
|
||||
@SuccessResponse('200', 'Lesson retrieved successfully')
|
||||
@Response('401', 'Unauthorized')
|
||||
@Response('403', 'Not enrolled or lesson not published')
|
||||
@Response('404', 'Lesson not found')
|
||||
public async getLesson(
|
||||
@Request() request: any,
|
||||
@Path() courseId: number,
|
||||
@Path() chapterId: number,
|
||||
@Path() lessonId: number
|
||||
): Promise<GetLessonResponse> {
|
||||
const token = request.headers.authorization?.replace('Bearer ', '');
|
||||
if (!token) throw new ValidationError('No token provided');
|
||||
return await chaptersLessonService.getLesson({ token, course_id: courseId, chapter_id: chapterId, lesson_id: lessonId });
|
||||
}
|
||||
}
|
||||
|
|
@ -23,25 +23,30 @@ export class LessonsController {
|
|||
* @param video ไฟล์วิดีโอ (สำหรับ type=VIDEO เท่านั้น)
|
||||
* @param attachments ไฟล์แนบ (PDFs, เอกสาร, รูปภาพ)
|
||||
*/
|
||||
async createLesson(req: LessonUploadRequest, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
const token = req.headers.authorization?.replace('Bearer ', '');
|
||||
if (!token) {
|
||||
throw new ValidationError('No token provided');
|
||||
}
|
||||
@Post('upload')
|
||||
@Security('jwt', ['instructor'])
|
||||
@SuccessResponse('201', 'Lesson created successfully')
|
||||
@Response('400', 'Validation error')
|
||||
@Response('401', 'Unauthorized')
|
||||
@Response('403', 'Forbidden')
|
||||
public async createLessonWithFiles(
|
||||
@Request() request: any,
|
||||
@Path() courseId: number,
|
||||
@Path() chapterId: number,
|
||||
@FormField() title: string,
|
||||
@FormField() type: 'VIDEO' | 'QUIZ',
|
||||
@FormField() content?: string,
|
||||
@FormField() sort_order?: string,
|
||||
@UploadedFile() video?: Express.Multer.File,
|
||||
@UploadedFiles() attachments?: Express.Multer.File[]
|
||||
): Promise<CreateLessonResponse> {
|
||||
const token = request.headers.authorization?.replace('Bearer ', '');
|
||||
if (!token) throw new ValidationError('No token provided');
|
||||
|
||||
const courseId = parseInt(req.params.courseId, 10);
|
||||
const chapterId = parseInt(req.params.chapterId, 10);
|
||||
|
||||
if (isNaN(courseId) || isNaN(chapterId)) {
|
||||
throw new ValidationError('Invalid course ID or chapter ID');
|
||||
}
|
||||
|
||||
// Parse JSON fields from multipart form
|
||||
const title = JSON.parse(req.body.title || '{}');
|
||||
const content = req.body.content ? JSON.parse(req.body.content) : undefined;
|
||||
const type = req.body.type as 'VIDEO' | 'QUIZ';
|
||||
const sortOrder = req.body.sort_order ? parseInt(req.body.sort_order, 10) : undefined;
|
||||
// Parse JSON fields
|
||||
const parsedTitle: MultiLanguageText = JSON.parse(title);
|
||||
const parsedContent = content ? JSON.parse(content) : undefined;
|
||||
const sortOrder = sort_order ? parseInt(sort_order, 10) : undefined;
|
||||
|
||||
if (!parsedTitle.th || !parsedTitle.en) {
|
||||
throw new ValidationError('Title must have both Thai (th) and English (en) values');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue