Skip to content

Commit e2c5d58

Browse files
authored
Merge pull request #69 from nhandang02/develop
refactor: chang response data for endpoint imoport csv
2 parents afd0a62 + b09999c commit e2c5d58

3 files changed

Lines changed: 145 additions & 13 deletions

File tree

src/files/files.service.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export class FilesService {
4040
accessKeyId: process.env.CLOUDFLARE_R2_ACCESS_KEY!,
4141
secretAccessKey: process.env.CLOUDFLARE_R2_SECRET_KEY!,
4242
},
43+
forcePathStyle: true, // Required for Cloudflare R2 compatibility
4344
});
4445
}
4546

src/question-banks/question-banks.controller.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,30 @@ export class QuestionBanksController {
7070
description: 'Lấy danh sách questions. Chỉ instructor mới có thể xem. Có thể filter theo courseId. Instructor chỉ thấy questions của courses mà họ tạo.'
7171
})
7272
@ApiQuery({ name: 'courseId', required: false, description: 'Filter by course ID' })
73+
@ApiQuery({ name: 'page', required: false, type: Number, description: 'Page number (default: 1)' })
74+
@ApiQuery({ name: 'limit', required: false, type: Number, description: 'Items per page (default: 50, max: 100)' })
7375
@ApiResponse({ status: 200, description: 'Questions retrieved successfully' })
7476
findAll(
7577
@Req() req: RequestWithUser,
7678
@Query('courseId') courseId?: string,
79+
@Query('page') page?: string,
80+
@Query('limit') limit?: string,
7781
) {
78-
return this.questionBanksService.findAll(req.user, courseId);
82+
const pageNum = page ? Math.max(1, parseInt(page, 10)) : 1;
83+
const limitNum = limit ? Math.min(100, Math.max(1, parseInt(limit, 10))) : 50;
84+
85+
const result = this.questionBanksService.findAll(req.user, courseId, pageNum, limitNum);
86+
return result.then((data) => ({
87+
success: true,
88+
message: 'Questions retrieved successfully',
89+
data: data.data,
90+
pagination: {
91+
total: data.total,
92+
page: data.page,
93+
limit: data.limit,
94+
totalPages: Math.ceil(data.total / data.limit),
95+
},
96+
}));
7997
}
8098

8199
@Get(':id')

src/question-banks/question-banks.service.ts

Lines changed: 125 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,24 @@ export class QuestionBanksService {
5151
throw new BadRequestException('At least 2 choices are required');
5252
}
5353

