feat: monitoring data to csv

This commit is contained in:
elangptra 2024-10-28 09:36:05 +07:00
parent cbd3977858
commit c5aecb2af9
8 changed files with 444 additions and 22 deletions

View File

@ -1,6 +1,6 @@
APP_PORT = 3001
NODE_ENV = development
FE_DOMAIN = http://localhost:5173
CLIENT_URL = http://localhost:5173
DB_HOST = localhost
DB_USER = root

View File

@ -389,7 +389,7 @@ export const forgotPassword = async (req, res) => {
}
);
const resetLink = `${process.env.FE_DOMAIN}/resetPassword/${resetToken}`;
const resetLink = `${process.env.CLIENT_URL}/resetPassword/${resetToken}`;
const mailOptions = {
from: process.env.EMAIL_USER,

View File

@ -170,7 +170,6 @@ export const getStudentAnswersByStdLearningId = async (req, res) => {
const exerciseDetails = exercise.stdExerciseExercises;
const questionType = exerciseDetails.QUESTION_TYPE;
// Create the formatted exercise object
const formattedExercise = {
ID_STUDENT_EXERCISE: exerciseData.ID_STUDENT_EXERCISE,
ID_ADMIN_EXERCISE: exerciseDetails.ID_ADMIN_EXERCISE,
@ -183,7 +182,6 @@ export const getStudentAnswersByStdLearningId = async (req, res) => {
RESULT_SCORE_STUDENT: exercise.RESULT_SCORE_STUDENT,
};
// Include appropriate details based on question type
if (questionType === "MCQ") {
formattedExercise.multipleChoices = exerciseDetails.multipleChoices;
} else if (questionType === "MPQ") {
@ -193,12 +191,22 @@ export const getStudentAnswersByStdLearningId = async (req, res) => {
return formattedExercise;
});
return response(200, {
...stdLearningData,
stdExercises: mappedExercises,
}, "Student learning exercises retrieved successfully", res);
return response(
200,
{
...stdLearningData,
stdExercises: mappedExercises,
},
"Student learning exercises retrieved successfully",
res
);
} catch (error) {
console.error(error);
return response(500, null, "Error retrieving student learning exercises", res);
return response(
500,
null,
"Error retrieving student learning exercises",
res
);
}
};

View File

@ -1,5 +1,10 @@
import response from "../../response.js";
import models from "../../models/index.js";
import { createObjectCsvWriter } from "csv-writer";
import { Readable } from "stream";
import fs from "fs";
import os from "os";
import path from "path";
export const getMonitorings = async (req, res) => {
try {
@ -373,7 +378,21 @@ export const monitoringStudentProgressById = async (req, res) => {
} else if (sort === "score") {
return b.SCORE - a.SCORE;
} else if (sort === "feedback") {
return a.FEEDBACK_STUDENT.localeCompare(b.FEEDBACK_STUDENT);
if (a.FEEDBACK_STUDENT === null && b.FEEDBACK_STUDENT !== null) {
return 1;
} else if (
a.FEEDBACK_STUDENT !== null &&
b.FEEDBACK_STUDENT === null
) {
return -1;
} else if (
a.FEEDBACK_STUDENT === null &&
b.FEEDBACK_STUDENT === null
) {
return 0;
} else {
return a.FEEDBACK_STUDENT.localeCompare(b.FEEDBACK_STUDENT);
}
} else if (sort === "start") {
return new Date(a.STUDENT_START) - new Date(b.STUDENT_START);
} else if (sort === "finish") {
@ -848,27 +867,41 @@ export const getClassMonitoringDataByClassAndTopic = async (req, res) => {
"STUDENT_START",
"STUDENT_FINISH",
],
order: [["STUDENT_FINISH", "DESC"]],
});
const sortedRecords = allStdLearning.sort((a, b) => {
const userComparison = a.learningUser.NAME_USERS.localeCompare(
b.learningUser.NAME_USERS
);
if (userComparison !== 0) {
return userComparison;
}
if (sort === "nisn") {
return String(a.learningUser.students.NISN).localeCompare(
String(b.learningUser.students.NISN)
);
} else if (sort === "user") {
return a.learningUser.NAME_USERS.localeCompare(
b.learningUser.NAME_USERS
);
} else if (sort === "name") {
return userComparison;
} else if (sort === "level") {
return a.level.NAME_LEVEL.localeCompare(b.level.NAME_LEVEL);
} else if (sort === "score") {
return b.SCORE - a.SCORE;
} else if (sort === "feedback") {
return a.FEEDBACK_STUDENT.localeCompare(b.FEEDBACK_STUDENT);
if (a.FEEDBACK_STUDENT === null && b.FEEDBACK_STUDENT !== null) {
return 1;
} else if (a.FEEDBACK_STUDENT !== null && b.FEEDBACK_STUDENT === null) {
return -1;
} else if (a.FEEDBACK_STUDENT === null && b.FEEDBACK_STUDENT === null) {
return 0;
} else {
return a.FEEDBACK_STUDENT.localeCompare(b.FEEDBACK_STUDENT);
}
} else if (sort === "start") {
return new Date(a.STUDENT_START) - new Date(b.STUDENT_START);
}
// Default sorting by student finish
return new Date(a.STUDENT_FINISH) - new Date(b.STUDENT_FINISH);
});
@ -956,8 +989,6 @@ export const monitoringFeedbackByClassAndTopic = async (req, res) => {
],
});
console.log("Monitoring Data:", JSON.stringify(monitoringData, null, 2));
if (!monitoringData || monitoringData.length === 0) {
return response(
404,
@ -1027,3 +1058,375 @@ export const monitoringFeedbackByClassAndTopic = async (req, res) => {
response(500, null, "Error updating teacher feedback!", res);
}
};
export const monitoringStudentProgressCSVById = 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"],
include: [
{
model: models.Topic,
as: "levelTopic",
attributes: ["NAME_TOPIC"],
include: [
{
model: models.Section,
as: "topicSection",
attributes: ["NAME_SECTION"],
},
],
},
],
},
{
model: models.User,
as: "learningUser",
attributes: ["NAME_USERS"],
include: [
{
model: models.Student,
as: "students",
attributes: ["NISN"],
},
],
},
],
},
],
});
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 userID = stdLearning.ID;
const topicID = stdLearning.level.ID_TOPIC;
const studentName = stdLearning.learningUser.NAME_USERS;
const nisn = stdLearning.learningUser.students.NISN;
const topicName = stdLearning.level.levelTopic.NAME_TOPIC;
const sectionName = stdLearning.level.levelTopic.topicSection.NAME_SECTION;
const levels = await models.StdLearning.findAll({
where: {
ID: userID,
STUDENT_FINISH: {
[models.Sequelize.Op.ne]: null,
},
},
include: [
{
model: models.Level,
as: "level",
attributes: ["NAME_LEVEL", "ID_TOPIC"],
where: {
ID_TOPIC: topicID,
},
},
],
attributes: [
"SCORE",
"FEEDBACK_STUDENT",
"STUDENT_START",
"STUDENT_FINISH",
],
order: [["STUDENT_FINISH", "DESC"]],
distinct: true,
});
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 tempDir = os.tmpdir();
const tempFilePath = path.join(
tempDir,
`Student_Monitoring_${nisn}_${studentName}.csv`
);
const csvWriter = createObjectCsvWriter({
path: tempFilePath,
header: [
{ id: "field", title: "Field" },
{ id: "value", title: "Value" },
],
});
const records = [
{ field: "Section Name", value: sectionName },
{ field: "Topic Name", value: topicName },
{ field: "Student Name", value: studentName },
{ field: "NISN", value: nisn },
];
await csvWriter.writeRecords(records);
const levelCsvWriter = createObjectCsvWriter({
path: tempFilePath,
append: true,
header: [
{ id: "NAME_LEVEL", title: "Level Name" },
{ id: "SCORE", title: "Score" },
{ id: "FEEDBACK_STUDENT", title: "Student Feedback" },
{ id: "STUDENT_START", title: "Start Exercise" },
{ id: "STUDENT_FINISH", title: "Finish Exercise" },
],
});
fs.appendFileSync(tempFilePath, "\n");
fs.appendFileSync(
tempFilePath,
"Name Level,Score,Student Feedback,Start Exercise,Finish Exercise\n"
);
await levelCsvWriter.writeRecords(levelArray);
res.setHeader("Content-Type", "text/csv");
res.setHeader(
"Content-Disposition",
`attachment; filename="Student_Monitoring_${nisn}_${studentName}.csv.csv"`
);
const fileStream = fs.createReadStream(tempFilePath);
fileStream.pipe(res).on("finish", () => {
fs.unlinkSync(tempFilePath);
});
} catch (error) {
console.error(error);
response(500, null, "Error retrieving student progress!", res);
}
};
export const classMonitoringCSVByClassAndTopic = async (req, res) => {
const { ID_CLASS, ID_TOPIC } = req.body;
try {
const classData = await models.Class.findOne({
where: { ID_CLASS: ID_CLASS },
attributes: ["ID_CLASS", "NAME_CLASS"],
});
if (!classData) {
return response(404, null, "Class not found!", res);
}
const className = classData.NAME_CLASS;
const topicData = await models.Topic.findOne({
where: { ID_TOPIC: ID_TOPIC },
include: [
{
model: models.Section,
as: "topicSection",
attributes: ["ID_SECTION", "NAME_SECTION"],
},
],
attributes: ["ID_TOPIC", "NAME_TOPIC"],
});
if (!topicData) {
return response(404, null, "Topic not found!", res);
}
const topicName = topicData.NAME_TOPIC;
const sectionName = topicData.topicSection?.NAME_SECTION;
const monitoringData = await models.Monitoring.findAll({
where: { ID_CLASS: ID_CLASS },
include: [
{
model: models.StdLearning,
as: "stdLearningMonitoring",
required: true,
attributes: ["ID", "ID_LEVEL"],
include: [
{
model: models.Level,
as: "level",
attributes: ["ID_LEVEL", "ID_TOPIC"],
where: { ID_TOPIC: ID_TOPIC },
required: true,
},
],
},
],
});
if (!monitoringData || monitoringData.length === 0) {
const result = {
ID_CLASS: classData.ID_CLASS,
ID_SECTION: topicData.topicSection?.ID_SECTION,
ID_TOPIC: topicData.ID_TOPIC,
NAME_CLASS: className,
NAME_SECTION: sectionName,
NAME_TOPIC: topicName,
};
return response(
200,
result,
"No monitoring data found for this class and topic!",
res
);
}
const userIds = monitoringData.map(
(monitoring) => monitoring.stdLearningMonitoring.ID
);
const allStdLearning = await models.StdLearning.findAll({
where: {
ID_LEVEL: {
[models.Sequelize.Op.in]: (
await models.Level.findAll({
where: { ID_TOPIC: ID_TOPIC },
attributes: ["ID_LEVEL"],
})
).map((level) => level.ID_LEVEL),
},
ID: {
[models.Sequelize.Op.in]: userIds,
},
STUDENT_FINISH: {
[models.Sequelize.Op.ne]: null,
},
},
include: [
{
model: models.Level,
as: "level",
attributes: ["NAME_LEVEL", "ID_TOPIC"],
},
{
model: models.User,
as: "learningUser",
attributes: ["NAME_USERS"],
include: [
{
model: models.Student,
as: "students",
attributes: ["NISN"],
},
],
},
],
attributes: [
"SCORE",
"FEEDBACK_STUDENT",
"STUDENT_START",
"STUDENT_FINISH",
],
order: [
[{ model: models.User, as: "learningUser" }, "NAME_USERS", "ASC"],
["STUDENT_FINISH", "DESC"],
],
});
const formattedData = allStdLearning.map((stdLearning) => {
const level = stdLearning?.level;
const user = stdLearning?.learningUser;
const student = user?.students;
return {
NISN: student?.NISN,
NAME_USERS: user?.NAME_USERS,
NAME_LEVEL: level?.NAME_LEVEL,
SCORE: stdLearning?.SCORE,
FEEDBACK_STUDENT: stdLearning?.FEEDBACK_STUDENT,
STUDENT_START: stdLearning?.STUDENT_START,
STUDENT_FINISH: stdLearning?.STUDENT_FINISH,
};
});
const sanitizedClassName = className.replace(/\s+/g, "_");
const sanitizedTopicName = topicName.replace(/\s+/g, "_");
const tempDir = os.tmpdir();
const tempFilePath = path.join(
tempDir,
`Class_Monitoring_${sanitizedClassName}_${sanitizedTopicName}.csv`
);
const csvWriter = createObjectCsvWriter({
path: tempFilePath,
header: [
{ id: "field", title: "Field" },
{ id: "value", title: "Value" },
],
});
const records = [
{ field: "Class Name", value: className },
{ field: "Section Name", value: sectionName },
{ field: "Topic Name", value: topicName },
];
await csvWriter.writeRecords(records);
const levelCsvWriter = createObjectCsvWriter({
path: tempFilePath,
append: true,
header: [
{ id: "NISN", title: "NISN" },
{ id: "NAME_USERS", title: "Student Name" },
{ id: "NAME_LEVEL", title: "Level Name" },
{ id: "SCORE", title: "Score" },
{ id: "FEEDBACK_STUDENT", title: "Student Feedback" },
{ id: "STUDENT_START", title: "Start Exercise" },
{ id: "STUDENT_FINISH", title: "Finish Exercise" },
],
});
fs.appendFileSync(tempFilePath, "\n");
fs.appendFileSync(
tempFilePath,
"NISN,Student Name,Level Name,Score,Student Feedback,Start Exercise,Finish Exercise\n"
);
await levelCsvWriter.writeRecords(formattedData);
res.setHeader("Content-Type", "text/csv");
res.setHeader(
"Content-Disposition",
`attachment; filename=Class_Monitoring_${sanitizedClassName}_${sanitizedTopicName}.csv`
);
const fileStream = fs.createReadStream(tempFilePath);
fileStream.pipe(res).on("finish", () => {
fs.unlinkSync(tempFilePath);
});
} catch (error) {
console.error("Error fetching monitoring data:", error);
response(500, null, "Error retrieving monitoring data!", res);
}
};

