feat: add API endpoint and service logic for reordering quiz questions.
This commit is contained in:
parent
44c61c8fb2
commit
84e4d478c7
3 changed files with 125 additions and 0 deletions
|
|
@ -22,6 +22,8 @@ import {
|
||||||
ReorderLessonsBody,
|
ReorderLessonsBody,
|
||||||
AddQuestionBody,
|
AddQuestionBody,
|
||||||
UpdateQuestionBody,
|
UpdateQuestionBody,
|
||||||
|
ReorderQuestionResponse,
|
||||||
|
ReorderQuestionBody,
|
||||||
} from '../types/ChaptersLesson.typs';
|
} from '../types/ChaptersLesson.typs';
|
||||||
|
|
||||||
const chaptersLessonService = new ChaptersLessonService();
|
const chaptersLessonService = new ChaptersLessonService();
|
||||||
|
|
@ -305,6 +307,28 @@ export class ChaptersLessonInstructorController {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Put('chapters/{chapterId}/lessons/{lessonId}/questions/{questionId}/reorder')
|
||||||
|
@Security('jwt', ['instructor'])
|
||||||
|
@SuccessResponse('200', 'Question reordered successfully')
|
||||||
|
public async reorderQuestion(
|
||||||
|
@Request() request: any,
|
||||||
|
@Path() courseId: number,
|
||||||
|
@Path() chapterId: number,
|
||||||
|
@Path() lessonId: number,
|
||||||
|
@Path() questionId: number,
|
||||||
|
@Body() body: ReorderQuestionBody
|
||||||
|
): Promise<ReorderQuestionResponse> {
|
||||||
|
const token = request.headers.authorization?.replace('Bearer ', '');
|
||||||
|
if (!token) throw new ValidationError('No token provided');
|
||||||
|
return await chaptersLessonService.reorderQuestion({
|
||||||
|
token,
|
||||||
|
course_id: courseId,
|
||||||
|
lesson_id: lessonId,
|
||||||
|
question_id: questionId,
|
||||||
|
sort_order: body.sort_order,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ลบคำถาม
|
* ลบคำถาม
|
||||||
* Delete a question
|
* Delete a question
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,8 @@ import {
|
||||||
DeleteQuestionInput,
|
DeleteQuestionInput,
|
||||||
DeleteQuestionResponse,
|
DeleteQuestionResponse,
|
||||||
QuizQuestionData,
|
QuizQuestionData,
|
||||||
|
ReorderQuestionInput,
|
||||||
|
ReorderQuestionResponse,
|
||||||
} from "../types/ChaptersLesson.typs";
|
} from "../types/ChaptersLesson.typs";
|
||||||
import { CoursesInstructorService } from './CoursesInstructor.service';
|
import { CoursesInstructorService } from './CoursesInstructor.service';
|
||||||
|
|
||||||
|
|
@ -832,6 +834,87 @@ export class ChaptersLessonService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async reorderQuestion(request: ReorderQuestionInput): Promise<ReorderQuestionResponse> {
|
||||||
|
try {
|
||||||
|
const { token, course_id, lesson_id, question_id, sort_order } = 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: true }
|
||||||
|
});
|
||||||
|
if (!lesson) throw new NotFoundError('Lesson not found');
|
||||||
|
if (lesson.type !== 'QUIZ') throw new ValidationError('Cannot reorder question in non-QUIZ type lesson');
|
||||||
|
if (!lesson.quiz) throw new NotFoundError('Quiz not found for this lesson');
|
||||||
|
|
||||||
|
// Verify question exists and belongs to this quiz
|
||||||
|
const existingQuestion = await prisma.question.findUnique({ where: { id: question_id } });
|
||||||
|
if (!existingQuestion || existingQuestion.quiz_id !== lesson.quiz.id) {
|
||||||
|
throw new NotFoundError('Question not found in this quiz');
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldSortOrder = existingQuestion.sort_order;
|
||||||
|
const quizId = lesson.quiz.id;
|
||||||
|
|
||||||
|
// If same position, no need to reorder
|
||||||
|
if (oldSortOrder === sort_order) {
|
||||||
|
const questions = await prisma.question.findMany({
|
||||||
|
where: { quiz_id: quizId },
|
||||||
|
orderBy: { sort_order: 'asc' },
|
||||||
|
include: { choices: { orderBy: { sort_order: 'asc' } } }
|
||||||
|
});
|
||||||
|
return { code: 200, message: 'Question reordered successfully', data: questions as QuizQuestionData[] };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shift other questions to make room for the insert
|
||||||
|
if (oldSortOrder > sort_order) {
|
||||||
|
// Moving up: shift questions between newSortOrder and oldSortOrder-1 down by 1
|
||||||
|
await prisma.question.updateMany({
|
||||||
|
where: {
|
||||||
|
quiz_id: quizId,
|
||||||
|
sort_order: { gte: sort_order, lt: oldSortOrder }
|
||||||
|
},
|
||||||
|
data: { sort_order: { increment: 1 } }
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Moving down: shift questions between oldSortOrder+1 and newSortOrder up by 1
|
||||||
|
await prisma.question.updateMany({
|
||||||
|
where: {
|
||||||
|
quiz_id: quizId,
|
||||||
|
sort_order: { gt: oldSortOrder, lte: sort_order }
|
||||||
|
},
|
||||||
|
data: { sort_order: { decrement: 1 } }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the question's sort order
|
||||||
|
await prisma.question.update({
|
||||||
|
where: { id: question_id },
|
||||||
|
data: { sort_order }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fetch all questions with updated order
|
||||||
|
const questions = await prisma.question.findMany({
|
||||||
|
where: { quiz_id: quizId },
|
||||||
|
orderBy: { sort_order: 'asc' },
|
||||||
|
include: { choices: { orderBy: { sort_order: 'asc' } } }
|
||||||
|
});
|
||||||
|
|
||||||
|
return { code: 200, message: 'Question reordered successfully', data: questions as QuizQuestionData[] };
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error reordering question: ${error}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ลบคำถาม
|
* ลบคำถาม
|
||||||
* Delete a question and all its choices
|
* Delete a question and all its choices
|
||||||
|
|
|
||||||
|
|
@ -477,6 +477,20 @@ export interface DeleteQuestionInput {
|
||||||
question_id: number;
|
question_id: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ReorderQuestionInput {
|
||||||
|
token: string;
|
||||||
|
course_id: number;
|
||||||
|
lesson_id: number;
|
||||||
|
question_id: number;
|
||||||
|
sort_order: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReorderQuestionResponse {
|
||||||
|
code: number;
|
||||||
|
message: string;
|
||||||
|
data: QuizQuestionData[];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Response for deleting a question
|
* Response for deleting a question
|
||||||
*/
|
*/
|
||||||
|
|
@ -526,6 +540,10 @@ export interface ReorderLessonsBody {
|
||||||
sort_order: number;
|
sort_order: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ReorderQuestionBody {
|
||||||
|
sort_order: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AddQuestionBody {
|
export interface AddQuestionBody {
|
||||||
question: MultiLanguageText;
|
question: MultiLanguageText;
|
||||||
explanation?: MultiLanguageText;
|
explanation?: MultiLanguageText;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue