feat: student learning API
This commit is contained in:
parent
a27ea19c5b
commit
56dad54d89
|
|
@ -1,202 +0,0 @@
|
||||||
import response from "../response.js";
|
|
||||||
import bcrypt from "bcrypt";
|
|
||||||
import jwt from "jsonwebtoken";
|
|
||||||
import nodemailer from 'nodemailer';
|
|
||||||
import models from "../models/index.js";
|
|
||||||
|
|
||||||
const transporter = nodemailer.createTransport({
|
|
||||||
service: 'gmail', // Anda bisa menggunakan layanan email lainnya
|
|
||||||
auth: {
|
|
||||||
user: process.env.EMAIL_USER,
|
|
||||||
pass: process.env.EMAIL_PASS,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const registerUser = async (req, res) => {
|
|
||||||
const { name, email, password, confirmPassword } = req.body;
|
|
||||||
let roles = "student";
|
|
||||||
|
|
||||||
if (!name) {
|
|
||||||
return res.status(400).json({ message: "Name is required!" });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!email) {
|
|
||||||
return res.status(400).json({ message: "Email is required!" });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!password) {
|
|
||||||
return res.status(400).json({ message: "Password is required!" });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!confirmPassword) {
|
|
||||||
return res.status(400).json({ message: "Confirm Password is required!" });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (password !== confirmPassword) {
|
|
||||||
return res.status(400).json({ message: "Passwords do not match!" });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const salt = await bcrypt.genSalt(10);
|
|
||||||
const hashedPassword = await bcrypt.hash(password, salt);
|
|
||||||
|
|
||||||
const newUser = await models.User.create({
|
|
||||||
name,
|
|
||||||
email,
|
|
||||||
password: hashedPassword,
|
|
||||||
roles,
|
|
||||||
});
|
|
||||||
|
|
||||||
res.status(200).json({ message: "Registration success", result: newUser });
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
|
|
||||||
// Check for unique constraint error on email
|
|
||||||
if (error.name === "SequelizeUniqueConstraintError") {
|
|
||||||
return res.status(400).json({ message: "Email already registered!" });
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(500).json({ message: "Internal Server Error" });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const loginUser = async (req, res) => {
|
|
||||||
const { email, password } = req.body;
|
|
||||||
|
|
||||||
if (!email) {
|
|
||||||
return response(400, null, "Email is required!", res);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!password) {
|
|
||||||
return response(400, null, "Password is required!", res);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const user = await models.User.findOne({ where: { email } });
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return response(404, null, "User data not found!", res);
|
|
||||||
}
|
|
||||||
|
|
||||||
const validPassword = await bcrypt.compare(password, user.password);
|
|
||||||
if (!validPassword) {
|
|
||||||
return response(401, null, "The password you entered is incorrect!", res);
|
|
||||||
}
|
|
||||||
|
|
||||||
const accessToken = jwt.sign(
|
|
||||||
{ id: user.id },
|
|
||||||
process.env.ACCESS_TOKEN_SECRET
|
|
||||||
);
|
|
||||||
|
|
||||||
// Set tokens as HTTP-only cookies
|
|
||||||
res.cookie("accessToken", accessToken, {
|
|
||||||
httpOnly: true,
|
|
||||||
secure: process.env.NODE_ENV === "production", // Use secure cookies in production
|
|
||||||
});
|
|
||||||
|
|
||||||
// Selectively pick fields to send in the response
|
|
||||||
const userResponse = {
|
|
||||||
id: user.id,
|
|
||||||
name: user.name,
|
|
||||||
email: user.email,
|
|
||||||
roles: user.roles,
|
|
||||||
};
|
|
||||||
|
|
||||||
response(200, userResponse, "Success", res);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
res.status(500).json({ message: "Internal Server Error" });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const logoutUser = (req, res) => {
|
|
||||||
res.clearCookie("accessToken", {
|
|
||||||
httpOnly: true,
|
|
||||||
secure: process.env.NODE_ENV === "production",
|
|
||||||
});
|
|
||||||
|
|
||||||
res.status(200).json({ message: "You have successfully logged out." });
|
|
||||||
};
|
|
||||||
|
|
||||||
export const forgotPassword = async (req, res) => {
|
|
||||||
const { email } = req.body;
|
|
||||||
|
|
||||||
if (!email) {
|
|
||||||
return response(400, null, "Email is required!", res);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const user = await models.User.findOne({ where: { email } });
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return response(404, null, "Email is not registered!", res);
|
|
||||||
}
|
|
||||||
|
|
||||||
const resetToken = jwt.sign({ id: user.id }, process.env.RESET_PASSWORD_SECRET, {
|
|
||||||
expiresIn: '1h', // Token valid for 1 hour
|
|
||||||
});
|
|
||||||
|
|
||||||
const resetLink = `http://localhost:${process.env.APP_PORT}/resetPassword/${resetToken}`;
|
|
||||||
|
|
||||||
const mailOptions = {
|
|
||||||
from: process.env.EMAIL_USER,
|
|
||||||
to: user.email,
|
|
||||||
subject: 'Password Reset',
|
|
||||||
text: `You are receiving this because you (or someone else) have requested the reset of the password for your account.
|
|
||||||
Please click on the following link, or paste this into your browser to complete the process:
|
|
||||||
${resetLink}
|
|
||||||
If you did not request this, please ignore this email and your password will remain unchanged.`,
|
|
||||||
};
|
|
||||||
|
|
||||||
await transporter.sendMail(mailOptions);
|
|
||||||
|
|
||||||
response(200, null, "Password reset email sent successfully!", res);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
res.status(500).json({ message: "Internal Server Error" });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const resetPassword = async (req, res) => {
|
|
||||||
const { token, newPassword, confirmNewPassword } = req.body;
|
|
||||||
|
|
||||||
if (!token) {
|
|
||||||
return response(400, null, "Token is required!", res);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!newPassword) {
|
|
||||||
return response(400, null, "New password is required!", res);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!confirmNewPassword) {
|
|
||||||
return response(400, null, "Confirm new password is required!", res);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newPassword !== confirmNewPassword) {
|
|
||||||
return response(400, null, "Passwords do not match!", res);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const decoded = jwt.verify(token, process.env.RESET_PASSWORD_SECRET);
|
|
||||||
const user = await models.User.findOne({ where: { id: decoded.id } });
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return response(404, null, "User data not found!", res);
|
|
||||||
}
|
|
||||||
|
|
||||||
const salt = await bcrypt.genSalt(10);
|
|
||||||
const hashedPassword = await bcrypt.hash(newPassword, salt);
|
|
||||||
|
|
||||||
user.password = hashedPassword;
|
|
||||||
await user.save();
|
|
||||||
|
|
||||||
response(200, null, "Password has been reset successfully!", res);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
if (error.name === "TokenExpiredError") {
|
|
||||||
return response(400, null, "Reset token has expired!", res);
|
|
||||||
} else {
|
|
||||||
return res.status(500).json({ message: "Internal Server Error" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
371
controllers/auth/auth.js
Normal file
371
controllers/auth/auth.js
Normal file
|
|
@ -0,0 +1,371 @@
|
||||||
|
import response from "../../response.js";
|
||||||
|
import bcrypt from "bcrypt";
|
||||||
|
import jwt from "jsonwebtoken";
|
||||||
|
import nodemailer from "nodemailer";
|
||||||
|
import models from "../../models/index.js";
|
||||||
|
|
||||||
|
const transporter = nodemailer.createTransport({
|
||||||
|
service: "gmail",
|
||||||
|
auth: {
|
||||||
|
user: process.env.EMAIL_USER,
|
||||||
|
pass: process.env.EMAIL_PASS,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const registerAdmin = async (req, res) => {
|
||||||
|
const { name, email, password, confirmPassword } = req.body;
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
return response(400, null, "Name is required!", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!email) {
|
||||||
|
return response(400, null, "Email is required!", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!password) {
|
||||||
|
return response(400, null, "Password is required!", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!confirmPassword) {
|
||||||
|
return response(400, null, "Confirm Password is required!", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password !== confirmPassword) {
|
||||||
|
return response(400, null, "Passwords do not match!", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const salt = await bcrypt.genSalt(10);
|
||||||
|
const hashedPassword = await bcrypt.hash(password, salt);
|
||||||
|
|
||||||
|
const newUser = await models.User.create({
|
||||||
|
NAME_USERS: name,
|
||||||
|
EMAIL: email,
|
||||||
|
PASSWORD: hashedPassword,
|
||||||
|
ROLE: "admin",
|
||||||
|
PICTURE: "default-avatar.jpeg",
|
||||||
|
});
|
||||||
|
|
||||||
|
const adminResponse = {
|
||||||
|
id: newUser.ID,
|
||||||
|
name: newUser.NAME_USERS,
|
||||||
|
email: newUser.EMAIL,
|
||||||
|
role: newUser.ROLE,
|
||||||
|
picture: newUser.PICTURE,
|
||||||
|
};
|
||||||
|
|
||||||
|
response(200, adminResponse, "Admin registration successful", res);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
|
||||||
|
if (error.name === "SequelizeUniqueConstraintError") {
|
||||||
|
return response(400, null, "Email already registered!", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
response(500, null, "Internal Server Error", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const registerTeacher = async (req, res) => {
|
||||||
|
const { name, email, nip, password, confirmPassword } = req.body;
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
return response(400, null, "Name is required!", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!email) {
|
||||||
|
return response(400, null, "Email is required!", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nip) {
|
||||||
|
return response(400, null, "NIP is required for teachers!", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!password) {
|
||||||
|
return response(400, null, "Password is required!", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!confirmPassword) {
|
||||||
|
return response(400, null, "Confirm Password is required!", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password !== confirmPassword) {
|
||||||
|
return response(400, null, "Passwords do not match!", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const transaction = await models.db.transaction();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const salt = await bcrypt.genSalt(10);
|
||||||
|
const hashedPassword = await bcrypt.hash(password, salt);
|
||||||
|
|
||||||
|
const newUser = await models.User.create(
|
||||||
|
{
|
||||||
|
NAME_USERS: name,
|
||||||
|
EMAIL: email,
|
||||||
|
PASSWORD: hashedPassword,
|
||||||
|
ROLE: "teacher",
|
||||||
|
PICTURE: "default-avatar.jpeg",
|
||||||
|
},
|
||||||
|
{ transaction }
|
||||||
|
);
|
||||||
|
|
||||||
|
await models.Teacher.create(
|
||||||
|
{
|
||||||
|
ID: newUser.ID,
|
||||||
|
NIP: nip,
|
||||||
|
},
|
||||||
|
{ transaction }
|
||||||
|
);
|
||||||
|
|
||||||
|
await transaction.commit();
|
||||||
|
|
||||||
|
const teacherResponse = {
|
||||||
|
id: newUser.ID,
|
||||||
|
name: newUser.NAME_USERS,
|
||||||
|
email: newUser.EMAIL,
|
||||||
|
nip: nip,
|
||||||
|
role: newUser.ROLE,
|
||||||
|
picture: newUser.PICTURE,
|
||||||
|
};
|
||||||
|
|
||||||
|
response(200, teacherResponse, "Teacher registration successful", res);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
await transaction.rollback();
|
||||||
|
|
||||||
|
if (error.name === "SequelizeUniqueConstraintError") {
|
||||||
|
const field = error.original.sqlMessage.match(/for key '(.+)'/)[1];
|
||||||
|
|
||||||
|
if (field === "teacher_unique_nip") {
|
||||||
|
return response(400, null, "NIP already registered!", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field === "user_unique_email") {
|
||||||
|
return response(400, null, "Email already registered!", res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response(500, null, "Internal Server Error", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const registerStudent = async (req, res) => {
|
||||||
|
const { name, email, nisn, password, confirmPassword } = req.body;
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
return response(400, null, "Name is required!", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!email) {
|
||||||
|
return response(400, null, "Email is required!", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nisn) {
|
||||||
|
return response(400, null, "NISN is required for students!", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!password) {
|
||||||
|
return response(400, null, "Password is required!", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!confirmPassword) {
|
||||||
|
return response(400, null, "Confirm Password is required!", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password !== confirmPassword) {
|
||||||
|
return response(400, null, "Passwords do not match!", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const transaction = await models.db.transaction();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const salt = await bcrypt.genSalt(10);
|
||||||
|
const hashedPassword = await bcrypt.hash(password, salt);
|
||||||
|
|
||||||
|
const newUser = await models.User.create(
|
||||||
|
{
|
||||||
|
NAME_USERS: name,
|
||||||
|
EMAIL: email,
|
||||||
|
PASSWORD: hashedPassword,
|
||||||
|
ROLE: "student",
|
||||||
|
PICTURE: "default-avatar.jpeg",
|
||||||
|
},
|
||||||
|
{ transaction }
|
||||||
|
);
|
||||||
|
|
||||||
|
await models.Student.create(
|
||||||
|
{
|
||||||
|
ID: newUser.ID,
|
||||||
|
NISN: nisn,
|
||||||
|
},
|
||||||
|
{ transaction }
|
||||||
|
);
|
||||||
|
|
||||||
|
await transaction.commit();
|
||||||
|
|
||||||
|
const studentResponse = {
|
||||||
|
id: newUser.ID,
|
||||||
|
name: newUser.NAME_USERS,
|
||||||
|
email: newUser.EMAIL,
|
||||||
|
nisn: nisn,
|
||||||
|
role: newUser.ROLE,
|
||||||
|
picture: newUser.PICTURE,
|
||||||
|
};
|
||||||
|
|
||||||
|
response(200, studentResponse, "Student registration successful", res);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
await transaction.rollback();
|
||||||
|
|
||||||
|
if (error.name === "SequelizeUniqueConstraintError") {
|
||||||
|
const field = error.original.sqlMessage.match(/for key '(.+)'/)[1];
|
||||||
|
|
||||||
|
if (field === "student_unique_nisn") {
|
||||||
|
return response(400, null, "NISN already registered!", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field === "user_unique_email") {
|
||||||
|
return response(400, null, "Email already registered!", res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response(500, null, "Internal Server Error", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const loginUser = async (req, res) => {
|
||||||
|
const { email, password } = req.body;
|
||||||
|
|
||||||
|
if (!email) {
|
||||||
|
return response(400, null, "Email is required!", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!password) {
|
||||||
|
return response(400, null, "Password is required!", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const user = await models.User.findOne({ where: { email } });
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return response(404, null, "User data not found!", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const validPassword = await bcrypt.compare(password, user.PASSWORD);
|
||||||
|
if (!validPassword) {
|
||||||
|
return response(401, null, "The password you entered is incorrect!", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const accessToken = jwt.sign(
|
||||||
|
{ id: user.ID },
|
||||||
|
process.env.ACCESS_TOKEN_SECRET,
|
||||||
|
{ expiresIn: "6h" }
|
||||||
|
);
|
||||||
|
|
||||||
|
const userResponse = {
|
||||||
|
id: user.ID,
|
||||||
|
name: user.NAME_USERS,
|
||||||
|
email: user.EMAIL,
|
||||||
|
roles: user.ROLE,
|
||||||
|
token: `Bearer ${accessToken}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
response(200, userResponse, "Login successful", res);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
response(500, null, "Internal Server Error", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const logoutUser = (req, res) => {
|
||||||
|
response(200, null, "You have successfully logged out.", res);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const forgotPassword = async (req, res) => {
|
||||||
|
const { email } = req.body;
|
||||||
|
|
||||||
|
if (!email) {
|
||||||
|
return response(400, null, "Email is required!", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const user = await models.User.findOne({ where: { EMAIL: email } });
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return response(404, null, "Email is not registered!", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetToken = jwt.sign(
|
||||||
|
{ id: user.ID },
|
||||||
|
process.env.RESET_PASSWORD_SECRET,
|
||||||
|
{
|
||||||
|
expiresIn: "1h",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const resetLink = `http://localhost:${process.env.APP_PORT}/resetPassword/${resetToken}`;
|
||||||
|
|
||||||
|
const mailOptions = {
|
||||||
|
from: process.env.EMAIL_USER,
|
||||||
|
to: user.EMAIL,
|
||||||
|
subject: "Password Reset",
|
||||||
|
text: `You are receiving this because you (or someone else) have requested the reset of the password for your account.
|
||||||
|
Please click on the following link, or paste this into your browser to complete the process:
|
||||||
|
${resetLink}
|
||||||
|
If you did not request this, please ignore this email and your password will remain unchanged.`,
|
||||||
|
};
|
||||||
|
|
||||||
|
await transporter.sendMail(mailOptions);
|
||||||
|
|
||||||
|
response(200, null, "Password reset email sent successfully!", res);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
response(500, null, "Internal Server Error", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resetPassword = async (req, res) => {
|
||||||
|
const { token, newPassword, confirmNewPassword } = req.body;
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return response(400, null, "Token is required!", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!newPassword) {
|
||||||
|
return response(400, null, "New password is required!", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!confirmNewPassword) {
|
||||||
|
return response(400, null, "Confirm new password is required!", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newPassword !== confirmNewPassword) {
|
||||||
|
return response(400, null, "Passwords do not match!", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const decoded = jwt.verify(token, process.env.RESET_PASSWORD_SECRET);
|
||||||
|
const user = await models.User.findOne({ where: { ID: decoded.id } });
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return response(404, null, "User data not found!", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const salt = await bcrypt.genSalt(10);
|
||||||
|
const hashedPassword = await bcrypt.hash(newPassword, salt);
|
||||||
|
|
||||||
|
user.PASSWORD = hashedPassword;
|
||||||
|
await user.save();
|
||||||
|
|
||||||
|
response(200, null, "Password has been reset successfully!", res);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
if (error.name === "TokenExpiredError") {
|
||||||
|
return response(400, null, "Reset token has expired!", res);
|
||||||
|
} else {
|
||||||
|
return response(500, null, "Internal Server Error", res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
312
controllers/contentControllers/exercise.js
Normal file
312
controllers/contentControllers/exercise.js
Normal file
|
|
@ -0,0 +1,312 @@
|
||||||
|
import response from "../../response.js";
|
||||||
|
import models from "../../models/index.js";
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
export const getExercises = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const exercises = await models.Exercise.findAll({
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: models.MultipleChoices,
|
||||||
|
as: "multipleChoices",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: models.MatchingPairs,
|
||||||
|
as: "matchingPairs",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: models.TrueFalse,
|
||||||
|
as: "trueFalse",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (exercises.length === 0) {
|
||||||
|
return response(404, null, "No exercises found", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = exercises.map((exercise) => {
|
||||||
|
const exerciseData = { ...exercise.dataValues };
|
||||||
|
const questionType = exercise.QUESTION_TYPE;
|
||||||
|
|
||||||
|
if (questionType === "MCQ") {
|
||||||
|
delete exerciseData.matchingPairs;
|
||||||
|
delete exerciseData.trueFalse;
|
||||||
|
} else if (questionType === "MPQ") {
|
||||||
|
delete exerciseData.multipleChoices;
|
||||||
|
delete exerciseData.trueFalse;
|
||||||
|
} else if (questionType === "TFQ") {
|
||||||
|
delete exerciseData.multipleChoices;
|
||||||
|
delete exerciseData.matchingPairs;
|
||||||
|
} else {
|
||||||
|
delete exerciseData.multipleChoices;
|
||||||
|
delete exerciseData.matchingPairs;
|
||||||
|
delete exerciseData.trueFalse;
|
||||||
|
}
|
||||||
|
|
||||||
|
return exerciseData;
|
||||||
|
});
|
||||||
|
|
||||||
|
response(200, result, "Success", res);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
res.status(500).json({ message: "Internal Server Error" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getExercisesForAdmin = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const exercises = await models.Exercise.findAll({
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: models.MultipleChoices,
|
||||||
|
as: "multipleChoices",
|
||||||
|
attributes: ["ANSWER_KEY"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: models.MatchingPairs,
|
||||||
|
as: "matchingPairs",
|
||||||
|
attributes: ["LEFT_PAIR", "RIGHT_PAIR"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: models.TrueFalse,
|
||||||
|
as: "trueFalse",
|
||||||
|
attributes: ["IS_TRUE"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const formattedExercises = exercises.map((exercise) => {
|
||||||
|
const questionType = exercise.QUESTION_TYPE;
|
||||||
|
let answerKey = null;
|
||||||
|
|
||||||
|
if (questionType === "MCQ" && exercise.multipleChoices.length > 0) {
|
||||||
|
answerKey = exercise.multipleChoices[0].ANSWER_KEY;
|
||||||
|
} else if (
|
||||||
|
questionType === "MPQ" &&
|
||||||
|
exercise.matchingPairs.length > 0
|
||||||
|
) {
|
||||||
|
answerKey = exercise.matchingPairs
|
||||||
|
.map((pair) => `${pair.LEFT_PAIR}-${pair.RIGHT_PAIR}`)
|
||||||
|
.join(", ");
|
||||||
|
} else if (
|
||||||
|
questionType === "TFQ" &&
|
||||||
|
exercise.trueFalse.length > 0
|
||||||
|
) {
|
||||||
|
answerKey = exercise.trueFalse[0].IS_TRUE === 1 ? "true" : "false";
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ID_ADMIN_EXERCISE: exercise.ID_ADMIN_EXERCISE,
|
||||||
|
ID_LEVEL: exercise.ID_LEVEL,
|
||||||
|
TITLE: exercise.TITLE,
|
||||||
|
QUESTION: exercise.QUESTION,
|
||||||
|
SCORE_WEIGHT: exercise.SCORE_WEIGHT,
|
||||||
|
QUESTION_TYPE: questionType,
|
||||||
|
AUDIO: exercise.AUDIO,
|
||||||
|
VIDEO: exercise.VIDEO,
|
||||||
|
IMAGE: exercise.IMAGE,
|
||||||
|
ANSWER_KEY: answerKey,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
response(200, formattedExercises, "Success", res);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
response(500, null, "Error retrieving exercises data!", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getExerciseById = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
const exercise = await models.Exercise.findByPk(id, {
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: models.MultipleChoices,
|
||||||
|
as: "multipleChoices",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: models.MatchingPairs,
|
||||||
|
as: "matchingPairs",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: models.TrueFalse,
|
||||||
|
as: "trueFalse",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!exercise) {
|
||||||
|
return response(404, null, "Exercise not found", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const exerciseData = { ...exercise.dataValues };
|
||||||
|
const questionType = exercise.QUESTION_TYPE;
|
||||||
|
|
||||||
|
if (questionType === "MCQ") {
|
||||||
|
delete exerciseData.matchingPairs;
|
||||||
|
delete exerciseData.trueFalse;
|
||||||
|
} else if (questionType === "MPQ") {
|
||||||
|
delete exerciseData.multipleChoices;
|
||||||
|
delete exerciseData.trueFalse;
|
||||||
|
} else if (questionType === "TFQ") {
|
||||||
|
delete exerciseData.multipleChoices;
|
||||||
|
delete exerciseData.matchingPairs;
|
||||||
|
} else {
|
||||||
|
delete exerciseData.multipleChoices;
|
||||||
|
delete exerciseData.matchingPairs;
|
||||||
|
delete exerciseData.trueFalse;
|
||||||
|
}
|
||||||
|
|
||||||
|
response(200, exerciseData, "Success", res);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
res.status(500).json({ message: "Internal Server Error" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteExerciseById = async (req, res) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
const transaction = await models.db.transaction();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const exercise = await models.Exercise.findByPk(id, {
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: models.MultipleChoices,
|
||||||
|
as: "multipleChoices",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: models.MatchingPairs,
|
||||||
|
as: "matchingPairs",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: models.TrueFalse,
|
||||||
|
as: "trueFalse",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!exercise) {
|
||||||
|
await transaction.rollback();
|
||||||
|
return response(404, null, "Exercise not found", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exercise.VIDEO) {
|
||||||
|
const videoPath = path.join(
|
||||||
|
"public/uploads/exercise/video",
|
||||||
|
exercise.VIDEO
|
||||||
|
);
|
||||||
|
if (fs.existsSync(videoPath)) fs.unlinkSync(videoPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
response(200, null, "Exercise and related data deleted successfully", res);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
await transaction.rollback();
|
||||||
|
response(500, null, "Internal Server Error", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteExerciseFileById = async (req, res) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
const { fileType } = req.body;
|
||||||
|
|
||||||
|
if (!["audio", "video", "image"].includes(fileType)) {
|
||||||
|
return response(400, null, "Invalid file type specified", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const exercise = await models.Exercise.findByPk(id);
|
||||||
|
|
||||||
|
if (!exercise) {
|
||||||
|
return response(404, null, "Exercise not found", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
let filePath;
|
||||||
|
let fileName;
|
||||||
|
|
||||||
|
if (fileType === "video" && exercise.VIDEO) {
|
||||||
|
fileName = exercise.VIDEO;
|
||||||
|
filePath = path.join("public/uploads/exercise/video", fileName);
|
||||||
|
exercise.VIDEO = null;
|
||||||
|
} else if (fileType === "audio" && exercise.AUDIO) {
|
||||||
|
fileName = exercise.AUDIO;
|
||||||
|
filePath = path.join("public/uploads/exercise/audio", fileName);
|
||||||
|
exercise.AUDIO = null;
|
||||||
|
} else if (fileType === "image" && exercise.IMAGE) {
|
||||||
|
fileName = exercise.IMAGE;
|
||||||
|
filePath = path.join("public/uploads/exercise/image", fileName);
|
||||||
|
exercise.IMAGE = null;
|
||||||
|
} else {
|
||||||
|
return response(
|
||||||
|
404,
|
||||||
|
null,
|
||||||
|
`${
|
||||||
|
fileType.charAt(0).toUpperCase() + fileType.slice(1)
|
||||||
|
} file not found`,
|
||||||
|
res
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
|
fs.unlinkSync(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
await exercise.save();
|
||||||
|
|
||||||
|
response(
|
||||||
|
200,
|
||||||
|
exercise,
|
||||||
|
`${
|
||||||
|
fileType.charAt(0).toUpperCase() + fileType.slice(1)
|
||||||
|
} file deleted successfully`,
|
||||||
|
res
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
response(500, null, "Internal Server Error", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
332
controllers/contentControllers/level.js
Normal file
332
controllers/contentControllers/level.js
Normal file
|
|
@ -0,0 +1,332 @@
|
||||||
|
import response from "../../response.js";
|
||||||
|
import models from "../../models/index.js";
|
||||||
|
import {
|
||||||
|
clearFileBuffers,
|
||||||
|
saveFileToDisk,
|
||||||
|
} from "../../middlewares/Level/uploadLevel.js";
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import {
|
||||||
|
updateOtherLevelsRoutes,
|
||||||
|
updateOtherLevelsRoutesOnDelete,
|
||||||
|
} from "../../middlewares/Level/checkLevel.js";
|
||||||
|
|
||||||
|
export const getLevels = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const levels = await models.Level.findAll({
|
||||||
|
attributes: {
|
||||||
|
exclude: [
|
||||||
|
"ROUTE_1",
|
||||||
|
"ROUTE_2",
|
||||||
|
"ROUTE_3",
|
||||||
|
"ROUTE_4",
|
||||||
|
"ROUTE_5",
|
||||||
|
"ROUTE_6",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
response(200, levels, "Success", res);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
res.status(500).json({ message: "Internal Server Error" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getLevelById = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const level = await models.Level.findByPk(id, {
|
||||||
|
attributes: {
|
||||||
|
exclude: [
|
||||||
|
"ROUTE_1",
|
||||||
|
"ROUTE_2",
|
||||||
|
"ROUTE_3",
|
||||||
|
"ROUTE_4",
|
||||||
|
"ROUTE_5",
|
||||||
|
"ROUTE_6",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!level) {
|
||||||
|
return response(404, null, "Level not found", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
response(200, level, "Success", res);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
res.status(500).json({ message: "Internal Server Error" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createLevel = async (req, res, next) => {
|
||||||
|
const { NAME_LEVEL, ID_SECTION, ID_TOPIC, CONTENT } = req.body;
|
||||||
|
const { video, image, audio } = req.filesToSave || {};
|
||||||
|
|
||||||
|
if (!NAME_LEVEL) {
|
||||||
|
clearFileBuffers({ video, image, audio });
|
||||||
|
return response(400, null, "Level name is required", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ID_SECTION) {
|
||||||
|
clearFileBuffers({ video, image, audio });
|
||||||
|
return response(400, null, "Section is required", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ID_TOPIC) {
|
||||||
|
clearFileBuffers({ video, image, audio });
|
||||||
|
return response(400, null, "Topic is required", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const sectionWithTopic = await models.Topic.findOne({
|
||||||
|
where: { ID_SECTION, ID_TOPIC },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!sectionWithTopic) {
|
||||||
|
clearFileBuffers({ video, image, audio });
|
||||||
|
return response(
|
||||||
|
400,
|
||||||
|
null,
|
||||||
|
"Topic does not relate to the provided Section!",
|
||||||
|
res
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingLevel = await models.Level.findOne({
|
||||||
|
where: { NAME_LEVEL, ID_TOPIC },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingLevel) {
|
||||||
|
clearFileBuffers({ video, image, audio });
|
||||||
|
return response(
|
||||||
|
409,
|
||||||
|
null,
|
||||||
|
"A level with this name already exists under this topic",
|
||||||
|
res
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newLevel = await models.Level.create({
|
||||||
|
NAME_LEVEL,
|
||||||
|
ID_SECTION,
|
||||||
|
ID_TOPIC,
|
||||||
|
IS_PRETEST: req.body.IS_PRETEST || 0,
|
||||||
|
CONTENT,
|
||||||
|
VIDEO: null,
|
||||||
|
AUDIO: null,
|
||||||
|
IMAGE: null,
|
||||||
|
ROUTE_1: req.body.ROUTE_1,
|
||||||
|
ROUTE_2: req.body.ROUTE_2,
|
||||||
|
ROUTE_3: req.body.ROUTE_3,
|
||||||
|
ROUTE_4: req.body.ROUTE_4,
|
||||||
|
ROUTE_5: req.body.ROUTE_5,
|
||||||
|
ROUTE_6: req.body.ROUTE_6,
|
||||||
|
});
|
||||||
|
|
||||||
|
req.body.newLevelId = newLevel.ID_LEVEL;
|
||||||
|
|
||||||
|
const videoFilename = video
|
||||||
|
? saveFileToDisk(video, "video", ID_TOPIC, ID_SECTION, newLevel.ID_LEVEL)
|
||||||
|
: null;
|
||||||
|
const audioFilename = audio
|
||||||
|
? saveFileToDisk(audio, "audio", ID_TOPIC, ID_SECTION, newLevel.ID_LEVEL)
|
||||||
|
: null;
|
||||||
|
const imageFilename = image
|
||||||
|
? saveFileToDisk(image, "image", ID_TOPIC, ID_SECTION, newLevel.ID_LEVEL)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
newLevel.VIDEO = videoFilename;
|
||||||
|
newLevel.AUDIO = audioFilename;
|
||||||
|
newLevel.IMAGE = imageFilename;
|
||||||
|
await newLevel.save();
|
||||||
|
|
||||||
|
await updateOtherLevelsRoutes(req, res, next);
|
||||||
|
|
||||||
|
response(201, newLevel, "Level created successfully", res);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
clearFileBuffers({ video, image, audio });
|
||||||
|
return response(500, null, "Internal Server Error", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateLevelById = async (req, res, next) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
const { NAME_LEVEL, ID_SECTION, ID_TOPIC, CONTENT } = req.body;
|
||||||
|
|
||||||
|
const { video, image, audio } = req.filesToSave || {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const level = await models.Level.findByPk(id);
|
||||||
|
|
||||||
|
if (!level) {
|
||||||
|
clearFileBuffers({ video, image, audio });
|
||||||
|
return response(404, null, "Level not found", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sectionWithTopic = await models.Topic.findOne({
|
||||||
|
where: { ID_SECTION, ID_TOPIC },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!sectionWithTopic) {
|
||||||
|
clearFileBuffers({ video, image, audio });
|
||||||
|
return response(
|
||||||
|
400,
|
||||||
|
null,
|
||||||
|
"Topic does not relate to the provided Section",
|
||||||
|
res
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (NAME_LEVEL && ID_TOPIC) {
|
||||||
|
const existingLevel = await models.Level.findOne({
|
||||||
|
where: {
|
||||||
|
NAME_LEVEL,
|
||||||
|
ID_TOPIC,
|
||||||
|
ID_LEVEL: { [models.Sequelize.Op.ne]: id },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingLevel) {
|
||||||
|
clearFileBuffers({ video, image, audio });
|
||||||
|
return response(
|
||||||
|
409,
|
||||||
|
null,
|
||||||
|
"A level with this name already exists under this topic",
|
||||||
|
res
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (NAME_LEVEL) {
|
||||||
|
level.NAME_LEVEL = NAME_LEVEL;
|
||||||
|
if (NAME_LEVEL === "Level 1") {
|
||||||
|
level.IS_PRETEST = 1;
|
||||||
|
} else {
|
||||||
|
level.IS_PRETEST = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ID_SECTION) level.ID_SECTION = ID_SECTION;
|
||||||
|
if (ID_TOPIC) level.ID_TOPIC = ID_TOPIC;
|
||||||
|
if (CONTENT) level.CONTENT = CONTENT;
|
||||||
|
|
||||||
|
if (video) {
|
||||||
|
if (level.VIDEO) {
|
||||||
|
const oldVideoPath = path.join(
|
||||||
|
"public/uploads/level/video",
|
||||||
|
level.VIDEO
|
||||||
|
);
|
||||||
|
if (fs.existsSync(oldVideoPath)) {
|
||||||
|
fs.unlinkSync(oldVideoPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
level.VIDEO = saveFileToDisk(
|
||||||
|
video,
|
||||||
|
"video",
|
||||||
|
ID_TOPIC || level.ID_TOPIC,
|
||||||
|
ID_SECTION || level.ID_SECTION,
|
||||||
|
level.ID_LEVEL
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (audio) {
|
||||||
|
if (level.AUDIO) {
|
||||||
|
const oldAudioPath = path.join(
|
||||||
|
"public/uploads/level/audio",
|
||||||
|
level.AUDIO
|
||||||
|
);
|
||||||
|
if (fs.existsSync(oldAudioPath)) {
|
||||||
|
fs.unlinkSync(oldAudioPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
level.AUDIO = saveFileToDisk(
|
||||||
|
audio,
|
||||||
|
"audio",
|
||||||
|
ID_TOPIC || level.ID_TOPIC,
|
||||||
|
ID_SECTION || level.ID_SECTION,
|
||||||
|
level.ID_LEVEL
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (image) {
|
||||||
|
if (level.IMAGE) {
|
||||||
|
const oldImagePath = path.join(
|
||||||
|
"public/uploads/level/image",
|
||||||
|
level.IMAGE
|
||||||
|
);
|
||||||
|
if (fs.existsSync(oldImagePath)) {
|
||||||
|
fs.unlinkSync(oldImagePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
level.IMAGE = saveFileToDisk(
|
||||||
|
image,
|
||||||
|
"image",
|
||||||
|
ID_TOPIC || level.ID_TOPIC,
|
||||||
|
ID_SECTION || level.ID_SECTION,
|
||||||
|
level.ID_LEVEL
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await level.save();
|
||||||
|
|
||||||
|
req.body.newLevelId = level.ID_LEVEL;
|
||||||
|
|
||||||
|
await updateOtherLevelsRoutes(req, res, next);
|
||||||
|
|
||||||
|
response(200, level, "Level updated successfully", res);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
clearFileBuffers({ video, image, audio });
|
||||||
|
return response(500, null, "Internal Server Error", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteLevelById = async (req, res, next) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const level = await models.Level.findByPk(id);
|
||||||
|
|
||||||
|
if (!level) {
|
||||||
|
return response(404, null, "Level not found", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteFile = (filePath) => {
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
|
fs.unlinkSync(filePath);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (level.VIDEO) {
|
||||||
|
const videoPath = path.join("public/uploads/level/video", level.VIDEO);
|
||||||
|
deleteFile(videoPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (level.AUDIO) {
|
||||||
|
const audioPath = path.join("public/uploads/level/audio", level.AUDIO);
|
||||||
|
deleteFile(audioPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (level.IMAGE) {
|
||||||
|
const imagePath = path.join("public/uploads/level/image", level.IMAGE);
|
||||||
|
deleteFile(imagePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
req.body.newLevelId = level.ID_LEVEL;
|
||||||
|
|
||||||
|
await level.destroy();
|
||||||
|
|
||||||
|
await updateOtherLevelsRoutesOnDelete(req, res, next);
|
||||||
|
|
||||||
|
response(200, null, "Level deleted successfully", res);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
return response(500, null, "Internal Server Error", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const unlockPreviousRoutes = async (req, res) => {
|
||||||
|
const { NEXT_LEARNING } = req.params;
|
||||||
|
};
|
||||||
144
controllers/contentControllers/section.js
Normal file
144
controllers/contentControllers/section.js
Normal file
|
|
@ -0,0 +1,144 @@
|
||||||
|
import response from "../../response.js";
|
||||||
|
import models from "../../models/index.js";
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import {
|
||||||
|
clearFileBuffers,
|
||||||
|
saveFileToDisk,
|
||||||
|
} from "../../middlewares/uploadSection.js";
|
||||||
|
|
||||||
|
export const getSections = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const sections = await models.Section.findAll();
|
||||||
|
response(200, sections, "Success", res);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
response(500, null, "Error retrieving sections data!", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getSectionById = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const section = await models.Section.findByPk(id);
|
||||||
|
|
||||||
|
if (!section) {
|
||||||
|
return response(404, null, "Section not found", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
response(200, section, "Success", res);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
response(500, null, "Internal Server Error", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createSection = async (req, res) => {
|
||||||
|
const { NAME_SECTION, DESCRIPTION_SECTION } = req.body;
|
||||||
|
|
||||||
|
const { thumbnail } = req.filesToSave || {};
|
||||||
|
|
||||||
|
if (!NAME_SECTION) {
|
||||||
|
clearFileBuffers({ thumbnail });
|
||||||
|
return response(400, null, "Section name is required", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!DESCRIPTION_SECTION) {
|
||||||
|
clearFileBuffers({ thumbnail });
|
||||||
|
return response(400, null, "Description is required", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newSection = await models.Section.create({
|
||||||
|
NAME_SECTION,
|
||||||
|
DESCRIPTION_SECTION,
|
||||||
|
THUMBNAIL: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const thumbnailFilename = thumbnail
|
||||||
|
? saveFileToDisk(thumbnail, "thumbnail", newSection.ID_SECTION)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
newSection.THUMBNAIL = thumbnailFilename;
|
||||||
|
await newSection.save();
|
||||||
|
|
||||||
|
response(201, newSection, "Section created successfully", res);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
clearFileBuffers({ thumbnail });
|
||||||
|
response(500, null, "Internal Server Error", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateSectionById = async (req, res) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
const { NAME_SECTION, DESCRIPTION_SECTION } = req.body;
|
||||||
|
|
||||||
|
const { thumbnail } = req.filesToSave || {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const section = await models.Section.findByPk(id);
|
||||||
|
|
||||||
|
if (!section) {
|
||||||
|
clearFileBuffers({ thumbnail });
|
||||||
|
return response(404, null, "Section not found", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (NAME_SECTION) section.NAME_SECTION = NAME_SECTION;
|
||||||
|
if (DESCRIPTION_SECTION) section.DESCRIPTION_SECTION = DESCRIPTION_SECTION;
|
||||||
|
|
||||||
|
if (thumbnail) {
|
||||||
|
if (section.THUMBNAIL) {
|
||||||
|
const oldThumbnailPath = path.join(
|
||||||
|
"public/uploads/section",
|
||||||
|
section.THUMBNAIL
|
||||||
|
);
|
||||||
|
if (fs.existsSync(oldThumbnailPath)) {
|
||||||
|
fs.unlinkSync(oldThumbnailPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
section.THUMBNAIL = saveFileToDisk(
|
||||||
|
thumbnail,
|
||||||
|
"thumbnail",
|
||||||
|
section.ID_SECTION
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await section.save();
|
||||||
|
|
||||||
|
response(200, section, "Section updated successfully", res);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
clearFileBuffers({ thumbnail });
|
||||||
|
response(500, null, "Internal Server Error", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteSectionById = async (req, res) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const section = await models.Section.findByPk(id);
|
||||||
|
|
||||||
|
if (!section) {
|
||||||
|
return response(404, null, "Section not found", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (section.THUMBNAIL) {
|
||||||
|
const thumbnailPath = path.join(
|
||||||
|
"public/uploads/section",
|
||||||
|
section.THUMBNAIL
|
||||||
|
);
|
||||||
|
if (fs.existsSync(thumbnailPath)) {
|
||||||
|
fs.unlinkSync(thumbnailPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await section.destroy();
|
||||||
|
|
||||||
|
response(200, null, "Section deleted successfully", res);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
response(500, null, "Internal Server Error", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import response from "../response.js";
|
import response from "../../response.js";
|
||||||
import models from "../models/index.js";
|
import models from "../../models/index.js";
|
||||||
|
|
||||||
export const getTopics = async (req, res) => {
|
export const getTopics = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -23,33 +23,35 @@ export const getTopicById = async (req, res) => {
|
||||||
response(200, topic, "Success", res);
|
response(200, topic, "Success", res);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
res.status(500).json({ message: "Internal Server Error" });
|
response(500, null, "Internal Server Error", res);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createTopic = async (req, res) => {
|
export const createTopic = async (req, res) => {
|
||||||
const { subject_id, title } = req.body;
|
const { ID_SECTION, NAME_TOPIC, DESCRIPTION_TOPIC } = req.body;
|
||||||
|
|
||||||
// Validate subject_id
|
if (!ID_SECTION) {
|
||||||
if (!subject_id) {
|
return response(400, null, "Section ID is required", res);
|
||||||
return response(400, null, "Subject ID is required", res);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate title
|
if (!NAME_TOPIC) {
|
||||||
if (!title) {
|
return response(400, null, "Topic name is required", res);
|
||||||
return response(400, null, "Title is required", res);
|
}
|
||||||
|
|
||||||
|
if (!DESCRIPTION_TOPIC) {
|
||||||
|
return response(400, null, "Topic Description is required", res);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Verify that the subject_id exists in the m_subjects table
|
const section = await models.Section.findByPk(ID_SECTION);
|
||||||
const subject = await models.Subject.findByPk(subject_id);
|
if (!section) {
|
||||||
if (!subject) {
|
return response(404, null, "Section not found", res);
|
||||||
return response(404, null, "Subject not found", res);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const newTopic = await models.Topic.create({
|
const newTopic = await models.Topic.create({
|
||||||
subject_id,
|
ID_SECTION,
|
||||||
title,
|
NAME_TOPIC,
|
||||||
|
DESCRIPTION_TOPIC,
|
||||||
});
|
});
|
||||||
|
|
||||||
response(201, newTopic, "Topic created successfully", res);
|
response(201, newTopic, "Topic created successfully", res);
|
||||||
|
|
@ -61,31 +63,31 @@ export const createTopic = async (req, res) => {
|
||||||
|
|
||||||
export const updateTopicById = async (req, res) => {
|
export const updateTopicById = async (req, res) => {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const { subject_id, title } = req.body;
|
const { ID_SECTION, NAME_TOPIC, DESCRIPTION_TOPIC } = req.body;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Find the topic by its ID
|
|
||||||
const topic = await models.Topic.findByPk(id);
|
const topic = await models.Topic.findByPk(id);
|
||||||
|
|
||||||
if (!topic) {
|
if (!topic) {
|
||||||
return response(404, null, "Topic not found", res);
|
return response(404, null, "Topic not found", res);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate and update subject_id if provided
|
if (ID_SECTION) {
|
||||||
if (subject_id) {
|
const section = await models.Section.findByPk(ID_SECTION);
|
||||||
const subject = await models.Subject.findByPk(subject_id);
|
if (!section) {
|
||||||
if (!subject) {
|
return response(404, null, "Section not found", res);
|
||||||
return response(404, null, "Subject not found", res);
|
|
||||||
}
|
}
|
||||||
topic.subject_id = subject_id;
|
topic.ID_SECTION = ID_SECTION;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate and update title if provided
|
if (NAME_TOPIC) {
|
||||||
if (title) {
|
topic.NAME_TOPIC = NAME_TOPIC;
|
||||||
topic.title = title;
|
}
|
||||||
|
|
||||||
|
if (DESCRIPTION_TOPIC) {
|
||||||
|
topic.DESCRIPTION_TOPIC = DESCRIPTION_TOPIC;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save the updated topic
|
|
||||||
await topic.save();
|
await topic.save();
|
||||||
|
|
||||||
response(200, topic, "Topic updated successfully", res);
|
response(200, topic, "Topic updated successfully", res);
|
||||||
|
|
@ -99,14 +101,12 @@ export const deleteTopicById = async (req, res) => {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Find the topic by its ID
|
|
||||||
const topic = await models.Topic.findByPk(id);
|
const topic = await models.Topic.findByPk(id);
|
||||||
|
|
||||||
if (!topic) {
|
if (!topic) {
|
||||||
return response(404, null, "Topic not found", res);
|
return response(404, null, "Topic not found", res);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete the topic
|
|
||||||
await topic.destroy();
|
await topic.destroy();
|
||||||
|
|
||||||
response(200, null, "Topic deleted successfully", res);
|
response(200, null, "Topic deleted successfully", res);
|
||||||
273
controllers/exerciseTypesControllers/matchingPairs.js
Normal file
273
controllers/exerciseTypesControllers/matchingPairs.js
Normal file
|
|
@ -0,0 +1,273 @@
|
||||||
|
import response from "../../response.js";
|
||||||
|
import models from "../../models/index.js";
|
||||||
|
import {
|
||||||
|
clearFileBuffers,
|
||||||
|
saveFileToDisk,
|
||||||
|
} from "../../middlewares/uploadExercise.js";
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
export const createMatchingPairsExercise = async (req, res) => {
|
||||||
|
const { ID_LEVEL, TITLE, QUESTION, SCORE_WEIGHT } = req.body;
|
||||||
|
let PAIRS = req.body.PAIRS;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (typeof PAIRS === "string") {
|
||||||
|
PAIRS = JSON.parse(PAIRS);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return response(400, null, "Invalid PAIRS format", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { video, image, audio } = req.filesToSave || {};
|
||||||
|
|
||||||
|
if (!ID_LEVEL) return response(400, null, "Level ID is required", res);
|
||||||
|
if (!QUESTION) return response(400, null, "Question is required", res);
|
||||||
|
if (!PAIRS || !Array.isArray(PAIRS) || PAIRS.length === 0)
|
||||||
|
return response(400, null, "At least one pair is required", res);
|
||||||
|
if (!SCORE_WEIGHT)
|
||||||
|
return response(400, null, "Score weight is required", res);
|
||||||
|
|
||||||
|
const transaction = await models.db.transaction();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const level = await models.Level.findByPk(ID_LEVEL);
|
||||||
|
if (!level) {
|
||||||
|
clearFileBuffers({ video, image, audio });
|
||||||
|
return response(404, null, "Level not found", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
let generatedTitle = TITLE;
|
||||||
|
if (!TITLE) {
|
||||||
|
const exerciseCount = await models.Exercise.count({
|
||||||
|
where: { ID_LEVEL },
|
||||||
|
});
|
||||||
|
generatedTitle = `Soal ${exerciseCount + 1}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingExercise = await models.Exercise.findOne({
|
||||||
|
where: { ID_LEVEL, TITLE: generatedTitle },
|
||||||
|
});
|
||||||
|
if (existingExercise) {
|
||||||
|
clearFileBuffers({ video, image, audio });
|
||||||
|
return response(
|
||||||
|
400,
|
||||||
|
null,
|
||||||
|
"An exercise with the same title already exists in this level",
|
||||||
|
res
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newExercise = await models.Exercise.create(
|
||||||
|
{
|
||||||
|
ID_LEVEL,
|
||||||
|
TITLE: generatedTitle,
|
||||||
|
QUESTION,
|
||||||
|
SCORE_WEIGHT,
|
||||||
|
QUESTION_TYPE: "MPQ",
|
||||||
|
AUDIO: null,
|
||||||
|
VIDEO: null,
|
||||||
|
IMAGE: null,
|
||||||
|
},
|
||||||
|
{ transaction }
|
||||||
|
);
|
||||||
|
|
||||||
|
const videoFilename = video
|
||||||
|
? saveFileToDisk(video, "video", ID_LEVEL, newExercise.ID_ADMIN_EXERCISE)
|
||||||
|
: 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.VIDEO = videoFilename;
|
||||||
|
newExercise.AUDIO = audioFilename;
|
||||||
|
newExercise.IMAGE = imageFilename;
|
||||||
|
await newExercise.save({ transaction });
|
||||||
|
|
||||||
|
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 }
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const newMatchingPairs = await Promise.all(matchingPairsPromises);
|
||||||
|
|
||||||
|
await transaction.commit();
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
exercise: newExercise,
|
||||||
|
matchingPairs: newMatchingPairs,
|
||||||
|
};
|
||||||
|
|
||||||
|
response(201, payload, "Matching Pairs Exercise created successfully", res);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
await transaction.rollback();
|
||||||
|
clearFileBuffers({ video, image, audio });
|
||||||
|
response(500, null, "Internal Server Error", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateMatchingPairsExerciseById = async (req, res) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
const { ID_LEVEL, QUESTION, SCORE_WEIGHT } = req.body;
|
||||||
|
let PAIRS = req.body.PAIRS;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (typeof PAIRS === "string") {
|
||||||
|
PAIRS = JSON.parse(PAIRS);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return response(400, null, "Invalid PAIRS format", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { video, image, audio } = req.filesToSave || {};
|
||||||
|
|
||||||
|
const transaction = await models.db.transaction();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const exercise = await models.Exercise.findByPk(id, { transaction });
|
||||||
|
if (!exercise) {
|
||||||
|
clearFileBuffers({ video, image, audio });
|
||||||
|
await transaction.rollback();
|
||||||
|
return response(404, null, "Exercise not found", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ID_LEVEL) {
|
||||||
|
const level = await models.Level.findByPk(ID_LEVEL, { transaction });
|
||||||
|
if (!level) {
|
||||||
|
clearFileBuffers({ video, 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) {
|
||||||
|
if (exercise.VIDEO) {
|
||||||
|
const oldVideoPath = path.join(
|
||||||
|
"public/uploads/exercise/video",
|
||||||
|
exercise.VIDEO
|
||||||
|
);
|
||||||
|
if (fs.existsSync(oldVideoPath)) {
|
||||||
|
fs.unlinkSync(oldVideoPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exercise.VIDEO = saveFileToDisk(
|
||||||
|
video,
|
||||||
|
"video",
|
||||||
|
ID_LEVEL || exercise.ID_LEVEL,
|
||||||
|
exercise.ID_ADMIN_EXERCISE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 });
|
||||||
|
|
||||||
|
if (PAIRS && Array.isArray(PAIRS)) {
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await transaction.commit();
|
||||||
|
|
||||||
|
const updatedPairs = await models.MatchingPairs.findAll({
|
||||||
|
where: { ID_ADMIN_EXERCISE: exercise.ID_ADMIN_EXERCISE },
|
||||||
|
});
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
exercise,
|
||||||
|
matchingPairs: updatedPairs,
|
||||||
|
};
|
||||||
|
|
||||||
|
response(200, payload, "Matching Pairs Exercise updated successfully", res);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
await transaction.rollback();
|
||||||
|
clearFileBuffers({ video, image, audio });
|
||||||
|
response(500, null, "Internal Server Error", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
255
controllers/exerciseTypesControllers/multipleChoices.js
Normal file
255
controllers/exerciseTypesControllers/multipleChoices.js
Normal file
|
|
@ -0,0 +1,255 @@
|
||||||
|
import response from "../../response.js";
|
||||||
|
import models from "../../models/index.js";
|
||||||
|
import {
|
||||||
|
clearFileBuffers,
|
||||||
|
saveFileToDisk,
|
||||||
|
} from "../../middlewares/uploadExercise.js";
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
export const createMultipleChoicesExercise = async (req, res) => {
|
||||||
|
const {
|
||||||
|
ID_LEVEL,
|
||||||
|
TITLE,
|
||||||
|
QUESTION,
|
||||||
|
OPTION_A,
|
||||||
|
OPTION_B,
|
||||||
|
OPTION_C,
|
||||||
|
OPTION_D,
|
||||||
|
OPTION_E,
|
||||||
|
ANSWER_KEY,
|
||||||
|
SCORE_WEIGHT,
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
const { video, image, audio } = req.filesToSave || {};
|
||||||
|
|
||||||
|
if (!ID_LEVEL) return response(400, null, "Level ID is required", res);
|
||||||
|
if (!QUESTION) return response(400, null, "Question is required", res);
|
||||||
|
if (!OPTION_A) return response(400, null, "Option A is required", res);
|
||||||
|
if (!OPTION_B) return response(400, null, "Option B is required", res);
|
||||||
|
if (!OPTION_C) return response(400, null, "Option C is required", res);
|
||||||
|
if (!OPTION_D) return response(400, null, "Option D is required", res);
|
||||||
|
if (!OPTION_E) return response(400, null, "Option E is required", res);
|
||||||
|
if (!ANSWER_KEY) return response(400, null, "Answer key is required", res);
|
||||||
|
if (!SCORE_WEIGHT)
|
||||||
|
return response(400, null, "Score weight is required", res);
|
||||||
|
|
||||||
|
const transaction = await models.db.transaction();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const level = await models.Level.findByPk(ID_LEVEL);
|
||||||
|
if (!level) {
|
||||||
|
clearFileBuffers({ video, image, audio });
|
||||||
|
return response(404, null, "Level not found", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
let generatedTitle = TITLE;
|
||||||
|
if (!TITLE) {
|
||||||
|
const exerciseCount = await models.Exercise.count({
|
||||||
|
where: { ID_LEVEL },
|
||||||
|
});
|
||||||
|
generatedTitle = `Soal ${exerciseCount + 1}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingExercise = await models.Exercise.findOne({
|
||||||
|
where: { ID_LEVEL, TITLE: generatedTitle },
|
||||||
|
});
|
||||||
|
if (existingExercise) {
|
||||||
|
clearFileBuffers({ video, image, audio });
|
||||||
|
return response(
|
||||||
|
400,
|
||||||
|
null,
|
||||||
|
"An exercise with the same title already exists in this level",
|
||||||
|
res
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newExercise = await models.Exercise.create(
|
||||||
|
{
|
||||||
|
ID_LEVEL,
|
||||||
|
TITLE: generatedTitle,
|
||||||
|
QUESTION,
|
||||||
|
SCORE_WEIGHT,
|
||||||
|
QUESTION_TYPE: "MCQ",
|
||||||
|
AUDIO: null,
|
||||||
|
VIDEO: null,
|
||||||
|
IMAGE: null,
|
||||||
|
},
|
||||||
|
{ transaction }
|
||||||
|
);
|
||||||
|
|
||||||
|
const videoFilename = video
|
||||||
|
? saveFileToDisk(video, "video", ID_LEVEL, newExercise.ID_ADMIN_EXERCISE)
|
||||||
|
: 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.VIDEO = videoFilename;
|
||||||
|
newExercise.AUDIO = audioFilename;
|
||||||
|
newExercise.IMAGE = imageFilename;
|
||||||
|
await newExercise.save({ transaction });
|
||||||
|
|
||||||
|
const newMultipleChoices = await models.MultipleChoices.create(
|
||||||
|
{
|
||||||
|
ID_ADMIN_EXERCISE: newExercise.ID_ADMIN_EXERCISE,
|
||||||
|
OPTION_A,
|
||||||
|
OPTION_B,
|
||||||
|
OPTION_C,
|
||||||
|
OPTION_D,
|
||||||
|
OPTION_E,
|
||||||
|
ANSWER_KEY,
|
||||||
|
},
|
||||||
|
{ transaction }
|
||||||
|
);
|
||||||
|
|
||||||
|
await transaction.commit();
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
exercise: newExercise,
|
||||||
|
multipleChoices: newMultipleChoices,
|
||||||
|
};
|
||||||
|
|
||||||
|
response(201, payload, "Exercise created successfully", res);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
await transaction.rollback();
|
||||||
|
clearFileBuffers({ video, image, audio });
|
||||||
|
response(500, null, "Internal Server Error", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateMultipleChoicesExerciseById = async (req, res) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
const {
|
||||||
|
ID_LEVEL,
|
||||||
|
QUESTION,
|
||||||
|
OPTION_A,
|
||||||
|
OPTION_B,
|
||||||
|
OPTION_C,
|
||||||
|
OPTION_D,
|
||||||
|
OPTION_E,
|
||||||
|
ANSWER_KEY,
|
||||||
|
SCORE_WEIGHT,
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
const { video, image, audio } = req.filesToSave || {};
|
||||||
|
|
||||||
|
const transaction = await models.db.transaction();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const exercise = await models.Exercise.findByPk(id, { transaction });
|
||||||
|
|
||||||
|
if (!exercise) {
|
||||||
|
clearFileBuffers({ video, image, audio });
|
||||||
|
await transaction.rollback();
|
||||||
|
return response(404, null, "Exercise not found", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ID_LEVEL) {
|
||||||
|
const level = await models.Level.findByPk(ID_LEVEL, { transaction });
|
||||||
|
if (!level) {
|
||||||
|
clearFileBuffers({ video, 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) {
|
||||||
|
if (exercise.VIDEO) {
|
||||||
|
const oldVideoPath = path.join(
|
||||||
|
"public/uploads/exercise/video",
|
||||||
|
exercise.VIDEO
|
||||||
|
);
|
||||||
|
if (fs.existsSync(oldVideoPath)) {
|
||||||
|
fs.unlinkSync(oldVideoPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exercise.VIDEO = saveFileToDisk(
|
||||||
|
video,
|
||||||
|
"video",
|
||||||
|
ID_LEVEL || exercise.ID_LEVEL,
|
||||||
|
exercise.ID_ADMIN_EXERCISE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 });
|
||||||
|
|
||||||
|
const multipleChoices = await models.MultipleChoices.findOne({
|
||||||
|
where: { ID_ADMIN_EXERCISE: exercise.ID_ADMIN_EXERCISE },
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!multipleChoices) {
|
||||||
|
clearFileBuffers({ video, image, audio });
|
||||||
|
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 });
|
||||||
|
|
||||||
|
await transaction.commit();
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
exercise,
|
||||||
|
multipleChoices,
|
||||||
|
};
|
||||||
|
|
||||||
|
response(200, payload, "Exercise updated successfully", res);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
clearFileBuffers({ video, image, audio });
|
||||||
|
await transaction.rollback();
|
||||||
|
response(500, null, "Internal Server Error", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
222
controllers/exerciseTypesControllers/trueFalse.js
Normal file
222
controllers/exerciseTypesControllers/trueFalse.js
Normal file
|
|
@ -0,0 +1,222 @@
|
||||||
|
import response from "../../response.js";
|
||||||
|
import models from "../../models/index.js";
|
||||||
|
import {
|
||||||
|
clearFileBuffers,
|
||||||
|
saveFileToDisk,
|
||||||
|
} from "../../middlewares/uploadExercise.js";
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
export const createTrueFalseExercise = async (req, res) => {
|
||||||
|
const { ID_LEVEL, TITLE, QUESTION, IS_TRUE, SCORE_WEIGHT } = req.body;
|
||||||
|
|
||||||
|
const { video, image, audio } = req.filesToSave || {};
|
||||||
|
|
||||||
|
if (!ID_LEVEL) return response(400, null, "Level ID is required", res);
|
||||||
|
if (!QUESTION) return response(400, null, "Question is required", res);
|
||||||
|
if (typeof IS_TRUE === "undefined")
|
||||||
|
return response(400, null, "IS_TRUE is required", res);
|
||||||
|
if (!SCORE_WEIGHT)
|
||||||
|
return response(400, null, "Score weight is required", res);
|
||||||
|
|
||||||
|
const transaction = await models.db.transaction();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const level = await models.Level.findByPk(ID_LEVEL);
|
||||||
|
if (!level) {
|
||||||
|
clearFileBuffers({ video, image, audio });
|
||||||
|
return response(404, null, "Level not found", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
let generatedTitle = TITLE;
|
||||||
|
if (!TITLE) {
|
||||||
|
const exerciseCount = await models.Exercise.count({
|
||||||
|
where: { ID_LEVEL },
|
||||||
|
});
|
||||||
|
generatedTitle = `Soal ${exerciseCount + 1}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingExercise = await models.Exercise.findOne({
|
||||||
|
where: { ID_LEVEL, TITLE: generatedTitle },
|
||||||
|
});
|
||||||
|
if (existingExercise) {
|
||||||
|
clearFileBuffers({ video, image, audio });
|
||||||
|
return response(
|
||||||
|
400,
|
||||||
|
null,
|
||||||
|
"An exercise with the same title already exists in this level",
|
||||||
|
res
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newExercise = await models.Exercise.create(
|
||||||
|
{
|
||||||
|
ID_LEVEL,
|
||||||
|
TITLE: generatedTitle,
|
||||||
|
QUESTION,
|
||||||
|
SCORE_WEIGHT,
|
||||||
|
QUESTION_TYPE: "TFQ",
|
||||||
|
AUDIO: null,
|
||||||
|
VIDEO: null,
|
||||||
|
IMAGE: null,
|
||||||
|
},
|
||||||
|
{ transaction }
|
||||||
|
);
|
||||||
|
|
||||||
|
const videoFilename = video
|
||||||
|
? saveFileToDisk(video, "video", ID_LEVEL, newExercise.ID_ADMIN_EXERCISE)
|
||||||
|
: 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.VIDEO = videoFilename;
|
||||||
|
newExercise.AUDIO = audioFilename;
|
||||||
|
newExercise.IMAGE = imageFilename;
|
||||||
|
await newExercise.save({ transaction });
|
||||||
|
|
||||||
|
const newTrueFalse = await models.TrueFalse.create(
|
||||||
|
{
|
||||||
|
ID_ADMIN_EXERCISE: newExercise.ID_ADMIN_EXERCISE,
|
||||||
|
IS_TRUE,
|
||||||
|
},
|
||||||
|
{ transaction }
|
||||||
|
);
|
||||||
|
|
||||||
|
await transaction.commit();
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
exercise: newExercise,
|
||||||
|
trueFalse: newTrueFalse,
|
||||||
|
};
|
||||||
|
|
||||||
|
response(201, payload, "True/False exercise created successfully", res);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
await transaction.rollback();
|
||||||
|
clearFileBuffers({ video, image, audio });
|
||||||
|
response(500, null, "Internal Server Error", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateTrueFalseExerciseById = async (req, res) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
const { ID_LEVEL, QUESTION, IS_TRUE, SCORE_WEIGHT } = req.body;
|
||||||
|
|
||||||
|
const { video, image, audio } = req.filesToSave || {};
|
||||||
|
|
||||||
|
const transaction = await models.db.transaction();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const exercise = await models.Exercise.findByPk(id, { transaction });
|
||||||
|
|
||||||
|
if (!exercise) {
|
||||||
|
clearFileBuffers({ video, image, audio });
|
||||||
|
await transaction.rollback();
|
||||||
|
return response(404, null, "Exercise not found", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ID_LEVEL) {
|
||||||
|
const level = await models.Level.findByPk(ID_LEVEL, { transaction });
|
||||||
|
if (!level) {
|
||||||
|
clearFileBuffers({ video, 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) {
|
||||||
|
if (exercise.VIDEO) {
|
||||||
|
const oldVideoPath = path.join(
|
||||||
|
"public/uploads/exercise/video",
|
||||||
|
exercise.VIDEO
|
||||||
|
);
|
||||||
|
if (fs.existsSync(oldVideoPath)) {
|
||||||
|
fs.unlinkSync(oldVideoPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exercise.VIDEO = saveFileToDisk(
|
||||||
|
video,
|
||||||
|
"video",
|
||||||
|
ID_LEVEL || exercise.ID_LEVEL,
|
||||||
|
exercise.ID_ADMIN_EXERCISE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 });
|
||||||
|
|
||||||
|
const trueFalse = await models.TrueFalse.findOne({
|
||||||
|
where: { ID_ADMIN_EXERCISE: exercise.ID_ADMIN_EXERCISE },
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!trueFalse) {
|
||||||
|
clearFileBuffers({ video, image, audio });
|
||||||
|
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 });
|
||||||
|
|
||||||
|
await transaction.commit();
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
exercise,
|
||||||
|
trueFalse,
|
||||||
|
};
|
||||||
|
|
||||||
|
response(200, payload, "True/False exercise updated successfully", res);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
clearFileBuffers({ video, image, audio });
|
||||||
|
await transaction.rollback();
|
||||||
|
response(500, null, "Internal Server Error", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
93
controllers/learningControllers/stdExercise.js
Normal file
93
controllers/learningControllers/stdExercise.js
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
import response from "../../response.js";
|
||||||
|
import models from "../../models/index.js";
|
||||||
|
|
||||||
|
export const getStdExercises = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const stdExercise = await models.StdExercise.findAll();
|
||||||
|
response(200, stdExercise, "Success", res);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
response(500, null, "Error retrieving student exercise data!", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getStdExerciseById = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const stdExercise = await models.StdExercise.findByPk(id);
|
||||||
|
|
||||||
|
if (!stdExercise) {
|
||||||
|
return response(404, null, "Student exercise data not found", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
response(200, stdExercise, "Success", res);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
res.status(500).json({ message: "Internal Server Error" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const stdAnswerExercise = async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const { ID_STUDENT_LEARNING, ID_ADMIN_EXERCISE, ANSWER_STUDENT } = req.body;
|
||||||
|
|
||||||
|
if (!ID_STUDENT_LEARNING) {
|
||||||
|
return response(400, null, "Id student learning is required", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingStdLearning = await models.StdLearning.findByPk(
|
||||||
|
ID_STUDENT_LEARNING
|
||||||
|
);
|
||||||
|
if (!existingStdLearning) {
|
||||||
|
return response(404, null, "Id student learning not found", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ID_ADMIN_EXERCISE) {
|
||||||
|
return response(400, null, "Id exercise is required", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const exercise = await models.Exercise.findOne({
|
||||||
|
where: {
|
||||||
|
ID_ADMIN_EXERCISE: ID_ADMIN_EXERCISE,
|
||||||
|
ID_LEVEL: existingStdLearning.ID_LEVEL,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!exercise) {
|
||||||
|
return response(404, null, "Exercise not found in this level", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ANSWER_STUDENT) {
|
||||||
|
return response(400, null, "Answer is required", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingStdExercise = await models.StdExercise.findOne({
|
||||||
|
where: {
|
||||||
|
ID_STUDENT_LEARNING: ID_STUDENT_LEARNING,
|
||||||
|
ID_ADMIN_EXERCISE: ID_ADMIN_EXERCISE,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingStdExercise) {
|
||||||
|
existingStdExercise.ANSWER_STUDENT = ANSWER_STUDENT;
|
||||||
|
|
||||||
|
await existingStdExercise.save();
|
||||||
|
} else {
|
||||||
|
await models.StdExercise.create({
|
||||||
|
ID_STUDENT_LEARNING,
|
||||||
|
ID_ADMIN_EXERCISE,
|
||||||
|
ANSWER_STUDENT,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
req.params.id = ID_STUDENT_LEARNING;
|
||||||
|
next();
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
return response(
|
||||||
|
500,
|
||||||
|
null,
|
||||||
|
"Error creating or updating student exercise data!",
|
||||||
|
res
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
358
controllers/learningControllers/stdLearning.js
Normal file
358
controllers/learningControllers/stdLearning.js
Normal file
|
|
@ -0,0 +1,358 @@
|
||||||
|
import response from "../../response.js";
|
||||||
|
import models from "../../models/index.js";
|
||||||
|
import { createMonitoring } from "../monitoringControllers/monitoring.js";
|
||||||
|
|
||||||
|
export const getStdLearnings = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const stdLearning = await models.StdLearning.findAll();
|
||||||
|
response(200, stdLearning, "Success", res);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
response(500, null, "Error retrieving student learning data!", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getStdLearningById = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const stdLearning = await models.StdLearning.findByPk(id);
|
||||||
|
|
||||||
|
if (!stdLearning) {
|
||||||
|
return response(404, null, "Student learning data not found", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
response(200, stdLearning, "Success", res);
|
||||||
|
} catch (error) {
|
||||||
|
response(500, null, "Internal Server Error", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createStdLearning = async (req, res) => {
|
||||||
|
const { ID_LEVEL } = req.body;
|
||||||
|
|
||||||
|
if (!req.user) {
|
||||||
|
return response(401, null, "User not authenticated", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ID_LEVEL) {
|
||||||
|
return response(400, null, "Level ID is required", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ID = req.user.ID;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const level = await models.Level.findByPk(ID_LEVEL);
|
||||||
|
if (!level) {
|
||||||
|
return response(404, null, "Level not found", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newStdLearning = await models.StdLearning.create({
|
||||||
|
ID,
|
||||||
|
ID_LEVEL,
|
||||||
|
});
|
||||||
|
|
||||||
|
response(
|
||||||
|
201,
|
||||||
|
newStdLearning,
|
||||||
|
"Student Learning data created successfully",
|
||||||
|
res
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
response(500, null, "Internal Server Error", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateStdLearningById = async (req, res) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
const { FEEDBACK_STUDENT } = req.body;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stdLearning = await models.StdLearning.findByPk(id, {
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: models.Level,
|
||||||
|
as: "level",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!stdLearning) {
|
||||||
|
return response(404, null, "Student Learning record not found", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
stdLearning.STUDENT_FINISH = new Date();
|
||||||
|
stdLearning.SCORE = req.body.SCORE;
|
||||||
|
stdLearning.NEXT_LEARNING = req.body.NEXT_LEARNING;
|
||||||
|
stdLearning.IS_PASS = req.body.IS_PASS;
|
||||||
|
|
||||||
|
if (FEEDBACK_STUDENT) {
|
||||||
|
stdLearning.FEEDBACK_STUDENT = FEEDBACK_STUDENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
await stdLearning.save();
|
||||||
|
|
||||||
|
const level6 = await models.Level.findOne({
|
||||||
|
where: {
|
||||||
|
NAME_LEVEL: "Level 6",
|
||||||
|
ID_TOPIC: stdLearning.level.ID_TOPIC,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!level6) {
|
||||||
|
return response(404, null, "Level 6 not found", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
stdLearning.level.ID_LEVEL === level6.ID_LEVEL &&
|
||||||
|
stdLearning.IS_PASS === 1
|
||||||
|
) {
|
||||||
|
req.body.ID_STUDENT_LEARNING = id;
|
||||||
|
|
||||||
|
const existingMonitoring = await models.Monitoring.findOne({
|
||||||
|
where: { ID_STUDENT_LEARNING: id },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existingMonitoring) {
|
||||||
|
const newMonitoring = await createMonitoring(req);
|
||||||
|
|
||||||
|
const { level, ...responseData } = stdLearning.toJSON();
|
||||||
|
|
||||||
|
const combinedPayload = {
|
||||||
|
...responseData,
|
||||||
|
MONITORING: newMonitoring,
|
||||||
|
};
|
||||||
|
|
||||||
|
return response(
|
||||||
|
200,
|
||||||
|
combinedPayload,
|
||||||
|
"Student Learning record updated and monitoring created successfully",
|
||||||
|
res
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const monitoringToDelete = await models.Monitoring.findOne({
|
||||||
|
where: { ID_STUDENT_LEARNING: id },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (monitoringToDelete) {
|
||||||
|
await monitoringToDelete.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { level, ...responseData } = stdLearning.toJSON();
|
||||||
|
|
||||||
|
response(
|
||||||
|
200,
|
||||||
|
responseData,
|
||||||
|
"Student Learning record updated successfully",
|
||||||
|
res
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
response(500, null, "Internal Server Error", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const learningHistory = async (req, res) => {
|
||||||
|
try {
|
||||||
|
if (!req.user) {
|
||||||
|
return response(401, null, "User not authenticated", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { ID } = req.user;
|
||||||
|
|
||||||
|
const stdLearnings = await models.StdLearning.findAll({
|
||||||
|
where: {
|
||||||
|
ID: ID,
|
||||||
|
},
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: models.Level,
|
||||||
|
as: "level",
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: models.Topic,
|
||||||
|
as: "levelTopic",
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: models.Section,
|
||||||
|
as: "topicSection",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await Promise.all(
|
||||||
|
stdLearnings.map(async (learning) => {
|
||||||
|
let nextLevelName = null;
|
||||||
|
if (learning.NEXT_LEARNING) {
|
||||||
|
const nextLevel = await models.Level.findOne({
|
||||||
|
where: { ID_LEVEL: learning.NEXT_LEARNING },
|
||||||
|
});
|
||||||
|
if (nextLevel) {
|
||||||
|
nextLevelName = nextLevel.NAME_LEVEL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
SCORE: learning.SCORE,
|
||||||
|
CURRENT_LEVEL: learning.level.NAME_LEVEL,
|
||||||
|
NEXT_LEVEL: nextLevelName,
|
||||||
|
STUDENT_FINISH: learning.STUDENT_FINISH,
|
||||||
|
TOPIC_NAME: learning.level.levelTopic.NAME_TOPIC,
|
||||||
|
SECTION_NAME: learning.level.levelTopic.topicSection.NAME_SECTION,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
response(200, result, "Success", res);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
response(500, null, "Internal Server Error", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const learningHistoryBySectionId = async (req, res) => {
|
||||||
|
try {
|
||||||
|
if (!req.user) {
|
||||||
|
return response(401, null, "User not authenticated", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { ID } = req.user;
|
||||||
|
const { sectionId } = req.params;
|
||||||
|
|
||||||
|
const stdLearnings = await models.StdLearning.findAll({
|
||||||
|
where: {
|
||||||
|
ID: ID,
|
||||||
|
},
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: models.Level,
|
||||||
|
as: "level",
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: models.Topic,
|
||||||
|
as: "levelTopic",
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: models.Section,
|
||||||
|
as: "topicSection",
|
||||||
|
where: { ID_SECTION: sectionId },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!stdLearnings.length) {
|
||||||
|
return response(
|
||||||
|
404,
|
||||||
|
null,
|
||||||
|
"No learning history found for the specified section",
|
||||||
|
res
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await Promise.all(
|
||||||
|
stdLearnings.map(async (learning) => {
|
||||||
|
let nextLevelName = null;
|
||||||
|
if (learning.NEXT_LEARNING) {
|
||||||
|
const nextLevel = await models.Level.findOne({
|
||||||
|
where: { ID_LEVEL: learning.NEXT_LEARNING },
|
||||||
|
});
|
||||||
|
if (nextLevel) {
|
||||||
|
nextLevelName = nextLevel.NAME_LEVEL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
SCORE: learning.SCORE,
|
||||||
|
CURRENT_LEVEL: learning.level.NAME_LEVEL,
|
||||||
|
NEXT_LEVEL: nextLevelName,
|
||||||
|
STUDENT_FINISH: learning.STUDENT_FINISH,
|
||||||
|
TOPIC_NAME: learning.level.levelTopic.NAME_TOPIC,
|
||||||
|
SECTION_NAME: learning.level.levelTopic.topicSection.NAME_SECTION,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
response(200, result, "Success", res);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
response(500, null, "Internal Server Error", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const learningHistoryByTopicId = async (req, res) => {
|
||||||
|
try {
|
||||||
|
if (!req.user) {
|
||||||
|
return response(401, null, "User not authenticated", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { ID } = req.user;
|
||||||
|
const { topicId } = req.params;
|
||||||
|
|
||||||
|
if (!topicId) {
|
||||||
|
return response(400, null, "Topic ID is required", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const stdLearnings = await models.StdLearning.findAll({
|
||||||
|
where: {
|
||||||
|
ID: ID,
|
||||||
|
},
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: models.Level,
|
||||||
|
as: "level",
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: models.Topic,
|
||||||
|
as: "levelTopic",
|
||||||
|
where: { ID_TOPIC: topicId },
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: models.Section,
|
||||||
|
as: "topicSection",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await Promise.all(
|
||||||
|
stdLearnings.map(async (learning) => {
|
||||||
|
let nextLevelName = null;
|
||||||
|
if (learning.NEXT_LEARNING) {
|
||||||
|
const nextLevel = await models.Level.findOne({
|
||||||
|
where: { ID_LEVEL: learning.NEXT_LEARNING },
|
||||||
|
});
|
||||||
|
if (nextLevel) {
|
||||||
|
nextLevelName = nextLevel.NAME_LEVEL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
SCORE: learning.SCORE,
|
||||||
|
CURRENT_LEVEL: learning.level.NAME_LEVEL,
|
||||||
|
NEXT_LEVEL: nextLevelName,
|
||||||
|
STUDENT_FINISH: learning.STUDENT_FINISH,
|
||||||
|
TOPIC_NAME: learning.level.levelTopic.NAME_TOPIC,
|
||||||
|
SECTION_NAME: learning.level.levelTopic.topicSection.NAME_SECTION,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
response(200, result, "Success", res);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
response(500, null, "Internal Server Error", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -1,397 +0,0 @@
|
||||||
import response from "../response.js";
|
|
||||||
import models from "../models/index.js";
|
|
||||||
import {
|
|
||||||
clearFileBuffers,
|
|
||||||
saveFileToDisk,
|
|
||||||
generateHash,
|
|
||||||
} from "../middlewares/uploadLevel.js";
|
|
||||||
import fs from "fs";
|
|
||||||
import path from "path";
|
|
||||||
import crypto from "crypto";
|
|
||||||
|
|
||||||
export const getAllLevels = async (req, res) => {
|
|
||||||
try {
|
|
||||||
const levels = await models.Level.findAll();
|
|
||||||
response(200, levels, "Success", res);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
response(500, null, "Error retrieving levels data!", res);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getAllLevelById = async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { id } = req.params;
|
|
||||||
const level = await models.Level.findByPk(id);
|
|
||||||
|
|
||||||
if (!level) {
|
|
||||||
return response(404, null, "Level not found", res);
|
|
||||||
}
|
|
||||||
|
|
||||||
response(200, level, "Success", res);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
res.status(500).json({ message: "Internal Server Error" });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getLevels = async (req, res) => {
|
|
||||||
try {
|
|
||||||
const levels = await models.Level.findAll({
|
|
||||||
attributes: {
|
|
||||||
exclude: ["route1", "route2", "route3", "route4"],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
response(200, levels, "Success", res);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
res.status(500).json({ message: "Internal Server Error" });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getLevelById = async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { id } = req.params;
|
|
||||||
const level = await models.Level.findByPk(id, {
|
|
||||||
attributes: {
|
|
||||||
exclude: ["route1", "route2", "route3", "route4"],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!level) {
|
|
||||||
return response(404, null, "Level not found", res);
|
|
||||||
}
|
|
||||||
|
|
||||||
response(200, level, "Success", res);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
res.status(500).json({ message: "Internal Server Error" });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createLevel = async (req, res) => {
|
|
||||||
const { title, subject_id, topic_id, is_pretest, content, youtube } =
|
|
||||||
req.body;
|
|
||||||
|
|
||||||
// Files to be saved if everything else is okay
|
|
||||||
const { video, audio, image } = req.filesToSave || {};
|
|
||||||
|
|
||||||
// Validate title
|
|
||||||
if (!title) {
|
|
||||||
clearFileBuffers({ video, audio, image });
|
|
||||||
return response(400, null, "Title is required", res);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate subject_id
|
|
||||||
if (!subject_id) {
|
|
||||||
clearFileBuffers({ video, audio, image });
|
|
||||||
return response(400, null, "Subject ID is required", res);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate topic_id
|
|
||||||
if (!topic_id) {
|
|
||||||
clearFileBuffers({ video, audio, image });
|
|
||||||
return response(400, null, "Topic ID is required", res);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Check if the title already exists under the same topic_id
|
|
||||||
const existingLevel = await models.Level.findOne({
|
|
||||||
where: { title, topic_id },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (existingLevel) {
|
|
||||||
clearFileBuffers({ video, audio, image });
|
|
||||||
return response(
|
|
||||||
409,
|
|
||||||
null,
|
|
||||||
"A level with this title already exists under this topic",
|
|
||||||
res
|
|
||||||
); // 409 Conflict
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save files to disk
|
|
||||||
const videoFilename = video
|
|
||||||
? saveFileToDisk(video, "video", title, topic_id, subject_id)
|
|
||||||
: null;
|
|
||||||
const audioFilename = audio
|
|
||||||
? saveFileToDisk(audio, "audio", title, topic_id, subject_id)
|
|
||||||
: null;
|
|
||||||
const imageFilename = image
|
|
||||||
? saveFileToDisk(image, "image", title, topic_id, subject_id)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
// Create the new level
|
|
||||||
const newLevel = await models.Level.create({
|
|
||||||
title,
|
|
||||||
subject_id,
|
|
||||||
topic_id,
|
|
||||||
is_pretest: is_pretest || 0,
|
|
||||||
content,
|
|
||||||
video: videoFilename,
|
|
||||||
audio: audioFilename,
|
|
||||||
image: imageFilename,
|
|
||||||
youtube,
|
|
||||||
route1: 0,
|
|
||||||
route2: 0,
|
|
||||||
route3: 0,
|
|
||||||
route4: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update routes with the newly created level's ID
|
|
||||||
await newLevel.update({
|
|
||||||
route1: newLevel.id,
|
|
||||||
route2: newLevel.id,
|
|
||||||
route3: newLevel.id,
|
|
||||||
route4: newLevel.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
response(201, newLevel, "Level created successfully", res);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
clearFileBuffers({ video, audio, image });
|
|
||||||
return response(500, null, "Internal Server Error", res);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateLevelById = async (req, res) => {
|
|
||||||
const { id } = req.params;
|
|
||||||
const { title, subject_id, topic_id, is_pretest, content, youtube } =
|
|
||||||
req.body;
|
|
||||||
|
|
||||||
// Files to be saved if everything else is okay
|
|
||||||
const { video, audio, image } = req.filesToSave || {};
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Find the existing level by ID
|
|
||||||
const level = await models.Level.findByPk(id);
|
|
||||||
|
|
||||||
if (!level) {
|
|
||||||
clearFileBuffers({ video, audio, image });
|
|
||||||
return response(404, null, "Level not found", res);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if a level with the same title under the same topic already exists
|
|
||||||
if (title && topic_id) {
|
|
||||||
const existingLevel = await models.Level.findOne({
|
|
||||||
where: {
|
|
||||||
title,
|
|
||||||
topic_id,
|
|
||||||
id: { [models.Sequelize.Op.ne]: id }, // Exclude the current level from the check
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (existingLevel) {
|
|
||||||
clearFileBuffers({ video, audio, image });
|
|
||||||
return response(
|
|
||||||
409,
|
|
||||||
null,
|
|
||||||
"A level with this title already exists under this topic",
|
|
||||||
res
|
|
||||||
); // 409 Conflict
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update level fields
|
|
||||||
if (title) level.title = title;
|
|
||||||
if (subject_id) level.subject_id = subject_id;
|
|
||||||
if (topic_id) level.topic_id = topic_id;
|
|
||||||
if (is_pretest !== undefined) level.is_pretest = is_pretest;
|
|
||||||
if (content) level.content = content;
|
|
||||||
if (youtube) level.youtube = youtube;
|
|
||||||
|
|
||||||
// Handle video update
|
|
||||||
if (video) {
|
|
||||||
if (level.video) {
|
|
||||||
const oldVideoPath = path.join(
|
|
||||||
"public/uploads/level/video",
|
|
||||||
level.video
|
|
||||||
);
|
|
||||||
if (fs.existsSync(oldVideoPath)) {
|
|
||||||
fs.unlinkSync(oldVideoPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
level.video = saveFileToDisk(
|
|
||||||
video,
|
|
||||||
"video",
|
|
||||||
title || level.title,
|
|
||||||
topic_id || level.topic_id,
|
|
||||||
subject_id || level.subject_id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle audio update
|
|
||||||
if (audio) {
|
|
||||||
if (level.audio) {
|
|
||||||
const oldAudioPath = path.join(
|
|
||||||
"public/uploads/level/audio",
|
|
||||||
level.audio
|
|
||||||
);
|
|
||||||
if (fs.existsSync(oldAudioPath)) {
|
|
||||||
fs.unlinkSync(oldAudioPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
level.audio = saveFileToDisk(
|
|
||||||
audio,
|
|
||||||
"audio",
|
|
||||||
title || level.title,
|
|
||||||
topic_id || level.topic_id,
|
|
||||||
subject_id || level.subject_id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle image update
|
|
||||||
if (image) {
|
|
||||||
if (level.image) {
|
|
||||||
const oldImagePath = path.join(
|
|
||||||
"public/uploads/level/image",
|
|
||||||
level.image
|
|
||||||
);
|
|
||||||
if (fs.existsSync(oldImagePath)) {
|
|
||||||
fs.unlinkSync(oldImagePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
level.image = saveFileToDisk(
|
|
||||||
image,
|
|
||||||
"image",
|
|
||||||
title || level.title,
|
|
||||||
topic_id || level.topic_id,
|
|
||||||
subject_id || level.subject_id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await level.save();
|
|
||||||
|
|
||||||
response(200, level, "Level updated successfully", res);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
clearFileBuffers({ video, audio, image });
|
|
||||||
return response(500, null, "Internal Server Error", res);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deleteLevelById = async (req, res) => {
|
|
||||||
const { id } = req.params;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Find the existing level by ID
|
|
||||||
const level = await models.Level.findByPk(id);
|
|
||||||
|
|
||||||
if (!level) {
|
|
||||||
return response(404, null, "Level not found", res);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete associated files from disk if they exist
|
|
||||||
const deleteFile = (filePath) => {
|
|
||||||
if (fs.existsSync(filePath)) {
|
|
||||||
fs.unlinkSync(filePath);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (level.video) {
|
|
||||||
const videoPath = path.join("public/uploads/level/video", level.video);
|
|
||||||
deleteFile(videoPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (level.audio) {
|
|
||||||
const audioPath = path.join("public/uploads/level/audio", level.audio);
|
|
||||||
deleteFile(audioPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (level.image) {
|
|
||||||
const imagePath = path.join("public/uploads/level/image", level.image);
|
|
||||||
deleteFile(imagePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete the level from the database
|
|
||||||
await level.destroy();
|
|
||||||
|
|
||||||
response(200, null, "Level deleted successfully", res);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
return response(500, null, "Internal Server Error", res);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getRoutes = async (req, res) => {
|
|
||||||
try {
|
|
||||||
const levels = await models.Level.findAll({
|
|
||||||
attributes: {
|
|
||||||
exclude: [
|
|
||||||
"subject_id",
|
|
||||||
"topic_id",
|
|
||||||
"is_pretest",
|
|
||||||
"content",
|
|
||||||
"video",
|
|
||||||
"audio",
|
|
||||||
"image",
|
|
||||||
"youtube",
|
|
||||||
"ts_entri",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
response(200, levels, "Success", res);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
res.status(500).json({ message: "Internal Server Error" });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getRouteById = async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { id } = req.params;
|
|
||||||
const level = await models.Level.findByPk(id, {
|
|
||||||
attributes: {
|
|
||||||
exclude: [
|
|
||||||
"subject_id",
|
|
||||||
"topic_id",
|
|
||||||
"is_pretest",
|
|
||||||
"content",
|
|
||||||
"video",
|
|
||||||
"audio",
|
|
||||||
"image",
|
|
||||||
"youtube",
|
|
||||||
"ts_entri",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!level) {
|
|
||||||
return response(404, null, "Level not found", res);
|
|
||||||
}
|
|
||||||
|
|
||||||
response(200, level, "Success", res);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
res.status(500).json({ message: "Internal Server Error" });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateRouteById = async (req, res) => {
|
|
||||||
const { id } = req.params;
|
|
||||||
const { route1, route2, route3, route4 } = req.body;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Find the existing level by ID
|
|
||||||
const level = await models.Level.findByPk(id);
|
|
||||||
|
|
||||||
if (!level) {
|
|
||||||
return response(404, null, "Level not found", res);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update only the route fields
|
|
||||||
await level.update({
|
|
||||||
route1: route1 !== undefined ? route1 : level.route1,
|
|
||||||
route2: route2 !== undefined ? route2 : level.route2,
|
|
||||||
route3: route3 !== undefined ? route3 : level.route3,
|
|
||||||
route4: route4 !== undefined ? route4 : level.route4,
|
|
||||||
});
|
|
||||||
|
|
||||||
response(200, level, "Routes updated successfully", res);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
return response(500, null, "Internal Server Error", res);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
231
controllers/monitoringControllers/class.js
Normal file
231
controllers/monitoringControllers/class.js
Normal file
|
|
@ -0,0 +1,231 @@
|
||||||
|
import response from "../../response.js";
|
||||||
|
import models from "../../models/index.js";
|
||||||
|
import { updateMonitoringClass } from "./monitoring.js";
|
||||||
|
|
||||||
|
export const getClasses = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const classes = await models.Class.findAll();
|
||||||
|
response(200, classes, "Success", res);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
response(500, null, "Error retrieving classes data!", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getClassById = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const classes = await models.Class.findByPk(id);
|
||||||
|
|
||||||
|
if (!classes) {
|
||||||
|
return response(404, null, "Class not found", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
response(200, classes, "Success", res);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
response(500, null, "Internal Server Error", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createClass = async (req, res) => {
|
||||||
|
const { NAME_CLASS, TOTAL_STUDENT } = req.body;
|
||||||
|
|
||||||
|
if (!NAME_CLASS) {
|
||||||
|
return response(400, null, "Class name is required", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TOTAL_STUDENT) {
|
||||||
|
return response(400, null, "Total student is required", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newClass = await models.Class.create({
|
||||||
|
NAME_CLASS,
|
||||||
|
TOTAL_STUDENT,
|
||||||
|
});
|
||||||
|
|
||||||
|
response(201, newClass, "Class created successfully", res);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
response(500, null, "Internal Server Error", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateClassById = async (req, res) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
const { NAME_CLASS, TOTAL_STUDENT } = req.body;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const classes = await models.Class.findByPk(id);
|
||||||
|
|
||||||
|
if (!classes) {
|
||||||
|
return response(404, null, "Class not found", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (NAME_CLASS) classes.NAME_CLASS = NAME_CLASS;
|
||||||
|
if (TOTAL_STUDENT) classes.TOTAL_STUDENT = TOTAL_STUDENT;
|
||||||
|
|
||||||
|
await classes.save();
|
||||||
|
|
||||||
|
response(200, classes, "Class updated successfully", res);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
response(500, null, "Internal Server Error", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteClassById = async (req, res) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const classes = await models.Class.findByPk(id);
|
||||||
|
|
||||||
|
if (!classes) {
|
||||||
|
return response(404, null, "Class not found", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
await classes.destroy();
|
||||||
|
|
||||||
|
response(200, null, "Class deleted successfully", res);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
response(500, null, "Internal Server Error", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateStudentClassByName = async (req, res) => {
|
||||||
|
const { NAME_CLASS, STUDENTS } = req.body;
|
||||||
|
|
||||||
|
if (!NAME_CLASS || !Array.isArray(STUDENTS) || STUDENTS.length === 0) {
|
||||||
|
return response(
|
||||||
|
400,
|
||||||
|
null,
|
||||||
|
"Class name and students data are required",
|
||||||
|
res
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const classRecord = await models.Class.findOne({
|
||||||
|
where: { NAME_CLASS },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!classRecord) {
|
||||||
|
return response(404, null, "Class not found", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateResults = [];
|
||||||
|
let hasError = false;
|
||||||
|
|
||||||
|
for (const { NAME_USERS, NISN } of STUDENTS) {
|
||||||
|
if (!NAME_USERS || !NISN) {
|
||||||
|
updateResults.push({
|
||||||
|
NAME_USERS,
|
||||||
|
NISN,
|
||||||
|
error: "User name and NISN are required for each student",
|
||||||
|
});
|
||||||
|
hasError = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const student = await models.Student.findOne({
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: models.User,
|
||||||
|
as: "studentUser",
|
||||||
|
where: { NAME_USERS },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
where: { NISN },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!student) {
|
||||||
|
updateResults.push({
|
||||||
|
NAME_USERS,
|
||||||
|
NISN,
|
||||||
|
error: "Student with the given name and NISN not found",
|
||||||
|
});
|
||||||
|
hasError = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (student.ID_CLASS === classRecord.ID_CLASS) {
|
||||||
|
updateResults.push({
|
||||||
|
NAME_USERS,
|
||||||
|
NISN,
|
||||||
|
error: "Student is already in the selected class",
|
||||||
|
});
|
||||||
|
hasError = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
student.ID_CLASS = classRecord.ID_CLASS;
|
||||||
|
await student.save();
|
||||||
|
|
||||||
|
const stdLearningRecords = await models.StdLearning.findAll({
|
||||||
|
where: { ID: student.ID },
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: models.Level,
|
||||||
|
as: "level",
|
||||||
|
where: { NAME_LEVEL: "Level 6" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const studentUpdateResults = [];
|
||||||
|
|
||||||
|
for (const stdLearning of stdLearningRecords) {
|
||||||
|
try {
|
||||||
|
const result = await updateMonitoringClass({
|
||||||
|
ID_STUDENT_LEARNING: stdLearning.ID_STUDENT_LEARNING,
|
||||||
|
ID_CLASS: classRecord.ID_CLASS,
|
||||||
|
});
|
||||||
|
studentUpdateResults.push(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating monitoring class:", error.message);
|
||||||
|
studentUpdateResults.push({ error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateResults.push({
|
||||||
|
NAME_USERS,
|
||||||
|
NISN,
|
||||||
|
message:
|
||||||
|
"Student's class and related monitoring updated successfully",
|
||||||
|
studentUpdateResults,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error processing student:", error.message);
|
||||||
|
updateResults.push({
|
||||||
|
NAME_USERS,
|
||||||
|
NISN,
|
||||||
|
error: "Error processing student",
|
||||||
|
details: error.message,
|
||||||
|
});
|
||||||
|
hasError = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasError) {
|
||||||
|
return response(
|
||||||
|
400,
|
||||||
|
{ updateResults },
|
||||||
|
"Some students could not be updated due to errors",
|
||||||
|
res
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return response(
|
||||||
|
200,
|
||||||
|
{ updateResults },
|
||||||
|
"Students classes updated successfully",
|
||||||
|
res
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
response(500, null, "Internal Server Error", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
347
controllers/monitoringControllers/monitoring.js
Normal file
347
controllers/monitoringControllers/monitoring.js
Normal file
|
|
@ -0,0 +1,347 @@
|
||||||
|
import response from "../../response.js";
|
||||||
|
import models from "../../models/index.js";
|
||||||
|
|
||||||
|
export const getMonitorings = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const monitoring = await models.Monitoring.findAll();
|
||||||
|
response(200, monitoring, "Success", res);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
response(500, null, "Error retrieving monitoring data!", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMonitoringById = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const monitoring = await models.Monitoring.findByPk(id);
|
||||||
|
|
||||||
|
if (!monitoring) {
|
||||||
|
return response(404, null, "Monitoring data not found", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
response(200, monitoring, "Success", res);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
response(500, null, "Internal Server Error", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createMonitoring = async (req) => {
|
||||||
|
const { ID_STUDENT_LEARNING } = req.body;
|
||||||
|
|
||||||
|
if (!req.user) {
|
||||||
|
throw new Error("User not authenticated");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ID_STUDENT_LEARNING) {
|
||||||
|
throw new Error("Student Learning ID is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const existingMonitoring = await models.Monitoring.findOne({
|
||||||
|
where: { ID_STUDENT_LEARNING: ID_STUDENT_LEARNING },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingMonitoring) {
|
||||||
|
return existingMonitoring.toJSON();
|
||||||
|
}
|
||||||
|
|
||||||
|
const stdLearning = await models.StdLearning.findByPk(ID_STUDENT_LEARNING);
|
||||||
|
|
||||||
|
if (!stdLearning) {
|
||||||
|
throw new Error("Student learning data not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const userID = stdLearning.ID;
|
||||||
|
|
||||||
|
const student = await models.Student.findOne({ where: { ID: userID } });
|
||||||
|
|
||||||
|
if (!student) {
|
||||||
|
throw new Error("Student data not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const { ID_SISWA } = student;
|
||||||
|
|
||||||
|
const studentClass = await models.Student.findOne({
|
||||||
|
where: { ID_SISWA: ID_SISWA },
|
||||||
|
attributes: ["ID_CLASS"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const ID_CLASS =
|
||||||
|
studentClass && studentClass.ID_CLASS ? studentClass.ID_CLASS : null;
|
||||||
|
|
||||||
|
const newMonitoring = await models.Monitoring.create({
|
||||||
|
ID_STUDENT_LEARNING,
|
||||||
|
ID_CLASS,
|
||||||
|
});
|
||||||
|
|
||||||
|
return newMonitoring.toJSON();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
throw new Error("Internal Server Error");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateMonitoringClass = async ({
|
||||||
|
ID_STUDENT_LEARNING,
|
||||||
|
ID_CLASS,
|
||||||
|
}) => {
|
||||||
|
if (!ID_STUDENT_LEARNING || !ID_CLASS) {
|
||||||
|
throw new Error("Student Learning ID and Class ID are required");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const monitoring = await models.Monitoring.findOne({
|
||||||
|
where: { ID_STUDENT_LEARNING },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!monitoring) {
|
||||||
|
throw new Error("Monitoring data not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
monitoring.ID_CLASS = ID_CLASS;
|
||||||
|
await monitoring.save();
|
||||||
|
|
||||||
|
return monitoring;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const monitoringStudentsProgress = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const result = await models.Monitoring.findAll({
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: models.StdLearning,
|
||||||
|
as: "stdLearningMonitoring",
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: models.User,
|
||||||
|
as: "learningUser",
|
||||||
|
attributes: ["NAME_USERS"],
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: models.Student,
|
||||||
|
as: "students",
|
||||||
|
attributes: ["NISN"],
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: models.Class,
|
||||||
|
as: "studentClass",
|
||||||
|
attributes: ["NAME_CLASS"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: models.Level,
|
||||||
|
as: "level",
|
||||||
|
attributes: ["NAME_LEVEL"],
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: models.Topic,
|
||||||
|
as: "levelTopic",
|
||||||
|
attributes: ["NAME_TOPIC"],
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: models.Section,
|
||||||
|
as: "topicSection",
|
||||||
|
attributes: ["NAME_SECTION"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const formattedResult = result.map((monitoring) => {
|
||||||
|
return {
|
||||||
|
ID_MONITORING: monitoring.ID_MONITORING,
|
||||||
|
NISN: monitoring.stdLearningMonitoring?.learningUser?.students?.NISN,
|
||||||
|
NAME_USERS: monitoring.stdLearningMonitoring?.learningUser?.NAME_USERS,
|
||||||
|
NAME_SECTION:
|
||||||
|
monitoring.stdLearningMonitoring?.level?.levelTopic?.topicSection
|
||||||
|
?.NAME_SECTION,
|
||||||
|
NAME_TOPIC:
|
||||||
|
monitoring.stdLearningMonitoring?.level?.levelTopic?.NAME_TOPIC,
|
||||||
|
NAME_CLASS:
|
||||||
|
monitoring.stdLearningMonitoring?.learningUser?.students?.studentClass
|
||||||
|
?.NAME_CLASS ?? null,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
response(
|
||||||
|
200,
|
||||||
|
formattedResult,
|
||||||
|
"Success retrieving student monitoring progress!",
|
||||||
|
res
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
response(500, null, "Error retrieving student monitoring progress!", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const monitoringStudentProgressById = async (req, res) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const monitoring = await models.Monitoring.findOne({
|
||||||
|
where: { ID_MONITORING: id },
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: models.StdLearning,
|
||||||
|
as: "stdLearningMonitoring",
|
||||||
|
attributes: [
|
||||||
|
"ID",
|
||||||
|
"ID_LEVEL",
|
||||||
|
"SCORE",
|
||||||
|
"FEEDBACK_STUDENT",
|
||||||
|
"STUDENT_START",
|
||||||
|
"STUDENT_FINISH",
|
||||||
|
"ID_STUDENT_LEARNING",
|
||||||
|
],
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: models.Level,
|
||||||
|
as: "level",
|
||||||
|
attributes: ["ID_TOPIC", "NAME_LEVEL"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!monitoring) {
|
||||||
|
return response(404, null, "Monitoring data not found!", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const stdLearning = monitoring.stdLearningMonitoring;
|
||||||
|
|
||||||
|
if (!stdLearning || stdLearning.length === 0) {
|
||||||
|
return response(404, null, "No student learning data found!", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { ID } = stdLearning;
|
||||||
|
|
||||||
|
const levels = await models.StdLearning.findAll({
|
||||||
|
where: {
|
||||||
|
ID,
|
||||||
|
},
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: models.Level,
|
||||||
|
as: "level",
|
||||||
|
attributes: ["NAME_LEVEL", "ID_TOPIC"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
attributes: [
|
||||||
|
"SCORE",
|
||||||
|
"FEEDBACK_STUDENT",
|
||||||
|
"STUDENT_START",
|
||||||
|
"STUDENT_FINISH",
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const levelArray = levels.map((learning) => ({
|
||||||
|
NAME_LEVEL: learning.level.NAME_LEVEL,
|
||||||
|
SCORE: learning.SCORE,
|
||||||
|
FEEDBACK_STUDENT: learning.FEEDBACK_STUDENT,
|
||||||
|
STUDENT_START: learning.STUDENT_START,
|
||||||
|
STUDENT_FINISH: learning.STUDENT_FINISH,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
ID_MONITORING: monitoring.ID_MONITORING,
|
||||||
|
levels: levelArray,
|
||||||
|
};
|
||||||
|
|
||||||
|
response(200, result, "Success retrieving student progress!", res);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
response(500, null, "Error retrieving student progress!", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const monitoringFeedback = async (req, res) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
const { feedback } = req.body;
|
||||||
|
|
||||||
|
if (!req.user) {
|
||||||
|
return response(401, null, "User not authenticated", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { ID } = req.user;
|
||||||
|
|
||||||
|
if (!ID) {
|
||||||
|
return response(401, null, "Unauthorized: User ID not provided", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const teacher = await models.Teacher.findOne({
|
||||||
|
where: { ID: ID },
|
||||||
|
attributes: ["ID_GURU"],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!teacher) {
|
||||||
|
return response(404, null, "Teacher not found!", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [updatedRows] = await models.Monitoring.update(
|
||||||
|
{
|
||||||
|
ID_GURU: teacher.ID_GURU,
|
||||||
|
FEEDBACK_GURU: feedback,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
where: { ID_MONITORING: id },
|
||||||
|
returning: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (updatedRows === 0) {
|
||||||
|
return response(404, null, "Monitoring data not found!", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const monitoringWithRelations = await models.Monitoring.findOne({
|
||||||
|
where: { ID_MONITORING: id },
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: models.Teacher,
|
||||||
|
as: "monitoringTeacher",
|
||||||
|
attributes: ["ID_GURU"],
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: models.User,
|
||||||
|
as: "teacherUser",
|
||||||
|
attributes: ["NAME_USERS"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!monitoringWithRelations) {
|
||||||
|
return response(404, null, "Updated monitoring data not found!", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
ID_MONITORING: monitoringWithRelations.ID_MONITORING,
|
||||||
|
FEEDBACK_GURU: monitoringWithRelations.FEEDBACK_GURU,
|
||||||
|
ID_GURU: monitoringWithRelations.monitoringTeacher?.ID_GURU,
|
||||||
|
TEACHER_NAME:
|
||||||
|
monitoringWithRelations.monitoringTeacher?.teacherUser?.NAME_USERS,
|
||||||
|
};
|
||||||
|
|
||||||
|
response(200, result, "Success updating teacher feedback!", res);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in monitoringFeedback:", error);
|
||||||
|
response(500, null, "Error updating teacher feedback!", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -1,167 +0,0 @@
|
||||||
import response from "../response.js";
|
|
||||||
import models from "../models/index.js";
|
|
||||||
import fs from "fs";
|
|
||||||
import path from "path";
|
|
||||||
import {
|
|
||||||
clearFileBuffers,
|
|
||||||
saveFileToDisk,
|
|
||||||
} from "../middlewares/uploadSubject.js";
|
|
||||||
|
|
||||||
export const getSubjects = async (req, res) => {
|
|
||||||
try {
|
|
||||||
const subjects = await models.Subject.findAll();
|
|
||||||
response(200, subjects, "Success", res);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
response(500, null, "Error retrieving subjects data!", res);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getSubjectById = async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { id } = req.params;
|
|
||||||
const subject = await models.Subject.findByPk(id);
|
|
||||||
|
|
||||||
if (!subject) {
|
|
||||||
return response(404, null, "Subject not found", res);
|
|
||||||
}
|
|
||||||
|
|
||||||
response(200, subject, "Success", res);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
res.status(500).json({ message: "Internal Server Error" });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createSubject = async (req, res) => {
|
|
||||||
const { name, description } = req.body;
|
|
||||||
|
|
||||||
// Files to be saved if everything else is okay
|
|
||||||
const { icon, thumbnail } = req.filesToSave || {};
|
|
||||||
|
|
||||||
// Validate name
|
|
||||||
if (!name) {
|
|
||||||
clearFileBuffers({ icon, thumbnail });
|
|
||||||
return response(400, null, "Name is required", res);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate description
|
|
||||||
if (!description) {
|
|
||||||
clearFileBuffers({ icon, thumbnail });
|
|
||||||
return response(400, null, "Description is required", res);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const iconFilename = icon
|
|
||||||
? saveFileToDisk(icon, `${name}-icon`, name)
|
|
||||||
: null;
|
|
||||||
const thumbnailFilename = thumbnail
|
|
||||||
? saveFileToDisk(thumbnail, `${name}-thumbnail`, name)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const newSubject = await models.Subject.create({
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
icon: iconFilename,
|
|
||||||
thumbnail: thumbnailFilename,
|
|
||||||
});
|
|
||||||
|
|
||||||
response(201, newSubject, "Subject created successfully", res);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
clearFileBuffers({ icon, thumbnail });
|
|
||||||
res.status(500).json({ message: "Internal Server Error" });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateSubjectById = async (req, res) => {
|
|
||||||
const { id } = req.params;
|
|
||||||
const { name, description } = req.body;
|
|
||||||
|
|
||||||
// Files to be saved if everything else is okay
|
|
||||||
const { icon, thumbnail } = req.filesToSave || {};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const subject = await models.Subject.findByPk(id);
|
|
||||||
|
|
||||||
if (!subject) {
|
|
||||||
clearFileBuffers({ icon, thumbnail });
|
|
||||||
return response(404, null, "Subject not found", res);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update subject fields
|
|
||||||
if (name) subject.name = name;
|
|
||||||
if (description) subject.description = description;
|
|
||||||
|
|
||||||
// Handle icon update
|
|
||||||
if (icon) {
|
|
||||||
if (subject.icon) {
|
|
||||||
const oldIconPath = path.join("public/uploads/subject", subject.icon);
|
|
||||||
if (fs.existsSync(oldIconPath)) {
|
|
||||||
fs.unlinkSync(oldIconPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
subject.icon = saveFileToDisk(icon, `${name}-icon`, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle thumbnail update
|
|
||||||
if (thumbnail) {
|
|
||||||
if (subject.thumbnail) {
|
|
||||||
const oldThumbnailPath = path.join(
|
|
||||||
"public/uploads/subject",
|
|
||||||
subject.thumbnail
|
|
||||||
);
|
|
||||||
if (fs.existsSync(oldThumbnailPath)) {
|
|
||||||
fs.unlinkSync(oldThumbnailPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
subject.thumbnail = saveFileToDisk(thumbnail, `${name}-thumbnail`, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
await subject.save();
|
|
||||||
|
|
||||||
response(200, subject, "Subject updated successfully", res);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
clearFileBuffers({ icon, thumbnail });
|
|
||||||
response(500, null, "Internal Server Error", res);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deleteSubjectById = async (req, res) => {
|
|
||||||
const { id } = req.params;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const subject = await models.Subject.findByPk(id);
|
|
||||||
|
|
||||||
if (!subject) {
|
|
||||||
return response(404, null, "Subject not found", res);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove associated icon if it exists
|
|
||||||
if (subject.icon) {
|
|
||||||
const iconPath = path.join("public/uploads/subject", subject.icon);
|
|
||||||
if (fs.existsSync(iconPath)) {
|
|
||||||
fs.unlinkSync(iconPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove associated thumbnail if it exists
|
|
||||||
if (subject.thumbnail) {
|
|
||||||
const thumbnailPath = path.join(
|
|
||||||
"public/uploads/subject",
|
|
||||||
subject.thumbnail
|
|
||||||
);
|
|
||||||
if (fs.existsSync(thumbnailPath)) {
|
|
||||||
fs.unlinkSync(thumbnailPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await subject.destroy();
|
|
||||||
|
|
||||||
response(200, null, "Subject deleted successfully", res);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
response(500, null, "Internal Server Error", res);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
@ -1,102 +0,0 @@
|
||||||
import response from "../response.js";
|
|
||||||
import models from "../models/index.js";
|
|
||||||
import bcrypt from "bcrypt";
|
|
||||||
|
|
||||||
export const getUsers = async (req, res) => {
|
|
||||||
try {
|
|
||||||
const users = await models.User.findAll({
|
|
||||||
attributes: {
|
|
||||||
exclude: ["password"],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
response(200, users, "Success", res);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
response(500, null, "Error retrieving users data!", res);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getUserById = async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { id } = req.params;
|
|
||||||
const user = await models.User.findByPk(id, {
|
|
||||||
attributes: {
|
|
||||||
exclude: ["password"],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return response(404, null, "User not found", res);
|
|
||||||
}
|
|
||||||
|
|
||||||
response(200, user, "Success", res);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
res.status(500).json({ message: "Internal Server Error" });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateUserById = async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { id } = req.params;
|
|
||||||
const { name, email, password, roles } = req.body;
|
|
||||||
|
|
||||||
// Find the user by ID
|
|
||||||
const user = await models.User.findByPk(id);
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return response(404, null, "User not found", res);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the email is unique if it is being updated
|
|
||||||
if (email && email !== user.email) {
|
|
||||||
const emailExists = await models.User.findOne({ where: { email } });
|
|
||||||
if (emailExists) {
|
|
||||||
return response(400, null, "Email already in use", res);
|
|
||||||
}
|
|
||||||
user.email = email;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hash the password if it is being updated
|
|
||||||
if (password) {
|
|
||||||
const salt = await bcrypt.genSalt(10);
|
|
||||||
user.password = await bcrypt.hash(password, salt);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update other user information
|
|
||||||
user.name = name || user.name;
|
|
||||||
user.roles = roles || user.roles;
|
|
||||||
|
|
||||||
// Manually update the updated_at field
|
|
||||||
user.updated_at = new Date();
|
|
||||||
|
|
||||||
// Save the updated user information
|
|
||||||
await user.save();
|
|
||||||
|
|
||||||
response(200, user, "User updated successfully", res);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
res.status(500).json({ message: "Internal Server Error" });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deleteUserById = async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { id } = req.params;
|
|
||||||
|
|
||||||
// Find the user by ID
|
|
||||||
const user = await models.User.findByPk(id);
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return response(404, null, "User not found", res);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete the user
|
|
||||||
await user.destroy();
|
|
||||||
|
|
||||||
response(200, null, "User deleted successfully", res);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
res.status(500).json({ message: "Internal Server Error" });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
473
controllers/usersControllers/user.js
Normal file
473
controllers/usersControllers/user.js
Normal file
|
|
@ -0,0 +1,473 @@
|
||||||
|
import response from "../../response.js";
|
||||||
|
import models from "../../models/index.js";
|
||||||
|
import bcrypt from "bcrypt";
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import {
|
||||||
|
clearFileBuffers,
|
||||||
|
saveFileToDisk,
|
||||||
|
} from "../../middlewares/User/uploadUser.js";
|
||||||
|
|
||||||
|
export const getUsers = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const users = await models.User.findAll({
|
||||||
|
attributes: {
|
||||||
|
exclude: ["PASSWORD"],
|
||||||
|
include: [
|
||||||
|
[
|
||||||
|
models.Sequelize.literal(
|
||||||
|
`COALESCE(\`teachers\`.\`NIP\`, \`students\`.\`NISN\`)`
|
||||||
|
),
|
||||||
|
"NIP/NISN",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: models.Teacher,
|
||||||
|
as: "teachers",
|
||||||
|
attributes: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: models.Student,
|
||||||
|
as: "students",
|
||||||
|
attributes: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
response(200, users, "Success", res);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
response(500, null, "Error retrieving users data!", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAdmins = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const admins = await models.User.findAll({
|
||||||
|
where: {
|
||||||
|
ROLE: "admin",
|
||||||
|
},
|
||||||
|
attributes: {
|
||||||
|
exclude: ["PASSWORD"],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
response(200, admins, "Success", res);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
response(500, null, "Error retrieving admin data!", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTeachers = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const teachers = await models.User.findAll({
|
||||||
|
where: {
|
||||||
|
ROLE: "teacher",
|
||||||
|
},
|
||||||
|
attributes: ["ID", "NAME_USERS", "EMAIL", "ROLE"],
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: models.Teacher,
|
||||||
|
as: "teachers",
|
||||||
|
attributes: ["NIP"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
raw: true,
|
||||||
|
nest: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const formattedTeachers = teachers.map((teacher) => ({
|
||||||
|
ID: teacher.ID,
|
||||||
|
NAME_USERS: teacher.NAME_USERS,
|
||||||
|
EMAIL: teacher.EMAIL,
|
||||||
|
NISN: teacher.teachers.NIP,
|
||||||
|
ROLE: teacher.ROLE,
|
||||||
|
}));
|
||||||
|
|
||||||
|
response(200, formattedTeachers, "Success", res);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
response(500, null, "Error retrieving teacher data!", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getStudents = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const students = await models.User.findAll({
|
||||||
|
where: {
|
||||||
|
ROLE: "student",
|
||||||
|
},
|
||||||
|
attributes: ["ID", "NAME_USERS", "EMAIL", "ROLE"],
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: models.Student,
|
||||||
|
as: "students",
|
||||||
|
attributes: ["NISN"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
raw: true,
|
||||||
|
nest: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const formattedStudents = students.map((student) => ({
|
||||||
|
ID: student.ID,
|
||||||
|
NAME_USERS: student.NAME_USERS,
|
||||||
|
EMAIL: student.EMAIL,
|
||||||
|
NISN: student.students.NISN,
|
||||||
|
ROLE: student.ROLE,
|
||||||
|
}));
|
||||||
|
|
||||||
|
response(200, formattedStudents, "Success", res);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
response(500, null, "Error retrieving student data!", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getUserById = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
const user = await models.User.findByPk(id, {
|
||||||
|
attributes: {
|
||||||
|
exclude: ["PASSWORD"],
|
||||||
|
},
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: models.Teacher,
|
||||||
|
as: "teachers",
|
||||||
|
attributes: ["NIP"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: models.Student,
|
||||||
|
as: "students",
|
||||||
|
attributes: ["NISN"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return response(404, null, "User not found", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
let additionalField = null;
|
||||||
|
if (user.ROLE === "teacher") {
|
||||||
|
additionalField = { NIP: user.teachers.NIP };
|
||||||
|
} else if (user.ROLE === "student") {
|
||||||
|
additionalField = { NISN: user.students.NISN };
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseObject = {
|
||||||
|
ID: user.ID,
|
||||||
|
NAME_USERS: user.NAME_USERS,
|
||||||
|
EMAIL: user.EMAIL,
|
||||||
|
ROLE: user.ROLE,
|
||||||
|
...additionalField,
|
||||||
|
};
|
||||||
|
|
||||||
|
response(200, responseObject, "Success", res);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
response(500, null, "Error retrieving user data!", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getUserByName = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { name } = req.params;
|
||||||
|
|
||||||
|
const user = await models.User.findOne({
|
||||||
|
where: { NAME_USERS: name },
|
||||||
|
attributes: {
|
||||||
|
exclude: ["PASSWORD"],
|
||||||
|
},
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: models.Teacher,
|
||||||
|
as: "teachers",
|
||||||
|
attributes: ["NIP"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: models.Student,
|
||||||
|
as: "students",
|
||||||
|
attributes: ["NISN"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return response(404, null, "User not found", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
let additionalField = null;
|
||||||
|
if (user.ROLE === "teacher" ) {
|
||||||
|
additionalField = { NIP: user.teachers.NIP };
|
||||||
|
} else if (user.ROLE === "student") {
|
||||||
|
additionalField = { NISN: user.students.NISN };
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseObject = {
|
||||||
|
ID: user.ID,
|
||||||
|
NAME_USERS: user.NAME_USERS,
|
||||||
|
EMAIL: user.EMAIL,
|
||||||
|
ROLE: user.ROLE,
|
||||||
|
...additionalField,
|
||||||
|
};
|
||||||
|
|
||||||
|
return response(200, responseObject, "Success", res);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
return response(500, null, "Error retrieving user data!", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateUserById = async (req, res) => {
|
||||||
|
const transaction = await models.db.transaction();
|
||||||
|
|
||||||
|
const { picture } = req.filesToSave || {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const { name, email, nip, nisn } = req.body;
|
||||||
|
|
||||||
|
const user = await models.User.findByPk(id, {
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: models.Teacher,
|
||||||
|
as: "teachers",
|
||||||
|
attributes: ["NIP"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: models.Student,
|
||||||
|
as: "students",
|
||||||
|
attributes: ["NISN"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
clearFileBuffers({ picture });
|
||||||
|
await transaction.rollback();
|
||||||
|
return response(404, null, "User not found", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.ROLE === "teacher" && nisn) {
|
||||||
|
clearFileBuffers({ picture });
|
||||||
|
await transaction.rollback();
|
||||||
|
return response(400, null, "Role is teacher, but NISN is provided", res);
|
||||||
|
}
|
||||||
|
if (user.ROLE === "student" && nip) {
|
||||||
|
clearFileBuffers({ picture });
|
||||||
|
await transaction.rollback();
|
||||||
|
return response(400, null, "Role is student, but NIP is provided", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (email && email !== user.EMAIL) {
|
||||||
|
const emailExists = await models.User.findOne({
|
||||||
|
where: { EMAIL: email },
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
if (emailExists) {
|
||||||
|
clearFileBuffers({ picture });
|
||||||
|
await transaction.rollback();
|
||||||
|
return response(400, null, "Email already in use", res);
|
||||||
|
}
|
||||||
|
user.EMAIL = email;
|
||||||
|
}
|
||||||
|
|
||||||
|
user.NAME_USERS = name || user.NAME_USERS;
|
||||||
|
|
||||||
|
if (user.ROLE === "teacher" && nip) {
|
||||||
|
let teacher = await models.Teacher.findOne({
|
||||||
|
where: { ID: id },
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
if (teacher) {
|
||||||
|
teacher.NIP = nip;
|
||||||
|
await teacher.save({ transaction });
|
||||||
|
} else {
|
||||||
|
teacher = await models.Teacher.create(
|
||||||
|
{ ID: id, NIP: nip },
|
||||||
|
{ transaction }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.ROLE === "student" && nisn) {
|
||||||
|
let student = await models.Student.findOne({
|
||||||
|
where: { ID: id },
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
if (student) {
|
||||||
|
student.NISN = nisn;
|
||||||
|
await student.save({ transaction });
|
||||||
|
} else {
|
||||||
|
student = await models.Student.create(
|
||||||
|
{ ID: id, NISN: nisn },
|
||||||
|
{ transaction }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (picture) {
|
||||||
|
if (user.PICTURE) {
|
||||||
|
const oldPicturePath = path.join("public/uploads/avatar", user.PICTURE);
|
||||||
|
if (fs.existsSync(oldPicturePath)) {
|
||||||
|
fs.unlinkSync(oldPicturePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
user.PICTURE = saveFileToDisk(picture, user.ID, user.NAME_USERS);
|
||||||
|
}
|
||||||
|
|
||||||
|
await user.save({ transaction });
|
||||||
|
|
||||||
|
await user.reload({
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: models.Teacher,
|
||||||
|
as: "teachers",
|
||||||
|
attributes: ["NIP"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: models.Student,
|
||||||
|
as: "students",
|
||||||
|
attributes: ["NISN"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
|
||||||
|
await transaction.commit();
|
||||||
|
|
||||||
|
return response(200, user, "User updated successfully", res);
|
||||||
|
} catch (error) {
|
||||||
|
clearFileBuffers({ picture });
|
||||||
|
await transaction.rollback();
|
||||||
|
console.log(error);
|
||||||
|
return response(500, null, "Internal Server Error", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateUserPasswordById = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const { oldPassword, password, confirmPassword } = req.body;
|
||||||
|
|
||||||
|
if (!oldPassword || !password || !confirmPassword) {
|
||||||
|
return response(400, null, "All fields must be filled.", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password !== confirmPassword) {
|
||||||
|
return response(
|
||||||
|
400,
|
||||||
|
null,
|
||||||
|
"New password and confirm password do not match.",
|
||||||
|
res
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await models.User.findByPk(id);
|
||||||
|
if (!user) {
|
||||||
|
return response(404, null, "User not found.", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isMatch = await bcrypt.compare(oldPassword, user.PASSWORD);
|
||||||
|
if (!isMatch) {
|
||||||
|
return response(400, null, "Incorrect old password.", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const salt = await bcrypt.genSalt(10);
|
||||||
|
const hashedPassword = await bcrypt.hash(password, salt);
|
||||||
|
|
||||||
|
user.PASSWORD = hashedPassword;
|
||||||
|
await user.save();
|
||||||
|
|
||||||
|
response(200, null, "Password updated successfully.", res);
|
||||||
|
} catch (error) {
|
||||||
|
response(500, null, "Internal Server Error", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteUserById = async (req, res) => {
|
||||||
|
const transaction = await models.db.transaction();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
const user = await models.User.findByPk(id, { transaction });
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
await transaction.rollback();
|
||||||
|
return response(404, null, "User not found", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.ROLE === "teacher") {
|
||||||
|
await models.Teacher.destroy({ where: { ID: id }, transaction });
|
||||||
|
} else if (user.ROLE === "student") {
|
||||||
|
await models.Student.destroy({ where: { ID: id }, transaction });
|
||||||
|
}
|
||||||
|
|
||||||
|
await user.destroy({ transaction });
|
||||||
|
|
||||||
|
await transaction.commit();
|
||||||
|
return response(200, null, "User deleted successfully", res);
|
||||||
|
} catch (error) {
|
||||||
|
await transaction.rollback();
|
||||||
|
console.log(error);
|
||||||
|
return response(500, null, "Internal Server Error", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMe = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const user = req.user; // User object from verifyLoginUser middleware
|
||||||
|
|
||||||
|
// Retrieve teacher or student details based on the user's role
|
||||||
|
const userWithDetails = await models.User.findByPk(user.ID, {
|
||||||
|
attributes: {
|
||||||
|
exclude: ["PASSWORD"], // Exclude sensitive information
|
||||||
|
},
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: models.Teacher,
|
||||||
|
as: "teachers",
|
||||||
|
attributes: ["NIP"], // Include NIP for teacher
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: models.Student,
|
||||||
|
as: "students",
|
||||||
|
attributes: ["NISN"], // Include NISN for student
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!userWithDetails) {
|
||||||
|
return response(404, null, "User not found", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine additional field based on user role
|
||||||
|
let additionalField = null;
|
||||||
|
if (userWithDetails.ROLE === "teacher") {
|
||||||
|
additionalField = { NIP: userWithDetails.teachers.NIP };
|
||||||
|
} else if (userWithDetails.ROLE === "student") {
|
||||||
|
additionalField = { NISN: userWithDetails.students.NISN };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct the response object
|
||||||
|
const responseObject = {
|
||||||
|
ID: userWithDetails.ID,
|
||||||
|
NAME_USERS: userWithDetails.NAME_USERS,
|
||||||
|
EMAIL: userWithDetails.EMAIL,
|
||||||
|
ROLE: userWithDetails.ROLE,
|
||||||
|
...additionalField,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Send the response
|
||||||
|
response(200, responseObject, "Success", res);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
response(500, null, "Error retrieving user data!", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -12,6 +12,7 @@ const db = new Sequelize(name, username, password, {
|
||||||
host: host,
|
host: host,
|
||||||
dialect: "mysql",
|
dialect: "mysql",
|
||||||
logging: false,
|
logging: false,
|
||||||
|
timezone: '+07:00',
|
||||||
});
|
});
|
||||||
|
|
||||||
const testConnection = async () => {
|
const testConnection = async () => {
|
||||||
|
|
|
||||||
26
index.js
26
index.js
|
|
@ -1,23 +1,33 @@
|
||||||
import express from 'express';
|
import express from "express";
|
||||||
import cors from 'cors';
|
import cors from "cors";
|
||||||
import dotenv from 'dotenv';
|
import dotenv from "dotenv";
|
||||||
import { testConnection } from "./database/db.js";
|
import { testConnection } from "./database/db.js";
|
||||||
import router from "./routes/index.js";
|
import router from "./routes/index.js";
|
||||||
import cookieParser from 'cookie-parser';
|
import cookieParser from "cookie-parser";
|
||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
app.use(cors());
|
const corsOptions = {
|
||||||
|
origin: "*",
|
||||||
|
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
||||||
|
allowedHeaders: ["Content-Type", "Authorization"],
|
||||||
|
credentials: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
app.use(cors(corsOptions));
|
||||||
|
|
||||||
app.use(cookieParser());
|
app.use(cookieParser());
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(express.urlencoded({ extended: true }));
|
app.use(express.urlencoded({ extended: true }));
|
||||||
app.use(router);
|
app.use(router);
|
||||||
|
|
||||||
// Serve static files from the uploads directory
|
// Serve static files from the uploads directory
|
||||||
app.use(express.static('public'));
|
app.use(express.static("public"));
|
||||||
|
|
||||||
app.listen(process.env.APP_PORT, () => {
|
app.listen(process.env.APP_PORT, () => {
|
||||||
testConnection();
|
testConnection();
|
||||||
console.log(`Server running on port http://localhost:${process.env.APP_PORT}`);
|
console.log(
|
||||||
})
|
`Server running on port http://localhost:${process.env.APP_PORT}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
|
||||||
316
middlewares/Level/checkLevel.js
Normal file
316
middlewares/Level/checkLevel.js
Normal file
|
|
@ -0,0 +1,316 @@
|
||||||
|
import models from "../../models/index.js";
|
||||||
|
import response from "../../response.js";
|
||||||
|
|
||||||
|
export const checkLevelsPerTopic = async (req, res, next) => {
|
||||||
|
const { ID_TOPIC } = req.body;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const levelCount = await models.Level.count({
|
||||||
|
where: { ID_TOPIC },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (levelCount >= 7) {
|
||||||
|
return response(
|
||||||
|
400,
|
||||||
|
null,
|
||||||
|
"Cannot add more than 6 levels to a single topic",
|
||||||
|
res
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
return response(500, null, "Internal Server Error", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getSectionAndTopicByLevelId = async (req, res, next) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const level = await models.Level.findByPk(id, {
|
||||||
|
attributes: ["ID_SECTION", "ID_TOPIC"],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!level) {
|
||||||
|
return response(404, null, "Level not found", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
req.body.ID_SECTION = level.ID_SECTION;
|
||||||
|
req.body.ID_TOPIC = level.ID_TOPIC;
|
||||||
|
|
||||||
|
next();
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
return response(500, null, "Internal Server Error", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const autoCalculateRoutes = async (req, res, next) => {
|
||||||
|
const { NAME_LEVEL, ID_TOPIC } = req.body;
|
||||||
|
|
||||||
|
try {
|
||||||
|
let routes = {
|
||||||
|
ROUTE_1: 0,
|
||||||
|
ROUTE_2: 0,
|
||||||
|
ROUTE_3: 0,
|
||||||
|
ROUTE_4: 0,
|
||||||
|
ROUTE_5: 0,
|
||||||
|
ROUTE_6: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const levelTitles = [
|
||||||
|
"Pretest",
|
||||||
|
"Level 1",
|
||||||
|
"Level 2",
|
||||||
|
"Level 3",
|
||||||
|
"Level 4",
|
||||||
|
"Level 5",
|
||||||
|
"Level 6",
|
||||||
|
];
|
||||||
|
|
||||||
|
const levels = await models.Level.findAll({
|
||||||
|
where: {
|
||||||
|
ID_TOPIC,
|
||||||
|
NAME_LEVEL: {
|
||||||
|
[models.Sequelize.Op.in]: levelTitles,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
levels.sort(
|
||||||
|
(a, b) =>
|
||||||
|
levelTitles.indexOf(a.NAME_LEVEL) - levelTitles.indexOf(b.NAME_LEVEL)
|
||||||
|
);
|
||||||
|
|
||||||
|
const levelMap = {};
|
||||||
|
levels.forEach((level) => {
|
||||||
|
levelMap[level.NAME_LEVEL] = level.ID_LEVEL;
|
||||||
|
});
|
||||||
|
|
||||||
|
switch (NAME_LEVEL) {
|
||||||
|
case "Pretest":
|
||||||
|
req.body.IS_PRETEST = 1;
|
||||||
|
routes.ROUTE_1 = levelMap["Level 1"] || 0;
|
||||||
|
routes.ROUTE_2 = levelMap["Level 2"] || 0;
|
||||||
|
routes.ROUTE_3 = levelMap["Level 3"] || 0;
|
||||||
|
routes.ROUTE_4 = levelMap["Level 4"] || 0;
|
||||||
|
routes.ROUTE_5 = levelMap["Level 5"] || 0;
|
||||||
|
routes.ROUTE_6 = levelMap["Level 6"] || 0;
|
||||||
|
break;
|
||||||
|
case "Level 1":
|
||||||
|
routes.ROUTE_1 = levelMap["Pretest"] || 0;
|
||||||
|
routes.ROUTE_2 = levelMap["Level 2"] || 0;
|
||||||
|
routes.ROUTE_3 = levelMap["Level 3"] || 0;
|
||||||
|
routes.ROUTE_4 = levelMap["Level 4"] || 0;
|
||||||
|
routes.ROUTE_5 = levelMap["Level 5"] || 0;
|
||||||
|
routes.ROUTE_6 = levelMap["Level 6"] || 0;
|
||||||
|
break;
|
||||||
|
case "Level 2":
|
||||||
|
routes.ROUTE_1 = levelMap["Pretest"] || 0;
|
||||||
|
routes.ROUTE_2 = levelMap["Level 1"] || 0;
|
||||||
|
routes.ROUTE_3 = levelMap["Level 3"] || 0;
|
||||||
|
routes.ROUTE_4 = levelMap["Level 4"] || 0;
|
||||||
|
routes.ROUTE_5 = levelMap["Level 5"] || 0;
|
||||||
|
routes.ROUTE_6 = levelMap["Level 6"] || 0;
|
||||||
|
break;
|
||||||
|
case "Level 3":
|
||||||
|
routes.ROUTE_1 = levelMap["Pretest"] || 0;
|
||||||
|
routes.ROUTE_2 = levelMap["Level 1"] || 0;
|
||||||
|
routes.ROUTE_3 = levelMap["Level 2"] || 0;
|
||||||
|
routes.ROUTE_4 = levelMap["Level 4"] || 0;
|
||||||
|
routes.ROUTE_5 = levelMap["Level 5"] || 0;
|
||||||
|
routes.ROUTE_6 = levelMap["Level 6"] || 0;
|
||||||
|
break;
|
||||||
|
case "Level 4":
|
||||||
|
routes.ROUTE_1 = levelMap["Pretest"] || 0;
|
||||||
|
routes.ROUTE_2 = levelMap["Level 1"] || 0;
|
||||||
|
routes.ROUTE_3 = levelMap["Level 2"] || 0;
|
||||||
|
routes.ROUTE_4 = levelMap["Level 3"] || 0;
|
||||||
|
routes.ROUTE_5 = levelMap["Level 5"] || 0;
|
||||||
|
routes.ROUTE_6 = levelMap["Level 6"] || 0;
|
||||||
|
break;
|
||||||
|
case "Level 5":
|
||||||
|
routes.ROUTE_1 = levelMap["Pretest"] || 0;
|
||||||
|
routes.ROUTE_2 = levelMap["Level 1"] || 0;
|
||||||
|
routes.ROUTE_3 = levelMap["Level 2"] || 0;
|
||||||
|
routes.ROUTE_4 = levelMap["Level 3"] || 0;
|
||||||
|
routes.ROUTE_5 = levelMap["Level 4"] || 0;
|
||||||
|
routes.ROUTE_6 = levelMap["Level 6"] || 0;
|
||||||
|
break;
|
||||||
|
case "Level 6":
|
||||||
|
routes.ROUTE_1 = levelMap["Pretest"] || 0;
|
||||||
|
routes.ROUTE_2 = levelMap["Level 1"] || 0;
|
||||||
|
routes.ROUTE_3 = levelMap["Level 2"] || 0;
|
||||||
|
routes.ROUTE_4 = levelMap["Level 3"] || 0;
|
||||||
|
routes.ROUTE_5 = levelMap["Level 4"] || 0;
|
||||||
|
routes.ROUTE_6 = levelMap["Level 5"] || 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
req.body.ROUTE_1 = routes.ROUTE_1;
|
||||||
|
req.body.ROUTE_2 = routes.ROUTE_2;
|
||||||
|
req.body.ROUTE_3 = routes.ROUTE_3;
|
||||||
|
req.body.ROUTE_4 = routes.ROUTE_4;
|
||||||
|
req.body.ROUTE_5 = routes.ROUTE_5;
|
||||||
|
req.body.ROUTE_6 = routes.ROUTE_6;
|
||||||
|
|
||||||
|
next();
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
return response(500, null, "Internal Server Error", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateOtherLevelsRoutes = async (req, res, next) => {
|
||||||
|
const { NAME_LEVEL, ID_TOPIC, newLevelId } = req.body;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const levelTitles = [
|
||||||
|
"Pretest",
|
||||||
|
"Level 1",
|
||||||
|
"Level 2",
|
||||||
|
"Level 3",
|
||||||
|
"Level 4",
|
||||||
|
"Level 5",
|
||||||
|
"Level 6",
|
||||||
|
];
|
||||||
|
const currentLevelIndex = levelTitles.indexOf(NAME_LEVEL);
|
||||||
|
|
||||||
|
if (currentLevelIndex !== -1) {
|
||||||
|
const levels = await models.Level.findAll({
|
||||||
|
where: {
|
||||||
|
ID_TOPIC,
|
||||||
|
NAME_LEVEL: {
|
||||||
|
[models.Sequelize.Op.in]: levelTitles,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
levels.sort(
|
||||||
|
(a, b) =>
|
||||||
|
levelTitles.indexOf(a.NAME_LEVEL) - levelTitles.indexOf(b.NAME_LEVEL)
|
||||||
|
);
|
||||||
|
|
||||||
|
const levelMap = {};
|
||||||
|
levels.forEach((level) => {
|
||||||
|
levelMap[level.NAME_LEVEL] = level.ID_LEVEL;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (newLevelId) {
|
||||||
|
levelMap[NAME_LEVEL] = newLevelId;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < levelTitles.length; i++) {
|
||||||
|
if (i === currentLevelIndex) continue;
|
||||||
|
|
||||||
|
const levelTitle = levelTitles[i];
|
||||||
|
const updateData = {};
|
||||||
|
|
||||||
|
if (i === 0) {
|
||||||
|
updateData.ROUTE_1 = levelMap["Level 1"] || 0;
|
||||||
|
updateData.ROUTE_2 = levelMap["Level 2"] || 0;
|
||||||
|
updateData.ROUTE_3 = levelMap["Level 3"] || 0;
|
||||||
|
updateData.ROUTE_4 = levelMap["Level 4"] || 0;
|
||||||
|
updateData.ROUTE_5 = levelMap["Level 5"] || 0;
|
||||||
|
updateData.ROUTE_6 = levelMap["Level 6"] || 0;
|
||||||
|
} else if (i === 1) {
|
||||||
|
updateData.ROUTE_1 = levelMap["Pretest"] || 0;
|
||||||
|
updateData.ROUTE_2 = levelMap["Level 2"] || 0;
|
||||||
|
updateData.ROUTE_3 = levelMap["Level 3"] || 0;
|
||||||
|
updateData.ROUTE_4 = levelMap["Level 4"] || 0;
|
||||||
|
updateData.ROUTE_5 = levelMap["Level 5"] || 0;
|
||||||
|
updateData.ROUTE_6 = levelMap["Level 6"] || 0;
|
||||||
|
} else if (i === 2) {
|
||||||
|
updateData.ROUTE_1 = levelMap["Pretest"] || 0;
|
||||||
|
updateData.ROUTE_2 = levelMap["Level 1"] || 0;
|
||||||
|
updateData.ROUTE_3 = levelMap["Level 3"] || 0;
|
||||||
|
updateData.ROUTE_4 = levelMap["Level 4"] || 0;
|
||||||
|
updateData.ROUTE_5 = levelMap["Level 5"] || 0;
|
||||||
|
updateData.ROUTE_6 = levelMap["Level 6"] || 0;
|
||||||
|
} else if (i === 3) {
|
||||||
|
updateData.ROUTE_1 = levelMap["Pretest"] || 0;
|
||||||
|
updateData.ROUTE_2 = levelMap["Level 1"] || 0;
|
||||||
|
updateData.ROUTE_3 = levelMap["Level 2"] || 0;
|
||||||
|
updateData.ROUTE_4 = levelMap["Level 4"] || 0;
|
||||||
|
updateData.ROUTE_5 = levelMap["Level 5"] || 0;
|
||||||
|
updateData.ROUTE_6 = levelMap["Level 6"] || 0;
|
||||||
|
} else if (i === 4) {
|
||||||
|
updateData.ROUTE_1 = levelMap["Pretest"] || 0;
|
||||||
|
updateData.ROUTE_2 = levelMap["Level 1"] || 0;
|
||||||
|
updateData.ROUTE_3 = levelMap["Level 2"] || 0;
|
||||||
|
updateData.ROUTE_4 = levelMap["Level 3"] || 0;
|
||||||
|
updateData.ROUTE_5 = levelMap["Level 5"] || 0;
|
||||||
|
updateData.ROUTE_6 = levelMap["Level 6"] || 0;
|
||||||
|
} else if (i === 5) {
|
||||||
|
updateData.ROUTE_1 = levelMap["Pretest"] || 0;
|
||||||
|
updateData.ROUTE_2 = levelMap["Level 1"] || 0;
|
||||||
|
updateData.ROUTE_3 = levelMap["Level 2"] || 0;
|
||||||
|
updateData.ROUTE_4 = levelMap["Level 3"] || 0;
|
||||||
|
updateData.ROUTE_5 = levelMap["Level 4"] || 0;
|
||||||
|
updateData.ROUTE_6 = levelMap["Level 6"] || 0;
|
||||||
|
} else if (i === 6) {
|
||||||
|
updateData.ROUTE_1 = levelMap["Pretest"] || 0;
|
||||||
|
updateData.ROUTE_2 = levelMap["Level 1"] || 0;
|
||||||
|
updateData.ROUTE_3 = levelMap["Level 2"] || 0;
|
||||||
|
updateData.ROUTE_4 = levelMap["Level 3"] || 0;
|
||||||
|
updateData.ROUTE_5 = levelMap["Level 4"] || 0;
|
||||||
|
updateData.ROUTE_6 = levelMap["Level 5"] || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
await models.Level.update(updateData, {
|
||||||
|
where: {
|
||||||
|
ID_TOPIC,
|
||||||
|
NAME_LEVEL: levelTitle,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
return response(500, null, "Internal Server Error", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateOtherLevelsRoutesOnDelete = async (req, res, next) => {
|
||||||
|
const { newLevelId } = req.body;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!newLevelId) {
|
||||||
|
return response(400, null, "No level provided for deletion!", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const levelsToUpdate = await models.Level.findAll({
|
||||||
|
where: {
|
||||||
|
[models.Sequelize.Op.or]: [
|
||||||
|
{ ROUTE_1: newLevelId },
|
||||||
|
{ ROUTE_2: newLevelId },
|
||||||
|
{ ROUTE_3: newLevelId },
|
||||||
|
{ ROUTE_4: newLevelId },
|
||||||
|
{ ROUTE_5: newLevelId },
|
||||||
|
{ ROUTE_6: newLevelId },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const level of levelsToUpdate) {
|
||||||
|
const updateData = {};
|
||||||
|
if (level.ROUTE_1 === newLevelId) updateData.ROUTE_1 = 0;
|
||||||
|
if (level.ROUTE_2 === newLevelId) updateData.ROUTE_2 = 0;
|
||||||
|
if (level.ROUTE_3 === newLevelId) updateData.ROUTE_3 = 0;
|
||||||
|
if (level.ROUTE_4 === newLevelId) updateData.ROUTE_4 = 0;
|
||||||
|
if (level.ROUTE_5 === newLevelId) updateData.ROUTE_5 = 0;
|
||||||
|
if (level.ROUTE_6 === newLevelId) updateData.ROUTE_6 = 0;
|
||||||
|
|
||||||
|
await level.update(updateData);
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
return response(500, null, "Internal Server Error", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -2,12 +2,10 @@ import multer from "multer";
|
||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import response from "../response.js";
|
import response from "../../response.js";
|
||||||
|
|
||||||
// Setup memory storage for Multer
|
|
||||||
const memoryStorage = multer.memoryStorage();
|
const memoryStorage = multer.memoryStorage();
|
||||||
|
|
||||||
// Filter untuk membatasi tipe file dan ukuran file
|
|
||||||
const fileFilter = (req, file, cb) => {
|
const fileFilter = (req, file, cb) => {
|
||||||
const ext = path.extname(file.originalname).toLowerCase();
|
const ext = path.extname(file.originalname).toLowerCase();
|
||||||
|
|
||||||
|
|
@ -56,12 +54,11 @@ const fileFilter = (req, file, cb) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Set up Multer untuk menangani upload
|
|
||||||
const upload = multer({
|
const upload = multer({
|
||||||
storage: memoryStorage,
|
storage: memoryStorage,
|
||||||
fileFilter,
|
fileFilter,
|
||||||
limits: {
|
limits: {
|
||||||
fileSize: 100 * 1024 * 1024, // Total file size limit if needed
|
fileSize: 100 * 1024 * 1024,
|
||||||
},
|
},
|
||||||
}).fields([
|
}).fields([
|
||||||
{ name: "video", maxCount: 1 },
|
{ name: "video", maxCount: 1 },
|
||||||
|
|
@ -69,7 +66,6 @@ const upload = multer({
|
||||||
{ name: "image", maxCount: 1 },
|
{ name: "image", maxCount: 1 },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Middleware untuk menangani upload dan pengecekan file size
|
|
||||||
const handleUpload = (req, res, next) => {
|
const handleUpload = (req, res, next) => {
|
||||||
upload(req, res, (err) => {
|
upload(req, res, (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
|
@ -85,7 +81,6 @@ const handleUpload = (req, res, next) => {
|
||||||
let validFiles = true;
|
let validFiles = true;
|
||||||
let errorMessages = [];
|
let errorMessages = [];
|
||||||
|
|
||||||
// Validate file sizes
|
|
||||||
if (video && video.size > 30 * 1024 * 1024) {
|
if (video && video.size > 30 * 1024 * 1024) {
|
||||||
validFiles = false;
|
validFiles = false;
|
||||||
video.buffer = null;
|
video.buffer = null;
|
||||||
|
|
@ -105,11 +100,9 @@ const handleUpload = (req, res, next) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (validFiles) {
|
if (validFiles) {
|
||||||
// Attach files to the request object for further processing
|
|
||||||
req.filesToSave = { video, audio, image };
|
req.filesToSave = { video, audio, image };
|
||||||
next();
|
next();
|
||||||
} else {
|
} else {
|
||||||
// Clear file buffers and return error response with specific messages
|
|
||||||
clearFileBuffers({ video, audio, image });
|
clearFileBuffers({ video, audio, image });
|
||||||
return response(400, null, errorMessages.join("; "), res);
|
return response(400, null, errorMessages.join("; "), res);
|
||||||
}
|
}
|
||||||
|
|
@ -121,7 +114,6 @@ const handleUpload = (req, res, next) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Function to clear file buffers
|
|
||||||
export const clearFileBuffers = (files) => {
|
export const clearFileBuffers = (files) => {
|
||||||
for (const file of Object.values(files)) {
|
for (const file of Object.values(files)) {
|
||||||
if (file && file.buffer) {
|
if (file && file.buffer) {
|
||||||
|
|
@ -130,19 +122,17 @@ export const clearFileBuffers = (files) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const generateHash = (subjectId, filename, bufferLength) => {
|
export const generateHash = (sectionId, topicId, filename, bufferLength) => {
|
||||||
return crypto
|
return crypto
|
||||||
.createHash("md5")
|
.createHash("md5")
|
||||||
.update(subjectId + filename + bufferLength)
|
.update(sectionId + topicId + filename + bufferLength)
|
||||||
.digest("hex");
|
.digest("hex");
|
||||||
};
|
};
|
||||||
|
|
||||||
// Function to save files to disk
|
export const saveFileToDisk = (file, type, topicId, sectionId, levelId) => {
|
||||||
export const saveFileToDisk = (file, type, title, topicId, subjectId) => {
|
|
||||||
const formattedTitle = title.replace(/\s+/g, '').toLowerCase();
|
|
||||||
const ext = path.extname(file.originalname);
|
const ext = path.extname(file.originalname);
|
||||||
const hash = generateHash(subjectId, file.originalname, file.buffer.length);
|
const hash = generateHash(sectionId, topicId, file.originalname, file.buffer.length);
|
||||||
const filename = `${topicId}-${formattedTitle}-${type}-${hash}${ext}`;
|
const filename = `${type}-${levelId}-${hash}${ext}`;
|
||||||
|
|
||||||
let folderPath;
|
let folderPath;
|
||||||
switch (type) {
|
switch (type) {
|
||||||
73
middlewares/User/authUser.js
Normal file
73
middlewares/User/authUser.js
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
import jwt from "jsonwebtoken";
|
||||||
|
import response from "../../response.js";
|
||||||
|
import models from "../../models/index.js";
|
||||||
|
|
||||||
|
export const verifyLoginUser = async (req, res, next) => {
|
||||||
|
const authHeader = req.headers.authorization;
|
||||||
|
|
||||||
|
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
||||||
|
return response(401, null, "Please log in to your account first!", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = authHeader.split(" ")[1];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const decoded = jwt.verify(token, process.env.ACCESS_TOKEN_SECRET);
|
||||||
|
|
||||||
|
const user = await models.User.findByPk(decoded.id, {
|
||||||
|
attributes: {
|
||||||
|
exclude: ["PASSWORD"],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return response(404, null, "User not found!", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
req.user = user;
|
||||||
|
|
||||||
|
next();
|
||||||
|
} catch (error) {
|
||||||
|
if (error.name === "JsonWebTokenError") {
|
||||||
|
return response(403, null, "Invalid token!", res);
|
||||||
|
} else {
|
||||||
|
return response(500, null, "An error occurred on the server!", res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const adminOnly = (req, res, next) => {
|
||||||
|
if (!req.user || req.user.ROLE !== "admin") {
|
||||||
|
return response(
|
||||||
|
403,
|
||||||
|
null,
|
||||||
|
"Access denied! You do not have admin access.",
|
||||||
|
res
|
||||||
|
);
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const teacherOnly = (req, res, next) => {
|
||||||
|
if (!req.user || req.user.ROLE !== "teacher") {
|
||||||
|
return response(
|
||||||
|
403,
|
||||||
|
null,
|
||||||
|
"Access denied! You do not have teacher access.",
|
||||||
|
res
|
||||||
|
);
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const adminOrTeacherOnly = (req, res, next) => {
|
||||||
|
if (!req.user || (req.user.ROLE !== "admin" && req.user.ROLE !== "teacher")) {
|
||||||
|
return response(
|
||||||
|
403,
|
||||||
|
null,
|
||||||
|
"Access denied! You do not have access for this feature.",
|
||||||
|
res
|
||||||
|
);
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
};
|
||||||
91
middlewares/User/uploadUser.js
Normal file
91
middlewares/User/uploadUser.js
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
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();
|
||||||
|
if (ext === ".png" || ext === ".jpg" || ext === ".jpeg") {
|
||||||
|
cb(null, true);
|
||||||
|
} else {
|
||||||
|
cb(
|
||||||
|
new Error(
|
||||||
|
"Invalid file type, only .png, .jpg, and .jpeg files are allowed!"
|
||||||
|
),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const upload = multer({
|
||||||
|
storage: memoryStorage,
|
||||||
|
fileFilter,
|
||||||
|
limits: { fileSize: 5 * 1024 * 1024 },
|
||||||
|
}).fields([{ name: "picture", maxCount: 1 }]);
|
||||||
|
|
||||||
|
const handleUpload = (req, res, next) => {
|
||||||
|
upload(req, res, (err) => {
|
||||||
|
if (err) {
|
||||||
|
return response(400, null, err.message, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = req.files;
|
||||||
|
const picture = files?.picture ? files.picture[0] : null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
let validFiles = true;
|
||||||
|
let errorMessages = [];
|
||||||
|
|
||||||
|
if (picture && picture.size > 5 * 1024 * 1024) {
|
||||||
|
validFiles = false;
|
||||||
|
picture.buffer = null;
|
||||||
|
errorMessages.push("Picture file exceeds the size limit of 5MB");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validFiles) {
|
||||||
|
req.filesToSave = { picture };
|
||||||
|
next();
|
||||||
|
} else {
|
||||||
|
clearFileBuffers({ picture });
|
||||||
|
return response(400, null, errorMessages.join(", "), res);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
clearFileBuffers(file);
|
||||||
|
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 saveFileToDisk = (file, userId, userName) => {
|
||||||
|
const ext = path.extname(file.originalname);
|
||||||
|
const hash = crypto
|
||||||
|
.createHash("md5")
|
||||||
|
.update(
|
||||||
|
userId + userName + file.originalname + file.buffer.length.toString()
|
||||||
|
)
|
||||||
|
.digest("hex");
|
||||||
|
const filename = `user-${hash}${ext}`;
|
||||||
|
const folderPath = path.join("public/uploads/avatar");
|
||||||
|
|
||||||
|
if (!fs.existsSync(folderPath)) {
|
||||||
|
fs.mkdirSync(folderPath, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const filepath = path.join(folderPath, filename);
|
||||||
|
fs.writeFileSync(filepath, file.buffer);
|
||||||
|
return filename;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default handleUpload;
|
||||||
|
|
@ -1,64 +0,0 @@
|
||||||
import jwt from "jsonwebtoken";
|
|
||||||
import models from "../models/index.js";
|
|
||||||
|
|
||||||
export const verifyLoginUser = async (req, res, next) => {
|
|
||||||
const { accessToken } = req.cookies;
|
|
||||||
|
|
||||||
if (!accessToken) {
|
|
||||||
return res
|
|
||||||
.status(401)
|
|
||||||
.json({ message: "Please log in to your account first!" });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Verifikasi token dan dapatkan payload yang didekode
|
|
||||||
const decoded = jwt.verify(accessToken, process.env.ACCESS_TOKEN_SECRET);
|
|
||||||
|
|
||||||
// Cari user berdasarkan id yang ada di token
|
|
||||||
const user = await models.User.findByPk(decoded.id, {
|
|
||||||
attributes: {
|
|
||||||
exclude: ["password"],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return res.status(404).json({ message: "User not found!" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simpan informasi user di req.user untuk penggunaan selanjutnya
|
|
||||||
req.user = user;
|
|
||||||
|
|
||||||
// Lanjutkan ke route handler berikutnya
|
|
||||||
next();
|
|
||||||
} catch (error) {
|
|
||||||
if (error.name === "JsonWebTokenError") {
|
|
||||||
return res.status(403).json({ message: "Invalid token!" });
|
|
||||||
} else {
|
|
||||||
return res
|
|
||||||
.status(500)
|
|
||||||
.json({ message: "An error occurred on the server!" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Middleware untuk memverifikasi apakah pengguna adalah admin
|
|
||||||
export const adminOnly = (req, res, next) => {
|
|
||||||
if (!req.user || req.user.roles !== "admin") {
|
|
||||||
return res.status(403).json({
|
|
||||||
message:
|
|
||||||
"Access denied! You do not have admin access.",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
next();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Middleware untuk memverifikasi apakah pengguna adalah teacher
|
|
||||||
export const teacherOnly = (req, res, next) => {
|
|
||||||
if (!req.user || req.user.roles !== "teacher") {
|
|
||||||
return res.status(403).json({
|
|
||||||
message:
|
|
||||||
"Access denied! You do not have teacher access.",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
next();
|
|
||||||
};
|
|
||||||
275
middlewares/autoGrading.js
Normal file
275
middlewares/autoGrading.js
Normal file
|
|
@ -0,0 +1,275 @@
|
||||||
|
import models from "../models/index.js";
|
||||||
|
import response from "../response.js";
|
||||||
|
|
||||||
|
export const checkCorrectAnswers = async (req, res, next) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stdExercises = await models.StdExercise.findAll({
|
||||||
|
where: { ID_STUDENT_LEARNING: id },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!stdExercises || stdExercises.length === 0) {
|
||||||
|
return response(404, null, "No student exercises found", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const stdExercise of stdExercises) {
|
||||||
|
const { ID_ADMIN_EXERCISE, ANSWER_STUDENT } = stdExercise;
|
||||||
|
|
||||||
|
const exercise = await models.Exercise.findByPk(ID_ADMIN_EXERCISE);
|
||||||
|
if (!exercise) continue;
|
||||||
|
|
||||||
|
const weight = parseFloat(exercise.SCORE_WEIGHT); // Ensure weight is a float
|
||||||
|
const questionType = exercise.QUESTION_TYPE;
|
||||||
|
|
||||||
|
switch (questionType) {
|
||||||
|
case "MCQ": {
|
||||||
|
const multipleChoice = await models.MultipleChoices.findOne({
|
||||||
|
where: { ID_ADMIN_EXERCISE },
|
||||||
|
});
|
||||||
|
if (multipleChoice) {
|
||||||
|
stdExercise.IS_CORRECT =
|
||||||
|
ANSWER_STUDENT === multipleChoice.ANSWER_KEY ? 1 : 0;
|
||||||
|
stdExercise.RESULT_SCORE_STUDENT =
|
||||||
|
ANSWER_STUDENT === multipleChoice.ANSWER_KEY ? weight : 0.0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "TFQ": {
|
||||||
|
const trueFalse = await models.TrueFalse.findOne({
|
||||||
|
where: { ID_ADMIN_EXERCISE },
|
||||||
|
});
|
||||||
|
if (trueFalse) {
|
||||||
|
const isTrueStudent = ANSWER_STUDENT === "1";
|
||||||
|
stdExercise.IS_CORRECT =
|
||||||
|
isTrueStudent === Boolean(trueFalse.IS_TRUE) ? 1 : 0;
|
||||||
|
stdExercise.RESULT_SCORE_STUDENT =
|
||||||
|
isTrueStudent === Boolean(trueFalse.IS_TRUE) ? weight : 0.0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "MPQ": {
|
||||||
|
const matchingPairs = await models.MatchingPairs.findAll({
|
||||||
|
where: { ID_ADMIN_EXERCISE },
|
||||||
|
});
|
||||||
|
if (matchingPairs && matchingPairs.length > 0) {
|
||||||
|
const studentAnswers = ANSWER_STUDENT.split(",").map((pair) => {
|
||||||
|
const [left, right] = pair.split("-");
|
||||||
|
|
||||||
|
return {
|
||||||
|
LEFT_PAIR: left ? left.trim() : "",
|
||||||
|
RIGHT_PAIR: right ? right.trim() : "",
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
let correctCount = 0;
|
||||||
|
for (const studentAnswer of studentAnswers) {
|
||||||
|
if (
|
||||||
|
matchingPairs.some(
|
||||||
|
(pair) =>
|
||||||
|
pair.LEFT_PAIR === studentAnswer.LEFT_PAIR &&
|
||||||
|
pair.RIGHT_PAIR === studentAnswer.RIGHT_PAIR
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
correctCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const correctPercentage = correctCount / matchingPairs.length;
|
||||||
|
stdExercise.IS_CORRECT = correctCount > 0 ? 1 : 0;
|
||||||
|
stdExercise.RESULT_SCORE_STUDENT = correctPercentage * weight; // Use float arithmetic
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
await stdExercise.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error checking correct answers:", error);
|
||||||
|
response(500, null, "Internal Server Error", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const calculateScore = async (req, res, next) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stdLearning = await models.StdLearning.findByPk(id);
|
||||||
|
|
||||||
|
if (!stdLearning) {
|
||||||
|
return response(404, null, "Student Learning record not found", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const allExercises = await models.Exercise.findAll({
|
||||||
|
where: { ID_LEVEL: stdLearning.ID_LEVEL },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!allExercises || allExercises.length === 0) {
|
||||||
|
return response(404, null, "No exercises found for this level", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const stdExercises = await models.StdExercise.findAll({
|
||||||
|
where: { ID_STUDENT_LEARNING: id },
|
||||||
|
});
|
||||||
|
|
||||||
|
let totalWeight = 0;
|
||||||
|
for (const exercise of allExercises) {
|
||||||
|
totalWeight += exercise.SCORE_WEIGHT;
|
||||||
|
}
|
||||||
|
|
||||||
|
let totalScore = 0;
|
||||||
|
for (const stdExercise of stdExercises) {
|
||||||
|
const exercise = allExercises.find(
|
||||||
|
(ex) => ex.ID_ADMIN_EXERCISE === stdExercise.ID_ADMIN_EXERCISE
|
||||||
|
);
|
||||||
|
|
||||||
|
if (exercise && stdExercise.RESULT_SCORE_STUDENT !== null) {
|
||||||
|
totalScore += stdExercise.RESULT_SCORE_STUDENT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalScore = Math.round((totalScore / totalWeight) * 100);
|
||||||
|
|
||||||
|
req.body.SCORE = finalScore;
|
||||||
|
|
||||||
|
next();
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
response(500, null, "Internal Server Error", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const checkFirstFiveCorrect = async (req, res, next) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stdLearning = await models.StdLearning.findByPk(id);
|
||||||
|
|
||||||
|
if (!stdLearning) {
|
||||||
|
return response(404, null, "Student Learning record not found", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstFiveExercises = await models.Exercise.findAll({
|
||||||
|
where: { ID_LEVEL: stdLearning.ID_LEVEL },
|
||||||
|
order: [["TITLE", "ASC"]],
|
||||||
|
limit: 5,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!firstFiveExercises || firstFiveExercises.length < 5) {
|
||||||
|
return response(404, null, "Not enough exercises for this level", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const stdExercises = await models.StdExercise.findAll({
|
||||||
|
where: { ID_STUDENT_LEARNING: id },
|
||||||
|
});
|
||||||
|
|
||||||
|
const allCorrect = firstFiveExercises.every((exercise) => {
|
||||||
|
const stdExercise = stdExercises.find(
|
||||||
|
(se) => se.ID_ADMIN_EXERCISE === exercise.ID_ADMIN_EXERCISE
|
||||||
|
);
|
||||||
|
return stdExercise && stdExercise.IS_CORRECT === 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
req.body.FIRST_FIVE_CORRECT = allCorrect;
|
||||||
|
|
||||||
|
next();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error checking first five correct answers:", error);
|
||||||
|
res.status(500).json({ message: "Internal Server Error" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const nextLearning = async (req, res, next) => {
|
||||||
|
const { SCORE, FIRST_FIVE_CORRECT } = req.body;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stdLearning = await models.StdLearning.findByPk(req.params.id, {
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: models.Level,
|
||||||
|
as: "level",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!stdLearning) {
|
||||||
|
return response(404, null, "Student Learning record not found", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const topic_id = stdLearning.level.ID_TOPIC;
|
||||||
|
|
||||||
|
const levels = await models.Level.findAll({
|
||||||
|
where: { ID_TOPIC: topic_id },
|
||||||
|
order: [["NAME_LEVEL", "ASC"]],
|
||||||
|
});
|
||||||
|
|
||||||
|
const levelMap = levels.reduce((map, level) => {
|
||||||
|
map[level.NAME_LEVEL] = level;
|
||||||
|
return map;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
let nextLearningLevel = null;
|
||||||
|
let currentLevelNumber = parseInt(
|
||||||
|
stdLearning.level.NAME_LEVEL.split(" ")[1]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (stdLearning.level.IS_PRETEST) {
|
||||||
|
if (SCORE >= 0 && SCORE <= 50) {
|
||||||
|
nextLearningLevel = levelMap["Level 1"];
|
||||||
|
} else if (SCORE >= 51 && SCORE <= 60) {
|
||||||
|
nextLearningLevel = levelMap["Level 2"];
|
||||||
|
} else if (SCORE >= 61 && SCORE <= 70) {
|
||||||
|
nextLearningLevel = levelMap["Level 3"];
|
||||||
|
} else if (SCORE >= 71 && SCORE <= 80) {
|
||||||
|
nextLearningLevel = levelMap["Level 4"];
|
||||||
|
} else if (SCORE >= 81 && SCORE <= 90) {
|
||||||
|
nextLearningLevel = levelMap["Level 5"];
|
||||||
|
} else if (SCORE >= 91 && SCORE <= 100) {
|
||||||
|
nextLearningLevel = levelMap["Level 6"];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (SCORE >= 75 && FIRST_FIVE_CORRECT) {
|
||||||
|
currentLevelNumber += SCORE >= 85 ? 2 : 1;
|
||||||
|
currentLevelNumber = Math.min(6, currentLevelNumber);
|
||||||
|
} else if (
|
||||||
|
(SCORE >= 75 && !FIRST_FIVE_CORRECT) ||
|
||||||
|
(SCORE >= 51 && SCORE <= 74)
|
||||||
|
) {
|
||||||
|
currentLevelNumber = currentLevelNumber;
|
||||||
|
} else if (SCORE >= 41 && SCORE <= 50) {
|
||||||
|
currentLevelNumber = Math.max(1, currentLevelNumber - 1);
|
||||||
|
} else if (SCORE >= 0 && SCORE <= 40) {
|
||||||
|
currentLevelNumber = Math.max(1, currentLevelNumber - 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
currentLevelNumber = Math.max(1, Math.min(6, currentLevelNumber));
|
||||||
|
|
||||||
|
nextLearningLevel = levelMap[`Level ${currentLevelNumber}`];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nextLearningLevel) {
|
||||||
|
return response(404, null, "Next learning level not found", res);
|
||||||
|
}
|
||||||
|
|
||||||
|
req.body.NEXT_LEARNING = nextLearningLevel.ID_LEVEL;
|
||||||
|
req.body.IS_PASS =
|
||||||
|
SCORE > 85 &&
|
||||||
|
stdLearning.level.NAME_LEVEL === "Level 6" &&
|
||||||
|
FIRST_FIVE_CORRECT
|
||||||
|
? 1
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
next();
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
response(500, null, "Error determining next learning path", res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
import models from "../models/index.js";
|
|
||||||
import response from "../response.js";
|
|
||||||
|
|
||||||
export const checkMaxLevelsPerTopic = async (req, res, next) => {
|
|
||||||
const { topic_id } = req.body;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Hitung jumlah level yang ada pada topic_id yang diberikan
|
|
||||||
const levelCount = await models.Level.count({
|
|
||||||
where: { topic_id },
|
|
||||||
});
|
|
||||||
|
|
||||||
// Periksa apakah jumlah level sudah mencapai 5
|
|
||||||
if (levelCount >= 5) {
|
|
||||||
return response(
|
|
||||||
400,
|
|
||||||
null,
|
|
||||||
"Cannot add more than 5 levels to a single topic",
|
|
||||||
res
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lanjutkan ke middleware atau route handler berikutnya jika belum mencapai 5
|
|
||||||
next();
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
return response(500, null, "Internal Server Error", res);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
161
middlewares/uploadExercise.js
Normal file
161
middlewares/uploadExercise.js
Normal file
|
|
@ -0,0 +1,161 @@
|
||||||
|
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 "video":
|
||||||
|
if (ext === ".mp4") {
|
||||||
|
cb(null, true);
|
||||||
|
} else {
|
||||||
|
cb(
|
||||||
|
new Error(
|
||||||
|
"Invalid file type, only .mp4 files are allowed for video!"
|
||||||
|
),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
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: "video", maxCount: 1 },
|
||||||
|
{ name: "audio", maxCount: 1 },
|
||||||
|
{ name: "image", maxCount: 1 },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handleUpload = (req, res, next) => {
|
||||||
|
upload(req, res, (err) => {
|
||||||
|
if (err) {
|
||||||
|
return response(400, null, err.message, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = req.files;
|
||||||
|
const video = files?.video ? files.video[0] : null;
|
||||||
|
const audio = files?.audio ? files.audio[0] : null;
|
||||||
|
const image = files?.image ? files.image[0] : null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
let validFiles = true;
|
||||||
|
let errorMessages = [];
|
||||||
|
|
||||||
|
if (video && video.size > 30 * 1024 * 1024) {
|
||||||
|
validFiles = false;
|
||||||
|
video.buffer = null;
|
||||||
|
errorMessages.push("Video file exceeds the size limit of 30MB");
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = { video, audio, image };
|
||||||
|
next();
|
||||||
|
} else {
|
||||||
|
clearFileBuffers({ video, 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 "video":
|
||||||
|
folderPath = path.join("public/uploads/exercise/video");
|
||||||
|
break;
|
||||||
|
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 handleUpload;
|
||||||
|
|
@ -23,11 +23,8 @@ const fileFilter = (req, file, cb) => {
|
||||||
const upload = multer({
|
const upload = multer({
|
||||||
storage: memoryStorage,
|
storage: memoryStorage,
|
||||||
fileFilter,
|
fileFilter,
|
||||||
limits: { fileSize: 10 * 1024 * 1024 }, // Limit file size to 5MB
|
limits: { fileSize: 5 * 1024 * 1024 },
|
||||||
}).fields([
|
}).fields([{ name: "thumbnail", maxCount: 1 }]);
|
||||||
{ name: "icon", maxCount: 1 },
|
|
||||||
{ name: "thumbnail", maxCount: 1 },
|
|
||||||
]);
|
|
||||||
|
|
||||||
const handleUpload = (req, res, next) => {
|
const handleUpload = (req, res, next) => {
|
||||||
upload(req, res, (err) => {
|
upload(req, res, (err) => {
|
||||||
|
|
@ -36,20 +33,12 @@ const handleUpload = (req, res, next) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const files = req.files;
|
const files = req.files;
|
||||||
const icon = files?.icon ? files.icon[0] : null;
|
|
||||||
const thumbnail = files?.thumbnail ? files.thumbnail[0] : null;
|
const thumbnail = files?.thumbnail ? files.thumbnail[0] : null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let validFiles = true;
|
let validFiles = true;
|
||||||
let errorMessages = [];
|
let errorMessages = [];
|
||||||
|
|
||||||
// Validate file sizes
|
|
||||||
if (icon && icon.size > 5 * 1024 * 1024) {
|
|
||||||
validFiles = false;
|
|
||||||
icon.buffer = null;
|
|
||||||
errorMessages.push("Icon file exceeds the size limit of 5MB");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (thumbnail && thumbnail.size > 5 * 1024 * 1024) {
|
if (thumbnail && thumbnail.size > 5 * 1024 * 1024) {
|
||||||
validFiles = false;
|
validFiles = false;
|
||||||
thumbnail.buffer = null;
|
thumbnail.buffer = null;
|
||||||
|
|
@ -57,15 +46,15 @@ const handleUpload = (req, res, next) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (validFiles) {
|
if (validFiles) {
|
||||||
req.filesToSave = { icon, thumbnail };
|
req.filesToSave = { thumbnail };
|
||||||
next();
|
next();
|
||||||
} else {
|
} else {
|
||||||
clearFileBuffers({ icon, thumbnail });
|
clearFileBuffers({ thumbnail });
|
||||||
return response(400, null, errorMessages.join(", "), res);
|
return response(400, null, errorMessages.join(", "), res);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
clearFileBuffers({ icon, thumbnail });
|
clearFileBuffers({ thumbnail });
|
||||||
return response(500, null, "Internal Server Error", res);
|
return response(500, null, "Internal Server Error", res);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -79,15 +68,14 @@ export const clearFileBuffers = (files) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const saveFileToDisk = (file, fieldName, subjectName) => {
|
export const saveFileToDisk = (file, fieldName, idSection) => {
|
||||||
const ext = path.extname(file.originalname);
|
const ext = path.extname(file.originalname);
|
||||||
const hash = crypto
|
const hash = crypto
|
||||||
.createHash("md5")
|
.createHash("md5")
|
||||||
.update(subjectName + file.originalname + file.buffer.length.toString())
|
.update(file.originalname + file.buffer.length.toString())
|
||||||
.digest("hex")
|
.digest("hex");
|
||||||
.slice(0, 8);
|
const filename = `${fieldName}-${idSection}-${hash}${ext}`;
|
||||||
const filename = `${fieldName}-${hash}${ext}`;
|
const folderPath = path.join("public/uploads/section");
|
||||||
const folderPath = path.join("public/uploads/subject");
|
|
||||||
|
|
||||||
if (!fs.existsSync(folderPath)) {
|
if (!fs.existsSync(folderPath)) {
|
||||||
fs.mkdirSync(folderPath, { recursive: true });
|
fs.mkdirSync(folderPath, { recursive: true });
|
||||||
83
models/contentModels/exerciseModel.js
Normal file
83
models/contentModels/exerciseModel.js
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
import db from "../../database/db.js";
|
||||||
|
|
||||||
|
const ExerciseModel = (DataTypes) => {
|
||||||
|
const Exercises = db.define(
|
||||||
|
"admin_exercise",
|
||||||
|
{
|
||||||
|
ID_ADMIN_EXERCISE: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
primaryKey: true,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
allowNull: false,
|
||||||
|
validate: {
|
||||||
|
notEmpty: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ID_LEVEL: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: false,
|
||||||
|
references: {
|
||||||
|
model: "level",
|
||||||
|
key: "ID_LEVEL",
|
||||||
|
},
|
||||||
|
validate: {
|
||||||
|
notEmpty: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
TITLE: {
|
||||||
|
type: DataTypes.STRING(100),
|
||||||
|
allowNull: false,
|
||||||
|
validate: {
|
||||||
|
notEmpty: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
QUESTION: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: false,
|
||||||
|
validate: {
|
||||||
|
notEmpty: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
SCORE_WEIGHT: {
|
||||||
|
type: DataTypes.INTEGER(6),
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 1,
|
||||||
|
validate: {
|
||||||
|
notEmpty: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
QUESTION_TYPE: {
|
||||||
|
type: DataTypes.STRING(100),
|
||||||
|
allowNull: false,
|
||||||
|
validate: {
|
||||||
|
notEmpty: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
AUDIO: {
|
||||||
|
type: DataTypes.STRING(1024),
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
VIDEO: {
|
||||||
|
type: DataTypes.STRING(1024),
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
IMAGE: {
|
||||||
|
type: DataTypes.STRING(1024),
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
TIME_ADMIN_EXC: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: DataTypes.NOW,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: false,
|
||||||
|
tableName: "admin_exercise",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return Exercises;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExerciseModel;
|
||||||
107
models/contentModels/levelModel.js
Normal file
107
models/contentModels/levelModel.js
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
import db from "../../database/db.js";
|
||||||
|
|
||||||
|
const LevelModel = (DataTypes) => {
|
||||||
|
const Levels = db.define(
|
||||||
|
"level",
|
||||||
|
{
|
||||||
|
ID_LEVEL: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
primaryKey: true,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
validate: {
|
||||||
|
notEmpty: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ID_TOPIC: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: false,
|
||||||
|
validate: {
|
||||||
|
notEmpty: true,
|
||||||
|
},
|
||||||
|
references: {
|
||||||
|
model: "topic",
|
||||||
|
key: "ID_TOPIC",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ID_SECTION: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: false,
|
||||||
|
validate: {
|
||||||
|
notEmpty: true,
|
||||||
|
},
|
||||||
|
references: {
|
||||||
|
model: "section",
|
||||||
|
key: "ID_SECTION",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
NAME_LEVEL: {
|
||||||
|
type: DataTypes.STRING(100),
|
||||||
|
allowNull: false,
|
||||||
|
validate: {
|
||||||
|
notEmpty: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
CONTENT: {
|
||||||
|
type: DataTypes.STRING(1024),
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
AUDIO: {
|
||||||
|
type: DataTypes.STRING(1024),
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
IMAGE: {
|
||||||
|
type: DataTypes.STRING(1024),
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
VIDEO: {
|
||||||
|
type: DataTypes.STRING(1024),
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
IS_PRETEST: {
|
||||||
|
type: DataTypes.INTEGER(1),
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: 0,
|
||||||
|
validate: {
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ROUTE_1: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
ROUTE_2: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
ROUTE_3: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
ROUTE_4: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
ROUTE_5: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
ROUTE_6: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
TIME_LEVEL: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: DataTypes.NOW,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: false,
|
||||||
|
tableName: "level",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return Levels;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LevelModel;
|
||||||
47
models/contentModels/sectionModel.js
Normal file
47
models/contentModels/sectionModel.js
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
import db from "../../database/db.js";
|
||||||
|
|
||||||
|
const SectionModel = (DataTypes) => {
|
||||||
|
const Sections = db.define(
|
||||||
|
"section",
|
||||||
|
{
|
||||||
|
ID_SECTION: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
primaryKey: true,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
validate: {
|
||||||
|
notEmpty: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
NAME_SECTION: {
|
||||||
|
type: DataTypes.STRING(100),
|
||||||
|
allowNull: false,
|
||||||
|
validate: {
|
||||||
|
notEmpty: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
DESCRIPTION_SECTION: {
|
||||||
|
type: DataTypes.STRING(1024),
|
||||||
|
allowNull: false,
|
||||||
|
validate: {
|
||||||
|
notEmpty: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
THUMBNAIL: {
|
||||||
|
type: DataTypes.STRING(255),
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
TIME_SECTION: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: DataTypes.NOW,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: false,
|
||||||
|
tableName: "section",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return Sections;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SectionModel;
|
||||||
54
models/contentModels/topicModel.js
Normal file
54
models/contentModels/topicModel.js
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
import db from "../../database/db.js";
|
||||||
|
|
||||||
|
const TopicModel = (DataTypes) => {
|
||||||
|
const Topics = db.define(
|
||||||
|
"topic",
|
||||||
|
{
|
||||||
|
ID_TOPIC: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
primaryKey: true,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
validate: {
|
||||||
|
notEmpty: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ID_SECTION: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: false,
|
||||||
|
validate: {
|
||||||
|
notEmpty: true,
|
||||||
|
},
|
||||||
|
references: {
|
||||||
|
model: "section",
|
||||||
|
key: "ID_SECTION",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
NAME_TOPIC: {
|
||||||
|
type: DataTypes.STRING(100),
|
||||||
|
allowNull: false,
|
||||||
|
validate: {
|
||||||
|
notEmpty: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
DESCRIPTION_TOPIC: {
|
||||||
|
type: DataTypes.STRING(1024),
|
||||||
|
allowNull: false,
|
||||||
|
validate: {
|
||||||
|
notEmpty: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
TIME_TOPIC: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: DataTypes.NOW,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: false,
|
||||||
|
tableName: "topic",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return Topics;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TopicModel;
|
||||||
50
models/exerciseTypesModels/matchingPairsModel.js
Normal file
50
models/exerciseTypesModels/matchingPairsModel.js
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
import db from "../../database/db.js";
|
||||||
|
|
||||||
|
const MatchingPairsModel = (DataTypes) => {
|
||||||
|
const MatchingPairs = db.define(
|
||||||
|
"matching_pairs",
|
||||||
|
{
|
||||||
|
ID_MATCHING_PAIRS: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
primaryKey: true,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
allowNull: false,
|
||||||
|
validate: {
|
||||||
|
notEmpty: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ID_ADMIN_EXERCISE: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: false,
|
||||||
|
references: {
|
||||||
|
model: "admin_exercise",
|
||||||
|
key: "ID_ADMIN_EXERCISE",
|
||||||
|
},
|
||||||
|
validate: {
|
||||||
|
notEmpty: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
LEFT_PAIR: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
RIGHT_PAIR: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
TIME_MATCHING_PAIRS: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: DataTypes.NOW,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: false,
|
||||||
|
tableName: "matching_pairs",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return MatchingPairs;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MatchingPairsModel;
|
||||||
66
models/exerciseTypesModels/multipleChoicesModel.js
Normal file
66
models/exerciseTypesModels/multipleChoicesModel.js
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
import db from "../../database/db.js";
|
||||||
|
|
||||||
|
const MultipleChoicesModel = (DataTypes) => {
|
||||||
|
const MultipleChoices = db.define(
|
||||||
|
"multiple_choices",
|
||||||
|
{
|
||||||
|
ID_MULTIPLE_CHOICES: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
primaryKey: true,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
allowNull: false,
|
||||||
|
validate: {
|
||||||
|
notEmpty: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ID_ADMIN_EXERCISE: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: false,
|
||||||
|
references: {
|
||||||
|
model: "admin_exercise",
|
||||||
|
key: "ID_ADMIN_EXERCISE",
|
||||||
|
},
|
||||||
|
validate: {
|
||||||
|
notEmpty: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
OPTION_A: {
|
||||||
|
type: DataTypes.STRING(200),
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
OPTION_B: {
|
||||||
|
type: DataTypes.STRING(200),
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
OPTION_C: {
|
||||||
|
type: DataTypes.STRING(200),
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
OPTION_D: {
|
||||||
|
type: DataTypes.STRING(200),
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
OPTION_E: {
|
||||||
|
type: DataTypes.STRING(200),
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
ANSWER_KEY: {
|
||||||
|
type: DataTypes.STRING(5),
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
TIME_MULTIPLE_CHOICES: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: DataTypes.NOW,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: false,
|
||||||
|
tableName: "multiple_choices",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return MultipleChoices;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MultipleChoicesModel;
|
||||||
46
models/exerciseTypesModels/trueFalseModel.js
Normal file
46
models/exerciseTypesModels/trueFalseModel.js
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
import db from "../../database/db.js";
|
||||||
|
|
||||||
|
const TrueFalseModel = (DataTypes) => {
|
||||||
|
const TrueFalse = db.define(
|
||||||
|
"true_false",
|
||||||
|
{
|
||||||
|
ID_TRUE_FALSE: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
primaryKey: true,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
allowNull: false,
|
||||||
|
validate: {
|
||||||
|
notEmpty: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ID_ADMIN_EXERCISE: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: false,
|
||||||
|
references: {
|
||||||
|
model: "admin_exercise",
|
||||||
|
key: "ID_ADMIN_EXERCISE",
|
||||||
|
},
|
||||||
|
validate: {
|
||||||
|
notEmpty: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
IS_TRUE: {
|
||||||
|
type: DataTypes.TINYINT(1),
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
TIME_TRUE_FALSE: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: DataTypes.NOW,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: false,
|
||||||
|
tableName: "true_false",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return TrueFalse;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TrueFalseModel;
|
||||||
148
models/index.js
148
models/index.js
|
|
@ -1,19 +1,149 @@
|
||||||
import { Sequelize } from "sequelize";
|
import { Sequelize } from "sequelize";
|
||||||
import UserModel from "./userModel.js";
|
import db from "../database/db.js";
|
||||||
import SubjectModel from "./subjectModel.js";
|
import UserModel from "./usersModels/userModel.js";
|
||||||
import TopicModel from "./topicModel.js";
|
import TeacherModel from "./usersModels/teacherModel.js";
|
||||||
import LevelModel from "./levelModel.js";
|
import StudentModel from "./usersModels/studentModel.js";
|
||||||
|
import SectionModel from "./contentModels/sectionModel.js";
|
||||||
|
import TopicModel from "./contentModels/topicModel.js";
|
||||||
|
import LevelModel from "./contentModels/levelModel.js";
|
||||||
|
import ExerciseModel from "./contentModels/exerciseModel.js";
|
||||||
|
import MultipleChoicesModel from "./exerciseTypesModels/multipleChoicesModel.js";
|
||||||
|
import MatchingPairsModel from "./exerciseTypesModels/matchingPairsModel.js";
|
||||||
|
import TrueFalseModel from "./exerciseTypesModels/trueFalseModel.js";
|
||||||
|
import StdLearningModel from "./learningModels/stdLearningModel.js";
|
||||||
|
import StdExerciseModel from "./learningModels/stdExerciseModel.js";
|
||||||
|
import ClassModel from "./monitoringModels/classModel.js";
|
||||||
|
import MonitoringModel from "./monitoringModels/monitoringModel.js";
|
||||||
|
|
||||||
// Impor operator Op
|
|
||||||
const Op = Sequelize.Op;
|
const Op = Sequelize.Op;
|
||||||
|
|
||||||
|
const User = UserModel(Sequelize.DataTypes);
|
||||||
|
const Teacher = TeacherModel(Sequelize.DataTypes);
|
||||||
|
const Student = StudentModel(Sequelize.DataTypes);
|
||||||
|
const Section = SectionModel(Sequelize.DataTypes);
|
||||||
|
const Topic = TopicModel(Sequelize.DataTypes);
|
||||||
|
const Level = LevelModel(Sequelize.DataTypes);
|
||||||
|
const Exercise = ExerciseModel(Sequelize.DataTypes);
|
||||||
|
const MultipleChoices = MultipleChoicesModel(Sequelize.DataTypes);
|
||||||
|
const MatchingPairs = MatchingPairsModel(Sequelize.DataTypes);
|
||||||
|
const TrueFalse = TrueFalseModel(Sequelize.DataTypes);
|
||||||
|
const StdLearning = StdLearningModel(Sequelize.DataTypes);
|
||||||
|
const StdExercise = StdExerciseModel(Sequelize.DataTypes);
|
||||||
|
const Class = ClassModel(Sequelize.DataTypes);
|
||||||
|
const Monitoring = MonitoringModel(Sequelize.DataTypes);
|
||||||
|
|
||||||
|
User.hasOne(Teacher, { foreignKey: "ID", as: "teachers" });
|
||||||
|
Teacher.belongsTo(User, { foreignKey: "ID", as: "teacherUser" });
|
||||||
|
|
||||||
|
User.hasOne(Student, { foreignKey: "ID", as: "students" });
|
||||||
|
Student.belongsTo(User, { foreignKey: "ID", as: "studentUser" });
|
||||||
|
|
||||||
|
Section.hasMany(Topic, { foreignKey: "ID_SECTION", as: "topics" });
|
||||||
|
Topic.belongsTo(Section, { foreignKey: "ID_SECTION", as: "topicSection" });
|
||||||
|
|
||||||
|
Topic.hasMany(Level, { foreignKey: "ID_TOPIC", as: "levels" });
|
||||||
|
Level.belongsTo(Topic, { foreignKey: "ID_TOPIC", as: "levelTopic" });
|
||||||
|
|
||||||
|
Level.hasMany(Exercise, { foreignKey: "ID_LEVEL", as: "exercises" });
|
||||||
|
Exercise.belongsTo(Level, { foreignKey: "ID_LEVEL", as: "levelExercise" });
|
||||||
|
|
||||||
|
Exercise.hasMany(MultipleChoices, {
|
||||||
|
foreignKey: "ID_ADMIN_EXERCISE",
|
||||||
|
as: "multipleChoices",
|
||||||
|
});
|
||||||
|
MultipleChoices.belongsTo(Exercise, {
|
||||||
|
foreignKey: "ID_ADMIN_EXERCISE",
|
||||||
|
as: "exerciseMultipleChoices",
|
||||||
|
});
|
||||||
|
|
||||||
|
Exercise.hasMany(MatchingPairs, {
|
||||||
|
foreignKey: "ID_ADMIN_EXERCISE",
|
||||||
|
as: "matchingPairs",
|
||||||
|
});
|
||||||
|
MatchingPairs.belongsTo(Exercise, {
|
||||||
|
foreignKey: "ID_ADMIN_EXERCISE",
|
||||||
|
as: "exerciseMatchingPairs",
|
||||||
|
});
|
||||||
|
|
||||||
|
Exercise.hasMany(TrueFalse, {
|
||||||
|
foreignKey: "ID_ADMIN_EXERCISE",
|
||||||
|
as: "trueFalse",
|
||||||
|
});
|
||||||
|
TrueFalse.belongsTo(Exercise, {
|
||||||
|
foreignKey: "ID_ADMIN_EXERCISE",
|
||||||
|
as: "exerciseTrueFalse",
|
||||||
|
});
|
||||||
|
|
||||||
|
User.hasMany(StdLearning, { foreignKey: "ID", as: "userLearning" });
|
||||||
|
StdLearning.belongsTo(User, { foreignKey: "ID", as: "learningUser" });
|
||||||
|
|
||||||
|
Level.hasMany(StdLearning, { foreignKey: "ID_LEVEL", as: "stdLearning" });
|
||||||
|
StdLearning.belongsTo(Level, { foreignKey: "ID_LEVEL", as: "level" });
|
||||||
|
|
||||||
|
Level.hasOne(StdLearning, { foreignKey: "NEXT_LEARNING", as: "nextLevel" });
|
||||||
|
StdLearning.belongsTo(Level, { foreignKey: "NEXT_LEARNING", as: "nextLevel" });
|
||||||
|
|
||||||
|
StdLearning.hasMany(StdExercise, {
|
||||||
|
foreignKey: "ID_STUDENT_LEARNING",
|
||||||
|
as: "stdExercises",
|
||||||
|
});
|
||||||
|
StdExercise.belongsTo(StdLearning, {
|
||||||
|
foreignKey: "ID_STUDENT_LEARNING",
|
||||||
|
as: "stdLearningExercise",
|
||||||
|
});
|
||||||
|
|
||||||
|
StdExercise.belongsTo(Exercise, {
|
||||||
|
foreignKey: "ID_ADMIN_EXERCISE",
|
||||||
|
as: "stdExerciseExercises",
|
||||||
|
});
|
||||||
|
Exercise.hasMany(StdExercise, {
|
||||||
|
foreignKey: "ID_ADMIN_EXERCISE",
|
||||||
|
as: "exerciseStdExercises",
|
||||||
|
});
|
||||||
|
|
||||||
|
Class.hasMany(Student, { foreignKey: "ID_CLASS", as: "ClassStudents" });
|
||||||
|
Student.belongsTo(Class, { foreignKey: "ID_CLASS", as: "studentClass" });
|
||||||
|
|
||||||
|
Monitoring.hasOne(StdLearning, {
|
||||||
|
foreignKey: "ID_STUDENT_LEARNING",
|
||||||
|
sourceKey: "ID_STUDENT_LEARNING",
|
||||||
|
as: "stdLearningMonitoring",
|
||||||
|
});
|
||||||
|
StdLearning.belongsTo(Monitoring, {
|
||||||
|
foreignKey: "ID_STUDENT_LEARNING",
|
||||||
|
as: "monitoringLearning",
|
||||||
|
});
|
||||||
|
|
||||||
|
Monitoring.hasMany(Class, { foreignKey: "ID_CLASS", as: "monitoringClasses" });
|
||||||
|
Class.belongsTo(Monitoring, { foreignKey: "ID_CLASS", as: "monitoringClass" });
|
||||||
|
|
||||||
|
Teacher.hasMany(Monitoring, {
|
||||||
|
foreignKey: "ID_GURU",
|
||||||
|
as: "teacherMonitorings",
|
||||||
|
});
|
||||||
|
Monitoring.belongsTo(Teacher, {
|
||||||
|
foreignKey: "ID_GURU",
|
||||||
|
as: "monitoringTeacher",
|
||||||
|
});
|
||||||
|
|
||||||
const models = {
|
const models = {
|
||||||
User: UserModel(Sequelize.DataTypes),
|
User,
|
||||||
Subject: SubjectModel(Sequelize.DataTypes),
|
Teacher,
|
||||||
Topic: TopicModel(Sequelize.DataTypes),
|
Student,
|
||||||
Level: LevelModel(Sequelize.DataTypes),
|
Section,
|
||||||
|
Topic,
|
||||||
|
Level,
|
||||||
|
Exercise,
|
||||||
|
MultipleChoices,
|
||||||
|
MatchingPairs,
|
||||||
|
TrueFalse,
|
||||||
|
StdLearning,
|
||||||
|
StdExercise,
|
||||||
|
Class,
|
||||||
|
Monitoring,
|
||||||
Sequelize,
|
Sequelize,
|
||||||
Op,
|
Op,
|
||||||
|
db,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default models;
|
export default models;
|
||||||
75
models/learningModels/stdExerciseModel.js
Normal file
75
models/learningModels/stdExerciseModel.js
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
import db from "../../database/db.js";
|
||||||
|
|
||||||
|
const StdExerciseModel = (DataTypes) => {
|
||||||
|
const StdExercise = db.define(
|
||||||
|
"student_exercise",
|
||||||
|
{
|
||||||
|
ID_STUDENT_EXERCISE: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
primaryKey: true,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
allowNull: false,
|
||||||
|
validate: {
|
||||||
|
notEmpty: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ID_ADMIN_EXERCISE: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: false,
|
||||||
|
validate: {
|
||||||
|
notEmpty: true,
|
||||||
|
},
|
||||||
|
references: {
|
||||||
|
model: "admin_exercise",
|
||||||
|
key: "ID_ADMIN_EXERCISE",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ID_STUDENT_LEARNING: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: false,
|
||||||
|
validate: {
|
||||||
|
notEmpty: true,
|
||||||
|
},
|
||||||
|
references: {
|
||||||
|
model: "student_learning",
|
||||||
|
key: "ID_STUDENT_LEARNING",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ANSWER_STUDENT: {
|
||||||
|
type: DataTypes.CHAR(1),
|
||||||
|
allowNull: false,
|
||||||
|
validate: {
|
||||||
|
notEmpty: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
IS_CORRECT: {
|
||||||
|
type: DataTypes.INTEGER(1),
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 0,
|
||||||
|
validate: {
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
RESULT_SCORE_STUDENT: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: null,
|
||||||
|
},
|
||||||
|
TIME_STUDENT_EXC: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: DataTypes.NOW,
|
||||||
|
onUpdate: DataTypes.NOW,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: false,
|
||||||
|
tableName: "student_exercise",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return StdExercise;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StdExerciseModel;
|
||||||
92
models/learningModels/stdLearningModel.js
Normal file
92
models/learningModels/stdLearningModel.js
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
import db from "../../database/db.js";
|
||||||
|
|
||||||
|
const StdLearningModel = (DataTypes) => {
|
||||||
|
const StdLearning = db.define(
|
||||||
|
"student_learning",
|
||||||
|
{
|
||||||
|
ID_STUDENT_LEARNING: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
primaryKey: true,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
allowNull: false,
|
||||||
|
validate: {
|
||||||
|
notEmpty: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ID: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: false,
|
||||||
|
validate: {
|
||||||
|
notEmpty: true,
|
||||||
|
},
|
||||||
|
references: {
|
||||||
|
model: "users",
|
||||||
|
key: "ID",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ID_LEVEL: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: false,
|
||||||
|
validate: {
|
||||||
|
notEmpty: true,
|
||||||
|
},
|
||||||
|
references: {
|
||||||
|
model: "level",
|
||||||
|
key: "ID_LEVEL",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
STUDENT_START: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: DataTypes.NOW,
|
||||||
|
},
|
||||||
|
STUDENT_FINISH: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: null,
|
||||||
|
},
|
||||||
|
SCORE: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: null,
|
||||||
|
},
|
||||||
|
IS_PASS: {
|
||||||
|
type: DataTypes.INTEGER(1),
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 0,
|
||||||
|
validate: {
|
||||||
|
min: 0,
|
||||||
|
max: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
NEXT_LEARNING: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: null,
|
||||||
|
references: {
|
||||||
|
model: "level",
|
||||||
|
key: "ID_LEVEL",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
FEEDBACK_STUDENT: {
|
||||||
|
type: DataTypes.STRING(200),
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: null,
|
||||||
|
},
|
||||||
|
TIME_LEARNING: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: DataTypes.NOW,
|
||||||
|
onUpdate: DataTypes.NOW,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: false,
|
||||||
|
tableName: "student_learning",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return StdLearning;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StdLearningModel;
|
||||||
|
|
@ -1,103 +0,0 @@
|
||||||
import db from "../database/db.js";
|
|
||||||
|
|
||||||
const LevelModel = (DataTypes) => {
|
|
||||||
const Levels = db.define(
|
|
||||||
"m_levels",
|
|
||||||
{
|
|
||||||
id: {
|
|
||||||
type: DataTypes.INTEGER,
|
|
||||||
primaryKey: true,
|
|
||||||
autoIncrement: true,
|
|
||||||
validate: {
|
|
||||||
notEmpty: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
type: DataTypes.STRING(100),
|
|
||||||
allowNull: false,
|
|
||||||
validate: {
|
|
||||||
notEmpty: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
subject_id: {
|
|
||||||
type: DataTypes.INTEGER,
|
|
||||||
allowNull: false,
|
|
||||||
validate: {
|
|
||||||
notEmpty: true,
|
|
||||||
},
|
|
||||||
references: {
|
|
||||||
model: 'm_subjects', // Name of the referenced table
|
|
||||||
key: 'id', // Key in the referenced table
|
|
||||||
},
|
|
||||||
},
|
|
||||||
topic_id: {
|
|
||||||
type: DataTypes.INTEGER,
|
|
||||||
allowNull: false,
|
|
||||||
validate: {
|
|
||||||
notEmpty: true,
|
|
||||||
},
|
|
||||||
references: {
|
|
||||||
model: 'm_topics', // Name of the referenced table
|
|
||||||
key: 'id', // Key in the referenced table
|
|
||||||
},
|
|
||||||
},
|
|
||||||
is_pretest: {
|
|
||||||
type: DataTypes.TINYINT(1),
|
|
||||||
allowNull: true,
|
|
||||||
defaultValue: 0,
|
|
||||||
validate: {
|
|
||||||
min: 0,
|
|
||||||
max: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
content: {
|
|
||||||
type: DataTypes.STRING(200),
|
|
||||||
allowNull: true,
|
|
||||||
},
|
|
||||||
video: {
|
|
||||||
type: DataTypes.STRING(200),
|
|
||||||
allowNull: true,
|
|
||||||
},
|
|
||||||
audio: {
|
|
||||||
type: DataTypes.STRING(200),
|
|
||||||
allowNull: true,
|
|
||||||
},
|
|
||||||
image: {
|
|
||||||
type: DataTypes.STRING(200),
|
|
||||||
allowNull: true,
|
|
||||||
},
|
|
||||||
youtube: {
|
|
||||||
type: DataTypes.STRING(200),
|
|
||||||
allowNull: true,
|
|
||||||
},
|
|
||||||
route1: {
|
|
||||||
type: DataTypes.INTEGER,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
route2: {
|
|
||||||
type: DataTypes.INTEGER,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
route3: {
|
|
||||||
type: DataTypes.INTEGER,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
route4: {
|
|
||||||
type: DataTypes.INTEGER,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
ts_entri: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
timestamps: false, // Disable Sequelize's automatic timestamp fields (createdAt, updatedAt)
|
|
||||||
tableName: "m_levels", // Ensure the table name matches the actual table name
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return Levels;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default LevelModel;
|
|
||||||
36
models/monitoringModels/classModel.js
Normal file
36
models/monitoringModels/classModel.js
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
import db from "../../database/db.js";
|
||||||
|
|
||||||
|
const ClassModel = (DataTypes) => {
|
||||||
|
const Classes = db.define(
|
||||||
|
"class",
|
||||||
|
{
|
||||||
|
ID_CLASS: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
primaryKey: true,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
allowNull: false,
|
||||||
|
validate: {
|
||||||
|
notEmpty: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
NAME_CLASS: {
|
||||||
|
type: DataTypes.STRING(100),
|
||||||
|
allowNull: false,
|
||||||
|
validate: {
|
||||||
|
notEmpty: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
TOTAL_STUDENT: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: false,
|
||||||
|
tableName: "class",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return Classes;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ClassModel;
|
||||||
56
models/monitoringModels/monitoringModel.js
Normal file
56
models/monitoringModels/monitoringModel.js
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
import db from "../../database/db.js";
|
||||||
|
|
||||||
|
const MonitoringModel = (DataTypes) => {
|
||||||
|
const Monitorings = db.define(
|
||||||
|
"monitoring",
|
||||||
|
{
|
||||||
|
ID_MONITORING: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
primaryKey: true,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
allowNull: false,
|
||||||
|
validate: {
|
||||||
|
notEmpty: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ID_STUDENT_LEARNING: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: false,
|
||||||
|
validate: {
|
||||||
|
notEmpty: true,
|
||||||
|
},
|
||||||
|
references: {
|
||||||
|
model: "student_learning",
|
||||||
|
key: "ID_STUDENT_LEARNING",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ID_CLASS: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: true,
|
||||||
|
references: {
|
||||||
|
model: "class",
|
||||||
|
key: "ID_CLASS",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ID_GURU: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: true,
|
||||||
|
references: {
|
||||||
|
model: "teacher",
|
||||||
|
key: "ID_GURU",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
FEEDBACK_GURU: {
|
||||||
|
type: DataTypes.STRING(200),
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: false,
|
||||||
|
tableName: "monitoring",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return Monitorings;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MonitoringModel;
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
import db from "../database/db.js";
|
|
||||||
|
|
||||||
const SubjectModel = (DataTypes) => {
|
|
||||||
const Subjects = db.define(
|
|
||||||
"m_subjects",
|
|
||||||
{
|
|
||||||
id: {
|
|
||||||
type: DataTypes.INTEGER,
|
|
||||||
primaryKey: true,
|
|
||||||
autoIncrement: true,
|
|
||||||
validate: {
|
|
||||||
notEmpty: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
name: {
|
|
||||||
type: DataTypes.STRING(50),
|
|
||||||
allowNull: false,
|
|
||||||
validate: {
|
|
||||||
notEmpty: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: false,
|
|
||||||
validate: {
|
|
||||||
notEmpty: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
icon: {
|
|
||||||
type: DataTypes.STRING(255),
|
|
||||||
allowNull: true,
|
|
||||||
},
|
|
||||||
thumbnail: {
|
|
||||||
type: DataTypes.STRING(255),
|
|
||||||
allowNull: true,
|
|
||||||
},
|
|
||||||
ts_entri: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
timestamps: false, // Disable Sequelize's automatic timestamp fields (createdAt, updatedAt)
|
|
||||||
tableName: "m_subjects", // Ensure the table name matches the actual table name
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return Subjects;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SubjectModel;
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
import db from "../database/db.js";
|
|
||||||
|
|
||||||
const TopicModel = (DataTypes) => {
|
|
||||||
const Topics = db.define(
|
|
||||||
"m_topics",
|
|
||||||
{
|
|
||||||
id: {
|
|
||||||
type: DataTypes.INTEGER,
|
|
||||||
primaryKey: true,
|
|
||||||
autoIncrement: true,
|
|
||||||
validate: {
|
|
||||||
notEmpty: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
subject_id: {
|
|
||||||
type: DataTypes.INTEGER,
|
|
||||||
allowNull: false,
|
|
||||||
validate: {
|
|
||||||
notEmpty: true,
|
|
||||||
},
|
|
||||||
references: {
|
|
||||||
model: 'm_subjects', // Name of the referenced table
|
|
||||||
key: 'id', // Key in the referenced table
|
|
||||||
},
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
type: DataTypes.STRING(255),
|
|
||||||
allowNull: false,
|
|
||||||
validate: {
|
|
||||||
notEmpty: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
ts_entri: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
timestamps: false, // Disable Sequelize's automatic timestamp fields (createdAt, updatedAt)
|
|
||||||
tableName: "m_topics", // Ensure the table name matches the actual table name
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return Topics;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TopicModel;
|
|
||||||
44
models/usersModels/studentModel.js
Normal file
44
models/usersModels/studentModel.js
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
import db from "../../database/db.js";
|
||||||
|
|
||||||
|
const StudentModel = (DataTypes) => {
|
||||||
|
const Students = db.define(
|
||||||
|
"student",
|
||||||
|
{
|
||||||
|
ID_SISWA: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
primaryKey: true,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
allowNull: false,
|
||||||
|
validate: {
|
||||||
|
notEmpty: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ID: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: false,
|
||||||
|
validate: {
|
||||||
|
notEmpty: true,
|
||||||
|
},
|
||||||
|
references: {
|
||||||
|
model: "users",
|
||||||
|
key: "ID",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
NISN: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
unique: true,
|
||||||
|
validate: {
|
||||||
|
notEmpty: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: false,
|
||||||
|
tableName: "student",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return Students;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StudentModel;
|
||||||
44
models/usersModels/teacherModel.js
Normal file
44
models/usersModels/teacherModel.js
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
import db from "../../database/db.js";
|
||||||
|
|
||||||
|
const TeacherModel = (DataTypes) => {
|
||||||
|
const Teachers = db.define(
|
||||||
|
"teacher",
|
||||||
|
{
|
||||||
|
ID_GURU: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
primaryKey: true,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
allowNull: false,
|
||||||
|
validate: {
|
||||||
|
notEmpty: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ID: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: false,
|
||||||
|
validate: {
|
||||||
|
notEmpty: true,
|
||||||
|
},
|
||||||
|
references: {
|
||||||
|
model: "users",
|
||||||
|
key: "ID",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
NIP: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
unique: true,
|
||||||
|
validate: {
|
||||||
|
notEmpty: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: false,
|
||||||
|
tableName: "teacher",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return Teachers;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TeacherModel;
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import db from "../database/db.js";
|
import db from "../../database/db.js";
|
||||||
|
|
||||||
const UserModel = (DataTypes) => {
|
const UserModel = (DataTypes) => {
|
||||||
const Users = db.define(
|
const Users = db.define(
|
||||||
"users",
|
"users",
|
||||||
{
|
{
|
||||||
id: {
|
ID: {
|
||||||
type: DataTypes.UUID,
|
type: DataTypes.UUID,
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
defaultValue: DataTypes.UUIDV4,
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
|
@ -13,14 +13,14 @@ const UserModel = (DataTypes) => {
|
||||||
notEmpty: true,
|
notEmpty: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
name: {
|
NAME_USERS: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
validate: {
|
validate: {
|
||||||
notEmpty: true,
|
notEmpty: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
email: {
|
EMAIL: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
unique: true,
|
unique: true,
|
||||||
|
|
@ -29,36 +29,22 @@ const UserModel = (DataTypes) => {
|
||||||
isEmail: true,
|
isEmail: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
email_verified_at: {
|
PASSWORD: {
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true,
|
|
||||||
},
|
|
||||||
password: {
|
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
},
|
},
|
||||||
roles: {
|
ROLE: {
|
||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
},
|
},
|
||||||
remember_token: {
|
PICTURE: {
|
||||||
type: DataTypes.STRING(100),
|
type: DataTypes.STRING,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
},
|
},
|
||||||
created_at: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
},
|
|
||||||
updated_at: {
|
|
||||||
type: DataTypes.DATE,
|
|
||||||
allowNull: true,
|
|
||||||
defaultValue: DataTypes.NOW,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
timestamps: false, // Disable Sequelize's automatic timestamp fields (createdAt, updatedAt)
|
timestamps: false,
|
||||||
tableName: "users", // Ensure the table name matches the actual table name
|
tableName: "users",
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
return Users;
|
return Users;
|
||||||
1
package-lock.json
generated
1
package-lock.json
generated
|
|
@ -15,6 +15,7 @@
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"moment-timezone": "^0.5.45",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
"mysql2": "^3.11.0",
|
"mysql2": "^3.11.0",
|
||||||
"nodemailer": "^6.9.14",
|
"nodemailer": "^6.9.14",
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"moment-timezone": "^0.5.45",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
"mysql2": "^3.11.0",
|
"mysql2": "^3.11.0",
|
||||||
"nodemailer": "^6.9.14",
|
"nodemailer": "^6.9.14",
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 63 KiB |
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 7.7 KiB |
|
|
@ -1,6 +1,7 @@
|
||||||
const response = (statusCode, data, message, res) => {
|
const response = (statusCode, data, message, res) => {
|
||||||
res.status(statusCode).json({
|
res.status(statusCode).json({
|
||||||
payload: data,
|
payload: data,
|
||||||
|
statusCode: statusCode,
|
||||||
message: message,
|
message: message,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
import express from "express";
|
|
||||||
import { registerUser, loginUser, logoutUser, forgotPassword, resetPassword } from "../controllers/auth.js";
|
|
||||||
|
|
||||||
const router = express.Router();
|
|
||||||
|
|
||||||
router.post("/register", registerUser);
|
|
||||||
|
|
||||||
router.post("/login", loginUser);
|
|
||||||
|
|
||||||
router.post("/logout", logoutUser);
|
|
||||||
|
|
||||||
router.post("/forgotPassword", forgotPassword)
|
|
||||||
|
|
||||||
router.post("/resetPassword", resetPassword)
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
21
routes/auth/auth.js
Normal file
21
routes/auth/auth.js
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import express from "express";
|
||||||
|
import { registerTeacher, registerStudent, registerAdmin, loginUser, logoutUser, forgotPassword, resetPassword } from "../../controllers/auth/auth.js";
|
||||||
|
import { verifyLoginUser, adminOnly } from "../../middlewares/User/authUser.js";
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.post("/register/teacher", registerTeacher);
|
||||||
|
|
||||||
|
router.post("/register/student", registerStudent);
|
||||||
|
|
||||||
|
router.post("/register/admin", verifyLoginUser, adminOnly, registerAdmin);
|
||||||
|
|
||||||
|
router.post("/login", loginUser);
|
||||||
|
|
||||||
|
router.post("/logout", logoutUser);
|
||||||
|
|
||||||
|
router.post("/forgotPassword", forgotPassword)
|
||||||
|
|
||||||
|
router.post("/resetPassword", resetPassword)
|
||||||
|
|
||||||
|
export default router;
|
||||||
34
routes/contents/exercise.js
Normal file
34
routes/contents/exercise.js
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
import express from "express";
|
||||||
|
import { getExercises, getExercisesForAdmin, getExerciseById, deleteExerciseById, deleteExerciseFileById } from "../../controllers/contentControllers/exercise.js";
|
||||||
|
import { createMultipleChoicesExercise, updateMultipleChoicesExerciseById } from "../../controllers/exerciseTypesControllers/multipleChoices.js";
|
||||||
|
import { createMatchingPairsExercise, updateMatchingPairsExerciseById } from "../../controllers/exerciseTypesControllers/matchingPairs.js";
|
||||||
|
import { createTrueFalseExercise, updateTrueFalseExerciseById } from "../../controllers/exerciseTypesControllers/trueFalse.js";
|
||||||
|
import { verifyLoginUser, adminOnly } from "../../middlewares/User/authUser.js";
|
||||||
|
import handleUpload from '../../middlewares/uploadExercise.js';
|
||||||
|
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.get("/exercise", verifyLoginUser, getExercises);
|
||||||
|
|
||||||
|
router.get("/exercise/admin", verifyLoginUser, adminOnly, getExercisesForAdmin);
|
||||||
|
|
||||||
|
router.get("/exercise/:id", verifyLoginUser, getExerciseById);
|
||||||
|
|
||||||
|
router.post("/exercise/multiple-choices", verifyLoginUser, adminOnly, handleUpload, createMultipleChoicesExercise);
|
||||||
|
|
||||||
|
router.post("/exercise/matching-pairs", verifyLoginUser, adminOnly, handleUpload, createMatchingPairsExercise);
|
||||||
|
|
||||||
|
router.post("/exercise/true-false", verifyLoginUser, adminOnly, handleUpload, createTrueFalseExercise);
|
||||||
|
|
||||||
|
router.put("/exercise/multiple-choices/:id", verifyLoginUser, adminOnly, handleUpload, updateMultipleChoicesExerciseById);
|
||||||
|
|
||||||
|
router.put("/exercise/matching-pairs/:id", verifyLoginUser, adminOnly, handleUpload, updateMatchingPairsExerciseById);
|
||||||
|
|
||||||
|
router.put("/exercise/true-false/:id", verifyLoginUser, adminOnly, handleUpload, updateTrueFalseExerciseById);
|
||||||
|
|
||||||
|
router.delete("/exercise/:id", verifyLoginUser, adminOnly, deleteExerciseById);
|
||||||
|
|
||||||
|
router.delete("/exercise/file/:id", verifyLoginUser, adminOnly, deleteExerciseFileById);
|
||||||
|
|
||||||
|
export default router
|
||||||
25
routes/contents/level.js
Normal file
25
routes/contents/level.js
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import express from "express";
|
||||||
|
// import { getAllLevels, getAllLevelById, getLevels, getLevelById, createLevel, updateLevelById, deleteLevelById, getRoutes, getRouteById, updateRouteById } from "../controllers/level.js";
|
||||||
|
import { getLevels, getLevelById, createLevel, updateLevelById, deleteLevelById } from "../../controllers/contentControllers/level.js";
|
||||||
|
import { verifyLoginUser, adminOnly } from "../../middlewares/User/authUser.js";
|
||||||
|
import handleUpload from '../../middlewares/Level/uploadLevel.js';
|
||||||
|
import {checkLevelsPerTopic, autoCalculateRoutes, getSectionAndTopicByLevelId } from '../../middlewares/Level/checkLevel.js';
|
||||||
|
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
// router.get("/levels", verifyLoginUser, adminOnly, getAllLevels);
|
||||||
|
|
||||||
|
// router.get("/levels/:id", verifyLoginUser, adminOnly, getAllLevelById);
|
||||||
|
|
||||||
|
router.get("/level", verifyLoginUser, getLevels);
|
||||||
|
|
||||||
|
router.get("/level/:id", verifyLoginUser, getLevelById);
|
||||||
|
|
||||||
|
router.post("/level", verifyLoginUser, adminOnly, handleUpload, checkLevelsPerTopic, autoCalculateRoutes, createLevel);
|
||||||
|
|
||||||
|
router.put("/level/:id", verifyLoginUser, adminOnly, handleUpload, getSectionAndTopicByLevelId, autoCalculateRoutes, updateLevelById);
|
||||||
|
|
||||||
|
router.delete("/level/:id", verifyLoginUser, adminOnly, getSectionAndTopicByLevelId, deleteLevelById);
|
||||||
|
|
||||||
|
export default router
|
||||||
19
routes/contents/section.js
Normal file
19
routes/contents/section.js
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import express from "express";
|
||||||
|
import handleUpload from '../../middlewares/uploadSection.js';
|
||||||
|
import { getSections, getSectionById, createSection, updateSectionById, deleteSectionById } from "../../controllers/contentControllers/section.js";
|
||||||
|
import { verifyLoginUser, adminOnly } from "../../middlewares/User/authUser.js";
|
||||||
|
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.get("/section", verifyLoginUser, getSections);
|
||||||
|
|
||||||
|
router.get("/section/:id", verifyLoginUser, getSectionById);
|
||||||
|
|
||||||
|
router.post("/section", verifyLoginUser, adminOnly, handleUpload, createSection);
|
||||||
|
|
||||||
|
router.put('/section/update/:id', verifyLoginUser, adminOnly, handleUpload, updateSectionById);
|
||||||
|
|
||||||
|
router.delete('/section/delete/:id', verifyLoginUser, adminOnly, deleteSectionById);
|
||||||
|
|
||||||
|
export default router
|
||||||
18
routes/contents/topic.js
Normal file
18
routes/contents/topic.js
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import express from "express";
|
||||||
|
import { getTopics, getTopicById, createTopic, updateTopicById, deleteTopicById } from "../../controllers/contentControllers/topic.js";
|
||||||
|
import { verifyLoginUser, adminOnly } from "../../middlewares/User/authUser.js";
|
||||||
|
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.get("/topic", verifyLoginUser, getTopics);
|
||||||
|
|
||||||
|
router.get("/topic/:id", verifyLoginUser, getTopicById);
|
||||||
|
|
||||||
|
router.post("/topic", verifyLoginUser, adminOnly, createTopic);
|
||||||
|
|
||||||
|
router.put("/topic/:id", verifyLoginUser, adminOnly, updateTopicById);
|
||||||
|
|
||||||
|
router.delete("/topic/:id", verifyLoginUser, adminOnly, deleteTopicById);
|
||||||
|
|
||||||
|
export default router
|
||||||
|
|
@ -1,15 +1,25 @@
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import user_routes from "./user.js";
|
import user_routes from "./user/user.js";
|
||||||
import auth_routes from "./auth.js";
|
import auth_routes from "./auth/auth.js";
|
||||||
import subject_routes from "./subject.js";
|
import section_routes from "./contents/section.js";
|
||||||
import topic_routes from "./topic.js";
|
import topic_routes from "./contents/topic.js";
|
||||||
import level_routes from "./level.js";
|
import level_routes from "./contents/level.js";
|
||||||
|
import exercise_routes from "./contents/exercise.js";
|
||||||
|
import stdLearning_routes from "./learning/stdLearning.js";
|
||||||
|
import stdExercise_routes from "./learning/stdExercise.js";
|
||||||
|
import class_routes from "./monitoring/class.js";
|
||||||
|
import monitoring_routes from "./monitoring/monitoring.js";
|
||||||
|
|
||||||
const route = express();
|
const route = express();
|
||||||
route.use(user_routes);
|
route.use(user_routes);
|
||||||
route.use(auth_routes);
|
route.use(auth_routes);
|
||||||
route.use(subject_routes);
|
route.use(section_routes);
|
||||||
route.use(topic_routes);
|
route.use(topic_routes);
|
||||||
route.use(level_routes);
|
route.use(level_routes);
|
||||||
|
route.use(exercise_routes);
|
||||||
|
route.use(stdLearning_routes);
|
||||||
|
route.use(stdExercise_routes);
|
||||||
|
route.use(class_routes);
|
||||||
|
route.use(monitoring_routes);
|
||||||
|
|
||||||
export default route;
|
export default route;
|
||||||
|
|
|
||||||
15
routes/learning/stdExercise.js
Normal file
15
routes/learning/stdExercise.js
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import express from "express";
|
||||||
|
import { getStdExercises, getStdExerciseById, stdAnswerExercise } from "../../controllers/learningControllers/stdExercise.js";
|
||||||
|
import { verifyLoginUser } from "../../middlewares/User/authUser.js";
|
||||||
|
import { updateStdLearningById } from "../../controllers/learningControllers/stdLearning.js";
|
||||||
|
import { checkCorrectAnswers,calculateScore, checkFirstFiveCorrect, nextLearning } from "../../middlewares/autoGrading.js";
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.get("/stdExercise", verifyLoginUser, getStdExercises);
|
||||||
|
|
||||||
|
router.get("/stdExercise/:id", verifyLoginUser, getStdExerciseById);
|
||||||
|
|
||||||
|
router.post("/stdExercise", verifyLoginUser, stdAnswerExercise, checkCorrectAnswers, calculateScore, checkFirstFiveCorrect, nextLearning, updateStdLearningById);
|
||||||
|
|
||||||
|
export default router
|
||||||
19
routes/learning/stdLearning.js
Normal file
19
routes/learning/stdLearning.js
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import express from "express";
|
||||||
|
import { getStdLearnings, getStdLearningById, createStdLearning, learningHistory, learningHistoryBySectionId, learningHistoryByTopicId } from "../../controllers/learningControllers/stdLearning.js";
|
||||||
|
import { verifyLoginUser } from "../../middlewares/User/authUser.js";
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.get("/stdLearning", verifyLoginUser, getStdLearnings);
|
||||||
|
|
||||||
|
router.get("/stdLearning/:id", verifyLoginUser, getStdLearningById);
|
||||||
|
|
||||||
|
router.get("/learningHistory", verifyLoginUser, learningHistory);
|
||||||
|
|
||||||
|
router.get("/learningHistory/section/:sectionId", verifyLoginUser, learningHistoryBySectionId);
|
||||||
|
|
||||||
|
router.get("/learningHistory/topic/:topicId", verifyLoginUser, learningHistoryByTopicId);
|
||||||
|
|
||||||
|
router.post("/stdLearning", verifyLoginUser, createStdLearning);
|
||||||
|
|
||||||
|
export default router
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
import express from "express";
|
|
||||||
import { getAllLevels, getAllLevelById, getLevels, getLevelById, createLevel, updateLevelById, deleteLevelById, getRoutes, getRouteById, updateRouteById } from "../controllers/level.js";
|
|
||||||
import { verifyLoginUser, adminOnly, teacherOnly } from "../middlewares/authUser.js";
|
|
||||||
import handleUpload from '../middlewares/uploadLevel.js';
|
|
||||||
import {checkMaxLevelsPerTopic } from '../middlewares/checkLevel.js';
|
|
||||||
|
|
||||||
|
|
||||||
const router = express.Router();
|
|
||||||
|
|
||||||
router.get("/levels", getAllLevels);
|
|
||||||
|
|
||||||
router.get("/levels/:id", getAllLevelById);
|
|
||||||
|
|
||||||
router.get("/level", getLevels);
|
|
||||||
|
|
||||||
router.get("/level/:id", getLevelById);
|
|
||||||
|
|
||||||
router.post("/level", handleUpload, checkMaxLevelsPerTopic, createLevel);
|
|
||||||
|
|
||||||
router.put("/level/:id", handleUpload, updateLevelById);
|
|
||||||
|
|
||||||
router.delete("/level/:id", deleteLevelById);
|
|
||||||
|
|
||||||
router.get("/route", getRoutes);
|
|
||||||
|
|
||||||
router.get("/route/:id", getRouteById);
|
|
||||||
|
|
||||||
router.put("/route/:id", updateRouteById);
|
|
||||||
|
|
||||||
export default router
|
|
||||||
19
routes/monitoring/class.js
Normal file
19
routes/monitoring/class.js
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import express from "express";
|
||||||
|
import { getClasses, getClassById, createClass, updateClassById, deleteClassById, updateStudentClassByName } from "../../controllers/monitoringControllers/class.js";
|
||||||
|
import { verifyLoginUser } from "../../middlewares/User/authUser.js";
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.get("/classes", verifyLoginUser, getClasses);
|
||||||
|
|
||||||
|
router.get("/class/:id", verifyLoginUser, getClassById);
|
||||||
|
|
||||||
|
router.post("/class", verifyLoginUser, createClass);
|
||||||
|
|
||||||
|
router.put("/class/update/:id", verifyLoginUser, updateClassById);
|
||||||
|
|
||||||
|
router.delete("/class/delete/:id", verifyLoginUser, deleteClassById);
|
||||||
|
|
||||||
|
router.post("/class/student/update", verifyLoginUser, updateStudentClassByName);
|
||||||
|
|
||||||
|
export default router
|
||||||
13
routes/monitoring/monitoring.js
Normal file
13
routes/monitoring/monitoring.js
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import express from "express";
|
||||||
|
import { monitoringStudentsProgress, monitoringStudentProgressById, monitoringFeedback } from "../../controllers/monitoringControllers/monitoring.js";
|
||||||
|
import { verifyLoginUser, adminOrTeacherOnly } from "../../middlewares/User/authUser.js";
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.get("/monitoring/progress", verifyLoginUser, adminOrTeacherOnly, monitoringStudentsProgress);
|
||||||
|
|
||||||
|
router.get("/monitoring/progress/:id", verifyLoginUser, adminOrTeacherOnly, monitoringStudentProgressById);
|
||||||
|
|
||||||
|
router.post("/monitoring/feedback/:id", verifyLoginUser, adminOrTeacherOnly, monitoringFeedback);
|
||||||
|
|
||||||
|
export default router
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
import express from "express";
|
|
||||||
import handleUpload from '../middlewares/uploadSubject.js';
|
|
||||||
import { getSubjects, getSubjectById, createSubject, updateSubjectById, deleteSubjectById } from "../controllers/subject.js";
|
|
||||||
import { verifyLoginUser, adminOnly, teacherOnly } from "../middlewares/authUser.js";
|
|
||||||
|
|
||||||
|
|
||||||
const router = express.Router();
|
|
||||||
|
|
||||||
router.get("/subject", verifyLoginUser, adminOnly, getSubjects);
|
|
||||||
|
|
||||||
router.get("/subject/:id", getSubjectById);
|
|
||||||
|
|
||||||
router.post("/subject", handleUpload, createSubject);
|
|
||||||
|
|
||||||
router.put('/subject/:id', handleUpload, updateSubjectById);
|
|
||||||
|
|
||||||
router.delete('/subject/:id', deleteSubjectById);
|
|
||||||
|
|
||||||
export default router
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
import express from "express";
|
|
||||||
import { getTopics, getTopicById, createTopic, updateTopicById, deleteTopicById } from "../controllers/topic.js";
|
|
||||||
import { verifyLoginUser, adminOnly, teacherOnly } from "../middlewares/authUser.js";
|
|
||||||
|
|
||||||
|
|
||||||
const router = express.Router();
|
|
||||||
|
|
||||||
router.get("/topic", getTopics);
|
|
||||||
|
|
||||||
router.get("/topic/:id", getTopicById);
|
|
||||||
|
|
||||||
router.post("/topic", createTopic);
|
|
||||||
|
|
||||||
router.put("/topic/:id", updateTopicById);
|
|
||||||
|
|
||||||
router.delete("/topic/:id", deleteTopicById);
|
|
||||||
|
|
||||||
export default router
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
import express from "express";
|
|
||||||
import { getUsers, getUserById, updateUserById, deleteUserById } from "../controllers/user.js";
|
|
||||||
import { verifyLoginUser, adminOnly, teacherOnly } from "../middlewares/authUser.js";
|
|
||||||
|
|
||||||
|
|
||||||
const router = express.Router();
|
|
||||||
|
|
||||||
router.get("/user", verifyLoginUser, adminOnly, getUsers);
|
|
||||||
|
|
||||||
router.get("/user/:id", getUserById);
|
|
||||||
|
|
||||||
router.post("/user/update/:id", updateUserById);
|
|
||||||
|
|
||||||
router.delete("/user/delete/:id", deleteUserById);
|
|
||||||
|
|
||||||
export default router
|
|
||||||
29
routes/user/user.js
Normal file
29
routes/user/user.js
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import express from "express";
|
||||||
|
import { getUsers, getAdmins, getTeachers, getStudents, getUserById, getUserByName, updateUserById, updateUserPasswordById, deleteUserById, getMe } from "../../controllers/usersControllers/user.js";
|
||||||
|
import { verifyLoginUser, adminOnly, adminOrTeacherOnly } from "../../middlewares/User/authUser.js";
|
||||||
|
import handleUpload from "../../middlewares/User/uploadUser.js";
|
||||||
|
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.get("/user", verifyLoginUser, adminOnly, getUsers);
|
||||||
|
|
||||||
|
router.get("/user/admin", verifyLoginUser, adminOnly, getAdmins);
|
||||||
|
|
||||||
|
router.get("/user/teacher", verifyLoginUser, adminOnly, getTeachers);
|
||||||
|
|
||||||
|
router.get("/user/student", verifyLoginUser, adminOrTeacherOnly, getStudents);
|
||||||
|
|
||||||
|
router.get("/user/:id", verifyLoginUser, getUserById);
|
||||||
|
|
||||||
|
router.get("/user/name/:name", verifyLoginUser, adminOrTeacherOnly, getUserByName);
|
||||||
|
|
||||||
|
router.get("/getMe", verifyLoginUser, getMe);
|
||||||
|
|
||||||
|
router.post("/user/update/:id", verifyLoginUser, handleUpload, updateUserById);
|
||||||
|
|
||||||
|
router.post("/user/update/password/:id", verifyLoginUser, updateUserPasswordById);
|
||||||
|
|
||||||
|
router.delete("/user/delete/:id", verifyLoginUser, adminOnly, deleteUserById);
|
||||||
|
|
||||||
|
export default router
|
||||||
Loading…
Reference in New Issue
Block a user