Allowed admins and others to download reports related to other users

This commit is contained in:
Tiago Ribeiro
2024-01-21 12:48:29 +00:00
parent f6bb69f994
commit 83b8ab7774

View File

@@ -1,33 +1,20 @@
import type { NextApiRequest, NextApiResponse } from "next"; import type {NextApiRequest, NextApiResponse} from "next";
import { app, storage } from "@/firebase"; import {app, storage} from "@/firebase";
import { import {getFirestore, doc, getDoc, updateDoc, getDocs, query, collection, where} from "firebase/firestore";
getFirestore, import {withIronSessionApiRoute} from "iron-session/next";
doc, import {sessionOptions} from "@/lib/session";
getDoc,
updateDoc,
getDocs,
query,
collection,
where,
} from "firebase/firestore";
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import ReactPDF from "@react-pdf/renderer"; import ReactPDF from "@react-pdf/renderer";
import TestReport from "@/exams/pdf/test.report"; import TestReport from "@/exams/pdf/test.report";
import { ref, uploadBytes, getDownloadURL } from "firebase/storage"; import {ref, uploadBytes, getDownloadURL} from "firebase/storage";
import { DemographicInformation, User } from "@/interfaces/user"; import {DemographicInformation, User} from "@/interfaces/user";
import { Module } from "@/interfaces"; import {Module} from "@/interfaces";
import { ModuleScore } from "@/interfaces/module.scores"; import {ModuleScore} from "@/interfaces/module.scores";
import { SkillExamDetails } from "@/exams/pdf/details/skill.exam"; import {SkillExamDetails} from "@/exams/pdf/details/skill.exam";
import { LevelExamDetails } from "@/exams/pdf/details/level.exam"; import {LevelExamDetails} from "@/exams/pdf/details/level.exam";
import { calculateBandScore } from "@/utils/score"; import {calculateBandScore} from "@/utils/score";
import axios from "axios"; import axios from "axios";
import { moduleLabels } from "@/utils/moduleUtils"; import {moduleLabels} from "@/utils/moduleUtils";
import { import {generateQRCode, getRadialProgressPNG, streamToBuffer} from "@/utils/pdf";
generateQRCode,
getRadialProgressPNG,
streamToBuffer,
} from "@/utils/pdf";
import moment from "moment-timezone"; import moment from "moment-timezone";
const db = getFirestore(app); const db = getFirestore(app);
@@ -35,350 +22,318 @@ const db = getFirestore(app);
export default withIronSessionApiRoute(handler, sessionOptions); export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) { async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "GET") return get(req, res); if (req.method === "GET") return get(req, res);
if (req.method === "POST") return post(req, res); if (req.method === "POST") return post(req, res);
} }
const getExamSummary = (score: number) => { const getExamSummary = (score: number) => {
if (score > 0.8) { if (score > 0.8) {
return "Scoring between 81% and 100% on the English exam demonstrates an outstanding level of proficiency in writing, speaking, listening, and reading. Mastery of key concepts is evident across all language domains, showcasing not only a high level of skill but also a dedication to excellence. Continuing to challenge oneself with advanced material in writing, speaking, listening, and reading will further refine the already impressive command of the English language."; return "Scoring between 81% and 100% on the English exam demonstrates an outstanding level of proficiency in writing, speaking, listening, and reading. Mastery of key concepts is evident across all language domains, showcasing not only a high level of skill but also a dedication to excellence. Continuing to challenge oneself with advanced material in writing, speaking, listening, and reading will further refine the already impressive command of the English language.";
} }
if (score > 0.6) { if (score > 0.6) {
return "Scoring between 61% and 80% on the English exam, encompassing writing, speaking, listening, and reading, reflects a commendable level of proficiency in each domain. There's evidence of a solid grasp of key concepts, and effective application of skills. Room for refinement and deeper exploration in writing, speaking, listening, and reading remains, presenting an opportunity for further mastery."; return "Scoring between 61% and 80% on the English exam, encompassing writing, speaking, listening, and reading, reflects a commendable level of proficiency in each domain. There's evidence of a solid grasp of key concepts, and effective application of skills. Room for refinement and deeper exploration in writing, speaking, listening, and reading remains, presenting an opportunity for further mastery.";
} }
if (score > 0.4) { if (score > 0.4) {
return "Scoring between 41% and 60% on the English exam across writing, speaking, listening, and reading demonstrates a moderate level of understanding in each domain. While there's a commendable grasp of key concepts, refining fundamental skills in writing, speaking, listening, and reading can lead to notable improvement. Consistent effort and targeted focus on weaker areas are recommended."; return "Scoring between 41% and 60% on the English exam across writing, speaking, listening, and reading demonstrates a moderate level of understanding in each domain. While there's a commendable grasp of key concepts, refining fundamental skills in writing, speaking, listening, and reading can lead to notable improvement. Consistent effort and targeted focus on weaker areas are recommended.";
} }
if (score > 0.2) { if (score > 0.2) {
return "Scoring between 21% and 40% on the English exam, spanning writing, speaking, listening, and reading, indicates some understanding of key concepts in each domain. However, there's room for improvement in fundamental skills. Strengthening writing, speaking, listening, and reading abilities through consistent effort and focused study will contribute to overall proficiency."; return "Scoring between 21% and 40% on the English exam, spanning writing, speaking, listening, and reading, indicates some understanding of key concepts in each domain. However, there's room for improvement in fundamental skills. Strengthening writing, speaking, listening, and reading abilities through consistent effort and focused study will contribute to overall proficiency.";
} }
return "This student's performance on the English exam, encompassing writing, speaking, listening, and reading, reflects a significant need for improvement, scoring between 0% and 20%. There's a notable gap in understanding key concepts across all language domains. Strengthening fundamental skills in writing, speaking, listening, and reading is crucial. Developing a consistent study routine and seeking additional support in each area can contribute to substantial progress."; return "This student's performance on the English exam, encompassing writing, speaking, listening, and reading, reflects a significant need for improvement, scoring between 0% and 20%. There's a notable gap in understanding key concepts across all language domains. Strengthening fundamental skills in writing, speaking, listening, and reading is crucial. Developing a consistent study routine and seeking additional support in each area can contribute to substantial progress.";
}; };
const getLevelSummary = (score: number) => { const getLevelSummary = (score: number) => {
if (score > 0.8) { if (score > 0.8) {
return "Scoring between 81% and 100% on the English exam showcases an outstanding level of understanding and proficiency. Your performance reflects a mastery of key concepts, including grammar, vocabulary, and comprehension. You exhibit a high level of skill in applying these elements effectively. Your dedication to excellence is evident, and your consistent, stellar performance is commendable. Continue to challenge yourself with advanced material to further refine your already impressive command of the English language. Your commitment to excellence positions you as a standout student in English studies, and your achievements are a testament to your hard work and capability."; return "Scoring between 81% and 100% on the English exam showcases an outstanding level of understanding and proficiency. Your performance reflects a mastery of key concepts, including grammar, vocabulary, and comprehension. You exhibit a high level of skill in applying these elements effectively. Your dedication to excellence is evident, and your consistent, stellar performance is commendable. Continue to challenge yourself with advanced material to further refine your already impressive command of the English language. Your commitment to excellence positions you as a standout student in English studies, and your achievements are a testament to your hard work and capability.";
} }
if (score > 0.6) { if (score > 0.6) {
return "Scoring between 61% and 80% on the English exam reflects a commendable level of understanding and proficiency. You have demonstrated a solid grasp of key concepts, including grammar, vocabulary, and comprehension. There's evidence of effective application of skills, but room for refinement and deeper exploration remains. Consistent effort in honing nuanced aspects of language will contribute to even greater mastery. Continue engaging with challenging material and seeking opportunities for advanced comprehension. With sustained dedication, you have the potential to elevate your performance to an exceptional level and further excel in your English studies."; return "Scoring between 61% and 80% on the English exam reflects a commendable level of understanding and proficiency. You have demonstrated a solid grasp of key concepts, including grammar, vocabulary, and comprehension. There's evidence of effective application of skills, but room for refinement and deeper exploration remains. Consistent effort in honing nuanced aspects of language will contribute to even greater mastery. Continue engaging with challenging material and seeking opportunities for advanced comprehension. With sustained dedication, you have the potential to elevate your performance to an exceptional level and further excel in your English studies.";
} }
if (score > 0.4) { if (score > 0.4) {
return "Scoring between 41% and 60% on the English exam reflects a moderate level of understanding. You demonstrate a grasp of some key concepts, but there's room for refinement in areas like grammar, vocabulary, and comprehension. Consistent effort and a strategic focus on weaker areas can lead to notable improvement. Engaging with supplementary resources and seeking feedback will further enhance your skills. With continued dedication, there's a solid foundation to build upon, and achieving a higher level of proficiency is within reach. Keep up the good work and aim for sustained progress in your English studies."; return "Scoring between 41% and 60% on the English exam reflects a moderate level of understanding. You demonstrate a grasp of some key concepts, but there's room for refinement in areas like grammar, vocabulary, and comprehension. Consistent effort and a strategic focus on weaker areas can lead to notable improvement. Engaging with supplementary resources and seeking feedback will further enhance your skills. With continued dedication, there's a solid foundation to build upon, and achieving a higher level of proficiency is within reach. Keep up the good work and aim for sustained progress in your English studies.";
} }
if (score > 0.2) { if (score > 0.2) {
return "Scoring between 21% and 40% on the English exam shows some understanding of key concepts, but there's still ample room for improvement. Strengthening foundational skills, such as grammar, vocabulary, and comprehension, is essential. Consistent effort and focused study can help bridge gaps in knowledge and elevate your performance. Consider seeking additional guidance or resources to refine your understanding of the material. With commitment and targeted improvements, you have the potential to make significant strides in your English proficiency."; return "Scoring between 21% and 40% on the English exam shows some understanding of key concepts, but there's still ample room for improvement. Strengthening foundational skills, such as grammar, vocabulary, and comprehension, is essential. Consistent effort and focused study can help bridge gaps in knowledge and elevate your performance. Consider seeking additional guidance or resources to refine your understanding of the material. With commitment and targeted improvements, you have the potential to make significant strides in your English proficiency.";
} }
return "Your performance on the English exam falls within the 0% to 20% range, indicating a need for improvement. There's room to enhance your grasp of fundamental concepts like grammar, vocabulary, and comprehension. Establishing a consistent study routine and seeking extra support can be beneficial. With dedication and targeted efforts, you have the potential to significantly boost your performance in upcoming assessments."; return "Your performance on the English exam falls within the 0% to 20% range, indicating a need for improvement. There's room to enhance your grasp of fundamental concepts like grammar, vocabulary, and comprehension. Establishing a consistent study routine and seeking extra support can be beneficial. With dedication and targeted efforts, you have the potential to significantly boost your performance in upcoming assessments.";
}; };
const getPerformanceSummary = (module: Module, score: number) => { const getPerformanceSummary = (module: Module, score: number) => {
if (module === "level") return getLevelSummary(score); if (module === "level") return getLevelSummary(score);
return getExamSummary(score); return getExamSummary(score);
}; };
interface SkillsFeedbackRequest { interface SkillsFeedbackRequest {
code: Module; code: Module;
name: string; name: string;
grade: number; grade: number;
} }
interface SkillsFeedbackResponse extends SkillsFeedbackRequest { interface SkillsFeedbackResponse extends SkillsFeedbackRequest {
evaluation: string; evaluation: string;
suggestions: string; suggestions: string;
} }
const getSkillsFeedback = async (sections: SkillsFeedbackRequest[]) => { const getSkillsFeedback = async (sections: SkillsFeedbackRequest[]) => {
const backendRequest = await axios.post( const backendRequest = await axios.post(
`${process.env.BACKEND_URL}/grading_summary`, `${process.env.BACKEND_URL}/grading_summary`,
{ sections }, {sections},
{ {
headers: { headers: {
Authorization: `Bearer ${process.env.BACKEND_JWT}`, Authorization: `Bearer ${process.env.BACKEND_JWT}`,
}, },
} },
); );
return backendRequest.data?.sections; return backendRequest.data?.sections;
}; };
// perform the request with several retries if needed // perform the request with several retries if needed
const handleSkillsFeedbackRequest = async ( const handleSkillsFeedbackRequest = async (sections: SkillsFeedbackRequest[]): Promise<SkillsFeedbackResponse[] | null> => {
sections: SkillsFeedbackRequest[] let i = 0;
): Promise<SkillsFeedbackResponse[] | null> => { try {
let i = 0; const data = await getSkillsFeedback(sections);
try { return data;
const data = await getSkillsFeedback(sections); } catch (err) {
return data; if (i < 3) {
} catch (err) { i++;
if (i < 3) { return handleSkillsFeedbackRequest(sections);
i++; }
return handleSkillsFeedbackRequest(sections);
}
return null; return null;
} }
}; };
async function post(req: NextApiRequest, res: NextApiResponse) { async function post(req: NextApiRequest, res: NextApiResponse) {
// verify if it's a logged user that is trying to export // verify if it's a logged user that is trying to export
if (req.session.user) { if (req.session.user) {
const { id } = req.query as { id: string }; const {id} = req.query as {id: string};
// fetch stats entries for this particular user with the requested exam session // fetch stats entries for this particular user with the requested exam session
const docsSnap = await getDocs( const docsSnap = await getDocs(query(collection(db, "stats"), where("session", "==", id)));
query(
collection(db, "stats"),
where("session", "==", id),
where("user", "==", req.session.user.id)
)
);
if (docsSnap.empty) { if (docsSnap.empty) {
res.status(400).end(); res.status(400).end();
return; return;
} }
const stats = docsSnap.docs.map((d) => d.data()); const stats = docsSnap.docs.map((d) => d.data());
// verify if the stats already have a pdf generated // verify if the stats already have a pdf generated
const hasPDF = stats.find((s) => s.pdf); const hasPDF = stats.find((s) => s.pdf);
if (hasPDF) { if (hasPDF) {
// if it does, return the pdf url // if it does, return the pdf url
const fileRef = ref(storage, hasPDF.pdf); const fileRef = ref(storage, hasPDF.pdf);
const url = await getDownloadURL(fileRef); const url = await getDownloadURL(fileRef);
res.status(200).end(url); res.status(200).end(url);
return; return;
} }
try { try {
// generate the pdf report // generate the pdf report
const docUser = await getDoc(doc(db, "users", req.session.user.id)); const docUser = await getDoc(doc(db, "users", req.session.user.id));
if (docUser.exists()) { if (docUser.exists()) {
// we'll need the user in order to get the user data (name, email, focus, etc); // we'll need the user in order to get the user data (name, email, focus, etc);
const user = docUser.data() as User; const user = docUser.data() as User;
// generate the QR code for the report // generate the QR code for the report
const qrcode = await generateQRCode( const qrcode = await generateQRCode((req.headers.origin || "") + req.url);
(req.headers.origin || "") + req.url
);
if (!qrcode) { if (!qrcode) {
res.status(500).json({ ok: false }); res.status(500).json({ok: false});
return; return;
} }
// stats may contain multiple exams of the same type so we need to aggregate them // stats may contain multiple exams of the same type so we need to aggregate them
const results = ( const results = (
stats.reduce((accm: ModuleScore[], { module, score }) => { stats.reduce((accm: ModuleScore[], {module, score}) => {
const fixedModuleStr = const fixedModuleStr = module[0].toUpperCase() + module.substring(1);
module[0].toUpperCase() + module.substring(1); if (accm.find((e: ModuleScore) => e.module === fixedModuleStr)) {
if (accm.find((e: ModuleScore) => e.module === fixedModuleStr)) { return accm.map((e: ModuleScore) => {
return accm.map((e: ModuleScore) => { if (e.module === fixedModuleStr) {
if (e.module === fixedModuleStr) { return {
return { ...e,
...e, score: e.score + score.correct,
score: e.score + score.correct, total: e.total + score.total,
total: e.total + score.total, };
}; }
}
return e; return e;
}); });
} }
return [ return [
...accm, ...accm,
{ {
module: fixedModuleStr, module: fixedModuleStr,
score: score.correct, score: score.correct,
total: score.total, total: score.total,
code: module, code: module,
}, },
]; ];
}, []) as ModuleScore[] }, []) as ModuleScore[]
).map((moduleScore) => { ).map((moduleScore) => {
const { score, total } = moduleScore; const {score, total} = moduleScore;
// with all the scores aggreated we can calculate the band score for each module // with all the scores aggreated we can calculate the band score for each module
const bandScore = calculateBandScore( const bandScore = calculateBandScore(score, total, moduleScore.code as Module, user.focus);
score,
total,
moduleScore.code as Module,
user.focus
);
return { return {
...moduleScore, ...moduleScore,
// generate the closest radial progress png for the score // generate the closest radial progress png for the score
png: getRadialProgressPNG("azul", score, total), png: getRadialProgressPNG("azul", score, total),
bandScore, bandScore,
}; };
}); });
// get the skills feedback from the backend based on the module grade // get the skills feedback from the backend based on the module grade
const skillsFeedback = (await handleSkillsFeedbackRequest( const skillsFeedback = (await handleSkillsFeedbackRequest(
results.map(({ code, bandScore }) => ({ results.map(({code, bandScore}) => ({
code, code,
name: moduleLabels[code], name: moduleLabels[code],
grade: bandScore, grade: bandScore,
})) })),
)) as SkillsFeedbackResponse[]; )) as SkillsFeedbackResponse[];
if (!skillsFeedback) { if (!skillsFeedback) {
res.status(500).json({ ok: false }); res.status(500).json({ok: false});
return; return;
} }
// assign the feedback to the results // assign the feedback to the results
const finalResults = results.map((result) => { const finalResults = results.map((result) => {
const feedback = skillsFeedback.find( const feedback = skillsFeedback.find((f: SkillsFeedbackResponse) => f.code === result.code);
(f: SkillsFeedbackResponse) => f.code === result.code
);
if (feedback) { if (feedback) {
return { return {
...result, ...result,
evaluation: feedback?.evaluation, evaluation: feedback?.evaluation,
suggestions: feedback?.suggestions, suggestions: feedback?.suggestions,
}; };
} }
return result; return result;
}); });
// calculate the overall score out of all the aggregated results // calculate the overall score out of all the aggregated results
const overallScore = results.reduce( const overallScore = results.reduce((accm, {score}) => accm + score, 0);
(accm, { score }) => accm + score, const overallTotal = results.reduce((accm, {total}) => accm + total, 0);
0 const overallResult = overallScore / overallTotal;
);
const overallTotal = results.reduce(
(accm, { total }) => accm + total,
0
);
const overallResult = overallScore / overallTotal;
const overallPNG = getRadialProgressPNG("laranja", overallScore, overallTotal); const overallPNG = getRadialProgressPNG("laranja", overallScore, overallTotal);
// generate the overall detail report // generate the overall detail report
const overallDetail = { const overallDetail = {
module: "Overall", module: "Overall",
score: overallScore, score: overallScore,
total: overallTotal, total: overallTotal,
png: overallPNG, png: overallPNG,
} as ModuleScore; } as ModuleScore;
const testDetails = [overallDetail, ...finalResults]; const testDetails = [overallDetail, ...finalResults];
const [stat] = stats; const [stat] = stats;
// generate the performance summary based on the overall result // generate the performance summary based on the overall result
const performanceSummary = getPerformanceSummary( const performanceSummary = getPerformanceSummary(stat.module, overallResult);
stat.module,
overallResult
);
// level exams have a different report structure than the skill exams // level exams have a different report structure than the skill exams
const getCustomData = () => { const getCustomData = () => {
if (stat.module === "level") { if (stat.module === "level") {
return { return {
title: "ENGLISH LEVEL TEST RESULT REPORT ", title: "ENGLISH LEVEL TEST RESULT REPORT ",
details: ( details: <LevelExamDetails detail={overallDetail} title="Level as per CEFR Levels" />,
<LevelExamDetails };
detail={overallDetail} }
title="Level as per CEFR Levels"
/>
),
};
}
return { return {
title: "ENGLISH SKILLS TEST RESULT REPORT", title: "ENGLISH SKILLS TEST RESULT REPORT",
details: <SkillExamDetails testDetails={testDetails} />, details: <SkillExamDetails testDetails={testDetails} />,
}; };
}; };
const { title, details } = getCustomData(); const {title, details} = getCustomData();
const demographicInformation = user.demographicInformation as DemographicInformation; const demographicInformation = user.demographicInformation as DemographicInformation;
const pdfStream = await ReactPDF.renderToStream( const pdfStream = await ReactPDF.renderToStream(
<TestReport <TestReport
title={title} title={title}
date={moment(stat.date).tz(user.demographicInformation?.timezone || 'UTC').format('ll HH:mm:ss')} date={moment(stat.date)
name={user.name} .tz(user.demographicInformation?.timezone || "UTC")
email={user.email} .format("ll HH:mm:ss")}
id={user.id} name={user.name}
gender={demographicInformation?.gender} email={user.email}
summary={performanceSummary} id={user.id}
testDetails={testDetails} gender={demographicInformation?.gender}
renderDetails={details} summary={performanceSummary}
logo={"public/logo_title.png"} testDetails={testDetails}
qrcode={qrcode} renderDetails={details}
summaryPNG={overallPNG} logo={"public/logo_title.png"}
summaryScore={`${(overallResult * 100).toFixed(0)}%`} qrcode={qrcode}
passportId={demographicInformation?.passport_id || ""} summaryPNG={overallPNG}
/> summaryScore={`${(overallResult * 100).toFixed(0)}%`}
); passportId={demographicInformation?.passport_id || ""}
/>,
);
// generate the file ref for storage // generate the file ref for storage
const fileName = `${Date.now().toString()}.pdf`; const fileName = `${Date.now().toString()}.pdf`;
const refName = `exam_report/${fileName}`; const refName = `exam_report/${fileName}`;
const fileRef = ref(storage, refName); const fileRef = ref(storage, refName);
// upload the pdf to storage // upload the pdf to storage
const pdfBuffer = await streamToBuffer(pdfStream); const pdfBuffer = await streamToBuffer(pdfStream);
const snapshot = await uploadBytes(fileRef, pdfBuffer, { const snapshot = await uploadBytes(fileRef, pdfBuffer, {
contentType: "application/pdf", contentType: "application/pdf",
}); });
// update the stats entries with the pdf url to prevent duplication // update the stats entries with the pdf url to prevent duplication
docsSnap.docs.forEach(async (doc) => { docsSnap.docs.forEach(async (doc) => {
await updateDoc(doc.ref, { await updateDoc(doc.ref, {
pdf: refName, pdf: refName,
}); });
}); });
const url = await getDownloadURL(fileRef); const url = await getDownloadURL(fileRef);
res.status(200).end(url); res.status(200).end(url);
return; return;
} }
res.status(401).json({ ok: false }); res.status(401).json({ok: false});
return; return;
} catch (err) { } catch (err) {
res.status(500).json({ ok: false }); res.status(500).json({ok: false});
return; return;
} }
} }
res.status(401).json({ ok: false }); res.status(401).json({ok: false});
return; return;
} }
async function get(req: NextApiRequest, res: NextApiResponse) { async function get(req: NextApiRequest, res: NextApiResponse) {
const { id } = req.query as { id: string }; const {id} = req.query as {id: string};
const docsSnap = await getDocs( const docsSnap = await getDocs(query(collection(db, "stats"), where("session", "==", id)));
query(collection(db, "stats"), where("session", "==", id))
);
if (docsSnap.empty) { if (docsSnap.empty) {
res.status(404).end(); res.status(404).end();
return; return;
} }
const stats = docsSnap.docs.map((d) => d.data()); const stats = docsSnap.docs.map((d) => d.data());
const hasPDF = stats.find((s) => s.pdf); const hasPDF = stats.find((s) => s.pdf);
if (hasPDF) { if (hasPDF) {
const fileRef = ref(storage, hasPDF.pdf); const fileRef = ref(storage, hasPDF.pdf);
const url = await getDownloadURL(fileRef); const url = await getDownloadURL(fileRef);
return res.redirect(url); return res.redirect(url);
} }
res.status(500).end(); res.status(500).end();
} }