diff --git a/controllers/auth/auth.js b/controllers/auth/auth.js index a317369..3e737cd 100644 --- a/controllers/auth/auth.js +++ b/controllers/auth/auth.js @@ -293,7 +293,6 @@ export const loginUser = async (req, res) => { }; export const refreshToken = async (req, res) => { - // Check for refresh token in cookies or body const refreshToken = req.cookies?.refreshToken || req.body?.REFRESH_TOKEN; if (!refreshToken) { diff --git a/controllers/contentControllers/exercise.js b/controllers/contentControllers/exercise.js index a720b8c..edc3375 100644 --- a/controllers/contentControllers/exercise.js +++ b/controllers/contentControllers/exercise.js @@ -2,10 +2,15 @@ import response from "../../response.js"; import models from "../../models/index.js"; import fs from "fs"; import path from "path"; +import { + clearFileBuffers, + saveFileToDisk, +} from "../../middlewares/uploadExercise.js"; export const getExercises = async (req, res) => { try { const exercises = await models.Exercise.findAll({ + where: { IS_DELETED: 0 }, include: [ { model: models.MultipleChoices, @@ -55,68 +60,12 @@ export const getExercises = async (req, res) => { } }; -export const getExercisesForAdmin = async (req, res) => { - try { - const exercises = await models.Exercise.findAll({ - include: [ - { - model: models.MultipleChoices, - as: "multipleChoices", - attributes: ["ANSWER_KEY"], - }, - { - model: models.MatchingPairs, - as: "matchingPairs", - attributes: ["LEFT_PAIR", "RIGHT_PAIR"], - }, - { - model: models.TrueFalse, - as: "trueFalse", - attributes: ["IS_TRUE"], - }, - ], - }); - - const formattedExercises = exercises.map((exercise) => { - const questionType = exercise.QUESTION_TYPE; - let answerKey = null; - - if (questionType === "MCQ" && exercise.multipleChoices.length > 0) { - answerKey = exercise.multipleChoices[0].ANSWER_KEY; - } else if (questionType === "MPQ" && exercise.matchingPairs.length > 0) { - answerKey = exercise.matchingPairs - .map((pair) => `${pair.LEFT_PAIR}-${pair.RIGHT_PAIR}`) - .join(", "); - } else if (questionType === "TFQ" && exercise.trueFalse.length > 0) { - answerKey = exercise.trueFalse[0].IS_TRUE === 1 ? "true" : "false"; - } - - return { - ID_ADMIN_EXERCISE: exercise.ID_ADMIN_EXERCISE, - ID_LEVEL: exercise.ID_LEVEL, - TITLE: exercise.TITLE, - QUESTION: exercise.QUESTION, - SCORE_WEIGHT: exercise.SCORE_WEIGHT, - QUESTION_TYPE: questionType, - AUDIO: exercise.AUDIO, - VIDEO: exercise.VIDEO, - IMAGE: exercise.IMAGE, - ANSWER_KEY: answerKey, - }; - }); - - response(200, formattedExercises, "Success", res); - } catch (error) { - console.log(error); - response(500, null, "Error retrieving exercises data!", res); - } -}; - export const getExerciseById = async (req, res) => { try { const { id } = req.params; - const exercise = await models.Exercise.findByPk(id, { + const exercise = await models.Exercise.findOne({ + where: { ID_ADMIN_EXERCISE: id, IS_DELETED: 0 }, include: [ { model: models.MultipleChoices, @@ -143,29 +92,24 @@ export const getExerciseById = async (req, res) => { if (questionType === "MCQ") { if (exerciseData.multipleChoices) { exerciseData.multipleChoices = exerciseData.multipleChoices.map( - (choice) => { - const { ANSWER_KEY, ...rest } = choice.dataValues; - return rest; - } + (choice) => choice.dataValues ); } delete exerciseData.matchingPairs; delete exerciseData.trueFalse; } else if (questionType === "MPQ") { if (exerciseData.matchingPairs) { - exerciseData.matchingPairs = exerciseData.matchingPairs.map((pair) => { - const { LEFT_PAIR, RIGHT_PAIR, ...rest } = pair.dataValues; - return rest; - }); + 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) => { - const { IS_TRUE, ...rest } = tf.dataValues; - return rest; - }); + exerciseData.trueFalse = exerciseData.trueFalse.map( + (tf) => tf.dataValues + ); } delete exerciseData.multipleChoices; delete exerciseData.matchingPairs; @@ -182,6 +126,178 @@ export const getExerciseById = async (req, res) => { } }; +export const getExercisesForAdmin = async (req, res) => { + const { page = 1, limit = 10, search = "", sort = "time" } = req.query; + + try { + const { rows: exercises } = await models.Exercise.findAndCountAll({ + where: { + IS_DELETED: 0, + ...(search && { + [models.Op.or]: [ + { + "$levelExercise->levelTopic->topicSection.NAME_SECTION$": { + [models.Op.like]: `%${search}%`, + }, + }, + { + "$levelExercise->levelTopic.NAME_TOPIC$": { + [models.Op.like]: `%${search}%`, + }, + }, + { + "$levelExercise.NAME_LEVEL$": { [models.Op.like]: `%${search}%` }, + }, + { QUESTION: { [models.Op.like]: `%${search}%` } }, + ], + }), + }, + distinct: true, + include: [ + { + model: models.Level, + as: "levelExercise", + attributes: ["NAME_LEVEL"], + include: [ + { + model: models.Topic, + as: "levelTopic", + attributes: ["NAME_TOPIC"], + include: [ + { + model: models.Section, + as: "topicSection", + attributes: ["NAME_SECTION"], + }, + ], + }, + ], + }, + { + model: models.MultipleChoices, + as: "multipleChoices", + attributes: ["ANSWER_KEY"], + }, + { + model: models.MatchingPairs, + as: "matchingPairs", + attributes: ["LEFT_PAIR", "RIGHT_PAIR"], + }, + { + model: models.TrueFalse, + as: "trueFalse", + attributes: ["IS_TRUE"], + }, + ], + }); + + const formattedExercises = exercises.map((exercise) => { + const questionType = exercise.QUESTION_TYPE; + let answerKey = null; + + const nameSection = + exercise.levelExercise?.levelTopic?.topicSection?.NAME_SECTION || + "Unknown Section"; + const nameTopic = + exercise.levelExercise?.levelTopic?.NAME_TOPIC || "Unknown Topic"; + + if (questionType === "MCQ" && exercise.multipleChoices.length > 0) { + answerKey = exercise.multipleChoices[0].ANSWER_KEY; + } else if (questionType === "MPQ" && exercise.matchingPairs.length > 0) { + answerKey = exercise.matchingPairs + .map((pair) => `${pair.LEFT_PAIR}-${pair.RIGHT_PAIR}`) + .join(", "); + } else if (questionType === "TFQ" && exercise.trueFalse.length > 0) { + answerKey = exercise.trueFalse[0].IS_TRUE === 1 ? "true" : "false"; + } + + return { + ID_ADMIN_EXERCISE: exercise.ID_ADMIN_EXERCISE, + NAME_SECTION: nameSection, + NAME_TOPIC: nameTopic, + NAME_LEVEL: exercise.levelExercise.NAME_LEVEL, + TITLE: exercise.TITLE, + QUESTION: exercise.QUESTION, + QUESTION_TYPE: questionType, + ANSWER_KEY: answerKey, + TIME_ADMIN_EXC: exercise.TIME_ADMIN_EXC, + }; + }); + + const filteredExercises = formattedExercises.filter( + (exercise) => + exercise.NAME_SECTION !== "Unknown Section" && + exercise.NAME_TOPIC !== "Unknown Topic" + ); + + if (sort === "section") { + filteredExercises.sort((a, b) => { + return a.NAME_SECTION.localeCompare(b.NAME_SECTION); + }); + } else if (sort === "topic") { + filteredExercises.sort((a, b) => { + return a.NAME_TOPIC.localeCompare(b.NAME_TOPIC); + }); + } else if (sort === "level") { + filteredExercises.sort((a, b) => { + return a.NAME_LEVEL.localeCompare(b.NAME_LEVEL); + }); + } else if (sort === "question") { + filteredExercises.sort((a, b) => { + return a.QUESTION.localeCompare(b.QUESTION); + }); + } else if (sort === "answer") { + filteredExercises.sort((a, b) => { + return (a.ANSWER_KEY || "").localeCompare(b.ANSWER_KEY || ""); + }); + } else { + filteredExercises.sort((a, b) => { + return new Date(b.TIME_ADMIN_EXC) - new Date(a.TIME_ADMIN_EXC); + }); + } + + const formattedSortedExercises = filteredExercises.map((exercise) => { + const formattedTimeAdminExc = new Date(exercise.TIME_ADMIN_EXC) + .toLocaleString("en-GB", { + hour12: false, + hour: "2-digit", + minute: "2-digit", + day: "2-digit", + month: "2-digit", + year: "numeric", + }) + .replace(/(\d{2}\/\d{2}\/\d{4}), (\d{2}:\d{2})/, "$2 $1"); + + return { + ...exercise, + TIME_ADMIN_EXC: formattedTimeAdminExc, + }; + }); + + const paginatedExercises = formattedSortedExercises.slice( + (page - 1) * limit, + page * limit + ); + + const totalPages = Math.ceil(formattedSortedExercises.length / limit); + + response( + 200, + { + exercises: paginatedExercises, + currentPage: parseInt(page), + totalPages: totalPages, + totalExercises: formattedSortedExercises.length, + }, + "Success", + res + ); + } catch (error) { + console.log(error); + response(500, null, "Error retrieving exercises data!", res); + } +}; + export const getExerciseByLevelId = async (req, res) => { try { const { idLevel } = req.params; @@ -202,7 +318,7 @@ export const getExerciseByLevelId = async (req, res) => { } const exercises = await models.Exercise.findAll({ - where: { ID_LEVEL: idLevel }, + where: { ID_LEVEL: idLevel, IS_DELETED: 0 }, include: [ { model: models.MultipleChoices, @@ -277,78 +393,393 @@ export const getExerciseByLevelId = async (req, res) => { } }; +export const createExercises = async (req, res) => { + const { exercises } = req.body; + + if (!exercises || !Array.isArray(exercises) || exercises.length === 0) { + return response(400, null, "Exercises array is required", res); + } + + const transaction = await models.db.transaction(); + + try { + const createdExercises = []; + + const levelId = exercises[0]?.ID_LEVEL; + let lastExercise = await models.Exercise.findOne({ + where: { ID_LEVEL: levelId }, + order: [["TITLE", "DESC"]], + limit: 1, + }); + + let lastTitleNumber = 0; + + if (lastExercise && lastExercise.TITLE) { + const lastTitleParts = lastExercise.TITLE.split(" "); + lastTitleNumber = parseInt(lastTitleParts[1], 10) || 0; + } + + for (let i = 0; i < exercises.length; i++) { + const exerciseData = exercises[i]; + const { ID_LEVEL, TITLE, QUESTION, SCORE_WEIGHT, VIDEO, QUESTION_TYPE } = + exerciseData; + + if (!ID_LEVEL) throw new Error("Level ID is required"); + if (!QUESTION) throw new Error("Question is required"); + if (!SCORE_WEIGHT) throw new Error("Score weight is required"); + + const level = await models.Level.findByPk(ID_LEVEL); + if (!level) throw new Error("Level not found"); + + let generatedTitle = TITLE; + if (!TITLE) { + lastTitleNumber++; + generatedTitle = `Soal ${lastTitleNumber}`; + } + + const existingExercise = await models.Exercise.findOne({ + where: { ID_LEVEL, TITLE: generatedTitle }, + }); + if (existingExercise) { + throw new Error( + `An exercise with the title ${generatedTitle} already exists` + ); + } + + const newExercise = await models.Exercise.create( + { + ID_LEVEL, + TITLE: generatedTitle, + QUESTION, + SCORE_WEIGHT, + QUESTION_TYPE, + AUDIO: null, + VIDEO: VIDEO || null, + IMAGE: null, + }, + { transaction } + ); + + const AUDIO = req.filesToSave[`AUDIO[${i}]`] || null; + const IMAGE = req.filesToSave[`IMAGE[${i}]`] || null; + + const audioFilename = AUDIO + ? saveFileToDisk( + AUDIO, + "AUDIO", + ID_LEVEL, + newExercise.ID_ADMIN_EXERCISE + ) + : null; + const imageFilename = IMAGE + ? saveFileToDisk( + IMAGE, + "IMAGE", + ID_LEVEL, + newExercise.ID_ADMIN_EXERCISE + ) + : null; + + newExercise.AUDIO = audioFilename; + newExercise.IMAGE = imageFilename; + await newExercise.save({ transaction }); + + let questionDetails = null; + switch (QUESTION_TYPE) { + case "MCQ": + const { + OPTION_A, + OPTION_B, + OPTION_C, + OPTION_D, + OPTION_E, + ANSWER_KEY, + } = exerciseData; + if ( + !OPTION_A || + !OPTION_B || + !OPTION_C || + !OPTION_D || + !OPTION_E || + !ANSWER_KEY + ) { + throw new Error( + "All options and answer key are required for Multiple Choice" + ); + } + + questionDetails = await models.MultipleChoices.create( + { + ID_ADMIN_EXERCISE: newExercise.ID_ADMIN_EXERCISE, + OPTION_A, + OPTION_B, + OPTION_C, + OPTION_D, + OPTION_E, + ANSWER_KEY, + }, + { transaction } + ); + break; + + case "MPQ": + const { PAIRS } = exerciseData; + if (!PAIRS || !Array.isArray(PAIRS) || PAIRS.length === 0) { + throw new Error("At least one pair is required for Matching Pairs"); + } + + const matchingPairsPromises = PAIRS.map((pair) => + models.MatchingPairs.create( + { + ID_ADMIN_EXERCISE: newExercise.ID_ADMIN_EXERCISE, + LEFT_PAIR: pair.LEFT_PAIR, + RIGHT_PAIR: pair.RIGHT_PAIR, + }, + { transaction } + ) + ); + await Promise.all(matchingPairsPromises); + questionDetails = PAIRS; + break; + + case "TFQ": + const { IS_TRUE } = exerciseData; + if (typeof IS_TRUE === "undefined") { + throw new Error("IS_TRUE is required for True/False"); + } + + questionDetails = await models.TrueFalse.create( + { + ID_ADMIN_EXERCISE: newExercise.ID_ADMIN_EXERCISE, + IS_TRUE, + }, + { transaction } + ); + break; + + default: + throw new Error("Unsupported question type"); + } + + newExercise.dataValues.questionDetails = questionDetails; + createdExercises.push(newExercise); + } + + await transaction.commit(); + + response(201, createdExercises, "Exercises created successfully", res); + } catch (error) { + console.error(error); + await transaction.rollback(); + response(500, null, error.message || "Internal Server Error", res); + } +}; + +export const updateExerciseById = async (req, res) => { + const { id } = req.params; + const { ID_LEVEL, QUESTION, SCORE_WEIGHT, VIDEO, PAIRS } = req.body; + const { IMAGE, AUDIO } = req.filesToSave || {}; + + const transaction = await models.db.transaction(); + + try { + const exercise = await models.Exercise.findByPk(id, { transaction }); + + if (!exercise) { + clearFileBuffers({ IMAGE, AUDIO }); + await transaction.rollback(); + return response(404, null, "Exercise not found", res); + } + + const { QUESTION_TYPE } = exercise; + + if (ID_LEVEL) { + const level = await models.Level.findByPk(ID_LEVEL, { transaction }); + if (!level) { + clearFileBuffers({ IMAGE, AUDIO }); + await transaction.rollback(); + return response(404, null, "Level not found", res); + } + exercise.ID_LEVEL = ID_LEVEL; + } + if (QUESTION) exercise.QUESTION = QUESTION; + if (SCORE_WEIGHT) exercise.SCORE_WEIGHT = SCORE_WEIGHT; + if (VIDEO) exercise.VIDEO = VIDEO; + + if (AUDIO) { + if (exercise.AUDIO) { + const oldAudioPath = path.join( + "public/uploads/exercise/audio", + exercise.AUDIO + ); + if (fs.existsSync(oldAudioPath)) { + fs.unlinkSync(oldAudioPath); + } + } + exercise.AUDIO = saveFileToDisk( + AUDIO, + "AUDIO", + ID_LEVEL || exercise.ID_LEVEL, + exercise.ID_ADMIN_EXERCISE + ); + } + + if (IMAGE) { + if (exercise.IMAGE) { + const oldImagePath = path.join( + "public/uploads/exercise/image", + exercise.IMAGE + ); + if (fs.existsSync(oldImagePath)) { + fs.unlinkSync(oldImagePath); + } + } + exercise.IMAGE = saveFileToDisk( + IMAGE, + "IMAGE", + ID_LEVEL || exercise.ID_LEVEL, + exercise.ID_ADMIN_EXERCISE + ); + } + + await exercise.save({ transaction }); + + let payload = { + ID_ADMIN_EXERCISE: exercise.ID_ADMIN_EXERCISE, + ID_LEVEL: exercise.ID_LEVEL, + QUESTION: exercise.QUESTION, + SCORE_WEIGHT: exercise.SCORE_WEIGHT, + QUESTION_TYPE: exercise.QUESTION_TYPE, + AUDIO: exercise.AUDIO, + VIDEO: exercise.VIDEO, + IMAGE: exercise.IMAGE, + IS_DELETED: exercise.IS_DELETED, + TIME_ADMIN_EXC: exercise.TIME_ADMIN_EXC, + }; + + if (QUESTION_TYPE === "MPQ") { + if (!Array.isArray(PAIRS) || PAIRS.length === 0) { + await transaction.rollback(); + return response( + 400, + null, + "At least one pair is required for Matching Pairs", + res + ); + } + + const existingPairs = await models.MatchingPairs.findAll({ + where: { ID_ADMIN_EXERCISE: exercise.ID_ADMIN_EXERCISE }, + transaction, + }); + + const pairsToDelete = new Set( + existingPairs.map((pair) => pair.ID_MATCHING_PAIRS) + ); + + for (const pair of PAIRS) { + if (pair.ID_MATCHING_PAIRS) { + const existingPair = existingPairs.find( + (p) => p.ID_MATCHING_PAIRS === pair.ID_MATCHING_PAIRS + ); + if (existingPair) { + existingPair.LEFT_PAIR = pair.LEFT_PAIR; + existingPair.RIGHT_PAIR = pair.RIGHT_PAIR; + await existingPair.save({ transaction }); + pairsToDelete.delete(existingPair.ID_MATCHING_PAIRS); + } + } else { + await models.MatchingPairs.create( + { + ID_ADMIN_EXERCISE: exercise.ID_ADMIN_EXERCISE, + LEFT_PAIR: pair.LEFT_PAIR, + RIGHT_PAIR: pair.RIGHT_PAIR, + }, + { transaction } + ); + } + } + + for (const pairId of pairsToDelete) { + await models.MatchingPairs.destroy({ + where: { ID_MATCHING_PAIRS: pairId }, + transaction, + }); + } + + const updatedPairs = await models.MatchingPairs.findAll({ + where: { ID_ADMIN_EXERCISE: exercise.ID_ADMIN_EXERCISE }, + }); + payload.matchingPairs = updatedPairs; + } else if (QUESTION_TYPE === "MCQ") { + const { OPTION_A, OPTION_B, OPTION_C, OPTION_D, OPTION_E, ANSWER_KEY } = + req.body; + const multipleChoices = await models.MultipleChoices.findOne({ + where: { ID_ADMIN_EXERCISE: exercise.ID_ADMIN_EXERCISE }, + transaction, + }); + + if (!multipleChoices) { + await transaction.rollback(); + return response(404, null, "Multiple Choices not found", res); + } + + if (OPTION_A) multipleChoices.OPTION_A = OPTION_A; + if (OPTION_B) multipleChoices.OPTION_B = OPTION_B; + if (OPTION_C) multipleChoices.OPTION_C = OPTION_C; + if (OPTION_D) multipleChoices.OPTION_D = OPTION_D; + if (OPTION_E) multipleChoices.OPTION_E = OPTION_E; + if (ANSWER_KEY) multipleChoices.ANSWER_KEY = ANSWER_KEY; + + await multipleChoices.save({ transaction }); + payload.multipleChoices = multipleChoices; + } else if (QUESTION_TYPE === "TFQ") { + const { IS_TRUE } = req.body; + const trueFalse = await models.TrueFalse.findOne({ + where: { ID_ADMIN_EXERCISE: exercise.ID_ADMIN_EXERCISE }, + transaction, + }); + + if (!trueFalse) { + await transaction.rollback(); + return response(404, null, "True/False exercise not found", res); + } + + if (typeof IS_TRUE !== "undefined") { + trueFalse.IS_TRUE = IS_TRUE; + } + + await trueFalse.save({ transaction }); + payload.trueFalse = trueFalse; + } + + await transaction.commit(); + + response(200, payload, "Exercise updated successfully", res); + } catch (error) { + console.error(error); + clearFileBuffers({ IMAGE, AUDIO }); + await transaction.rollback(); + response(500, null, "Internal Server Error", res); + } +}; + export const deleteExerciseById = async (req, res) => { const { id } = req.params; const transaction = await models.db.transaction(); try { - const exercise = await models.Exercise.findByPk(id, { - include: [ - { - model: models.MultipleChoices, - as: "multipleChoices", - }, - { - model: models.MatchingPairs, - as: "matchingPairs", - }, - { - model: models.TrueFalse, - as: "trueFalse", - }, - ], - }); + const exercise = await models.Exercise.findByPk(id); if (!exercise) { await transaction.rollback(); return response(404, null, "Exercise not found", res); } - await models.StdExercise.destroy({ - where: { ID_ADMIN_EXERCISE: id }, - transaction, - }); - - if (exercise.AUDIO) { - const audioPath = path.join( - "public/uploads/exercise/audio", - exercise.AUDIO - ); - if (fs.existsSync(audioPath)) fs.unlinkSync(audioPath); - } - - if (exercise.IMAGE) { - const imagePath = path.join( - "public/uploads/exercise/image", - exercise.IMAGE - ); - if (fs.existsSync(imagePath)) fs.unlinkSync(imagePath); - } - - const questionType = exercise.QUESTION_TYPE; - - if (questionType === "MCQ") { - await models.MultipleChoices.destroy({ - where: { ID_ADMIN_EXERCISE: id }, - transaction, - }); - } else if (questionType === "MPQ") { - await models.MatchingPairs.destroy({ - where: { ID_ADMIN_EXERCISE: id }, - transaction, - }); - } else if (questionType === "TFQ") { - await models.TrueFalse.destroy({ - where: { ID_ADMIN_EXERCISE: id }, - transaction, - }); - } - - await exercise.destroy({ transaction }); + await exercise.update({ IS_DELETED: 1 }, { transaction }); await transaction.commit(); - - response(200, null, "Exercise and related data deleted successfully", res); + response(200, null, "Exercise soft-deleted successfully", res); } catch (error) { console.log(error); await transaction.rollback(); diff --git a/controllers/contentControllers/level.js b/controllers/contentControllers/level.js index 011af9c..36f0952 100644 --- a/controllers/contentControllers/level.js +++ b/controllers/contentControllers/level.js @@ -14,6 +14,9 @@ import { export const getLevels = async (req, res) => { try { const levels = await models.Level.findAll({ + where: { + IS_DELETED: 0, + }, attributes: { exclude: [ "ROUTE_1", @@ -38,6 +41,9 @@ export const getLevelById = async (req, res) => { const { id } = req.params; const level = await models.Level.findByPk(id, { + where: { + IS_DELETED: 0, + }, attributes: { exclude: [ "ROUTE_1", @@ -86,6 +92,117 @@ export const getLevelById = async (req, res) => { } }; +export const getLevelForAdmin = async (req, res) => { + const { page = 1, limit = 10, search = "", sort = "time" } = req.query; + + try { + const { count, rows: levels } = await models.Level.findAndCountAll({ + where: { + IS_DELETED: 0, + ...(search && { + [models.Op.or]: [ + { + "$levelTopic->topicSection.NAME_SECTION$": { + [models.Op.like]: `%${search}%`, + }, + }, + { + "$levelTopic.NAME_TOPIC$": { + [models.Op.like]: `%${search}%`, + }, + }, + { + NAME_LEVEL: { [models.Op.like]: `%${search}%` }, + }, + ], + }), + }, + attributes: ["ID_LEVEL", "NAME_LEVEL", "TIME_LEVEL"], + include: [ + { + model: models.Topic, + as: "levelTopic", + attributes: ["ID_TOPIC", "NAME_TOPIC"], + include: [ + { + model: models.Section, + as: "topicSection", + attributes: ["ID_SECTION", "NAME_SECTION"], + }, + ], + }, + ], + distinct: true, + }); + + const formattedLevels = levels.map((level) => ({ + ID_LEVEL: level.ID_LEVEL, + ID_SECTION: level.levelTopic.topicSection.ID_SECTION, + ID_TOPIC: level.levelTopic.ID_TOPIC, + NAME_SECTION: level.levelTopic.topicSection.NAME_SECTION, + NAME_TOPIC: level.levelTopic.NAME_TOPIC, + NAME_LEVEL: level.NAME_LEVEL, + TIME_LEVEL: level.TIME_LEVEL, + })); + + if (sort === "section") { + formattedLevels.sort((a, b) => + a.NAME_SECTION.localeCompare(b.NAME_SECTION) + ); + } else if (sort === "topic") { + formattedLevels.sort((a, b) => { + const topicComparison = a.NAME_TOPIC.localeCompare(b.NAME_TOPIC); + if (topicComparison === 0) { + if (a.NAME_LEVEL === "Pretest") return -1; + if (b.NAME_LEVEL === "Pretest") return 1; + + const levelA = parseInt(a.NAME_LEVEL.replace("Level ", "")) || 0; + const levelB = parseInt(b.NAME_LEVEL.replace("Level ", "")) || 0; + return levelA - levelB; + } + return topicComparison; + }); + } else if (sort === "level") { + formattedLevels.sort((a, b) => { + if (a.NAME_LEVEL === "Pretest") return -1; + if (b.NAME_LEVEL === "Pretest") return 1; + + const levelA = parseInt(a.NAME_LEVEL.replace("Level ", "")) || 0; + const levelB = parseInt(b.NAME_LEVEL.replace("Level ", "")) || 0; + + return levelA - levelB; + }); + } else { + formattedLevels.sort( + (a, b) => new Date(b.TIME_LEVEL) - new Date(a.TIME_LEVEL) + ); + } + + const paginatedLevels = formattedLevels.slice( + (page - 1) * limit, + page * limit + ); + + const totalPages = Math.ceil(count / limit); + const currentPage = parseInt(page); + + response( + 200, + { + levels: paginatedLevels, + currentPage, + totalPages, + totalItems: count, + }, + "Levels retrieved successfully", + res + ); + } catch (error) { + console.log(error); + response(500, null, "Error retrieving levels data!", res); + } +}; + export const getLevelsByTopicId = async (req, res) => { try { const { idTopic } = req.params; @@ -106,6 +223,7 @@ export const getLevelsByTopicId = async (req, res) => { const levels = await models.Level.findAll({ where: { ID_TOPIC: idTopic, + IS_DELETED: 0, }, attributes: { exclude: [ @@ -126,6 +244,7 @@ export const getLevelsByTopicId = async (req, res) => { "ID_STUDENT_LEARNING", "STUDENT_START", "STUDENT_FINISH", + "NEXT_LEARNING", ], where: { ID: ID, @@ -156,9 +275,7 @@ export const getLevelsByTopicId = async (req, res) => { const lastCompletedLearning = await models.StdLearning.findOne({ where: { ID: ID, - STUDENT_FINISH: { - [models.Op.not]: null, - }, + STUDENT_FINISH: { [models.Op.not]: null }, }, include: [ { @@ -170,6 +287,31 @@ export const getLevelsByTopicId = async (req, res) => { order: [["STUDENT_FINISH", "DESC"]], }); + const nextLearningLevel = lastCompletedLearning?.NEXT_LEARNING + ? await models.Level.findOne({ + where: { + ID_LEVEL: lastCompletedLearning.NEXT_LEARNING, + IS_DELETED: 0, + }, + attributes: ["ID_LEVEL", "NAME_LEVEL"], + }) + : null; + + const unlockedLevels = lastCompletedLearning + ? await models.Level.findAll({ + where: { + ID_TOPIC: idTopic, + IS_DELETED: 0, + [models.Op.or]: [ + { NAME_LEVEL: { [models.Op.lt]: nextLearningLevel.NAME_LEVEL } }, + { NAME_LEVEL: "Pretest" }, + ], + }, + attributes: ["NAME_LEVEL"], + order: [["NAME_LEVEL", "ASC"]], + }) + : [{ NAME_LEVEL: "Pretest" }]; + const levelsWithScore = levels.map((level) => { const SCORE = level.stdLearning && level.stdLearning.length > 0 @@ -181,7 +323,6 @@ export const getLevelsByTopicId = async (req, res) => { : null; const levelJSON = level.toJSON(); - const NAME_SECTION = levelJSON.levelTopic.topicSection.NAME_SECTION; const NAME_TOPIC = levelJSON.levelTopic.NAME_TOPIC; @@ -216,9 +357,15 @@ export const getLevelsByTopicId = async (req, res) => { NAME_LEVEL: lastCompletedLearning.level.NAME_LEVEL, SCORE: lastCompletedLearning.SCORE, NEXT_LEARNING: lastCompletedLearning.NEXT_LEARNING, + NEXT_LEARNING_LEVEL: nextLearningLevel + ? nextLearningLevel.NAME_LEVEL + : null, FINISHED_AT: lastCompletedLearning.STUDENT_FINISH, + UNLOCKED_LEVELS: unlockedLevels.map((lvl) => lvl.NAME_LEVEL), } - : null, + : { + UNLOCKED_LEVELS: ["Pretest"], + }, levels: sortedLevels, }; @@ -247,6 +394,7 @@ 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 { @@ -267,7 +415,11 @@ export const createLevel = async (req, res, next) => { } const existingLevel = await models.Level.findOne({ - where: { NAME_LEVEL, ID_TOPIC }, + where: { + NAME_LEVEL, + ID_TOPIC, + IS_DELETED: 0, + }, transaction, }); @@ -367,6 +519,7 @@ export const updateLevelById = async (req, res, next) => { NAME_LEVEL, ID_TOPIC, ID_LEVEL: { [models.Sequelize.Op.ne]: id }, + IS_DELETED: 0, }, transaction, }); @@ -385,7 +538,7 @@ export const updateLevelById = async (req, res, next) => { if (NAME_LEVEL) { level.NAME_LEVEL = NAME_LEVEL; - level.IS_PRETEST = NAME_LEVEL === "Level 1" ? 1 : 0; + level.IS_PRETEST = NAME_LEVEL === "Pretest" ? 1 : 0; } if (ID_SECTION) level.ID_SECTION = ID_SECTION; if (ID_TOPIC) level.ID_TOPIC = ID_TOPIC; @@ -451,41 +604,98 @@ export const deleteLevelById = async (req, res, next) => { return response(404, null, "Level not found", res); } - const deleteFile = (filePath) => { - if (fs.existsSync(filePath)) { - fs.unlinkSync(filePath); - } - }; + level.IS_DELETED = 1; + await level.save(); - if (level.AUDIO) { - const audioPath = path.join("public/uploads/level/audio", level.AUDIO); - deleteFile(audioPath); - } - - if (level.IMAGE) { - const imagePath = path.join("public/uploads/level/image", level.IMAGE); - deleteFile(imagePath); - } + await models.Exercise.update( + { IS_DELETED: 1 }, + { where: { ID_LEVEL: id } } + ); req.body.newLevelId = level.ID_LEVEL; - await level.destroy(); - await updateOtherLevelsRoutesOnDelete(req, res, next); - response(200, null, "Level deleted successfully", res); + response( + 200, + null, + "Level and related exercises soft deleted successfully", + res + ); } catch (error) { console.log(error); return response(500, null, "Internal Server Error", res); } }; +export const deleteLevelFileById = async (req, res) => { + const { id } = req.params; + const { fileType } = req.body; + + if (!["audio", "image", "video"].includes(fileType)) { + return response(400, null, "Invalid file type specified", res); + } + + try { + const level = await models.Level.findByPk(id); + + if (!level) { + return response(404, null, "Level not found", res); + } + + let filePath; + let fileName; + + if (fileType === "audio" && level.AUDIO) { + fileName = level.AUDIO; + filePath = path.join("public/uploads/level/audio", fileName); + level.AUDIO = null; + } else if (fileType === "image" && level.IMAGE) { + fileName = level.IMAGE; + filePath = path.join("public/uploads/level/image", fileName); + level.IMAGE = null; + } else if (fileType === "video" && level.VIDEO) { + level.VIDEO = null; + } else { + return response( + 404, + null, + `${ + fileType.charAt(0).toUpperCase() + fileType.slice(1) + } file not found`, + res + ); + } + + if (filePath && fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + } + + await level.save(); + + response( + 200, + level, + `${ + fileType.charAt(0).toUpperCase() + fileType.slice(1) + } file deleted successfully`, + res + ); + } catch (error) { + console.log(error); + response(500, null, "Internal Server Error", res); + } +}; + export const getPreviousLevel = async (req, res) => { try { const { next_learning } = req.params; const { ID } = req.user; const currentLevel = await models.Level.findByPk(next_learning, { + where: { + IS_DELETED: 0, + }, include: [ { model: models.StdLearning, @@ -530,6 +740,7 @@ export const getPreviousLevel = async (req, res) => { const previousLevels = await models.Level.findAll({ where: { ID_TOPIC: ID_TOPIC, + IS_DELETED: 0, NAME_LEVEL: { [models.Op.or]: [ { [models.Op.like]: "Pretest" }, @@ -545,7 +756,6 @@ export const getPreviousLevel = async (req, res) => { }, ], }, - order: [["NAME_LEVEL", "ASC"]], attributes: { exclude: [ "ROUTE_1", @@ -610,13 +820,23 @@ export const getPreviousLevel = async (req, res) => { delete currentLevelWithScore.stdLearning; - if (!previousLevelsWithScore.length && !currentLevelWithScore) { + const sortedLevels = previousLevelsWithScore.sort((a, b) => { + if (a.NAME_LEVEL === "Pretest") return -1; + if (b.NAME_LEVEL === "Pretest") return 1; + + const levelA = parseInt(a.NAME_LEVEL.replace("Level ", "")); + const levelB = parseInt(b.NAME_LEVEL.replace("Level ", "")); + + return levelA - levelB; + }); + + if (!sortedLevels.length && !currentLevelWithScore) { return res.status(404).json({ message: "No levels found" }); } const result = { currentLevel: currentLevelWithScore, - previousLevels: previousLevelsWithScore, + previousLevels: sortedLevels, }; res.status(200).json({ message: "Success", data: result }); diff --git a/controllers/contentControllers/section.js b/controllers/contentControllers/section.js index 510401a..57a2a75 100644 --- a/controllers/contentControllers/section.js +++ b/controllers/contentControllers/section.js @@ -9,7 +9,11 @@ import { export const getSections = async (req, res) => { try { - const sections = await models.Section.findAll(); + const sections = await models.Section.findAll({ + where: { + IS_DELETED: 0, + }, + }); response(200, sections, "Success", res); } catch (error) { console.log(error); @@ -33,6 +37,74 @@ export const getSectionById = async (req, res) => { } }; +export const getSectionForAdmin = async (req, res) => { + const { page = 1, limit = 10, search = "", sort = "time" } = req.query; + + try { + const { count, rows: sections } = await models.Section.findAndCountAll({ + where: { + IS_DELETED: 0, + ...(search && { + [models.Op.or]: [ + { + NAME_SECTION: { + [models.Op.like]: `%${search}%`, + }, + }, + { + DESCRIPTION_SECTION: { + [models.Op.like]: `%${search}%`, + }, + }, + ], + }), + }, + attributes: ["ID_SECTION", "NAME_SECTION", "DESCRIPTION_SECTION", "TIME_SECTION"], + distinct: true, + }); + + const formattedSections = sections.map((section) => ({ + ID_SECTION: section.ID_SECTION, + NAME_SECTION: section.NAME_SECTION, + DESCRIPTION_SECTION: section.DESCRIPTION_SECTION, + TIME_SECTION: section.TIME_SECTION, + })); + + if (sort === "section") { + formattedSections.sort((a, b) => a.NAME_SECTION.localeCompare(b.NAME_SECTION)); + } else if (sort === "description") { + formattedSections.sort((a, b) => a.DESCRIPTION_SECTION.localeCompare(b.DESCRIPTION_SECTION)); + } else { + formattedSections.sort( + (a, b) => new Date(b.TIME_SECTION) - new Date(a.TIME_SECTION) + ); + } + + const paginatedSections = formattedSections.slice( + (page - 1) * limit, + page * limit + ); + + const totalPages = Math.ceil(count / limit); + const currentPage = parseInt(page); + + response( + 200, + { + sections: paginatedSections, + currentPage, + totalPages, + totalItems: count, + }, + "Sections retrieved successfully", + res + ); + } catch (error) { + console.log(error); + response(500, null, "Error retrieving sections data!", res); + } +}; + export const createSection = async (req, res) => { const { NAME_SECTION, DESCRIPTION_SECTION } = req.body; const { THUMBNAIL } = req.filesToSave || {}; @@ -51,7 +123,7 @@ export const createSection = async (req, res) => { try { const existingSection = await models.Section.findOne({ - where: { NAME_SECTION }, + where: { NAME_SECTION, IS_DELETED: 0 }, transaction, }); @@ -167,19 +239,43 @@ export const deleteSectionById = async (req, res) => { return response(404, null, "Section not found", res); } - if (section.THUMBNAIL) { - const thumbnailPath = path.join( - "public/uploads/section", - section.THUMBNAIL - ); - if (fs.existsSync(thumbnailPath)) { - fs.unlinkSync(thumbnailPath); + section.IS_DELETED = 1; + await section.save(); + + await models.Topic.update({ IS_DELETED: 1 }, { where: { ID_SECTION: id } }); + + await models.Level.update( + { IS_DELETED: 1 }, + { + where: { + ID_TOPIC: { + [models.Op.in]: models.Sequelize.literal( + `(SELECT ID_TOPIC FROM topic WHERE ID_SECTION = '${id}')` + ), + }, + }, } - } + ); - await section.destroy(); + await models.Exercise.update( + { IS_DELETED: 1 }, + { + where: { + ID_LEVEL: { + [models.Op.in]: models.Sequelize.literal( + `(SELECT ID_LEVEL FROM level WHERE ID_TOPIC IN (SELECT ID_TOPIC FROM topic WHERE ID_SECTION = '${id}') )` + ), + }, + }, + } + ); - response(200, null, "Section deleted successfully", res); + response( + 200, + null, + "Section, topics, levels, and related exercises soft deleted successfully", + res + ); } catch (error) { console.log(error); response(500, null, "Internal Server Error", res); diff --git a/controllers/contentControllers/topic.js b/controllers/contentControllers/topic.js index 7d58e67..a5ea60c 100644 --- a/controllers/contentControllers/topic.js +++ b/controllers/contentControllers/topic.js @@ -3,7 +3,11 @@ import models from "../../models/index.js"; export const getTopics = async (req, res) => { try { - const topics = await models.Topic.findAll(); + const topics = await models.Topic.findAll({ + where: { + IS_DELETED: 0, + }, + }); response(200, topics, "Success", res); } catch (error) { console.log(error); @@ -30,6 +34,7 @@ export const getTopicById = async (req, res) => { export const getTopicBySectionId = async (req, res) => { try { const { sectionId } = req.params; + const userId = req.user.ID; const sectionExists = await models.Section.findByPk(sectionId); if (!sectionExists) { @@ -37,17 +42,124 @@ export const getTopicBySectionId = async (req, res) => { } const topics = await models.Topic.findAll({ - where: { ID_SECTION: sectionId }, + where: { ID_SECTION: sectionId, IS_DELETED: 0 }, + attributes: ["ID_TOPIC", "NAME_TOPIC", "DESCRIPTION_TOPIC"], }); if (!topics || topics.length === 0) { return response(404, null, "No topics found for this section", res); } - response(200, topics, "Success", res); + const topicsWithCompletionStatus = await Promise.all( + topics.map(async (topic) => { + const level6 = await models.Level.findOne({ + where: { + NAME_LEVEL: "Level 6", + ID_TOPIC: topic.ID_TOPIC, + }, + }); + + let isCompleted = 0; + if (level6) { + const stdLearning = await models.StdLearning.findOne({ + where: { + ID: userId, + ID_LEVEL: level6.ID_LEVEL, + IS_PASS: 1, + }, + }); + isCompleted = stdLearning ? 1 : 0; + } + + return { + ...topic.get({ plain: true }), + IS_COMPLETED: isCompleted, + }; + }) + ); + + response(200, topicsWithCompletionStatus, "Success", res); + } catch (error) { + console.error(error); + response(500, null, "Internal Server Error", res); + } +}; + +export const getTopicForAdmin = async (req, res) => { + const { page = 1, limit = 10, search = "", sort = "time" } = req.query; + + try { + const { count, rows: topics } = await models.Topic.findAndCountAll({ + where: { + IS_DELETED: 0, + ...(search && { + [models.Op.or]: [ + { + "$topicSection.NAME_SECTION$": { + [models.Op.like]: `%${search}%`, + }, + }, + { + NAME_TOPIC: { + [models.Op.like]: `%${search}%`, + }, + }, + ], + }), + }, + attributes: ["ID_TOPIC", "NAME_TOPIC", "TIME_TOPIC"], + include: [ + { + model: models.Section, + as: "topicSection", + attributes: ["ID_SECTION", "NAME_SECTION"], + }, + ], + distinct: true, + }); + + const formattedTopics = topics.map((topic) => ({ + ID_SECTION: topic.topicSection.ID_SECTION, + ID_TOPIC: topic.ID_TOPIC, + NAME_SECTION: topic.topicSection.NAME_SECTION, + NAME_TOPIC: topic.NAME_TOPIC, + TIME_TOPIC: topic.TIME_TOPIC, + })); + + if (sort === "section") { + formattedTopics.sort((a, b) => + a.NAME_SECTION.localeCompare(b.NAME_SECTION) + ); + } else if (sort === "topic") { + formattedTopics.sort((a, b) => a.NAME_TOPIC.localeCompare(b.NAME_TOPIC)); + } else { + formattedTopics.sort( + (a, b) => new Date(b.TIME_TOPIC) - new Date(a.TIME_TOPIC) + ); + } + + const paginatedTopics = formattedTopics.slice( + (page - 1) * limit, + page * limit + ); + + const totalPages = Math.ceil(count / limit); + const currentPage = parseInt(page); + + response( + 200, + { + topics: paginatedTopics, + currentPage, + totalPages, + totalItems: count, + }, + "Topics retrieved successfully", + res + ); } catch (error) { console.log(error); - response(500, null, "Internal Server Error", res); + response(500, null, "Error retrieving topics data!", res); } }; @@ -131,9 +243,30 @@ export const deleteTopicById = async (req, res) => { return response(404, null, "Topic not found", res); } - await topic.destroy(); + topic.IS_DELETED = 1; + await topic.save(); - response(200, null, "Topic deleted successfully", res); + await models.Level.update({ IS_DELETED: 1 }, { where: { ID_TOPIC: id } }); + + await models.Exercise.update( + { IS_DELETED: 1 }, + { + where: { + ID_LEVEL: { + [models.Op.in]: models.Sequelize.literal( + `(SELECT ID_LEVEL FROM level WHERE ID_TOPIC = '${id}')` + ), + }, + }, + } + ); + + response( + 200, + null, + "Topic, levels, and related exercises soft deleted successfully", + res + ); } catch (error) { console.log(error); response(500, null, "Internal Server Error", res); @@ -177,9 +310,9 @@ export const getCompletedTopicsBySection = async (req, res) => { ], }); - // if (!completedLevels.length) { - // return response(404, null, "No completed topics found", res); - // } + if (!completedLevels.length) { + return response(200, null, "No completed topics found", res); + } const completedSections = {}; @@ -217,9 +350,9 @@ export const getCompletedTopicsBySection = async (req, res) => { const result = Object.values(completedSections); - // if (!result.length) { - // return response(404, null, "No section with completed topics found", res); - // } + if (!result.length) { + return response(200, null, "No section with completed topics found", res); + } response( 200, diff --git a/controllers/exerciseTypesControllers/matchingPairs.js b/controllers/exerciseTypesControllers/matchingPairs.js index 99e3442..7a092c8 100644 --- a/controllers/exerciseTypesControllers/matchingPairs.js +++ b/controllers/exerciseTypesControllers/matchingPairs.js @@ -38,11 +38,20 @@ export const createMatchingPairsExercise = async (req, res) => { } let generatedTitle = TITLE; + if (!TITLE) { - const exerciseCount = await models.Exercise.count({ + const lastExercise = await models.Exercise.findOne({ where: { ID_LEVEL }, + order: [["TITLE", "DESC"]], + limit: 1, }); - generatedTitle = `Soal ${exerciseCount + 1}`; + + if (lastExercise && lastExercise.TITLE) { + const lastTitleNumber = parseInt(lastExercise.TITLE.split(" ")[1], 10); + generatedTitle = `Soal ${lastTitleNumber + 1}`; + } else { + generatedTitle = `Soal 1`; + } } const existingExercise = await models.Exercise.findOne({ diff --git a/controllers/exerciseTypesControllers/multipleChoices.js b/controllers/exerciseTypesControllers/multipleChoices.js index 26940da..c3b50ca 100644 --- a/controllers/exerciseTypesControllers/multipleChoices.js +++ b/controllers/exerciseTypesControllers/multipleChoices.js @@ -45,11 +45,20 @@ export const createMultipleChoicesExercise = async (req, res) => { } let generatedTitle = TITLE; + if (!TITLE) { - const exerciseCount = await models.Exercise.count({ + const lastExercise = await models.Exercise.findOne({ where: { ID_LEVEL }, + order: [["TITLE", "DESC"]], + limit: 1, }); - generatedTitle = `Soal ${exerciseCount + 1}`; + + if (lastExercise && lastExercise.TITLE) { + const lastTitleNumber = parseInt(lastExercise.TITLE.split(" ")[1], 10); + generatedTitle = `Soal ${lastTitleNumber + 1}`; + } else { + generatedTitle = `Soal 1`; + } } const existingExercise = await models.Exercise.findOne({ diff --git a/controllers/exerciseTypesControllers/trueFalse.js b/controllers/exerciseTypesControllers/trueFalse.js index 20a0e6c..09bdd9d 100644 --- a/controllers/exerciseTypesControllers/trueFalse.js +++ b/controllers/exerciseTypesControllers/trueFalse.js @@ -28,11 +28,20 @@ export const createTrueFalseExercise = async (req, res) => { } let generatedTitle = TITLE; + if (!TITLE) { - const exerciseCount = await models.Exercise.count({ + const lastExercise = await models.Exercise.findOne({ where: { ID_LEVEL }, + order: [["TITLE", "DESC"]], + limit: 1, }); - generatedTitle = `Soal ${exerciseCount + 1}`; + + if (lastExercise && lastExercise.TITLE) { + const lastTitleNumber = parseInt(lastExercise.TITLE.split(" ")[1], 10); + generatedTitle = `Soal ${lastTitleNumber + 1}`; + } else { + generatedTitle = `Soal 1`; + } } const existingExercise = await models.Exercise.findOne({ @@ -196,4 +205,4 @@ export const updateTrueFalseExerciseById = async (req, res) => { await transaction.rollback(); response(500, null, "Internal Server Error", res); } -}; \ No newline at end of file +}; diff --git a/controllers/learningControllers/stdLearning.js b/controllers/learningControllers/stdLearning.js index 14e7399..cc336a9 100644 --- a/controllers/learningControllers/stdLearning.js +++ b/controllers/learningControllers/stdLearning.js @@ -86,10 +86,14 @@ export const updateStdLearningById = async (req, res) => { return response(404, null, "Student Learning record not found", res); } + if (req.user.ID !== stdLearning.ID) { + return response(403, null, "Unauthorized to update this record", res); + } + stdLearning.STUDENT_FINISH = new Date(); - stdLearning.SCORE = req.body.SCORE; - stdLearning.NEXT_LEARNING = req.body.NEXT_LEARNING; - stdLearning.IS_PASS = req.body.IS_PASS; + stdLearning.SCORE = req.body.SCORE || stdLearning.SCORE; + stdLearning.NEXT_LEARNING = req.body.NEXT_LEARNING || stdLearning.NEXT_LEARNING; + stdLearning.IS_PASS = req.body.IS_PASS || stdLearning.IS_PASS; if (FEEDBACK_STUDENT) { stdLearning.FEEDBACK_STUDENT = FEEDBACK_STUDENT; diff --git a/controllers/usersControllers/user.js b/controllers/usersControllers/user.js index bb2f3a6..fe6d680 100644 --- a/controllers/usersControllers/user.js +++ b/controllers/usersControllers/user.js @@ -81,7 +81,7 @@ export const getTeachers = async (req, res) => { ID: teacher.ID, NAME_USERS: teacher.NAME_USERS, EMAIL: teacher.EMAIL, - NISN: teacher.teachers.NIP, + NIP: teacher.teachers.NIP, ROLE: teacher.ROLE, })); diff --git a/index.js b/index.js index b1f4ca1..bf09f21 100644 --- a/index.js +++ b/index.js @@ -9,7 +9,7 @@ dotenv.config(); const app = express(); const corsOptions = { - origin: "*", + origin: "http://localhost:5173", methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"], allowedHeaders: ["Content-Type", "Authorization"], credentials: true, diff --git a/middlewares/Level/checkLevel.js b/middlewares/Level/checkLevel.js index f131dfa..82690e5 100644 --- a/middlewares/Level/checkLevel.js +++ b/middlewares/Level/checkLevel.js @@ -6,7 +6,10 @@ export const checkLevelsPerTopic = async (req, res, next) => { try { const levelCount = await models.Level.count({ - where: { ID_TOPIC }, + where: { + ID_TOPIC, + IS_DELETED: 0, + }, }); if (levelCount >= 7) { @@ -76,6 +79,7 @@ export const autoCalculateRoutes = async (req, res, next) => { NAME_LEVEL: { [models.Sequelize.Op.in]: levelTitles, }, + IS_DELETED: 0, }, }); @@ -185,6 +189,7 @@ export const updateOtherLevelsRoutes = async (req, res, next) => { NAME_LEVEL: { [models.Sequelize.Op.in]: levelTitles, }, + IS_DELETED: 0, }, }); @@ -285,6 +290,7 @@ export const updateOtherLevelsRoutesOnDelete = async (req, res, next) => { const levelsToUpdate = await models.Level.findAll({ where: { + IS_DELETED: 0, [models.Sequelize.Op.or]: [ { ROUTE_1: newLevelId }, { ROUTE_2: newLevelId }, diff --git a/middlewares/autoGrading.js b/middlewares/autoGrading.js index 379ee98..d54ee3c 100644 --- a/middlewares/autoGrading.js +++ b/middlewares/autoGrading.js @@ -109,7 +109,10 @@ export const calculateScore = async (req, res, next) => { } const allExercises = await models.Exercise.findAll({ - where: { ID_LEVEL: stdLearning.ID_LEVEL }, + where: { + ID_LEVEL: stdLearning.ID_LEVEL, + IS_DELETED: 0, + }, }); if (!allExercises || allExercises.length === 0) { @@ -158,7 +161,10 @@ export const checkFirstFiveCorrect = async (req, res, next) => { } const firstFiveExercises = await models.Exercise.findAll({ - where: { ID_LEVEL: stdLearning.ID_LEVEL }, + where: { + ID_LEVEL: stdLearning.ID_LEVEL, + IS_DELETED: 0, + }, order: [["TITLE", "ASC"]], limit: 5, }); @@ -207,7 +213,10 @@ export const nextLearning = async (req, res, next) => { const topic_id = stdLearning.level.ID_TOPIC; const levels = await models.Level.findAll({ - where: { ID_TOPIC: topic_id }, + where: { + ID_TOPIC: topic_id, + IS_DELETED: 0, + }, order: [["NAME_LEVEL", "ASC"]], }); diff --git a/middlewares/uploadExercise.js b/middlewares/uploadExercise.js index 70c9460..1721b61 100644 --- a/middlewares/uploadExercise.js +++ b/middlewares/uploadExercise.js @@ -9,35 +9,28 @@ const memoryStorage = multer.memoryStorage(); const fileFilter = (req, file, cb) => { const ext = path.extname(file.originalname).toLowerCase(); - switch (file.fieldname) { - case "AUDIO": - if (ext === ".mp3") { - cb(null, true); - } else { - cb( - new Error( - "Invalid file type, only .mp3 files are allowed for audio!" - ), - false - ); - } - break; - - case "IMAGE": - if (ext === ".jpg" || ext === ".jpeg" || ext === ".png") { - cb(null, true); - } else { - cb( - new Error( - "Invalid file type, only .jpg, .jpeg, and .png files are allowed for image!" - ), - false - ); - } - break; - - default: - cb(new Error("Invalid file type!"), false); + if (file.fieldname.startsWith("AUDIO")) { + if (ext === ".mp3") { + cb(null, true); + } else { + cb( + new Error("Invalid file type, only .mp3 files are allowed for audio!"), + false + ); + } + } else if (file.fieldname.startsWith("IMAGE")) { + if (ext === ".jpg" || ext === ".jpeg" || ext === ".png") { + cb(null, true); + } else { + cb( + new Error( + "Invalid file type, only .jpg, .jpeg, and .png files are allowed for image!" + ), + false + ); + } + } else { + cb(new Error("Invalid file type!"), false); } }; @@ -47,10 +40,7 @@ const upload = multer({ limits: { fileSize: 100 * 1024 * 1024, }, -}).fields([ - { name: "AUDIO", maxCount: 1 }, - { name: "IMAGE", maxCount: 1 }, -]); +}).any(); const handleUpload = (req, res, next) => { upload(req, res, (err) => { @@ -58,37 +48,35 @@ const handleUpload = (req, res, next) => { return response(400, null, err.message, res); } - const files = req.files; - const AUDIO = files?.AUDIO ? files.AUDIO[0] : null; - const IMAGE = files?.IMAGE ? files.IMAGE[0] : null; + const files = req.files || []; + req.filesToSave = {}; - try { - let validFiles = true; - let errorMessages = []; + let validFiles = true; + let errorMessages = []; - if (AUDIO && AUDIO.size > 10 * 1024 * 1024) { - validFiles = false; - AUDIO.buffer = null; - errorMessages.push("Audio file exceeds the size limit of 10MB"); + files.forEach((file) => { + if (file.fieldname.startsWith("AUDIO")) { + if (file.size > 10 * 1024 * 1024) { + validFiles = false; + errorMessages.push(`Audio file exceeds the size limit of 10MB`); + } else { + req.filesToSave[file.fieldname] = file; + } + } else if (file.fieldname.startsWith("IMAGE")) { + if (file.size > 5 * 1024 * 1024) { + validFiles = false; + errorMessages.push(`Image file exceeds the size limit of 5MB`); + } else { + req.filesToSave[file.fieldname] = file; + } } + }); - if (IMAGE && IMAGE.size > 5 * 1024 * 1024) { - validFiles = false; - IMAGE.buffer = null; - errorMessages.push("Image file exceeds the size limit of 5MB"); - } - - if (validFiles) { - req.filesToSave = { AUDIO, IMAGE }; - next(); - } else { - clearFileBuffers({ AUDIO, IMAGE }); - return response(400, null, errorMessages.join("; "), res); - } - } catch (error) { - console.log(error); - clearFileBuffers({ video, AUDIO, IMAGE }); - return response(500, null, "Internal Server Error", res); + if (validFiles) { + next(); + } else { + clearFileBuffers(req.filesToSave); + return response(400, null, errorMessages.join("; "), res); } }); }; diff --git a/middlewares/uploadExerciseSingle.js b/middlewares/uploadExerciseSingle.js new file mode 100644 index 0000000..c61602c --- /dev/null +++ b/middlewares/uploadExerciseSingle.js @@ -0,0 +1,137 @@ +import multer from "multer"; +import crypto from "crypto"; +import path from "path"; +import fs from "fs"; +import response from "../response.js"; + +const memoryStorage = multer.memoryStorage(); + +const fileFilter = (req, file, cb) => { + const ext = path.extname(file.originalname).toLowerCase(); + + switch (file.fieldname) { + case "AUDIO": + if (ext === ".mp3") { + cb(null, true); + } else { + cb( + new Error( + "Invalid file type, only .mp3 files are allowed for audio!" + ), + false + ); + } + break; + + case "IMAGE": + if (ext === ".jpg" || ext === ".jpeg" || ext === ".png") { + cb(null, true); + } else { + cb( + new Error( + "Invalid file type, only .jpg, .jpeg, and .png files are allowed for image!" + ), + false + ); + } + break; + + default: + cb(new Error("Invalid file type!"), false); + } +}; + +const upload = multer({ + storage: memoryStorage, + fileFilter, + limits: { + fileSize: 100 * 1024 * 1024, + }, +}).fields([ + { name: "AUDIO", maxCount: 1 }, + { name: "IMAGE", maxCount: 1 }, +]); + +const handleUploadSingleExercise = (req, res, next) => { + upload(req, res, (err) => { + if (err) { + return response(400, null, err.message, res); + } + + const files = req.files; + const AUDIO = files?.AUDIO ? files.AUDIO[0] : null; + const IMAGE = files?.IMAGE ? files.IMAGE[0] : null; + + try { + let validFiles = true; + let errorMessages = []; + + if (AUDIO && AUDIO.size > 10 * 1024 * 1024) { + validFiles = false; + AUDIO.buffer = null; + errorMessages.push("Audio file exceeds the size limit of 10MB"); + } + + if (IMAGE && IMAGE.size > 5 * 1024 * 1024) { + validFiles = false; + IMAGE.buffer = null; + errorMessages.push("Image file exceeds the size limit of 5MB"); + } + + if (validFiles) { + req.filesToSave = { AUDIO, IMAGE }; + next(); + } else { + clearFileBuffers({ AUDIO, IMAGE }); + return response(400, null, errorMessages.join("; "), res); + } + } catch (error) { + console.log(error); + clearFileBuffers({ video, AUDIO, IMAGE }); + return response(500, null, "Internal Server Error", res); + } + }); +}; + +export const clearFileBuffers = (files) => { + for (const file of Object.values(files)) { + if (file && file.buffer) { + file.buffer = null; + } + } +}; + +export const generateHash = (levelId, filename, bufferLength) => { + return crypto + .createHash("md5") + .update(levelId + filename + bufferLength) + .digest("hex"); +}; + +export const saveFileToDisk = (file, type, levelId, exerciseId) => { + const ext = path.extname(file.originalname); + const hash = generateHash(levelId, file.originalname, file.buffer.length); + const filename = `${type}-${exerciseId}-${hash}${ext}`; + + let folderPath; + switch (type) { + case "AUDIO": + folderPath = path.join("public/uploads/exercise/audio"); + break; + case "IMAGE": + folderPath = path.join("public/uploads/exercise/image"); + break; + default: + folderPath = path.join("public/uploads/exercise"); + } + + if (!fs.existsSync(folderPath)) { + fs.mkdirSync(folderPath, { recursive: true }); + } + + const filepath = path.join(folderPath, filename); + fs.writeFileSync(filepath, file.buffer); + return filename; +}; + +export default handleUploadSingleExercise; diff --git a/models/contentModels/exerciseModel.js b/models/contentModels/exerciseModel.js index d4945c7..e1d8c9f 100644 --- a/models/contentModels/exerciseModel.js +++ b/models/contentModels/exerciseModel.js @@ -65,6 +65,15 @@ const ExerciseModel = (DataTypes) => { type: DataTypes.STRING(1024), allowNull: true, }, + IS_DELETED: { + type: DataTypes.INTEGER(1), + allowNull: true, + defaultValue: 0, + validate: { + min: 0, + max: 1, + }, + }, TIME_ADMIN_EXC: { type: DataTypes.DATE, allowNull: true, diff --git a/models/contentModels/levelModel.js b/models/contentModels/levelModel.js index c0505df..4fec91a 100644 --- a/models/contentModels/levelModel.js +++ b/models/contentModels/levelModel.js @@ -90,6 +90,15 @@ const LevelModel = (DataTypes) => { type: DataTypes.UUID, allowNull: false, }, + IS_DELETED: { + type: DataTypes.INTEGER(1), + allowNull: true, + defaultValue: 0, + validate: { + min: 0, + max: 1, + }, + }, TIME_LEVEL: { type: DataTypes.DATE, allowNull: true, diff --git a/models/contentModels/sectionModel.js b/models/contentModels/sectionModel.js index 3249ade..e6ecb9e 100644 --- a/models/contentModels/sectionModel.js +++ b/models/contentModels/sectionModel.js @@ -30,6 +30,15 @@ const SectionModel = (DataTypes) => { type: DataTypes.STRING(255), allowNull: true, }, + IS_DELETED: { + type: DataTypes.INTEGER(1), + allowNull: true, + defaultValue: 0, + validate: { + min: 0, + max: 1, + }, + }, TIME_SECTION: { type: DataTypes.DATE, allowNull: false, diff --git a/models/contentModels/topicModel.js b/models/contentModels/topicModel.js index 8488580..5009110 100644 --- a/models/contentModels/topicModel.js +++ b/models/contentModels/topicModel.js @@ -37,6 +37,15 @@ const TopicModel = (DataTypes) => { notEmpty: true, }, }, + IS_DELETED: { + type: DataTypes.INTEGER(1), + allowNull: true, + defaultValue: 0, + validate: { + min: 0, + max: 1, + }, + }, TIME_TOPIC: { type: DataTypes.DATE, allowNull: true, diff --git a/public/uploads/exercise/audio/AUDIO-171da194-1048-45f8-8561-ae000621f3ed-c4775f42af4a2e1e3e4d5cb875e261dd.mp3 b/public/uploads/exercise/audio/AUDIO-171da194-1048-45f8-8561-ae000621f3ed-c4775f42af4a2e1e3e4d5cb875e261dd.mp3 new file mode 100644 index 0000000..9bf1b59 Binary files /dev/null and b/public/uploads/exercise/audio/AUDIO-171da194-1048-45f8-8561-ae000621f3ed-c4775f42af4a2e1e3e4d5cb875e261dd.mp3 differ diff --git a/public/uploads/exercise/audio/AUDIO-22393dd3-e223-4ca2-8b37-c514a2991576-c4775f42af4a2e1e3e4d5cb875e261dd.mp3 b/public/uploads/exercise/audio/AUDIO-22393dd3-e223-4ca2-8b37-c514a2991576-c4775f42af4a2e1e3e4d5cb875e261dd.mp3 new file mode 100644 index 0000000..9bf1b59 Binary files /dev/null and b/public/uploads/exercise/audio/AUDIO-22393dd3-e223-4ca2-8b37-c514a2991576-c4775f42af4a2e1e3e4d5cb875e261dd.mp3 differ diff --git a/public/uploads/exercise/audio/AUDIO-3d7d4354-0d8c-431a-ac18-839400bba470-c4775f42af4a2e1e3e4d5cb875e261dd.mp3 b/public/uploads/exercise/audio/AUDIO-3d7d4354-0d8c-431a-ac18-839400bba470-c4775f42af4a2e1e3e4d5cb875e261dd.mp3 new file mode 100644 index 0000000..9bf1b59 Binary files /dev/null and b/public/uploads/exercise/audio/AUDIO-3d7d4354-0d8c-431a-ac18-839400bba470-c4775f42af4a2e1e3e4d5cb875e261dd.mp3 differ diff --git a/public/uploads/exercise/audio/AUDIO-41a858f0-c750-4757-8ef1-5c02e0916c71-c4775f42af4a2e1e3e4d5cb875e261dd.mp3 b/public/uploads/exercise/audio/AUDIO-41a858f0-c750-4757-8ef1-5c02e0916c71-c4775f42af4a2e1e3e4d5cb875e261dd.mp3 new file mode 100644 index 0000000..9bf1b59 Binary files /dev/null and b/public/uploads/exercise/audio/AUDIO-41a858f0-c750-4757-8ef1-5c02e0916c71-c4775f42af4a2e1e3e4d5cb875e261dd.mp3 differ diff --git a/public/uploads/exercise/audio/AUDIO-69312f33-7d65-40c5-9600-8fc6d197a6ae-c4775f42af4a2e1e3e4d5cb875e261dd.mp3 b/public/uploads/exercise/audio/AUDIO-69312f33-7d65-40c5-9600-8fc6d197a6ae-c4775f42af4a2e1e3e4d5cb875e261dd.mp3 new file mode 100644 index 0000000..9bf1b59 Binary files /dev/null and b/public/uploads/exercise/audio/AUDIO-69312f33-7d65-40c5-9600-8fc6d197a6ae-c4775f42af4a2e1e3e4d5cb875e261dd.mp3 differ diff --git a/public/uploads/exercise/audio/AUDIO-6b56e2c2-069a-4c5d-8045-85a52f238acb-c4775f42af4a2e1e3e4d5cb875e261dd.mp3 b/public/uploads/exercise/audio/AUDIO-6b56e2c2-069a-4c5d-8045-85a52f238acb-c4775f42af4a2e1e3e4d5cb875e261dd.mp3 new file mode 100644 index 0000000..9bf1b59 Binary files /dev/null and b/public/uploads/exercise/audio/AUDIO-6b56e2c2-069a-4c5d-8045-85a52f238acb-c4775f42af4a2e1e3e4d5cb875e261dd.mp3 differ diff --git a/public/uploads/exercise/audio/AUDIO-88d4f716-ea30-414b-af95-fe04267b102c-c4775f42af4a2e1e3e4d5cb875e261dd.mp3 b/public/uploads/exercise/audio/AUDIO-88d4f716-ea30-414b-af95-fe04267b102c-c4775f42af4a2e1e3e4d5cb875e261dd.mp3 new file mode 100644 index 0000000..9bf1b59 Binary files /dev/null and b/public/uploads/exercise/audio/AUDIO-88d4f716-ea30-414b-af95-fe04267b102c-c4775f42af4a2e1e3e4d5cb875e261dd.mp3 differ diff --git a/public/uploads/exercise/audio/AUDIO-abaedaea-68f7-45a0-9aba-4e7fdda60ff6-c4775f42af4a2e1e3e4d5cb875e261dd.mp3 b/public/uploads/exercise/audio/AUDIO-abaedaea-68f7-45a0-9aba-4e7fdda60ff6-c4775f42af4a2e1e3e4d5cb875e261dd.mp3 new file mode 100644 index 0000000..9bf1b59 Binary files /dev/null and b/public/uploads/exercise/audio/AUDIO-abaedaea-68f7-45a0-9aba-4e7fdda60ff6-c4775f42af4a2e1e3e4d5cb875e261dd.mp3 differ diff --git a/public/uploads/exercise/audio/AUDIO-c122e34e-c1d6-4cd6-b067-4725a3ecf9bb-c4775f42af4a2e1e3e4d5cb875e261dd.mp3 b/public/uploads/exercise/audio/AUDIO-c122e34e-c1d6-4cd6-b067-4725a3ecf9bb-c4775f42af4a2e1e3e4d5cb875e261dd.mp3 new file mode 100644 index 0000000..9bf1b59 Binary files /dev/null and b/public/uploads/exercise/audio/AUDIO-c122e34e-c1d6-4cd6-b067-4725a3ecf9bb-c4775f42af4a2e1e3e4d5cb875e261dd.mp3 differ diff --git a/public/uploads/exercise/audio/AUDIO-da50d8f9-da1c-484e-8b9c-a330b5218918-c4775f42af4a2e1e3e4d5cb875e261dd.mp3 b/public/uploads/exercise/audio/AUDIO-da50d8f9-da1c-484e-8b9c-a330b5218918-c4775f42af4a2e1e3e4d5cb875e261dd.mp3 new file mode 100644 index 0000000..9bf1b59 Binary files /dev/null and b/public/uploads/exercise/audio/AUDIO-da50d8f9-da1c-484e-8b9c-a330b5218918-c4775f42af4a2e1e3e4d5cb875e261dd.mp3 differ diff --git a/public/uploads/exercise/audio/AUDIO-ee01f3c8-08f8-43f0-bd8f-17470bb89fc1-c4775f42af4a2e1e3e4d5cb875e261dd.mp3 b/public/uploads/exercise/audio/AUDIO-ee01f3c8-08f8-43f0-bd8f-17470bb89fc1-c4775f42af4a2e1e3e4d5cb875e261dd.mp3 new file mode 100644 index 0000000..9bf1b59 Binary files /dev/null and b/public/uploads/exercise/audio/AUDIO-ee01f3c8-08f8-43f0-bd8f-17470bb89fc1-c4775f42af4a2e1e3e4d5cb875e261dd.mp3 differ diff --git a/public/uploads/exercise/audio/AUDIO-f4d85d1e-7863-489c-905c-bf7ba6a0e0d6-c4775f42af4a2e1e3e4d5cb875e261dd.mp3 b/public/uploads/exercise/audio/AUDIO-f4d85d1e-7863-489c-905c-bf7ba6a0e0d6-c4775f42af4a2e1e3e4d5cb875e261dd.mp3 new file mode 100644 index 0000000..9bf1b59 Binary files /dev/null and b/public/uploads/exercise/audio/AUDIO-f4d85d1e-7863-489c-905c-bf7ba6a0e0d6-c4775f42af4a2e1e3e4d5cb875e261dd.mp3 differ diff --git a/public/uploads/exercise/audio/AUDIO-fc322b53-e285-452a-b365-4961d48b0d77-c4775f42af4a2e1e3e4d5cb875e261dd.mp3 b/public/uploads/exercise/audio/AUDIO-fc322b53-e285-452a-b365-4961d48b0d77-c4775f42af4a2e1e3e4d5cb875e261dd.mp3 new file mode 100644 index 0000000..9bf1b59 Binary files /dev/null and b/public/uploads/exercise/audio/AUDIO-fc322b53-e285-452a-b365-4961d48b0d77-c4775f42af4a2e1e3e4d5cb875e261dd.mp3 differ diff --git a/public/uploads/exercise/image/IMAGE-171da194-1048-45f8-8561-ae000621f3ed-df84fe4f264501985fe2845d0e43a19e.jpeg b/public/uploads/exercise/image/IMAGE-171da194-1048-45f8-8561-ae000621f3ed-df84fe4f264501985fe2845d0e43a19e.jpeg new file mode 100644 index 0000000..39859e4 Binary files /dev/null and b/public/uploads/exercise/image/IMAGE-171da194-1048-45f8-8561-ae000621f3ed-df84fe4f264501985fe2845d0e43a19e.jpeg differ diff --git a/public/uploads/exercise/image/IMAGE-41a858f0-c750-4757-8ef1-5c02e0916c71-df84fe4f264501985fe2845d0e43a19e.jpeg b/public/uploads/exercise/image/IMAGE-41a858f0-c750-4757-8ef1-5c02e0916c71-df84fe4f264501985fe2845d0e43a19e.jpeg new file mode 100644 index 0000000..39859e4 Binary files /dev/null and b/public/uploads/exercise/image/IMAGE-41a858f0-c750-4757-8ef1-5c02e0916c71-df84fe4f264501985fe2845d0e43a19e.jpeg differ diff --git a/public/uploads/exercise/image/IMAGE-69312f33-7d65-40c5-9600-8fc6d197a6ae-df84fe4f264501985fe2845d0e43a19e.jpeg b/public/uploads/exercise/image/IMAGE-69312f33-7d65-40c5-9600-8fc6d197a6ae-df84fe4f264501985fe2845d0e43a19e.jpeg new file mode 100644 index 0000000..39859e4 Binary files /dev/null and b/public/uploads/exercise/image/IMAGE-69312f33-7d65-40c5-9600-8fc6d197a6ae-df84fe4f264501985fe2845d0e43a19e.jpeg differ diff --git a/public/uploads/exercise/image/IMAGE-6b56e2c2-069a-4c5d-8045-85a52f238acb-df84fe4f264501985fe2845d0e43a19e.jpeg b/public/uploads/exercise/image/IMAGE-6b56e2c2-069a-4c5d-8045-85a52f238acb-df84fe4f264501985fe2845d0e43a19e.jpeg new file mode 100644 index 0000000..39859e4 Binary files /dev/null and b/public/uploads/exercise/image/IMAGE-6b56e2c2-069a-4c5d-8045-85a52f238acb-df84fe4f264501985fe2845d0e43a19e.jpeg differ diff --git a/public/uploads/exercise/image/IMAGE-75c008be-3942-4e15-9552-961b9ee86648-1cdda93a0a9afe71dfff785b7e431280.jpeg b/public/uploads/exercise/image/IMAGE-75c008be-3942-4e15-9552-961b9ee86648-1cdda93a0a9afe71dfff785b7e431280.jpeg new file mode 100644 index 0000000..39859e4 Binary files /dev/null and b/public/uploads/exercise/image/IMAGE-75c008be-3942-4e15-9552-961b9ee86648-1cdda93a0a9afe71dfff785b7e431280.jpeg differ diff --git a/public/uploads/exercise/image/IMAGE-88d4f716-ea30-414b-af95-fe04267b102c-df84fe4f264501985fe2845d0e43a19e.jpeg b/public/uploads/exercise/image/IMAGE-88d4f716-ea30-414b-af95-fe04267b102c-df84fe4f264501985fe2845d0e43a19e.jpeg new file mode 100644 index 0000000..39859e4 Binary files /dev/null and b/public/uploads/exercise/image/IMAGE-88d4f716-ea30-414b-af95-fe04267b102c-df84fe4f264501985fe2845d0e43a19e.jpeg differ diff --git a/public/uploads/exercise/image/IMAGE-abaedaea-68f7-45a0-9aba-4e7fdda60ff6-df84fe4f264501985fe2845d0e43a19e.jpeg b/public/uploads/exercise/image/IMAGE-abaedaea-68f7-45a0-9aba-4e7fdda60ff6-df84fe4f264501985fe2845d0e43a19e.jpeg new file mode 100644 index 0000000..39859e4 Binary files /dev/null and b/public/uploads/exercise/image/IMAGE-abaedaea-68f7-45a0-9aba-4e7fdda60ff6-df84fe4f264501985fe2845d0e43a19e.jpeg differ diff --git a/public/uploads/exercise/image/IMAGE-c41bc706-688b-43dd-b2a4-da1e44c251f4-5401d6ad0f82c84d96a681597c7f39c4.jpeg b/public/uploads/exercise/image/IMAGE-c41bc706-688b-43dd-b2a4-da1e44c251f4-5401d6ad0f82c84d96a681597c7f39c4.jpeg new file mode 100644 index 0000000..39859e4 Binary files /dev/null and b/public/uploads/exercise/image/IMAGE-c41bc706-688b-43dd-b2a4-da1e44c251f4-5401d6ad0f82c84d96a681597c7f39c4.jpeg differ diff --git a/public/uploads/exercise/image/IMAGE-da50d8f9-da1c-484e-8b9c-a330b5218918-df84fe4f264501985fe2845d0e43a19e.jpeg b/public/uploads/exercise/image/IMAGE-da50d8f9-da1c-484e-8b9c-a330b5218918-df84fe4f264501985fe2845d0e43a19e.jpeg new file mode 100644 index 0000000..39859e4 Binary files /dev/null and b/public/uploads/exercise/image/IMAGE-da50d8f9-da1c-484e-8b9c-a330b5218918-df84fe4f264501985fe2845d0e43a19e.jpeg differ diff --git a/public/uploads/exercise/image/IMAGE-f4d85d1e-7863-489c-905c-bf7ba6a0e0d6-df84fe4f264501985fe2845d0e43a19e.jpeg b/public/uploads/exercise/image/IMAGE-f4d85d1e-7863-489c-905c-bf7ba6a0e0d6-df84fe4f264501985fe2845d0e43a19e.jpeg new file mode 100644 index 0000000..39859e4 Binary files /dev/null and b/public/uploads/exercise/image/IMAGE-f4d85d1e-7863-489c-905c-bf7ba6a0e0d6-df84fe4f264501985fe2845d0e43a19e.jpeg differ diff --git a/public/uploads/exercise/image/IMAGE-fc322b53-e285-452a-b365-4961d48b0d77-df84fe4f264501985fe2845d0e43a19e.jpeg b/public/uploads/exercise/image/IMAGE-fc322b53-e285-452a-b365-4961d48b0d77-df84fe4f264501985fe2845d0e43a19e.jpeg new file mode 100644 index 0000000..39859e4 Binary files /dev/null and b/public/uploads/exercise/image/IMAGE-fc322b53-e285-452a-b365-4961d48b0d77-df84fe4f264501985fe2845d0e43a19e.jpeg differ diff --git a/public/uploads/level/image/IMAGE-157216c2-0b33-440c-8782-c23e4dfc4af3-9b7abd31eb795a753c654509ff2357ea.jpeg b/public/uploads/level/image/IMAGE-157216c2-0b33-440c-8782-c23e4dfc4af3-9b7abd31eb795a753c654509ff2357ea.jpeg new file mode 100644 index 0000000..39859e4 Binary files /dev/null and b/public/uploads/level/image/IMAGE-157216c2-0b33-440c-8782-c23e4dfc4af3-9b7abd31eb795a753c654509ff2357ea.jpeg differ diff --git a/public/uploads/level/image/IMAGE-e31ebd27-2faf-4f2e-9caa-a92dd05a620f-2ab9e444352fdad5f4a9d6f4421ead17.jpeg b/public/uploads/level/image/IMAGE-e31ebd27-2faf-4f2e-9caa-a92dd05a620f-2ab9e444352fdad5f4a9d6f4421ead17.jpeg new file mode 100644 index 0000000..39859e4 Binary files /dev/null and b/public/uploads/level/image/IMAGE-e31ebd27-2faf-4f2e-9caa-a92dd05a620f-2ab9e444352fdad5f4a9d6f4421ead17.jpeg differ diff --git a/public/uploads/level/image/IMAGE-f4602333-c35c-4847-9ae0-b41972783fae-d9c46fae1e94080ee5f5d7aff6ed00ee.jpeg b/public/uploads/level/image/IMAGE-f4602333-c35c-4847-9ae0-b41972783fae-d9c46fae1e94080ee5f5d7aff6ed00ee.jpeg new file mode 100644 index 0000000..39859e4 Binary files /dev/null and b/public/uploads/level/image/IMAGE-f4602333-c35c-4847-9ae0-b41972783fae-d9c46fae1e94080ee5f5d7aff6ed00ee.jpeg differ diff --git a/public/uploads/section/THUMBNAIL-a4cbb7d2-57e6-4829-a74d-3256afee965a-3b4db19865d8b70386db0834852659ac.jpeg b/public/uploads/section/THUMBNAIL-a4cbb7d2-57e6-4829-a74d-3256afee965a-3b4db19865d8b70386db0834852659ac.jpeg new file mode 100644 index 0000000..39859e4 Binary files /dev/null and b/public/uploads/section/THUMBNAIL-a4cbb7d2-57e6-4829-a74d-3256afee965a-3b4db19865d8b70386db0834852659ac.jpeg differ diff --git a/routes/contents/exercise.js b/routes/contents/exercise.js index 462f3dd..f7ae643 100644 --- a/routes/contents/exercise.js +++ b/routes/contents/exercise.js @@ -1,33 +1,38 @@ import express from "express"; -import { getExercises, getExercisesForAdmin, getExerciseById, getExerciseByLevelId, deleteExerciseById, deleteExerciseFileById } from "../../controllers/contentControllers/exercise.js"; +import { getExercises, getExerciseById, getExercisesForAdmin, getExerciseByLevelId, 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"; import { verifyLoginUser, adminOnly } from "../../middlewares/User/authUser.js"; import handleUpload from '../../middlewares/uploadExercise.js'; +import handleUploadSingleExercise from '../../middlewares/uploadExerciseSingle.js'; const router = express.Router(); router.get("/exercise", verifyLoginUser, getExercises); -router.get("/exercise/admin", verifyLoginUser, adminOnly, getExercisesForAdmin); - router.get("/exercise/level/:idLevel", verifyLoginUser, getExerciseByLevelId); +router.get("/exercise/admin", verifyLoginUser, adminOnly, getExercisesForAdmin); + router.get("/exercise/:id", verifyLoginUser, getExerciseById); -router.post("/exercise/multiple-choices", verifyLoginUser, adminOnly, handleUpload, createMultipleChoicesExercise); +router.post("/exercises", verifyLoginUser, adminOnly, handleUpload, createExercises); -router.post("/exercise/matching-pairs", verifyLoginUser, adminOnly, handleUpload, createMatchingPairsExercise); +router.post("/exercise/multiple-choices", verifyLoginUser, adminOnly, handleUploadSingleExercise, createMultipleChoicesExercise); -router.post("/exercise/true-false", verifyLoginUser, adminOnly, handleUpload, createTrueFalseExercise); +router.post("/exercise/matching-pairs", verifyLoginUser, adminOnly, handleUploadSingleExercise, createMatchingPairsExercise); -router.put("/exercise/multiple-choices/:id", verifyLoginUser, adminOnly, handleUpload, updateMultipleChoicesExerciseById); +router.post("/exercise/true-false", verifyLoginUser, adminOnly, handleUploadSingleExercise, createTrueFalseExercise); -router.put("/exercise/matching-pairs/:id", verifyLoginUser, adminOnly, handleUpload, updateMatchingPairsExerciseById); +router.put("/exercise/:id", verifyLoginUser, adminOnly, handleUpload, updateExerciseById); -router.put("/exercise/true-false/:id", verifyLoginUser, adminOnly, handleUpload, updateTrueFalseExerciseById); +router.put("/exercise/multiple-choices/:id", verifyLoginUser, adminOnly, handleUploadSingleExercise, updateMultipleChoicesExerciseById); + +router.put("/exercise/matching-pairs/:id", verifyLoginUser, adminOnly, handleUploadSingleExercise, updateMatchingPairsExerciseById); + +router.put("/exercise/true-false/:id", verifyLoginUser, adminOnly, handleUploadSingleExercise, updateTrueFalseExerciseById); router.delete("/exercise/:id", verifyLoginUser, adminOnly, deleteExerciseById); diff --git a/routes/contents/level.js b/routes/contents/level.js index ee50d16..d7ca706 100644 --- a/routes/contents/level.js +++ b/routes/contents/level.js @@ -1,5 +1,5 @@ import express from "express"; -import { getLevels, getLevelById, getLevelsByTopicId, createLevel, updateLevelById, deleteLevelById, getPreviousLevel } from "../../controllers/contentControllers/level.js"; +import { getLevels, getLevelById, getLevelForAdmin, getLevelsByTopicId, createLevel, updateLevelById, deleteLevelById, deleteLevelFileById, getPreviousLevel } from "../../controllers/contentControllers/level.js"; import { verifyLoginUser, adminOnly } from "../../middlewares/User/authUser.js"; import handleUpload from '../../middlewares/Level/uploadLevel.js'; import {checkLevelsPerTopic, autoCalculateRoutes, getSectionAndTopicByLevelId } from '../../middlewares/Level/checkLevel.js'; @@ -11,6 +11,8 @@ router.get("/level", verifyLoginUser, getLevels); router.get("/level/topic/:idTopic", verifyLoginUser, getLevelsByTopicId); +router.get("/level/admin", verifyLoginUser, adminOnly, getLevelForAdmin); + router.get("/level/:id", verifyLoginUser, getLevelById); router.get("/previous/level/:next_learning", verifyLoginUser, getPreviousLevel); @@ -21,4 +23,6 @@ router.put("/level/:id", verifyLoginUser, adminOnly, handleUpload, getSectionAnd router.delete("/level/:id", verifyLoginUser, adminOnly, getSectionAndTopicByLevelId, deleteLevelById); +router.delete("/level/file/:id", verifyLoginUser, adminOnly, deleteLevelFileById); + export default router \ No newline at end of file diff --git a/routes/contents/section.js b/routes/contents/section.js index 60ae065..5331d16 100644 --- a/routes/contents/section.js +++ b/routes/contents/section.js @@ -1,13 +1,15 @@ import express from "express"; -import handleUpload from '../../middlewares/uploadSection.js'; -import { getSections, getSectionById, createSection, updateSectionById, deleteSectionById } from "../../controllers/contentControllers/section.js"; +import { getSections, getSectionById, getSectionForAdmin, createSection, updateSectionById, deleteSectionById } from "../../controllers/contentControllers/section.js"; import { verifyLoginUser, adminOnly } from "../../middlewares/User/authUser.js"; +import handleUpload from '../../middlewares/uploadSection.js'; const router = express.Router(); router.get("/section", verifyLoginUser, getSections); +router.get("/section/admin", verifyLoginUser, getSectionForAdmin); + router.get("/section/:id", verifyLoginUser, getSectionById); router.post("/section", verifyLoginUser, adminOnly, handleUpload, createSection); diff --git a/routes/contents/topic.js b/routes/contents/topic.js index 435095f..a2707e5 100644 --- a/routes/contents/topic.js +++ b/routes/contents/topic.js @@ -1,5 +1,5 @@ import express from "express"; -import { getTopics, getTopicById, getTopicBySectionId, createTopic, updateTopicById, deleteTopicById, getCompletedTopicsBySection } from "../../controllers/contentControllers/topic.js"; +import { getTopics, getTopicById, getTopicBySectionId, getTopicForAdmin, createTopic, updateTopicById, deleteTopicById, getCompletedTopicsBySection } from "../../controllers/contentControllers/topic.js"; import { verifyLoginUser, adminOnly } from "../../middlewares/User/authUser.js"; @@ -9,6 +9,8 @@ router.get("/topic", verifyLoginUser, getTopics); router.get("/topic/complete", verifyLoginUser, getCompletedTopicsBySection); +router.get("/topic/admin", verifyLoginUser, getTopicForAdmin); + router.get("/topic/section/:sectionId", verifyLoginUser, getTopicBySectionId); router.get("/topic/:id", verifyLoginUser, getTopicById); diff --git a/routes/learning/stdExercise.js b/routes/learning/stdExercise.js index af12be1..0036e8d 100644 --- a/routes/learning/stdExercise.js +++ b/routes/learning/stdExercise.js @@ -10,8 +10,8 @@ router.get("/stdExercise", verifyLoginUser, getStdExercises); router.get("/stdExercise/:id", verifyLoginUser, getStdExerciseById); +router.get("/studentAnswers/:id", verifyLoginUser, getStudentAnswersByStdLearningId); + 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 diff --git a/routes/learning/stdLearning.js b/routes/learning/stdLearning.js index a907a18..f4bcd75 100644 --- a/routes/learning/stdLearning.js +++ b/routes/learning/stdLearning.js @@ -1,5 +1,5 @@ import express from "express"; -import { getStdLearnings, getStdLearningById, createStdLearning, learningScoreByStdLearningId, learningHistory, learningHistoryBySectionId, learningHistoryByTopicId, getLastCreatedStdLearningByLevelId } from "../../controllers/learningControllers/stdLearning.js"; +import { getStdLearnings, getStdLearningById, createStdLearning, updateStdLearningById, learningScoreByStdLearningId, learningHistory, learningHistoryBySectionId, learningHistoryByTopicId, getLastCreatedStdLearningByLevelId } from "../../controllers/learningControllers/stdLearning.js"; import { checkStdLearning } from "../../middlewares/checkStdLearning.js"; import { verifyLoginUser } from "../../middlewares/User/authUser.js"; @@ -21,4 +21,6 @@ router.get("/stdLearning/level/:levelId", verifyLoginUser, getLastCreatedStdLear router.post("/stdLearning", verifyLoginUser, checkStdLearning, createStdLearning); +router.put("/stdLearning/:id", verifyLoginUser, updateStdLearningById); + export default router \ No newline at end of file