Merge remote-tracking branch 'origin/develop' into feature/ExamGenRework

This commit is contained in:
Carlos-Mesquita
2024-11-10 07:09:25 +00:00
17 changed files with 909 additions and 915 deletions

View File

@@ -1,6 +1,6 @@
/* eslint-disable @next/next/no-img-element */
import {Module} from "@/interfaces";
import {useEffect, useState} from "react";
import { Module } from "@/interfaces";
import { useEffect, useState } from "react";
import AbandonPopup from "@/components/AbandonPopup";
import Layout from "@/components/High/Layout";
@@ -12,15 +12,15 @@ import Selection from "@/exams/Selection";
import Speaking from "@/exams/Speaking";
import Writing from "@/exams/Writing";
import useUser from "@/hooks/useUser";
import {Exam, LevelExam, UserSolution, Variant} from "@/interfaces/exam";
import {Stat, User} from "@/interfaces/user";
import { Exam, LevelExam, UserSolution, Variant } from "@/interfaces/exam";
import { Stat, User } from "@/interfaces/user";
import useExamStore from "@/stores/examStore";
import {evaluateSpeakingAnswer, evaluateWritingAnswer} from "@/utils/evaluation";
import {defaultExamUserSolutions, getExam} from "@/utils/exams";
import { evaluateSpeakingAnswer, evaluateWritingAnswer } from "@/utils/evaluation";
import { defaultExamUserSolutions, getExam } from "@/utils/exams";
import axios from "axios";
import {useRouter} from "next/router";
import {toast, ToastContainer} from "react-toastify";
import {v4 as uuidv4} from "uuid";
import { useRouter } from "next/router";
import { toast, ToastContainer } from "react-toastify";
import { v4 as uuidv4 } from "uuid";
import useSessions from "@/hooks/useSessions";
import ShortUniqueId from "short-unique-id";
import clsx from "clsx";
@@ -31,10 +31,11 @@ import { mapBy } from "@/utils";
interface Props {
page: "exams" | "exercises";
user: User;
destination?: string
hideSidebar?: boolean
}
export default function ExamPage({page, user, hideSidebar = false}: Props) {
export default function ExamPage({ page, user, destination = "/exam", hideSidebar = false }: Props) {
const [variant, setVariant] = useState<Variant>("full");
const [avoidRepeated, setAvoidRepeated] = useState(false);
const [hasBeenUploaded, setHasBeenUploaded] = useState(false);
@@ -49,18 +50,18 @@ export default function ExamPage({page, user, hideSidebar = false}: Props) {
const assignment = useExamStore((state) => state.assignment);
const initialTimeSpent = useExamStore((state) => state.timeSpent);
const {exam, setExam} = useExamStore((state) => state);
const {exams, setExams} = useExamStore((state) => state);
const {sessionId, setSessionId} = useExamStore((state) => state);
const {partIndex, setPartIndex} = useExamStore((state) => state);
const {moduleIndex, setModuleIndex} = useExamStore((state) => state);
const {questionIndex, setQuestionIndex} = useExamStore((state) => state);
const {exerciseIndex, setExerciseIndex} = useExamStore((state) => state);
const {userSolutions, setUserSolutions} = useExamStore((state) => state);
const {showSolutions, setShowSolutions} = useExamStore((state) => state);
const {selectedModules, setSelectedModules} = useExamStore((state) => state);
const {inactivity, setInactivity} = useExamStore((state) => state);
const {bgColor, setBgColor} = useExamStore((state) => state);
const { exam, setExam } = useExamStore((state) => state);
const { exams, setExams } = useExamStore((state) => state);
const { sessionId, setSessionId } = useExamStore((state) => state);
const { partIndex, setPartIndex } = useExamStore((state) => state);
const { moduleIndex, setModuleIndex } = useExamStore((state) => state);
const { questionIndex, setQuestionIndex } = useExamStore((state) => state);
const { exerciseIndex, setExerciseIndex } = useExamStore((state) => state);
const { userSolutions, setUserSolutions } = useExamStore((state) => state);
const { showSolutions, setShowSolutions } = useExamStore((state) => state);
const { selectedModules, setSelectedModules } = useExamStore((state) => state);
const { inactivity, setInactivity } = useExamStore((state) => state);
const { bgColor, setBgColor } = useExamStore((state) => state);
const setShuffleMaps = useExamStore((state) => state.setShuffles);
const router = useRouter();
@@ -262,11 +263,11 @@ export default function ExamPage({page, user, hideSidebar = false}: Props) {
date: new Date().getTime(),
isDisabled: solution.isDisabled,
shuffleMaps: solution.shuffleMaps,
...(assignment ? {assignment: assignment.id} : {}),
...(assignment ? { assignment: assignment.id } : {}),
}));
axios
.post<{ok: boolean}>("/api/stats", newStats)
.post<{ ok: boolean }>("/api/stats", newStats)
.then((response) => setHasBeenUploaded(response.data.ok))
.catch(() => setHasBeenUploaded(false));
}
@@ -329,7 +330,7 @@ export default function ExamPage({page, user, hideSidebar = false}: Props) {
),
}),
);
return Object.assign(exam, {parts});
return Object.assign(exam, { parts });
}
const exercises = exam.exercises.map((x) =>
@@ -337,7 +338,7 @@ export default function ExamPage({page, user, hideSidebar = false}: Props) {
userSolutions: userSolutions.find((y) => x.id === y.exercise)?.solutions,
}),
);
return Object.assign(exam, {exercises});
return Object.assign(exam, { exercises });
};
const onFinish = async (solutions: UserSolution[]) => {
@@ -392,7 +393,7 @@ export default function ExamPage({page, user, hideSidebar = false}: Props) {
correct: number;
}[] => {
const scores: {
[key in Module]: {total: number; missing: number; correct: number};
[key in Module]: { total: number; missing: number; correct: number };
} = {
reading: {
total: 0,
@@ -434,7 +435,7 @@ export default function ExamPage({page, user, hideSidebar = false}: Props) {
return Object.keys(scores)
.filter((x) => scores[x as Module].total > 0)
.map((x) => ({module: x as Module, ...scores[x as Module]}));
.map((x) => ({ module: x as Module, ...scores[x as Module] }));
};
const renderScreen = () => {
@@ -465,6 +466,7 @@ export default function ExamPage({page, user, hideSidebar = false}: Props) {
timeSpent,
inactivity: totalInactivity,
}}
destination={destination}
onViewResults={(index?: number) => {
if (exams[0].module === "level") {
const levelExam = exams[0] as LevelExam;

View File

@@ -6,43 +6,43 @@ import ProgressBar from "@/components/Low/ProgressBar";
import Select from "@/components/Low/Select";
import Separator from "@/components/Low/Separator";
import useExams from "@/hooks/useExams";
import {useListSearch} from "@/hooks/useListSearch";
import { useListSearch } from "@/hooks/useListSearch";
import usePagination from "@/hooks/usePagination";
import {Module} from "@/interfaces";
import {EntityWithRoles} from "@/interfaces/entity";
import {InstructorGender, Variant} from "@/interfaces/exam";
import {Assignment} from "@/interfaces/results";
import {Group, User} from "@/interfaces/user";
import {sessionOptions} from "@/lib/session";
import {mapBy, redirect, serialize} from "@/utils";
import { Module } from "@/interfaces";
import { EntityWithRoles } from "@/interfaces/entity";
import { InstructorGender, Variant } from "@/interfaces/exam";
import { Assignment } from "@/interfaces/results";
import { Group, User } from "@/interfaces/user";
import { sessionOptions } from "@/lib/session";
import { mapBy, redirect, serialize } from "@/utils";
import { requestUser } from "@/utils/api";
import {getAssignment} from "@/utils/assignments.be";
import {getEntitiesWithRoles} from "@/utils/entities.be";
import {getGroups, getGroupsByEntities} from "@/utils/groups.be";
import {checkAccess, doesEntityAllow, findAllowedEntities} from "@/utils/permissions";
import {calculateAverageLevel} from "@/utils/score";
import {getEntitiesUsers, getUsers} from "@/utils/users.be";
import { getAssignment } from "@/utils/assignments.be";
import { getEntitiesWithRoles } from "@/utils/entities.be";
import { getGroups, getGroupsByEntities } from "@/utils/groups.be";
import { checkAccess, doesEntityAllow, findAllowedEntities } from "@/utils/permissions";
import { calculateAverageLevel } from "@/utils/score";
import { getEntitiesUsers, getUsers } from "@/utils/users.be";
import axios from "axios";
import clsx from "clsx";
import {withIronSessionSsr} from "iron-session/next";
import {capitalize} from "lodash";
import { withIronSessionSsr } from "iron-session/next";
import { capitalize } from "lodash";
import moment from "moment";
import Head from "next/head";
import Link from "next/link";
import {useRouter} from "next/router";
import {generate} from "random-words";
import {useEffect, useMemo, useState} from "react";
import { useRouter } from "next/router";
import { generate } from "random-words";
import { useEffect, useMemo, useState } from "react";
import ReactDatePicker from "react-datepicker";
import {BsBook, BsCheckCircle, BsChevronLeft, BsClipboard, BsHeadphones, BsMegaphone, BsPen, BsXCircle} from "react-icons/bs";
import {toast} from "react-toastify";
import { BsBook, BsCheckCircle, BsChevronLeft, BsClipboard, BsHeadphones, BsMegaphone, BsPen, BsXCircle } from "react-icons/bs";
import { toast } from "react-toastify";
export const getServerSideProps = withIronSessionSsr(async ({req, res, params}) => {
export const getServerSideProps = withIronSessionSsr(async ({ req, res, params }) => {
const user = await requestUser(req, res)
if (!user) return redirect("/login")
res.setHeader("Cache-Control", "public, s-maxage=10, stale-while-revalidate=59");
const {id} = params as {id: string};
const { id } = params as { id: string };
const entityIDS = mapBy(user.entities, "id") || [];
const assignment = await getAssignment(id);
@@ -59,7 +59,7 @@ export const getServerSideProps = withIronSessionSsr(async ({req, res, params})
const users = await (checkAccess(user, ["developer", "admin"]) ? getUsers() : getEntitiesUsers(mapBy(allowedEntities, 'id')));
const groups = await (checkAccess(user, ["developer", "admin"]) ? getGroups() : getGroupsByEntities(mapBy(allowedEntities, 'id')));
return {props: serialize({user, users, entities: allowedEntities, assignment, groups})};
return { props: serialize({ user, users, entities: allowedEntities, assignment, groups }) };
}, sessionOptions);
interface Props {
@@ -72,7 +72,7 @@ interface Props {
const SIZE = 9;
export default function AssignmentsPage({assignment, user, users, entities, groups}: Props) {
export default function AssignmentsPage({ assignment, user, users, entities, groups }: Props) {
const [selectedModules, setSelectedModules] = useState<Module[]>(assignment.exams.map((e) => e.module));
const [assignees, setAssignees] = useState<string[]>(assignment.assignees);
const [teachers, setTeachers] = useState<string[]>(assignment.teachers || []);
@@ -90,12 +90,11 @@ export default function AssignmentsPage({assignment, user, users, entities, grou
const [released, setReleased] = useState<boolean>(assignment.released || false);
const [autoStart, setAutostart] = useState<boolean>(assignment.autoStart || false);
const [autoStartDate, setAutoStartDate] = useState<Date | null>(moment(assignment.autoStartDate).toDate());
const [useRandomExams, setUseRandomExams] = useState(true);
const [examIDs, setExamIDs] = useState<{id: string; module: Module}[]>([]);
const [examIDs, setExamIDs] = useState<{ id: string; module: Module }[]>([]);
const {exams} = useExams();
const { exams } = useExams();
const router = useRouter();
const classrooms = useMemo(() => groups.filter((e) => e.entity === entity), [entity, groups]);
@@ -103,11 +102,11 @@ export default function AssignmentsPage({assignment, user, users, entities, grou
const userStudents = useMemo(() => users.filter((x) => x.type === "student"), [users]);
const userTeachers = useMemo(() => users.filter((x) => x.type === "teacher"), [users]);
const {rows: filteredStudentsRows, renderSearch: renderStudentSearch} = useListSearch([["name"], ["email"]], userStudents);
const {rows: filteredTeachersRows, renderSearch: renderTeacherSearch} = useListSearch([["name"], ["email"]], userTeachers);
const { rows: filteredStudentsRows, renderSearch: renderStudentSearch } = useListSearch([["name"], ["email"]], userStudents);
const { rows: filteredTeachersRows, renderSearch: renderTeacherSearch } = useListSearch([["name"], ["email"]], userTeachers);
const {items: studentRows, renderMinimal: renderStudentPagination} = usePagination(filteredStudentsRows, SIZE);
const {items: teacherRows, renderMinimal: renderTeacherPagination} = usePagination(filteredTeachersRows, SIZE);
const { items: studentRows, renderMinimal: renderStudentPagination } = usePagination(filteredStudentsRows, SIZE);
const { items: teacherRows, renderMinimal: renderTeacherPagination } = usePagination(filteredTeachersRows, SIZE);
useEffect(() => {
setExamIDs((prev) => prev.filter((x) => selectedModules.includes(x.module)));
@@ -148,7 +147,6 @@ export default function AssignmentsPage({assignment, user, users, entities, grou
instructorGender,
released,
autoStart,
autoStartDate,
})
.then(() => {
toast.success(`The assignment "${name}" has been updated successfully!`);
@@ -316,9 +314,9 @@ export default function AssignmentsPage({assignment, user, users, entities, grou
<Input type="text" name="name" onChange={(e) => setName(e)} defaultValue={name} label="Assignment Name" required />
<Select
label="Entity"
options={entities.map((e) => ({value: e.id, label: e.label}))}
options={entities.map((e) => ({ value: e.id, label: e.label }))}
onChange={(v) => setEntity(v ? v.value! : undefined)}
defaultValue={{value: entities[0]?.id, label: entities[0]?.label}}
defaultValue={{ value: entities[0]?.id, label: entities[0]?.label }}
/>
</div>
@@ -355,24 +353,6 @@ export default function AssignmentsPage({assignment, user, users, entities, grou
onChange={(date) => setEndDate(date)}
/>
</div>
{autoStart && (
<div className="flex flex-col gap-2">
<label className="font-normal text-base text-mti-gray-dim">Automatic Start Date *</label>
<ReactDatePicker
className={clsx(
"p-6 w-full min-h-[70px] flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
"hover:border-mti-purple tooltip z-10",
"transition duration-300 ease-in-out",
)}
popperClassName="!z-20"
filterTime={(date) => moment(date).isSameOrAfter(new Date())}
dateFormat="dd/MM/yyyy HH:mm"
selected={autoStartDate}
showTimeSelect
onChange={(date) => setAutoStartDate(date)}
/>
</div>
)}
</div>
{selectedModules.includes("speaking") && (
@@ -386,9 +366,9 @@ export default function AssignmentsPage({assignment, user, users, entities, grou
onChange={(value) => (value ? setInstructorGender(value.value as InstructorGender) : null)}
disabled={!selectedModules.includes("speaking") || !!assignment}
options={[
{value: "male", label: "Male"},
{value: "female", label: "Female"},
{value: "varied", label: "Varied"},
{ value: "male", label: "Male" },
{ value: "female", label: "Female" },
{ value: "varied", label: "Varied" },
]}
/>
</div>
@@ -412,14 +392,14 @@ export default function AssignmentsPage({assignment, user, users, entities, grou
onChange={(value) =>
value
? setExamIDs((prev) => [
...prev.filter((x) => x.module !== module),
{id: value.value!, module},
])
...prev.filter((x) => x.module !== module),
{ id: value.value!, module },
])
: setExamIDs((prev) => prev.filter((x) => x.module !== module))
}
options={exams
.filter((x) => !x.isDiagnostic && x.module === module)
.map((x) => ({value: x.id, label: x.id}))}
.map((x) => ({ value: x.id, label: x.id }))}
/>
</div>
))}
@@ -446,7 +426,7 @@ export default function AssignmentsPage({assignment, user, users, entities, grou
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light",
"transition duration-300 ease-in-out",
users.filter((u) => g.participants.includes(u.id)).every((u) => assignees.includes(u.id)) &&
"!bg-mti-purple-light !text-white",
"!bg-mti-purple-light !text-white",
)}>
{g.name}
</button>
@@ -510,7 +490,7 @@ export default function AssignmentsPage({assignment, user, users, entities, grou
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light",
"transition duration-300 ease-in-out",
users.filter((u) => g.participants.includes(u.id)).every((u) => teachers.includes(u.id)) &&
"!bg-mti-purple-light !text-white",
"!bg-mti-purple-light !text-white",
)}>
{g.name}
</button>

View File

@@ -6,37 +6,37 @@ import ProgressBar from "@/components/Low/ProgressBar";
import Select from "@/components/Low/Select";
import Separator from "@/components/Low/Separator";
import useExams from "@/hooks/useExams";
import {useListSearch} from "@/hooks/useListSearch";
import { useListSearch } from "@/hooks/useListSearch";
import usePagination from "@/hooks/usePagination";
import {Module} from "@/interfaces";
import {EntityWithRoles, WithEntity} from "@/interfaces/entity";
import {InstructorGender, Variant} from "@/interfaces/exam";
import {Assignment} from "@/interfaces/results";
import {Group, User} from "@/interfaces/user";
import {sessionOptions} from "@/lib/session";
import {mapBy, redirect, serialize} from "@/utils";
import { Module } from "@/interfaces";
import { EntityWithRoles, WithEntity } from "@/interfaces/entity";
import { InstructorGender, Variant } from "@/interfaces/exam";
import { Assignment } from "@/interfaces/results";
import { Group, User } from "@/interfaces/user";
import { sessionOptions } from "@/lib/session";
import { mapBy, redirect, serialize } from "@/utils";
import { requestUser } from "@/utils/api";
import {getEntitiesWithRoles} from "@/utils/entities.be";
import {getGroups, getGroupsByEntities} from "@/utils/groups.be";
import {checkAccess, findAllowedEntities} from "@/utils/permissions";
import {calculateAverageLevel} from "@/utils/score";
import { getEntitiesWithRoles } from "@/utils/entities.be";
import { getGroups, getGroupsByEntities } from "@/utils/groups.be";
import { checkAccess, findAllowedEntities } from "@/utils/permissions";
import { calculateAverageLevel } from "@/utils/score";
import { isAdmin } from "@/utils/users";
import {getEntitiesUsers, getUsers} from "@/utils/users.be";
import { getEntitiesUsers, getUsers } from "@/utils/users.be";
import axios from "axios";
import clsx from "clsx";
import {withIronSessionSsr} from "iron-session/next";
import {capitalize} from "lodash";
import { withIronSessionSsr } from "iron-session/next";
import { capitalize } from "lodash";
import moment from "moment";
import Head from "next/head";
import Link from "next/link";
import {useRouter} from "next/router";
import {generate} from "random-words";
import {useEffect, useMemo, useState} from "react";
import { useRouter } from "next/router";
import { generate } from "random-words";
import { useEffect, useMemo, useState } from "react";
import ReactDatePicker from "react-datepicker";
import {BsBook, BsCheckCircle, BsChevronLeft, BsClipboard, BsHeadphones, BsMegaphone, BsPen, BsXCircle} from "react-icons/bs";
import {toast} from "react-toastify";
import { BsBook, BsCheckCircle, BsChevronLeft, BsClipboard, BsHeadphones, BsMegaphone, BsPen, BsXCircle } from "react-icons/bs";
import { toast } from "react-toastify";
export const getServerSideProps = withIronSessionSsr(async ({req, res}) => {
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
const user = await requestUser(req, res)
if (!user) return redirect("/login")
@@ -49,7 +49,7 @@ export const getServerSideProps = withIronSessionSsr(async ({req, res}) => {
const users = await (isAdmin(user) ? getUsers() : getEntitiesUsers(mapBy(allowedEntities, 'id')));
const groups = await (isAdmin(user) ? getGroups() : getGroupsByEntities(mapBy(allowedEntities, 'id')));
return {props: serialize({user, users, entities, groups})};
return { props: serialize({ user, users, entities, groups }) };
}, sessionOptions);
interface Props {
@@ -62,7 +62,7 @@ interface Props {
const SIZE = 9;
export default function AssignmentsPage({user, users, groups, entities}: Props) {
export default function AssignmentsPage({ user, users, groups, entities }: Props) {
const [selectedModules, setSelectedModules] = useState<Module[]>([]);
const [assignees, setAssignees] = useState<string[]>([]);
const [teachers, setTeachers] = useState<string[]>([...(user.type === "teacher" ? [user.id] : [])]);
@@ -88,12 +88,11 @@ export default function AssignmentsPage({user, users, groups, entities}: Props)
const [released, setReleased] = useState<boolean>(false);
const [autoStart, setAutostart] = useState<boolean>(false);
const [autoStartDate, setAutoStartDate] = useState<Date | null>(new Date());
const [useRandomExams, setUseRandomExams] = useState(true);
const [examIDs, setExamIDs] = useState<{id: string; module: Module}[]>([]);
const [examIDs, setExamIDs] = useState<{ id: string; module: Module }[]>([]);
const {exams} = useExams();
const { exams } = useExams();
const router = useRouter();
const classrooms = useMemo(() => groups.filter((e) => e.entity?.id === entity), [entity, groups]);
@@ -102,11 +101,11 @@ export default function AssignmentsPage({user, users, groups, entities}: Props)
const userStudents = useMemo(() => allowedUsers.filter((x) => x.type === "student"), [allowedUsers]);
const userTeachers = useMemo(() => allowedUsers.filter((x) => x.type === "teacher"), [allowedUsers]);
const {rows: filteredStudentsRows, renderSearch: renderStudentSearch} = useListSearch([["name"], ["email"]], userStudents);
const {rows: filteredTeachersRows, renderSearch: renderTeacherSearch} = useListSearch([["name"], ["email"]], userTeachers);
const { rows: filteredStudentsRows, renderSearch: renderStudentSearch } = useListSearch([["name"], ["email"]], userStudents);
const { rows: filteredTeachersRows, renderSearch: renderTeacherSearch } = useListSearch([["name"], ["email"]], userTeachers);
const {items: studentRows, renderMinimal: renderStudentPagination} = usePagination(filteredStudentsRows, SIZE);
const {items: teacherRows, renderMinimal: renderTeacherPagination} = usePagination(filteredTeachersRows, SIZE);
const { items: studentRows, renderMinimal: renderStudentPagination } = usePagination(filteredStudentsRows, SIZE);
const { items: teacherRows, renderMinimal: renderTeacherPagination } = usePagination(filteredTeachersRows, SIZE);
useEffect(() => {
setExamIDs((prev) => prev.filter((x) => selectedModules.includes(x.module)));
@@ -148,7 +147,6 @@ export default function AssignmentsPage({user, users, groups, entities}: Props)
instructorGender,
released,
autoStart,
autoStartDate,
})
.then((result) => {
toast.success(`The assignment "${name}" has been created successfully!`);
@@ -274,9 +272,9 @@ export default function AssignmentsPage({user, users, groups, entities}: Props)
<Input type="text" name="name" onChange={(e) => setName(e)} defaultValue={name} label="Assignment Name" required />
<Select
label="Entity"
options={entities.map((e) => ({value: e.id, label: e.label}))}
options={entities.map((e) => ({ value: e.id, label: e.label }))}
onChange={(v) => setEntity(v ? v.value! : undefined)}
defaultValue={{value: entities[0]?.id, label: entities[0]?.label}}
defaultValue={{ value: entities[0]?.id, label: entities[0]?.label }}
/>
</div>
@@ -313,24 +311,6 @@ export default function AssignmentsPage({user, users, groups, entities}: Props)
onChange={(date) => setEndDate(date)}
/>
</div>
{autoStart && (
<div className="flex flex-col gap-2">
<label className="font-normal text-base text-mti-gray-dim">Automatic Start Date *</label>
<ReactDatePicker
className={clsx(
"p-6 w-full min-h-[70px] flex justify-center text-sm font-normal rounded-full border focus:outline-none cursor-pointer",
"hover:border-mti-purple tooltip z-10",
"transition duration-300 ease-in-out",
)}
popperClassName="!z-20"
filterTime={(date) => moment(date).isSameOrAfter(new Date())}
dateFormat="dd/MM/yyyy HH:mm"
selected={autoStartDate}
showTimeSelect
onChange={(date) => setAutoStartDate(date)}
/>
</div>
)}
</div>
{selectedModules.includes("speaking") && (
@@ -344,9 +324,9 @@ export default function AssignmentsPage({user, users, groups, entities}: Props)
onChange={(value) => (value ? setInstructorGender(value.value as InstructorGender) : null)}
disabled={!selectedModules.includes("speaking")}
options={[
{value: "male", label: "Male"},
{value: "female", label: "Female"},
{value: "varied", label: "Varied"},
{ value: "male", label: "Male" },
{ value: "female", label: "Female" },
{ value: "varied", label: "Varied" },
]}
/>
</div>
@@ -370,14 +350,14 @@ export default function AssignmentsPage({user, users, groups, entities}: Props)
onChange={(value) =>
value
? setExamIDs((prev) => [
...prev.filter((x) => x.module !== module),
{id: value.value!, module},
])
...prev.filter((x) => x.module !== module),
{ id: value.value!, module },
])
: setExamIDs((prev) => prev.filter((x) => x.module !== module))
}
options={exams
.filter((x) => !x.isDiagnostic && x.module === module)
.map((x) => ({value: x.id, label: x.id}))}
.map((x) => ({ value: x.id, label: x.id }))}
/>
</div>
))}
@@ -404,7 +384,7 @@ export default function AssignmentsPage({user, users, groups, entities}: Props)
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light",
"transition duration-300 ease-in-out",
users.filter((u) => g.participants.includes(u.id)).every((u) => assignees.includes(u.id)) &&
"!bg-mti-purple-light !text-white",
"!bg-mti-purple-light !text-white",
)}>
{g.name}
</button>
@@ -468,7 +448,7 @@ export default function AssignmentsPage({user, users, groups, entities}: Props)
"bg-mti-purple-ultralight text-mti-purple px-4 py-2 rounded-full hover:text-white hover:bg-mti-purple-light",
"transition duration-300 ease-in-out",
users.filter((u) => g.participants.includes(u.id)).every((u) => teachers.includes(u.id)) &&
"!bg-mti-purple-light !text-white",
"!bg-mti-purple-light !text-white",
)}>
{g.name}
</button>

View File

@@ -1,11 +1,11 @@
/* eslint-disable @next/next/no-img-element */
import {withIronSessionSsr} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import {shouldRedirectHome} from "@/utils/navigation.disabled";
import { withIronSessionSsr } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import { shouldRedirectHome } from "@/utils/navigation.disabled";
import ExamPage from "./(exam)/ExamPage";
import Head from "next/head";
import {User} from "@/interfaces/user";
import { User } from "@/interfaces/user";
import { filterBy, findBy, redirect, serialize } from "@/utils";
import { requestUser } from "@/utils/api";
import { getAssignment, getAssignments, getAssignmentsByAssignee } from "@/utils/assignments.be";
@@ -20,36 +20,38 @@ import { useRouter } from "next/router";
import { getSessionByAssignment, getSessionsByUser } from "@/utils/sessions.be";
import { Session } from "@/hooks/useSessions";
import moment from "moment";
import { activeAssignmentFilter } from "@/utils/assignments";
export const getServerSideProps = withIronSessionSsr(async ({req, res, query}) => {
export const getServerSideProps = withIronSessionSsr(async ({ req, res, query }) => {
const user = await requestUser(req, res)
const destination = Buffer.from(req.url || "/").toString("base64")
if (!user) return redirect(`/login?destination=${destination}`)
const loginDestination = Buffer.from(req.url || "/").toString("base64")
if (!user) return redirect(`/login?destination=${loginDestination}`)
if (shouldRedirectHome(user)) return redirect("/")
const {assignment: assignmentID} = query as {assignment?: string}
const { assignment: assignmentID, destination } = query as { assignment?: string, destination?: string }
const destinationURL = !!destination ? Buffer.from(destination, 'base64').toString() : undefined
if (assignmentID) {
const assignment = await getAssignment(assignmentID)
if (!assignment) return redirect("/exam")
if (!assignment) return redirect(destinationURL || "/exam")
if (!assignment.assignees.includes(user.id) && !["admin", "developer"].includes(user.type))
return redirect("/exam")
return redirect(destinationURL || "/exam")
if (filterBy(assignment.results, 'user', user.id).length > 0)
return redirect("/exam")
return redirect(destinationURL || "/exam")
const exams = await getExamsByIds(uniqBy(assignment.exams, "id"))
const session = await getSessionByAssignment(assignmentID)
return {
props: serialize({user, assignment, exams, session: session ?? undefined})
props: serialize({ user, assignment, exams, destinationURL, session: session ?? undefined })
}
}
return {
props: serialize({user}),
props: serialize({ user, destinationURL }),
};
}, sessionOptions);
@@ -58,16 +60,17 @@ interface Props {
assignment?: Assignment
exams?: Exam[]
session?: Session
destinationURL?: string
}
export default function Page({user, assignment, exams = [], session}: Props) {
export default function Page({ user, assignment, exams = [], destinationURL = "/exam", session }: Props) {
const router = useRouter()
const state = useExamStore((state) => state)
useEffect(() => {
if (assignment && exams.length > 0 && !state.assignment && !session) {
if ((moment(assignment.startDate).isAfter(moment()) || moment(assignment.endDate).isBefore(moment())) && assignment.start) return
if (!activeAssignmentFilter(assignment)) return
state.setUserSolutions([]);
state.setShowSolutions(false);
@@ -82,12 +85,12 @@ export default function Page({user, assignment, exams = [], session}: Props) {
router.replace(router.asPath)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [assignment, exams, session])
useEffect(() => {
if (assignment && exams.length > 0 && !state.assignment && !!session) {
state.setShuffles(session.userSolutions.map((x) => ({exerciseID: x.exercise, shuffles: x.shuffleMaps ? x.shuffleMaps : []})));
state.setShuffles(session.userSolutions.map((x) => ({ exerciseID: x.exercise, shuffles: x.shuffleMaps ? x.shuffleMaps : [] })));
state.setSelectedModules(session.selectedModules);
state.setExam(session.exam);
state.setExams(session.exams);
@@ -103,7 +106,7 @@ export default function Page({user, assignment, exams = [], session}: Props) {
router.replace(router.asPath)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [assignment, exams, session])
return (
@@ -117,7 +120,7 @@ export default function Page({user, assignment, exams = [], session}: Props) {
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<ExamPage page="exams" user={user} hideSidebar={!!assignment || !!state.assignment} />
<ExamPage page="exams" destination={destinationURL} user={user} hideSidebar={!!assignment || !!state.assignment} />
</>
);
}

View File

@@ -10,31 +10,57 @@ import clsx from "clsx";
import { MODULE_ARRAY } from "@/utils/moduleUtils";
import { capitalize } from "lodash";
import Input from "@/components/Low/Input";
import { checkAccess } from "@/utils/permissions";
import { checkAccess, findAllowedEntities } from "@/utils/permissions";
import { User } from "@/interfaces/user";
import useExamEditorStore from "@/stores/examEditor";
import ExamEditorStore from "@/stores/examEditor/types";
import ExamEditor from "@/components/ExamEditor";
import MultipleAudioUploader from "@/components/ExamEditor/Shared/AudioEdit";
import { redirect, serialize } from "@/utils";
import { mapBy, redirect, serialize } from "@/utils";
import { requestUser } from "@/utils/api";
import { Module } from "@/interfaces";
import { getExam, getExams } from "@/utils/exams.be";
import { Exam, Exercise, InteractiveSpeakingExercise, ListeningPart, SpeakingExercise } from "@/interfaces/exam";
import { useEffect } from "react";
import { Exercise, InteractiveSpeakingExercise, ListeningPart, SpeakingExercise } from "@/interfaces/exam";
import { getEntitiesWithRoles } from "@/utils/entities.be";
import { isAdmin } from "@/utils/users";
import axios from "axios";
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
type Permission = { [key in Module]: boolean }
export const getServerSideProps = withIronSessionSsr(async ({ req, res, query }) => {
const user = await requestUser(req, res)
if (!user) return redirect("/login")
if (shouldRedirectHome(user) || !checkAccess(user, ["admin", "mastercorporate", "developer", "corporate"]))
return redirect("/")
if (shouldRedirectHome(user)) return redirect("/")
const entityIDs = mapBy(user.entities, 'id')
const entities = await getEntitiesWithRoles(isAdmin(user) ? undefined : entityIDs)
const permissions: Permission = {
reading: findAllowedEntities(user, entities, `generate_reading`).length > 0,
listening: findAllowedEntities(user, entities, `generate_listening`).length > 0,
writing: findAllowedEntities(user, entities, `generate_writing`).length > 0,
speaking: findAllowedEntities(user, entities, `generate_speaking`).length > 0,
level: findAllowedEntities(user, entities, `generate_level`).length > 0,
}
if (Object.keys(permissions).every(p => !permissions[p as Module])) return redirect("/")
const { id, module } = query as { id?: string, module?: Module }
if (!id || !module) return { props: serialize({ user, permissions }) };
if (!permissions[module]) return redirect("/generation")
const exam = await getExam(module, id)
if (!exam) return redirect("/generation")
return {
props: serialize({ user }),
props: serialize({ user, exam, permissions }),
};
}, sessionOptions);
export default function Generation({ user }: { user: User; }) {
export default function Generation({ user, exam, permissions }: { user: User; exam?: Exam, permissions: Permission }) {
const { title, currentModule, modules, dispatch } = useExamEditorStore();
const updateRoot = (updates: Partial<ExamEditorStore>) => {
@@ -97,6 +123,9 @@ export default function Generation({ user }: { user: User; }) {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (exam) { }
}, [exam])
return (
<>
@@ -130,7 +159,7 @@ export default function Generation({ user }: { user: User; }) {
value={currentModule}
onChange={(currentModule) => updateRoot({ currentModule })}
className="flex flex-row -2xl:flex-wrap w-full gap-4 -md:justify-center justify-between">
{[...MODULE_ARRAY].map((x) => (
{[...MODULE_ARRAY].filter(m => permissions[m]).map((x) => (
<Radio value={x} key={x}>
{({ checked }) => (
<span

View File

@@ -4,34 +4,34 @@ import Layout from "@/components/High/Layout";
import Button from "@/components/Low/Button";
import Separator from "@/components/Low/Separator";
import ProfileSummary from "@/components/ProfileSummary";
import {Session} from "@/hooks/useSessions";
import {Grading} from "@/interfaces";
import {EntityWithRoles} from "@/interfaces/entity";
import {Exam} from "@/interfaces/exam";
import { Session } from "@/hooks/useSessions";
import { Grading } from "@/interfaces";
import { EntityWithRoles } from "@/interfaces/entity";
import { Exam } from "@/interfaces/exam";
import { InviteWithEntity } from "@/interfaces/invite";
import {Assignment} from "@/interfaces/results";
import {Stat, User} from "@/interfaces/user";
import {sessionOptions} from "@/lib/session";
import { Assignment } from "@/interfaces/results";
import { Stat, User } from "@/interfaces/user";
import { sessionOptions } from "@/lib/session";
import useExamStore from "@/stores/examStore";
import {findBy, mapBy, redirect, serialize} from "@/utils";
import { filterBy, findBy, mapBy, redirect, serialize } from "@/utils";
import { requestUser } from "@/utils/api";
import {activeAssignmentFilter} from "@/utils/assignments";
import {getAssignmentsByAssignee} from "@/utils/assignments.be";
import {getEntitiesWithRoles} from "@/utils/entities.be";
import {getExamsByIds} from "@/utils/exams.be";
import {sortByModule} from "@/utils/moduleUtils";
import {checkAccess} from "@/utils/permissions";
import {getSessionsByUser} from "@/utils/sessions.be";
import { activeAssignmentFilter, futureAssignmentFilter } from "@/utils/assignments";
import { getAssignmentsByAssignee } from "@/utils/assignments.be";
import { getEntitiesWithRoles } from "@/utils/entities.be";
import { getExamsByIds } from "@/utils/exams.be";
import { sortByModule } from "@/utils/moduleUtils";
import { checkAccess } from "@/utils/permissions";
import { getSessionsByUser } from "@/utils/sessions.be";
import axios from "axios";
import clsx from "clsx";
import {withIronSessionSsr} from "iron-session/next";
import {uniqBy} from "lodash";
import { withIronSessionSsr } from "iron-session/next";
import { uniqBy } from "lodash";
import moment from "moment";
import Head from "next/head";
import {useRouter} from "next/router";
import { useRouter } from "next/router";
import { useMemo, useState } from "react";
import {BsArrowRepeat} from "react-icons/bs";
import {ToastContainer} from "react-toastify";
import { BsArrowRepeat } from "react-icons/bs";
import { ToastContainer } from "react-toastify";
interface Props {
user: User;
@@ -44,7 +44,7 @@ interface Props {
grading: Grading;
}
export const getServerSideProps = withIronSessionSsr(async ({req, res}) => {
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
const user = await requestUser(req, res)
const destination = Buffer.from(req.url || "/").toString("base64")
if (!user) return redirect(`/login?destination=${destination}`)
@@ -55,21 +55,23 @@ export const getServerSideProps = withIronSessionSsr(async ({req, res}) => {
const entityIDS = mapBy(user.entities, "id") || [];
const entities = await getEntitiesWithRoles(entityIDS);
const assignments = await getAssignmentsByAssignee(user.id, {archived: {$ne: true}});
const assignments = await getAssignmentsByAssignee(user.id, { archived: { $ne: true } });
const sessions = await getSessionsByUser(user.id, 0, { "assignment.id": { $in: mapBy(assignments, 'id') } });
const examIDs = uniqBy(
assignments.flatMap((a) =>
a.exams.filter((e) => e.assignee === user.id).map((e) => ({module: e.module, id: e.id, key: `${e.module}_${e.id}`})),
filterBy(a.exams, 'assignee', user.id).map((e) => ({ module: e.module, id: e.id, key: `${e.module}_${e.id}` })),
),
"key",
);
const exams = await getExamsByIds(examIDs);
return {props: serialize({user, entities, assignments, exams, sessions})};
return { props: serialize({ user, entities, assignments, exams, sessions }) };
}, sessionOptions);
export default function OfficialExam({user, entities, assignments, sessions, exams}: Props) {
const destination = Buffer.from("/official-exam").toString("base64")
export default function OfficialExam({ user, entities, assignments, sessions, exams }: Props) {
const [isLoading, setIsLoading] = useState(false)
const router = useRouter();
@@ -93,12 +95,12 @@ export default function OfficialExam({user, entities, assignments, sessions, exa
state.setSelectedModules(mapBy(assignmentExams.sort(sortByModule), 'module'));
state.setAssignment(assignment);
router.push("/exam");
router.push(`/exam?assignment=${assignment.id}&destination=${destination}`);
}
};
const loadSession = async (session: Session) => {
state.setShuffles(session.userSolutions.map((x) => ({exerciseID: x.exercise, shuffles: x.shuffleMaps ? x.shuffleMaps : []})));
state.setShuffles(session.userSolutions.map((x) => ({ exerciseID: x.exercise, shuffles: x.shuffleMaps ? x.shuffleMaps : [] })));
state.setSelectedModules(session.selectedModules);
state.setExam(session.exam);
state.setExams(session.exams);
@@ -112,16 +114,20 @@ export default function OfficialExam({user, entities, assignments, sessions, exa
state.setShowSolutions(false);
state.setQuestionIndex(session.questionIndex);
router.push("/exam");
router.push(`/exam?assignment=${session.assignment?.id}&destination=${destination}`);
};
const logout = async () => {
axios.post("/api/logout").finally(() => {
setTimeout(() => router.reload(), 500);
});
};
const logout = async () => {
axios.post("/api/logout").finally(() => {
setTimeout(() => router.reload(), 500);
});
};
const studentAssignments = useMemo(() => [
...assignments.filter(activeAssignmentFilter), ...assignments.filter(futureAssignmentFilter)],
[assignments]
);
const studentAssignments = useMemo(() => assignments.filter(activeAssignmentFilter), [assignments]);
const assignmentSessions = useMemo(() => sessions.filter(s => mapBy(studentAssignments, 'id').includes(s.assignment?.id || "")), [sessions, studentAssignments])
return (