/
TimurIsm
/
OpenEyes
Обзор
Документация
Войти
/
TimurIsm
/
OpenEyes
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
main
back/controllers/testsController.js
295 строк
12 KB
TimurIsm
first_commit
16 май 2026, 15:34
16 май 2026, 15:34
4d3f198
Код
Авторство
О чём код?
const { VisionTest, TestQuestion } = require('../models/models'); const ApiError = require('../error/ApiError'); const { Op } = require('sequelize'); const { deepTrim, isEmptyOrInvisible } = require('../utils/stringUtils'); const MAX_TYPE_LENGTH = 100; const MAX_SHORT_DESC_LENGTH = 250; const MAX_MAIN_IMAGE_LENGTH = 100; const MAX_RESULT_LENGTH = 300; const MAX_CONTENT_LENGTH = 100; const MAX_ANSWER_LENGTH = 100; const MAX_OPTION_LENGTH = 100; const MAX_OPTIONS_COUNT = 7; const validateTextField = (value, fieldName, maxLength, isRequired = true) => { if (isRequired && (!value || isEmptyOrInvisible(value))) { throw new Error(`${fieldName} не может быть пустым!`); } if (value && value.length > maxLength) { throw new Error(`${fieldName} не может превышать ${maxLength} символов!`); } return value; }; const validateTestBasicFields = (type, short_description, full_description, main_image) => { validateTextField(type, 'Название теста', MAX_TYPE_LENGTH); validateTextField(short_description, 'Краткое описание', MAX_SHORT_DESC_LENGTH); validateTextField(full_description, 'Полное описание', MAX_SHORT_DESC_LENGTH); validateTextField(main_image, 'Главное изображение', MAX_MAIN_IMAGE_LENGTH); }; const validateResults = (results) => { if (!results || !Array.isArray(results) || results.length === 0) { throw new Error('Необходимо указать результаты для теста!'); } const processedResults = results.map(result => ({ ...result, text: deepTrim(result.text) })); for (const result of processedResults) { validateTextField(result.text, 'Текст результата', MAX_RESULT_LENGTH); } const hasSuccess = processedResults.some(r => r.type === 'SUCCESS'); const hasFailure = processedResults.some(r => r.type === 'FAILURE'); if (!hasSuccess || !hasFailure) { throw new Error('Необходимо указать результаты прохождения теста!'); } return processedResults; }; const validateQuestion = (question, index) => { const cleanedContent = deepTrim(question.content); const cleanedCorrectAnswer = deepTrim(question.correct_answer); const cleanedAnswerOptions = (question.answer_options || []).map(opt => deepTrim(opt)); validateTextField(cleanedContent, `Содержание вопроса ${index + 1}`, MAX_CONTENT_LENGTH); validateTextField(cleanedCorrectAnswer, `Правильный ответ для вопроса ${index + 1}`, MAX_ANSWER_LENGTH); if (question.data_type === 'select') { if (cleanedAnswerOptions.length === 0) { throw new Error(`Для вопроса ${index + 1} с типом "select" необходимо добавить хотя бы один вариант ответа!`); } if (cleanedAnswerOptions.length > MAX_OPTIONS_COUNT) { throw new Error(`Для вопроса ${index + 1} количество вариантов ответа не может превышать ${MAX_OPTIONS_COUNT}!`); } for (let j = 0; j < cleanedAnswerOptions.length; j++) { validateTextField(cleanedAnswerOptions[j], `Вариант ответа ${j + 1} для вопроса ${index + 1}`, MAX_OPTION_LENGTH); } if (!cleanedAnswerOptions.includes(cleanedCorrectAnswer)) { throw new Error(`Для вопроса ${index + 1} правильный ответ должен соответствовать одному из вариантов ответа!`); } } return { data_type: question.data_type, content: cleanedContent, correct_answer: cleanedCorrectAnswer, answer_options: cleanedAnswerOptions }; }; const validateQuestions = (questions) => { if (!Array.isArray(questions) || questions.length === 0) { throw new Error('Добавьте хотя бы один вопрос!'); } return questions.map((question, i) => validateQuestion(question, i)); }; const parseTestResponse = (test) => { const testJson = test.toJSON(); testJson.results = JSON.parse(testJson.results || '[]'); if (testJson.questions) { testJson.questions = testJson.questions.map(question => ({ ...question, answer_options: JSON.parse(question.answer_options || '[]') })); } return testJson; }; const processTestData = (body, adminId = null) => { let { type, short_description, full_description, main_image, results, questions } = body; type = deepTrim(type); short_description = deepTrim(short_description); full_description = deepTrim(full_description); main_image = deepTrim(main_image); validateTestBasicFields(type, short_description, full_description, main_image); const processedResults = validateResults(results); const processedQuestions = validateQuestions(questions); return { type, short_description, full_description, main_image, results: processedResults, questions: processedQuestions, adminId }; }; class TestsController { async createTest(req, res, next) { try { const { type, short_description, full_description, main_image, results, questions, adminId } = processTestData(req.body, req.user.id); const existingTest = await VisionTest.findOne({ where: { type } }); if (existingTest) { return next(ApiError.badRequest('Тест с таким названием уже существует!')); } const test = await VisionTest.create({ type, short_description, full_description, main_image, results: JSON.stringify(results), adminId }); const questionPromises = questions.map(question => TestQuestion.create({ data_type: question.data_type, content: question.content, correct_answer: question.correct_answer, answer_options: JSON.stringify(question.answer_options || []), testId: test.id, }) ); await Promise.all(questionPromises); const createdTest = await VisionTest.findOne({ where: { id: test.id }, include: [{ model: TestQuestion, as: 'questions' }], }); return res.json(parseTestResponse(createdTest)); } catch (e) { if (e instanceof Error && e.message.includes('не может быть пустым') || e.message.includes('превышать')) { return next(ApiError.badRequest(e.message)); } return next(ApiError.internal('Ошибка при создании теста!')); } } async getAllTests(req, res, next) { try { const tests = await VisionTest.findAll({ include: [{ model: TestQuestion, as: 'questions' }], order: [['id', 'ASC']], }); const parsedTests = tests.map(parseTestResponse); return res.json(parsedTests); } catch (e) { return next(ApiError.internal('Ошибка при получении тестов!')); } } async getTestById(req, res, next) { const { id } = req.params; try { const test = await VisionTest.findOne({ where: { id }, include: [{ model: TestQuestion, as: 'questions' }], }); if (!test) { return next(ApiError.badRequest('Тест не найден!')); } return res.json(parseTestResponse(test)); } catch (e) { return next(ApiError.internal('Ошибка при получении теста!')); } } async deleteTest(req, res, next) { const { id } = req.params; try { const test = await VisionTest.findOne({ where: { id } }); if (!test) { return next(ApiError.badRequest('Тест не найден!')); } await TestQuestion.destroy({ where: { testId: id } }); await VisionTest.destroy({ where: { id } }); return res.json({ message: 'Тест успешно удален!' }); } catch (e) { return next(ApiError.internal('Ошибка при удалении теста!')); } } async updateTest(req, res, next) { const { id } = req.params; try { const test = await VisionTest.findOne({ where: { id } }); if (!test) { return next(ApiError.badRequest('Тест не найден!')); } const { type, short_description, full_description, main_image, results, questions } = processTestData(req.body); const existingTest = await VisionTest.findOne({ where: { type, id: { [Op.ne]: id } } }); if (existingTest) { return next(ApiError.badRequest('Тест с таким названием уже существует!')); } await VisionTest.update( { type, short_description, full_description, main_image, results: JSON.stringify(results) }, { where: { id } } ); const updatedQuestions = []; for (let i = 0; i < questions.length; i++) { const question = questions[i]; if (question.id) { await TestQuestion.update( { data_type: question.data_type, content: question.content, correct_answer: question.correct_answer, answer_options: JSON.stringify(question.answer_options), }, { where: { id: question.id } } ); } else { const newQuestion = await TestQuestion.create({ data_type: question.data_type, content: question.content, correct_answer: question.correct_answer, answer_options: JSON.stringify(question.answer_options), testId: id, }); updatedQuestions.push(newQuestion.id); } } const existingQuestionIds = questions.map(q => q.id).filter(Boolean); const allQuestionIds = [...existingQuestionIds, ...updatedQuestions]; await TestQuestion.destroy({ where: { testId: id, id: { [Op.notIn]: allQuestionIds } }, }); const updatedTest = await VisionTest.findOne({ where: { id }, include: [{ model: TestQuestion, as: 'questions' }], }); return res.json(parseTestResponse(updatedTest)); } catch (e) { if (e instanceof Error && e.message.includes('не может быть пустым') || e.message.includes('превышать')) { return next(ApiError.badRequest(e.message)); } return next(ApiError.internal('Ошибка при обновлении теста!')); } } } module.exports = new TestsController();