From 4917583c677fb2692e10e53560c01a96134c06c0 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Thu, 17 Oct 2024 18:24:39 +0100 Subject: [PATCH] Created a system to go directly to an assignment from a URL --- src/components/High/Layout.tsx | 32 ++++++----- src/pages/(exam)/ExamPage.tsx | 21 ++++---- src/pages/assignments/[id].tsx | 3 +- src/pages/assignments/index.tsx | 9 ---- src/pages/classrooms/create.tsx | 14 ++--- src/pages/exam.tsx | 93 ++++++++++++++++++++++++++++++-- src/pages/exercises.tsx | 94 ++++++++++++++++++++++++++++++--- src/pages/login.tsx | 15 +++--- src/utils/exams.be.ts | 3 +- src/utils/sessions.be.ts | 5 ++ 10 files changed, 232 insertions(+), 57 deletions(-) diff --git a/src/components/High/Layout.tsx b/src/components/High/Layout.tsx index eb72323f..b4a3dbc6 100644 --- a/src/components/High/Layout.tsx +++ b/src/components/High/Layout.tsx @@ -11,36 +11,42 @@ interface Props { className?: string; navDisabled?: boolean; focusMode?: boolean; + hideSidebar?: boolean bgColor?: string; onFocusLayerMouseEnter?: () => void; } -export default function Layout({user, children, className, bgColor="bg-white", navDisabled = false, focusMode = false, onFocusLayerMouseEnter}: Props) { +export default function Layout({user, children, className, bgColor="bg-white", hideSidebar, navDisabled = false, focusMode = false, onFocusLayerMouseEnter}: Props) { const router = useRouter(); return (
- -
- + )} +
+ {!hideSidebar && ( + + )}
{children} diff --git a/src/pages/(exam)/ExamPage.tsx b/src/pages/(exam)/ExamPage.tsx index d9150598..2f080c77 100644 --- a/src/pages/(exam)/ExamPage.tsx +++ b/src/pages/(exam)/ExamPage.tsx @@ -25,13 +25,16 @@ import useSessions from "@/hooks/useSessions"; import ShortUniqueId from "short-unique-id"; import clsx from "clsx"; import useGradingSystem from "@/hooks/useGrading"; +import { Assignment } from "@/interfaces/results"; +import { mapBy } from "@/utils"; interface Props { page: "exams" | "exercises"; user: User; + hideSidebar?: boolean } -export default function ExamPage({page, user}: Props) { +export default function ExamPage({page, user, hideSidebar = false}: Props) { const [variant, setVariant] = useState("full"); const [avoidRepeated, setAvoidRepeated] = useState(false); const [hasBeenUploaded, setHasBeenUploaded] = useState(false); @@ -210,15 +213,14 @@ export default function ExamPage({page, user}: Props) { }, [setModuleIndex, showSolutions]); useEffect(() => { - (async () => { - if (selectedModules.length > 0 && exams.length > 0 && moduleIndex < selectedModules.length) { - const nextExam = exams[moduleIndex]; + console.log(selectedModules) + if (selectedModules.length > 0 && exams.length > 0 && moduleIndex < selectedModules.length) { + const nextExam = exams[moduleIndex]; - if (partIndex === -1 && nextExam?.module !== "listening") setPartIndex(0); - if (exerciseIndex === -1 && !["reading", "listening"].includes(nextExam?.module)) setExerciseIndex(0); - setExam(nextExam ? updateExamWithUserSolutions(nextExam) : undefined); - } - })(); + if (partIndex === -1 && nextExam?.module !== "listening") setPartIndex(0); + if (exerciseIndex === -1 && !["reading", "listening"].includes(nextExam?.module)) setExerciseIndex(0); + setExam(nextExam ? updateExamWithUserSolutions(nextExam) : undefined); + } // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedModules, moduleIndex, exams]); @@ -520,6 +522,7 @@ export default function ExamPage({page, user}: Props) { setShowAbandonPopup(true)}> diff --git a/src/pages/assignments/[id].tsx b/src/pages/assignments/[id].tsx index 9cafc5ba..07e2f010 100644 --- a/src/pages/assignments/[id].tsx +++ b/src/pages/assignments/[id].tsx @@ -52,7 +52,8 @@ export const getServerSideProps = withIronSessionSsr(async ({req, res, params}) const entity = await getEntityWithRoles(assignment.entity || "") if (!entity) return redirect("/assignments") - if (!doesEntityAllow(user, entity, 'view_assignments')) return redirect("/assignments") + if (!doesEntityAllow(user, entity, 'view_assignments') && !["admin", "developer"].includes(user.type)) + return redirect("/assignments") const users = await (checkAccess(user, ["developer", "admin"]) ? getUsers() : getEntityUsers(entity.id)); diff --git a/src/pages/assignments/index.tsx b/src/pages/assignments/index.tsx index 6a33c1ca..33e2ca69 100644 --- a/src/pages/assignments/index.tsx +++ b/src/pages/assignments/index.tsx @@ -119,15 +119,6 @@ export default function AssignmentsPage({assignments, corporateAssignments, enti Total: {activeAssignments.reduce((acc, curr) => acc + curr.results.length, 0)}/ {activeAssignments.reduce((acc, curr) => curr.exams.length + acc, 0)} - {Object.keys(groupBy(corporateAssignments, (x) => x.corporate?.id)).map((x) => ( -
- {getUserCompanyName(users.find((u) => u.id === x)!, users, groups)}: - - {groupBy(corporateAssignments, (x) => x.corporate?.id)[x].reduce((acc, curr) => curr.results.length + acc, 0)}/ - {groupBy(corporateAssignments, (x) => x.corporate?.id)[x].reduce((acc, curr) => curr.exams.length + acc, 0)} - -
- ))}
diff --git a/src/pages/classrooms/create.tsx b/src/pages/classrooms/create.tsx index 2a61bd18..c62682e8 100644 --- a/src/pages/classrooms/create.tsx +++ b/src/pages/classrooms/create.tsx @@ -9,11 +9,11 @@ import {Entity, EntityWithRoles} from "@/interfaces/entity"; import {User} from "@/interfaces/user"; import {sessionOptions} from "@/lib/session"; import {USER_TYPE_LABELS} from "@/resources/user"; -import {mapBy, redirect, serialize} from "@/utils"; +import {filterBy, mapBy, redirect, serialize} from "@/utils"; import {getEntities, getEntitiesWithRoles} from "@/utils/entities.be"; import {shouldRedirectHome} from "@/utils/navigation.disabled"; import {getUserName} from "@/utils/users"; -import {getLinkedUsers} from "@/utils/users.be"; +import {getEntitiesUsers, getLinkedUsers} from "@/utils/users.be"; import axios from "axios"; import clsx from "clsx"; import {withIronSessionSsr} from "iron-session/next"; @@ -22,7 +22,7 @@ import Head from "next/head"; import Link from "next/link"; import {useRouter} from "next/router"; import {Divider} from "primereact/divider"; -import {useState} from "react"; +import {useMemo, useState} from "react"; import {BsCheck, BsChevronLeft, BsClockFill, BsEnvelopeFill, BsStopwatchFill} from "react-icons/bs"; import {toast, ToastContainer} from "react-toastify"; import { requestUser } from "@/utils/api"; @@ -34,12 +34,12 @@ export const getServerSideProps = withIronSessionSsr(async ({req, res}) => { if (shouldRedirectHome(user)) return redirect("/") - const linkedUsers = await getLinkedUsers(user.id, user.type); + const users = await getEntitiesUsers(mapBy(user.entities, 'id')) const entities = await getEntitiesWithRoles(mapBy(user.entities, "id")); const allowedEntities = findAllowedEntities(user, entities, "create_classroom") return { - props: serialize({user, entities: allowedEntities, users: linkedUsers.users.filter((x) => x.id !== user.id)}), + props: serialize({user, entities: allowedEntities, users: users.filter((x) => x.id !== user.id)}), }; }, sessionOptions); @@ -55,7 +55,9 @@ export default function Home({user, users, entities}: Props) { const [name, setName] = useState(""); const [entity, setEntity] = useState(entities[0]?.id); - const {rows, renderSearch} = useListSearch([["name"], ["corporateInformation", "companyInformation", "name"]], users); + const entityUsers = useMemo(() => !entity ? users : users.filter(u => mapBy(u.entities, 'id').includes(entity)), [entity, users]) + + const {rows, renderSearch} = useListSearch([["name"], ["corporateInformation", "companyInformation", "name"]], entityUsers); const {items, renderMinimal} = usePagination(rows, 16); const router = useRouter(); diff --git a/src/pages/exam.tsx b/src/pages/exam.tsx index c8ecfba7..e8f4440e 100644 --- a/src/pages/exam.tsx +++ b/src/pages/exam.tsx @@ -6,15 +6,52 @@ import {shouldRedirectHome} from "@/utils/navigation.disabled"; import ExamPage from "./(exam)/ExamPage"; import Head from "next/head"; import {User} from "@/interfaces/user"; -import { redirect, serialize } from "@/utils"; +import { filterBy, findBy, redirect, serialize } from "@/utils"; import { requestUser } from "@/utils/api"; +import { getAssignment, getAssignments, getAssignmentsByAssignee } from "@/utils/assignments.be"; +import { Assignment } from "@/interfaces/results"; +import useExamStore from "@/stores/examStore"; +import { useEffect } from "react"; +import { Exam } from "@/interfaces/exam"; +import { getExamsByIds } from "@/utils/exams.be"; +import { sortByModule } from "@/utils/moduleUtils"; +import { uniqBy } from "lodash"; +import { useRouter } from "next/router"; +import { getSessionByAssignment, getSessionsByUser } from "@/utils/sessions.be"; +import { Session } from "@/hooks/useSessions"; +import moment from "moment"; -export const getServerSideProps = withIronSessionSsr(async ({req, res}) => { +export const getServerSideProps = withIronSessionSsr(async ({req, res, query}) => { const user = await requestUser(req, res) - if (!user) return redirect("/login") + const destination = Buffer.from(req.url || "/").toString("base64") + if (!user) return redirect(`/login?destination=${destination}`) if (shouldRedirectHome(user)) return redirect("/") + const {assignment: assignmentID} = query as {assignment?: string} + + if (assignmentID) { + const assignment = await getAssignment(assignmentID) + + if (!assignment) return redirect("/exam") + if (!assignment.assignees.includes(user.id) && !["admin", "developer"].includes(user.type)) + return redirect("/exam") + + const exams = await getExamsByIds(uniqBy(assignment.exams, "id")) + const session = await getSessionByAssignment(assignmentID) + + if ( + filterBy(assignment.results, 'user', user.id).length > 0 || + moment(assignment.startDate).isAfter(moment()) || + moment(assignment.endDate).isBefore(moment()) + ) + return redirect("/exam") + + return { + props: serialize({user, assignment, exams, session}) + } + } + return { props: serialize({user}), }; @@ -22,9 +59,55 @@ export const getServerSideProps = withIronSessionSsr(async ({req, res}) => { interface Props { user: User; + assignment?: Assignment + exams?: Exam[] + session?: Session } -export default function Page({user}: Props) { +export default function Page({user, assignment, exams = [], session}: Props) { + const router = useRouter() + + const state = useExamStore((state) => state) + + useEffect(() => { + if (assignment && exams.length > 0 && !state.assignment && !session) { + state.setUserSolutions([]); + state.setShowSolutions(false); + state.setAssignment(assignment); + state.setExams(exams.sort(sortByModule)); + state.setSelectedModules( + exams + .map((x) => x!) + .sort(sortByModule) + .map((x) => x!.module), + ); + + router.replace(router.asPath) + } + // 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.setSelectedModules(session.selectedModules); + state.setExam(session.exam); + state.setExams(session.exams); + state.setSessionId(session.sessionId); + state.setAssignment(session.assignment); + state.setExerciseIndex(session.exerciseIndex); + state.setPartIndex(session.partIndex); + state.setModuleIndex(session.moduleIndex); + state.setTimeSpent(session.timeSpent); + state.setUserSolutions(session.userSolutions); + state.setShowSolutions(false); + state.setQuestionIndex(session.questionIndex); + + router.replace(router.asPath) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [assignment, exams, session]) + return ( <> @@ -36,7 +119,7 @@ export default function Page({user}: Props) { - + ); } diff --git a/src/pages/exercises.tsx b/src/pages/exercises.tsx index 29034f93..87585ade 100644 --- a/src/pages/exercises.tsx +++ b/src/pages/exercises.tsx @@ -6,15 +6,51 @@ import {shouldRedirectHome} from "@/utils/navigation.disabled"; import ExamPage from "./(exam)/ExamPage"; import Head from "next/head"; import {User} from "@/interfaces/user"; -import { redirect, serialize } from "@/utils"; +import { filterBy, findBy, redirect, serialize } from "@/utils"; import { requestUser } from "@/utils/api"; +import { getAssignment, getAssignments, getAssignmentsByAssignee } from "@/utils/assignments.be"; +import { Assignment } from "@/interfaces/results"; +import useExamStore from "@/stores/examStore"; +import { useEffect } from "react"; +import { Exam } from "@/interfaces/exam"; +import { getExamsByIds } from "@/utils/exams.be"; +import { sortByModule } from "@/utils/moduleUtils"; +import { uniqBy } from "lodash"; +import { useRouter } from "next/router"; +import { getSessionByAssignment, getSessionsByUser } from "@/utils/sessions.be"; +import { Session } from "@/hooks/useSessions"; +import moment from "moment"; -export const getServerSideProps = withIronSessionSsr(async ({req, res}) => { +export const getServerSideProps = withIronSessionSsr(async ({req, res, query}) => { const user = await requestUser(req, res) - if (!user) return redirect("/login") + const destination = Buffer.from(req.url || "/").toString("base64") + if (!user) return redirect(`/login?destination=${destination}`) if (shouldRedirectHome(user)) return redirect("/") + const {assignment: assignmentID} = query as {assignment?: string} + + if (assignmentID) { + const assignment = await getAssignment(assignmentID) + + if (!assignment) return redirect("/exercises") + if (!["admin", "developer"].includes(user.type) && !assignment.assignees.includes(user.id)) return redirect("/exercises") + + const exams = await getExamsByIds(uniqBy(assignment.exams, "id")) + const session = await getSessionByAssignment(assignmentID) + + if ( + filterBy(assignment.results, 'user', user.id) || + moment(assignment.startDate).isBefore(moment()) || + moment(assignment.endDate).isAfter(moment()) + ) + return redirect("/exercises") + + return { + props: serialize({user, assignment, exams, session}) + } + } + return { props: serialize({user}), }; @@ -22,13 +58,59 @@ export const getServerSideProps = withIronSessionSsr(async ({req, res}) => { interface Props { user: User; + assignment?: Assignment + exams?: Exam[] + session?: Session } -export default function Page({user}: Props) { +export default function Page({user, assignment, exams = [], session}: Props) { + const router = useRouter() + + const state = useExamStore((state) => state) + + useEffect(() => { + if (assignment && exams.length > 0 && !state.assignment && !session) { + state.setUserSolutions([]); + state.setShowSolutions(false); + state.setAssignment(assignment); + state.setExams(exams.sort(sortByModule)); + state.setSelectedModules( + exams + .map((x) => x!) + .sort(sortByModule) + .map((x) => x!.module), + ); + + router.replace(router.asPath) + } + // 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.setSelectedModules(session.selectedModules); + state.setExam(session.exam); + state.setExams(session.exams); + state.setSessionId(session.sessionId); + state.setAssignment(session.assignment); + state.setExerciseIndex(session.exerciseIndex); + state.setPartIndex(session.partIndex); + state.setModuleIndex(session.moduleIndex); + state.setTimeSpent(session.timeSpent); + state.setUserSolutions(session.userSolutions); + state.setShowSolutions(false); + state.setQuestionIndex(session.questionIndex); + + router.replace(router.asPath) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [assignment, exams, session]) + return ( <> - Exercises | EnCoach + Exams | EnCoach - + ); } diff --git a/src/pages/login.tsx b/src/pages/login.tsx index 7510d2fc..d03ffe4a 100644 --- a/src/pages/login.tsx +++ b/src/pages/login.tsx @@ -20,16 +20,17 @@ import { redirect } from "@/utils"; const EMAIL_REGEX = new RegExp(/^[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*@[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*$/g); -export const getServerSideProps = withIronSessionSsr(async ({req, res}) => { +export const getServerSideProps = withIronSessionSsr(async ({req, res, query}) => { + const destination = !query.destination ? "/" : Buffer.from(query.destination as string, 'base64').toString() const user = await requestUser(req, res) - if (user) return redirect("/") + if (user) return redirect(destination) return { - props: {user: null}, + props: {user: null, destination}, }; }, sessionOptions); -export default function Login() { +export default function Login({ destination }: { destination: string }) { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [rememberPassword, setRememberPassword] = useState(false); @@ -38,13 +39,13 @@ export default function Login() { const router = useRouter(); const {user, mutateUser} = useUser({ - redirectTo: "/", + redirectTo: destination, redirectIfFound: true, }); useEffect(() => { - if (user) router.push("/"); - }, [router, user]); + if (user) router.push(destination); + }, [router, user, destination]); const forgotPassword = () => { if (!email || email.length < 0 || !EMAIL_REGEX.test(email)) { diff --git a/src/utils/exams.be.ts b/src/utils/exams.be.ts index f9686dc2..dedc4d4c 100644 --- a/src/utils/exams.be.ts +++ b/src/utils/exams.be.ts @@ -8,6 +8,7 @@ import {getUserCorporate} from "./groups.be"; import {Db, ObjectId} from "mongodb"; import client from "@/lib/mongodb"; import {MODULE_ARRAY} from "./moduleUtils"; +import { mapBy } from "."; const db = client.db(process.env.MONGODB_DB); @@ -37,7 +38,7 @@ export const getExamsByIds = async (ids: {module: Module; id: string}[]) => { async (m) => await db .collection(m) - .find({id: {$in: groupedByModule[m]}}) + .find({id: {$in: mapBy(groupedByModule[m], 'id')}}) .toArray(), ), ) diff --git a/src/utils/sessions.be.ts b/src/utils/sessions.be.ts index a6fbe357..ef4e710c 100644 --- a/src/utils/sessions.be.ts +++ b/src/utils/sessions.be.ts @@ -9,3 +9,8 @@ export const getSessionsByUser = async (id: string, limit?: number) => .find({user: id}) .limit(limit || 0) .toArray(); + +export const getSessionByAssignment = async (assignmentID: string) => + await db + .collection("sessions") + .findOne({"assignment.id": assignmentID})