refactor: section, topic, level, and exercise model
|
|
@ -293,7 +293,6 @@ export const loginUser = async (req, res) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const refreshToken = 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;
|
const refreshToken = req.cookies?.refreshToken || req.body?.REFRESH_TOKEN;
|
||||||
|
|
||||||
if (!refreshToken) {
|
if (!refreshToken) {
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,15 @@ import response from "../../response.js";
|
||||||
import models from "../../models/index.js";
|
import models from "../../models/index.js";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
import {
|
||||||
|
clearFileBuffers,
|
||||||
|
saveFileToDisk,
|
||||||
|
} from "../../middlewares/uploadExercise.js";
|
||||||
|
|
||||||
export const getExercises = async (req, res) => {
|
export const getExercises = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const exercises = await models.Exercise.findAll({
|
const exercises = await models.Exercise.findAll({
|
||||||
|
where: { IS_DELETED: 0 },
|
||||||
include: [
|
include: [
|
||||||
{
|
{
|
||||||
model: models.MultipleChoices,
|
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) => {
|
export const getExerciseById = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
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: [
|
include: [
|
||||||
{
|
{
|
||||||
model: models.MultipleChoices,
|
model: models.MultipleChoices,
|
||||||
|
|
@ -143,29 +92,24 @@ export const getExerciseById = async (req, res) => {
|
||||||
if (questionType === "MCQ") {
|
if (questionType === "MCQ") {
|
||||||
if (exerciseData.multipleChoices) {
|
if (exerciseData.multipleChoices) {
|
||||||
exerciseData.multipleChoices = exerciseData.multipleChoices.map(
|
exerciseData.multipleChoices = exerciseData.multipleChoices.map(
|
||||||
(choice) => {
|
(choice) => choice.dataValues
|
||||||
const { ANSWER_KEY, ...rest } = choice.dataValues;
|
|
||||||
return rest;
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
delete exerciseData.matchingPairs;
|
delete exerciseData.matchingPairs;
|
||||||
delete exerciseData.trueFalse;
|
delete exerciseData.trueFalse;
|
||||||
} else if (questionType === "MPQ") {
|
} else if (questionType === "MPQ") {
|
||||||
if (exerciseData.matchingPairs) {
|
if (exerciseData.matchingPairs) {
|
||||||
exerciseData.matchingPairs = exerciseData.matchingPairs.map((pair) => {
|
exerciseData.matchingPairs = exerciseData.matchingPairs.map(
|
||||||
const { LEFT_PAIR, RIGHT_PAIR, ...rest } = pair.dataValues;
|
(pair) => pair.dataValues
|
||||||
return rest;
|
);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
delete exerciseData.multipleChoices;
|
delete exerciseData.multipleChoices;
|
||||||
delete exerciseData.trueFalse;
|
delete exerciseData.trueFalse;
|
||||||
} else if (questionType === "TFQ") {
|
} else if (questionType === "TFQ") {
|
||||||
if (exerciseData.trueFalse) {
|
if (exerciseData.trueFalse) {
|
||||||
exerciseData.trueFalse = exerciseData.trueFalse.map((tf) => {
|
exerciseData.trueFalse = exerciseData.trueFalse.map(
|
||||||
const { IS_TRUE, ...rest } = tf.dataValues;
|
(tf) => tf.dataValues
|
||||||
return rest;
|
);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
delete exerciseData.multipleChoices;
|
delete exerciseData.multipleChoices;
|
||||||
delete exerciseData.matchingPairs;
|
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) => {
|
export const getExerciseByLevelId = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { idLevel } = req.params;
|
const { idLevel } = req.params;
|
||||||
|
|
@ -202,7 +318,7 @@ export const getExerciseByLevelId = async (req, res) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const exercises = await models.Exercise.findAll({
|
const exercises = await models.Exercise.findAll({
|
||||||
where: { ID_LEVEL: idLevel },
|
where: { ID_LEVEL: idLevel, IS_DELETED: 0 },
|
||||||
include: [
|
include: [
|
||||||
{
|
{
|
||||||
model: models.MultipleChoices,
|
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) => {
|
export const deleteExerciseById = async (req, res) => {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const transaction = await models.db.transaction();
|
const transaction = await models.db.transaction();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const exercise = await models.Exercise.findByPk(id, {
|
const exercise = await models.Exercise.findByPk(id);
|
||||||
include: [
|
|
||||||
{
|
|
||||||
model: models.MultipleChoices,
|
|
||||||
as: "multipleChoices",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
model: models.MatchingPairs,
|
|
||||||
as: "matchingPairs",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
model: models.TrueFalse,
|
|
||||||
as: "trueFalse",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!exercise) {
|
if (!exercise) {
|
||||||
await transaction.rollback();
|
await transaction.rollback();
|
||||||
return response(404, null, "Exercise not found", res);
|
return response(404, null, "Exercise not found", res);
|
||||||
}
|
}
|
||||||
|
|
||||||
await models.StdExercise.destroy({
|
await exercise.update({ IS_DELETED: 1 }, { transaction });
|
||||||
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 transaction.commit();
|
await transaction.commit();
|
||||||
|
response(200, null, "Exercise soft-deleted successfully", res);
|
||||||
response(200, null, "Exercise and related data deleted successfully", res);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
await transaction.rollback();
|
await transaction.rollback();
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,9 @@ import {
|
||||||
export const getLevels = async (req, res) => {
|
export const getLevels = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const levels = await models.Level.findAll({
|
const levels = await models.Level.findAll({
|
||||||
|
where: {
|
||||||
|
IS_DELETED: 0,
|
||||||
|
},
|
||||||
attributes: {
|
attributes: {
|
||||||
exclude: [
|
exclude: [
|
||||||
"ROUTE_1",
|
"ROUTE_1",
|
||||||
|
|
@ -38,6 +41,9 @@ export const getLevelById = async (req, res) => {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
|
|
||||||
const level = await models.Level.findByPk(id, {
|
const level = await models.Level.findByPk(id, {
|
||||||
|
where: {
|
||||||
|
IS_DELETED: 0,
|
||||||
|
},
|
||||||
attributes: {
|
attributes: {
|
||||||
exclude: [
|
exclude: [
|
||||||
"ROUTE_1",
|
"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) => {
|
export const getLevelsByTopicId = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { idTopic } = req.params;
|
const { idTopic } = req.params;
|
||||||
|
|
@ -106,6 +223,7 @@ export const getLevelsByTopicId = async (req, res) => {
|
||||||
const levels = await models.Level.findAll({
|
const levels = await models.Level.findAll({
|
||||||
where: {
|
where: {
|
||||||
ID_TOPIC: idTopic,
|
ID_TOPIC: idTopic,
|
||||||
|
IS_DELETED: 0,
|
||||||
},
|
},
|
||||||
attributes: {
|
attributes: {
|
||||||
exclude: [
|
exclude: [
|
||||||
|
|
@ -126,6 +244,7 @@ export const getLevelsByTopicId = async (req, res) => {
|
||||||
"ID_STUDENT_LEARNING",
|
"ID_STUDENT_LEARNING",
|
||||||
"STUDENT_START",
|
"STUDENT_START",
|
||||||
"STUDENT_FINISH",
|
"STUDENT_FINISH",
|
||||||
|
"NEXT_LEARNING",
|
||||||
],
|
],
|
||||||
where: {
|
where: {
|
||||||
ID: ID,
|
ID: ID,
|
||||||
|
|
@ -156,9 +275,7 @@ export const getLevelsByTopicId = async (req, res) => {
|
||||||
const lastCompletedLearning = await models.StdLearning.findOne({
|
const lastCompletedLearning = await models.StdLearning.findOne({
|
||||||
where: {
|
where: {
|
||||||
ID: ID,
|
ID: ID,
|
||||||
STUDENT_FINISH: {
|
STUDENT_FINISH: { [models.Op.not]: null },
|
||||||
[models.Op.not]: null,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
include: [
|
include: [
|
||||||
{
|
{
|
||||||
|
|
@ -170,6 +287,31 @@ export const getLevelsByTopicId = async (req, res) => {
|
||||||
order: [["STUDENT_FINISH", "DESC"]],
|
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 levelsWithScore = levels.map((level) => {
|
||||||
const SCORE =
|
const SCORE =
|
||||||
level.stdLearning && level.stdLearning.length > 0
|
level.stdLearning && level.stdLearning.length > 0
|
||||||
|
|
@ -181,7 +323,6 @@ export const getLevelsByTopicId = async (req, res) => {
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const levelJSON = level.toJSON();
|
const levelJSON = level.toJSON();
|
||||||
|
|
||||||
const NAME_SECTION = levelJSON.levelTopic.topicSection.NAME_SECTION;
|
const NAME_SECTION = levelJSON.levelTopic.topicSection.NAME_SECTION;
|
||||||
const NAME_TOPIC = levelJSON.levelTopic.NAME_TOPIC;
|
const NAME_TOPIC = levelJSON.levelTopic.NAME_TOPIC;
|
||||||
|
|
||||||
|
|
@ -216,9 +357,15 @@ export const getLevelsByTopicId = async (req, res) => {
|
||||||
NAME_LEVEL: lastCompletedLearning.level.NAME_LEVEL,
|
NAME_LEVEL: lastCompletedLearning.level.NAME_LEVEL,
|
||||||
SCORE: lastCompletedLearning.SCORE,
|
SCORE: lastCompletedLearning.SCORE,
|
||||||
NEXT_LEARNING: lastCompletedLearning.NEXT_LEARNING,
|
NEXT_LEARNING: lastCompletedLearning.NEXT_LEARNING,
|
||||||
FINISHED_AT: lastCompletedLearning.STUDENT_FINISH,
|
NEXT_LEARNING_LEVEL: nextLearningLevel
|
||||||
}
|
? nextLearningLevel.NAME_LEVEL
|
||||||
: null,
|
: null,
|
||||||
|
FINISHED_AT: lastCompletedLearning.STUDENT_FINISH,
|
||||||
|
UNLOCKED_LEVELS: unlockedLevels.map((lvl) => lvl.NAME_LEVEL),
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
UNLOCKED_LEVELS: ["Pretest"],
|
||||||
|
},
|
||||||
levels: sortedLevels,
|
levels: sortedLevels,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -247,6 +394,7 @@ export const createLevel = async (req, res, next) => {
|
||||||
clearFileBuffers({ AUDIO, IMAGE });
|
clearFileBuffers({ AUDIO, IMAGE });
|
||||||
return response(400, null, "Topic is required", res);
|
return response(400, null, "Topic is required", res);
|
||||||
}
|
}
|
||||||
|
|
||||||
const transaction = await models.db.transaction();
|
const transaction = await models.db.transaction();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -267,7 +415,11 @@ export const createLevel = async (req, res, next) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingLevel = await models.Level.findOne({
|
const existingLevel = await models.Level.findOne({
|
||||||
where: { NAME_LEVEL, ID_TOPIC },
|
where: {
|
||||||
|
NAME_LEVEL,
|
||||||
|
ID_TOPIC,
|
||||||
|
IS_DELETED: 0,
|
||||||
|
},
|
||||||
transaction,
|
transaction,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -367,6 +519,7 @@ export const updateLevelById = async (req, res, next) => {
|
||||||
NAME_LEVEL,
|
NAME_LEVEL,
|
||||||
ID_TOPIC,
|
ID_TOPIC,
|
||||||
ID_LEVEL: { [models.Sequelize.Op.ne]: id },
|
ID_LEVEL: { [models.Sequelize.Op.ne]: id },
|
||||||
|
IS_DELETED: 0,
|
||||||
},
|
},
|
||||||
transaction,
|
transaction,
|
||||||
});
|
});
|
||||||
|
|
@ -385,7 +538,7 @@ export const updateLevelById = async (req, res, next) => {
|
||||||
|
|
||||||
if (NAME_LEVEL) {
|
if (NAME_LEVEL) {
|
||||||
level.NAME_LEVEL = 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_SECTION) level.ID_SECTION = ID_SECTION;
|
||||||
if (ID_TOPIC) level.ID_TOPIC = ID_TOPIC;
|
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);
|
return response(404, null, "Level not found", res);
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteFile = (filePath) => {
|
level.IS_DELETED = 1;
|
||||||
if (fs.existsSync(filePath)) {
|
await level.save();
|
||||||
fs.unlinkSync(filePath);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (level.AUDIO) {
|
await models.Exercise.update(
|
||||||
const audioPath = path.join("public/uploads/level/audio", level.AUDIO);
|
{ IS_DELETED: 1 },
|
||||||
deleteFile(audioPath);
|
{ where: { ID_LEVEL: id } }
|
||||||
}
|
);
|
||||||
|
|
||||||
if (level.IMAGE) {
|
|
||||||
const imagePath = path.join("public/uploads/level/image", level.IMAGE);
|
|
||||||
deleteFile(imagePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
req.body.newLevelId = level.ID_LEVEL;
|
req.body.newLevelId = level.ID_LEVEL;
|
||||||
|
|
||||||
await level.destroy();
|
|
||||||
|
|
||||||
await updateOtherLevelsRoutesOnDelete(req, res, next);
|
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) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
return response(500, null, "Internal Server Error", res);
|
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) => {
|
export const getPreviousLevel = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { next_learning } = req.params;
|
const { next_learning } = req.params;
|
||||||
const { ID } = req.user;
|
const { ID } = req.user;
|
||||||
|
|
||||||
const currentLevel = await models.Level.findByPk(next_learning, {
|
const currentLevel = await models.Level.findByPk(next_learning, {
|
||||||
|
where: {
|
||||||
|
IS_DELETED: 0,
|
||||||
|
},
|
||||||
include: [
|
include: [
|
||||||
{
|
{
|
||||||
model: models.StdLearning,
|
model: models.StdLearning,
|
||||||
|
|
@ -530,6 +740,7 @@ export const getPreviousLevel = async (req, res) => {
|
||||||
const previousLevels = await models.Level.findAll({
|
const previousLevels = await models.Level.findAll({
|
||||||
where: {
|
where: {
|
||||||
ID_TOPIC: ID_TOPIC,
|
ID_TOPIC: ID_TOPIC,
|
||||||
|
IS_DELETED: 0,
|
||||||
NAME_LEVEL: {
|
NAME_LEVEL: {
|
||||||
[models.Op.or]: [
|
[models.Op.or]: [
|
||||||
{ [models.Op.like]: "Pretest" },
|
{ [models.Op.like]: "Pretest" },
|
||||||
|
|
@ -545,7 +756,6 @@ export const getPreviousLevel = async (req, res) => {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
order: [["NAME_LEVEL", "ASC"]],
|
|
||||||
attributes: {
|
attributes: {
|
||||||
exclude: [
|
exclude: [
|
||||||
"ROUTE_1",
|
"ROUTE_1",
|
||||||
|
|
@ -610,13 +820,23 @@ export const getPreviousLevel = async (req, res) => {
|
||||||
|
|
||||||
delete currentLevelWithScore.stdLearning;
|
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" });
|
return res.status(404).json({ message: "No levels found" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
currentLevel: currentLevelWithScore,
|
currentLevel: currentLevelWithScore,
|
||||||
previousLevels: previousLevelsWithScore,
|
previousLevels: sortedLevels,
|
||||||
};
|
};
|
||||||
|
|
||||||
res.status(200).json({ message: "Success", data: result });
|
res.status(200).json({ message: "Success", data: result });
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,11 @@ import {
|
||||||
|
|
||||||
export const getSections = async (req, res) => {
|
export const getSections = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const sections = await models.Section.findAll();
|
const sections = await models.Section.findAll({
|
||||||
|
where: {
|
||||||
|
IS_DELETED: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
response(200, sections, "Success", res);
|
response(200, sections, "Success", res);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(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) => {
|
export const createSection = async (req, res) => {
|
||||||
const { NAME_SECTION, DESCRIPTION_SECTION } = req.body;
|
const { NAME_SECTION, DESCRIPTION_SECTION } = req.body;
|
||||||
const { THUMBNAIL } = req.filesToSave || {};
|
const { THUMBNAIL } = req.filesToSave || {};
|
||||||
|
|
@ -51,7 +123,7 @@ export const createSection = async (req, res) => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const existingSection = await models.Section.findOne({
|
const existingSection = await models.Section.findOne({
|
||||||
where: { NAME_SECTION },
|
where: { NAME_SECTION, IS_DELETED: 0 },
|
||||||
transaction,
|
transaction,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -167,19 +239,43 @@ export const deleteSectionById = async (req, res) => {
|
||||||
return response(404, null, "Section not found", res);
|
return response(404, null, "Section not found", res);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (section.THUMBNAIL) {
|
section.IS_DELETED = 1;
|
||||||
const thumbnailPath = path.join(
|
await section.save();
|
||||||
"public/uploads/section",
|
|
||||||
section.THUMBNAIL
|
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}')`
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
);
|
);
|
||||||
if (fs.existsSync(thumbnailPath)) {
|
|
||||||
fs.unlinkSync(thumbnailPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
response(500, null, "Internal Server Error", res);
|
response(500, null, "Internal Server Error", res);
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,11 @@ import models from "../../models/index.js";
|
||||||
|
|
||||||
export const getTopics = async (req, res) => {
|
export const getTopics = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const topics = await models.Topic.findAll();
|
const topics = await models.Topic.findAll({
|
||||||
|
where: {
|
||||||
|
IS_DELETED: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
response(200, topics, "Success", res);
|
response(200, topics, "Success", res);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
|
|
@ -30,6 +34,7 @@ export const getTopicById = async (req, res) => {
|
||||||
export const getTopicBySectionId = async (req, res) => {
|
export const getTopicBySectionId = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { sectionId } = req.params;
|
const { sectionId } = req.params;
|
||||||
|
const userId = req.user.ID;
|
||||||
|
|
||||||
const sectionExists = await models.Section.findByPk(sectionId);
|
const sectionExists = await models.Section.findByPk(sectionId);
|
||||||
if (!sectionExists) {
|
if (!sectionExists) {
|
||||||
|
|
@ -37,17 +42,124 @@ export const getTopicBySectionId = async (req, res) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const topics = await models.Topic.findAll({
|
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) {
|
if (!topics || topics.length === 0) {
|
||||||
return response(404, null, "No topics found for this section", res);
|
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) {
|
} catch (error) {
|
||||||
console.log(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);
|
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) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
response(500, null, "Internal Server Error", res);
|
response(500, null, "Internal Server Error", res);
|
||||||
|
|
@ -177,9 +310,9 @@ export const getCompletedTopicsBySection = async (req, res) => {
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
// if (!completedLevels.length) {
|
if (!completedLevels.length) {
|
||||||
// return response(404, null, "No completed topics found", res);
|
return response(200, null, "No completed topics found", res);
|
||||||
// }
|
}
|
||||||
|
|
||||||
const completedSections = {};
|
const completedSections = {};
|
||||||
|
|
||||||
|
|
@ -217,9 +350,9 @@ export const getCompletedTopicsBySection = async (req, res) => {
|
||||||
|
|
||||||
const result = Object.values(completedSections);
|
const result = Object.values(completedSections);
|
||||||
|
|
||||||
// if (!result.length) {
|
if (!result.length) {
|
||||||
// return response(404, null, "No section with completed topics found", res);
|
return response(200, null, "No section with completed topics found", res);
|
||||||
// }
|
}
|
||||||
|
|
||||||
response(
|
response(
|
||||||
200,
|
200,
|
||||||
|
|
|
||||||
|
|
@ -38,11 +38,20 @@ export const createMatchingPairsExercise = async (req, res) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
let generatedTitle = TITLE;
|
let generatedTitle = TITLE;
|
||||||
|
|
||||||
if (!TITLE) {
|
if (!TITLE) {
|
||||||
const exerciseCount = await models.Exercise.count({
|
const lastExercise = await models.Exercise.findOne({
|
||||||
where: { ID_LEVEL },
|
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({
|
const existingExercise = await models.Exercise.findOne({
|
||||||
|
|
|
||||||
|
|
@ -45,11 +45,20 @@ export const createMultipleChoicesExercise = async (req, res) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
let generatedTitle = TITLE;
|
let generatedTitle = TITLE;
|
||||||
|
|
||||||
if (!TITLE) {
|
if (!TITLE) {
|
||||||
const exerciseCount = await models.Exercise.count({
|
const lastExercise = await models.Exercise.findOne({
|
||||||
where: { ID_LEVEL },
|
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({
|
const existingExercise = await models.Exercise.findOne({
|
||||||
|
|
|
||||||
|
|
@ -28,11 +28,20 @@ export const createTrueFalseExercise = async (req, res) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
let generatedTitle = TITLE;
|
let generatedTitle = TITLE;
|
||||||
|
|
||||||
if (!TITLE) {
|
if (!TITLE) {
|
||||||
const exerciseCount = await models.Exercise.count({
|
const lastExercise = await models.Exercise.findOne({
|
||||||
where: { ID_LEVEL },
|
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({
|
const existingExercise = await models.Exercise.findOne({
|
||||||
|
|
|
||||||
|
|
@ -86,10 +86,14 @@ export const updateStdLearningById = async (req, res) => {
|
||||||
return response(404, null, "Student Learning record not found", 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.STUDENT_FINISH = new Date();
|
||||||
stdLearning.SCORE = req.body.SCORE;
|
stdLearning.SCORE = req.body.SCORE || stdLearning.SCORE;
|
||||||
stdLearning.NEXT_LEARNING = req.body.NEXT_LEARNING;
|
stdLearning.NEXT_LEARNING = req.body.NEXT_LEARNING || stdLearning.NEXT_LEARNING;
|
||||||
stdLearning.IS_PASS = req.body.IS_PASS;
|
stdLearning.IS_PASS = req.body.IS_PASS || stdLearning.IS_PASS;
|
||||||
|
|
||||||
if (FEEDBACK_STUDENT) {
|
if (FEEDBACK_STUDENT) {
|
||||||
stdLearning.FEEDBACK_STUDENT = FEEDBACK_STUDENT;
|
stdLearning.FEEDBACK_STUDENT = FEEDBACK_STUDENT;
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@ export const getTeachers = async (req, res) => {
|
||||||
ID: teacher.ID,
|
ID: teacher.ID,
|
||||||
NAME_USERS: teacher.NAME_USERS,
|
NAME_USERS: teacher.NAME_USERS,
|
||||||
EMAIL: teacher.EMAIL,
|
EMAIL: teacher.EMAIL,
|
||||||
NISN: teacher.teachers.NIP,
|
NIP: teacher.teachers.NIP,
|
||||||
ROLE: teacher.ROLE,
|
ROLE: teacher.ROLE,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
|
||||||
2
index.js
|
|
@ -9,7 +9,7 @@ dotenv.config();
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
const corsOptions = {
|
const corsOptions = {
|
||||||
origin: "*",
|
origin: "http://localhost:5173",
|
||||||
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
||||||
allowedHeaders: ["Content-Type", "Authorization"],
|
allowedHeaders: ["Content-Type", "Authorization"],
|
||||||
credentials: true,
|
credentials: true,
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,10 @@ export const checkLevelsPerTopic = async (req, res, next) => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const levelCount = await models.Level.count({
|
const levelCount = await models.Level.count({
|
||||||
where: { ID_TOPIC },
|
where: {
|
||||||
|
ID_TOPIC,
|
||||||
|
IS_DELETED: 0,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (levelCount >= 7) {
|
if (levelCount >= 7) {
|
||||||
|
|
@ -76,6 +79,7 @@ export const autoCalculateRoutes = async (req, res, next) => {
|
||||||
NAME_LEVEL: {
|
NAME_LEVEL: {
|
||||||
[models.Sequelize.Op.in]: levelTitles,
|
[models.Sequelize.Op.in]: levelTitles,
|
||||||
},
|
},
|
||||||
|
IS_DELETED: 0,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -185,6 +189,7 @@ export const updateOtherLevelsRoutes = async (req, res, next) => {
|
||||||
NAME_LEVEL: {
|
NAME_LEVEL: {
|
||||||
[models.Sequelize.Op.in]: levelTitles,
|
[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({
|
const levelsToUpdate = await models.Level.findAll({
|
||||||
where: {
|
where: {
|
||||||
|
IS_DELETED: 0,
|
||||||
[models.Sequelize.Op.or]: [
|
[models.Sequelize.Op.or]: [
|
||||||
{ ROUTE_1: newLevelId },
|
{ ROUTE_1: newLevelId },
|
||||||
{ ROUTE_2: newLevelId },
|
{ ROUTE_2: newLevelId },
|
||||||
|
|
|
||||||
|
|
@ -109,7 +109,10 @@ export const calculateScore = async (req, res, next) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const allExercises = await models.Exercise.findAll({
|
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) {
|
if (!allExercises || allExercises.length === 0) {
|
||||||
|
|
@ -158,7 +161,10 @@ export const checkFirstFiveCorrect = async (req, res, next) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const firstFiveExercises = await models.Exercise.findAll({
|
const firstFiveExercises = await models.Exercise.findAll({
|
||||||
where: { ID_LEVEL: stdLearning.ID_LEVEL },
|
where: {
|
||||||
|
ID_LEVEL: stdLearning.ID_LEVEL,
|
||||||
|
IS_DELETED: 0,
|
||||||
|
},
|
||||||
order: [["TITLE", "ASC"]],
|
order: [["TITLE", "ASC"]],
|
||||||
limit: 5,
|
limit: 5,
|
||||||
});
|
});
|
||||||
|
|
@ -207,7 +213,10 @@ export const nextLearning = async (req, res, next) => {
|
||||||
const topic_id = stdLearning.level.ID_TOPIC;
|
const topic_id = stdLearning.level.ID_TOPIC;
|
||||||
|
|
||||||
const levels = await models.Level.findAll({
|
const levels = await models.Level.findAll({
|
||||||
where: { ID_TOPIC: topic_id },
|
where: {
|
||||||
|
ID_TOPIC: topic_id,
|
||||||
|
IS_DELETED: 0,
|
||||||
|
},
|
||||||
order: [["NAME_LEVEL", "ASC"]],
|
order: [["NAME_LEVEL", "ASC"]],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,21 +9,16 @@ const memoryStorage = multer.memoryStorage();
|
||||||
const fileFilter = (req, file, cb) => {
|
const fileFilter = (req, file, cb) => {
|
||||||
const ext = path.extname(file.originalname).toLowerCase();
|
const ext = path.extname(file.originalname).toLowerCase();
|
||||||
|
|
||||||
switch (file.fieldname) {
|
if (file.fieldname.startsWith("AUDIO")) {
|
||||||
case "AUDIO":
|
|
||||||
if (ext === ".mp3") {
|
if (ext === ".mp3") {
|
||||||
cb(null, true);
|
cb(null, true);
|
||||||
} else {
|
} else {
|
||||||
cb(
|
cb(
|
||||||
new Error(
|
new Error("Invalid file type, only .mp3 files are allowed for audio!"),
|
||||||
"Invalid file type, only .mp3 files are allowed for audio!"
|
|
||||||
),
|
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
break;
|
} else if (file.fieldname.startsWith("IMAGE")) {
|
||||||
|
|
||||||
case "IMAGE":
|
|
||||||
if (ext === ".jpg" || ext === ".jpeg" || ext === ".png") {
|
if (ext === ".jpg" || ext === ".jpeg" || ext === ".png") {
|
||||||
cb(null, true);
|
cb(null, true);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -34,9 +29,7 @@ const fileFilter = (req, file, cb) => {
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
break;
|
} else {
|
||||||
|
|
||||||
default:
|
|
||||||
cb(new Error("Invalid file type!"), false);
|
cb(new Error("Invalid file type!"), false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -47,10 +40,7 @@ const upload = multer({
|
||||||
limits: {
|
limits: {
|
||||||
fileSize: 100 * 1024 * 1024,
|
fileSize: 100 * 1024 * 1024,
|
||||||
},
|
},
|
||||||
}).fields([
|
}).any();
|
||||||
{ name: "AUDIO", maxCount: 1 },
|
|
||||||
{ name: "IMAGE", maxCount: 1 },
|
|
||||||
]);
|
|
||||||
|
|
||||||
const handleUpload = (req, res, next) => {
|
const handleUpload = (req, res, next) => {
|
||||||
upload(req, res, (err) => {
|
upload(req, res, (err) => {
|
||||||
|
|
@ -58,38 +48,36 @@ const handleUpload = (req, res, next) => {
|
||||||
return response(400, null, err.message, res);
|
return response(400, null, err.message, res);
|
||||||
}
|
}
|
||||||
|
|
||||||
const files = req.files;
|
const files = req.files || [];
|
||||||
const AUDIO = files?.AUDIO ? files.AUDIO[0] : null;
|
req.filesToSave = {};
|
||||||
const IMAGE = files?.IMAGE ? files.IMAGE[0] : null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
let validFiles = true;
|
let validFiles = true;
|
||||||
let errorMessages = [];
|
let errorMessages = [];
|
||||||
|
|
||||||
if (AUDIO && AUDIO.size > 10 * 1024 * 1024) {
|
files.forEach((file) => {
|
||||||
|
if (file.fieldname.startsWith("AUDIO")) {
|
||||||
|
if (file.size > 10 * 1024 * 1024) {
|
||||||
validFiles = false;
|
validFiles = false;
|
||||||
AUDIO.buffer = null;
|
errorMessages.push(`Audio file exceeds the size limit of 10MB`);
|
||||||
errorMessages.push("Audio file exceeds the size limit of 10MB");
|
} else {
|
||||||
|
req.filesToSave[file.fieldname] = file;
|
||||||
}
|
}
|
||||||
|
} else if (file.fieldname.startsWith("IMAGE")) {
|
||||||
if (IMAGE && IMAGE.size > 5 * 1024 * 1024) {
|
if (file.size > 5 * 1024 * 1024) {
|
||||||
validFiles = false;
|
validFiles = false;
|
||||||
IMAGE.buffer = null;
|
errorMessages.push(`Image file exceeds the size limit of 5MB`);
|
||||||
errorMessages.push("Image file exceeds the size limit of 5MB");
|
} else {
|
||||||
|
req.filesToSave[file.fieldname] = file;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (validFiles) {
|
if (validFiles) {
|
||||||
req.filesToSave = { AUDIO, IMAGE };
|
|
||||||
next();
|
next();
|
||||||
} else {
|
} else {
|
||||||
clearFileBuffers({ AUDIO, IMAGE });
|
clearFileBuffers(req.filesToSave);
|
||||||
return response(400, null, errorMessages.join("; "), res);
|
return response(400, null, errorMessages.join("; "), res);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
clearFileBuffers({ video, AUDIO, IMAGE });
|
|
||||||
return response(500, null, "Internal Server Error", res);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
137
middlewares/uploadExerciseSingle.js
Normal file
|
|
@ -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;
|
||||||
|
|
@ -65,6 +65,15 @@ const ExerciseModel = (DataTypes) => {
|
||||||
type: DataTypes.STRING(1024),
|
type: DataTypes.STRING(1024),
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
},
|
},
|
||||||
|
IS_DELETED: {
|
||||||
|
type: DataTypes.INTEGER(1),
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: 0,
|
||||||
|
validate: {
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
TIME_ADMIN_EXC: {
|
TIME_ADMIN_EXC: {
|
||||||
type: DataTypes.DATE,
|
type: DataTypes.DATE,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,15 @@ const LevelModel = (DataTypes) => {
|
||||||
type: DataTypes.UUID,
|
type: DataTypes.UUID,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
},
|
},
|
||||||
|
IS_DELETED: {
|
||||||
|
type: DataTypes.INTEGER(1),
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: 0,
|
||||||
|
validate: {
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
TIME_LEVEL: {
|
TIME_LEVEL: {
|
||||||
type: DataTypes.DATE,
|
type: DataTypes.DATE,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,15 @@ const SectionModel = (DataTypes) => {
|
||||||
type: DataTypes.STRING(255),
|
type: DataTypes.STRING(255),
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
},
|
},
|
||||||
|
IS_DELETED: {
|
||||||
|
type: DataTypes.INTEGER(1),
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: 0,
|
||||||
|
validate: {
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
TIME_SECTION: {
|
TIME_SECTION: {
|
||||||
type: DataTypes.DATE,
|
type: DataTypes.DATE,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,15 @@ const TopicModel = (DataTypes) => {
|
||||||
notEmpty: true,
|
notEmpty: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
IS_DELETED: {
|
||||||
|
type: DataTypes.INTEGER(1),
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: 0,
|
||||||
|
validate: {
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
TIME_TOPIC: {
|
TIME_TOPIC: {
|
||||||
type: DataTypes.DATE,
|
type: DataTypes.DATE,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
|
|
|
||||||
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
|
@ -1,33 +1,38 @@
|
||||||
import express from "express";
|
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 { createMultipleChoicesExercise, updateMultipleChoicesExerciseById } from "../../controllers/exerciseTypesControllers/multipleChoices.js";
|
||||||
import { createMatchingPairsExercise, updateMatchingPairsExerciseById } from "../../controllers/exerciseTypesControllers/matchingPairs.js";
|
import { createMatchingPairsExercise, updateMatchingPairsExerciseById } from "../../controllers/exerciseTypesControllers/matchingPairs.js";
|
||||||
import { createTrueFalseExercise, updateTrueFalseExerciseById } from "../../controllers/exerciseTypesControllers/trueFalse.js";
|
import { createTrueFalseExercise, updateTrueFalseExerciseById } from "../../controllers/exerciseTypesControllers/trueFalse.js";
|
||||||
import { verifyLoginUser, adminOnly } from "../../middlewares/User/authUser.js";
|
import { verifyLoginUser, adminOnly } from "../../middlewares/User/authUser.js";
|
||||||
import handleUpload from '../../middlewares/uploadExercise.js';
|
import handleUpload from '../../middlewares/uploadExercise.js';
|
||||||
|
import handleUploadSingleExercise from '../../middlewares/uploadExerciseSingle.js';
|
||||||
|
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
router.get("/exercise", verifyLoginUser, getExercises);
|
router.get("/exercise", verifyLoginUser, getExercises);
|
||||||
|
|
||||||
router.get("/exercise/admin", verifyLoginUser, adminOnly, getExercisesForAdmin);
|
|
||||||
|
|
||||||
router.get("/exercise/level/:idLevel", verifyLoginUser, getExerciseByLevelId);
|
router.get("/exercise/level/:idLevel", verifyLoginUser, getExerciseByLevelId);
|
||||||
|
|
||||||
|
router.get("/exercise/admin", verifyLoginUser, adminOnly, getExercisesForAdmin);
|
||||||
|
|
||||||
router.get("/exercise/:id", verifyLoginUser, getExerciseById);
|
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);
|
router.delete("/exercise/:id", verifyLoginUser, adminOnly, deleteExerciseById);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import express from "express";
|
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 { verifyLoginUser, adminOnly } from "../../middlewares/User/authUser.js";
|
||||||
import handleUpload from '../../middlewares/Level/uploadLevel.js';
|
import handleUpload from '../../middlewares/Level/uploadLevel.js';
|
||||||
import {checkLevelsPerTopic, autoCalculateRoutes, getSectionAndTopicByLevelId } from '../../middlewares/Level/checkLevel.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/topic/:idTopic", verifyLoginUser, getLevelsByTopicId);
|
||||||
|
|
||||||
|
router.get("/level/admin", verifyLoginUser, adminOnly, getLevelForAdmin);
|
||||||
|
|
||||||
router.get("/level/:id", verifyLoginUser, getLevelById);
|
router.get("/level/:id", verifyLoginUser, getLevelById);
|
||||||
|
|
||||||
router.get("/previous/level/:next_learning", verifyLoginUser, getPreviousLevel);
|
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/:id", verifyLoginUser, adminOnly, getSectionAndTopicByLevelId, deleteLevelById);
|
||||||
|
|
||||||
|
router.delete("/level/file/:id", verifyLoginUser, adminOnly, deleteLevelFileById);
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|
@ -1,13 +1,15 @@
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import handleUpload from '../../middlewares/uploadSection.js';
|
import { getSections, getSectionById, getSectionForAdmin, createSection, updateSectionById, deleteSectionById } from "../../controllers/contentControllers/section.js";
|
||||||
import { getSections, getSectionById, createSection, updateSectionById, deleteSectionById } from "../../controllers/contentControllers/section.js";
|
|
||||||
import { verifyLoginUser, adminOnly } from "../../middlewares/User/authUser.js";
|
import { verifyLoginUser, adminOnly } from "../../middlewares/User/authUser.js";
|
||||||
|
import handleUpload from '../../middlewares/uploadSection.js';
|
||||||
|
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
router.get("/section", verifyLoginUser, getSections);
|
router.get("/section", verifyLoginUser, getSections);
|
||||||
|
|
||||||
|
router.get("/section/admin", verifyLoginUser, getSectionForAdmin);
|
||||||
|
|
||||||
router.get("/section/:id", verifyLoginUser, getSectionById);
|
router.get("/section/:id", verifyLoginUser, getSectionById);
|
||||||
|
|
||||||
router.post("/section", verifyLoginUser, adminOnly, handleUpload, createSection);
|
router.post("/section", verifyLoginUser, adminOnly, handleUpload, createSection);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import express from "express";
|
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";
|
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/complete", verifyLoginUser, getCompletedTopicsBySection);
|
||||||
|
|
||||||
|
router.get("/topic/admin", verifyLoginUser, getTopicForAdmin);
|
||||||
|
|
||||||
router.get("/topic/section/:sectionId", verifyLoginUser, getTopicBySectionId);
|
router.get("/topic/section/:sectionId", verifyLoginUser, getTopicBySectionId);
|
||||||
|
|
||||||
router.get("/topic/:id", verifyLoginUser, getTopicById);
|
router.get("/topic/:id", verifyLoginUser, getTopicById);
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,8 @@ router.get("/stdExercise", verifyLoginUser, getStdExercises);
|
||||||
|
|
||||||
router.get("/stdExercise/:id", verifyLoginUser, getStdExerciseById);
|
router.get("/stdExercise/:id", verifyLoginUser, getStdExerciseById);
|
||||||
|
|
||||||
|
router.get("/studentAnswers/:id", verifyLoginUser, getStudentAnswersByStdLearningId);
|
||||||
|
|
||||||
router.post("/stdExercise", verifyLoginUser, stdAnswerExercise, checkCorrectAnswers, calculateScore, checkFirstFiveCorrect, nextLearning, updateStdLearningById);
|
router.post("/stdExercise", verifyLoginUser, stdAnswerExercise, checkCorrectAnswers, calculateScore, checkFirstFiveCorrect, nextLearning, updateStdLearningById);
|
||||||
|
|
||||||
router.post("/studentAnswers/:id", verifyLoginUser, getStudentAnswersByStdLearningId);
|
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import express from "express";
|
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 { checkStdLearning } from "../../middlewares/checkStdLearning.js";
|
||||||
import { verifyLoginUser } from "../../middlewares/User/authUser.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.post("/stdLearning", verifyLoginUser, checkStdLearning, createStdLearning);
|
||||||
|
|
||||||
|
router.put("/stdLearning/:id", verifyLoginUser, updateStdLearningById);
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||