View File

@ -9,7 +9,7 @@ dotenv.config();
const app = express();
const corsOptions = {
origin: "http://localhost:5173",
origin: `${process.env.CLIENT_URL}`,
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allowedHeaders: ["Content-Type", "Authorization"],
credentials: true,
@ -27,6 +27,6 @@ app.use(express.static("public"));
app.listen(process.env.APP_PORT, () => {
testConnection();
console.log(
`Server running on port http://localhost:${process.env.APP_PORT}`
`Server running on port ${process.env.APP_PORT}`
);
});

6
package-lock.json generated
View File

@ -12,6 +12,7 @@
"bcrypt": "^5.1.1",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"csv-writer": "^1.6.0",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"jsonwebtoken": "^9.0.2",
@ -646,6 +647,11 @@
"node": ">= 8"
}
},
"node_modules/csv-writer": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/csv-writer/-/csv-writer-1.6.0.tgz",
"integrity": "sha512-NOx7YDFWEsM/fTRAJjRpPp8t+MKRVvniAg9wQlUKx20MFrPs73WLJhFf5iteqrxNYnsy924K3Iroh3yNHeYd2g=="
},
"node_modules/d": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz",

View File

@ -23,6 +23,7 @@
"bcrypt": "^5.1.1",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"csv-writer": "^1.6.0",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"jsonwebtoken": "^9.0.2",

