Skip to content

Commit fa02ace

Browse files
committed
fix: fix csv import
1 parent 2b65795 commit fa02ace

1 file changed

Lines changed: 149 additions & 40 deletions

File tree

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

Lines changed: 149 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -221,11 +221,6 @@ export class QuestionBanksService {
221221
throw new BadRequestException('File is required');
222222
}
223223

224-
const workbook = xlsx.read(file.buffer, { type: 'buffer' });
225-
const sheetName = workbook.SheetNames[0];
226-
const worksheet = workbook.Sheets[sheetName];
227-
const data = xlsx.utils.sheet_to_json(worksheet);
228-
229224
// Lấy danh sách questionText hiện có trong course để kiểm tra trùng lặp
230225
const existingQuestions = await this.questionRepository.find({
231226
where: { courseId },
@@ -235,6 +230,53 @@ export class QuestionBanksService {
235230
existingQuestions.map((q) => q.questionText.toLowerCase().trim()),
236231
);
237232

233+
// Parse file (CSV hoặc XLSX)
234+
// xlsx library tự động xử lý cả CSV và XLSX
235+
const workbook = xlsx.read(file.buffer, {
236+
type: 'buffer',
237+
cellDates: false,
238+
cellNF: false,
239+
cellText: false,
240+
});
241+
const sheetName = workbook.SheetNames[0];
242+
const worksheet = workbook.Sheets[sheetName];
243+
244+
// Parse với header tự động (sẽ tạo object với keys từ dòng đầu tiên)
245+
// Không dùng header: 1 vì nó sẽ trả về array of arrays thay vì array of objects
246+
let data = xlsx.utils.sheet_to_json(worksheet, {
247+
defval: '', // Giá trị mặc định cho cell rỗng
248+
raw: false, // Convert tất cả về string
249+
blankrows: false, // Bỏ qua dòng trống
250+
});
251+
252+
// Normalize tên cột: xóa khoảng trắng thừa và chuẩn hóa
253+
// Xử lý trường hợp xlsx có thể parse tên cột khác đi (do dấu ngoặc kép trong CSV)
254+
data = data.map((row: any) => {
255+
const normalizedRow: Record<string, any> = {};
256+
257+
// Map các tên cột có thể có (xlsx có thể parse khác do dấu ngoặc kép)
258+
Object.keys(row).forEach((key) => {
259+
const normalizedKey = key.trim().replace(/^["']|["']$/g, ''); // Xóa dấu ngoặc kép nếu có
260+
const value = row[key];
261+
262+
// Chuẩn hóa tên cột
263+
if (normalizedKey === 'Question Text' || normalizedKey === 'QuestionText' || normalizedKey === 'Question') {
264+
normalizedRow['Question Text'] = value;
265+
} else if (normalizedKey === 'Choices' || normalizedKey === 'Choice') {
266+
normalizedRow['Choices'] = value;
267+
} else if (normalizedKey === 'Correct Answer' || normalizedKey === 'CorrectAnswer' || normalizedKey === 'Correct') {
268+
normalizedRow['Correct Answer'] = value;
269+
} else if (normalizedKey === 'Difficulty') {
270+
normalizedRow['Difficulty'] = value;
271+
} else {
272+
// Giữ nguyên nếu không match (có thể là tên cột khác)
273+
normalizedRow[key] = value;
274+
}
275+
});
276+
277+
return normalizedRow;
278+
});
279+
238280
// Mảng để lưu kết quả từng câu hỏi
239281
const questionResults: Array<{
240282
rowNumber: number;
@@ -252,30 +294,49 @@ export class QuestionBanksService {
252294
for (const [index, row] of data.entries()) {
253295
const rowNumber = index + 2; // 1-based index + header row
254296
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;
297+
298+
// Debug: Log row data để kiểm tra
299+
// console.log(`Row ${rowNumber}:`, JSON.stringify(rowData));
300+
301+
// Lấy questionText, xử lý các trường hợp có thể có
302+
let questionText = '';
303+
if (rowData['Question Text']) {
304+
questionText = String(rowData['Question Text']).trim();
305+
} else if (rowData['QuestionText']) {
306+
// Fallback nếu không có khoảng trắng
307+
questionText = String(rowData['QuestionText']).trim();
308+
} else if (rowData['Question']) {
309+
// Fallback nếu chỉ có "Question"
310+
questionText = String(rowData['Question']).trim();
267311
}
268-
questionTextsInFile.add(questionTextLower);
269312

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;
313+
// Nếu questionText rỗng, bỏ qua kiểm tra trùng lặp và để mapRowToQuestionDto báo lỗi
314+
let questionTextLower = '';
315+
if (questionText) {
316+
questionTextLower = questionText.toLowerCase();
317+
318+
// Kiểm tra trùng lặp trong file
319+
if (questionTextsInFile.has(questionTextLower)) {
320+
questionResults.push({
321+
rowNumber,
322+
questionText,
323+
success: false,
324+
error: 'Question is duplicated in file',
325+
});
326+
continue;
327+
}
328+
questionTextsInFile.add(questionTextLower);
329+
330+
// Kiểm tra trùng lặp với database
331+
if (existingQuestionTexts.has(questionTextLower)) {
332+
questionResults.push({
333+
rowNumber,
334+
questionText,
335+
success: false,
336+
error: 'Question already exists in question bank',
337+
});
338+
continue;
339+
}
279340
}
280341

281342
// Validate và map dữ liệu
@@ -327,41 +388,89 @@ export class QuestionBanksService {
327388
}
328389

329390
private mapRowToQuestionDto(row: any, courseId: string, createdById: string): CreateQuestionDto {
330-
if (!row['Question Text'] || !row['Choices'] || !row['Correct Answer'] || !row['Difficulty']) {
331-
throw new Error('Missing required fields');
391+
// Kiểm tra và convert các trường về string, xử lý null/undefined
392+
const questionText = row['Question Text']
393+
? String(row['Question Text']).trim()
394+
: null;
395+
const choicesValue = row['Choices']
396+
? String(row['Choices']).trim()
397+
: null;
398+
const correctAnswerValue = row['Correct Answer'] !== undefined && row['Correct Answer'] !== null
399+
? String(row['Correct Answer']).trim()
400+
: null;
401+
const difficultyValue = row['Difficulty']
402+
? String(row['Difficulty']).trim()
403+
: null;
404+
405+
// Kiểm tra từng trường và báo lỗi cụ thể
406+
const missingFields: string[] = [];
407+
if (!questionText) missingFields.push('Question Text');
408+
if (!choicesValue) missingFields.push('Choices');
409+
if (!correctAnswerValue) missingFields.push('Correct Answer');
410+
if (!difficultyValue) missingFields.push('Difficulty');
411+
412+
if (missingFields.length > 0) {
413+
throw new Error(`Missing required fields: ${missingFields.join(', ')}`);
332414
}
333415

416+
// Parse Choices
417+
if (!choicesValue) {
418+
throw new Error('Choices is required');
419+
}
334420
let choices: string[];
335421
try {
336422
// Try parsing as JSON array first
337-
choices = JSON.parse(row['Choices']);
423+
choices = JSON.parse(choicesValue);
424+
if (!Array.isArray(choices)) {
425+
throw new Error('Choices must be a JSON array');
426+
}
338427
} catch {
339-
// Fallback: split by comma or pipe if it's a string
340-
if (typeof row['Choices'] === 'string') {
341-
choices = row['Choices'].split('|').map((c) => c.trim());
428+
// Fallback: split by pipe if it's a string
429+
if (typeof choicesValue === 'string') {
430+
choices = choicesValue.split('|').map((c) => c.trim()).filter((c) => c.length > 0);
342431
} else {
343-
throw new Error('Invalid choices format');
432+
throw new Error('Invalid choices format: must be a string separated by "|" or a JSON array');
344433
}
345434
}
346435

436+
// Validate số lượng choices
347437
if (!Array.isArray(choices) || choices.length < 2) {
348-
throw new Error('At least 2 choices are required');
438+
throw new Error(`At least 2 choices are required, but found ${choices.length} choice(s)`);
439+
}
440+
441+
// Kiểm tra choices rỗng
442+
const emptyChoices = choices.filter((c) => !c || c.trim().length === 0);
443+
if (emptyChoices.length > 0) {
444+
throw new Error(`Found ${emptyChoices.length} empty choice(s). All choices must have content.`);
349445
}
350446

351-
const correctAnswer = Number(row['Correct Answer']);
447+
// Validate Correct Answer
448+
const correctAnswer = Number(correctAnswerValue);
449+
if (isNaN(correctAnswer)) {
450+
throw new Error(`Correct Answer must be a number, but got: ${correctAnswerValue}`);
451+
}
452+
352453
if (correctAnswer < 0 || correctAnswer >= choices.length) {
353-
throw new Error(`correctAnswer must be between 0 and ${choices.length - 1}`);
454+
throw new Error(`Correct Answer must be between 0 and ${choices.length - 1}, but got: ${correctAnswer}`);
354455
}
355456

356-
const difficulty = row['Difficulty'].toLowerCase();
457+
// Validate Difficulty
458+
if (!difficultyValue) {
459+
throw new Error('Difficulty is required');
460+
}
461+
const difficulty = difficultyValue.toLowerCase();
357462
if (!Object.values(QuestionDifficulty).includes(difficulty as QuestionDifficulty)) {
358-
throw new Error(`Invalid difficulty: ${difficulty}`);
463+
throw new Error(`Invalid difficulty: "${difficultyValue}". Must be one of: ${Object.values(QuestionDifficulty).join(', ')}`);
464+
}
465+
466+
if (!questionText) {
467+
throw new Error('Question Text is required');
359468
}
360469

361470
return {
362471
courseId,
363472
createdById,
364-
questionText: row['Question Text'],
473+
questionText,
365474
choices,
366475
correctAnswer,
367476
difficulty: difficulty as QuestionDifficulty,

0 commit comments

Comments
 (0)