From aa4e13a18da64451358b129e6439afb87e3d0fb5 Mon Sep 17 00:00:00 2001 From: Joao Ramos Date: Tue, 27 Aug 2024 23:11:40 +0100 Subject: [PATCH 1/9] Fixed an use case where the export of Excel failed if no students had answered it --- src/pages/api/assignments/[id]/[export]/excel.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/pages/api/assignments/[id]/[export]/excel.ts b/src/pages/api/assignments/[id]/[export]/excel.ts index bf19569e..d4fa7f05 100644 --- a/src/pages/api/assignments/[id]/[export]/excel.ts +++ b/src/pages/api/assignments/[id]/[export]/excel.ts @@ -195,12 +195,16 @@ function commonExcel({ // K10:M12 = 10,11,12,13 // horizontally group Test Sections - worksheet.mergeCells( - startIndexTable, - staticHeaders.length + 1, - startIndexTable, - tableColumnHeadersFirstPart.length - ); + + // if there are test section headers to even merge: + if(testSectionHeaders.length > 1) { + worksheet.mergeCells( + startIndexTable, + staticHeaders.length + 1, + startIndexTable, + tableColumnHeadersFirstPart.length + ); + } // Add the dynamic second and third header rows for test sections and sub-columns worksheet.addRow([ From 614a7a2a294b4c733c8630766cbbb91339c6bcdb Mon Sep 17 00:00:00 2001 From: Joao Ramos Date: Wed, 28 Aug 2024 07:38:38 +0100 Subject: [PATCH 2/9] Improved asset download criteria --- src/dashboards/AssignmentCard.tsx | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/src/dashboards/AssignmentCard.tsx b/src/dashboards/AssignmentCard.tsx index 6466a335..d3243656 100644 --- a/src/dashboards/AssignmentCard.tsx +++ b/src/dashboards/AssignmentCard.tsx @@ -62,6 +62,30 @@ export default function AssignmentCard({ return resultModuleBandScores.length === 0 ? -1 : resultModuleBandScores.reduce((acc, curr) => acc + curr, 0) / results.length; }; + const uniqModules = uniqBy(exams, (x) => x.module); + + const shouldRenderPDF = () => { + if(released && allowDownload) { + // in order to be downloadable, the assignment has to be released + // the component should have the allowDownload prop + // and the assignment should not have the level module + return uniqModules.every(({ module }) => module !== 'level'); + } + + return false; + } + + const shouldRenderExcel = () => { + if(released && allowExcelDownload) { + // in order to be downloadable, the assignment has to be released + // the component should have the allowExcelDownload prop + // and the assignment should have the level module + return uniqModules.some(({ module }) => module === 'level'); + } + + return false; + } + return (

{name}

- {allowDownload && released && renderPdfIcon(id, "text-mti-gray-dim", "text-mti-gray-dim")} - {allowExcelDownload && released && renderExcelIcon(id, "text-mti-gray-dim", "text-mti-gray-dim")} + {shouldRenderPDF() && renderPdfIcon(id, "text-mti-gray-dim", "text-mti-gray-dim")} + {shouldRenderExcel() && renderExcelIcon(id, "text-mti-gray-dim", "text-mti-gray-dim")} {allowArchive && !archived && renderArchiveIcon("text-mti-gray-dim", "text-mti-gray-dim")} {allowUnarchive && archived && renderUnarchiveIcon("text-mti-gray-dim", "text-mti-gray-dim")} {!released && renderReleaseIcon("text-mti-gray-dim", "text-mti-gray-dim")} @@ -94,7 +118,7 @@ export default function AssignmentCard({ Assigner: {getUserName(users.find((x) => x.id === assigner))}
- {uniqBy(exams, (x) => x.module).map(({module}) => ( + {uniqModules.map(({module}) => (
Date: Wed, 28 Aug 2024 07:39:51 +0100 Subject: [PATCH 3/9] Added default value in case on the user not being found in the DB --- .../api/assignments/[id]/[export]/excel.ts | 52 ++++++++++++++----- 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/src/pages/api/assignments/[id]/[export]/excel.ts b/src/pages/api/assignments/[id]/[export]/excel.ts index d4fa7f05..2502f03c 100644 --- a/src/pages/api/assignments/[id]/[export]/excel.ts +++ b/src/pages/api/assignments/[id]/[export]/excel.ts @@ -84,9 +84,16 @@ function commonExcel({ .map((assignee: string) => { const userStats = allStats.filter((s: any) => s.user === assignee); const dates = userStats.map((s: any) => moment(s.date)); + const user = users.find((u) => u.id === assignee); return { userId: assignee, - user: users.find((u) => u.id === assignee), + // added some default values in case the user is not found + // could it be possible to have an assigned user deleted from the database? + user: user || { + name: "Unknown", + email: "Unknown", + demographicInformation: { passportId: "Unknown", gender: "Unknown" }, + }, ...userStats.reduce( (acc: any, curr: any) => { return { @@ -152,7 +159,7 @@ function commonExcel({ }); // added empty arrays to force row spacings - const customTableAndLine = [[],...customTable, []]; + const customTableAndLine = [[], ...customTable, []]; customTableAndLine.forEach((row: string[], index) => { worksheet.addRow(row); }); @@ -188,7 +195,8 @@ function commonExcel({ worksheet.addRow(tableColumnHeaders); // 1 headers rows - const startIndexTable = firstSectionData.length + customTableAndLine.length + 1; + const startIndexTable = + firstSectionData.length + customTableAndLine.length + 1; // // Merge "Test Sections" over dynamic number of columns // const tableColumns = staticHeaders.length + numberOfTestSections; @@ -197,7 +205,7 @@ function commonExcel({ // horizontally group Test Sections // if there are test section headers to even merge: - if(testSectionHeaders.length > 1) { + if (testSectionHeaders.length > 1) { worksheet.mergeCells( startIndexTable, staticHeaders.length + 1, @@ -233,7 +241,12 @@ function commonExcel({ // vertically group based on the part, exercise and type staticHeaders.forEach((header, index) => { - worksheet.mergeCells(startIndexTable, index + 1, startIndexTable + 3, index + 1); + worksheet.mergeCells( + startIndexTable, + index + 1, + startIndexTable + 3, + index + 1 + ); }); assigneesData.forEach((data, index) => { @@ -320,13 +333,17 @@ async function mastercorporateAssignment( const adminsData = await getSpecificUsers(adminUsers); const companiesData = adminsData.map((user) => { const name = getUserName(user); - const users = userGroupsParticipants - .filter((p) => data.assignees.includes(p)); + const users = userGroupsParticipants.filter((p) => + data.assignees.includes(p) + ); const stats = data.results .flatMap((r: any) => r.stats) .filter((s: any) => users.includes(s.user)); - const correct = stats.reduce((acc: number, s: any) => acc + s.score.correct, 0); + const correct = stats.reduce( + (acc: number, s: any) => acc + s.score.correct, + 0 + ); const total = stats.reduce( (acc: number, curr: any) => acc + curr.score.total, 0 @@ -346,9 +363,11 @@ async function mastercorporateAssignment( correct: companiesData.reduce((acc, curr) => acc + curr.correct, 0), total: companiesData.reduce((acc, curr) => acc + curr.total, 0), }, - ].map((c) => [c.name, `${c.correct}/${c.total}`]) + ].map((c) => [c.name, `${c.correct}/${c.total}`]); - const customTableHeaders = [{ name: "Corporate", helper: (data: any) => data.user.corporateName}]; + const customTableHeaders = [ + { name: "Corporate", helper: (data: any) => data.user.corporateName }, + ]; return commonExcel({ data, userName: user.corporateInformation?.companyInformation?.name || "", @@ -358,12 +377,13 @@ async function mastercorporateAssignment( return { ...u, corporateName: getUserName(admin), - } + }; }), sectionName: "Master Corporate Name :", - customTable: [['Corporate Summary'], ...customTable], + customTable: [["Corporate Summary"], ...customTable], customTableHeaders: customTableHeaders.map((h) => h.name), - renderCustomTableData: (data) => customTableHeaders.map((h) => h.helper(data)), + renderCustomTableData: (data) => + customTableHeaders.map((h) => h.helper(data)), }); } @@ -415,7 +435,11 @@ async function post(req: NextApiRequest, res: NextApiResponse) { case "corporate": return corporateAssignment(user as CorporateUser, data, users); case "mastercorporate": - return mastercorporateAssignment(user as MasterCorporateUser, data, users); + return mastercorporateAssignment( + user as MasterCorporateUser, + data, + users + ); default: throw new Error("Invalid user type"); } From 99039f8bf3c630e8f17ee29f8b47d1efa89b06d9 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Wed, 28 Aug 2024 09:57:00 +0100 Subject: [PATCH 4/9] Corrected a problem related to getting the corporate of a user --- src/pages/api/grading/index.ts | 2 +- src/utils/groups.be.ts | 23 +++++++++++++++ src/utils/groups.ts | 54 +++++++++++++++------------------- 3 files changed, 48 insertions(+), 31 deletions(-) diff --git a/src/pages/api/grading/index.ts b/src/pages/api/grading/index.ts index 70ccbd08..6beb7e24 100644 --- a/src/pages/api/grading/index.ts +++ b/src/pages/api/grading/index.ts @@ -10,7 +10,7 @@ import {v4} from "uuid"; import {checkAccess} from "@/utils/permissions"; import {CEFR_STEPS} from "@/resources/grading"; import {getCorporateUser} from "@/resources/user"; -import {getUserCorporate} from "@/utils/groups"; +import {getUserCorporate} from "@/utils/groups.be"; import {Grading} from "@/interfaces"; import {getGroupsForUser} from "@/utils/groups.be"; import {uniq} from "lodash"; diff --git a/src/utils/groups.be.ts b/src/utils/groups.be.ts index 4ece42aa..99929752 100644 --- a/src/utils/groups.be.ts +++ b/src/utils/groups.be.ts @@ -33,11 +33,34 @@ export const updateExpiryDateOnGroup = async (participantID: string, corporateID return; }; +export const getUserCorporate = async (id: string) => { + const user = await getUser(id); + if (user.type === "corporate" || user.type === "mastercorporate") return user; + + const groups = await getParticipantGroups(id); + const admins = await Promise.all(groups.map((x) => x.admin).map(getUser)); + const corporates = admins.filter((x) => x.type === "corporate"); + + if (corporates.length === 0) return undefined; + return corporates.shift() as CorporateUser; +}; + export const getGroups = async () => { const groupDocs = await getDocs(collection(db, "groups")); return groupDocs.docs.map((x) => ({...x.data(), id: x.id})) as Group[]; }; +export const getParticipantGroups = async (id: string) => { + const snapshot = await getDocs(query(collection(db, "groups"), where("participants", "array-contains", id))); + + const groups = snapshot.docs.map((doc) => ({ + id: doc.id, + ...doc.data(), + })) as Group[]; + + return groups; +}; + export const getUserGroups = async (id: string): Promise => { const groupDocs = await getDocs(query(collection(db, "groups"), where("admin", "==", id))); return groupDocs.docs.map((x) => ({...x.data(), id})) as Group[]; diff --git a/src/utils/groups.ts b/src/utils/groups.ts index 1987a2d5..8416683b 100644 --- a/src/utils/groups.ts +++ b/src/utils/groups.ts @@ -1,43 +1,37 @@ -import { CorporateUser, Group, User, Type } from "@/interfaces/user"; +import {CorporateUser, Group, User, Type} from "@/interfaces/user"; import axios from "axios"; export const isUserFromCorporate = async (userID: string) => { - const groups = (await axios.get(`/api/groups?participant=${userID}`)) - .data; - const users = (await axios.get("/api/users/list")).data; + const groups = (await axios.get(`/api/groups?participant=${userID}`)).data; + const users = (await axios.get("/api/users/list")).data; - const adminTypes = groups.map( - (g) => users.find((u) => u.id === g.admin)?.type - ); - return adminTypes.includes("corporate"); + const adminTypes = groups.map((g) => users.find((u) => u.id === g.admin)?.type); + return adminTypes.includes("corporate"); }; const getAdminForGroup = async (userID: string, role: Type) => { - const groups = (await axios.get(`/api/groups?participant=${userID}`)) - .data; + const groups = (await axios.get(`/api/groups?participant=${userID}`)).data; - const adminRequests = await Promise.all( - groups.map(async (g) => { - const userRequest = await axios.get(`/api/users/${g.admin}`); - if (userRequest.status === 200) return userRequest.data; - return undefined; - }) - ); + const adminRequests = await Promise.all( + groups.map(async (g) => { + const userRequest = await axios.get(`/api/users/${g.admin}`); + if (userRequest.status === 200) return userRequest.data; + return undefined; + }), + ); - const admins = adminRequests.filter((x) => x?.type === role); - return admins.length > 0 ? (admins[0] as CorporateUser) : undefined; + const admins = adminRequests.filter((x) => x?.type === role); + return admins.length > 0 ? (admins[0] as CorporateUser) : undefined; }; -export const getUserCorporate = async ( - userID: string -): Promise => { - const userRequest = await axios.get(`/api/users/${userID}`); - if (userRequest.status === 200) { - const user = userRequest.data; - if (user.type === "corporate") { - return getAdminForGroup(userID, "mastercorporate"); - } - } +export const getUserCorporate = async (userID: string): Promise => { + const userRequest = await axios.get(`/api/users/${userID}`); + if (userRequest.status === 200) { + const user = userRequest.data; + if (user.type === "corporate") { + return getAdminForGroup(userID, "mastercorporate"); + } + } - return getAdminForGroup(userID, "corporate"); + return getAdminForGroup(userID, "corporate"); }; From 951ca5736e2fd8236f31a65a6dbe89214c61c022 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Wed, 28 Aug 2024 10:17:01 +0100 Subject: [PATCH 5/9] Added the ability for some exams to be private and not chosen randomly --- src/interfaces/exam.ts | 40 ++++++++++++++++++---------------------- src/utils/exams.be.ts | 4 ++-- 2 files changed, 20 insertions(+), 24 deletions(-) diff --git a/src/interfaces/exam.ts b/src/interfaces/exam.ts index fe919b0b..fa2a7384 100644 --- a/src/interfaces/exam.ts +++ b/src/interfaces/exam.ts @@ -1,4 +1,4 @@ -import { Module } from "."; +import {Module} from "."; export type Exam = ReadingExam | ListeningExam | WritingExam | SpeakingExam | LevelExam; export type Variant = "full" | "partial"; @@ -15,6 +15,7 @@ interface ExamBase { shuffle?: boolean; createdBy?: string; // option as it has been added later createdAt?: string; // option as it has been added later + private?: boolean; } export interface ReadingExam extends ExamBase { module: "reading"; @@ -67,7 +68,7 @@ export interface UserSolution { }; exercise: string; isDisabled?: boolean; - shuffleMaps?: ShuffleMap[] + shuffleMaps?: ShuffleMap[]; } export interface WritingExam extends ExamBase { @@ -99,24 +100,19 @@ export type Exercise = export interface Evaluation { comment: string; overall: number; - task_response: { [key: string]: number | { grade: number; comment: string } }; - misspelled_pairs?: { correction: string | null; misspelled: string }[]; + task_response: {[key: string]: number | {grade: number; comment: string}}; + misspelled_pairs?: {correction: string | null; misspelled: string}[]; } - type InteractivePerfectAnswerKey = `perfect_answer_${number}`; type InteractiveTranscriptKey = `transcript_${number}`; type InteractiveFixedTextKey = `fixed_text_${number}`; -type InteractivePerfectAnswerType = { [key in InteractivePerfectAnswerKey]: { answer: string } }; -type InteractiveTranscriptType = { [key in InteractiveTranscriptKey]?: string }; -type InteractiveFixedTextType = { [key in InteractiveFixedTextKey]?: string }; - -interface InteractiveSpeakingEvaluation extends Evaluation, - InteractivePerfectAnswerType, - InteractiveTranscriptType, - InteractiveFixedTextType { } +type InteractivePerfectAnswerType = {[key in InteractivePerfectAnswerKey]: {answer: string}}; +type InteractiveTranscriptType = {[key in InteractiveTranscriptKey]?: string}; +type InteractiveFixedTextType = {[key in InteractiveFixedTextKey]?: string}; +interface InteractiveSpeakingEvaluation extends Evaluation, InteractivePerfectAnswerType, InteractiveTranscriptType, InteractiveFixedTextType {} interface SpeakingEvaluation extends CommonEvaluation { perfect_answer_1?: string; @@ -189,10 +185,10 @@ export interface InteractiveSpeakingExercise { first_title?: string; second_title?: string; text: string; - prompts: { text: string; video_url: string }[]; + prompts: {text: string; video_url: string}[]; userSolutions: { id: string; - solution: { questionIndex: number; question: string; answer: string }[]; + solution: {questionIndex: number; question: string; answer: string}[]; evaluation?: InteractiveSpeakingEvaluation; }[]; topic?: string; @@ -208,14 +204,14 @@ export interface FillBlanksMCOption { B: string; C: string; D: string; - } + }; } export interface FillBlanksExercise { prompt: string; // *EXAMPLE: "Complete the summary below. Click a blank to select the corresponding word for it." type: "fillBlanks"; id: string; - words: (string | { letter: string; word: string } | FillBlanksMCOption)[]; // *EXAMPLE: ["preserve", "unaware"] + words: (string | {letter: string; word: string} | FillBlanksMCOption)[]; // *EXAMPLE: ["preserve", "unaware"] text: string; // *EXAMPLE: "They tried to {{1}} burning" allowRepetition?: boolean; solutions: { @@ -234,7 +230,7 @@ export interface TrueFalseExercise { id: string; prompt: string; // *EXAMPLE: "Select the appropriate option." questions: TrueFalseQuestion[]; - userSolutions: { id: string; solution: "true" | "false" | "not_given" }[]; + userSolutions: {id: string; solution: "true" | "false" | "not_given"}[]; } export interface TrueFalseQuestion { @@ -263,7 +259,7 @@ export interface MatchSentencesExercise { type: "matchSentences"; id: string; prompt: string; - userSolutions: { question: string; option: string }[]; + userSolutions: {question: string; option: string}[]; sentences: MatchSentenceExerciseSentence[]; allowRepetition: boolean; options: MatchSentenceExerciseOption[]; @@ -286,7 +282,7 @@ export interface MultipleChoiceExercise { id: string; prompt: string; // *EXAMPLE: "Select the appropriate option." questions: MultipleChoiceQuestion[]; - userSolutions: { question: string; option: string }[]; + userSolutions: {question: string; option: string}[]; } export interface MultipleChoiceQuestion { @@ -306,10 +302,10 @@ export interface ShuffleMap { questionID: string; map: { [key: string]: string; - } + }; } export interface Shuffles { exerciseID: string; - shuffles: ShuffleMap[] + shuffles: ShuffleMap[]; } diff --git a/src/utils/exams.be.ts b/src/utils/exams.be.ts index be8d1837..2466c61d 100644 --- a/src/utils/exams.be.ts +++ b/src/utils/exams.be.ts @@ -1,4 +1,4 @@ -import {collection, getDocs, query, where, setDoc, doc, Firestore, getDoc} from "firebase/firestore"; +import {collection, getDocs, query, where, setDoc, doc, Firestore, getDoc, and} from "firebase/firestore"; import {shuffle} from "lodash"; import {Difficulty, Exam, InstructorGender, SpeakingExam, Variant, WritingExam} from "@/interfaces/exam"; import {DeveloperUser, Stat, StudentUser, User} from "@/interfaces/user"; @@ -17,7 +17,7 @@ export const getExams = async ( ): Promise => { const moduleRef = collection(db, module); - const q = query(moduleRef, where("isDiagnostic", "==", false)); + const q = query(moduleRef, and(where("isDiagnostic", "==", false), where("private", "!=", true))); const snapshot = await getDocs(q); const allExams = shuffle( From dbf262598f4d6aa07271e44a4dc5d0f457615ad0 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Wed, 28 Aug 2024 10:26:45 +0100 Subject: [PATCH 6/9] Added the ability to set an exam as private --- src/pages/(admin)/Lists/ExamList.tsx | 38 +++++++++++++++++++++++++--- src/pages/api/exam/[module]/[id].ts | 22 +++++++++++++++- 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/src/pages/(admin)/Lists/ExamList.tsx b/src/pages/(admin)/Lists/ExamList.tsx index 93ffb05c..2c990677 100644 --- a/src/pages/(admin)/Lists/ExamList.tsx +++ b/src/pages/(admin)/Lists/ExamList.tsx @@ -13,7 +13,7 @@ import axios from "axios"; import clsx from "clsx"; import {capitalize} from "lodash"; import {useRouter} from "next/router"; -import {BsCheck, BsTrash, BsUpload} from "react-icons/bs"; +import {BsBan, BsBanFill, BsCheck, BsCircle, BsStop, BsTrash, BsUpload, BsX} from "react-icons/bs"; import {toast} from "react-toastify"; import {useListSearch} from "@/hooks/useListSearch"; @@ -72,6 +72,28 @@ export default function ExamList({user}: {user: User}) { router.push("/exercises"); }; + const privatizeExam = async (exam: Exam) => { + if (!confirm(`Are you sure you want to make this ${capitalize(exam.module)} exam ${exam.private ? "public" : "private"}?`)) return; + + axios + .patch(`/api/exam/${exam.module}/${exam.id}`, {private: !exam.private}) + .then(() => toast.success(`Updated the "${exam.id}" exam`)) + .catch((reason) => { + if (reason.response.status === 404) { + toast.error("Exam not found!"); + return; + } + + if (reason.response.status === 403) { + toast.error("You do not have permission to update this exam!"); + return; + } + + toast.error("Something went wrong, please try again later."); + }) + .finally(reload); + }; + const deleteExam = async (exam: Exam) => { if (!confirm(`Are you sure you want to delete this ${capitalize(exam.module)} exam?`)) return; @@ -119,6 +141,10 @@ export default function ExamList({user}: {user: User}) { header: "Timer", cell: (info) => <>{info.getValue()} minute(s), }), + columnHelper.accessor("private", { + header: "Private", + cell: (info) => {!info.getValue() ? : }, + }), columnHelper.accessor("createdAt", { header: "Created At", cell: (info) => { @@ -140,12 +166,18 @@ export default function ExamList({user}: {user: User}) { cell: ({row}: {row: {original: Exam}}) => { return (
-
await privatizeExam(row.original)} + className="cursor-pointer tooltip"> + {row.original.private ? : } + +
+ {PERMISSIONS.examManagement.delete.includes(user.type) && (
deleteExam(row.original)}> diff --git a/src/pages/api/exam/[module]/[id].ts b/src/pages/api/exam/[module]/[id].ts index fc80debf..50bf3c7d 100644 --- a/src/pages/api/exam/[module]/[id].ts +++ b/src/pages/api/exam/[module]/[id].ts @@ -1,7 +1,7 @@ // Next.js API route support: https://nextjs.org/docs/api-routes/introduction import type {NextApiRequest, NextApiResponse} from "next"; import {app} from "@/firebase"; -import {getFirestore, doc, getDoc, deleteDoc} from "firebase/firestore"; +import {getFirestore, doc, getDoc, deleteDoc, setDoc} from "firebase/firestore"; import {withIronSessionApiRoute} from "iron-session/next"; import {sessionOptions} from "@/lib/session"; import {PERMISSIONS} from "@/constants/userPermissions"; @@ -12,6 +12,7 @@ export default withIronSessionApiRoute(handler, sessionOptions); async function handler(req: NextApiRequest, res: NextApiResponse) { if (req.method === "GET") return get(req, res); + if (req.method === "PATCH") return patch(req, res); if (req.method === "DELETE") return del(req, res); } @@ -37,6 +38,25 @@ async function get(req: NextApiRequest, res: NextApiResponse) { } } +async function patch(req: NextApiRequest, res: NextApiResponse) { + if (!req.session.user) { + res.status(401).json({ok: false}); + return; + } + + const {module, id} = req.query as {module: string; id: string}; + + const docRef = doc(db, module, id); + const docSnap = await getDoc(docRef); + + if (docSnap.exists()) { + await setDoc(docRef, req.body, {merge: true}); + res.status(200).json({ok: true}); + } else { + res.status(404).json({ok: false}); + } +} + async function del(req: NextApiRequest, res: NextApiResponse) { if (!req.session.user) { res.status(401).json({ok: false}); From e518323d99157bdfc103ff0a36beabe277fe48e3 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Wed, 28 Aug 2024 10:46:02 +0100 Subject: [PATCH 7/9] Updated the generation to allow for private exams --- src/components/Low/Checkbox.tsx | 12 ++++++----- src/pages/(generation)/LevelGeneration.tsx | 21 +++++++++++++------ .../(generation)/ListeningGeneration.tsx | 19 ++++++++++++----- src/pages/(generation)/ReadingGeneration.tsx | 17 +++++++++++---- src/pages/(generation)/SpeakingGeneration.tsx | 20 +++++++++++++----- src/pages/(generation)/WritingGeneration.tsx | 18 ++++++++++++---- 6 files changed, 78 insertions(+), 29 deletions(-) diff --git a/src/components/Low/Checkbox.tsx b/src/components/Low/Checkbox.tsx index d5a7298d..fa90951f 100644 --- a/src/components/Low/Checkbox.tsx +++ b/src/components/Low/Checkbox.tsx @@ -11,14 +11,16 @@ interface Props { export default function Checkbox({isChecked, onChange, children, disabled}: Props) { return ( -
{ - if(disabled) return; - onChange(!isChecked); - }}> +
{ + if (disabled) return; + onChange(!isChecked); + }}>
diff --git a/src/pages/(generation)/LevelGeneration.tsx b/src/pages/(generation)/LevelGeneration.tsx index 09b1615a..480ca61a 100644 --- a/src/pages/(generation)/LevelGeneration.tsx +++ b/src/pages/(generation)/LevelGeneration.tsx @@ -1,6 +1,7 @@ import FillBlanksEdit from "@/components/Generation/fill.blanks.edit"; import MultipleChoiceEdit from "@/components/Generation/multiple.choice.edit"; import WriteBlankEdits from "@/components/Generation/write.blanks.edit"; +import Checkbox from "@/components/Low/Checkbox"; import Input from "@/components/Low/Input"; import Select from "@/components/Low/Select"; import { @@ -234,7 +235,7 @@ interface Props { id: string; } -const LevelGeneration = ({ id } : Props) => { +const LevelGeneration = ({id}: Props) => { const [generatedExam, setGeneratedExam] = useState(); const [isLoading, setIsLoading] = useState(false); const [resultingExam, setResultingExam] = useState(); @@ -242,6 +243,7 @@ const LevelGeneration = ({ id } : Props) => { const [difficulty, setDifficulty] = useState(sample(DIFFICULTIES)!); const [numberOfParts, setNumberOfParts] = useState(1); const [parts, setParts] = useState([{quantity: 10, type: "multiple_choice_4"}]); + const [isPrivate, setPrivate] = useState(false); useEffect(() => { setParts((prev) => Array.from(Array(numberOfParts)).map((_, i) => (!!prev.at(i) ? prev.at(i)! : {quantity: 10, type: "multiple_choice_4"}))); @@ -301,6 +303,7 @@ const LevelGeneration = ({ id } : Props) => { difficulty, variant: "full", isDiagnostic: false, + private: isPrivate, parts: parts .map((part, index) => { const currentExercise = result.data.exercises[`exercise_${index + 1}`] as any; @@ -424,7 +427,7 @@ const LevelGeneration = ({ id } : Props) => { return; } - if(!id) { + if (!id) { toast.error("Please insert a title before submitting"); return; } @@ -456,8 +459,8 @@ const LevelGeneration = ({ id } : Props) => { return ( <> -
-
+
+
setNumberOfParts(parseInt(v))} value={numberOfParts} />
-
+
setTimer(parseInt(v))} value={timer} />
+
+
+ + Privacy (Only available for Assignments) + +
diff --git a/src/pages/(generation)/ListeningGeneration.tsx b/src/pages/(generation)/ListeningGeneration.tsx index 5388b183..ef17c4ca 100644 --- a/src/pages/(generation)/ListeningGeneration.tsx +++ b/src/pages/(generation)/ListeningGeneration.tsx @@ -16,6 +16,7 @@ import {BsArrowRepeat, BsCheck} from "react-icons/bs"; import {toast} from "react-toastify"; import WriteBlanksEdit from "@/components/Generation/write.blanks.edit"; import {generate} from "random-words"; +import Checkbox from "@/components/Low/Checkbox"; const DIFFICULTIES: Difficulty[] = ["easy", "medium", "hard"]; @@ -232,7 +233,7 @@ interface Props { id: string; } -const ListeningGeneration = ({ id } : Props) => { +const ListeningGeneration = ({id}: Props) => { const [part1, setPart1] = useState(); const [part2, setPart2] = useState(); const [part3, setPart3] = useState(); @@ -241,6 +242,7 @@ const ListeningGeneration = ({ id } : Props) => { const [isLoading, setIsLoading] = useState(false); const [resultingExam, setResultingExam] = useState(); const [difficulty, setDifficulty] = useState(sample(DIFFICULTIES)!); + const [isPrivate, setPrivate] = useState(false); useEffect(() => { const part1Timer = part1 ? 5 : 0; @@ -262,11 +264,11 @@ const ListeningGeneration = ({ id } : Props) => { console.log({parts}); if (parts.length === 0) return toast.error("Please generate at least one section!"); - if(!id) { + if (!id) { toast.error("Please insert a title before submitting"); return; } - + setIsLoading(true); axios @@ -275,6 +277,7 @@ const ListeningGeneration = ({ id } : Props) => { parts, minTimer, difficulty, + private: isPrivate, }) .then((result) => { playSound("sent"); @@ -313,7 +316,7 @@ const ListeningGeneration = ({ id } : Props) => { return ( <> -
+
{ className="max-w-[300px]" />
-
+
{ className="max-w-[300px]" />
-
+
{ className="max-w-[300px]" />
-
+
{ className="max-w-[300px]" />
-
+