refactor: section, topic, level, and exercise model

This commit is contained in:
elangptra 2024-10-10 09:55:13 +07:00
parent 2eb140b8c0
commit 5f361b2435
53 changed files with 1375 additions and 274 deletions

View File

@ -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) {

View File

@ -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();

View File

@ -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,
NEXT_LEARNING_LEVEL: nextLearningLevel
? nextLearningLevel.NAME_LEVEL
: null,
FINISHED_AT: lastCompletedLearning.STUDENT_FINISH, FINISHED_AT: lastCompletedLearning.STUDENT_FINISH,
UNLOCKED_LEVELS: unlockedLevels.map((lvl) => lvl.NAME_LEVEL),
} }
: null, : {
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 });

View File

@ -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 } });
);
if (fs.existsSync(thumbnailPath)) { await models.Level.update(
fs.unlinkSync(thumbnailPath); { 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) { } catch (error) {
console.log(error); console.log(error);
response(500, null, "Internal Server Error", res); response(500, null, "Internal Server Error", res);

View File

@ -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,

View File

@ -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({

View File

@ -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({

View File

@ -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({
@ -196,4 +205,4 @@ export const updateTrueFalseExerciseById = async (req, res) => {
await transaction.rollback(); await transaction.rollback();
response(500, null, "Internal Server Error", res); response(500, null, "Internal Server Error", res);
} }
}; };

View File

@ -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;

View File

@ -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,
})); }));

View File

@ -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,

View File

@ -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 },

View File

@ -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"]],
}); });

View File

@ -9,35 +9,28 @@ 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("Invalid file type, only .mp3 files are allowed for audio!"),
new Error( false
"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);
break; } else {
cb(
case "IMAGE": new Error(
if (ext === ".jpg" || ext === ".jpeg" || ext === ".png") { "Invalid file type, only .jpg, .jpeg, and .png files are allowed for image!"
cb(null, true); ),
} else { false
cb( );
new Error( }
"Invalid file type, only .jpg, .jpeg, and .png files are allowed for image!" } else {
), cb(new Error("Invalid file type!"), false);
false
);
}
break;
default:
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,37 +48,35 @@ 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) => {
validFiles = false; if (file.fieldname.startsWith("AUDIO")) {
AUDIO.buffer = null; if (file.size > 10 * 1024 * 1024) {
errorMessages.push("Audio file exceeds the size limit of 10MB"); 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) { if (validFiles) {
validFiles = false; next();
IMAGE.buffer = null; } else {
errorMessages.push("Image file exceeds the size limit of 5MB"); clearFileBuffers(req.filesToSave);
} return response(400, null, errorMessages.join("; "), res);
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);
} }
}); });
}; };

View 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;

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

View File

@ -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);

View File

@ -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

View File

@ -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);

View File

@ -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);

View File

@ -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

View File

@ -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