diff --git a/controllers/contentControllers/exercise.js b/controllers/contentControllers/exercise.js index 75af06b..8c4c4a8 100644 --- a/controllers/contentControllers/exercise.js +++ b/controllers/contentControllers/exercise.js @@ -392,6 +392,97 @@ export const getExerciseByLevelId = async (req, res) => { } }; +export const getExerciseByLevelIdForAdmin = async (req, res) => { + try { + const { idLevel } = req.params; + + const levelExists = await models.Level.findByPk(idLevel, { + include: [ + { + model: models.Topic, + as: "levelTopic", + attributes: ["NAME_TOPIC"], + include: [ + { + model: models.Section, + as: "topicSection", + attributes: ["NAME_SECTION"], + }, + ], + }, + ], + attributes: ["NAME_LEVEL"], + }); + + if (!levelExists) { + return response(404, null, "Level not found", res); + } + + const exercises = await models.Exercise.findAll({ + where: { ID_LEVEL: idLevel, IS_DELETED: 0 }, + include: [ + { model: models.MultipleChoices, as: "multipleChoices" }, + { model: models.MatchingPairs, as: "matchingPairs" }, + { model: models.TrueFalse, as: "trueFalse" }, + ], + order: [["TITLE", "ASC"]], + }); + + if (!exercises || exercises.length === 0) { + return response(404, null, "No exercises found for this level", res); + } + + const formattedExercises = exercises.map((exercise) => { + const exerciseData = { ...exercise.dataValues }; + const questionType = exercise.QUESTION_TYPE; + + if (questionType === "MCQ") { + if (exerciseData.multipleChoices) { + exerciseData.multipleChoices = exerciseData.multipleChoices.map( + (choice) => choice.dataValues + ); + } + delete exerciseData.matchingPairs; + delete exerciseData.trueFalse; + } else if (questionType === "MPQ") { + if (exerciseData.matchingPairs) { + exerciseData.matchingPairs = exerciseData.matchingPairs.map( + (pair) => pair.dataValues + ); + } + delete exerciseData.multipleChoices; + delete exerciseData.trueFalse; + } else if (questionType === "TFQ") { + if (exerciseData.trueFalse) { + exerciseData.trueFalse = exerciseData.trueFalse.map( + (tf) => tf.dataValues + ); + } + delete exerciseData.multipleChoices; + delete exerciseData.matchingPairs; + } else { + delete exerciseData.multipleChoices; + delete exerciseData.matchingPairs; + delete exerciseData.trueFalse; + } + + return exerciseData; + }); + + const responsePayload = { + NAME_SECTION: levelExists.levelTopic.topicSection.NAME_SECTION, + NAME_TOPIC: levelExists.levelTopic.NAME_TOPIC, + NAME_LEVEL: levelExists.NAME_LEVEL, + EXERCISES: formattedExercises, + }; + + response(200, responsePayload, "Success", res); + } catch (error) { + console.log(error); + res.status(500).json({ message: "Internal Server Error" }); + } +}; + export const createExercises = async (req, res) => { const { exercises } = req.body; diff --git a/controllers/contentControllers/topic.js b/controllers/contentControllers/topic.js index a5ea60c..99fb0c7 100644 --- a/controllers/contentControllers/topic.js +++ b/controllers/contentControllers/topic.js @@ -1,5 +1,6 @@ import response from "../../response.js"; import models from "../../models/index.js"; +import { autoGenerateLevel } from "../../middlewares/Level/autoGenerateLevel.js"; export const getTopics = async (req, res) => { try { @@ -178,20 +179,44 @@ export const createTopic = async (req, res) => { return response(400, null, "Topic description is required", res); } + const transaction = await models.sequelize.transaction(); + try { const section = await models.Section.findByPk(ID_SECTION); if (!section) { + await transaction.rollback(); return response(404, null, "Section not found", res); } - const newTopic = await models.Topic.create({ - ID_SECTION, - NAME_TOPIC, - DESCRIPTION_TOPIC, - }); + const newTopic = await models.Topic.create( + { + ID_SECTION, + NAME_TOPIC, + DESCRIPTION_TOPIC, + }, + { transaction } + ); - response(201, newTopic, "Topic created successfully", res); + req.body.ID_TOPIC = newTopic.ID_TOPIC; + + await autoGenerateLevel(req, res, async () => { + const levels = res.locals.createdLevels || []; + + const payload = { + topic: newTopic, + levels, + }; + + await transaction.commit(); + response( + 201, + payload, + "Topic and related Level created successfully", + res + ); + }); } catch (error) { + await transaction.rollback(); console.log(error); response(500, null, "Internal Server Error", res); } diff --git a/middlewares/Level/autoGenerateLevel.js b/middlewares/Level/autoGenerateLevel.js new file mode 100644 index 0000000..753fc4f --- /dev/null +++ b/middlewares/Level/autoGenerateLevel.js @@ -0,0 +1,73 @@ +import models from "../../models/index.js"; +import response from "../../response.js"; +import { + autoCalculateRoutes, + autoGenerateLevelUpdateOtherRoutes, +} from "./checkLevel.js"; + +export const autoGenerateLevel = async (req, res, next) => { + const { ID_TOPIC } = req.body; + + try { + const topic = await models.Topic.findByPk(ID_TOPIC); + if (!topic) { + return response(404, null, "Topic not found", res); + } + + const { ID_SECTION } = topic; + + const levelNames = [ + "Pretest", + "Level 1", + "Level 2", + "Level 3", + "Level 4", + "Level 5", + "Level 6", + ]; + + const createdLevels = []; + + for (let i = 0; i < levelNames.length; i++) { + const levelData = { + ID_TOPIC, + ID_SECTION, + NAME_LEVEL: levelNames[i], + IS_PRETEST: levelNames[i] === "Pretest" ? 1 : 0, + CONTENT: null, + AUDIO: null, + IMAGE: null, + VIDEO: null, + ROUTE_1: 0, + ROUTE_2: 0, + ROUTE_3: 0, + ROUTE_4: 0, + ROUTE_5: 0, + ROUTE_6: 0, + }; + + req.body = { ...levelData }; + await autoCalculateRoutes(req, res, () => {}); + + const newLevel = await models.Level.create(req.body); + createdLevels.push(newLevel); + } + + await autoGenerateLevelUpdateOtherRoutes(req, res, createdLevels); + + const updatedLevels = await models.Level.findAll({ + where: { + ID_TOPIC, + IS_DELETED: 0, + }, + order: [["NAME_LEVEL", "ASC"]], + }); + + res.locals.createdLevels = updatedLevels; + + next(); + } catch (error) { + console.log(error); + response(500, null, "Internal Server Error", res); + } +}; diff --git a/middlewares/Level/checkLevel.js b/middlewares/Level/checkLevel.js index 82690e5..df8b74b 100644 --- a/middlewares/Level/checkLevel.js +++ b/middlewares/Level/checkLevel.js @@ -280,6 +280,118 @@ export const updateOtherLevelsRoutes = async (req, res, next) => { } }; +export const autoGenerateLevelUpdateOtherRoutes = async (req, res, createdLevels) => { + const { NAME_LEVEL, ID_TOPIC, newLevelId } = req.body; + + try { + const levelTitles = [ + "Pretest", + "Level 1", + "Level 2", + "Level 3", + "Level 4", + "Level 5", + "Level 6", + ]; + const currentLevelIndex = levelTitles.indexOf(NAME_LEVEL); + + if (currentLevelIndex !== -1) { + const levels = await models.Level.findAll({ + where: { + ID_TOPIC, + NAME_LEVEL: { + [models.Sequelize.Op.in]: levelTitles, + }, + IS_DELETED: 0, + }, + }); + + levels.sort( + (a, b) => + levelTitles.indexOf(a.NAME_LEVEL) - levelTitles.indexOf(b.NAME_LEVEL) + ); + + const levelMap = {}; + levels.forEach((level) => { + levelMap[level.NAME_LEVEL] = level.ID_LEVEL; + }); + + if (newLevelId) { + levelMap[NAME_LEVEL] = newLevelId; + } + + for (let i = 0; i < levelTitles.length; i++) { + if (i === currentLevelIndex) continue; + + const levelTitle = levelTitles[i]; + const updateData = {}; + + if (i === 0) { + updateData.ROUTE_1 = levelMap["Level 1"] || 0; + updateData.ROUTE_2 = levelMap["Level 2"] || 0; + updateData.ROUTE_3 = levelMap["Level 3"] || 0; + updateData.ROUTE_4 = levelMap["Level 4"] || 0; + updateData.ROUTE_5 = levelMap["Level 5"] || 0; + updateData.ROUTE_6 = levelMap["Level 6"] || 0; + } else if (i === 1) { + updateData.ROUTE_1 = levelMap["Pretest"] || 0; + updateData.ROUTE_2 = levelMap["Level 2"] || 0; + updateData.ROUTE_3 = levelMap["Level 3"] || 0; + updateData.ROUTE_4 = levelMap["Level 4"] || 0; + updateData.ROUTE_5 = levelMap["Level 5"] || 0; + updateData.ROUTE_6 = levelMap["Level 6"] || 0; + } else if (i === 2) { + updateData.ROUTE_1 = levelMap["Pretest"] || 0; + updateData.ROUTE_2 = levelMap["Level 1"] || 0; + updateData.ROUTE_3 = levelMap["Level 3"] || 0; + updateData.ROUTE_4 = levelMap["Level 4"] || 0; + updateData.ROUTE_5 = levelMap["Level 5"] || 0; + updateData.ROUTE_6 = levelMap["Level 6"] || 0; + } else if (i === 3) { + updateData.ROUTE_1 = levelMap["Pretest"] || 0; + updateData.ROUTE_2 = levelMap["Level 1"] || 0; + updateData.ROUTE_3 = levelMap["Level 2"] || 0; + updateData.ROUTE_4 = levelMap["Level 4"] || 0; + updateData.ROUTE_5 = levelMap["Level 5"] || 0; + updateData.ROUTE_6 = levelMap["Level 6"] || 0; + } else if (i === 4) { + updateData.ROUTE_1 = levelMap["Pretest"] || 0; + updateData.ROUTE_2 = levelMap["Level 1"] || 0; + updateData.ROUTE_3 = levelMap["Level 2"] || 0; + updateData.ROUTE_4 = levelMap["Level 3"] || 0; + updateData.ROUTE_5 = levelMap["Level 5"] || 0; + updateData.ROUTE_6 = levelMap["Level 6"] || 0; + } else if (i === 5) { + updateData.ROUTE_1 = levelMap["Pretest"] || 0; + updateData.ROUTE_2 = levelMap["Level 1"] || 0; + updateData.ROUTE_3 = levelMap["Level 2"] || 0; + updateData.ROUTE_4 = levelMap["Level 3"] || 0; + updateData.ROUTE_5 = levelMap["Level 4"] || 0; + updateData.ROUTE_6 = levelMap["Level 6"] || 0; + } else if (i === 6) { + updateData.ROUTE_1 = levelMap["Pretest"] || 0; + updateData.ROUTE_2 = levelMap["Level 1"] || 0; + updateData.ROUTE_3 = levelMap["Level 2"] || 0; + updateData.ROUTE_4 = levelMap["Level 3"] || 0; + updateData.ROUTE_5 = levelMap["Level 4"] || 0; + updateData.ROUTE_6 = levelMap["Level 5"] || 0; + } + + await models.Level.update(updateData, { + where: { + ID_TOPIC, + NAME_LEVEL: levelTitle, + }, + }); + } + } + } catch (error) { + console.log(error); + return response(500, null, "Internal Server Error", res); + } +}; + + export const updateOtherLevelsRoutesOnDelete = async (req, res, next) => { const { newLevelId } = req.body; diff --git a/package.json b/package.json index 0a6bc0c..7c67b34 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,8 @@ "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "api-start": "nodemon index.js", + "db:create": "npx sequelize-cli db:create --config config/config.js", + "db:drop": "npx sequelize-cli db:drop --config config/config.js", "db:migrate": "npx sequelize-cli db:migrate --migrations-path database/migrations --config config/config.js", "db:seed": "npx sequelize-cli db:seed:all --seeders-path database/seeders --config config/config.js", "db:migrate:undo": "npx sequelize-cli db:migrate:undo --migrations-path database/migrations --config config/config.js", diff --git a/routes/contents/exercise.js b/routes/contents/exercise.js index f7ae643..97274f2 100644 --- a/routes/contents/exercise.js +++ b/routes/contents/exercise.js @@ -1,5 +1,5 @@ import express from "express"; -import { getExercises, getExerciseById, getExercisesForAdmin, getExerciseByLevelId, createExercises, updateExerciseById, deleteExerciseById, deleteExerciseFileById } from "../../controllers/contentControllers/exercise.js"; +import { getExercises, getExerciseById, getExercisesForAdmin, getExerciseByLevelId, getExerciseByLevelIdForAdmin, createExercises, updateExerciseById, deleteExerciseById, deleteExerciseFileById } from "../../controllers/contentControllers/exercise.js"; import { createMultipleChoicesExercise, updateMultipleChoicesExerciseById } from "../../controllers/exerciseTypesControllers/multipleChoices.js"; import { createMatchingPairsExercise, updateMatchingPairsExerciseById } from "../../controllers/exerciseTypesControllers/matchingPairs.js"; import { createTrueFalseExercise, updateTrueFalseExerciseById } from "../../controllers/exerciseTypesControllers/trueFalse.js"; @@ -16,6 +16,8 @@ router.get("/exercise/level/:idLevel", verifyLoginUser, getExerciseByLevelId); router.get("/exercise/admin", verifyLoginUser, adminOnly, getExercisesForAdmin); +router.get("/exercise/admin/level/:idLevel", verifyLoginUser, adminOnly, getExerciseByLevelIdForAdmin); + router.get("/exercise/:id", verifyLoginUser, getExerciseById); router.post("/exercises", verifyLoginUser, adminOnly, handleUpload, createExercises);