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 video ไฟล์วิดีโอ (สำหรับ type=VIDEO เท่านั้น)
|
||||||
* @param attachments ไฟล์แนบ (PDFs, เอกสาร, รูปภาพ)
|
* @param attachments ไฟล์แนบ (PDFs, เอกสาร, รูปภาพ)
|
||||||
*/
|
*/
|
||||||
async createLesson(req: LessonUploadRequest, res: Response, next: NextFunction): Promise<void> {
|
@Post('upload')
|
||||||
try {
|
@Security('jwt', ['instructor'])
|
||||||
const token = req.headers.authorization?.replace('Bearer ', '');
|
@SuccessResponse('201', 'Lesson created successfully')
|
||||||
if (!token) {
|
@Response('400', 'Validation error')
|
||||||
throw new ValidationError('No token provided');
|
@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);
|
// Parse JSON fields
|
||||||
const chapterId = parseInt(req.params.chapterId, 10);
|
const parsedTitle: MultiLanguageText = JSON.parse(title);
|
||||||
|
const parsedContent = content ? JSON.parse(content) : undefined;
|
||||||
if (isNaN(courseId) || isNaN(chapterId)) {
|
const sortOrder = sort_order ? parseInt(sort_order, 10) : undefined;
|
||||||
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;
|
|
||||||
|
|
||||||
if (!parsedTitle.th || !parsedTitle.en) {
|
if (!parsedTitle.th || !parsedTitle.en) {
|
||||||
throw new ValidationError('Title must have both Thai (th) and English (en) values');
|
throw new ValidationError('Title must have both Thai (th) and English (en) values');
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue