Skip to content

Commit

Permalink
Merge pull request #114 from lennygir/feat/169-thesis-expiration
Browse files Browse the repository at this point in the history
feat: notify when a proposal expires in 7 days
  • Loading branch information
valerianoCarlos authored Jan 16, 2024
2 parents 1ba683f + d569877 commit df87c52
Show file tree
Hide file tree
Showing 8 changed files with 163 additions and 1 deletion.
23 changes: 23 additions & 0 deletions server/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"dependencies": {
"better-sqlite3": "^9.1.1",
"cors": "^2.8.5",
"cron": "^3.1.6",
"dayjs": "^1.11.10",
"dotenv": "^16.3.1",
"eslint-plugin-jest": "^27.6.0",
Expand Down
32 changes: 32 additions & 0 deletions server/src/cronjobs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
const { CronJob } = require('cron');
const { getProposalsThatExpireInXDays, notifyProposalExpiration } = require('./theses-dao');

const cronjobNames = {
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);
};

const runCronjob = (cronjobName) => {
console.log('[Cron] Running job ' + cronjobName);
if(cronjobs[cronjobName]) {
cronjobs[cronjobName].fireOnTick();
}
};

module.exports = {
cronjobNames,
runCronjob,
initCronjobs
};
21 changes: 21 additions & 0 deletions server/src/mail/proposal-expiration.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const proposalExpirationHtmlTemplate = (variables) => {
return `
<html>
<body>
<p>Dear ${variables.name},</p>
<p>your thesis proposal "${variables.thesis}" expires in ${variables.nbOfDays} days (${variables.date}).</p>
<p>Best regards,</p>
<p>the Thesis Managment system</p>
</body>
</html>
`;
};

const proposalExpirationTextTemplate = (variables) => {
return `Dear ${variables.name},\nyour thesis proposal "${variables.thesis}" expires in ${variables.nbOfDays} days (${variables.date}).\nBest regards,\nthe Thesis Managment system`;
};

exports.proposalExpirationTemplate = (variables) => ({
html: proposalExpirationHtmlTemplate(variables),
text: proposalExpirationTextTemplate(variables)
});
2 changes: 2 additions & 0 deletions server/src/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ const {
getAcceptedProposal,
} = require("./theses-dao");
const { getUser } = require("./user-dao");
const { runCronjob, cronjobNames } = require("./cronjobs");

function getDate() {
const clock = getDelta();
Expand Down Expand Up @@ -887,6 +888,7 @@ router.patch(
return res.status(400).json({ message: "Cannot go back in the past" });
}
setDelta(newDelta);
runCronjob(cronjobNames.THESIS_EXPIRED);
return res.status(200).json({ message: "Date successfully changed" });
} catch (err) {
return res.status(500).json({ message: "Internal Server Error" });
Expand Down
3 changes: 3 additions & 0 deletions server/src/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const session = require("express-session");
const passport = require("passport");
require("./passport");
const { router } = require("./routes");
const { initCronjobs } = require("./cronjobs");

const app = new express();

Expand All @@ -15,6 +16,8 @@ const corsOptions = {
};
app.use(cors(corsOptions));

initCronjobs();

app.use(
session({
secret: "Alright, then, keep your secrets.",
Expand Down
62 changes: 61 additions & 1 deletion server/src/theses-dao.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ const { newApplicationTemplate } = require("./mail/new-application");
const { supervisorStartRequestTemplate } = require("./mail/supervisor-start-request");
const { cosupervisorApplicationDecisionTemplate } = require("./mail/cosupervisor-application-decision");
const { cosupervisorStartRequestTemplate } = require("./mail/cosupervisor-start-request");
const dayjs = require("dayjs");
const { proposalExpirationTemplate } = require("./mail/proposal-expiration");

exports.insertApplication = (proposal, student, state) => {
const result = db
Expand Down Expand Up @@ -441,6 +443,35 @@ exports.notifyNewApplication = async (proposalId) => {
);
};

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) VALUES(?,?,?)",
).run(
proposal.supervisor,
"Your proposal expires in 7 days",
mailBody.text,
);
};

/**
* @param teacher_id
* @returns {[
Expand Down Expand Up @@ -482,6 +513,34 @@ exports.getApplicationsOfTeacher = (teacher_id) => {
.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(
Expand Down Expand Up @@ -553,9 +612,10 @@ exports.updateArchivedStateProposal = (new_archived_state, proposal_id) => {
);
};

exports.getDelta = () => {
const getDelta = () => {
return db.prepare("select delta from VIRTUAL_CLOCK where id = 1").get();
};
exports.getDelta = getDelta;

exports.setDelta = (delta) => {
return db
Expand Down
20 changes: 20 additions & 0 deletions server/tests/proposal.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,12 @@ const {
getPendingApplicationsOfStudent,
getAcceptedApplicationsOfStudent,
getRequestsForClerk,
getProposalsThatExpireInXDays,
} = require("../src/theses-dao");

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");
Expand Down Expand Up @@ -942,3 +944,21 @@ describe("GET /api/start-requests", () => {
.expect(200);
});
});

describe("Cronjobs", () => {
test("Notify on thesis expiration (7 days before)", async () => {
const expectedDate = dayjs().add(7 + 2, "day");
getDelta.mockReturnValue({ delta: 2 });
getProposalsThatExpireInXDays.mockReturnValue([
{
supervisor: "s234567",
teacher_surname: "Torchiano",
teacher_email: "[email protected]",
teacher_name: "Marco",
expiration_date: expectedDate.format("YYYY-MM-DD"),
title: "Test proposal"
}
]);
await runCronjob(cronjobNames.THESIS_EXPIRED);
});
});

0 comments on commit df87c52

Please sign in to comment.