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);
@@ -97,21 +84,19 @@ interface SkillsFeedbackResponse extends SkillsFeedbackRequest {
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[]
): Promise<SkillsFeedbackResponse[] | null> => {
let i = 0; let i = 0;
try { try {
const data = await getSkillsFeedback(sections); const data = await getSkillsFeedback(sections);
@@ -129,15 +114,9 @@ const handleSkillsFeedbackRequest = async (
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();
@@ -166,20 +145,17 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
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) {
@@ -205,14 +181,9 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
]; ];
}, []) 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,
@@ -224,23 +195,21 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
// 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 {
@@ -254,14 +223,8 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
}); });
// 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 overallTotal = results.reduce(
(accm, { total }) => accm + total,
0
);
const overallResult = overallScore / overallTotal; const overallResult = overallScore / overallTotal;
const overallPNG = getRadialProgressPNG("laranja", overallScore, overallTotal); const overallPNG = getRadialProgressPNG("laranja", overallScore, overallTotal);
@@ -278,22 +241,14 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
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"
/>
),
}; };
} }
@@ -303,13 +258,15 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
}; };
}; };
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)
.tz(user.demographicInformation?.timezone || "UTC")
.format("ll HH:mm:ss")}
name={user.name} name={user.name}
email={user.email} email={user.email}
id={user.id} id={user.id}
@@ -322,7 +279,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
summaryPNG={overallPNG} summaryPNG={overallPNG}
summaryScore={`${(overallResult * 100).toFixed(0)}%`} summaryScore={`${(overallResult * 100).toFixed(0)}%`}
passportId={demographicInformation?.passport_id || ""} passportId={demographicInformation?.passport_id || ""}
/> />,
); );
// generate the file ref for storage // generate the file ref for storage
@@ -347,23 +304,21 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
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();