diff --git a/controllers/contentControllers/level.js b/controllers/contentControllers/level.js index 4145aeb..39161ad 100644 --- a/controllers/contentControllers/level.js +++ b/controllers/contentControllers/level.js @@ -100,7 +100,7 @@ export const getLevelsByTopicId = async (req, res) => { }); if (!topicExists) { - return response(404, null, "Topic not found", res); + return res.status(404).json({ message: "Topic not found" }); } const levels = await models.Level.findAll({ @@ -121,11 +121,18 @@ export const getLevelsByTopicId = async (req, res) => { { model: models.StdLearning, as: "stdLearning", - attributes: ["SCORE", "ID_STUDENT_LEARNING"], + attributes: [ + "SCORE", + "ID_STUDENT_LEARNING", + "STUDENT_START", + "STUDENT_FINISH", + ], where: { ID: ID, }, required: false, + order: [["STUDENT_START", "DESC"]], + limit: 1, }, { model: models.Topic, @@ -165,10 +172,12 @@ export const getLevelsByTopicId = async (req, res) => { const levelsWithScore = levels.map((level) => { const SCORE = - level.stdLearning.length > 0 ? level.stdLearning[0].SCORE : 0; + level.stdLearning && level.stdLearning.length > 0 + ? level.stdLearning[0]?.SCORE + : null; const ID_STUDENT_LEARNING = - level.stdLearning.length > 0 - ? level.stdLearning[0].ID_STUDENT_LEARNING + level.stdLearning && level.stdLearning.length > 0 + ? level.stdLearning[0]?.ID_STUDENT_LEARNING : null; const levelJSON = level.toJSON(); @@ -213,7 +222,7 @@ export const getLevelsByTopicId = async (req, res) => { res.status(200).json({ message: "Success", data: responsePayload }); } catch (error) { - console.log(error); + console.error(error); res.status(500).json({ message: "Internal Server Error" }); } }; @@ -236,14 +245,17 @@ export const createLevel = async (req, res, next) => { clearFileBuffers({ AUDIO, IMAGE }); return response(400, null, "Topic is required", res); } + const transaction = await models.db.transaction(); try { const sectionWithTopic = await models.Topic.findOne({ where: { ID_SECTION, ID_TOPIC }, + transaction, }); if (!sectionWithTopic) { clearFileBuffers({ AUDIO, IMAGE }); + await transaction.rollback(); return response( 400, null, @@ -254,10 +266,12 @@ export const createLevel = async (req, res, next) => { const existingLevel = await models.Level.findOne({ where: { NAME_LEVEL, ID_TOPIC }, + transaction, }); if (existingLevel) { clearFileBuffers({ AUDIO, IMAGE }); + await transaction.rollback(); return response( 409, null, @@ -266,22 +280,25 @@ export const createLevel = async (req, res, next) => { ); } - const newLevel = await models.Level.create({ - NAME_LEVEL, - ID_SECTION, - ID_TOPIC, - IS_PRETEST: req.body.IS_PRETEST || 0, - CONTENT, - VIDEO: VIDEO || null, - AUDIO: null, - IMAGE: null, - ROUTE_1: req.body.ROUTE_1, - ROUTE_2: req.body.ROUTE_2, - ROUTE_3: req.body.ROUTE_3, - ROUTE_4: req.body.ROUTE_4, - ROUTE_5: req.body.ROUTE_5, - ROUTE_6: req.body.ROUTE_6, - }); + const newLevel = await models.Level.create( + { + NAME_LEVEL, + ID_SECTION, + ID_TOPIC, + IS_PRETEST: req.body.IS_PRETEST || 0, + CONTENT, + VIDEO: VIDEO || null, + AUDIO: null, + IMAGE: null, + ROUTE_1: req.body.ROUTE_1, + ROUTE_2: req.body.ROUTE_2, + ROUTE_3: req.body.ROUTE_3, + ROUTE_4: req.body.ROUTE_4, + ROUTE_5: req.body.ROUTE_5, + ROUTE_6: req.body.ROUTE_6, + }, + { transaction } + ); req.body.newLevelId = newLevel.ID_LEVEL; @@ -294,7 +311,9 @@ export const createLevel = async (req, res, next) => { newLevel.AUDIO = audioFilename; newLevel.IMAGE = imageFilename; - await newLevel.save(); + await newLevel.save({ transaction }); + + await transaction.commit(); await updateOtherLevelsRoutes(req, res, next); @@ -302,6 +321,7 @@ export const createLevel = async (req, res, next) => { } catch (error) { console.log(error); clearFileBuffers({ AUDIO, IMAGE }); + await transaction.rollback(); return response(500, null, "Internal Server Error", res); } }; @@ -312,20 +332,25 @@ export const updateLevelById = async (req, res, next) => { const { AUDIO, IMAGE } = req.filesToSave || {}; + const transaction = await models.db.transaction(); + try { - const level = await models.Level.findByPk(id); + const level = await models.Level.findByPk(id, { transaction }); if (!level) { clearFileBuffers({ AUDIO, IMAGE }); + await transaction.rollback(); return response(404, null, "Level not found", res); } const sectionWithTopic = await models.Topic.findOne({ where: { ID_SECTION, ID_TOPIC }, + transaction, }); if (!sectionWithTopic) { clearFileBuffers({ AUDIO, IMAGE }); + await transaction.rollback(); return response( 400, null, @@ -341,10 +366,12 @@ export const updateLevelById = async (req, res, next) => { ID_TOPIC, ID_LEVEL: { [models.Sequelize.Op.ne]: id }, }, + transaction, }); if (existingLevel) { clearFileBuffers({ AUDIO, IMAGE }); + await transaction.rollback(); return response( 409, null, @@ -356,19 +383,12 @@ export const updateLevelById = async (req, res, next) => { if (NAME_LEVEL) { level.NAME_LEVEL = NAME_LEVEL; - if (NAME_LEVEL === "Level 1") { - level.IS_PRETEST = 1; - } else { - level.IS_PRETEST = 0; - } + level.IS_PRETEST = NAME_LEVEL === "Level 1" ? 1 : 0; } if (ID_SECTION) level.ID_SECTION = ID_SECTION; if (ID_TOPIC) level.ID_TOPIC = ID_TOPIC; if (CONTENT) level.CONTENT = CONTENT; - - if (VIDEO) { - level.VIDEO = VIDEO; - } + if (VIDEO) level.VIDEO = VIDEO; if (AUDIO) { if (level.AUDIO) { @@ -376,9 +396,7 @@ export const updateLevelById = async (req, res, next) => { "public/uploads/level/audio", level.AUDIO ); - if (fs.existsSync(oldAudioPath)) { - fs.unlinkSync(oldAudioPath); - } + if (fs.existsSync(oldAudioPath)) fs.unlinkSync(oldAudioPath); } level.AUDIO = saveFileToDisk( AUDIO, @@ -395,9 +413,7 @@ export const updateLevelById = async (req, res, next) => { "public/uploads/level/image", level.IMAGE ); - if (fs.existsSync(oldImagePath)) { - fs.unlinkSync(oldImagePath); - } + if (fs.existsSync(oldImagePath)) fs.unlinkSync(oldImagePath); } level.IMAGE = saveFileToDisk( IMAGE, @@ -408,9 +424,9 @@ export const updateLevelById = async (req, res, next) => { ); } - await level.save(); + await level.save({ transaction }); - req.body.newLevelId = level.ID_LEVEL; + await transaction.commit(); await updateOtherLevelsRoutes(req, res, next); @@ -418,6 +434,7 @@ export const updateLevelById = async (req, res, next) => { } catch (error) { console.log(error); clearFileBuffers({ AUDIO, IMAGE }); + await transaction.rollback(); return response(500, null, "Internal Server Error", res); } }; @@ -471,11 +488,18 @@ export const getPreviousLevel = async (req, res) => { { model: models.StdLearning, as: "stdLearning", - attributes: ["SCORE", "ID_STUDENT_LEARNING"], + attributes: [ + "SCORE", + "ID_STUDENT_LEARNING", + "STUDENT_START", + "STUDENT_FINISH", + ], where: { ID: ID, }, required: false, + order: [["STUDENT_FINISH", "DESC"]], + limit: 1, }, ], attributes: { @@ -491,14 +515,14 @@ export const getPreviousLevel = async (req, res) => { }); if (!currentLevel) { - return response(404, null, "Level not found", res); + return res.status(404).json({ message: "Level not found" }); } const { NAME_LEVEL, ID_TOPIC } = currentLevel; const levelNumber = parseInt(NAME_LEVEL.replace("Level ", "")); if (isNaN(levelNumber)) { - return response(400, null, "Invalid level format", res); + return res.status(400).json({ message: "Invalid level format" }); } const previousLevels = await models.Level.findAll({ @@ -534,21 +558,30 @@ export const getPreviousLevel = async (req, res) => { { model: models.StdLearning, as: "stdLearning", - attributes: ["SCORE", "ID_STUDENT_LEARNING"], + attributes: [ + "SCORE", + "ID_STUDENT_LEARNING", + "STUDENT_START", + "STUDENT_FINISH", + ], where: { ID: ID, }, required: false, + order: [["STUDENT_START", "DESC"]], + limit: 1, }, ], }); const previousLevelsWithScore = previousLevels.map((level) => { const SCORE = - level.stdLearning.length > 0 ? level.stdLearning[0].SCORE : 0; + level.stdLearning && level.stdLearning.length > 0 + ? level.stdLearning[0]?.SCORE + : null; const ID_STUDENT_LEARNING = - level.stdLearning.length > 0 - ? level.stdLearning[0].ID_STUDENT_LEARNING + level.stdLearning && level.stdLearning.length > 0 + ? level.stdLearning[0]?.ID_STUDENT_LEARNING : null; const levelJSON = level.toJSON(); @@ -564,19 +597,19 @@ export const getPreviousLevel = async (req, res) => { const currentLevelWithScore = { ...currentLevel.toJSON(), ID_STUDENT_LEARNING: - currentLevel.stdLearning.length > 0 - ? currentLevel.stdLearning[0].ID_STUDENT_LEARNING + currentLevel.stdLearning && currentLevel.stdLearning.length > 0 + ? currentLevel.stdLearning[0]?.ID_STUDENT_LEARNING : null, SCORE: - currentLevel.stdLearning.length > 0 - ? currentLevel.stdLearning[0].SCORE - : 0, + currentLevel.stdLearning && currentLevel.stdLearning.length > 0 + ? currentLevel.stdLearning[0]?.SCORE + : null, }; delete currentLevelWithScore.stdLearning; if (!previousLevelsWithScore.length && !currentLevelWithScore) { - return response(404, null, "No levels found", res); + return res.status(404).json({ message: "No levels found" }); } const result = { @@ -584,9 +617,9 @@ export const getPreviousLevel = async (req, res) => { previousLevels: previousLevelsWithScore, }; - response(200, result, "Success", res); + res.status(200).json({ message: "Success", data: result }); } catch (error) { - console.log(error); + console.error(error); res.status(500).json({ message: "Internal Server Error" }); } }; diff --git a/controllers/contentControllers/section.js b/controllers/contentControllers/section.js index 7710aae..510401a 100644 --- a/controllers/contentControllers/section.js +++ b/controllers/contentControllers/section.js @@ -35,7 +35,6 @@ export const getSectionById = async (req, res) => { export const createSection = async (req, res) => { const { NAME_SECTION, DESCRIPTION_SECTION } = req.body; - const { THUMBNAIL } = req.filesToSave || {}; if (!NAME_SECTION) { @@ -48,24 +47,46 @@ export const createSection = async (req, res) => { return response(400, null, "Description is required", res); } + const transaction = await models.db.transaction(); + try { - const newSection = await models.Section.create({ - NAME_SECTION, - DESCRIPTION_SECTION, - THUMBNAIL: null, + const existingSection = await models.Section.findOne({ + where: { NAME_SECTION }, + transaction, }); + if (existingSection) { + clearFileBuffers({ THUMBNAIL }); + await transaction.rollback(); + return response(400, null, "Section name already exists", res); + } + + const newSection = await models.Section.create( + { + NAME_SECTION, + DESCRIPTION_SECTION, + THUMBNAIL: null, + }, + { transaction } + ); + const thumbnailFilename = THUMBNAIL ? saveFileToDisk(THUMBNAIL, "THUMBNAIL", newSection.ID_SECTION) : null; newSection.THUMBNAIL = thumbnailFilename; - await newSection.save(); + + await newSection.save({ transaction }); + + await transaction.commit(); response(201, newSection, "Section created successfully", res); } catch (error) { console.log(error); + + await transaction.rollback(); clearFileBuffers({ THUMBNAIL }); + response(500, null, "Internal Server Error", res); } }; @@ -73,18 +94,34 @@ export const createSection = async (req, res) => { export const updateSectionById = async (req, res) => { const { id } = req.params; const { NAME_SECTION, DESCRIPTION_SECTION } = req.body; - const { THUMBNAIL } = req.filesToSave || {}; + const transaction = await models.db.transaction(); + try { - const section = await models.Section.findByPk(id); + const section = await models.Section.findByPk(id, { transaction }); if (!section) { clearFileBuffers({ THUMBNAIL }); + await transaction.rollback(); return response(404, null, "Section not found", res); } - if (NAME_SECTION) section.NAME_SECTION = NAME_SECTION; + if (NAME_SECTION && NAME_SECTION !== section.NAME_SECTION) { + const existingSection = await models.Section.findOne({ + where: { NAME_SECTION }, + transaction, + }); + + if (existingSection) { + clearFileBuffers({ THUMBNAIL }); + await transaction.rollback(); + return response(400, null, "Section name already exists", res); + } + + section.NAME_SECTION = NAME_SECTION; + } + if (DESCRIPTION_SECTION) section.DESCRIPTION_SECTION = DESCRIPTION_SECTION; if (THUMBNAIL) { @@ -97,6 +134,7 @@ export const updateSectionById = async (req, res) => { fs.unlinkSync(oldThumbnailPath); } } + section.THUMBNAIL = saveFileToDisk( THUMBNAIL, "THUMBNAIL", @@ -104,12 +142,17 @@ export const updateSectionById = async (req, res) => { ); } - await section.save(); + await section.save({ transaction }); + + await transaction.commit(); response(200, section, "Section updated successfully", res); } catch (error) { console.log(error); + + await transaction.rollback(); clearFileBuffers({ THUMBNAIL }); + response(500, null, "Internal Server Error", res); } }; diff --git a/controllers/learningControllers/stdExercise.js b/controllers/learningControllers/stdExercise.js index cefc9b8..113fc85 100644 --- a/controllers/learningControllers/stdExercise.js +++ b/controllers/learningControllers/stdExercise.js @@ -100,5 +100,65 @@ export const stdAnswerExercise = async (req, res, next) => { }; export const getStudentAnswersByStdLearningId = async (req, res) => { - -} \ No newline at end of file + try { + const { id } = req.params; + + const stdLearning = await models.StdLearning.findByPk(id, { + include: [ + { + model: models.StdExercise, + as: "stdExercises", + include: [ + { + model: models.Exercise, + as: "stdExerciseExercises", + attributes: ["ID_ADMIN_EXERCISE", "TITLE", "QUESTION", "QUESTION_TYPE"], + }, + ], + attributes: [ + "ID_STUDENT_EXERCISE", + "ANSWER_STUDENT", + "IS_CORRECT", + "RESULT_SCORE_STUDENT", + "TIME_STUDENT_EXC", + ], + }, + ], + attributes: ["ID_STUDENT_LEARNING", "ID_LEVEL", "SCORE", "IS_PASS"], + }); + + if (!stdLearning) { + return res.status(404).json({ + message: "Student learning data not found", + }); + } + + const sortedExercises = stdLearning.stdExercises.sort((a, b) => { + const titleA = a.stdExerciseExercises.TITLE.toUpperCase(); + const titleB = b.stdExerciseExercises.TITLE.toUpperCase(); + + if (titleA < titleB) return -1; + if (titleA > titleB) return 1; + return 0; + }); + + const mappedExercises = sortedExercises.map((exercise) => ({ + ...exercise.toJSON(), + exerciseDetails: exercise.stdExerciseExercises, + stdExerciseExercises: undefined, + })); + + return res.status(200).json({ + message: "Student learning exercises retrieved successfully", + data: { + ...stdLearning.toJSON(), + stdExercises: mappedExercises, + }, + }); + } catch (error) { + console.error(error); + return res.status(500).json({ + message: "Error retrieving student learning exercises", + }); + } +}; diff --git a/public/uploads/level/audio/AUDIO-17bad807-45c3-44e6-b1b5-8bd52fa22079-d9937102a559fbce6d6c55d66e799066.mp3 b/public/uploads/level/audio/AUDIO-17bad807-45c3-44e6-b1b5-8bd52fa22079-d9937102a559fbce6d6c55d66e799066.mp3 new file mode 100644 index 0000000..9bf1b59 Binary files /dev/null and b/public/uploads/level/audio/AUDIO-17bad807-45c3-44e6-b1b5-8bd52fa22079-d9937102a559fbce6d6c55d66e799066.mp3 differ diff --git a/public/uploads/level/image/IMAGE-17bad807-45c3-44e6-b1b5-8bd52fa22079-f19cfaa031a896ebf1e3a3c28c18ad0c.jpeg b/public/uploads/level/image/IMAGE-17bad807-45c3-44e6-b1b5-8bd52fa22079-f19cfaa031a896ebf1e3a3c28c18ad0c.jpeg new file mode 100644 index 0000000..6439585 Binary files /dev/null and b/public/uploads/level/image/IMAGE-17bad807-45c3-44e6-b1b5-8bd52fa22079-f19cfaa031a896ebf1e3a3c28c18ad0c.jpeg differ diff --git a/public/uploads/section/THUMBNAIL-49390272-efbd-4f1c-9b63-39fa8905b891-3b4db19865d8b70386db0834852659ac.jpeg b/public/uploads/section/THUMBNAIL-49390272-efbd-4f1c-9b63-39fa8905b891-3b4db19865d8b70386db0834852659ac.jpeg new file mode 100644 index 0000000..39859e4 Binary files /dev/null and b/public/uploads/section/THUMBNAIL-49390272-efbd-4f1c-9b63-39fa8905b891-3b4db19865d8b70386db0834852659ac.jpeg differ diff --git a/public/uploads/section/THUMBNAIL-49390272-efbd-4f1c-9b63-39fa8905b891-81346cbd4317524acfd617bf14c006ce.jpeg b/public/uploads/section/THUMBNAIL-49390272-efbd-4f1c-9b63-39fa8905b891-81346cbd4317524acfd617bf14c006ce.jpeg deleted file mode 100644 index 953f28d..0000000 Binary files a/public/uploads/section/THUMBNAIL-49390272-efbd-4f1c-9b63-39fa8905b891-81346cbd4317524acfd617bf14c006ce.jpeg and /dev/null differ diff --git a/routes/learning/stdExercise.js b/routes/learning/stdExercise.js index f5032fd..af12be1 100644 --- a/routes/learning/stdExercise.js +++ b/routes/learning/stdExercise.js @@ -1,5 +1,5 @@ import express from "express"; -import { getStdExercises, getStdExerciseById, stdAnswerExercise } from "../../controllers/learningControllers/stdExercise.js"; +import { getStdExercises, getStdExerciseById, stdAnswerExercise, getStudentAnswersByStdLearningId } from "../../controllers/learningControllers/stdExercise.js"; import { verifyLoginUser } from "../../middlewares/User/authUser.js"; import { updateStdLearningById } from "../../controllers/learningControllers/stdLearning.js"; import { checkCorrectAnswers, calculateScore, checkFirstFiveCorrect, nextLearning } from "../../middlewares/autoGrading.js"; @@ -12,4 +12,6 @@ router.get("/stdExercise/:id", verifyLoginUser, getStdExerciseById); router.post("/stdExercise", verifyLoginUser, stdAnswerExercise, checkCorrectAnswers, calculateScore, checkFirstFiveCorrect, nextLearning, updateStdLearningById); +router.post("/studentAnswers/:id", verifyLoginUser, getStudentAnswersByStdLearningId); + export default router \ No newline at end of file