diff --git a/src/interfaces/exam.ts b/src/interfaces/exam.ts
index fe919b0b..fa2a7384 100644
--- a/src/interfaces/exam.ts
+++ b/src/interfaces/exam.ts
@@ -1,4 +1,4 @@
-import { Module } from ".";
+import {Module} from ".";
export type Exam = ReadingExam | ListeningExam | WritingExam | SpeakingExam | LevelExam;
export type Variant = "full" | "partial";
@@ -15,6 +15,7 @@ interface ExamBase {
shuffle?: boolean;
createdBy?: string; // option as it has been added later
createdAt?: string; // option as it has been added later
+ private?: boolean;
}
export interface ReadingExam extends ExamBase {
module: "reading";
@@ -67,7 +68,7 @@ export interface UserSolution {
};
exercise: string;
isDisabled?: boolean;
- shuffleMaps?: ShuffleMap[]
+ shuffleMaps?: ShuffleMap[];
}
export interface WritingExam extends ExamBase {
@@ -99,24 +100,19 @@ export type Exercise =
export interface Evaluation {
comment: string;
overall: number;
- task_response: { [key: string]: number | { grade: number; comment: string } };
- misspelled_pairs?: { correction: string | null; misspelled: string }[];
+ task_response: {[key: string]: number | {grade: number; comment: string}};
+ misspelled_pairs?: {correction: string | null; misspelled: string}[];
}
-
type InteractivePerfectAnswerKey = `perfect_answer_${number}`;
type InteractiveTranscriptKey = `transcript_${number}`;
type InteractiveFixedTextKey = `fixed_text_${number}`;
-type InteractivePerfectAnswerType = { [key in InteractivePerfectAnswerKey]: { answer: string } };
-type InteractiveTranscriptType = { [key in InteractiveTranscriptKey]?: string };
-type InteractiveFixedTextType = { [key in InteractiveFixedTextKey]?: string };
-
-interface InteractiveSpeakingEvaluation extends Evaluation,
- InteractivePerfectAnswerType,
- InteractiveTranscriptType,
- InteractiveFixedTextType { }
+type InteractivePerfectAnswerType = {[key in InteractivePerfectAnswerKey]: {answer: string}};
+type InteractiveTranscriptType = {[key in InteractiveTranscriptKey]?: string};
+type InteractiveFixedTextType = {[key in InteractiveFixedTextKey]?: string};
+interface InteractiveSpeakingEvaluation extends Evaluation, InteractivePerfectAnswerType, InteractiveTranscriptType, InteractiveFixedTextType {}
interface SpeakingEvaluation extends CommonEvaluation {
perfect_answer_1?: string;
@@ -189,10 +185,10 @@ export interface InteractiveSpeakingExercise {
first_title?: string;
second_title?: string;
text: string;
- prompts: { text: string; video_url: string }[];
+ prompts: {text: string; video_url: string}[];
userSolutions: {
id: string;
- solution: { questionIndex: number; question: string; answer: string }[];
+ solution: {questionIndex: number; question: string; answer: string}[];
evaluation?: InteractiveSpeakingEvaluation;
}[];
topic?: string;
@@ -208,14 +204,14 @@ export interface FillBlanksMCOption {
B: string;
C: string;
D: string;
- }
+ };
}
export interface FillBlanksExercise {
prompt: string; // *EXAMPLE: "Complete the summary below. Click a blank to select the corresponding word for it."
type: "fillBlanks";
id: string;
- words: (string | { letter: string; word: string } | FillBlanksMCOption)[]; // *EXAMPLE: ["preserve", "unaware"]
+ words: (string | {letter: string; word: string} | FillBlanksMCOption)[]; // *EXAMPLE: ["preserve", "unaware"]
text: string; // *EXAMPLE: "They tried to {{1}} burning"
allowRepetition?: boolean;
solutions: {
@@ -234,7 +230,7 @@ export interface TrueFalseExercise {
id: string;
prompt: string; // *EXAMPLE: "Select the appropriate option."
questions: TrueFalseQuestion[];
- userSolutions: { id: string; solution: "true" | "false" | "not_given" }[];
+ userSolutions: {id: string; solution: "true" | "false" | "not_given"}[];
}
export interface TrueFalseQuestion {
@@ -263,7 +259,7 @@ export interface MatchSentencesExercise {
type: "matchSentences";
id: string;
prompt: string;
- userSolutions: { question: string; option: string }[];
+ userSolutions: {question: string; option: string}[];
sentences: MatchSentenceExerciseSentence[];
allowRepetition: boolean;
options: MatchSentenceExerciseOption[];
@@ -286,7 +282,7 @@ export interface MultipleChoiceExercise {
id: string;
prompt: string; // *EXAMPLE: "Select the appropriate option."
questions: MultipleChoiceQuestion[];
- userSolutions: { question: string; option: string }[];
+ userSolutions: {question: string; option: string}[];
}
export interface MultipleChoiceQuestion {
@@ -306,10 +302,10 @@ export interface ShuffleMap {
questionID: string;
map: {
[key: string]: string;
- }
+ };
}
export interface Shuffles {
exerciseID: string;
- shuffles: ShuffleMap[]
+ shuffles: ShuffleMap[];
}
diff --git a/src/interfaces/results.ts b/src/interfaces/results.ts
index dcb04183..f14c0481 100644
--- a/src/interfaces/results.ts
+++ b/src/interfaces/results.ts
@@ -33,4 +33,4 @@ export interface Assignment {
start?: boolean;
}
-export type AssignmentWithCorporateId = Assignment & { corporateId: string };
+export type AssignmentWithCorporateId = Assignment & {corporateId: string};
diff --git a/src/pages/(admin)/Lists/ExamList.tsx b/src/pages/(admin)/Lists/ExamList.tsx
index 93ffb05c..2c990677 100644
--- a/src/pages/(admin)/Lists/ExamList.tsx
+++ b/src/pages/(admin)/Lists/ExamList.tsx
@@ -13,7 +13,7 @@ import axios from "axios";
import clsx from "clsx";
import {capitalize} from "lodash";
import {useRouter} from "next/router";
-import {BsCheck, BsTrash, BsUpload} from "react-icons/bs";
+import {BsBan, BsBanFill, BsCheck, BsCircle, BsStop, BsTrash, BsUpload, BsX} from "react-icons/bs";
import {toast} from "react-toastify";
import {useListSearch} from "@/hooks/useListSearch";
@@ -72,6 +72,28 @@ export default function ExamList({user}: {user: User}) {
router.push("/exercises");
};
+ const privatizeExam = async (exam: Exam) => {
+ if (!confirm(`Are you sure you want to make this ${capitalize(exam.module)} exam ${exam.private ? "public" : "private"}?`)) return;
+
+ axios
+ .patch(`/api/exam/${exam.module}/${exam.id}`, {private: !exam.private})
+ .then(() => toast.success(`Updated the "${exam.id}" exam`))
+ .catch((reason) => {
+ if (reason.response.status === 404) {
+ toast.error("Exam not found!");
+ return;
+ }
+
+ if (reason.response.status === 403) {
+ toast.error("You do not have permission to update this exam!");
+ return;
+ }
+
+ toast.error("Something went wrong, please try again later.");
+ })
+ .finally(reload);
+ };
+
const deleteExam = async (exam: Exam) => {
if (!confirm(`Are you sure you want to delete this ${capitalize(exam.module)} exam?`)) return;
@@ -119,6 +141,10 @@ export default function ExamList({user}: {user: User}) {
header: "Timer",
cell: (info) => <>{info.getValue()} minute(s)>,
}),
+ columnHelper.accessor("private", {
+ header: "Private",
+ cell: (info) =>
{!info.getValue() ? : } ,
+ }),
columnHelper.accessor("createdAt", {
header: "Created At",
cell: (info) => {
@@ -140,12 +166,18 @@ export default function ExamList({user}: {user: User}) {
cell: ({row}: {row: {original: Exam}}) => {
return (
-
await privatizeExam(row.original)}
+ className="cursor-pointer tooltip">
+ {row.original.private ? : }
+
+ await loadExam(row.original.module, row.original.id)}>
-
+
{PERMISSIONS.examManagement.delete.includes(user.type) && (
deleteExam(row.original)}>
diff --git a/src/pages/(exam)/ExamPage.tsx b/src/pages/(exam)/ExamPage.tsx
index 7b0169e0..ad18ed05 100644
--- a/src/pages/(exam)/ExamPage.tsx
+++ b/src/pages/(exam)/ExamPage.tsx
@@ -459,6 +459,7 @@ export default function ExamPage({page}: Props) {
user={user!}
modules={selectedModules}
solutions={userSolutions}
+ assignment={assignment}
information={{
timeSpent,
inactivity: totalInactivity,
diff --git a/src/pages/(generation)/LevelGeneration.tsx b/src/pages/(generation)/LevelGeneration.tsx
index 09b1615a..480ca61a 100644
--- a/src/pages/(generation)/LevelGeneration.tsx
+++ b/src/pages/(generation)/LevelGeneration.tsx
@@ -1,6 +1,7 @@
import FillBlanksEdit from "@/components/Generation/fill.blanks.edit";
import MultipleChoiceEdit from "@/components/Generation/multiple.choice.edit";
import WriteBlankEdits from "@/components/Generation/write.blanks.edit";
+import Checkbox from "@/components/Low/Checkbox";
import Input from "@/components/Low/Input";
import Select from "@/components/Low/Select";
import {
@@ -234,7 +235,7 @@ interface Props {
id: string;
}
-const LevelGeneration = ({ id } : Props) => {
+const LevelGeneration = ({id}: Props) => {
const [generatedExam, setGeneratedExam] = useState
();
const [isLoading, setIsLoading] = useState(false);
const [resultingExam, setResultingExam] = useState();
@@ -242,6 +243,7 @@ const LevelGeneration = ({ id } : Props) => {
const [difficulty, setDifficulty] = useState(sample(DIFFICULTIES)!);
const [numberOfParts, setNumberOfParts] = useState(1);
const [parts, setParts] = useState([{quantity: 10, type: "multiple_choice_4"}]);
+ const [isPrivate, setPrivate] = useState(false);
useEffect(() => {
setParts((prev) => Array.from(Array(numberOfParts)).map((_, i) => (!!prev.at(i) ? prev.at(i)! : {quantity: 10, type: "multiple_choice_4"})));
@@ -301,6 +303,7 @@ const LevelGeneration = ({ id } : Props) => {
difficulty,
variant: "full",
isDiagnostic: false,
+ private: isPrivate,
parts: parts
.map((part, index) => {
const currentExercise = result.data.exercises[`exercise_${index + 1}`] as any;
@@ -424,7 +427,7 @@ const LevelGeneration = ({ id } : Props) => {
return;
}
- if(!id) {
+ if (!id) {
toast.error("Please insert a title before submitting");
return;
}
@@ -456,8 +459,8 @@ const LevelGeneration = ({ id } : Props) => {
return (
<>
-
-
+
+
Difficulty
({
@@ -468,14 +471,20 @@ const LevelGeneration = ({ id } : Props) => {
value={{value: difficulty, label: capitalize(difficulty)}}
/>
-
+
Number of Parts
setNumberOfParts(parseInt(v))} value={numberOfParts} />
-
+
Timer (in minutes)
setTimer(parseInt(v))} value={timer} />
+
+
+
+ Privacy (Only available for Assignments)
+
+
diff --git a/src/pages/(generation)/ListeningGeneration.tsx b/src/pages/(generation)/ListeningGeneration.tsx
index 5388b183..ef17c4ca 100644
--- a/src/pages/(generation)/ListeningGeneration.tsx
+++ b/src/pages/(generation)/ListeningGeneration.tsx
@@ -16,6 +16,7 @@ import {BsArrowRepeat, BsCheck} from "react-icons/bs";
import {toast} from "react-toastify";
import WriteBlanksEdit from "@/components/Generation/write.blanks.edit";
import {generate} from "random-words";
+import Checkbox from "@/components/Low/Checkbox";
const DIFFICULTIES: Difficulty[] = ["easy", "medium", "hard"];
@@ -232,7 +233,7 @@ interface Props {
id: string;
}
-const ListeningGeneration = ({ id } : Props) => {
+const ListeningGeneration = ({id}: Props) => {
const [part1, setPart1] = useState();
const [part2, setPart2] = useState();
const [part3, setPart3] = useState();
@@ -241,6 +242,7 @@ const ListeningGeneration = ({ id } : Props) => {
const [isLoading, setIsLoading] = useState(false);
const [resultingExam, setResultingExam] = useState();
const [difficulty, setDifficulty] = useState(sample(DIFFICULTIES)!);
+ const [isPrivate, setPrivate] = useState(false);
useEffect(() => {
const part1Timer = part1 ? 5 : 0;
@@ -262,11 +264,11 @@ const ListeningGeneration = ({ id } : Props) => {
console.log({parts});
if (parts.length === 0) return toast.error("Please generate at least one section!");
- if(!id) {
+ if (!id) {
toast.error("Please insert a title before submitting");
return;
}
-
+
setIsLoading(true);
axios
@@ -275,6 +277,7 @@ const ListeningGeneration = ({ id } : Props) => {
parts,
minTimer,
difficulty,
+ private: isPrivate,
})
.then((result) => {
playSound("sent");
@@ -313,7 +316,7 @@ const ListeningGeneration = ({ id } : Props) => {
return (
<>
-
+
Timer
{
className="max-w-[300px]"
/>
-
+
Difficulty
({
@@ -336,6 +339,12 @@ const ListeningGeneration = ({ id } : Props) => {
disabled={!!part1 || !!part2 || !!part3 || !!part4}
/>
+
+
+
+ Privacy (Only available for Assignments)
+
+
diff --git a/src/pages/(generation)/ReadingGeneration.tsx b/src/pages/(generation)/ReadingGeneration.tsx
index 6bd81dde..51019a71 100644
--- a/src/pages/(generation)/ReadingGeneration.tsx
+++ b/src/pages/(generation)/ReadingGeneration.tsx
@@ -20,6 +20,7 @@ import TrueFalseEdit from "@/components/Generation/true.false.edit";
import WriteBlanksEdit from "@/components/Generation/write.blanks.edit";
import MatchSentencesEdit from "@/components/Generation/match.sentences.edit";
import MultipleChoiceEdit from "@/components/Generation/multiple.choice.edit";
+import Checkbox from "@/components/Low/Checkbox";
const DIFFICULTIES: Difficulty[] = ["easy", "medium", "hard"];
@@ -262,7 +263,7 @@ interface Props {
id: string;
}
-const ReadingGeneration = ({ id } : Props) => {
+const ReadingGeneration = ({id}: Props) => {
const [part1, setPart1] = useState();
const [part2, setPart2] = useState();
const [part3, setPart3] = useState();
@@ -270,6 +271,7 @@ const ReadingGeneration = ({ id } : Props) => {
const [isLoading, setIsLoading] = useState(false);
const [resultingExam, setResultingExam] = useState();
const [difficulty, setDifficulty] = useState(sample(DIFFICULTIES)!);
+ const [isPrivate, setPrivate] = useState(false);
useEffect(() => {
const parts = [part1, part2, part3].filter((x) => !!x);
@@ -304,7 +306,7 @@ const ReadingGeneration = ({ id } : Props) => {
return;
}
- if(!id) {
+ if (!id) {
toast.error("Please insert a title before submitting");
return;
}
@@ -319,6 +321,7 @@ const ReadingGeneration = ({ id } : Props) => {
type: "academic",
variant: parts.length === 3 ? "full" : "partial",
difficulty,
+ private: isPrivate,
};
axios
@@ -344,7 +347,7 @@ const ReadingGeneration = ({ id } : Props) => {
return (
<>
-
+
Timer
{
className="max-w-[300px]"
/>
-
+
Difficulty
({
@@ -367,6 +370,12 @@ const ReadingGeneration = ({ id } : Props) => {
disabled={!!part1 || !!part2 || !!part3}
/>
+
+
+
+ Privacy (Only available for Assignments)
+
+
diff --git a/src/pages/(generation)/SpeakingGeneration.tsx b/src/pages/(generation)/SpeakingGeneration.tsx
index c4959f6c..28aaa8a2 100644
--- a/src/pages/(generation)/SpeakingGeneration.tsx
+++ b/src/pages/(generation)/SpeakingGeneration.tsx
@@ -1,3 +1,4 @@
+import Checkbox from "@/components/Low/Checkbox";
import Input from "@/components/Low/Input";
import Select from "@/components/Low/Select";
import {Difficulty, Exercise, InteractiveSpeakingExercise, SpeakingExam, SpeakingExercise} from "@/interfaces/exam";
@@ -225,7 +226,7 @@ interface Props {
id: string;
}
-const SpeakingGeneration = ({ id } : Props) => {
+const SpeakingGeneration = ({id}: Props) => {
const [part1, setPart1] = useState();
const [part2, setPart2] = useState();
const [part3, setPart3] = useState();
@@ -233,6 +234,7 @@ const SpeakingGeneration = ({ id } : Props) => {
const [isLoading, setIsLoading] = useState(false);
const [resultingExam, setResultingExam] = useState();
const [difficulty, setDifficulty] = useState(sample(DIFFICULTIES)!);
+ const [isPrivate, setPrivate] = useState(false);
useEffect(() => {
const parts = [part1, part2, part3].filter((x) => !!x);
@@ -247,11 +249,11 @@ const SpeakingGeneration = ({ id } : Props) => {
const submitExam = () => {
if (!part1?.result && !part2?.result && !part3?.result) return toast.error("Please generate at least one task!");
- if(!id) {
+ if (!id) {
toast.error("Please insert a title before submitting");
return;
}
-
+
setIsLoading(true);
const genders = [part1?.gender, part2?.gender, part3?.gender].filter((x) => !!x);
@@ -272,6 +274,7 @@ const SpeakingGeneration = ({ id } : Props) => {
variant: minTimer >= 14 ? "full" : "partial",
module: "speaking",
instructorGender: genders.every((x) => x === "male") ? "male" : genders.every((x) => x === "female") ? "female" : "varied",
+ private: isPrivate,
};
axios
@@ -313,7 +316,7 @@ const SpeakingGeneration = ({ id } : Props) => {
return (
<>
-
+
Timer
{
className="max-w-[300px]"
/>
-
+
Difficulty
({
@@ -336,6 +339,13 @@ const SpeakingGeneration = ({ id } : Props) => {
disabled={!!part1 || !!part2 || !!part3}
/>
+
+
+
+
+ Privacy (Only available for Assignments)
+
+
diff --git a/src/pages/(generation)/WritingGeneration.tsx b/src/pages/(generation)/WritingGeneration.tsx
index be4e12cd..84115b00 100644
--- a/src/pages/(generation)/WritingGeneration.tsx
+++ b/src/pages/(generation)/WritingGeneration.tsx
@@ -1,3 +1,4 @@
+import Checkbox from "@/components/Low/Checkbox";
import Input from "@/components/Low/Input";
import Select from "@/components/Low/Select";
import {Difficulty, WritingExam, WritingExercise} from "@/interfaces/exam";
@@ -79,13 +80,14 @@ interface Props {
id: string;
}
-const WritingGeneration = ({ id } : Props) => {
+const WritingGeneration = ({id}: Props) => {
const [task1, setTask1] = useState();
const [task2, setTask2] = useState();
const [minTimer, setMinTimer] = useState(60);
const [isLoading, setIsLoading] = useState(false);
const [resultingExam, setResultingExam] = useState();
const [difficulty, setDifficulty] = useState(sample(DIFFICULTIES)!);
+ const [isPrivate, setPrivate] = useState(false);
useEffect(() => {
const task1Timer = task1 ? 20 : 0;
@@ -120,7 +122,7 @@ const WritingGeneration = ({ id } : Props) => {
return;
}
- if(!id) {
+ if (!id) {
toast.error("Please insert a title before submitting");
return;
}
@@ -164,6 +166,7 @@ const WritingGeneration = ({ id } : Props) => {
id,
variant: exercise1 && exercise2 ? "full" : "partial",
difficulty,
+ private: isPrivate,
};
axios
@@ -188,7 +191,7 @@ const WritingGeneration = ({ id } : Props) => {
return (
<>
-
+
Timer
{
className="max-w-[300px]"
/>
-
+
Difficulty
({value: x, label: capitalize(x)}))}
@@ -208,6 +211,13 @@ const WritingGeneration = ({ id } : Props) => {
disabled={!!task1 || !!task2}
/>
+
+
+
+
+ Privacy (Only available for Assignments)
+
+
diff --git a/src/pages/api/assignments/[id]/[export]/excel.ts b/src/pages/api/assignments/[id]/[export]/excel.ts
index bf19569e..2502f03c 100644
--- a/src/pages/api/assignments/[id]/[export]/excel.ts
+++ b/src/pages/api/assignments/[id]/[export]/excel.ts
@@ -84,9 +84,16 @@ function commonExcel({
.map((assignee: string) => {
const userStats = allStats.filter((s: any) => s.user === assignee);
const dates = userStats.map((s: any) => moment(s.date));
+ const user = users.find((u) => u.id === assignee);
return {
userId: assignee,
- user: users.find((u) => u.id === assignee),
+ // added some default values in case the user is not found
+ // could it be possible to have an assigned user deleted from the database?
+ user: user || {
+ name: "Unknown",
+ email: "Unknown",
+ demographicInformation: { passportId: "Unknown", gender: "Unknown" },
+ },
...userStats.reduce(
(acc: any, curr: any) => {
return {
@@ -152,7 +159,7 @@ function commonExcel({
});
// added empty arrays to force row spacings
- const customTableAndLine = [[],...customTable, []];
+ const customTableAndLine = [[], ...customTable, []];
customTableAndLine.forEach((row: string[], index) => {
worksheet.addRow(row);
});
@@ -188,19 +195,24 @@ function commonExcel({
worksheet.addRow(tableColumnHeaders);
// 1 headers rows
- const startIndexTable = firstSectionData.length + customTableAndLine.length + 1;
+ const startIndexTable =
+ firstSectionData.length + customTableAndLine.length + 1;
// // Merge "Test Sections" over dynamic number of columns
// const tableColumns = staticHeaders.length + numberOfTestSections;
// K10:M12 = 10,11,12,13
// horizontally group Test Sections
- worksheet.mergeCells(
- startIndexTable,
- staticHeaders.length + 1,
- startIndexTable,
- tableColumnHeadersFirstPart.length
- );
+
+ // if there are test section headers to even merge:
+ if (testSectionHeaders.length > 1) {
+ worksheet.mergeCells(
+ startIndexTable,
+ staticHeaders.length + 1,
+ startIndexTable,
+ tableColumnHeadersFirstPart.length
+ );
+ }
// Add the dynamic second and third header rows for test sections and sub-columns
worksheet.addRow([
@@ -229,7 +241,12 @@ function commonExcel({
// vertically group based on the part, exercise and type
staticHeaders.forEach((header, index) => {
- worksheet.mergeCells(startIndexTable, index + 1, startIndexTable + 3, index + 1);
+ worksheet.mergeCells(
+ startIndexTable,
+ index + 1,
+ startIndexTable + 3,
+ index + 1
+ );
});
assigneesData.forEach((data, index) => {
@@ -316,13 +333,17 @@ async function mastercorporateAssignment(
const adminsData = await getSpecificUsers(adminUsers);
const companiesData = adminsData.map((user) => {
const name = getUserName(user);
- const users = userGroupsParticipants
- .filter((p) => data.assignees.includes(p));
+ const users = userGroupsParticipants.filter((p) =>
+ data.assignees.includes(p)
+ );
const stats = data.results
.flatMap((r: any) => r.stats)
.filter((s: any) => users.includes(s.user));
- const correct = stats.reduce((acc: number, s: any) => acc + s.score.correct, 0);
+ const correct = stats.reduce(
+ (acc: number, s: any) => acc + s.score.correct,
+ 0
+ );
const total = stats.reduce(
(acc: number, curr: any) => acc + curr.score.total,
0
@@ -342,9 +363,11 @@ async function mastercorporateAssignment(
correct: companiesData.reduce((acc, curr) => acc + curr.correct, 0),
total: companiesData.reduce((acc, curr) => acc + curr.total, 0),
},
- ].map((c) => [c.name, `${c.correct}/${c.total}`])
+ ].map((c) => [c.name, `${c.correct}/${c.total}`]);
- const customTableHeaders = [{ name: "Corporate", helper: (data: any) => data.user.corporateName}];
+ const customTableHeaders = [
+ { name: "Corporate", helper: (data: any) => data.user.corporateName },
+ ];
return commonExcel({
data,
userName: user.corporateInformation?.companyInformation?.name || "",
@@ -354,12 +377,13 @@ async function mastercorporateAssignment(
return {
...u,
corporateName: getUserName(admin),
- }
+ };
}),
sectionName: "Master Corporate Name :",
- customTable: [['Corporate Summary'], ...customTable],
+ customTable: [["Corporate Summary"], ...customTable],
customTableHeaders: customTableHeaders.map((h) => h.name),
- renderCustomTableData: (data) => customTableHeaders.map((h) => h.helper(data)),
+ renderCustomTableData: (data) =>
+ customTableHeaders.map((h) => h.helper(data)),
});
}
@@ -411,7 +435,11 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
case "corporate":
return corporateAssignment(user as CorporateUser, data, users);
case "mastercorporate":
- return mastercorporateAssignment(user as MasterCorporateUser, data, users);
+ return mastercorporateAssignment(
+ user as MasterCorporateUser,
+ data,
+ users
+ );
default:
throw new Error("Invalid user type");
}
diff --git a/src/pages/api/exam/[module]/[id].ts b/src/pages/api/exam/[module]/[id].ts
index fc80debf..50bf3c7d 100644
--- a/src/pages/api/exam/[module]/[id].ts
+++ b/src/pages/api/exam/[module]/[id].ts
@@ -1,7 +1,7 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type {NextApiRequest, NextApiResponse} from "next";
import {app} from "@/firebase";
-import {getFirestore, doc, getDoc, deleteDoc} from "firebase/firestore";
+import {getFirestore, doc, getDoc, deleteDoc, setDoc} from "firebase/firestore";
import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import {PERMISSIONS} from "@/constants/userPermissions";
@@ -12,6 +12,7 @@ export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "GET") return get(req, res);
+ if (req.method === "PATCH") return patch(req, res);
if (req.method === "DELETE") return del(req, res);
}
@@ -37,6 +38,25 @@ async function get(req: NextApiRequest, res: NextApiResponse) {
}
}
+async function patch(req: NextApiRequest, res: NextApiResponse) {
+ if (!req.session.user) {
+ res.status(401).json({ok: false});
+ return;
+ }
+
+ const {module, id} = req.query as {module: string; id: string};
+
+ const docRef = doc(db, module, id);
+ const docSnap = await getDoc(docRef);
+
+ if (docSnap.exists()) {
+ await setDoc(docRef, req.body, {merge: true});
+ res.status(200).json({ok: true});
+ } else {
+ res.status(404).json({ok: false});
+ }
+}
+
async function del(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ok: false});
diff --git a/src/pages/api/grading/index.ts b/src/pages/api/grading/index.ts
index 70ccbd08..6beb7e24 100644
--- a/src/pages/api/grading/index.ts
+++ b/src/pages/api/grading/index.ts
@@ -10,7 +10,7 @@ import {v4} from "uuid";
import {checkAccess} from "@/utils/permissions";
import {CEFR_STEPS} from "@/resources/grading";
import {getCorporateUser} from "@/resources/user";
-import {getUserCorporate} from "@/utils/groups";
+import {getUserCorporate} from "@/utils/groups.be";
import {Grading} from "@/interfaces";
import {getGroupsForUser} from "@/utils/groups.be";
import {uniq} from "lodash";
diff --git a/src/utils/assignments.ts b/src/utils/assignments.ts
index d4dbc723..941397e3 100644
--- a/src/utils/assignments.ts
+++ b/src/utils/assignments.ts
@@ -1,18 +1,10 @@
import moment from "moment";
-import { Assignment } from "@/interfaces/results";
+import {Assignment} from "@/interfaces/results";
-export const futureAssignmentFilter = (a: Assignment) =>
- moment(a.startDate).isAfter(moment()) && !a.archived && !a.start;
+export const futureAssignmentFilter = (a: Assignment) => moment(a.startDate).isAfter(moment()) && !a.archived;
+
+export const pastAssignmentFilter = (a: Assignment) => moment(a.endDate).isBefore(moment()) && !a.archived;
-export const pastAssignmentFilter = (a: Assignment) =>
- (moment(a.endDate).isBefore(moment()) ||
- a.assignees.length === a.results.length ||
- (moment(a.startDate).isBefore(moment()) && !a.start)) &&
- !a.archived;
export const archivedAssignmentFilter = (a: Assignment) => a.archived;
-export const activeAssignmentFilter = (a: Assignment) =>
- moment(a.endDate).isAfter(moment()) &&
- // && moment(a.startDate).isBefore(moment())
- a.start &&
- a.assignees.length > a.results.length;
+export const activeAssignmentFilter = (a: Assignment) => moment(a.endDate).isAfter(moment()) && moment(a.startDate).isBefore(moment());
diff --git a/src/utils/exams.be.ts b/src/utils/exams.be.ts
index be8d1837..2466c61d 100644
--- a/src/utils/exams.be.ts
+++ b/src/utils/exams.be.ts
@@ -1,4 +1,4 @@
-import {collection, getDocs, query, where, setDoc, doc, Firestore, getDoc} from "firebase/firestore";
+import {collection, getDocs, query, where, setDoc, doc, Firestore, getDoc, and} from "firebase/firestore";
import {shuffle} from "lodash";
import {Difficulty, Exam, InstructorGender, SpeakingExam, Variant, WritingExam} from "@/interfaces/exam";
import {DeveloperUser, Stat, StudentUser, User} from "@/interfaces/user";
@@ -17,7 +17,7 @@ export const getExams = async (
): Promise => {
const moduleRef = collection(db, module);
- const q = query(moduleRef, where("isDiagnostic", "==", false));
+ const q = query(moduleRef, and(where("isDiagnostic", "==", false), where("private", "!=", true)));
const snapshot = await getDocs(q);
const allExams = shuffle(
diff --git a/src/utils/groups.be.ts b/src/utils/groups.be.ts
index 4ece42aa..99929752 100644
--- a/src/utils/groups.be.ts
+++ b/src/utils/groups.be.ts
@@ -33,11 +33,34 @@ export const updateExpiryDateOnGroup = async (participantID: string, corporateID
return;
};
+export const getUserCorporate = async (id: string) => {
+ const user = await getUser(id);
+ if (user.type === "corporate" || user.type === "mastercorporate") return user;
+
+ const groups = await getParticipantGroups(id);
+ const admins = await Promise.all(groups.map((x) => x.admin).map(getUser));
+ const corporates = admins.filter((x) => x.type === "corporate");
+
+ if (corporates.length === 0) return undefined;
+ return corporates.shift() as CorporateUser;
+};
+
export const getGroups = async () => {
const groupDocs = await getDocs(collection(db, "groups"));
return groupDocs.docs.map((x) => ({...x.data(), id: x.id})) as Group[];
};
+export const getParticipantGroups = async (id: string) => {
+ const snapshot = await getDocs(query(collection(db, "groups"), where("participants", "array-contains", id)));
+
+ const groups = snapshot.docs.map((doc) => ({
+ id: doc.id,
+ ...doc.data(),
+ })) as Group[];
+
+ return groups;
+};
+
export const getUserGroups = async (id: string): Promise => {
const groupDocs = await getDocs(query(collection(db, "groups"), where("admin", "==", id)));
return groupDocs.docs.map((x) => ({...x.data(), id})) as Group[];
diff --git a/src/utils/groups.ts b/src/utils/groups.ts
index 1987a2d5..8416683b 100644
--- a/src/utils/groups.ts
+++ b/src/utils/groups.ts
@@ -1,43 +1,37 @@
-import { CorporateUser, Group, User, Type } from "@/interfaces/user";
+import {CorporateUser, Group, User, Type} from "@/interfaces/user";
import axios from "axios";
export const isUserFromCorporate = async (userID: string) => {
- const groups = (await axios.get(`/api/groups?participant=${userID}`))
- .data;
- const users = (await axios.get("/api/users/list")).data;
+ const groups = (await axios.get(`/api/groups?participant=${userID}`)).data;
+ const users = (await axios.get("/api/users/list")).data;
- const adminTypes = groups.map(
- (g) => users.find((u) => u.id === g.admin)?.type
- );
- return adminTypes.includes("corporate");
+ const adminTypes = groups.map((g) => users.find((u) => u.id === g.admin)?.type);
+ return adminTypes.includes("corporate");
};
const getAdminForGroup = async (userID: string, role: Type) => {
- const groups = (await axios.get(`/api/groups?participant=${userID}`))
- .data;
+ const groups = (await axios.get(`/api/groups?participant=${userID}`)).data;
- const adminRequests = await Promise.all(
- groups.map(async (g) => {
- const userRequest = await axios.get(`/api/users/${g.admin}`);
- if (userRequest.status === 200) return userRequest.data;
- return undefined;
- })
- );
+ const adminRequests = await Promise.all(
+ groups.map(async (g) => {
+ const userRequest = await axios.get(`/api/users/${g.admin}`);
+ if (userRequest.status === 200) return userRequest.data;
+ return undefined;
+ }),
+ );
- const admins = adminRequests.filter((x) => x?.type === role);
- return admins.length > 0 ? (admins[0] as CorporateUser) : undefined;
+ const admins = adminRequests.filter((x) => x?.type === role);
+ return admins.length > 0 ? (admins[0] as CorporateUser) : undefined;
};
-export const getUserCorporate = async (
- userID: string
-): Promise => {
- const userRequest = await axios.get(`/api/users/${userID}`);
- if (userRequest.status === 200) {
- const user = userRequest.data;
- if (user.type === "corporate") {
- return getAdminForGroup(userID, "mastercorporate");
- }
- }
+export const getUserCorporate = async (userID: string): Promise => {
+ const userRequest = await axios.get(`/api/users/${userID}`);
+ if (userRequest.status === 200) {
+ const user = userRequest.data;
+ if (user.type === "corporate") {
+ return getAdminForGroup(userID, "mastercorporate");
+ }
+ }
- return getAdminForGroup(userID, "corporate");
+ return getAdminForGroup(userID, "corporate");
};