diff --git a/scripts/updatePrivateFieldExams.js b/scripts/updatePrivateFieldExams.js new file mode 100644 index 00000000..0fba9d8f --- /dev/null +++ b/scripts/updatePrivateFieldExams.js @@ -0,0 +1,51 @@ +import dotenv from "dotenv"; +dotenv.config(); +import { MongoClient } from "mongodb"; +const uri = process.env.MONGODB_URI || ""; +const options = { + maxPoolSize: 10, +}; +const dbName = process.env.MONGODB_DB; // change this to prod db when needed +async function migrateData() { + const MODULE_ARRAY = ["reading", "listening", "writing", "speaking", "level"]; + const client = new MongoClient(uri, options); + + try { + await client.connect(); + console.log("Connected to MongoDB"); + if (!process.env.MONGODB_DB) { + throw new Error("Missing env var: MONGODB_DB"); + } + const db = client.db(dbName); + for (const string of MODULE_ARRAY) { + const collection = db.collection(string); + const result = await collection.updateMany( + { private: { $exists: false } }, + { $set: { access: "public" } } + ); + const result2 = await collection.updateMany( + { private: true }, + { $set: { access: "private" }, $unset: { private: "" } } + ); + const result1 = await collection.updateMany( + { private: { $exists: true } }, + { $set: { access: "public" } } + ); + console.log( + `Updated ${ + result.modifiedCount + result1.modifiedCount + } documents to "access: public" in ${string}` + ); + console.log( + `Updated ${result2.modifiedCount} documents to "access: private" and removed private var in ${string}` + ); + } + console.log("Migration completed successfully!"); + } catch (error) { + console.error("Migration failed:", error); + } finally { + await client.close(); + console.log("MongoDB connection closed."); + } +} +//migrateData(); // uncomment to run the migration diff --git a/src/components/Diagnostic.tsx b/src/components/Diagnostic.tsx index 1ca52b73..af022262 100644 --- a/src/components/Diagnostic.tsx +++ b/src/components/Diagnostic.tsx @@ -1,17 +1,12 @@ -import {infoButtonStyle} from "@/constants/buttonStyles"; -import {Module} from "@/interfaces"; import {User} from "@/interfaces/user"; import useExamStore from "@/stores/exam"; -import {getExam, getExamById} from "@/utils/exams"; +import {getExam} from "@/utils/exams"; import {MODULE_ARRAY} from "@/utils/moduleUtils"; -import {writingMarking} from "@/utils/score"; -import {Menu} from "@headlessui/react"; import axios from "axios"; import clsx from "clsx"; -import {capitalize} from "lodash"; import {useRouter} from "next/router"; -import {useEffect, useState} from "react"; -import {BsBook, BsChevronDown, BsHeadphones, BsMegaphone, BsPen, BsQuestionSquare} from "react-icons/bs"; +import { useState} from "react"; +import { BsQuestionSquare} from "react-icons/bs"; import {toast} from "react-toastify"; import Button from "./Low/Button"; import ModuleLevelSelector from "./Medium/ModuleLevelSelector"; diff --git a/src/components/ExamEditor/SettingsEditor/index.tsx b/src/components/ExamEditor/SettingsEditor/index.tsx index faf6bcbc..fedbd0c7 100644 --- a/src/components/ExamEditor/SettingsEditor/index.tsx +++ b/src/components/ExamEditor/SettingsEditor/index.tsx @@ -19,7 +19,7 @@ interface SettingsEditorProps { children?: ReactNode; canPreview: boolean; canSubmit: boolean; - submitModule: () => void; + submitModule: (requiresApproval: boolean) => void; preview: () => void; } @@ -148,18 +148,33 @@ const SettingsEditor: React.FC = ({ {children} -
+
+
diff --git a/src/components/ExamEditor/SettingsEditor/level.tsx b/src/components/ExamEditor/SettingsEditor/level.tsx index d192c629..36dc8796 100644 --- a/src/components/ExamEditor/SettingsEditor/level.tsx +++ b/src/components/ExamEditor/SettingsEditor/level.tsx @@ -38,7 +38,7 @@ const LevelSettings: React.FC = () => { difficulty, sections, minTimer, - isPrivate, + access, } = useExamEditorStore(state => state.modules[currentModule]); const { localSettings, updateLocalAndScheduleGlobal } = useSettingsState( @@ -76,7 +76,7 @@ const LevelSettings: React.FC = () => { }); }); - const submitLevel = async () => { + const submitLevel = async (requiresApproval: boolean) => { if (title === "") { toast.error("Enter a title for the exam!"); return; @@ -195,12 +195,13 @@ const LevelSettings: React.FC = () => { category: s.settings.category }; }).filter(part => part.exercises.length > 0), - isDiagnostic: true, // using isDiagnostic to keep exam hidden until the respective approval workflow is completed. + requiresApproval: requiresApproval, + isDiagnostic: false, minTimer, module: "level", id: title, difficulty, - private: isPrivate, + access, }; const result = await axios.post('/api/exam/level', exam); @@ -243,7 +244,7 @@ const LevelSettings: React.FC = () => { isDiagnostic: false, variant: undefined, difficulty, - private: isPrivate, + access, } as LevelExam); setExerciseIndex(0); setQuestionIndex(0); diff --git a/src/components/ExamEditor/SettingsEditor/listening/index.tsx b/src/components/ExamEditor/SettingsEditor/listening/index.tsx index 0e1e733e..fa0ffc3e 100644 --- a/src/components/ExamEditor/SettingsEditor/listening/index.tsx +++ b/src/components/ExamEditor/SettingsEditor/listening/index.tsx @@ -27,7 +27,7 @@ const ListeningSettings: React.FC = () => { difficulty, sections, minTimer, - isPrivate, + access, instructionsState } = useExamEditorStore(state => state.modules[currentModule]); @@ -65,7 +65,7 @@ const ListeningSettings: React.FC = () => { } ]; - const submitListening = async () => { + const submitListening = async (requiresApproval: boolean) => { if (title === "") { toast.error("Enter a title for the exam!"); return; @@ -138,13 +138,14 @@ const ListeningSettings: React.FC = () => { category: s.settings.category }; }), - isDiagnostic: true, // using isDiagnostic to keep exam hidden until the respective approval workflow is completed. + requiresApproval: requiresApproval, + isDiagnostic: false, minTimer, module: "listening", id: title, variant: sections.length === 4 ? "full" : "partial", difficulty, - private: isPrivate, + access, instructions: instructionsURL }; @@ -191,7 +192,7 @@ const ListeningSettings: React.FC = () => { isDiagnostic: false, variant: sections.length === 4 ? "full" : "partial", difficulty, - private: isPrivate, + access, instructions: instructionsState.currentInstructionsURL } as ListeningExam); setExerciseIndex(0); diff --git a/src/components/ExamEditor/SettingsEditor/reading/index.tsx b/src/components/ExamEditor/SettingsEditor/reading/index.tsx index 45b00931..4c4f69b5 100644 --- a/src/components/ExamEditor/SettingsEditor/reading/index.tsx +++ b/src/components/ExamEditor/SettingsEditor/reading/index.tsx @@ -15,135 +15,138 @@ import ReadingComponents from "./components"; import { getExamById } from "@/utils/exams"; const ReadingSettings: React.FC = () => { - const router = useRouter(); + const router = useRouter(); - const { - setExam, - setExerciseIndex, - setPartIndex, - setQuestionIndex, - setBgColor, - } = usePersistentExamStore(); + const { + setExam, + setExerciseIndex, + setPartIndex, + setQuestionIndex, + setBgColor, + } = usePersistentExamStore(); - const { currentModule, title } = useExamEditorStore(); - const { - focusedSection, - difficulty, - sections, - minTimer, - isPrivate, - type, - } = useExamEditorStore(state => state.modules[currentModule]); + const { currentModule, title } = useExamEditorStore(); + const { focusedSection, difficulty, sections, minTimer, access, type } = + useExamEditorStore((state) => state.modules[currentModule]); - const { localSettings, updateLocalAndScheduleGlobal } = useSettingsState( - currentModule, - focusedSection - ); + const { localSettings, updateLocalAndScheduleGlobal } = + useSettingsState(currentModule, focusedSection); - const currentSection = sections.find((section) => section.sectionId == focusedSection)?.state as ReadingPart; + const currentSection = sections.find( + (section) => section.sectionId == focusedSection + )?.state as ReadingPart; + const defaultPresets: Option[] = [ + { + label: "Preset: Reading Passage 1", + value: + "Welcome to {part} of the {label}. You will read texts relating to everyday topics and situations. These may include advertisements, brochures, manuals, or official documents. Answer questions that test your ability to locate specific information and understand main ideas.", + }, + { + label: "Preset: Reading Passage 2", + value: + "Welcome to {part} of the {label}. You will read texts dealing with general interest topics that may include news articles, company policies, or workplace documents. Answer questions testing your understanding of main ideas, specific details, and the author's views.", + }, + { + label: "Preset: Reading Passage 3", + value: + "Welcome to {part} of the {label}. You will read longer academic texts that may include journal articles, academic essays, or research papers. Answer questions testing your ability to understand complex arguments, identify key points, and follow the development of ideas.", + }, + ]; - const defaultPresets: Option[] = [ - { - label: "Preset: Reading Passage 1", - value: "Welcome to {part} of the {label}. You will read texts relating to everyday topics and situations. These may include advertisements, brochures, manuals, or official documents. Answer questions that test your ability to locate specific information and understand main ideas." - }, - { - label: "Preset: Reading Passage 2", - value: "Welcome to {part} of the {label}. You will read texts dealing with general interest topics that may include news articles, company policies, or workplace documents. Answer questions testing your understanding of main ideas, specific details, and the author's views." - }, - { - label: "Preset: Reading Passage 3", - value: "Welcome to {part} of the {label}. You will read longer academic texts that may include journal articles, academic essays, or research papers. Answer questions testing your ability to understand complex arguments, identify key points, and follow the development of ideas." - } - ]; + const canPreviewOrSubmit = sections.some( + (s) => + (s.state as ReadingPart).exercises && + (s.state as ReadingPart).exercises.length > 0 + ); - const canPreviewOrSubmit = sections.some( - (s) => (s.state as ReadingPart).exercises && (s.state as ReadingPart).exercises.length > 0 - ); - - const submitReading = () => { - if (title === "") { - toast.error("Enter a title for the exam!"); - return; - } - const exam: ReadingExam = { - parts: sections.map((s) => { - const exercise = s.state as ReadingPart; - return { - ...exercise, - intro: localSettings.currentIntro, - category: localSettings.category - }; - }), - isDiagnostic: true, // using isDiagnostic to keep exam hidden until the respective approval workflow is completed. - minTimer, - module: "reading", - id: title, - variant: sections.length === 3 ? "full" : "partial", - difficulty, - private: isPrivate, - type: type! + const submitReading = (requiresApproval: boolean) => { + if (title === "") { + toast.error("Enter a title for the exam!"); + return; + } + const exam: ReadingExam = { + parts: sections.map((s) => { + const exercise = s.state as ReadingPart; + return { + ...exercise, + intro: localSettings.currentIntro, + category: localSettings.category, }; + }), + requiresApproval: requiresApproval, + isDiagnostic: false, + minTimer, + module: "reading", + id: title, + variant: sections.length === 3 ? "full" : "partial", + difficulty, + access, + type: type!, + }; - axios.post(`/api/exam/reading`, exam) - .then((result) => { - playSound("sent"); - // Successfully submitted exam - if (result.status === 200) { - toast.success(result.data.message); - } else if (result.status === 207) { - toast.warning(result.data.message); - } - }) - .catch((error) => { - console.log(error); - toast.error(error.response.data.error || "Something went wrong while submitting, please try again later."); - }) - } + axios + .post(`/api/exam/reading`, exam) + .then((result) => { + playSound("sent"); + // Successfully submitted exam + if (result.status === 200) { + toast.success(result.data.message); + } else if (result.status === 207) { + toast.warning(result.data.message); + } + }) + .catch((error) => { + console.log(error); + toast.error( + error.response.data.error || + "Something went wrong while submitting, please try again later." + ); + }); + }; - const preview = () => { - setExam({ - parts: sections.map((s) => { - const exercises = s.state as ReadingPart; - return { - ...exercises, - intro: s.settings.currentIntro, - category: s.settings.category - }; - }), - minTimer, - module: "reading", - id: title, - isDiagnostic: false, - variant: undefined, - difficulty, - private: isPrivate, - type: type! - } as ReadingExam); - setExerciseIndex(0); - setQuestionIndex(0); - setPartIndex(0); - setBgColor("bg-white"); - openDetachedTab("popout?type=Exam&module=reading", router) - } + const preview = () => { + setExam({ + parts: sections.map((s) => { + const exercises = s.state as ReadingPart; + return { + ...exercises, + intro: s.settings.currentIntro, + category: s.settings.category, + }; + }), + minTimer, + module: "reading", + id: title, + isDiagnostic: false, + variant: undefined, + difficulty, + access: access, + type: type!, + } as ReadingExam); + setExerciseIndex(0); + setQuestionIndex(0); + setPartIndex(0); + setBgColor("bg-white"); + openDetachedTab("popout?type=Exam&module=reading", router); + }; - return ( - - - - ); + return ( + + + + ); }; export default ReadingSettings; diff --git a/src/components/ExamEditor/SettingsEditor/speaking/index.tsx b/src/components/ExamEditor/SettingsEditor/speaking/index.tsx index 513b30b1..186b2c53 100644 --- a/src/components/ExamEditor/SettingsEditor/speaking/index.tsx +++ b/src/components/ExamEditor/SettingsEditor/speaking/index.tsx @@ -30,7 +30,7 @@ const SpeakingSettings: React.FC = () => { } = usePersistentExamStore(); const { title, currentModule } = useExamEditorStore(); - const { focusedSection, difficulty, sections, minTimer, isPrivate } = useExamEditorStore((store) => store.modules[currentModule]) + const { focusedSection, difficulty, sections, minTimer, access } = useExamEditorStore((store) => store.modules[currentModule]) const section = sections.find((section) => section.sectionId == focusedSection)?.state; @@ -84,7 +84,7 @@ const SpeakingSettings: React.FC = () => { }); })(); - const submitSpeaking = async () => { + const submitSpeaking = async (requiresApproval: boolean) => { if (title === "") { toast.error("Enter a title for the exam!"); return; @@ -181,11 +181,12 @@ const SpeakingSettings: React.FC = () => { minTimer, module: "speaking", id: title, - isDiagnostic: true, // using isDiagnostic to keep exam hidden until the respective approval workflow is completed. + requiresApproval: requiresApproval, + isDiagnostic: false, variant: undefined, difficulty, instructorGender: "varied", - private: isPrivate, + access, }; const result = await axios.post('/api/exam/speaking', exam); @@ -238,7 +239,7 @@ const SpeakingSettings: React.FC = () => { isDiagnostic: false, variant: undefined, difficulty, - private: isPrivate, + access, } as SpeakingExam); setExerciseIndex(0); setQuestionIndex(0); diff --git a/src/components/ExamEditor/SettingsEditor/writing/index.tsx b/src/components/ExamEditor/SettingsEditor/writing/index.tsx index 911d1817..9bbed3a8 100644 --- a/src/components/ExamEditor/SettingsEditor/writing/index.tsx +++ b/src/components/ExamEditor/SettingsEditor/writing/index.tsx @@ -23,7 +23,7 @@ const WritingSettings: React.FC = () => { const { minTimer, difficulty, - isPrivate, + access, sections, focusedSection, type, @@ -81,14 +81,14 @@ const WritingSettings: React.FC = () => { isDiagnostic: false, variant: undefined, difficulty, - private: isPrivate, + access, type: type! }); setExerciseIndex(0); openDetachedTab("popout?type=Exam&module=writing", router) } - const submitWriting = async () => { + const submitWriting = async (requiresApproval: boolean) => { if (title === "") { toast.error("Enter a title for the exam!"); return; @@ -131,10 +131,11 @@ const WritingSettings: React.FC = () => { minTimer, module: "writing", id: title, - isDiagnostic: true, // using isDiagnostic to keep exam hidden until the respective approval workflow is completed. + requiresApproval: requiresApproval, + isDiagnostic: false, variant: undefined, difficulty, - private: isPrivate, + access, type: type! }; diff --git a/src/components/ExamEditor/index.tsx b/src/components/ExamEditor/index.tsx index 599f804b..c53af7a9 100644 --- a/src/components/ExamEditor/index.tsx +++ b/src/components/ExamEditor/index.tsx @@ -3,12 +3,12 @@ import SectionRenderer from "./SectionRenderer"; import Checkbox from "../Low/Checkbox"; import Input from "../Low/Input"; import Select from "../Low/Select"; -import {capitalize} from "lodash"; -import {Difficulty} from "@/interfaces/exam"; -import {useCallback, useEffect, useMemo, useState} from "react"; -import {toast} from "react-toastify"; -import {ModuleState, SectionState} from "@/stores/examEditor/types"; -import {Module} from "@/interfaces"; +import { capitalize } from "lodash"; +import { AccessType, ACCESSTYPE, Difficulty } from "@/interfaces/exam"; +import { useCallback, useEffect, useState } from "react"; +import { toast } from "react-toastify"; +import { ModuleState, SectionState } from "@/stores/examEditor/types"; +import { Module } from "@/interfaces"; import useExamEditorStore from "@/stores/examEditor"; import WritingSettings from "./SettingsEditor/writing"; import ReadingSettings from "./SettingsEditor/reading"; @@ -16,243 +16,286 @@ import LevelSettings from "./SettingsEditor/level"; import ListeningSettings from "./SettingsEditor/listening"; import SpeakingSettings from "./SettingsEditor/speaking"; import ImportOrStartFromScratch from "./ImportExam/ImportOrFromScratch"; -import {defaultSectionSettings} from "@/stores/examEditor/defaults"; +import { defaultSectionSettings } from "@/stores/examEditor/defaults"; import Button from "../Low/Button"; import ResetModule from "./Standalone/ResetModule"; import ListeningInstructions from "./Standalone/ListeningInstructions"; -import {EntityWithRoles} from "@/interfaces/entity"; +import { EntityWithRoles } from "@/interfaces/entity"; const DIFFICULTIES: Difficulty[] = ["A1", "A2", "B1", "B2", "C1", "C2"]; -const ExamEditor: React.FC<{levelParts?: number; entitiesAllowEditPrivacy: EntityWithRoles[]}> = ({ - levelParts = 0, - entitiesAllowEditPrivacy = [], -}) => { - const {currentModule, dispatch} = useExamEditorStore(); - const {sections, minTimer, expandedSections, examLabel, isPrivate, difficulty, sectionLabels, importModule} = useExamEditorStore( - (state) => state.modules[currentModule], - ); +const ExamEditor: React.FC<{ + levelParts?: number; + entitiesAllowEditPrivacy: EntityWithRoles[]; +}> = ({ levelParts = 0, entitiesAllowEditPrivacy = [] }) => { + const { currentModule, dispatch } = useExamEditorStore(); + const { + sections, + minTimer, + expandedSections, + examLabel, + access, + difficulty, + sectionLabels, + importModule, + } = useExamEditorStore((state) => state.modules[currentModule]); - const [numberOfLevelParts, setNumberOfLevelParts] = useState(levelParts !== 0 ? levelParts : 1); - const [isResetModuleOpen, setIsResetModuleOpen] = useState(false); + const [numberOfLevelParts, setNumberOfLevelParts] = useState( + levelParts !== 0 ? levelParts : 1 + ); + const [isResetModuleOpen, setIsResetModuleOpen] = useState(false); - // For exam edits - useEffect(() => { - if (levelParts !== 0) { - setNumberOfLevelParts(levelParts); - dispatch({ - type: "UPDATE_MODULE", - payload: { - updates: { - sectionLabels: Array.from({length: levelParts}).map((_, i) => ({ - id: i + 1, - label: `Part ${i + 1}`, - })), - }, - module: "level", - }, - }); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [levelParts]); + // For exam edits + useEffect(() => { + if (levelParts !== 0) { + setNumberOfLevelParts(levelParts); + dispatch({ + type: "UPDATE_MODULE", + payload: { + updates: { + sectionLabels: Array.from({ length: levelParts }).map((_, i) => ({ + id: i + 1, + label: `Part ${i + 1}`, + })), + }, + module: "level", + }, + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [levelParts]); - useEffect(() => { - const currentSections = sections; - const currentLabels = sectionLabels; - let updatedSections: SectionState[]; - let updatedLabels: any; - if ((currentModule === "level" && currentSections.length !== currentLabels.length) || numberOfLevelParts !== currentSections.length) { - const newSections = [...currentSections]; - const newLabels = [...currentLabels]; - for (let i = currentLabels.length; i < numberOfLevelParts; i++) { - if (currentSections.length !== numberOfLevelParts) newSections.push(defaultSectionSettings(currentModule, i + 1)); - newLabels.push({ - id: i + 1, - label: `Part ${i + 1}`, - }); - } - updatedSections = newSections; - updatedLabels = newLabels; - } else if (numberOfLevelParts < currentSections.length) { - updatedSections = currentSections.slice(0, numberOfLevelParts); - updatedLabels = currentLabels.slice(0, numberOfLevelParts); - } else { - return; - } + useEffect(() => { + const currentSections = sections; + const currentLabels = sectionLabels; + let updatedSections: SectionState[]; + let updatedLabels: any; + if ( + (currentModule === "level" && + currentSections.length !== currentLabels.length) || + numberOfLevelParts !== currentSections.length + ) { + const newSections = [...currentSections]; + const newLabels = [...currentLabels]; + for (let i = currentLabels.length; i < numberOfLevelParts; i++) { + if (currentSections.length !== numberOfLevelParts) + newSections.push(defaultSectionSettings(currentModule, i + 1)); + newLabels.push({ + id: i + 1, + label: `Part ${i + 1}`, + }); + } + updatedSections = newSections; + updatedLabels = newLabels; + } else if (numberOfLevelParts < currentSections.length) { + updatedSections = currentSections.slice(0, numberOfLevelParts); + updatedLabels = currentLabels.slice(0, numberOfLevelParts); + } else { + return; + } - const updatedExpandedSections = expandedSections.filter((sectionId) => updatedSections.some((section) => section.sectionId === sectionId)); + const updatedExpandedSections = expandedSections.filter((sectionId) => + updatedSections.some((section) => section.sectionId === sectionId) + ); - dispatch({ - type: "UPDATE_MODULE", - payload: { - updates: { - sections: updatedSections, - sectionLabels: updatedLabels, - expandedSections: updatedExpandedSections, - }, - }, - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [numberOfLevelParts]); + dispatch({ + type: "UPDATE_MODULE", + payload: { + updates: { + sections: updatedSections, + sectionLabels: updatedLabels, + expandedSections: updatedExpandedSections, + }, + }, + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [numberOfLevelParts]); - const sectionIds = sections.map((section) => section.sectionId); + const sectionIds = sections.map((section) => section.sectionId); - const updateModule = useCallback( - (updates: Partial) => { - dispatch({type: "UPDATE_MODULE", payload: {updates}}); - }, - [dispatch], - ); + const updateModule = useCallback( + (updates: Partial) => { + dispatch({ type: "UPDATE_MODULE", payload: { updates } }); + }, + [dispatch] + ); - const toggleSection = (sectionId: number) => { - if (expandedSections.length === 1 && sectionIds.includes(sectionId)) { - toast.error("Include at least one section!"); - return; - } - dispatch({type: "TOGGLE_SECTION", payload: {sectionId}}); - }; + const toggleSection = (sectionId: number) => { + if (expandedSections.length === 1 && sectionIds.includes(sectionId)) { + toast.error("Include at least one section!"); + return; + } + dispatch({ type: "TOGGLE_SECTION", payload: { sectionId } }); + }; - const ModuleSettings: Record = { - reading: ReadingSettings, - writing: WritingSettings, - speaking: SpeakingSettings, - listening: ListeningSettings, - level: LevelSettings, - }; + const ModuleSettings: Record = { + reading: ReadingSettings, + writing: WritingSettings, + speaking: SpeakingSettings, + listening: ListeningSettings, + level: LevelSettings, + }; - const Settings = ModuleSettings[currentModule]; - const showImport = importModule && ["reading", "listening", "level"].includes(currentModule); + const Settings = ModuleSettings[currentModule]; + const showImport = + importModule && ["reading", "listening", "level"].includes(currentModule); - const updateLevelParts = (parts: number) => { - setNumberOfLevelParts(parts); - }; + const updateLevelParts = (parts: number) => { + setNumberOfLevelParts(parts); + }; - return ( - <> - {showImport ? ( - - ) : ( - <> - {isResetModuleOpen && ( - - )} -
-
-
- - - updateModule({ - minTimer: parseInt(e) < 15 ? 15 : parseInt(e), - }) - } - value={minTimer} - className="max-w-[300px]" - /> -
-
- - setNumberOfLevelParts(parseInt(v))} - value={numberOfLevelParts} - /> -
- )} -
-
- updateModule({isPrivate: checked})} - disabled={entitiesAllowEditPrivacy.length === 0}> - Privacy (Only available for Assignments) - -
-
-
-
- - updateModule({examLabel: text})} - roundness="xl" - value={examLabel} - required - /> -
- {currentModule === "listening" && } - -
-
- -
- -
-
- - )} - - ); + return ( + <> + {showImport ? ( + + ) : ( + <> + {isResetModuleOpen && ( + + )} +
+
+
+ + + updateModule({ + minTimer: parseInt(e) < 15 ? 15 : parseInt(e), + }) + } + value={minTimer} + className="max-w-[300px]" + /> +
+
+ + setNumberOfLevelParts(parseInt(v))} + value={numberOfLevelParts} + /> +
+ )} +
+
+ updateModule({ examLabel: text })} + roundness="xl" + value={examLabel} + required + /> +
+ {currentModule === "listening" && } + +
+
+ +
+ +
+
+ + )} + + ); }; export default ExamEditor; diff --git a/src/components/High/Table.tsx b/src/components/High/Table.tsx index 2d588ffc..572e619a 100644 --- a/src/components/High/Table.tsx +++ b/src/components/High/Table.tsx @@ -149,10 +149,16 @@ export default function Table({ ))} - {isLoading && ( + {isLoading ? (
+ ) : ( + rows.length === 0 && ( +
+ No data found... +
+ ) )}
); diff --git a/src/hooks/useApprovalWorkflows.tsx b/src/hooks/useApprovalWorkflows.tsx index 1b950c2f..7bfec920 100644 --- a/src/hooks/useApprovalWorkflows.tsx +++ b/src/hooks/useApprovalWorkflows.tsx @@ -2,7 +2,7 @@ import { ApprovalWorkflow } from "@/interfaces/approval.workflow"; import axios from "axios"; import { useCallback, useEffect, useState } from "react"; -export default function useApprovalWorkflows() { +export default function useApprovalWorkflows(entitiesString?: string) { const [workflows, setWorkflows] = useState([]); const [isLoading, setIsLoading] = useState(false); const [isError, setIsError] = useState(false); @@ -10,7 +10,7 @@ export default function useApprovalWorkflows() { const getData = useCallback(() => { setIsLoading(true); axios - .get(`/api/approval-workflows`) + .get(`/api/approval-workflows`, {params: { entityIds: entitiesString }}) .then((response) => setWorkflows(response.data)) .catch((error) => { setIsError(true); diff --git a/src/hooks/useExams.tsx b/src/hooks/useExams.tsx index c9e001b5..befc474d 100644 --- a/src/hooks/useExams.tsx +++ b/src/hooks/useExams.tsx @@ -1,21 +1,21 @@ -import {Exam} from "@/interfaces/exam"; +import { Exam } from "@/interfaces/exam"; import axios from "axios"; -import {useEffect, useState} from "react"; +import { useEffect, useState } from "react"; export default function useExams() { - const [exams, setExams] = useState([]); - const [isLoading, setIsLoading] = useState(false); - const [isError, setIsError] = useState(false); + const [exams, setExams] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [isError, setIsError] = useState(false); - const getData = () => { - setIsLoading(true); - axios - .get("/api/exam") - .then((response) => setExams(response.data)) - .finally(() => setIsLoading(false)); - }; + const getData = () => { + setIsLoading(true); + axios + .get(`/api/exam`) + .then((response) => setExams(response.data)) + .finally(() => setIsLoading(false)); + }; - useEffect(getData, []); + useEffect(getData, []); - return {exams, isLoading, isError, reload: getData}; + return { exams, isLoading, isError, reload: getData }; } diff --git a/src/hooks/usePagination.tsx b/src/hooks/usePagination.tsx index 9075dbbe..3c307f33 100644 --- a/src/hooks/usePagination.tsx +++ b/src/hooks/usePagination.tsx @@ -1,60 +1,141 @@ import Button from "@/components/Low/Button"; -import {useMemo, useState} from "react"; -import {BiChevronLeft} from "react-icons/bi"; -import {BsChevronDoubleLeft, BsChevronDoubleRight, BsChevronLeft, BsChevronRight} from "react-icons/bs"; +import { useEffect, useMemo, useState } from "react"; +import { + BsChevronDoubleLeft, + BsChevronDoubleRight, + BsChevronLeft, + BsChevronRight, +} from "react-icons/bs"; +import Select from "../components/Low/Select"; export default function usePagination(list: T[], size = 25) { - const [page, setPage] = useState(0); + const [page, setPage] = useState(0); + const [itemsPerPage, setItemsPerPage] = useState(size); - const items = useMemo(() => list.slice(page * size, (page + 1) * size), [page, size, list]); + const items = useMemo( + () => list.slice(page * itemsPerPage, (page + 1) * itemsPerPage), + [list, page, itemsPerPage] + ); + useEffect(() => { + if (page * itemsPerPage >= list.length) setPage(0); + }, [items, itemsPerPage, list.length, page]); - const render = () => ( -
-
- -
-
- - {page * size + 1} - {(page + 1) * size > list.length ? list.length : (page + 1) * size} / {list.length} - - -
-
- ); + const itemsPerPageOptions = [25, 50, 100, 200]; - const renderMinimal = () => ( -
-
- - -
- - {page * size + 1} - {(page + 1) * size > list.length ? list.length : (page + 1) * size} / {list.length} - -
- - -
-
- ); + const render = () => ( +
+
+ +
+
+
+ setItemsPerPage(parseInt(value!.value ?? "25"))} + options={itemsPerPageOptions.map((size) => ({ + label: size.toString(), + value: size.toString(), + }))} + isClearable={false} + styles={{ + control: (styles) => ({ ...styles, width: "100px" }), + container: (styles) => ({ ...styles, width: "100px" }), + }} + /> + + {page * itemsPerPage + 1} -{" "} + {itemsPerPage * (page + 1) > list.length + ? list.length + : itemsPerPage * (page + 1)} + / {list.length} + +
+
+ + +
+
+ ); + + return { page, items, setPage, render, renderMinimal }; } diff --git a/src/interfaces/exam.ts b/src/interfaces/exam.ts index 965ad45b..3fc363cc 100644 --- a/src/interfaces/exam.ts +++ b/src/interfaces/exam.ts @@ -1,4 +1,3 @@ -import instructions from "@/pages/api/exam/media/instructions"; import { Module } from "."; export type Exam = ReadingExam | ListeningExam | WritingExam | SpeakingExam | LevelExam; @@ -10,6 +9,9 @@ export type Difficulty = BasicDifficulty | CEFRLevels; // Left easy, medium and hard to support older exam versions export type BasicDifficulty = "easy" | "medium" | "hard"; export type CEFRLevels = "A1" | "A2" | "B1" | "B2" | "C1" | "C2"; +export const ACCESSTYPE = ["public", "private", "confidential"] as const; +export type AccessType = typeof ACCESSTYPE[number]; + export interface ExamBase { @@ -24,8 +26,9 @@ export interface ExamBase { shuffle?: boolean; createdBy?: string; // option as it has been added later createdAt?: string; // option as it has been added later - private?: boolean; + access: AccessType; label?: string; + requiresApproval?: boolean; } export interface ReadingExam extends ExamBase { module: "reading"; diff --git a/src/lib/createWorkflowsOnExamCreation.ts b/src/lib/createWorkflowsOnExamCreation.ts index 732a1f18..e2a791af 100644 --- a/src/lib/createWorkflowsOnExamCreation.ts +++ b/src/lib/createWorkflowsOnExamCreation.ts @@ -1,7 +1,10 @@ import { Module } from "@/interfaces"; import { getApprovalWorkflowByFormIntaker, createApprovalWorkflow } from "@/utils/approval.workflows.be"; +import client from "@/lib/mongodb"; -export async function createApprovalWorkflowsOnExamCreation(examAuthor: string, examEntities: string[], examId: string, examModule: string) { +const db = client.db(process.env.MONGODB_DB); + +/* 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); @@ -27,6 +30,53 @@ export async function createApprovalWorkflowsOnExamCreation(examAuthor: string, const successCount = results.filter((r) => r.created).length; const totalCount = examEntities.length; + return { + successCount, + totalCount, + }; +} */ + +// TEMPORARY BEHAVIOUR! ONLY THE FIRST CONFIGURED WORKFLOW FOUND IS STARTED +export async function createApprovalWorkflowOnExamCreation(examAuthor: string, examEntities: string[], examId: string, examModule: string) { + let successCount = 0; + let totalCount = 0; + + for (const entity of examEntities) { + const configuredWorkflow = await getApprovalWorkflowByFormIntaker(entity, examAuthor); + + if (!configuredWorkflow) { + continue; + } + + totalCount = 1; // a workflow was found + + configuredWorkflow.modules.push(examModule as Module); + configuredWorkflow.name = examId; + configuredWorkflow.examId = examId; + configuredWorkflow.entityId = entity; + configuredWorkflow.startDate = Date.now(); + configuredWorkflow.steps[0].completed = true; + configuredWorkflow.steps[0].completedBy = examAuthor; + configuredWorkflow.steps[0].completedDate = Date.now(); + + try { + await createApprovalWorkflow("active-workflows", configuredWorkflow); + successCount = 1; + break; // Stop after the first success + } catch (error: any) { + break; + } + } + + // prettier-ignore + if (totalCount === 0) { // current behaviour: if no workflow was found skip approval process + await db.collection(examModule).updateOne( + { id: examId }, + { $set: { id: examId, isDiagnostic: false }}, + { upsert: true } + ); + } + return { successCount, totalCount, diff --git a/src/pages/(admin)/CorporateGradingSystem.tsx b/src/pages/(admin)/CorporateGradingSystem.tsx index 9506108d..e4e9c829 100644 --- a/src/pages/(admin)/CorporateGradingSystem.tsx +++ b/src/pages/(admin)/CorporateGradingSystem.tsx @@ -265,7 +265,7 @@ export default function CorporateGradingSystem({ <>