Added a new type of e-mail to send to students when a new assignment is created

This commit is contained in:
Tiago Ribeiro
2024-01-17 15:55:33 +00:00
parent 63d2baf35f
commit 74dcccf089
6 changed files with 178 additions and 126 deletions

View File

@@ -34,8 +34,12 @@ export default function AssignmentCreator({isCreating, assignment, assigner, gro
const [assignees, setAssignees] = useState<string[]>(assignment?.assignees || []); const [assignees, setAssignees] = useState<string[]>(assignment?.assignees || []);
const [name, setName] = useState(assignment?.name || generate({minLength: 6, maxLength: 8, min: 2, max: 3, join: " ", formatter: capitalize})); const [name, setName] = useState(assignment?.name || generate({minLength: 6, maxLength: 8, min: 2, max: 3, join: " ", formatter: capitalize}));
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [startDate, setStartDate] = useState<Date | null>(assignment ? moment(assignment.startDate).toDate() : moment().add(1, "day").toDate()); const [startDate, setStartDate] = useState<Date | null>(
const [endDate, setEndDate] = useState<Date | null>(assignment ? moment(assignment.endDate).toDate() : moment().add(8, "day").toDate()); assignment ? moment(assignment.startDate).toDate() : moment().hours(0).minutes(0).add(1, "day").toDate(),
);
const [endDate, setEndDate] = useState<Date | null>(
assignment ? moment(assignment.endDate).toDate() : moment().hours(23).minutes(59).add(8, "day").toDate(),
);
// creates a new exam for each assignee or just one exam for all assignees // creates a new exam for each assignee or just one exam for all assignees
const [generateMultiple, setGenerateMultiple] = useState<boolean>(false); const [generateMultiple, setGenerateMultiple] = useState<boolean>(false);
@@ -51,23 +55,16 @@ export default function AssignmentCreator({isCreating, assignment, assigner, gro
const createAssignment = () => { const createAssignment = () => {
setIsLoading(true); setIsLoading(true);
(assignment ? axios.patch : axios.post)( (assignment ? axios.patch : axios.post)(`/api/assignments${assignment ? `/${assignment.id}` : ""}`, {
`/api/assignments${assignment ? `/${assignment.id}` : ""}`,
{
assignees, assignees,
name, name,
startDate, startDate,
endDate, endDate,
selectedModules, selectedModules,
generateMultiple, generateMultiple,
} })
)
.then(() => { .then(() => {
toast.success( toast.success(`The assignment "${name}" has been ${assignment ? "updated" : "created"} successfully!`);
`The assignment "${name}" has been ${
assignment ? "updated" : "created"
} successfully!`
);
cancelCreation(); cancelCreation();
}) })
.catch((e) => { .catch((e) => {
@@ -285,7 +282,7 @@ export default function AssignmentCreator({isCreating, assignment, assigner, gro
</div> </div>
</section> </section>
<div className="flex gap-4 w-full justify-end"> <div className="flex gap-4 w-full justify-end">
<Checkbox isChecked={generateMultiple} onChange={() => setGenerateMultiple(d => !d)}> <Checkbox isChecked={generateMultiple} onChange={() => setGenerateMultiple((d) => !d)}>
Generate different exams Generate different exams
</Checkbox> </Checkbox>
</div> </div>

View File

@@ -40,3 +40,15 @@ export function prepareMailOptions(context: object, to: string[], subject: strin
context, context,
}; };
} }
export async function sendEmail(template: string, context: object, to: string[], subject: string): Promise<boolean> {
try {
const transport = prepareMailer(template);
const mailOptions = prepareMailOptions(context, to, subject, template);
await transport.sendMail(mailOptions);
return true;
} catch {
return false;
}
}

View File

@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet">
</head>
<div>
<p>Hello {{user.name}},</p>
<br />
<p>You have just been given the assignment <b>"{{assignment.name}}"</b> by your teacher {{assignment.assigner}}!</p>
<br />
<p>It's start date will be on <b>{{assignment.startDate}}</b> and will only last until <b>{{assignment.endDate}}</b>
</p>
<br />
<p>For this assignment, you've been tasked with completing exams of the following modules:
<b>{{assignment.modules}}</b>.
</p>
<br />
<p>Don't forget to do it before its end date!</p>
<p>Click <b><a href="https://platform.encoach.com">here</a></b> to open the EnCoach Platform!</p>
<br />
<p>Thanks,</p>
<p>Your EnCoach team</p>
</div>
</html>

View File

@@ -0,0 +1,13 @@
{
"user": {
"name": "Tiago Ribeiro"
},
"assignment": {
"name": "Final Exam",
"assigner": "Teacher",
"assignees": [],
"modules": "Reading and Writing",
"startDate": "24/12/2023",
"endDate": "27/01/2024"
}
}

View File

@@ -1,14 +1,17 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction // Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type {NextApiRequest, NextApiResponse} from "next"; import type {NextApiRequest, NextApiResponse} from "next";
import {app} from "@/firebase"; import {app} from "@/firebase";
import {getFirestore, collection, getDocs, query, where, setDoc, doc} from "firebase/firestore"; import {getFirestore, collection, getDocs, query, where, setDoc, doc, getDoc} from "firebase/firestore";
import {withIronSessionApiRoute} from "iron-session/next"; import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session"; import {sessionOptions} from "@/lib/session";
import {uuidv4} from "@firebase/util"; import {uuidv4} from "@firebase/util";
import { Module } from "@/interfaces"; import {Module} from "@/interfaces";
import { getExams } from "@/utils/exams.be"; import {getExams} from "@/utils/exams.be";
import { Exam } from "@/interfaces/exam"; import {Exam} from "@/interfaces/exam";
import { flatten } from "lodash"; import {capitalize, flatten} from "lodash";
import {User} from "@/interfaces/user";
import moment from "moment";
import {sendEmail} from "@/email";
const db = getFirestore(app); const db = getFirestore(app);
@@ -49,86 +52,69 @@ function getRandomIndex(arr: any[]): number {
return randomIndex; return randomIndex;
} }
const generateExams = async ( const generateExams = async (generateMultiple: Boolean, selectedModules: Module[], assignees: string[]): Promise<ExamWithUser[]> => {
generateMultiple: Boolean,
selectedModules: Module[],
assignees: string[]
): Promise<ExamWithUser[]> => {
if (generateMultiple) { if (generateMultiple) {
// for optimization purposes, it would be better to create a new endpoint that returned the answers for all users at once // for optimization purposes, it would be better to create a new endpoint that returned the answers for all users at once
const allExams = await assignees.map(async (assignee) => { const allExams = await assignees.map(async (assignee) => {
const selectedModulePromises = await selectedModules.map( const selectedModulePromises = await selectedModules.map(async (module: Module) => {
async (module: Module) => {
try { try {
const exams: Exam[] = await getExams(db, module, "true", assignee); const exams: Exam[] = await getExams(db, module, "true", assignee);
const exam = exams[getRandomIndex(exams)]; const exam = exams[getRandomIndex(exams)];
if (exam) { if (exam) {
return { module: exam.module, id: exam.id, assignee }; return {module: exam.module, id: exam.id, assignee};
} }
return null; return null;
} catch (e) { } catch (e) {
console.error(e); console.error(e);
return null; return null;
} }
}, }, []);
[]
);
const newModules = await Promise.all(selectedModulePromises); const newModules = await Promise.all(selectedModulePromises);
return newModules; return newModules;
}, []); }, []);
const exams = flatten(await Promise.all(allExams)).filter( const exams = flatten(await Promise.all(allExams)).filter((x) => x !== null) as ExamWithUser[];
(x) => x !== null
) as ExamWithUser[];
return exams; return exams;
} }
const selectedModulePromises = await selectedModules.map( const selectedModulePromises = await selectedModules.map(async (module: Module) => {
async (module: Module) => {
const exams: Exam[] = await getExams(db, module, "false", undefined); const exams: Exam[] = await getExams(db, module, "false", undefined);
const exam = exams[getRandomIndex(exams)]; const exam = exams[getRandomIndex(exams)];
if (exam) { if (exam) {
return { module: exam.module, id: exam.id }; return {module: exam.module, id: exam.id};
} }
return null; return null;
} });
);
const exams = await Promise.all(selectedModulePromises); const exams = await Promise.all(selectedModulePromises);
const examesFiltered = exams.filter((x) => x !== null) as ExamWithUser[]; const examesFiltered = exams.filter((x) => x !== null) as ExamWithUser[];
return flatten( return flatten(assignees.map((assignee) => examesFiltered.map((exam) => ({...exam, assignee}))));
assignees.map((assignee) =>
examesFiltered.map((exam) => ({ ...exam, assignee }))
)
);
}; };
async function POST(req: NextApiRequest, res: NextApiResponse) { async function POST(req: NextApiRequest, res: NextApiResponse) {
const { const {
selectedModules, selectedModules,
assignees, assignees,
// Generarte multiple true would generate an unique exam for eacah user // Generate multiple true would generate an unique exam for each user
// false would generate the same exam for all usersa // false would generate the same exam for all users
generateMultiple = false, generateMultiple = false,
...body ...body
} = req.body as { } = req.body as {
selectedModules: Module[]; selectedModules: Module[];
assignees: string[]; assignees: string[];
generateMultiple: Boolean; generateMultiple: Boolean;
name: string;
startDate: string;
endDate: string;
}; };
const exams: ExamWithUser[] = await generateExams( const exams: ExamWithUser[] = await generateExams(generateMultiple, selectedModules, assignees);
generateMultiple,
selectedModules,
assignees
);
if (exams.length === 0) { if (exams.length === 0) {
res res.status(400).json({ok: false, error: "No exams found for the selected modules"});
.status(400)
.json({ ok: false, error: "No exams found for the selected modules" });
return; return;
} }
@@ -140,5 +126,24 @@ async function POST(req: NextApiRequest, res: NextApiResponse) {
...body, ...body,
}); });
res.status(200).json({ ok: true }); res.status(200).json({ok: true});
for (const assigneeID of assignees) {
const assigneeSnapshot = await getDoc(doc(db, "users", assigneeID));
if (!assigneeSnapshot.exists()) continue;
const assignee = {id: assigneeID, ...assigneeSnapshot.data()} as User;
const name = body.name;
const teacher = req.session.user!;
const examModulesLabel = exams.map((x) => capitalize(x.module)).join(", ");
const startDate = moment(body.startDate).format("DD/MM/YYYY - HH:mm");
const endDate = moment(body.endDate).format("DD/MM/YYYY - HH:mm");
await sendEmail(
"assignment",
{user: {name: assignee.name}, assignment: {name, startDate, endDate, modules: examModulesLabel, assigner: teacher.name}},
[assignee.email],
"EnCoach - New Assignment!",
);
}
} }

View File

@@ -4,7 +4,7 @@ import {getAuth as getAdminAuth, UserRecord} from "firebase-admin/auth";
import {app, adminApp} from "@/firebase"; import {app, adminApp} from "@/firebase";
import {sessionOptions} from "@/lib/session"; import {sessionOptions} from "@/lib/session";
import {withIronSessionApiRoute} from "iron-session/next"; import {withIronSessionApiRoute} from "iron-session/next";
import {prepareMailer, prepareMailOptions} from "@/email"; import {prepareMailer, prepareMailOptions, sendEmail} from "@/email";
import ShortUniqueId from "short-unique-id"; import ShortUniqueId from "short-unique-id";
export default withIronSessionApiRoute(sendVerification, sessionOptions); export default withIronSessionApiRoute(sendVerification, sessionOptions);
@@ -13,8 +13,8 @@ async function sendVerification(req: NextApiRequest, res: NextApiResponse) {
const short = new ShortUniqueId(); const short = new ShortUniqueId();
if (req.session.user) { if (req.session.user) {
const transport = prepareMailer("verification"); const ok = await sendEmail(
const mailOptions = prepareMailOptions( "verification",
{ {
name: req.session.user.name, name: req.session.user.name,
code: short.randomUUID(6), code: short.randomUUID(6),
@@ -22,12 +22,9 @@ async function sendVerification(req: NextApiRequest, res: NextApiResponse) {
}, },
[req.session.user.email], [req.session.user.email],
"EnCoach Verification", "EnCoach Verification",
"verification",
); );
await transport.sendMail(mailOptions); res.status(200).json({ok});
res.status(200).json({ok: true});
return; return;
} }
res.status(404).json({ok: false}); res.status(404).json({ok: false});