View File

@ -1,5 +1,5 @@
import express from "express";
import { getMonitorings, getMonitoringById,monitoringStudentsProgress, monitoringStudentProgressById, getClassMonitoringByClassId, getClassMonitoringDataByClassAndTopic, getMonitoringByTopicId, monitoringFeedback, monitoringFeedbackByClassAndTopic } from "../../controllers/monitoringControllers/monitoring.js";
import { getMonitorings, getMonitoringById,monitoringStudentsProgress, monitoringStudentProgressById, getClassMonitoringByClassId, getClassMonitoringDataByClassAndTopic, getMonitoringByTopicId, monitoringFeedback, monitoringFeedbackByClassAndTopic, monitoringStudentProgressCSVById, classMonitoringCSVByClassAndTopic } from "../../controllers/monitoringControllers/monitoring.js";
import { verifyLoginUser, adminOrTeacherOnly } from "../../middlewares/User/authUser.js";
const router = express.Router();
@ -12,12 +12,16 @@ router.get("/monitoring/class", verifyLoginUser, getClassMonitoringDataByClassAn
router.get("/monitoring/:id", verifyLoginUser, getMonitoringById);
router.get("/monitoring/class/csv", verifyLoginUser, adminOrTeacherOnly, classMonitoringCSVByClassAndTopic);
router.get("/monitoring/class/:classId", verifyLoginUser, adminOrTeacherOnly, getClassMonitoringByClassId);
router.get("/monitoring/topic/:topicId", verifyLoginUser, getMonitoringByTopicId);
router.get("/monitoring/progress/:id", verifyLoginUser, adminOrTeacherOnly, monitoringStudentProgressById);
router.get("/monitoring/progress/csv/:id", verifyLoginUser, adminOrTeacherOnly, monitoringStudentProgressCSVById);
router.post("/monitoring/feedback/class", verifyLoginUser, adminOrTeacherOnly, monitoringFeedbackByClassAndTopic);
router.post("/monitoring/feedback/:id", verifyLoginUser, adminOrTeacherOnly, monitoringFeedback);