diff --git a/server/src/cronjobs.js b/server/src/cronjobs.js index cd179fd..8d0286f 100644 --- a/server/src/cronjobs.js +++ b/server/src/cronjobs.js @@ -1,32 +1,40 @@ -const { CronJob } = require('cron'); -const { getProposalsThatExpireInXDays, notifyProposalExpiration } = require('./theses-dao'); +const { CronJob } = require("cron"); +const { getProposalsThatExpireInXDays } = require("./dao/proposals"); +const { notifyProposalExpiration } = require("./dao/misc"); const cronjobNames = { - THESIS_EXPIRED: 'THESIS_EXPIRED' + THESIS_EXPIRED: "THESIS_EXPIRED", }; - const cronjobs = {}; const initCronjobs = () => { - cronjobs[cronjobNames.THESIS_EXPIRED] = new CronJob("0 0 * * *" , () => { - const proposals = getProposalsThatExpireInXDays(7); - if(proposals) { - proposals.forEach((proposal) => { - notifyProposalExpiration(proposal); - }); - } - }, undefined, true, "Europe/Rome", undefined, true); + cronjobs[cronjobNames.THESIS_EXPIRED] = new CronJob( + "0 0 * * *", + () => { + const proposals = getProposalsThatExpireInXDays(7); + if (proposals) { + proposals.forEach((proposal) => { + notifyProposalExpiration(proposal); + }); + } + }, + undefined, + true, + "Europe/Rome", + undefined, + true, + ); }; const runCronjob = (cronjobName) => { - console.log('[Cron] Running job ' + cronjobName); - if(cronjobs[cronjobName]) { - cronjobs[cronjobName].fireOnTick(); - } + console.log("[Cron] Running job " + cronjobName); + if (cronjobs[cronjobName]) { + cronjobs[cronjobName].fireOnTick(); + } }; module.exports = { - cronjobNames, - runCronjob, - initCronjobs -}; \ No newline at end of file + cronjobNames, + runCronjob, + initCronjobs, +}; diff --git a/server/src/dao/applications.js b/server/src/dao/applications.js new file mode 100644 index 0000000..8d68fc3 --- /dev/null +++ b/server/src/dao/applications.js @@ -0,0 +1,154 @@ +"use strict"; + +const { db } = require("../db"); + +exports.insertApplication = (proposal, student, state) => { + const result = db + .prepare( + "insert into APPLICATIONS(proposal_id, student_id, state) values (?,?,?)", + ) + .run(proposal, student, state); + const applicationId = result.lastInsertRowid; + return { + application_id: applicationId, + proposal_id: proposal, + student_id: student, + state: state, + }; +}; + +exports.insertPDFInApplication = (file, applicationId) => { + return db + .prepare( + "update APPLICATIONS set attached_file = ? where main.APPLICATIONS.id = ?", + ) + .run(file, applicationId); +}; + +exports.getApplicationById = (id) => { + return db.prepare("select * from APPLICATIONS where id = ?").get(id); +}; + +exports.cancelPendingApplications = (of_proposal) => { + db.prepare( + "update APPLICATIONS set state = 'canceled' where proposal_id = ? AND state = 'pending'", + ).run(of_proposal); +}; + +exports.cancelPendingApplicationsOfStudent = (student_id) => { + db.prepare( + "update APPLICATIONS set state = 'canceled' where main.APPLICATIONS.student_id = ? and state = 'pending'", + ).run(student_id); +}; + +exports.updateApplication = (id, state) => { + db.prepare("update APPLICATIONS set state = ? where id = ?").run(state, id); +}; + +exports.getAcceptedApplicationsOfStudent = (student_id) => { + return db + .prepare( + `select * from APPLICATIONS where student_id = ? and state = 'accepted'`, + ) + .all(student_id); +}; + +exports.getPendingApplicationsOfStudent = (student_id) => { + return db + .prepare( + `select * from APPLICATIONS where student_id = ? and state = 'pending'`, + ) + .all(student_id); +}; + +exports.findRejectedApplication = (proposal_id, student_id) => { + return db + .prepare( + `select * from APPLICATIONS where proposal_id = ? and student_id = ? and state = 'rejected'`, + ) + .get(proposal_id, student_id); +}; + +/** + * @param teacher_id + * @returns {[ + * { + * application_id, + * proposal_id, + * teacher_id, + * state, + * student_name, + * student_surname, + * teacher_name, + * teacher_surname + * title + * } + * ]} + */ +exports.getApplicationsOfTeacher = (teacher_id) => { + return db + .prepare( + `select APPLICATIONS.id, + APPLICATIONS.proposal_id, + APPLICATIONS.student_id, + APPLICATIONS.state, + APPLICATIONS.attached_file, + STUDENT.name as student_name, + STUDENT.surname as student_surname, + TEACHER.name as teacher_name, + TEACHER.surname as teacher_surname, + PROPOSALS.title as title + from APPLICATIONS, + PROPOSALS, + STUDENT, + TEACHER + where APPLICATIONS.proposal_id = PROPOSALS.id + and PROPOSALS.supervisor = TEACHER.id + and APPLICATIONS.student_id = STUDENT.id + and PROPOSALS.supervisor = ?`, + ) + .all(teacher_id); +}; + +exports.getApplicationsOfStudent = (student_id) => { + return db + .prepare( + `select APPLICATIONS.id, + APPLICATIONS.proposal_id, + APPLICATIONS.student_id, + APPLICATIONS.state, + APPLICATIONS.attached_file, + STUDENT.name as student_name, + STUDENT.surname as student_surname, + TEACHER.name as teacher_name, + TEACHER.surname as teacher_surname, + PROPOSALS.title as title + from APPLICATIONS, + PROPOSALS, + STUDENT, + TEACHER + where APPLICATIONS.proposal_id = PROPOSALS.id + and PROPOSALS.supervisor = TEACHER.id + and APPLICATIONS.student_id = STUDENT.id + and APPLICATIONS.student_id = ?`, + ) + .all(student_id); +}; + +/** + * Is the given application accepted? + * @param proposal_id the proposal of the application + * @param student_id the student who applied + * @returns {boolean} accepted or not + */ +exports.isAccepted = (proposal_id, student_id) => { + const accepted_proposal = db + .prepare( + `select * from main.APPLICATIONS + where APPLICATIONS.proposal_id = ? + and APPLICATIONS.student_id = ? + and APPLICATIONS.state = 'accepted'`, + ) + .get(proposal_id, student_id); + return accepted_proposal !== undefined; +}; diff --git a/server/src/dao/dao_utils/utils.js b/server/src/dao/dao_utils/utils.js new file mode 100644 index 0000000..58049f6 --- /dev/null +++ b/server/src/dao/dao_utils/utils.js @@ -0,0 +1,13 @@ +"use strict"; +function getCosupervisorsFromProposal(proposal) { + return proposal.co_supervisors ? proposal.co_supervisors.split(", ") : []; +} + +function getArrayDifference(arrayIn, arrayNotIn) { + return arrayIn.filter((el) => !arrayNotIn.includes(el)); +} + +module.exports = { + getCosupervisorsFromProposal, + getArrayDifference, +}; diff --git a/server/src/dao/misc.js b/server/src/dao/misc.js new file mode 100644 index 0000000..cfccd05 --- /dev/null +++ b/server/src/dao/misc.js @@ -0,0 +1,356 @@ +"use strict"; + +const { db } = require("../db"); +const { nodemailer } = require("../smtp"); +const { applicationDecisionTemplate } = require("../mail/application-decision"); +const { newApplicationTemplate } = require("../mail/new-application"); +const dayjs = require("dayjs"); +const { proposalExpirationTemplate } = require("../mail/proposal-expiration"); +const { + supervisorStartRequestTemplate, +} = require("../mail/supervisor-start-request"); +const { + cosupervisorApplicationDecisionTemplate, +} = require("../mail/cosupervisor-application-decision"); +const { + cosupervisorStartRequestTemplate, +} = require("../mail/cosupervisor-start-request"); +const { removedCosupervisorTemplate } = require("../mail/removed-cosupervisor"); +const { getTeacherByEmail, getTeacherEmailById } = require("./user"); +const { + getCosupervisorsFromProposal, + getArrayDifference, +} = require("./dao_utils/utils"); +const { + changesStartRequestStudentTemplate, +} = require("../mail/changes-start-request-student"); +const { addedCosupervisorTemplate } = require("../mail/added-cosupervisor"); + +exports.getExamsOfStudent = (id) => { + return db.prepare("select * from main.CAREER where id = ?").all(id); +}; + +exports.getGroup = (cod_group) => { + return db.prepare("select * from GROUPS where cod_group = ?").get(cod_group); +}; + +exports.getGroups = () => { + return db.prepare("select cod_group from GROUPS").all(); +}; + +exports.getDegrees = () => { + return db.prepare("select cod_degree, title_degree from DEGREE").all(); +}; + +exports.notifyApplicationDecision = async (applicationId, decision) => { + // Retrieve the data + const applicationJoined = db + .prepare( + `SELECT S.id, P.title, P.co_supervisors, S.email, S.surname, S.name + FROM APPLICATIONS A + JOIN PROPOSALS P ON P.id = A.proposal_id + JOIN STUDENT S ON S.id = A.student_id + WHERE A.id = ?`, + ) + .get(applicationId); + let mailBody; + // Notify the student + // -- Email + mailBody = applicationDecisionTemplate({ + name: applicationJoined.surname + " " + applicationJoined.name, + thesis: applicationJoined.title, + decision: decision, + }); + try { + await nodemailer.sendMail({ + to: applicationJoined.email, + subject: "New decision on your thesis application", + text: mailBody.text, + html: mailBody.html, + }); + } catch (e) { + console.log("[mail service]", e); + } + // -- Website notification + db.prepare( + "INSERT INTO NOTIFICATIONS(student_id, object, content, date) VALUES(?,?,?, DATETIME(DATETIME('now'), '+' || (select delta from VIRTUAL_CLOCK where id = 1) || ' days'))", + ).run( + applicationJoined.id, + "New decision on your thesis application", + mailBody.text, + ); + // Notify the co-supervisors + if (applicationJoined.co_supervisors) { + for (const cosupervisor of applicationJoined.co_supervisors.split(", ")) { + const fullCosupervisor = getTeacherByEmail(cosupervisor); + // -- Email + mailBody = cosupervisorApplicationDecisionTemplate({ + name: fullCosupervisor.surname + " " + fullCosupervisor.name, + thesis: applicationJoined.title, + decision: decision, + }); + try { + await nodemailer.sendMail({ + to: cosupervisor, + subject: "New decision for a thesis you co-supervise", + text: mailBody.text, + html: mailBody.html, + }); + } catch (e) { + console.log("[mail service]", e); + } + // -- Website notification + db.prepare( + "INSERT INTO NOTIFICATIONS(teacher_id, object, content, date) VALUES(?,?,?, DATETIME(DATETIME('now'), '+' || (select delta from VIRTUAL_CLOCK where id = 1) || ' days'))", + ).run( + fullCosupervisor.id, + "New decision for a thesis you co-supervise", + mailBody.text, + ); + } + } +}; + +exports.notifyNewStartRequest = async (requestId) => { + const requestJoined = db + .prepare( + `SELECT S.student_id, S.supervisor, S.co_supervisors, T.name, T.surname + FROM START_REQUESTS S + JOIN TEACHER T ON T.id = S.supervisor + WHERE S.id = ?`, + ) + .get(requestId); + // Send email to the supervisor + let mailBody = supervisorStartRequestTemplate({ + name: requestJoined.surname + " " + requestJoined.name, + student: requestJoined.student_id, + }); + const teacher = getTeacherEmailById(requestJoined.supervisor); + try { + await nodemailer.sendMail({ + to: teacher.email, + subject: "New start request", + text: mailBody.text, + html: mailBody.html, + }); + } catch (e) { + console.log("[mail service]", e); + } + + // Save email in DB + db.prepare( + "INSERT INTO NOTIFICATIONS(teacher_id, object, content, date) VALUES(?,?,?, DATETIME(DATETIME('now'), '+' || (select delta from VIRTUAL_CLOCK where id = 1) || ' days'))", + ).run(requestJoined.supervisor, "New start request", mailBody.text); + + // Send email to the co-supervisors + if (requestJoined.co_supervisors) { + const coSupervisors = requestJoined.co_supervisors.split(", "); + for (const coSupervisorEmail of coSupervisors) { + const coSupervisor = getTeacherByEmail(coSupervisorEmail); + mailBody = cosupervisorStartRequestTemplate({ + name: coSupervisor.surname + " " + coSupervisor.name, + student: requestJoined.student_id, + }); + try { + await nodemailer.sendMail({ + to: coSupervisorEmail, + subject: "New start request", + text: mailBody.text, + html: mailBody.html, + }); + } catch (e) { + console.log("[mail service]", e); + } + + // Save email in DB + db.prepare( + "INSERT INTO NOTIFICATIONS(teacher_id, object, content, date) VALUES(?,?,?, DATETIME(DATETIME('now'), '+' || (select delta from VIRTUAL_CLOCK where id = 1) || ' days'))", + ).run(coSupervisor.id, "New start request", mailBody.text); + } + } +}; + +exports.notifyNewApplication = async (proposalId) => { + // Send email to the supervisor + const proposalJoined = db + .prepare( + `SELECT P.title, T.id, T.email, T.surname, T.name + FROM PROPOSALS P + JOIN TEACHER T ON T.id = P.supervisor + WHERE P.id = ?`, + ) + .get(proposalId); + const mailBody = newApplicationTemplate({ + name: proposalJoined.surname + " " + proposalJoined.name, + thesis: proposalJoined.title, + }); + try { + await nodemailer.sendMail({ + to: proposalJoined.email, + subject: "New application on your thesis proposal", + text: mailBody.text, + html: mailBody.html, + }); + } catch (e) { + console.log("[mail service]", e); + } + + // Save email in DB + db.prepare( + "INSERT INTO NOTIFICATIONS(teacher_id, object, content, date) VALUES(?,?,?, DATETIME(DATETIME('now'), '+' || (select delta from VIRTUAL_CLOCK where id = 1) || ' days'))", + ).run( + proposalJoined.id, + "New application on your thesis proposal", + mailBody.text, + ); +}; + +exports.notifyChangesRequestedOnStartRequest = async ( + message, + startRequestId, +) => { + // Send email to the supervisor + const startRequestJoined = db + .prepare( + `SELECT SR.title, S.id, S.email, S.surname, S.name + FROM START_REQUESTS SR + JOIN STUDENT S ON S.id = SR.student_id + WHERE SR.id = ?`, + ) + .get(startRequestId); + const mailBody = changesStartRequestStudentTemplate({ + name: startRequestJoined.surname + " " + startRequestJoined.name, + startRequest: startRequestJoined.title, + changes: message, + }); + try { + await nodemailer.sendMail({ + to: startRequestJoined.email, + subject: "Your start request requires changes", + text: mailBody.text, + html: mailBody.html, + }); + } catch (e) { + console.log("[mail service]", e); + } + + // Save email in DB + db.prepare( + "INSERT INTO NOTIFICATIONS(student_id, object, content) VALUES(?,?,?)", + ).run( + startRequestJoined.id, + "Your start request requires changes", + mailBody.text, + ); +}; + +exports.notifyProposalExpiration = async (proposal) => { + // Send email to the supervisor + const mailBody = proposalExpirationTemplate({ + name: proposal.teacher_surname + " " + proposal.teacher_name, + thesis: proposal.title, + nbOfDays: 7, + date: dayjs(proposal.expiration_date).format("DD/MM/YYYY"), + }); + try { + await nodemailer.sendMail({ + to: proposal.teacher_email, + subject: "Your proposal expires in 7 days", + text: mailBody.text, + html: mailBody.html, + }); + } catch (e) { + console.log("[mail service]", e); + } + + // Save email in DB + db.prepare( + "INSERT INTO NOTIFICATIONS(teacher_id, object, content, date) VALUES(?,?,?, DATETIME(DATETIME('now'), '+' || (select delta from VIRTUAL_CLOCK where id = 1) || ' days'))", + ).run(proposal.supervisor, "Your proposal expires in 7 days", mailBody.text); +}; + +exports.getNotifications = (user_id) => { + return db + .prepare( + "SELECT * FROM NOTIFICATIONS WHERE student_id = ? OR teacher_id = ?", + ) + .all(user_id, user_id); +}; + +exports.notifyRemovedCosupervisors = async (oldProposal, newProposal) => { + const oldCosupervisors = getCosupervisorsFromProposal(oldProposal); + const newCosupervisors = getCosupervisorsFromProposal(newProposal); + if (oldCosupervisors && newCosupervisors) { + const removedCosupervisors = getArrayDifference( + oldCosupervisors, + newCosupervisors, + ); + for (let cosupervisorEmail of removedCosupervisors) { + const teacher = getTeacherByEmail(cosupervisorEmail); + if (teacher) { + // -- Email + const mailBody = removedCosupervisorTemplate({ + name: teacher.surname + " " + teacher.name, + proposal: newProposal, + }); + try { + await nodemailer.sendMail({ + to: cosupervisorEmail, + subject: "You have been removed from a thesis proposal", + text: mailBody.text, + html: mailBody.html, + }); + } catch (e) { + console.log("[mail service]", e); + } + // -- Website notification + db.prepare( + "INSERT INTO NOTIFICATIONS(teacher_id, object, content, date) VALUES(?,?,?, DATETIME(DATETIME('now'), '+' || (select delta from VIRTUAL_CLOCK where id = 1) || ' days'))", + ).run( + teacher.id, + "You have been removed from a thesis proposal", + mailBody.text, + ); + } + } + } +}; + +exports.notifyAddedCosupervisors = async (oldProposal, newProposal) => { + const oldCosupervisors = getCosupervisorsFromProposal(oldProposal); + const newCosupervisors = getCosupervisorsFromProposal(newProposal); + if (oldCosupervisors && newCosupervisors) { + const addedCosupervisors = getArrayDifference( + newCosupervisors, + oldCosupervisors, + ); + for (let cosupervisorEmail of addedCosupervisors) { + const teacher = getTeacherByEmail(cosupervisorEmail); + if (teacher) { + // -- Email + const mailBody = addedCosupervisorTemplate({ + name: teacher.surname + " " + teacher.name, + proposal: newProposal, + }); + try { + await nodemailer.sendMail({ + to: cosupervisorEmail, + subject: "You have been added to a thesis proposal", + text: mailBody.text, + html: mailBody.html, + }); + } catch (e) { + console.log("[mail service]", e); + } + // -- Website notification + db.prepare( + "INSERT INTO NOTIFICATIONS(teacher_id, object, content, date) VALUES(?,?,?, DATETIME(DATETIME('now'), '+' || (select delta from VIRTUAL_CLOCK where id = 1) || ' days'))", + ).run( + teacher.id, + "You have been added to a thesis proposal", + mailBody.text, + ); + } + } + } +}; diff --git a/server/src/dao/proposals.js b/server/src/dao/proposals.js new file mode 100644 index 0000000..9c5a069 --- /dev/null +++ b/server/src/dao/proposals.js @@ -0,0 +1,165 @@ +"use strict"; + +const { db } = require("../db"); +const dayjs = require("dayjs"); +const { getDelta } = require("./virtual-clock"); + +exports.insertProposal = (proposal) => { + const { + title, + supervisor, + co_supervisors, + groups, + keywords, + types, + description, + required_knowledge, + notes, + expiration_date, + level, + cds, + } = proposal; + return db + .prepare( + "insert into PROPOSAlS(title, supervisor, co_supervisors, keywords, type, groups, description, required_knowledge, notes, expiration_date, level, cds) values(?,?,?,?,?,?,?,?,?,?,?,?)", + ) + .run( + title, + supervisor, + co_supervisors, + keywords, + types, + groups, + description, + required_knowledge, + notes, + expiration_date, + level, + cds, + ).lastInsertRowid; +}; + +exports.getProposalsForTeacher = (id, email) => { + return db + .prepare( + "select * from PROPOSALS where (supervisor = ? or co_supervisors like '%' || ? || '%') and deleted = 0", + ) + .all(id, email); +}; + +exports.getProposal = (id) => { + return db.prepare("select * from PROPOSALS where id = ?").get(id); +}; + +exports.getProposalsByDegree = (cds) => { + return db + .prepare( + ` + SELECT * + FROM PROPOSALS + WHERE cds = ? AND deleted = 0 AND id NOT IN ( + SELECT proposal_id + FROM APPLICATIONS + WHERE state = 'accepted' AND proposal_id IS NOT NULL + )`, + ) + .all(cds); +}; + +exports.getAcceptedProposal = (student_id) => { + return db + .prepare( + `select PROPOSALS.id, PROPOSALS.title, PROPOSALS.supervisor, PROPOSALS.co_supervisors, PROPOSALS.keywords, PROPOSALS.type, PROPOSALS.groups, PROPOSALS.description, PROPOSALS.required_knowledge, PROPOSALS.notes, PROPOSALS.expiration_date, PROPOSALS.level, PROPOSALS.cds, PROPOSALS.manually_archived, PROPOSALS.deleted + from main.PROPOSALS, main.APPLICATIONS + where APPLICATIONS.proposal_id = PROPOSALS.id and + APPLICATIONS.student_id = ? and + APPLICATIONS.state = 'accepted'`, + ) + .get(student_id); +}; + +exports.deleteProposal = (proposal_id) => { + db.prepare("update PROPOSALS set deleted = 1 WHERE id = ?").run(proposal_id); +}; + +exports.updateArchivedStateProposal = (new_archived_state, proposal_id) => { + db.prepare("update PROPOSALS set manually_archived = ? where id = ?").run( + new_archived_state, + proposal_id, + ); +}; + +exports.updateProposal = (proposal) => { + const { + proposal_id, + title, + supervisor, + co_supervisors, + groups, + keywords, + types, + description, + required_knowledge, + notes, + expiration_date, + level, + cds, + } = proposal; + return db + .prepare( + "UPDATE PROPOSALS SET title = ?, supervisor = ?, co_supervisors = ?, keywords = ?, type = ?, groups = ?, description = ?, required_knowledge = ?, notes = ?, expiration_date = ?, level = ?, cds = ? WHERE id = ?", + ) + .run( + title, + supervisor, + co_supervisors, + keywords, + types, + groups, + description, + required_knowledge, + notes, + expiration_date, + level, + cds, + proposal_id, + ); +}; + +exports.findAcceptedProposal = (proposal_id) => { + return db + .prepare( + `select * from APPLICATIONS where proposal_id = ? and state = 'accepted'`, + ) + .get(proposal_id); +}; + +/** + * @param nbOfDaysBeforeExpiration + * @returns {[ + * { + * supervisor, + * expiration_date, + * title + * } + * ]} + */ +exports.getProposalsThatExpireInXDays = (nbOfDaysBeforeExpiration) => { + const currentDate = dayjs().add(getDelta().delta, "day"); + const notificationDateFormatted = currentDate + .add(nbOfDaysBeforeExpiration, "day") + .format("YYYY-MM-DD"); + return db + .prepare( + `select supervisor, + t.surname as teacher_surname, + t.email as teacher_email, + t.name as teacher_name, + expiration_date, + title + from PROPOSALS p + join TEACHER t on p.supervisor = t.id + where expiration_date = ?`, + ) + .all(notificationDateFormatted); +}; diff --git a/server/src/dao/start-requests.js b/server/src/dao/start-requests.js new file mode 100644 index 0000000..2fcbd95 --- /dev/null +++ b/server/src/dao/start-requests.js @@ -0,0 +1,88 @@ +"use strict"; + +const { db } = require("../db"); + +exports.insertStartRequest = (startRequest) => { + const { title, description, supervisor, co_supervisors, studentId } = + startRequest; + return db + .prepare( + "insert into START_REQUESTS(title, description, supervisor, co_supervisors, student_id) values(?,?,?,?,?)", + ) + .run(title, description, supervisor, co_supervisors, studentId) + .lastInsertRowid; +}; + +exports.getNotRejectedStartRequest = (userId) => { + return db + .prepare( + "SELECT * FROM START_REQUESTS WHERE student_id = ? AND status != 'secretary_rejected' and status != 'teacher_rejected'", + ) + .all(userId); +}; + +exports.updateStatusOfStartRequest = (new_status, request_id) => { + return db + .prepare("update START_REQUESTS set status = ? where id = ?") + .run(new_status, request_id); +}; + +exports.setChangesRequestedOfStartRequest = (new_changes, request_id) => { + return db + .prepare("update START_REQUESTS set changes_requested = ? where id = ?") + .run(new_changes, request_id); +}; + +exports.setApprovalDateOfRequest = (new_date, request_id) => { + return db + .prepare("update START_REQUESTS set approval_date = ? where id = ?") + .run(new_date, request_id); +}; + +exports.getStartedThesisRequest = (student_id) => { + return db + .prepare( + "select * from main.START_REQUESTS where student_id = ? and status = 'started'", + ) + .get(student_id); +}; + +/** + * NOTE: Sets also the status of the request to `changed` + * @param id + * @param new_fields + * @returns {Database.RunResult} + */ +exports.updateStartRequest = (id, new_fields) => { + const { title, description, supervisor, co_supervisors } = new_fields; + return db + .prepare( + "UPDATE START_REQUESTS SET title = ?, description = ?, supervisor = ?, co_supervisors = ?, status = 'changed' WHERE id = ?", + ) + .run(title, description, supervisor, co_supervisors, id); +}; + +exports.getRequestById = (id) => { + return db.prepare("SELECT * FROM START_REQUESTS WHERE id=?").get(id); +}; + +exports.getRequestsForTeacher = (id, email) => { + return db + .prepare( + `select * from START_REQUESTS + where (supervisor = ? or co_supervisors LIKE '%' || ? || '%') + and status != 'secretary_rejected' + and status != 'requested'`, + ) + .all(id, email); +}; + +exports.getRequestsForClerk = () => { + return db.prepare("select * from main.START_REQUESTS").all(); +}; + +exports.getRequestsForStudent = (id) => { + return db + .prepare("select * from main.START_REQUESTS where student_id = ?") + .all(id); +}; diff --git a/server/src/user-dao.js b/server/src/dao/user.js similarity index 59% rename from server/src/user-dao.js rename to server/src/dao/user.js index a434dd9..6e4e471 100644 --- a/server/src/user-dao.js +++ b/server/src/dao/user.js @@ -2,7 +2,7 @@ /* Data Access Object (DAO) module for accessing users */ -const { db } = require("./db"); +const { db } = require("../db"); exports.getUser = (reqUser) => { const { email } = reqUser; @@ -24,3 +24,19 @@ exports.getUser = (reqUser) => { } return student || teacher || secretary_clerk; }; + +exports.getTeacher = (id) => { + return db.prepare("select * from TEACHER where id = ?").get(id); +}; + +exports.getTeacherByEmail = (email) => { + return db.prepare("select * from TEACHER where email = ?").get(email); +}; + +exports.getTeacherEmailById = (id) => { + return db.prepare("select email from TEACHER where id = ?").get(id); +}; + +exports.getTeachers = () => { + return db.prepare("select * from TEACHER").all(); +}; diff --git a/server/src/dao/virtual-clock.js b/server/src/dao/virtual-clock.js new file mode 100644 index 0000000..5f73cdb --- /dev/null +++ b/server/src/dao/virtual-clock.js @@ -0,0 +1,13 @@ +"use strict"; + +const { db } = require("../db"); + +exports.getDelta = () => { + return db.prepare("select delta from VIRTUAL_CLOCK where id = 1").get(); +}; + +exports.setDelta = (delta) => { + return db + .prepare("UPDATE VIRTUAL_CLOCK SET delta = ? WHERE id = 1") + .run(delta); +}; diff --git a/server/src/routes.js b/server/src/routes.js index a122e63..d5a9ac5 100644 --- a/server/src/routes.js +++ b/server/src/routes.js @@ -4,177 +4,66 @@ const router = require("express").Router(); const passport = require("passport"); const dayjs = require("dayjs"); const { check, validationResult } = require("express-validator"); -const isLoggedIn = require("./protect-routes"); +const { getUser, getTeacher, getTeachers } = require("./dao/user"); +const isLoggedIn = require("./routes_utils/protect-routes"); +const { + validateProposal, + determineNewStatus, + getDate, + isArchived, + setStateToApplication, +} = require("./routes_utils/utils"); const { - getTeachers, - getGroup, - getGroups, - getDegrees, insertProposal, getProposalsByDegree, getProposalsForTeacher, - updateApplication, + getAcceptedProposal, getProposal, - insertApplication, - insertStartRequest, - updateStatusOfStartRequest, - setChangesRequestedOfStartRequest, - setApprovalDateOfRequest, - getApplicationsOfTeacher, - getApplicationsOfStudent, - deleteProposal, - updateProposal, - updateStartRequest, - getApplicationById, - getTeacherByEmail, - cancelPendingApplications, findAcceptedProposal, - findRejectedApplication, - notifyApplicationDecision, - notifyNewApplication, - getDelta, - setDelta, - getNotifications, - getExamsOfStudent, - insertPDFInApplication, updateArchivedStateProposal, + updateProposal, + deleteProposal, +} = require("./dao/proposals"); +const { getNotRejectedStartRequest, + insertStartRequest, getRequestById, - getTeacher, - getAcceptedApplicationsOfStudent, - getPendingApplicationsOfStudent, - notifyNewStartRequest, - getRequestsForTeacher, + setApprovalDateOfRequest, + setChangesRequestedOfStartRequest, + updateStatusOfStartRequest, + getStartedThesisRequest, getRequestsForClerk, + getRequestsForTeacher, getRequestsForStudent, - isAccepted, - getAcceptedProposal, - getStartedThesisRequest, - cancelPendingApplicationsOfStudent, + updateStartRequest, +} = require("./dao/start-requests"); +const { + notifyNewStartRequest, + getGroups, + getDegrees, + notifyNewApplication, + getExamsOfStudent, + getNotifications, notifyRemovedCosupervisors, notifyAddedCosupervisors, notifyChangesRequestedOnStartRequest, -} = require("./theses-dao"); -const { getUser } = require("./user-dao"); +} = require("./dao/misc"); +const { + cancelPendingApplicationsOfStudent, + isAccepted, + getAcceptedApplicationsOfStudent, + getPendingApplicationsOfStudent, + findRejectedApplication, + insertApplication, + getApplicationsOfTeacher, + getApplicationsOfStudent, + getApplicationById, + insertPDFInApplication, + cancelPendingApplications, +} = require("./dao/applications"); +const { getDelta, setDelta } = require("./dao/virtual-clock"); const { runCronjob, cronjobNames } = require("./cronjobs"); -function getDate() { - const clock = getDelta(); - return dayjs().add(clock.delta, "day").format("YYYY-MM-DD"); -} - -function isArchived(proposal) { - return ( - !!proposal.manually_archived || - dayjs(proposal.expiration_date).isBefore(dayjs(getDate())) - ); -} - -function check_errors(start_request, user, old_status) { - if (user.id !== start_request.supervisor) { - throw new Error("You are not the supervisor of this thesis request"); - } - - if (old_status === "changes_requested") { - throw new Error( - "The thesis request is still waiting to be changed by the student", - ); - } - - if (old_status === "requested") { - throw new Error("The request has not been evaluated by the secretary yet."); - } - - if ( - old_status === "teacher_rejected" || - old_status === "secretary_rejected" || - old_status === "started" - ) { - throw new Error("The request has been already approved / rejected"); - } -} - -function determineNewStatus(start_request, user, decision) { - const old_status = start_request.status; - let new_status; - - if (user.role === "secretary_clerk") { - if (old_status !== "requested") { - throw new Error("The request has been already approved / rejected"); - } - if (decision === "approved") { - new_status = "secretary_accepted"; - } else if (decision === "rejected") { - new_status = "secretary_rejected"; - } else { - throw new Error( - "The secretary clerk has not the permission to perform this operation", - ); - } - } else if (user.role === "teacher") { - check_errors(start_request, user, old_status); - - if (decision === "approved") { - new_status = "started"; - } else if (decision === "rejected") { - new_status = "teacher_rejected"; - } else if (decision === "changes_requested") { - new_status = "changes_requested"; - } else { - throw new Error( - "The teacher has not the permission to perform this operation", - ); - } - } - return new_status; -} - -function validateProposal(res, proposal, user) { - const { co_supervisors, groups, level } = proposal; - for (const group of groups) { - if (getGroup(group) === undefined) { - throw new Error("Invalid proposal content"); - } - } - if (level !== "MSC" && level !== "BSC") { - throw new Error("Invalid proposal content"); - } - const legal_groups = [user.cod_group]; - for (const co_supervisor_email of co_supervisors) { - const co_supervisor = getTeacherByEmail(co_supervisor_email); - if (co_supervisor !== undefined) { - legal_groups.push(co_supervisor.cod_group); - } - } - if (!groups.every((group) => legal_groups.includes(group))) { - throw new Error("Invalid groups"); - } - if (co_supervisors.includes(user.email)) { - throw new Error( - "The supervisor's email is included in the list of co-supervisors", - ); - } -} - -async function setStateToApplication(req, res, state) { - const application = getApplicationById(req.params.id); - if (application === undefined) { - return res.status(400).json({ message: "Application not existent" }); - } - if (application.state !== "pending") { - return res.status(400).json({ - message: "You cannot modify an application already accepted or rejected", - }); - } - updateApplication(application.id, state); - notifyApplicationDecision(application.id, state); - if (state === "accepted") { - updateArchivedStateProposal(1, application.proposal_id); - cancelPendingApplications(application.proposal_id); - } - return res.status(200).json({ message: `Application ${state}` }); -} - // ================================================== // Routes // ================================================== @@ -843,8 +732,8 @@ router.put( level: level, cds: cds, }; - await notifyRemovedCosupervisors(proposal, newProposal); - await notifyAddedCosupervisors(proposal, newProposal); + notifyRemovedCosupervisors(proposal, newProposal); + notifyAddedCosupervisors(proposal, newProposal); updateProposal(newProposal); return res.status(200).json({ message: "Proposal updated successfully" }); } catch (e) { @@ -992,7 +881,7 @@ router.put( const { thesisRequestId } = req.params; - //controllo che esista + // check it exists const request = getRequestById(thesisRequestId); if (request === undefined) { return res.status(404).json({ diff --git a/server/src/protect-routes.js b/server/src/routes_utils/protect-routes.js similarity index 100% rename from server/src/protect-routes.js rename to server/src/routes_utils/protect-routes.js diff --git a/server/src/routes_utils/utils.js b/server/src/routes_utils/utils.js new file mode 100644 index 0000000..c22e27d --- /dev/null +++ b/server/src/routes_utils/utils.js @@ -0,0 +1,137 @@ +"use strict"; + +const { getDelta } = require("../dao/virtual-clock"); +const dayjs = require("dayjs"); +const { getGroup, notifyApplicationDecision } = require("../dao/misc"); +const { getTeacherByEmail } = require("../dao/user"); +const { + getApplicationById, + updateApplication, + cancelPendingApplications, +} = require("../dao/applications"); +const { updateArchivedStateProposal } = require("../dao/proposals"); + +function getDate() { + const clock = getDelta(); + return dayjs().add(clock.delta, "day").format("YYYY-MM-DD"); +} + +function isArchived(proposal) { + return ( + !!proposal.manually_archived || + dayjs(proposal.expiration_date).isBefore(dayjs(getDate())) + ); +} + +function check_errors(start_request, user, old_status) { + if (user.id !== start_request.supervisor) { + throw new Error("You are not the supervisor of this thesis request"); + } + + if (old_status === "changes_requested") { + throw new Error( + "The thesis request is still waiting to be changed by the student", + ); + } + + if (old_status === "requested") { + throw new Error("The request has not been evaluated by the secretary yet."); + } + + if ( + old_status === "teacher_rejected" || + old_status === "secretary_rejected" || + old_status === "started" + ) { + throw new Error("The request has been already approved / rejected"); + } +} + +function determineNewStatus(start_request, user, decision) { + const old_status = start_request.status; + let new_status; + + if (user.role === "secretary_clerk") { + if (old_status !== "requested") { + throw new Error("The request has been already approved / rejected"); + } + if (decision === "approved") { + new_status = "secretary_accepted"; + } else if (decision === "rejected") { + new_status = "secretary_rejected"; + } else { + throw new Error( + "The secretary clerk has not the permission to perform this operation", + ); + } + } else if (user.role === "teacher") { + check_errors(start_request, user, old_status); + + if (decision === "approved") { + new_status = "started"; + } else if (decision === "rejected") { + new_status = "teacher_rejected"; + } else if (decision === "changes_requested") { + new_status = "changes_requested"; + } else { + throw new Error( + "The teacher has not the permission to perform this operation", + ); + } + } + return new_status; +} + +function validateProposal(res, proposal, user) { + const { co_supervisors, groups, level } = proposal; + for (const group of groups) { + if (getGroup(group) === undefined) { + throw new Error("Invalid proposal content"); + } + } + if (level !== "MSC" && level !== "BSC") { + throw new Error("Invalid proposal content"); + } + const legal_groups = [user.cod_group]; + for (const co_supervisor_email of co_supervisors) { + const co_supervisor = getTeacherByEmail(co_supervisor_email); + if (co_supervisor !== undefined) { + legal_groups.push(co_supervisor.cod_group); + } + } + if (!groups.every((group) => legal_groups.includes(group))) { + throw new Error("Invalid groups"); + } + if (co_supervisors.includes(user.email)) { + throw new Error( + "The supervisor's email is included in the list of co-supervisors", + ); + } +} + +async function setStateToApplication(req, res, state) { + const application = getApplicationById(req.params.id); + if (application === undefined) { + return res.status(400).json({ message: "Application not existent" }); + } + if (application.state !== "pending") { + return res.status(400).json({ + message: "You cannot modify an application already accepted or rejected", + }); + } + updateApplication(application.id, state); + notifyApplicationDecision(application.id, state); + if (state === "accepted") { + updateArchivedStateProposal(1, application.proposal_id); + cancelPendingApplications(application.proposal_id); + } + return res.status(200).json({ message: `Application ${state}` }); +} + +module.exports = { + getDate, + isArchived, + determineNewStatus, + validateProposal, + setStateToApplication, +}; diff --git a/server/src/theses-dao.js b/server/src/theses-dao.js index 4db43a2..e69de29 100644 --- a/server/src/theses-dao.js +++ b/server/src/theses-dao.js @@ -1,762 +0,0 @@ -"use strict"; - -/* Data Access Object (DAO) module for accessing users */ - -const { db } = require("./db"); -const { nodemailer } = require("./smtp"); -const { applicationDecisionTemplate } = require("./mail/application-decision"); -const { newApplicationTemplate } = require("./mail/new-application"); - -const dayjs = require("dayjs"); -const { proposalExpirationTemplate } = require("./mail/proposal-expiration"); -const { supervisorStartRequestTemplate } = require("./mail/supervisor-start-request"); -const { cosupervisorApplicationDecisionTemplate } = require("./mail/cosupervisor-application-decision"); -const { cosupervisorStartRequestTemplate } = require("./mail/cosupervisor-start-request"); -const { removedCosupervisorTemplate } = require("./mail/removed-cosupervisor"); -const { addedCosupervisorTemplate } = require("./mail/added-cosupervisor"); -const { changesStartRequestStudentTemplate } = require("./mail/changes-start-request-student"); - -exports.insertApplication = (proposal, student, state) => { - const result = db - .prepare( - "insert into APPLICATIONS(proposal_id, student_id, state) values (?,?,?)", - ) - .run(proposal, student, state); - const applicationId = result.lastInsertRowid; - return { - application_id: applicationId, - proposal_id: proposal, - student_id: student, - state: state, - }; -}; - -exports.insertProposal = (proposal) => { - const { - title, - supervisor, - co_supervisors, - groups, - keywords, - types, - description, - required_knowledge, - notes, - expiration_date, - level, - cds, - } = proposal; - return db - .prepare( - "insert into PROPOSAlS(title, supervisor, co_supervisors, keywords, type, groups, description, required_knowledge, notes, expiration_date, level, cds) values(?,?,?,?,?,?,?,?,?,?,?,?)", - ) - .run( - title, - supervisor, - co_supervisors, - keywords, - types, - groups, - description, - required_knowledge, - notes, - expiration_date, - level, - cds, - ).lastInsertRowid; -}; - -exports.insertPDFInApplication = (file, applicationId) => { - return db - .prepare( - "update APPLICATIONS set attached_file = ? where main.APPLICATIONS.id = ?", - ) - .run(file, applicationId); -}; - -exports.getExamsOfStudent = (id) => { - return db.prepare("select * from main.CAREER where id = ?").all(id); -}; - -exports.insertStartRequest = (startRequest) => { - const { title, description, supervisor, co_supervisors, studentId } = - startRequest; - return db - .prepare( - "insert into START_REQUESTS(title, description, supervisor, co_supervisors, student_id) values(?,?,?,?,?)", - ) - .run(title, description, supervisor, co_supervisors, studentId) - .lastInsertRowid; -}; - -exports.getNotRejectedStartRequest = (userId) => { - return db - .prepare( - "SELECT * FROM START_REQUESTS WHERE student_id = ? AND status != 'secretary_rejected' and status != 'teacher_rejected'", - ) - .all(userId); -}; - -exports.updateStatusOfStartRequest = (new_status, request_id) => { - return db - .prepare("update START_REQUESTS set status = ? where id = ?") - .run(new_status, request_id); -}; - -exports.setChangesRequestedOfStartRequest = (new_changes, request_id) => { - return db - .prepare("update START_REQUESTS set changes_requested = ? where id = ?") - .run(new_changes, request_id); -}; - -exports.setApprovalDateOfRequest = (new_date, request_id) => { - return db - .prepare("update START_REQUESTS set approval_date = ? where id = ?") - .run(new_date, request_id); -}; - -exports.getApplicationById = (id) => { - return db.prepare("select * from APPLICATIONS where id = ?").get(id); -}; - -exports.getProposalsForTeacher = (id, email) => { - return db - .prepare( - "select * from PROPOSALS where (supervisor = ? or co_supervisors like '%' || ? || '%') and deleted = 0", - ) - .all(id, email); -}; - -exports.getTeacher = (id) => { - return db.prepare("select * from TEACHER where id = ?").get(id); -}; - -exports.getTeacherByEmail = (email) => { - return db.prepare("select * from TEACHER where email = ?").get(email); -}; - -exports.getTeacherEmailById = (id) => { - return db.prepare("select email from TEACHER where id = ?").get(id); -}; - -exports.getTeachers = () => { - return db.prepare("select * from TEACHER").all(); -}; - -exports.getProposal = (id) => { - return db.prepare("select * from PROPOSALS where id = ?").get(id); -}; - -exports.getGroup = (cod_group) => { - return db.prepare("select * from GROUPS where cod_group = ?").get(cod_group); -}; - -exports.getGroups = () => { - return db.prepare("select cod_group from GROUPS").all(); -}; - -exports.getDegrees = () => { - return db.prepare("select cod_degree, title_degree from DEGREE").all(); -}; - -exports.getProposalsByDegree = (cds) => { - return db - .prepare( - ` - SELECT * - FROM PROPOSALS - WHERE cds = ? AND deleted = 0 AND id NOT IN ( - SELECT proposal_id - FROM APPLICATIONS - WHERE state = 'accepted' AND proposal_id IS NOT NULL - )`, - ) - .all(cds); -}; - -exports.getAcceptedProposal = (student_id) => { - return db - .prepare( - `select PROPOSALS.id, PROPOSALS.title, PROPOSALS.supervisor, PROPOSALS.co_supervisors, PROPOSALS.keywords, PROPOSALS.type, PROPOSALS.groups, PROPOSALS.description, PROPOSALS.required_knowledge, PROPOSALS.notes, PROPOSALS.expiration_date, PROPOSALS.level, PROPOSALS.cds, PROPOSALS.manually_archived, PROPOSALS.deleted - from main.PROPOSALS, main.APPLICATIONS - where APPLICATIONS.proposal_id = PROPOSALS.id and - APPLICATIONS.student_id = ? and - APPLICATIONS.state = 'accepted'`, - ) - .get(student_id); -}; - -exports.cancelPendingApplications = (of_proposal) => { - db.prepare( - "update APPLICATIONS set state = 'canceled' where proposal_id = ? AND state = 'pending'", - ).run(of_proposal); -}; - -exports.cancelPendingApplicationsOfStudent = (student_id) => { - db.prepare( - "update APPLICATIONS set state = 'canceled' where main.APPLICATIONS.student_id = ? and state = 'pending'", - ).run(student_id); -}; - -exports.getStartedThesisRequest = (student_id) => { - return db - .prepare( - "select * from main.START_REQUESTS where student_id = ? and status = 'started'", - ) - .get(student_id); -}; - -exports.updateApplication = (id, state) => { - db.prepare("update APPLICATIONS set state = ? where id = ?").run(state, id); -}; - -exports.getAcceptedApplicationsOfStudent = (student_id) => { - return db - .prepare( - `select * from APPLICATIONS where student_id = ? and state = 'accepted'`, - ) - .all(student_id); -}; - -exports.getPendingApplicationsOfStudent = (student_id) => { - return db - .prepare( - `select * from APPLICATIONS where student_id = ? and state = 'pending'`, - ) - .all(student_id); -}; - -exports.findAcceptedProposal = (proposal_id) => { - return db - .prepare( - `select * from APPLICATIONS where proposal_id = ? and state = 'accepted'`, - ) - .get(proposal_id); -}; - -exports.findRejectedApplication = (proposal_id, student_id) => { - return db - .prepare( - `select * from APPLICATIONS where proposal_id = ? and student_id = ? and state = 'rejected'`, - ) - .get(proposal_id, student_id); -}; - -exports.notifyApplicationDecision = async (applicationId, decision) => { - // Retrieve the data - const applicationJoined = db - .prepare( - `SELECT S.id, P.title, P.co_supervisors, S.email, S.surname, S.name - FROM APPLICATIONS A - JOIN PROPOSALS P ON P.id = A.proposal_id - JOIN STUDENT S ON S.id = A.student_id - WHERE A.id = ?`, - ) - .get(applicationId); - let mailBody; - // Notify the student - // -- Email - mailBody = applicationDecisionTemplate({ - name: applicationJoined.surname + " " + applicationJoined.name, - thesis: applicationJoined.title, - decision: decision, - }); - try { - await nodemailer.sendMail({ - to: applicationJoined.email, - subject: "New decision on your thesis application", - text: mailBody.text, - html: mailBody.html, - }); - } catch (e) { - console.log("[mail service]", e); - } - // -- Website notification - db.prepare( - "INSERT INTO NOTIFICATIONS(student_id, object, content, date) VALUES(?,?,?, DATETIME(DATETIME('now'), '+' || (select delta from VIRTUAL_CLOCK where id = 1) || ' days'))", - ).run( - applicationJoined.id, - "New decision on your thesis application", - mailBody.text, - ); - // Notify the co-supervisors - if (applicationJoined.co_supervisors) { - for (const cosupervisor of applicationJoined.co_supervisors.split(", ")) { - const fullCosupervisor = this.getTeacherByEmail(cosupervisor); - // -- Email - mailBody = cosupervisorApplicationDecisionTemplate({ - name: fullCosupervisor.surname + " " + fullCosupervisor.name, - thesis: applicationJoined.title, - decision: decision, - }); - try { - await nodemailer.sendMail({ - to: cosupervisor, - subject: "New decision for a thesis you co-supervise", - text: mailBody.text, - html: mailBody.html, - }); - } catch (e) { - console.log("[mail service]", e); - } - // -- Website notification - db.prepare( - "INSERT INTO NOTIFICATIONS(teacher_id, object, content, date) VALUES(?,?,?, DATETIME(DATETIME('now'), '+' || (select delta from VIRTUAL_CLOCK where id = 1) || ' days'))", - ).run( - fullCosupervisor.id, - "New decision for a thesis you co-supervise", - mailBody.text, - ); - } - } -}; - -exports.notifyNewStartRequest = async (requestId) => { - const requestJoined = db - .prepare( - `SELECT S.student_id, S.supervisor, S.co_supervisors, T.name, T.surname - FROM START_REQUESTS S - JOIN TEACHER T ON T.id = S.supervisor - WHERE S.id = ?`, - ) - .get(requestId); - // Send email to the supervisor - let mailBody = supervisorStartRequestTemplate({ - name: requestJoined.surname + " " + requestJoined.name, - student: requestJoined.student_id, - }); - const teacher = this.getTeacherEmailById(requestJoined.supervisor); - try { - await nodemailer.sendMail({ - to: teacher.email, - subject: "New start request", - text: mailBody.text, - html: mailBody.html, - }); - } catch (e) { - console.log("[mail service]", e); - } - - // Save email in DB - db.prepare( - "INSERT INTO NOTIFICATIONS(teacher_id, object, content, date) VALUES(?,?,?, DATETIME(DATETIME('now'), '+' || (select delta from VIRTUAL_CLOCK where id = 1) || ' days'))", - ).run(requestJoined.supervisor, "New start request", mailBody.text); - - // Send email to the co-supervisors - if (requestJoined.co_supervisors) { - const coSupervisors = requestJoined.co_supervisors.split(", "); - for (const coSupervisorEmail of coSupervisors) { - const coSupervisor = this.getTeacherByEmail(coSupervisorEmail); - mailBody = cosupervisorStartRequestTemplate({ - name: coSupervisor.surname + " " + coSupervisor.name, - student: requestJoined.student_id, - }); - try { - await nodemailer.sendMail({ - to: coSupervisorEmail, - subject: "New start request", - text: mailBody.text, - html: mailBody.html, - }); - } catch (e) { - console.log("[mail service]", e); - } - - // Save email in DB - db.prepare( - "INSERT INTO NOTIFICATIONS(teacher_id, object, content, date) VALUES(?,?,?, DATETIME(DATETIME('now'), '+' || (select delta from VIRTUAL_CLOCK where id = 1) || ' days'))", - ).run(coSupervisor.id, "New start request", mailBody.text); - } - } -}; - -exports.notifyNewApplication = async (proposalId) => { - // Send email to the supervisor - const proposalJoined = db - .prepare( - `SELECT P.title, T.id, T.email, T.surname, T.name - FROM PROPOSALS P - JOIN TEACHER T ON T.id = P.supervisor - WHERE P.id = ?`, - ) - .get(proposalId); - const mailBody = newApplicationTemplate({ - name: proposalJoined.surname + " " + proposalJoined.name, - thesis: proposalJoined.title, - }); - try { - await nodemailer.sendMail({ - to: proposalJoined.email, - subject: "New application on your thesis proposal", - text: mailBody.text, - html: mailBody.html, - }); - } catch (e) { - console.log("[mail service]", e); - } - - // Save email in DB - db.prepare( - "INSERT INTO NOTIFICATIONS(teacher_id, object, content, date) VALUES(?,?,?, DATETIME(DATETIME('now'), '+' || (select delta from VIRTUAL_CLOCK where id = 1) || ' days'))", - ).run( - proposalJoined.id, - "New application on your thesis proposal", - mailBody.text, - ); -}; - -exports.notifyChangesRequestedOnStartRequest = async (message, startRequestId) => { - // Send email to the supervisor - const startRequestJoined = db - .prepare( - `SELECT SR.title, S.id, S.email, S.surname, S.name - FROM START_REQUESTS SR - JOIN STUDENT S ON S.id = SR.student_id - WHERE SR.id = ?`, - ) - .get(startRequestId); - const mailBody = changesStartRequestStudentTemplate({ - name: startRequestJoined.surname + " " + startRequestJoined.name, - startRequest: startRequestJoined.title, - changes: message, - }); - try { - await nodemailer.sendMail({ - to: startRequestJoined.email, - subject: "Your start request requires changes", - text: mailBody.text, - html: mailBody.html, - }); - } catch (e) { - console.log("[mail service]", e); - } - - // Save email in DB - db.prepare( - "INSERT INTO NOTIFICATIONS(student_id, object, content) VALUES(?,?,?)", - ).run( - startRequestJoined.id, - "Your start request requires changes", - mailBody.text, - ); -}; - -exports.notifyProposalExpiration = async (proposal) => { - // Send email to the supervisor - const mailBody = proposalExpirationTemplate({ - name: proposal.teacher_surname + " " + proposal.teacher_name, - thesis: proposal.title, - nbOfDays: 7, - date: dayjs(proposal.expiration_date).format("DD/MM/YYYY"), - }); - try { - await nodemailer.sendMail({ - to: proposal.teacher_email, - subject: "Your proposal expires in 7 days", - text: mailBody.text, - html: mailBody.html, - }); - } catch (e) { - console.log("[mail service]", e); - } - - // Save email in DB - db.prepare( - "INSERT INTO NOTIFICATIONS(teacher_id, object, content, date) VALUES(?,?,?, DATETIME(DATETIME('now'), '+' || (select delta from VIRTUAL_CLOCK where id = 1) || ' days'))", - ).run(proposal.supervisor, "Your proposal expires in 7 days", mailBody.text); -}; - -/** - * @param teacher_id - * @returns {[ - * { - * application_id, - * proposal_id, - * teacher_id, - * state, - * student_name, - * student_surname, - * teacher_name, - * teacher_surname - * title - * } - * ]} - */ -exports.getApplicationsOfTeacher = (teacher_id) => { - return db - .prepare( - `select APPLICATIONS.id, - APPLICATIONS.proposal_id, - APPLICATIONS.student_id, - APPLICATIONS.state, - APPLICATIONS.attached_file, - STUDENT.name as student_name, - STUDENT.surname as student_surname, - TEACHER.name as teacher_name, - TEACHER.surname as teacher_surname, - PROPOSALS.title as title - from APPLICATIONS, - PROPOSALS, - STUDENT, - TEACHER - where APPLICATIONS.proposal_id = PROPOSALS.id - and PROPOSALS.supervisor = TEACHER.id - and APPLICATIONS.student_id = STUDENT.id - and PROPOSALS.supervisor = ?`, - ) - .all(teacher_id); -}; - -/** - * @param nbOfDaysBeforeExpiration - * @returns {[ - * { - * supervisor, - * expiration_date, - * title - * } - * ]} - */ -exports.getProposalsThatExpireInXDays = (nbOfDaysBeforeExpiration) => { - const currentDate = dayjs().add(getDelta().delta, "day"); - const notificationDateFormatted = currentDate - .add(nbOfDaysBeforeExpiration, "day") - .format("YYYY-MM-DD"); - return db - .prepare( - `select supervisor, - t.surname as teacher_surname, - t.email as teacher_email, - t.name as teacher_name, - expiration_date, - title - from PROPOSALS p - join TEACHER t on p.supervisor = t.id - where expiration_date = ?`, - ) - .all(notificationDateFormatted); -}; - -exports.getApplicationsOfStudent = (student_id) => { - return db - .prepare( - `select APPLICATIONS.id, - APPLICATIONS.proposal_id, - APPLICATIONS.student_id, - APPLICATIONS.state, - APPLICATIONS.attached_file, - STUDENT.name as student_name, - STUDENT.surname as student_surname, - TEACHER.name as teacher_name, - TEACHER.surname as teacher_surname, - PROPOSALS.title as title - from APPLICATIONS, - PROPOSALS, - STUDENT, - TEACHER - where APPLICATIONS.proposal_id = PROPOSALS.id - and PROPOSALS.supervisor = TEACHER.id - and APPLICATIONS.student_id = STUDENT.id - and APPLICATIONS.student_id = ?`, - ) - .all(student_id); -}; - -exports.getNotifications = (user_id) => { - return db - .prepare( - "SELECT * FROM NOTIFICATIONS WHERE student_id = ? OR teacher_id = ?", - ) - .all(user_id, user_id); -}; - -exports.deleteProposal = (proposal_id) => { - db.prepare("update PROPOSALS set deleted = 1 WHERE id = ?").run(proposal_id); -}; - -exports.updateArchivedStateProposal = (new_archived_state, proposal_id) => { - db.prepare("update PROPOSALS set manually_archived = ? where id = ?").run( - new_archived_state, - proposal_id, - ); -}; - -const getDelta = () => { - return db.prepare("select delta from VIRTUAL_CLOCK where id = 1").get(); -}; -exports.getDelta = getDelta; - -exports.setDelta = (delta) => { - return db - .prepare("UPDATE VIRTUAL_CLOCK SET delta = ? WHERE id = 1") - .run(delta); -}; - -exports.isAccepted = (proposal_id, student_id) => { - const accepted_proposal = db - .prepare( - `select * from main.APPLICATIONS - where APPLICATIONS.proposal_id = ? - and APPLICATIONS.student_id = ? - and APPLICATIONS.state = 'accepted'`, - ) - .get(proposal_id, student_id); - return accepted_proposal !== undefined; -}; - -function getCosupervisorsFromProposal(proposal) { - return proposal.co_supervisors ? proposal.co_supervisors.split(", ") : []; -} - -function getArrayDifference(arrayIn, arrayNotIn) { - return arrayIn.filter((el) => !arrayNotIn.includes(el)); -} - -exports.notifyRemovedCosupervisors = async (oldProposal, newProposal) => { - const oldCosupervisors = getCosupervisorsFromProposal(oldProposal); - const newCosupervisors = getCosupervisorsFromProposal(newProposal); - if(oldCosupervisors && newCosupervisors) { - const removedCosupervisors = getArrayDifference(oldCosupervisors, newCosupervisors); - for(let cosupervisorEmail of removedCosupervisors) { - const teacher = this.getTeacherByEmail(cosupervisorEmail); - if(teacher) { - // -- Email - const mailBody = removedCosupervisorTemplate({ - name: teacher.surname + " " + teacher.name, - proposal: newProposal - }); - try { - await nodemailer.sendMail({ - to: cosupervisorEmail, - subject: "You have been removed from a thesis proposal", - text: mailBody.text, - html: mailBody.html, - }); - } catch (e) { - console.log("[mail service]", e); - } - // -- Website notification - db.prepare( - "INSERT INTO NOTIFICATIONS(teacher_id, object, content, date) VALUES(?,?,?, DATETIME(DATETIME('now'), '+' || (select delta from VIRTUAL_CLOCK where id = 1) || ' days'))", - ).run( - teacher.id, - "You have been removed from a thesis proposal", - mailBody.text, - ); - } - } - } -}; - -exports.notifyAddedCosupervisors = async (oldProposal, newProposal) => { - const oldCosupervisors = getCosupervisorsFromProposal(oldProposal); - const newCosupervisors = getCosupervisorsFromProposal(newProposal); - if(oldCosupervisors && newCosupervisors) { - const addedCosupervisors = getArrayDifference(newCosupervisors, oldCosupervisors); - for(let cosupervisorEmail of addedCosupervisors) { - const teacher = this.getTeacherByEmail(cosupervisorEmail); - if(teacher) { - // -- Email - const mailBody = addedCosupervisorTemplate({ - name: teacher.surname + " " + teacher.name, - proposal: newProposal - }); - try { - await nodemailer.sendMail({ - to: cosupervisorEmail, - subject: "You have been added to a thesis proposal", - text: mailBody.text, - html: mailBody.html, - }); - } catch (e) { - console.log("[mail service]", e); - } - // -- Website notification - db.prepare( - "INSERT INTO NOTIFICATIONS(teacher_id, object, content, date) VALUES(?,?,?, DATETIME(DATETIME('now'), '+' || (select delta from VIRTUAL_CLOCK where id = 1) || ' days'))", - ).run( - teacher.id, - "You have been added to a thesis proposal", - mailBody.text, - ); - } - } - } -}; - -exports.updateProposal = (proposal) => { - const { - proposal_id, - title, - supervisor, - co_supervisors, - groups, - keywords, - types, - description, - required_knowledge, - notes, - expiration_date, - level, - cds, - } = proposal; - return db - .prepare( - "UPDATE PROPOSALS SET title = ?, supervisor = ?, co_supervisors = ?, keywords = ?, type = ?, groups = ?, description = ?, required_knowledge = ?, notes = ?, expiration_date = ?, level = ?, cds = ? WHERE id = ?", - ) - .run( - title, - supervisor, - co_supervisors, - keywords, - types, - groups, - description, - required_knowledge, - notes, - expiration_date, - level, - cds, - proposal_id, - ); -}; - -/** - * NOTE: Sets also the status of the request to `changed` and the field `changes_requested` to `NULL` - * @param id - * @param new_fields - * @returns {Database.RunResult} - */ -exports.updateStartRequest = (id, new_fields) => { - const { title, description, supervisor, co_supervisors } = new_fields; - return db - .prepare( - "UPDATE START_REQUESTS SET title = ?, description = ?, supervisor = ?, co_supervisors = ?, status = 'changed' WHERE id = ?", - ) - .run(title, description, supervisor, co_supervisors, id); -}; - -exports.getRequestById = (id) => { - return db.prepare("SELECT * FROM START_REQUESTS WHERE id=?").get(id); -}; - -exports.getRequestsForTeacher = (id, email) => { - return db - .prepare( - `select * from START_REQUESTS - where (supervisor = ? or co_supervisors LIKE '%' || ? || '%') - and status != 'secretary_rejected' - and status != 'requested'`, - ) - .all(id, email); -}; - -exports.getRequestsForClerk = () => { - return db.prepare("select * from main.START_REQUESTS").all(); -}; - -exports.getRequestsForStudent = (id) => { - return db - .prepare("select * from main.START_REQUESTS where student_id = ?") - .all(id); -}; diff --git a/server/tests/integration.test.js b/server/tests/integration.test.js index a526ad8..08655e4 100644 --- a/server/tests/integration.test.js +++ b/server/tests/integration.test.js @@ -25,11 +25,11 @@ const { rejectRequest, requestChangesForRequest, modifyRequest, -} = require("../test_utils/requests"); -const { createPDF } = require("../test_utils/pdf"); +} = require("./test_utils/endpoint-requests"); +const { createPDF } = require("./test_utils/pdf"); jest.mock("../src/db"); -jest.mock("../src/protect-routes"); +jest.mock("../src/routes_utils/protect-routes"); let proposal, start_request; @@ -63,6 +63,14 @@ beforeEach(() => { db.prepare("delete from START_REQUESTS").run(); }); +it("Wrong route", async () => { + const response = await request(app).get("/api/wrong"); + expect(response.status).toBe(404); + expect(response.body).toEqual({ + message: "Endpoint not found, make sure you used the correct URL / Method", + }); +}); + describe("Protected routes", () => { it("Sets the logged in user", async () => { const email = "marco.torchiano@teacher.it"; @@ -566,6 +574,16 @@ describe("Proposal insertion tests", () => { expect(response.status).toBe(200); expect(response.body).toBeDefined(); }); + it("Insertion of a proposal with a wrong level", async () => { + proposal.level = "ZZZ"; + + logIn("marco.torchiano@teacher.it"); + const response = await insertProposal(proposal); + expect(response.status).toBe(400); + expect(response.body).toEqual({ + message: "Invalid proposal content", + }); + }); it("Insertion of a proposal with no keywords", async () => { logIn("marco.torchiano@teacher.it"); const response = await request(app) @@ -603,6 +621,16 @@ describe("Proposal insertion tests", () => { expect(response.status).toBe(400); expect(response.body.message).toBe("Invalid proposal content"); }); + it("Insertion of a proposal with a valid group but not of any co_supervisor", async () => { + logIn("marco.torchiano@teacher.it"); + proposal.co_supervisors = []; + proposal.groups.push("ELITE"); + + const response = await insertProposal(proposal); + + expect(response.status).toBe(400); + expect(response.body.message).toBe("Invalid groups"); + }); it("Insertion of a proposal with a single keyword (no array)", async () => { proposal.keywords = "SOFTWARE ENGINEERING"; logIn("marco.torchiano@teacher.it"); @@ -634,6 +662,22 @@ describe("Proposals retrieval tests", () => { expect(response.status).toBe(401); expect(response.body).toEqual({ message: "Unauthorized" }); }); + it("A secretary clerk should not view any proposal", async () => { + logIn("laura.ferrari@example.com"); + const response = await getProposals(); + expect(response.status).toBe(500); + expect(response.body).toEqual({ + message: "Internal server error", + }); + }); + it("Wrong archived query", async () => { + logIn("marco.torchiano@teacher.it"); + const response = await request(app).get("/api/proposals?archived=wrong"); + expect(response.status).toBe(400); + expect(response.body).toEqual({ + message: "Invalid parameters", + }); + }); }); describe("Application Insertion Tests", () => { @@ -1882,6 +1926,32 @@ describe("Story 28: the professor evaluates student request", () => { }); expect(response.status).toBe(404); }); + it("Only a student should be able to modify a request", async () => { + start_request.supervisor = "s123456"; // marco.torchiano@teacher.it + + logIn("s309618@studenti.polito.it"); + const thesis_request_id = (await startRequest(start_request)).body; + + logIn("laura.ferrari@example.com"); + await approveRequest(thesis_request_id); + + logIn("marco.torchiano@teacher.it"); + await requestChangesForRequest(thesis_request_id); + + logIn("laura.ferrari@example.com"); + + const modified_request = { + ...start_request, + title: "Modified title", + description: "Modified_description", + }; + modified_request.co_supervisors.push("giovanni.malnati@teacher.it"); + const response = await modifyRequest(thesis_request_id, modified_request); + expect(response.status).toBe(401); + expect(response.body).toEqual({ + message: "Only a student can change a thesis request", + }); + }); it("The secretary tries to change requests", async () => { start_request.supervisor = "s123456"; // marco.torchiano@teacher.it diff --git a/server/test_utils/requests.js b/server/tests/test_utils/endpoint-requests.js similarity index 98% rename from server/test_utils/requests.js rename to server/tests/test_utils/endpoint-requests.js index d18e20c..b1388c2 100644 --- a/server/test_utils/requests.js +++ b/server/tests/test_utils/endpoint-requests.js @@ -2,9 +2,9 @@ "use strict"; -const isLoggedIn = require("../src/protect-routes"); +const isLoggedIn = require("../../src/routes_utils/protect-routes"); const request = require("supertest"); -const { app } = require("../src/server"); +const { app } = require("../../src/server"); const dayjs = require("dayjs"); /** diff --git a/server/test_utils/pdf.js b/server/tests/test_utils/pdf.js similarity index 100% rename from server/test_utils/pdf.js rename to server/tests/test_utils/pdf.js diff --git a/server/tests/proposal.test.js b/server/tests/unit.test.js similarity index 88% rename from server/tests/proposal.test.js rename to server/tests/unit.test.js index b4646ee..1d14ef1 100644 --- a/server/tests/proposal.test.js +++ b/server/tests/unit.test.js @@ -2,41 +2,53 @@ const request = require("supertest"); const { app } = require("../src/server"); +const dayjs = require("dayjs"); +const isLoggedIn = require("../src/routes_utils/protect-routes"); +const { runCronjob, cronjobNames } = require("../src/cronjobs"); const { + getExamsOfStudent, + getGroup, getGroups, - getTeachers, - getTeacher, getDegrees, - updateApplication, - getApplicationById, - getTeacherByEmail, - getGroup, - cancelPendingApplications, + notifyRemovedCosupervisors, + notifyAddedCosupervisors, + getNotifications, +} = require("../src/dao/misc"); +const { getProposal, findAcceptedProposal, + getProposalsThatExpireInXDays, +} = require("../src/dao/proposals"); +const { + getTeacherByEmail, + getTeachers, + getTeacher, + getUser, +} = require("../src/dao/user"); +const { + getAcceptedApplicationsOfStudent, + getPendingApplicationsOfStudent, findRejectedApplication, - getNotifications, - getDelta, - setDelta, - getApplicationsOfTeacher, + getApplicationById, getApplicationsOfStudent, - getExamsOfStudent, + getApplicationsOfTeacher, + updateApplication, + cancelPendingApplications, +} = require("../src/dao/applications"); +const { getDelta, setDelta } = require("../src/dao/virtual-clock"); +const { getNotRejectedStartRequest, - getPendingApplicationsOfStudent, - getAcceptedApplicationsOfStudent, getRequestsForClerk, - getProposalsThatExpireInXDays, - notifyRemovedCosupervisors, - notifyAddedCosupervisors, -} = require("../src/theses-dao"); +} = require("../src/dao/start-requests"); -const dayjs = require("dayjs"); -const isLoggedIn = require("../src/protect-routes"); -const { runCronjob, cronjobNames } = require("../src/cronjobs"); - -jest.mock("../src/theses-dao"); -jest.mock("../src/protect-routes"); +jest.mock("../src/dao/misc"); +jest.mock("../src/dao/proposals"); +jest.mock("../src/dao/applications"); +jest.mock("../src/dao/user"); +jest.mock("../src/dao/start-requests"); +jest.mock("../src/dao/virtual-clock"); +jest.mock("../src/routes_utils/protect-routes"); const application = { proposal: 8, @@ -47,26 +59,38 @@ beforeEach(() => { application.proposal = 8; }); -describe('GET /api/sessions/current', () => { - test('should return user details if authenticated', async () => { - const mockedUser = { email: "s309618@studenti.polito.it", }; +describe("GET /api/sessions/current", () => { + test("should return user details if authenticated", async () => { + const mockedUser = { email: "s309618@studenti.polito.it" }; isLoggedIn.mockImplementation((req, res, next) => { req.user = mockedUser; next(); }); - const response = await request(app).get('/api/sessions/current'); + getUser.mockReturnValue({ + id: "s309618", + surname: "Bertetto", + name: "Lorenzo", + gender: "Male", + nationality: "Italy", + email: "s309618@studenti.polito.it", + cod_degree: "LM-32-D", + enrollment_year: 2022, + role: "student", + }); + const response = await request(app).get("/api/sessions/current"); expect(response.status).toBe(200); }); - test('should handle database error', async () => { - const mockedUser = { email: "s000000@studenti.polito.it", }; + test("should handle database error", async () => { + const mockedUser = { email: "s000000@studenti.polito.it" }; isLoggedIn.mockImplementation((req, res, next) => { req.user = mockedUser; next(); }); - const response = await request(app).get('/api/sessions/current'); + getUser.mockReturnValue(undefined); + const response = await request(app).get("/api/sessions/current"); expect(response.status).toBe(500); - expect(response.body).toEqual({ message: 'database error' }); + expect(response.body).toEqual({ message: "database error" }); }); }); @@ -102,7 +126,7 @@ describe("Career retrieval tests", () => { const studentId = "s309618"; isLoggedIn.mockImplementation((req, res, next) => next()); getExamsOfStudent.mockImplementation(() => { - throw "ERROR: SQL_SOMETHING"; + throw new Error("ERROR: SQL_SOMETHING"); }); await request(app).get(`/api/students/${studentId}/exams`).expect(500); }); @@ -126,6 +150,17 @@ describe("Application Insertion Tests", () => { }; next(); }); + getUser.mockReturnValue({ + id: "s309618", + surname: "Bertetto", + name: "Lorenzo", + gender: "Male", + nationality: "Italy", + email: "s309618@studenti.polito.it", + cod_degree: "LM-32-D", + enrollment_year: 2022, + role: "student", + }); }); test("Insertion of an application with a wrong proposal", () => { application.proposal = -5; @@ -163,6 +198,7 @@ describe("Application Insertion Tests", () => { }; next(); }); + getUser.mockReturnValue(undefined); return request(app) .post("/api/applications") .set("Content-Type", "application/json") @@ -469,6 +505,15 @@ describe("Applications retrieval tests", () => { }; next(); }); + getUser.mockReturnValue({ + id: "s123456", + surname: "Torchiano", + name: "Marco", + email: "marco.torchiano@teacher.it", + cod_group: "SOFTENG", + cod_department: "DAUIN", + role: "teacher", + }); getApplicationsOfTeacher.mockReturnValue([]); await request(app) .get("/api/applications") @@ -507,7 +552,6 @@ describe("Get All Teachers Test", () => { .then((response) => { // Assuming the response body is an array expect(Array.isArray(response.body)).toBe(true); - // todo: Add more specific checks on the response body if needed }); }); test("Get 200 for an empty group table db", () => { @@ -705,6 +749,17 @@ describe("PATCH /api/applications/:id", () => { getApplicationById.mockReturnValue({ state: "accepted", }); + getUser.mockReturnValue({ + id: "s309618", + surname: "Bertetto", + name: "Lorenzo", + gender: "Male", + nationality: "Italy", + email: "s309618@studenti.polito.it", + cod_degree: "LM-32-D", + enrollment_year: 2022, + role: "student", + }); const response = await request(app) .patch("/api/applications/1") .send(Buffer.alloc(5)) @@ -772,7 +827,7 @@ describe("GET /api/virtualClock", () => { getDelta.mockReturnValueOnce({ delta: 3 }); const response = await request(app).get("/api/virtualClock"); expect(response.status).toBe(200); - expect(response.body).toEqual(dayjs().add("3", "day").format("YYYY-MM-DD")); + expect(response.body).toEqual(dayjs().add(3, "day").format("YYYY-MM-DD")); }); it("should handle server error and respond with status 500", async () => { getDelta.mockImplementation(() => { @@ -851,6 +906,15 @@ describe("POST /api/start-requests", () => { }; next(); }); + getUser.mockReturnValue({ + id: "s234567", + surname: "Morisio", + name: "Maurizio", + email: "maurizio.morisio@teacher.it", + cod_group: "SOFTENG", + cod_department: "DAUIN", + role: "teacher", + }); getTeacher.mockReturnValue({ email: "fake@fake.com" }); getNotRejectedStartRequest.mockReturnValue([]); return request(app) @@ -874,6 +938,17 @@ describe("POST /api/start-requests", () => { }; next(); }); + getUser.mockReturnValue({ + id: "s309618", + surname: "Bertetto", + name: "Lorenzo", + gender: "Male", + nationality: "Italy", + email: "s309618@studenti.polito.it", + cod_degree: "LM-32-D", + enrollment_year: 2022, + role: "student", + }); getTeacher.mockReturnValue({ email: "fake@fake.com" }); getNotRejectedStartRequest.mockReturnValue([]); return request(app) @@ -922,7 +997,9 @@ describe("GET /api/start-requests", () => { }; next(); }); - + getUser.mockReturnValue({ + role: "secretary_clerk", + }); const expectedRequests = [ { id: 1, @@ -963,24 +1040,27 @@ describe("Cronjobs", () => { getDelta.mockReturnValue({ delta: 2 }); getProposalsThatExpireInXDays.mockReturnValue([ { - supervisor: "s234567", + supervisor: "s234567", teacher_surname: "Torchiano", teacher_email: "marco.torchiano@teacher.it", teacher_name: "Marco", expiration_date: expectedDate.format("YYYY-MM-DD"), - title: "Test proposal" - } + title: "Test proposal", + }, ]); - await runCronjob(cronjobNames.THESIS_EXPIRED); + runCronjob(cronjobNames.THESIS_EXPIRED); }); }); describe("PUT /api/proposals/:id", () => { test("Remove a co supervisor", async () => { - - const originalModule = jest.requireActual("../src/theses-dao"); - notifyRemovedCosupervisors.mockImplementation(originalModule.notifyRemovedCosupervisors); - notifyAddedCosupervisors.mockImplementation(originalModule.notifyAddedCosupervisors); + const originalModule = jest.requireActual("../src/dao/misc"); + notifyRemovedCosupervisors.mockImplementation( + originalModule.notifyRemovedCosupervisors, + ); + notifyAddedCosupervisors.mockImplementation( + originalModule.notifyAddedCosupervisors, + ); isLoggedIn.mockImplementation((req, res, next) => { req.user = { @@ -988,6 +1068,10 @@ describe("PUT /api/proposals/:id", () => { }; next(); }); + getUser.mockReturnValue({ + id: "s234567", + role: "teacher", + }); const proposal = { id: 1, @@ -1004,16 +1088,14 @@ describe("PUT /api/proposals/:id", () => { level: "MSC", cds: "LM-32 (DM270)", manually_archived: 0, - deleted: 0 + deleted: 0, }; const modifiedProposal = { id: 1, title: "test", description: "desc test", - co_supervisors: [ - "luigi.derussis@teacher.it" - ], + co_supervisors: ["luigi.derussis@teacher.it"], keywords: ["keyword1", "keyword2"], groups: [], types: ["EXPERIMENTAL"], @@ -1021,18 +1103,16 @@ describe("PUT /api/proposals/:id", () => { notes: "notes", expiration_date: "2021-01-01", level: "MSC", - cds: "LM-32 (DM270)" + cds: "LM-32 (DM270)", }; getProposal.mockReturnValue(proposal); - const requests = ( - await request(app) - .put("/api/proposals/1") - .set("Content-Type", "application/json") - .send(modifiedProposal) - .expect(200) - ); + const requests = await request(app) + .put("/api/proposals/1") + .set("Content-Type", "application/json") + .send(modifiedProposal) + .expect(200); expect(requests.body).toEqual({ message: "Proposal updated successfully" }); }); }); diff --git a/server/tests/utils.test.js b/server/tests/utils.test.js index cc03265..75c1af4 100644 --- a/server/tests/utils.test.js +++ b/server/tests/utils.test.js @@ -1,29 +1,29 @@ "use strict"; -const isLoggedIn = require('../src/protect-routes'); +const isLoggedIn = require("../src/routes_utils/protect-routes"); -describe('isLoggedIn Middleware', () => { - it('should call next() if res is not defined', () => { - const req = { isAuthenticated: jest.fn(() => true) }; - const res = undefined; - const next = jest.fn(); - isLoggedIn(req, res, next); - expect(next).toHaveBeenCalled(); - }); +describe("isLoggedIn Middleware", () => { + it("should call next() if res is not defined", () => { + const req = { isAuthenticated: jest.fn(() => true) }; + const res = undefined; + const next = jest.fn(); + isLoggedIn(req, res, next); + expect(next).toHaveBeenCalled(); + }); - it('should return 401 Unauthorized if user is not authenticated', () => { - const req = { isAuthenticated: jest.fn(() => false) }; - const mockRes = () => { - const res = {}; - res.status = jest.fn().mockReturnValue(res); - res.json = jest.fn().mockReturnValue(res); - return res; - };; - const res = mockRes(); - const next = jest.fn(); - isLoggedIn(req, res, next); - expect(next).not.toHaveBeenCalled(); - expect(res.status).toHaveBeenCalledWith(401); - expect(res.json).toHaveBeenCalledWith({ message: 'Unauthorized' }); - }); - }); \ No newline at end of file + it("should return 401 Unauthorized if user is not authenticated", () => { + const req = { isAuthenticated: jest.fn(() => false) }; + const mockRes = () => { + const res = {}; + res.status = jest.fn().mockReturnValue(res); + res.json = jest.fn().mockReturnValue(res); + return res; + }; + const res = mockRes(); + const next = jest.fn(); + isLoggedIn(req, res, next); + expect(next).not.toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(401); + expect(res.json).toHaveBeenCalledWith({ message: "Unauthorized" }); + }); +});