From 752881df4128fc4b743397ef223f84067fbba81f Mon Sep 17 00:00:00 2001 From: Joao Correia Date: Thu, 6 Feb 2025 13:16:32 +0000 Subject: [PATCH] - Fix bug where workflows were being created again after exam update - Moved createWorkflows function into an helper file instead of a post request. - Moved the workflow creation logic into the post of exam creation instead of a seperate post in each exam module --- .../ExamEditor/SettingsEditor/level.tsx | 37 ++---------- .../SettingsEditor/listening/index.tsx | 37 ++---------- .../SettingsEditor/reading/index.tsx | 37 +++--------- .../SettingsEditor/speaking/index.tsx | 37 ++---------- .../SettingsEditor/writing/index.tsx | 37 ++---------- src/lib/createWorkflowsOnExamCreation.ts | 34 +++++++++++ src/pages/api/approval-workflows/index.ts | 59 +------------------ src/pages/api/exam/[module]/index.ts | 56 ++++++++++++++---- 8 files changed, 112 insertions(+), 222 deletions(-) create mode 100644 src/lib/createWorkflowsOnExamCreation.ts diff --git a/src/components/ExamEditor/SettingsEditor/level.tsx b/src/components/ExamEditor/SettingsEditor/level.tsx index 376113d3..d192c629 100644 --- a/src/components/ExamEditor/SettingsEditor/level.tsx +++ b/src/components/ExamEditor/SettingsEditor/level.tsx @@ -205,7 +205,12 @@ const LevelSettings: React.FC = () => { const result = await axios.post('/api/exam/level', exam); playSound("sent"); - toast.success(`Submitted Exam ID: ${result.data.id}`); + // Successfully submitted exam + if (result.status === 200) { + toast.success(result.data.message); + } else if (result.status === 207) { + toast.warning(result.data.message); + } Array.from(audioMap.values()).forEach(url => { URL.revokeObjectURL(url); @@ -214,36 +219,6 @@ const LevelSettings: React.FC = () => { URL.revokeObjectURL(url); }); - const requestBody = await (async () => { - const handledExam = await getExamById("level", result.data.id); - return { - examAuthor: handledExam?.createdBy ?? "Unknown Author", - examEntities: handledExam?.entities ?? [], - examId: handledExam?.id ?? "Unknown ID", - examModule: "level" - }; - })(); - await axios - .post(`/api/approval-workflows`, requestBody) - .then((response) => { - if (response.status === 200) { - toast.success(`Approval Workflows for exam have been successfully created`); - } else if (response.status === 207) { - toast.warning( - `Approval Workflows were partially created. Exam author might not have a configured workflow for all its entities.` - ); - } - }) - .catch((reason) => { - if (reason.response?.status === 404) { - toast.error("No configured workflow found for examAuthor for any of its entities."); - } else { - toast.error( - "Something went wrong while creating approval workflow, please try again later." - ); - } - }); - } catch (error: any) { console.error('Error submitting exam:', error); toast.error( diff --git a/src/components/ExamEditor/SettingsEditor/listening/index.tsx b/src/components/ExamEditor/SettingsEditor/listening/index.tsx index 182f1d3f..0e1e733e 100644 --- a/src/components/ExamEditor/SettingsEditor/listening/index.tsx +++ b/src/components/ExamEditor/SettingsEditor/listening/index.tsx @@ -150,37 +150,12 @@ const ListeningSettings: React.FC = () => { const result = await axios.post('/api/exam/listening', exam); playSound("sent"); - toast.success(`Submitted Exam ID: ${result.data.id}`); - - const requestBody = await (async () => { - const handledExam = await getExamById("listening", result.data.id); - return { - examAuthor: handledExam?.createdBy ?? "Unknown Author", - examEntities: handledExam?.entities ?? [], - examId: handledExam?.id ?? "Unknown ID", - examModule: "listening" - }; - })(); - await axios - .post(`/api/approval-workflows`, requestBody) - .then((response) => { - if (response.status === 200) { - toast.success(`Approval Workflows for exam have been successfully created`); - } else if (response.status === 207) { - toast.warning( - `Approval Workflows were partially created. Exam author might not have a configured workflow for all its entities.` - ); - } - }) - .catch((reason) => { - if (reason.response?.status === 404) { - toast.error("No configured workflow found for examAuthor for any of its entities."); - } else { - toast.error( - "Something went wrong while creating approval workflow, please try again later." - ); - } - }); + // Successfully submitted exam + if (result.status === 200) { + toast.success(result.data.message); + } else if (result.status === 207) { + toast.warning(result.data.message); + } } else { toast.error('No audio sections found in the exam! Please either import them or generate them.'); diff --git a/src/components/ExamEditor/SettingsEditor/reading/index.tsx b/src/components/ExamEditor/SettingsEditor/reading/index.tsx index 4a16ff12..45b00931 100644 --- a/src/components/ExamEditor/SettingsEditor/reading/index.tsx +++ b/src/components/ExamEditor/SettingsEditor/reading/index.tsx @@ -89,38 +89,17 @@ const ReadingSettings: React.FC = () => { axios.post(`/api/exam/reading`, exam) .then((result) => { playSound("sent"); - toast.success(`Submitted Exam ID: ${result.data.id}`); - return getExamById("reading", result.data.id); - }) - .then((handledExam) => { - const requestBody = { - examAuthor: handledExam?.createdBy ?? "Unknown Author", - examEntities: handledExam?.entities ?? [], - examId: handledExam?.id ?? "Unknown ID", - examModule: "reading" - }; - - return axios.post(`/api/approval-workflows`, requestBody); - }) - .then((response) => { - if (response.status === 200) { - toast.success(`Approval Workflows for exam have been successfully created`); - } else if (response.status === 207) { - toast.warning( - `Approval Workflows were partially created. Exam author might not have a configured workflow for all its entities.` - ); + // Successfully submitted exam + if (result.status === 200) { + toast.success(result.data.message); + } else if (result.status === 207) { + toast.warning(result.data.message); } }) .catch((error) => { - if (error.response && error.response.status === 404) { - toast.error("No configured workflow found for examAuthor for any of its entities."); - } else { - toast.error( - error.response?.data?.error || - "Something went wrong, please try again later." - ); - } - }); + console.log(error); + toast.error(error.response.data.error || "Something went wrong while submitting, please try again later."); + }) } const preview = () => { diff --git a/src/components/ExamEditor/SettingsEditor/speaking/index.tsx b/src/components/ExamEditor/SettingsEditor/speaking/index.tsx index bdd86048..513b30b1 100644 --- a/src/components/ExamEditor/SettingsEditor/speaking/index.tsx +++ b/src/components/ExamEditor/SettingsEditor/speaking/index.tsx @@ -190,41 +190,16 @@ const SpeakingSettings: React.FC = () => { const result = await axios.post('/api/exam/speaking', exam); playSound("sent"); - toast.success(`Submitted Exam ID: ${result.data.id}`); + // Successfully submitted exam + if (result.status === 200) { + toast.success(result.data.message); + } else if (result.status === 207) { + toast.warning(result.data.message); + } Array.from(urlMap.values()).forEach(url => { URL.revokeObjectURL(url); }); - - const requestBody = await (async () => { - const handledExam = await getExamById("speaking", result.data.id); - return { - examAuthor: handledExam?.createdBy ?? "Unknown Author", - examEntities: handledExam?.entities ?? [], - examId: handledExam?.id ?? "Unknown ID", - examModule: "speaking" - }; - })(); - await axios - .post(`/api/approval-workflows`, requestBody) - .then((response) => { - if (response.status === 200) { - toast.success(`Approval Workflows for exam have been successfully created`); - } else if (response.status === 207) { - toast.warning( - `Approval Workflows were partially created. Exam author might not have a configured workflow for all its entities.` - ); - } - }) - .catch((reason) => { - if (reason.response?.status === 404) { - toast.error("No configured workflow found for examAuthor for any of its entities."); - } else { - toast.error( - "Something went wrong while creating approval workflow, please try again later." - ); - } - }); } catch (error: any) { toast.error( diff --git a/src/components/ExamEditor/SettingsEditor/writing/index.tsx b/src/components/ExamEditor/SettingsEditor/writing/index.tsx index d0fd0bbf..911d1817 100644 --- a/src/components/ExamEditor/SettingsEditor/writing/index.tsx +++ b/src/components/ExamEditor/SettingsEditor/writing/index.tsx @@ -140,37 +140,12 @@ const WritingSettings: React.FC = () => { const result = await axios.post(`/api/exam/writing`, exam) playSound("sent"); - toast.success(`Submitted Exam ID: ${result.data.id}`); - - const requestBody = await (async () => { - const handledExam = await getExamById("writing", result.data.id); - return { - examAuthor: handledExam?.createdBy ?? "Unknown Author", - examEntities: handledExam?.entities ?? [], - examId: handledExam?.id ?? "Unknown ID", - examModule: "writing" - }; - })(); - await axios - .post(`/api/approval-workflows`, requestBody) - .then((response) => { - if (response.status === 200) { - toast.success(`Approval Workflows for exam have been successfully created`); - } else if (response.status === 207) { - toast.warning( - `Approval Workflows were partially created. Exam author might not have a configured workflow for all its entities.` - ); - } - }) - .catch((reason) => { - if (reason.response?.status === 404) { - toast.error("No configured workflow found for examAuthor for any of its entities."); - } else { - toast.error( - "Something went wrong while creating approval workflow, please try again later." - ); - } - }); + // Successfully submitted exam + if (result.status === 200) { + toast.success(result.data.message); + } else if (result.status === 207) { + toast.warning(result.data.message); + } } catch (error: any) { console.error('Error submitting exam:', error); diff --git a/src/lib/createWorkflowsOnExamCreation.ts b/src/lib/createWorkflowsOnExamCreation.ts new file mode 100644 index 00000000..732a1f18 --- /dev/null +++ b/src/lib/createWorkflowsOnExamCreation.ts @@ -0,0 +1,34 @@ +import { Module } from "@/interfaces"; +import { getApprovalWorkflowByFormIntaker, createApprovalWorkflow } from "@/utils/approval.workflows.be"; + +export async function createApprovalWorkflowsOnExamCreation(examAuthor: string, examEntities: string[], examId: string, examModule: string) { + const results = await Promise.all( + examEntities.map(async (entity) => { + const configuredWorkflow = await getApprovalWorkflowByFormIntaker(entity, examAuthor); + if (!configuredWorkflow) { + return { entity, created: false }; + } + + configuredWorkflow.modules.push(examModule as Module); + configuredWorkflow.name = examId; + configuredWorkflow.examId = examId; + configuredWorkflow.entityId = entity; + configuredWorkflow.startDate = Date.now(); + + try { + await createApprovalWorkflow("active-workflows", configuredWorkflow); + return { entity, created: true }; + } catch (error: any) { + return { entity, created: false }; + } + }) + ); + + const successCount = results.filter((r) => r.created).length; + const totalCount = examEntities.length; + + return { + successCount, + totalCount, + }; +} diff --git a/src/pages/api/approval-workflows/index.ts b/src/pages/api/approval-workflows/index.ts index 26636186..bf859471 100644 --- a/src/pages/api/approval-workflows/index.ts +++ b/src/pages/api/approval-workflows/index.ts @@ -1,24 +1,14 @@ // Next.js API route support: https://nextjs.org/docs/api-routes/introduction -import { Module } from "@/interfaces"; import { sessionOptions } from "@/lib/session"; import { requestUser } from "@/utils/api"; -import { createApprovalWorkflow, getApprovalWorkflowByFormIntaker, getApprovalWorkflows } from "@/utils/approval.workflows.be"; +import { getApprovalWorkflows } from "@/utils/approval.workflows.be"; import { withIronSessionApiRoute } from "iron-session/next"; import type { NextApiRequest, NextApiResponse } from "next"; export default withIronSessionApiRoute(handler, sessionOptions); -interface PostRequestBody { - examAuthor: string; - examEntities: string[]; - examId: string; - examName: string; - examModule: Module; -} - async function handler(req: NextApiRequest, res: NextApiResponse) { if (req.method === "GET") return await get(req, res); - if (req.method === "POST") return await post(req, res); } async function get(req: NextApiRequest, res: NextApiResponse) { @@ -30,49 +20,4 @@ async function get(req: NextApiRequest, res: NextApiResponse) { } return res.status(200).json(await getApprovalWorkflows("active-workflows")); -} - -async function post(req: NextApiRequest, res: NextApiResponse) { - const user = await requestUser(req, res); - if (!user) return res.status(401).json({ ok: false }); - - if (!["admin", "developer", "corporate", "mastercorporate"].includes(user.type)) { - return res.status(403).json({ ok: false }); - } - - const { examAuthor, examEntities, examId, examModule } = req.body as PostRequestBody; - - const results = await Promise.all( - examEntities.map(async (entity) => { - const configuredWorkflow = await getApprovalWorkflowByFormIntaker(entity, examAuthor); - if (!configuredWorkflow) { - return { entity, created: false, error: "No configured workflow found for examAuthor." }; - } - - configuredWorkflow.modules.push(examModule); - configuredWorkflow.name = `${examId}`; - configuredWorkflow.examId = examId; - configuredWorkflow.entityId = entity; - configuredWorkflow.startDate = Date.now(); - - try { - const creationResponse = await createApprovalWorkflow("active-workflows", configuredWorkflow); - return { entity, created: true, creationResponse }; - } catch (error) { - const err = error as Error; - return { entity, created: false, error: err.message }; - } - }) - ); - - const successCount = results.filter((r) => r.created).length; - const totalCount = examEntities.length; - - if (successCount === totalCount) { - return res.status(200).json({ ok: true, results }); - } else if (successCount > 0) { - return res.status(207).json({ ok: true, results }); - } else { - return res.status(404).json({ ok: false, message: "No workflows were created", results }); - } -} +} \ No newline at end of file diff --git a/src/pages/api/exam/[module]/index.ts b/src/pages/api/exam/[module]/index.ts index 86e2e7d6..8c114e14 100644 --- a/src/pages/api/exam/[module]/index.ts +++ b/src/pages/api/exam/[module]/index.ts @@ -1,15 +1,15 @@ // Next.js API route support: https://nextjs.org/docs/api-routes/introduction -import type { NextApiRequest, NextApiResponse } from "next"; -import client from "@/lib/mongodb"; -import { withIronSessionApiRoute } from "iron-session/next"; -import { sessionOptions } from "@/lib/session"; -import { Exam, ExamBase, InstructorGender, Variant } from "@/interfaces/exam"; -import { getExams } from "@/utils/exams.be"; import { Module } from "@/interfaces"; -import { getUserCorporate } from "@/utils/groups.be"; -import { requestUser } from "@/utils/api"; -import { isAdmin } from "@/utils/users"; +import { Exam, ExamBase, InstructorGender, Variant } from "@/interfaces/exam"; +import { createApprovalWorkflowsOnExamCreation } from "@/lib/createWorkflowsOnExamCreation"; +import client from "@/lib/mongodb"; +import { sessionOptions } from "@/lib/session"; import { mapBy } from "@/utils"; +import { requestUser } from "@/utils/api"; +import { getExams } from "@/utils/exams.be"; +import { isAdmin } from "@/utils/users"; +import { withIronSessionApiRoute } from "iron-session/next"; +import type { NextApiRequest, NextApiResponse } from "next"; const db = client.db(process.env.MONGODB_DB); @@ -46,7 +46,7 @@ async function POST(req: NextApiRequest, res: NextApiResponse) { const { module } = req.query as { module: string }; const session = client.startSession(); - const entities = isAdmin(user) ? [] : mapBy(user.entities, "id"); + const entities = isAdmin(user) ? [] : mapBy(user.entities, "id"); // might need to change this with new approval workflows logic.. if an admin creates an exam no workflow is started because workflows must have entities configured. try { const exam = { @@ -57,6 +57,9 @@ async function POST(req: NextApiRequest, res: NextApiResponse) { createdAt: new Date().toISOString(), }; + let responseStatus: number; + let responseMessage: string; + await session.withTransaction(async () => { const docSnap = await db.collection(module).findOne({ id: req.body.id }, { session }); @@ -79,9 +82,38 @@ async function POST(req: NextApiRequest, res: NextApiResponse) { session, } ); - }); - res.status(200).json(exam); + // if it doesn't enter the next if condition it means the exam was updated and not created, so we can send this response. + responseStatus = 200; + responseMessage = `Successfully updated exam with ID: "${exam.id}"`; + // TODO maybe find a way to start missing approval workflows in case they were only configured after exam creation. + + // create workflow only if exam is being created for the first time + if (docSnap === null) { + try { + const { successCount, totalCount } = await createApprovalWorkflowsOnExamCreation(exam.createdBy, exam.entities, exam.id, module); + + if (successCount === totalCount) { + responseStatus = 200; + responseMessage = `Successfully created exam "${exam.id}" and started its Approval Workflow(s)`; + } else if (successCount > 0) { + responseStatus = 207; + responseMessage = `Successfully created exam with ID: "${exam.id}" but was not able to start/find an Approval Workflow for all the author's entities`; + } else { + responseStatus = 207; + responseMessage = `Successfully created exam with ID: "${exam.id}" but was not able to find any configured Approval Workflow for the author.`; + } + } catch (error) { + console.error("Workflow creation error:", error); + responseStatus = 207; + responseMessage = `Successfully created exam with ID: "${exam.id}" but something went wrong while creating the Approval Workflow(s).`; + } + } + + res.status(responseStatus).json({ + message: responseMessage, + }); + }); } catch (error) { console.error("Transaction failed: ", error); res.status(500).json({ ok: false, error: (error as any).message });