Untitled
unknown
plain_text
2 years ago
26 kB
7
Indexable
import {
getData,
setData,
DataStore,
Quiz,
User,
ErrorObject,
Question,
Trash,
Answer
} from './dataStore';
import { validQuizCheck, validQuizNameCheck, getRandomColourName } from './other';
/**
* Given basic details about a new quiz, create one for the logged in user
*
* @param {int} authUserId - id of authorised user
* @param {string} name - name of quiz
* @param {string} description - description of quiz
*
*
* @returns {quizId: int} - id of quiz
*/
interface QuizCreateReturn {
quizId: number;
}
function adminQuizCreate(authUserId: number, name: string, description: string): QuizCreateReturn | ErrorObject {
// Get data from dataStore
const data: DataStore = getData();
const user: User = data.users.find((user: User) => user.id === authUserId);
// Checks if AuthUserId is not a valid user.
if (!user) {
return { error: 'AuthUserId is not a valid user.' };
}
// checks if the name is valid and the user does not already have a quiz with
// this name
if (validQuizNameCheck(name, authUserId)) {
return validQuizNameCheck(name, authUserId);
}
// Checks if description is too long
if (description.length > 100) {
return {
error: 'Description is more than 100 characters in length'
};
}
let quizId: number;
if (data.quizzes.length === 0 && data.trash.length === 0) {
quizId = 1;
} else {
const quizIds = data.quizzes.map(ids => ids.id);
const maxInQuizzes = Math.max.apply(null, quizIds);
const trashQuizIds = data.trash.map(ids => ids.quiz.id);
const maxInTrash = Math.max.apply(null, trashQuizIds);
if (maxInQuizzes > maxInTrash) {
quizId = maxInQuizzes + 1;
} else quizId = maxInTrash + 1;
}
// add this quiz to the quizzes array in the dataStore
data.quizzes.push({
id: quizId,
ownerId: authUserId,
name: name,
description: description,
timeCreated: Math.floor(Date.now() / 1000),
timeLastEdited: Math.floor(Date.now() / 1000),
numQuestions: 0,
duration: 0,
questions: [],
});
// add the quizId to the ownedId array of the user
user.ownedQuizIds.push(quizId);
setData(data);
return {
quizId: quizId,
};
}
/**
* Given a particular quiz, move the quiz to the trash.
*
* @param {int} authUserId
* @param {int} quizId
*
* @returns {} - empty object
*
*/
function adminQuizRemove(authUserId: number, quizId: number): Record<string, never> | ErrorObject {
const data: DataStore = getData();
// checks if authUserId and quizId are valid and the quiz belongs to the user
if (validQuizCheck(authUserId, quizId)) {
return validQuizCheck(authUserId, quizId);
}
const quiz: Quiz = data.quizzes.find((quiz: Quiz) => quiz.id === quizId);
// find the user
const user: User = data.users.find((user: User) => user.id === authUserId);
// remove the quiz from the quizzes aray in the dataStore
data.quizzes.splice(
data.quizzes.findIndex((quiz: Quiz) => quiz.id === quizId),
1
);
// remove the quizId from the user's owned quiz Ids
user.ownedQuizIds.splice(
user.ownedQuizIds.findIndex((ownedId: number) => ownedId === quizId),
1
);
quiz.timeLastEdited = Math.floor(Date.now() / 1000);
data.trash.push({
ownerId: authUserId,
quiz: quiz
});
setData(data);
return {};
}
/**
* Get all of the relevant information about the current quiz.
*
* @param {int} authUserId - id of authorised user
* @param {int} quizId - id of quiz
*
* @returns {
* quizId: int,
* name: string,
* timeCreated: int
* timeLastEdited: int
* description: string
* } - information about quiz
*/
interface QuizInfoReturn {
quizId?: number;
name?: string;
timeCreated?: number;
timeLastEdited?: number;
description?: string;
numQuestions?: number,
duration?: number,
questions?: Question[],
error?: string;
}
function adminQuizInfo(authUserId: number, quizId: number): QuizInfoReturn {
const data: DataStore = getData();
// checks if authUserId and quizId are valid and the quiz belongs to the user
if (validQuizCheck(authUserId, quizId)) {
return validQuizCheck(authUserId, quizId);
}
// find the quiz
const foundQuiz: Quiz = data.quizzes.find((quiz: Quiz) => quiz.id === quizId);
// return information about the quiz
return {
quizId: foundQuiz.id,
name: foundQuiz.name,
timeCreated: foundQuiz.timeCreated,
timeLastEdited: foundQuiz.timeLastEdited,
description: foundQuiz.description,
numQuestions: foundQuiz.numQuestions,
duration: foundQuiz.duration,
questions: foundQuiz.questions
};
}
/**
* Update the name of the relevant quiz.
*
* @param {int} authUserId - id of authorised user
* @param {int} quizId - id of quiz
* @param {string} name - name of quiz
*
* @returns { } - empty object
*
*/
function adminQuizNameUpdate(authUserId: number, quizId: number, name: string): Record<string, never> | ErrorObject {
const data: DataStore = getData();
// checks if authUserId and quizId are valid and the quiz belongs to the user
if (validQuizCheck(authUserId, quizId)) {
return validQuizCheck(authUserId, quizId);
}
// checks if the name is valid and the user does not already have a quiz with
// this name
if (validQuizNameCheck(name, authUserId)) {
return validQuizNameCheck(name, authUserId);
}
// find the quiz
const foundQuiz: Quiz = data.quizzes.find((quiz: Quiz) => quiz.id === quizId);
// update name and timeLastEdited of specified quiz
foundQuiz.timeLastEdited = Math.floor(Date.now() / 1000);
foundQuiz.name = name;
setData(data);
return {};
}
/**
* Provide a list of allquizzes that are owned by the currently logged in user.
*
* @param {number} authUserID - authorised user's id stored as an integer
* @returns {quizzes : Array<{
* quizId: number,
* name: string
* }>
* } - an object of an array quizID and name of the quiz
*/
interface QuizList {
quizId: number;
name: string;
}
interface QuizListReturn {
quizzes?: QuizList[];
error?: string;
}
function adminQuizList(authUserId: number): QuizListReturn {
const data: DataStore = getData();
// finds specific user in database
const foundUser = data.users.find((user: User) => user.id === authUserId);
// checks if user exists
if (foundUser === undefined) {
return { error: 'User id does not correspond to a valid user' };
}
// goes through quizzes checking if user owns them and gets its details
const foundQuizzes = [];
for (const quiz of data.quizzes) {
if (foundUser.ownedQuizIds.includes(quiz.id)) {
const quizId: number = quiz.id;
const name: string = quiz.name;
foundQuizzes.push({ quizId: quizId, name: name });
}
}
return {
quizzes: foundQuizzes
};
}
/**
* This function updates the description of the relevant quiz
*
* @param {int} authUserId - id of authorised user
* @param {int} quizId - id of quiz
* @param {string} description - description of quiz
*
* @returns { } - empty object
*
*/
function adminQuizDescriptionUpdate(authUserId: number, quizId: number, description: string): Record<string, never> | ErrorObject {
const data: DataStore = getData();
const foundUser: User = data.users.find((user: User) => user.id === authUserId);
const foundQuiz: Quiz = data.quizzes.find((quiz: Quiz) => quiz.id === quizId);
if (!foundUser) {
return { error: 'authUserId is not a valid user.' };
}
if (!foundQuiz) {
return { error: 'quizId does not refer to a valid quiz.' };
}
if (!foundUser.ownedQuizIds.find((quiz) => quiz === quizId)) {
return { error: 'This user does not own this quiz.' };
}
if (description.length > 100) {
return { error: 'Description is more than 100 characters' };
}
foundQuiz.description = description;
foundQuiz.timeLastEdited = Math.floor(Date.now() / 1000);
setData(data);
return {};
}
/**
* This function transfers ownership of a quiz to a different user based on their email
*
* @param {int} authUserId - id of authorised user
* @param {int} quizId - id of quiz
* @param {string} userEmail - email of user to transfer ownership to
*
* @returns { } - empty object
*
*/
function adminQuizTransfer(authUserId: number, quizId: number, userEmail: string) {
const data: DataStore = getData();
// checks if authUserId and quizId are valid and the quiz belongs to the user
if (validQuizCheck(authUserId, quizId)) {
return validQuizCheck(authUserId, quizId);
}
const foundUser: User = data.users.find((user: User) => user.id === authUserId);
const foundUserToRecieve: User = data.users.find((user: User) => user.email === userEmail);
const foundReplaceQuiz: Quiz = data.quizzes.find((quiz: Quiz) => quiz.id === quizId);
if (!foundUserToRecieve) {
return { error: 'userEmail is not a valid email.' };
}
if (foundUser.email === userEmail) {
return { error: 'userEmail is the current logged in user.' };
}
for (const quiz of foundUserToRecieve.ownedQuizIds) {
const foundQuiz: Quiz = data.quizzes.find((specificQuiz: Quiz) => quiz === specificQuiz.id);
if (foundQuiz.name === foundReplaceQuiz.name) {
return { error: 'Quiz ID refers to a quiz that has a name that is already used by the target user.' };
}
}
// change ownerId of quiz to new owner
foundReplaceQuiz.ownerId = foundUserToRecieve.id;
// add the quizId to the ownedId array of the user
foundUserToRecieve.ownedQuizIds.push(quizId);
// remove the quizId from the user's owned quiz Ids
foundUser.ownedQuizIds.splice(
foundUser.ownedQuizIds.findIndex((ownedId: number) => ownedId === quizId),
1
);
setData(data);
return { };
}
/**
* This function creates a new question and adds it to a quiz
*
* @param {int} authUserId - id of authorised user
* @param {int} quizId - id of quiz
* @param {string} question - question being asked
* @param {int} duration - duration of question
* @param {int} points - how many points the question is worth
* @param {...{object}} points - possible answers to the question
*
* @returns {questionId: int} - id of created question
*
*/
export interface QuestionCreate {
questionId: number;
}
function adminQuestionCreate(authUserId: number, quizId: number, question: string, duration: number, points: number, answers: Answer[]): QuestionCreate | ErrorObject {
const data: DataStore = getData();
const foundUser: User = data.users.find((user: User) => user.id === authUserId);
const foundQuiz: Quiz = data.quizzes.find((quiz: Quiz) => quiz.id === quizId);
if (!foundUser) {
return { error: 'authUserId is not a valid user.' };
}
if (!foundQuiz) {
return { error: 'quizId does not refer to a valid quiz.' };
}
if (!foundUser.ownedQuizIds.includes(quizId)) {
return { error: 'This user does not own this quiz.' };
}
if (question.length < 5 || question.length > 50) {
return { error: 'The question length invalid' };
}
if (duration <= 0) {
return { error: 'The question duration must be a positive number' };
}
if (foundQuiz.duration + duration > 180) {
return { error: 'The duration of the quiz is longer than 3 minutes' };
}
if (points < 1 || points > 10) {
return { error: 'Invalid point distribution' };
}
const answersOnly = answers.map(answer => answer.answer);
const duplicateAnswers = answersOnly.filter((element, index, arr) => arr.indexOf(element) !== index);
if (duplicateAnswers.length > 0) {
return { error: 'There cannot be duplicate answers.' };
}
if (answers.length < 2 || answers.length > 6) {
return { error: 'Invalid amount of answers inputted' };
}
if (!answers.find(answers => answers.correct === true)) {
return { error: 'There must be at least one correct answer' };
}
if (answers.find(answer => answer.answer.length < 1 || answer.answer.length > 30)) {
return { error: 'Invalid answer length' };
}
foundQuiz.timeLastEdited = Math.floor(Date.now() / 1000);
foundQuiz.numQuestions++;
let questionId: number;
if (data.questions.length === 0) {
questionId = 1;
} else questionId = data.questions[data.questions.length - 1].questionId + 1;
let answerId: number;
if (data.answers.length === 0) {
answerId = 1;
} else answerId = data.answers[data.answers.length - 1].answerId + 1;
data.questions.push(
{
ownerId: foundQuiz.ownerId,
quizId: foundQuiz.id,
questionId: questionId
}
);
const allAnswersInfo = answers.map((answer, index) => ({
answerId: answerId + index,
answer: answer.answer,
colour: getRandomColourName(),
correct: answer.correct
}));
for (const answer of allAnswersInfo) {
data.answers.push(answer);
}
foundQuiz.duration += duration;
const questionCreated: Question = {
questionId: questionId,
question: question,
duration: duration,
points: points,
answers: allAnswersInfo
};
foundQuiz.questions.push(questionCreated);
setData(data);
return { questionId: questionId };
}
/**
* This function restores a quiz from the trash back to an active quiz
*
* @param {int} authUserId - id of authorised user
* @param {int} quizId - id of quiz
*
* @returns { } - empty object
*
*/
function adminQuizRestore(authUserId: number, quizId: number) {
const data: DataStore = getData();
// assumed valid user due to token (check spec)
const foundUser: User = data.users.find((user: User) => user.id === authUserId);
const foundQuiz: Quiz = data.quizzes.find((quiz: Quiz) => quiz.id === quizId);
const foundQuizTrash: Trash = data.trash.find((trash: Trash) => trash.quiz.id === quizId);
// Quiz ID does not refer to a valid quiz
if (!foundQuiz && !foundQuizTrash) {
return {
error: 'quizId does not refer to a valid quiz.'
};
}
// Quiz ID does not refer to a quiz that this user owns
if (!foundQuizTrash && foundUser.id !== foundQuiz.ownerId) {
return {
error: 'quizId does not refer to a quiz that this user owns.'
};
}
if (!foundQuiz && foundUser.id !== foundQuizTrash.ownerId) {
return {
error: 'quizId does not refer to a quiz that this user owns.'
};
}
// Quiz ID refers to a quiz that is not currently in the trash
if (foundQuiz) {
return {
error: 'quizId refers to a quiz that is not currently in the trash.'
};
}
// add quiz back to quizzes dataStore
data.quizzes.push(foundQuizTrash.quiz);
// add the quizId from the user's owned quiz Ids
foundUser.ownedQuizIds.push(foundQuizTrash.quiz.id);
// remove quiz from trash
data.trash.splice(data.trash.findIndex((trashId: Trash) => trashId.quiz.id === quizId), 1);
setData(data);
return { };
}
/**
* This function permanently deletes specific quizzes in the trash
*
* @param {int} authUserId - id of authorised user
* @param {string[]} quizIds - id of quizzes
*
* @returns { } - empty object
*
*/
export function trashEmpty(userId: number, quizIds: string[]) {
const data: DataStore = getData();
for (const quiz of quizIds) {
const id = parseInt(quiz);
const foundUser: User | undefined = data.users.find((user: User) => user.id === userId);
const foundQuiz: Quiz | undefined = data.quizzes.find((quiz: Quiz) => quiz.id === id);
const foundQuizTrash: Trash | undefined = data.trash.find((trash: Trash) => trash.quiz.id === id);
// Quiz ID does not refer to a valid quiz
if (!foundQuiz && !foundQuizTrash) {
return {
error: 'Invalid QuizId in Input!'
};
}
// Quiz ID does not refer to a quiz that this user owns
if (!foundQuizTrash && foundUser && foundQuiz && foundUser.id !== foundQuiz.ownerId) {
return {
error: 'Not all quizzes belong to this user!'
};
}
if (!foundQuiz && foundUser && foundQuizTrash && foundUser.id !== foundQuizTrash.ownerId) {
return {
error: 'Not all quizzes belong to this user!'
};
}
// Quiz ID refers to a quiz that is not currently in the trash
if (foundQuiz) {
return {
error: 'Not all quizzes are in the trash!'
};
}
}
for (const exists of quizIds) {
const id = parseInt(exists);
data.trash.splice(data.trash.findIndex((trashId: Trash) => trashId.quiz.id === id), 1);
}
setData(data);
return { };
}
/**
* This function returns a list of all quizzes in the trash
*
* @param {int} authUserId - id of authorised user
*
* @returns {object} - array of quizzes
*/
export interface TrashList {
quizId: number,
name: string,
}
export interface viewTrashReturn {
quizzes: TrashList[]
}
function adminViewTrash(authUserId: number): viewTrashReturn {
const data = getData();
// finds all quizzes that belong to user in trash
const trash = data.trash.filter(trash => trash.ownerId === authUserId);
const trashList = [];
for (const element of trash) {
trashList.push({
quizId: element.quiz.id,
name: element.quiz.name,
});
}
setData(data);
return {
quizzes: trashList
};
}
/**
* This function moves a question in a quiz to a specified position in that quiz
*
* @param {int} authUserId - id of authorised user
* @param {int} quizId - id of quiz
* @param {int} questionId - id of question
* @param {int} newPosition - position question will be moved to
*
* @returns { } - empty object
*
*/
function adminQuizMoveQuestion(authUserId: number, quizId: number, questionId: number, newPosition: number): Record<string, never> | ErrorObject {
const data: DataStore = getData();
// assumed valid user due to token (check spec)
const foundUser: User = data.users.find((user: User) => user.id === authUserId);
const foundQuiz: Quiz = data.quizzes.find((quiz: Quiz) => quiz.id === quizId);
// Quiz ID does not refer to a valid quiz
if (!foundQuiz) {
return {
error: 'quizId does not refer to a valid quiz.'
};
}
// Quiz ID does not refer to a quiz that this user owns
if (foundUser.id !== foundQuiz.ownerId) {
return {
error: 'quizId does not refer to a quiz that this user owns.'
};
}
const foundQuestion: Question = foundQuiz.questions.find((question: Question) => question.questionId === questionId);
if (!foundQuestion) {
return {
error: 'questionId does not refer to a valid question within this quiz.'
};
}
if (newPosition < 0 || newPosition > foundQuiz.questions.length - 1) {
return {
error: 'New position is invalid.',
};
}
const oldPosition = foundQuiz.questions.indexOf(foundQuestion);
if (oldPosition === newPosition) {
return {
error: 'Question is already in that position',
};
}
foundQuiz.questions.splice(oldPosition, 1);
foundQuiz.questions.splice(newPosition, 0, foundQuestion);
foundQuiz.timeLastEdited = Math.floor(Date.now() / 1000);
setData(data);
return { };
}
/**
* This function deletes a question from the quiz
*
* @param {int} authUserId - id of authorised user
* @param {int} quizId - id of quiz
* @param {int} questionId - id of the quiz question
*
* @returns { } - empty object
*
*/
function adminQuestionDelete(authUserId: number, quizId: number, questionId: number): Record<string, never> | ErrorObject {
const data: DataStore = getData();
const foundUser: User = data.users.find((user: User) => user.id === authUserId);
const foundQuiz: Quiz = data.quizzes.find((quiz: Quiz) => quiz.id === quizId);
if (!foundUser) {
return { error: 'authUserId is not a valid user.' };
}
if (!foundQuiz) {
return { error: 'quizId does not refer to a valid quiz' };
}
if (!foundUser.ownedQuizIds.includes(quizId)) {
return { error: 'This user does not own this quiz' };
}
const foundQuestion = foundQuiz.questions.find(question => question.questionId === questionId);
if (!foundQuestion) {
return { error: 'Question Id does not refer to a valid question within this quiz' };
}
for (const answer of foundQuestion.answers) {
const answerToRemove = data.answers.find(foundAnswer => foundAnswer.answerId === answer.answerId);
data.answers.splice(data.answers.indexOf(answerToRemove), 1);
}
const QuestionInData = data.questions.find(question => question.questionId === questionId);
foundQuiz.questions.splice(foundQuiz.questions.indexOf(foundQuestion), 1);
data.questions.splice(data.questions.indexOf(QuestionInData), 1);
foundQuiz.duration -= foundQuestion.duration;
foundQuiz.numQuestions--;
foundQuiz.timeLastEdited = Math.floor(Date.now() / 1000);
setData(data);
return {};
}
/**
* This function creates a new question and adds it to a quiz
*
* @param {int} authUserId - id of authorised user
* @param {int} quizId - id of quiz
* @param {int} questionId - id of question
* @param {string} question - question being asked
* @param {int} duration - duration of question
* @param {int} points - how many points the question is worth
* @param {...{object}} points - possible answers to the question
*
* @returns {}
*
*/
function adminQuestionUpdate(authUserId: number, quizId: number, questionId: number, question: string, duration: number, points: number, answers: Answer[]): Record<string, never> | ErrorObject {
const data: DataStore = getData();
const foundUser: User = data.users.find((user: User) => user.id === authUserId);
const foundQuiz: Quiz = data.quizzes.find((quiz: Quiz) => quiz.id === quizId);
if (!foundQuiz) {
return { error: 'quizId does not refer to a valid quiz.' };
}
if (!foundUser.ownedQuizIds.includes(quizId)) {
return { error: 'This user does not own this quiz.' };
}
if (question.length < 5 || question.length > 50) {
return { error: 'The question length invalid' };
}
if (duration <= 0) {
return { error: 'The question duration must be a positive number' };
}
if (points < 1 || points > 10) {
return { error: 'Invalid point distribution' };
}
const answersOnly = answers.map(answer => answer.answer);
const duplicateAnswers = answersOnly.filter((element, index, arr) => arr.indexOf(element) !== index);
if (duplicateAnswers.length > 0) {
return { error: 'There cannot be duplicate answers.' };
}
if (answers.length < 2 || answers.length > 6) {
return { error: 'Invalid amount of answers inputted' };
}
if (!answers.find(answers => answers.correct === true)) {
return { error: 'There must be at least one correct answer' };
}
if (answers.find(answer => answer.answer.length < 1 || answer.answer.length > 30)) {
return { error: 'Invalid answer length' };
}
const foundQuestion = foundQuiz.questions.find(question => question.questionId === questionId);
if (!foundQuestion) {
return { error: 'Question Id does not refer to a valid question within this quiz' };
}
if (foundQuiz.duration - foundQuestion.duration + duration > 180) {
return { error: 'The duration of the quiz is longer than 3 minutes' };
}
foundQuiz.duration = foundQuiz.duration - foundQuestion.duration + duration;
foundQuiz.timeLastEdited = Math.floor(Date.now() / 1000);
for (const answer of foundQuestion.answers) {
const answerToRemove = data.answers.find(foundAnswer => foundAnswer.answerId === answer.answerId);
data.answers.splice(data.answers.indexOf(answerToRemove), 1);
}
let answerId: number;
if (data.answers.length === 0) {
answerId = 1;
} else answerId = data.answers[data.answers.length - 1].answerId + 1;
const allAnswersInfo = answers.map((answer, index) => ({
answerId: answerId + index,
answer: answer.answer,
colour: getRandomColourName(),
correct: answer.correct
}));
for (const answer of allAnswersInfo) {
data.answers.push(answer);
}
foundQuestion.duration = duration;
foundQuestion.points = points;
foundQuestion.question = question;
foundQuestion.answers = allAnswersInfo;
return {};
}
/**
* This function duplicates a question in a quiz
*
* @param {int} authUserId - id of authorised user
* @param {int} quizId - id of quiz
* @param {int} questionId - id of the quiz question
*
* @returns {object} - new id of duplicated question
*
*/
interface DuplicateReturn {
newQuestionId: number
}
function adminQuestionDuplicate(authUserId: number, quizId: number, questionId: number): DuplicateReturn | ErrorObject {
const data: DataStore = getData();
const foundQuiz: Quiz = data.quizzes.find((quiz: Quiz) => quiz.id === quizId);
// checks if authUserId and quizId are valid and the quiz belongs to the user
if (validQuizCheck(authUserId, quizId)) {
return validQuizCheck(authUserId, quizId);
}
const foundQuestion = foundQuiz.questions.find(question => question.questionId === questionId);
if (!foundQuestion) {
return { error: 'Question Id does not refer to a valid question within this quiz' };
}
if (foundQuiz.duration + foundQuestion.duration > 180) {
return { error: 'Quiz duration cannot exceed 3 minutes' };
}
foundQuiz.numQuestions++;
foundQuiz.duration += foundQuestion.duration;
foundQuiz.timeLastEdited = Math.floor(Date.now() / 1000);
const dupeQuestion = Object.assign({}, foundQuestion);
const newQuestionId = data.questions[data.questions.length - 1].questionId + 1;
dupeQuestion.questionId = newQuestionId;
for (const dupeAnswer of dupeQuestion.answers) {
dupeAnswer.answerId = data.answers[data.answers.length - 1].answerId + 1;
data.answers.push(dupeAnswer);
}
// add the duplicated question to the index directly after the question that was duplicated
foundQuiz.questions.splice(foundQuiz.questions.indexOf(foundQuestion) + 1, 0, dupeQuestion);
data.questions.push(
{
ownerId: foundQuiz.ownerId,
quizId: foundQuiz.id,
questionId: newQuestionId,
});
setData(data);
return {
newQuestionId: newQuestionId
};
}
export {
adminQuizCreate,
adminQuizRemove,
adminQuizInfo,
adminQuizDescriptionUpdate,
adminQuizNameUpdate,
adminQuizList,
adminQuizTransfer,
adminQuestionCreate,
adminQuizRestore,
adminQuizMoveQuestion,
adminViewTrash,
adminQuestionDelete,
adminQuestionUpdate,
adminQuestionDuplicate
};
Editor is loading...