Merged in feature/ExamGenRework (pull request #120)
ENCOA-253, ENCOA-248, ENCOA-246 Approved-by: Tiago Ribeiro
This commit is contained in:
@@ -112,7 +112,7 @@ const SettingsEditor: React.FC<SettingsEditorProps> = ({
|
|||||||
onChange={(o) => onIntroOptionChange({ value: o!.value, label: o!.label })}
|
onChange={(o) => onIntroOptionChange({ value: o!.value, label: o!.label })}
|
||||||
value={localSettings.introOption}
|
value={localSettings.introOption}
|
||||||
/>
|
/>
|
||||||
{localSettings.introOption.value !== "None" && (
|
{localSettings.introOption && localSettings.introOption.value !== "None" && (
|
||||||
<AutoExpandingTextArea
|
<AutoExpandingTextArea
|
||||||
key={`section-${sectionId}`}
|
key={`section-${sectionId}`}
|
||||||
value={localSettings.currentIntro || ''}
|
value={localSettings.currentIntro || ''}
|
||||||
|
|||||||
@@ -33,21 +33,24 @@ const ExamEditor: React.FC<{ levelParts?: number }> = ({ levelParts = 0 }) => {
|
|||||||
importModule
|
importModule
|
||||||
} = useExamEditorStore(state => state.modules[currentModule]);
|
} = useExamEditorStore(state => state.modules[currentModule]);
|
||||||
|
|
||||||
const [numberOfLevelParts, setNumberOfLevelParts] = useState(levelParts !== undefined ? levelParts : 1);
|
const [numberOfLevelParts, setNumberOfLevelParts] = useState(levelParts !== 0 ? levelParts : 1);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setNumberOfLevelParts(levelParts);
|
if (levelParts !== 0) {
|
||||||
dispatch({
|
setNumberOfLevelParts(levelParts);
|
||||||
type: 'UPDATE_MODULE',
|
dispatch({
|
||||||
payload: {
|
type: 'UPDATE_MODULE',
|
||||||
updates: {
|
payload: {
|
||||||
sectionLabels: Array.from({ length: levelParts }).map((_, i) => ({
|
updates: {
|
||||||
id: i + 1,
|
sectionLabels: Array.from({ length: levelParts }).map((_, i) => ({
|
||||||
label: `Part ${i + 1}`
|
id: i + 1,
|
||||||
}))
|
label: `Part ${i + 1}`
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
module: "level"
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
})
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [levelParts])
|
}, [levelParts])
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ import Button from "@/components/Low/Button";
|
|||||||
import Modal from "@/components/Modal";
|
import Modal from "@/components/Modal";
|
||||||
import { Exam, LevelExam, MultipleChoiceExercise, ShuffleMap } from "@/interfaces/exam";
|
import { Exam, LevelExam, MultipleChoiceExercise, ShuffleMap } from "@/interfaces/exam";
|
||||||
import useExamStore, { usePersistentExamStore } from "@/stores/exam";
|
import useExamStore, { usePersistentExamStore } from "@/stores/exam";
|
||||||
|
import { defaultExamUserSolutions } from "@/utils/exams";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { BsFillGrid3X3GapFill } from "react-icons/bs";
|
import { BsFillGrid3X3GapFill } from "react-icons/bs";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -27,7 +28,10 @@ const MCQuestionGrid: React.FC<Props> = ({ exam, showSolutions, runOnClick, prev
|
|||||||
|
|
||||||
const currentExercise = useMemo(() => (exam as LevelExam).parts[sectionIndex!].exercises[exerciseIndex] as MultipleChoiceExercise, [exam, exerciseIndex, sectionIndex])
|
const currentExercise = useMemo(() => (exam as LevelExam).parts[sectionIndex!].exercises[exerciseIndex] as MultipleChoiceExercise, [exam, exerciseIndex, sectionIndex])
|
||||||
const userSolution = useMemo(() => userSolutions!.find((x) => x.exercise.toString() == currentExercise.id.toString())!, [currentExercise.id, userSolutions])
|
const userSolution = useMemo(() => userSolutions!.find((x) => x.exercise.toString() == currentExercise.id.toString())!, [currentExercise.id, userSolutions])
|
||||||
const answeredQuestions = useMemo(() => new Set(userSolution.solutions.map(sol => sol.question.toString())), [userSolution.solutions])
|
const answeredQuestions = useMemo(() =>
|
||||||
|
userSolution ? new Set(userSolution.solutions.map(sol => sol.question.toString())) : new Set(),
|
||||||
|
[userSolution]
|
||||||
|
);
|
||||||
const exerciseOffset = useMemo(() => Number(currentExercise.questions[0].id), [currentExercise.questions])
|
const exerciseOffset = useMemo(() => Number(currentExercise.questions[0].id), [currentExercise.questions])
|
||||||
const lastExercise = useMemo(() => exerciseOffset + (currentExercise.questions.length - 1),
|
const lastExercise = useMemo(() => exerciseOffset + (currentExercise.questions.length - 1),
|
||||||
[currentExercise.questions.length, exerciseOffset]);
|
[currentExercise.questions.length, exerciseOffset]);
|
||||||
|
|||||||
@@ -8,29 +8,38 @@ import { LevelExam, ListeningExam, ReadingExam, SpeakingExam, WritingExam } from
|
|||||||
import { User } from "@/interfaces/user";
|
import { User } from "@/interfaces/user";
|
||||||
import { usePersistentExamStore } from "@/stores/exam";
|
import { usePersistentExamStore } from "@/stores/exam";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
// TODO: perms
|
// TODO: perms
|
||||||
|
|
||||||
const Popout: React.FC<{ user: User }> = ({ user }) => {
|
const Popout: React.FC<{ user: User }> = ({ user }) => {
|
||||||
const state = usePersistentExamStore((state) => state);
|
const { bgColor, exam, partIndex, setUserSolutions } = usePersistentExamStore();
|
||||||
usePersistentStorage(usePersistentExamStore);
|
usePersistentStorage(usePersistentExamStore);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
setUserSolutions([]);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`relative flex w-full min-h-screen p-4 shadow-md items-center rounded-2xl ${state.bgColor}`}>
|
<div className={`relative flex w-full min-h-screen p-4 shadow-md items-center rounded-2xl ${bgColor}`}>
|
||||||
<div className={clsx("relative flex p-20 justify-center flex-1")}>
|
<div className={clsx("relative flex p-20 justify-center flex-1")}>
|
||||||
{state.exam?.module == "level" && state.exam.parts && state.partIndex >= 0 &&
|
{exam?.module == "level" && exam.parts && partIndex >= 0 &&
|
||||||
<Level exam={state.exam as LevelExam} preview={true} />
|
<Level exam={exam as LevelExam} preview={true} />
|
||||||
}
|
}
|
||||||
{state.exam?.module == "writing" && state.exam.exercises && state.partIndex >= 0 &&
|
{exam?.module == "writing" && exam.exercises && partIndex >= 0 &&
|
||||||
<Writing exam={state.exam as WritingExam} preview={true} />
|
<Writing exam={exam as WritingExam} preview={true} />
|
||||||
}
|
}
|
||||||
{state.exam?.module == "reading" && state.exam.parts.length > 0 &&
|
{exam?.module == "reading" && exam.parts.length > 0 &&
|
||||||
<Reading exam={state.exam as ReadingExam} preview={true} />
|
<Reading exam={exam as ReadingExam} preview={true} />
|
||||||
}
|
}
|
||||||
{state.exam?.module == "listening" && state.exam.parts.length > 0 &&
|
{exam?.module == "listening" && exam.parts.length > 0 &&
|
||||||
<Listening exam={state.exam as ListeningExam} preview={true} />
|
<Listening exam={exam as ListeningExam} preview={true} />
|
||||||
}
|
}
|
||||||
{state.exam?.module == "speaking" && state.exam.exercises.length > 0 &&
|
{exam?.module == "speaking" && exam.exercises.length > 0 &&
|
||||||
<Speaking exam={state.exam as SpeakingExam} preview={true} />
|
<Speaking exam={exam as SpeakingExam} preview={true} />
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ const Level: React.FC<ExamProps<LevelExam>> = ({ exam, showSolutions = false, pr
|
|||||||
const {
|
const {
|
||||||
nextExercise, previousExercise,
|
nextExercise, previousExercise,
|
||||||
showPartDivider, setShowPartDivider,
|
showPartDivider, setShowPartDivider,
|
||||||
seenParts, setSeenParts, startNow
|
seenParts, setSeenParts, startNow, setStartNow
|
||||||
} = useExamNavigation(
|
} = useExamNavigation(
|
||||||
{
|
{
|
||||||
exam, module: "level", showBlankModal: showQuestionsModal,
|
exam, module: "level", showBlankModal: showQuestionsModal,
|
||||||
@@ -361,14 +361,14 @@ const Level: React.FC<ExamProps<LevelExam>> = ({ exam, showSolutions = false, pr
|
|||||||
(!showPartDivider && !startNow) &&
|
(!showPartDivider && !startNow) &&
|
||||||
<Timer minTimer={exam.minTimer} disableTimer={showSolutions || preview} standalone={true} />
|
<Timer minTimer={exam.minTimer} disableTimer={showSolutions || preview} standalone={true} />
|
||||||
}
|
}
|
||||||
{(showPartDivider || startNow) ?
|
{(showPartDivider || (startNow && partIndex === 0)) ?
|
||||||
<PartDivider
|
<PartDivider
|
||||||
module="level"
|
module="level"
|
||||||
sectionLabel="Part"
|
sectionLabel="Part"
|
||||||
defaultTitle="Placement Test"
|
defaultTitle="Placement Test"
|
||||||
section={exam.parts[partIndex]}
|
section={exam.parts[partIndex]}
|
||||||
sectionIndex={partIndex}
|
sectionIndex={partIndex}
|
||||||
onNext={() => { setShowPartDivider(false); setBgColor("bg-white"); setSeenParts(prev => new Set(prev).add(partIndex)); }}
|
onNext={() => { setShowPartDivider(false); setStartNow(false); setBgColor("bg-white"); setSeenParts(prev => new Set(prev).add(partIndex)); }}
|
||||||
/> : (
|
/> : (
|
||||||
<>
|
<>
|
||||||
{exam.parts.length > 1 && <SectionNavbar
|
{exam.parts.length > 1 && <SectionNavbar
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ type UseExamNavigation = (props: {
|
|||||||
setShowPartDivider: React.Dispatch<React.SetStateAction<boolean>>;
|
setShowPartDivider: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
setSeenParts: React.Dispatch<React.SetStateAction<Set<number>>>;
|
setSeenParts: React.Dispatch<React.SetStateAction<Set<number>>>;
|
||||||
setIsBetweenParts: React.Dispatch<React.SetStateAction<boolean>>;
|
setIsBetweenParts: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
setStartNow: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const useExamNavigation: UseExamNavigation = ({
|
const useExamNavigation: UseExamNavigation = ({
|
||||||
@@ -50,8 +51,7 @@ const useExamNavigation: UseExamNavigation = ({
|
|||||||
exerciseIndex, setExerciseIndex,
|
exerciseIndex, setExerciseIndex,
|
||||||
partIndex, setPartIndex,
|
partIndex, setPartIndex,
|
||||||
questionIndex, setQuestionIndex,
|
questionIndex, setQuestionIndex,
|
||||||
userSolutions, setModuleIndex,
|
userSolutions, setBgColor,
|
||||||
setBgColor,
|
|
||||||
dispatch,
|
dispatch,
|
||||||
} = !preview ? examState : persistentExamState;
|
} = !preview ? examState : persistentExamState;
|
||||||
|
|
||||||
@@ -74,19 +74,34 @@ const useExamNavigation: UseExamNavigation = ({
|
|||||||
const [startNow, setStartNow] = useState(!showPartDivider && !showSolutions);
|
const [startNow, setStartNow] = useState(!showPartDivider && !showSolutions);
|
||||||
|
|
||||||
// when navbar is used
|
// when navbar is used
|
||||||
useEffect(()=> {
|
useEffect(() => {
|
||||||
if(startNow && !showPartDivider && partIndex !== 0) {
|
if (startNow && !showPartDivider && partIndex !== 0) {
|
||||||
setStartNow(false);
|
setStartNow(false);
|
||||||
}
|
}
|
||||||
} , [partIndex, startNow, showPartDivider])
|
}, [partIndex, startNow, showPartDivider])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!showSolutions && hasDivider(exam, isPartExam ? partIndex : exerciseIndex) && !seenParts.has(partIndex)) {
|
if (
|
||||||
setShowPartDivider(true);
|
(
|
||||||
|
module === "level" ?
|
||||||
|
(startNow || hasDivider(exam, isPartExam ? partIndex : exerciseIndex))
|
||||||
|
:
|
||||||
|
(hasDivider(exam, isPartExam ? partIndex : exerciseIndex))
|
||||||
|
)
|
||||||
|
&& !showSolutions && !seenParts.has(partIndex)
|
||||||
|
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
// partIndex === 0 -> startNow
|
||||||
|
(module === "level" && (partIndex === 0 || hasDivider(exam, isPartExam ? partIndex : exerciseIndex))) ||
|
||||||
|
module !== "level"
|
||||||
|
) {
|
||||||
|
setShowPartDivider(true);
|
||||||
|
}
|
||||||
setBgColor(`bg-ielts-${module}-light`);
|
setBgColor(`bg-ielts-${module}-light`);
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [partIndex]);
|
}, [partIndex, startNow]);
|
||||||
|
|
||||||
const nextExercise = (keepGoing: boolean = false) => {
|
const nextExercise = (keepGoing: boolean = false) => {
|
||||||
scrollToTop();
|
scrollToTop();
|
||||||
@@ -197,7 +212,7 @@ const useExamNavigation: UseExamNavigation = ({
|
|||||||
const previousPartExam = () => {
|
const previousPartExam = () => {
|
||||||
|
|
||||||
if (!showPartDivider && partIndex === 0 && exerciseIndex === 0 && questionIndex === 0 && !startNow) {
|
if (!showPartDivider && partIndex === 0 && exerciseIndex === 0 && questionIndex === 0 && !startNow) {
|
||||||
setStartNow(true);
|
if (module !== "level") setStartNow(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,7 +309,8 @@ const useExamNavigation: UseExamNavigation = ({
|
|||||||
setSeenParts,
|
setSeenParts,
|
||||||
isBetweenParts,
|
isBetweenParts,
|
||||||
setIsBetweenParts,
|
setIsBetweenParts,
|
||||||
startNow
|
startNow,
|
||||||
|
setStartNow
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export type Variant = "full" | "partial";
|
|||||||
export type InstructorGender = "male" | "female" | "varied";
|
export type InstructorGender = "male" | "female" | "varied";
|
||||||
export type Difficulty = "easy" | "medium" | "hard";
|
export type Difficulty = "easy" | "medium" | "hard";
|
||||||
|
|
||||||
interface ExamBase {
|
export interface ExamBase {
|
||||||
id: string;
|
id: string;
|
||||||
module: Module;
|
module: Module;
|
||||||
minTimer: number;
|
minTimer: number;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
|||||||
import client from "@/lib/mongodb";
|
import client from "@/lib/mongodb";
|
||||||
import { withIronSessionApiRoute } from "iron-session/next";
|
import { withIronSessionApiRoute } from "iron-session/next";
|
||||||
import { sessionOptions } from "@/lib/session";
|
import { sessionOptions } from "@/lib/session";
|
||||||
import { Exam, InstructorGender, Variant } from "@/interfaces/exam";
|
import { Exam, ExamBase, InstructorGender, Variant } from "@/interfaces/exam";
|
||||||
import { getExams } from "@/utils/exams.be";
|
import { getExams } from "@/utils/exams.be";
|
||||||
import { Module } from "@/interfaces";
|
import { Module } from "@/interfaces";
|
||||||
import { getUserCorporate } from "@/utils/groups.be";
|
import { getUserCorporate } from "@/utils/groups.be";
|
||||||
@@ -60,15 +60,22 @@ async function POST(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
await session.withTransaction(async () => {
|
await session.withTransaction(async () => {
|
||||||
const docSnap = await db.collection(module).findOne({ id: req.body.id }, { session });
|
const docSnap = await db.collection(module).findOne<ExamBase>({ id: req.body.id }, { session });
|
||||||
|
|
||||||
if (docSnap) {
|
// Check whether the id of the exam matches another exam with different
|
||||||
|
// owners, throw exception if there is, else allow editing
|
||||||
|
const ownersSet = new Set(docSnap?.owners || []);
|
||||||
|
if (docSnap?.owners?.length === exam.owners.lenght && exam.owners.every((e: string) => ownersSet.has(e))) {
|
||||||
throw new Error("Name already exists");
|
throw new Error("Name already exists");
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.collection(module).insertOne(
|
await db.collection(module).updateOne(
|
||||||
{ id: req.body.id, ...exam },
|
{ id: req.body.id },
|
||||||
{ session }
|
{ $set: { id: req.body.id, ...exam } },
|
||||||
|
{
|
||||||
|
upsert: true,
|
||||||
|
session
|
||||||
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user