54+
// Kiểm tra trùng lặp questionText trong cùng course
55+
const questionTextLower = createQuestionDto.questionText.toLowerCase().trim();
56+
const allQuestionsInCourse = await this.questionRepository.find({
57+
where: { courseId: createQuestionDto.courseId },
58+
select: ['questionText'],
59+
});
60+
61+
const isDuplicate = allQuestionsInCourse.some(
62+
(q) => q.questionText.toLowerCase().trim() === questionTextLower,
63+
);
64+
65+
if (isDuplicate) {
66+
throw new BadRequestException({
67+
message: 'Question already exists in question bank',
68+
statusCode: 400,
69+
});
70+
}
71+
5472
// Set createdById from user
5573
const question = this.questionRepository.create({
5674
...createQuestionDto,
@@ -59,11 +77,21 @@ export class QuestionBanksService {
5977
return this.questionRepository.save(question);
6078
}
6179

62-
async findAll(user?: User, courseId?: string): Promise<Question[]> {
80+
async findAll(
81+
user?: User,
82+
courseId?: string,
83+
page: number = 1,
84+
limit: number = 50,
85+
): Promise<{ data: Question[]; total: number; page: number; limit: number }> {
6386
if (!user || user.role !== UserRole.INSTRUCTOR) {
6487
throw new ForbiddenException('Only instructors can view questions');
6588
}
6689

90+
// Validate pagination parameters
91+
const pageNum = Math.max(1, page);
92+
const limitNum = Math.min(100, Math.max(1, limit));
93+
const skip = (pageNum - 1) * limitNum;
94+
6795
// Instructor: Xem questions của courses mà họ tạo
6896
const queryBuilder = this.questionRepository
6997
.createQueryBuilder('question')
@@ -76,7 +104,17 @@ export class QuestionBanksService {
76104
queryBuilder.andWhere('question.courseId = :courseId', { courseId });
77105
}
78106

79-
return queryBuilder.orderBy('question.createdAt', 'DESC').getMany();
107+
// Get total count
108+
const total = await queryBuilder.getCount();
109+
110+
// Get paginated data
111+
const data = await queryBuilder
112+
.orderBy('question.createdAt', 'DESC')
113+
.skip(skip)
114+
.take(limitNum)
115+
.getMany();
116+
117+
return { data, total, page: pageNum, limit: limitNum };
80118
}
81119

82120
async findOne(id: string, user?: User): Promise<Question> {
@@ -188,28 +226,103 @@ export class QuestionBanksService {
188226
const worksheet = workbook.Sheets[sheetName];
189227
const data = xlsx.utils.sheet_to_json(worksheet);
190228

191-
const questionsToCreate: CreateQuestionDto[] = [];
192-
const errors: string[] = [];
193-
229+
// Lấy danh sách questionText hiện có trong course để kiểm tra trùng lặp
230+
const existingQuestions = await this.questionRepository.find({
231+
where: { courseId },
232+
select: ['questionText'],
233+
});
234+
const existingQuestionTexts = new Set(
235+
existingQuestions.map((q) => q.questionText.toLowerCase().trim()),
236+
);
237+
238+
// Mảng để lưu kết quả từng câu hỏi
239+
const questionResults: Array<{
240+
rowNumber: number;
241+
questionText: string;
242+
success: boolean;
243+
questionId?: string;
244+
error?: string;
245+
}> = [];
246+
247+
// Mảng để lưu các câu hỏi hợp lệ để tạo (kèm index để map lại)
248+
const questionsToCreate: Array<{ dto: CreateQuestionDto; resultIndex: number }> = [];
249+
const questionTextsInFile = new Set<string>(); // Để kiểm tra trùng lặp trong file
250+
251+
// Xử lý từng dòng trong file
194252
for (const [index, row] of data.entries()) {
195253
const rowNumber = index + 2; // 1-based index + header row
254+
const rowData = row as Record<string, any>;
255+
const questionText = rowData['Question Text']?.toString().trim() || '';
256+
257+
// Kiểm tra trùng lặp trong file
258+
const questionTextLower = questionText.toLowerCase();
259+
if (questionTextsInFile.has(questionTextLower)) {
260+
questionResults.push({
261+
rowNumber,
262+
questionText,
263+
success: false,
264+
error: 'Question is duplicated in file',
265+
});
266+
continue;
267+
}
268+
questionTextsInFile.add(questionTextLower);
269+
270+
// Kiểm tra trùng lặp với database
271+
if (existingQuestionTexts.has(questionTextLower)) {
272+
questionResults.push({
273+
rowNumber,
274+
questionText,
275+
success: false,
276+
error: 'Question already exists in question bank',
277+
});
278+
continue;
279+
}
280+
281+
// Validate và map dữ liệu
196282
try {
197-
const questionDto = this.mapRowToQuestionDto(row, courseId, user.id);
198-
questionsToCreate.push(questionDto);
283+
const questionDto = this.mapRowToQuestionDto(rowData, courseId, user.id);
284+
const resultIndex = questionResults.length;
285+
questionsToCreate.push({ dto: questionDto, resultIndex });
286+
287+
// Tạm thời đánh dấu là success, sẽ cập nhật questionId sau khi save
288+
questionResults.push({
289+
rowNumber,
290+
questionText,
291+
success: true,
292+
});
199293
} catch (error) {
200-
errors.push(`Row ${rowNumber}: ${error.message}`);
294+
questionResults.push({
295+
rowNumber,
296+
questionText,
297+
success: false,
298+
error: error.message || 'Unknown error',
299+
});
201300
}
202301
}
203302

303+
// Lưu các câu hỏi hợp lệ vào database
204304
if (questionsToCreate.length > 0) {
205-
await this.questionRepository.save(this.questionRepository.create(questionsToCreate));
305+
const dtosToCreate = questionsToCreate.map((item) => item.dto);
306+
const savedQuestions = await this.questionRepository.save(
307+
this.questionRepository.create(dtosToCreate),
308+
);
309+
310+
// Cập nhật questionId cho các câu hỏi đã lưu thành công
311+
for (let i = 0; i < questionsToCreate.length; i++) {
312+
const { resultIndex } = questionsToCreate[i];
313+
questionResults[resultIndex].questionId = savedQuestions[i].id;
314+
}
206315
}
207316

317+
// Tính toán thống kê
318+
const imported = questionResults.filter((r) => r.success).length;
319+
const failed = questionResults.filter((r) => !r.success).length;
320+
208321
return {
209322
total: data.length,
210-
imported: questionsToCreate.length,
211-
failed: errors.length,
212-
errors,
323+
imported,
324+
failed,
325+
questions: questionResults, // Mảng chi tiết từng câu hỏi
213326
};
214327
}
215328

0 commit comments

Comments
 (0)