Skip to content

Commit

Permalink
Merge pull request #93 from jolynloh/feature/admin-add-test-cases
Browse files Browse the repository at this point in the history
Add and store test cases and code templates
  • Loading branch information
guanquann authored Nov 4, 2024
2 parents 73012f8 + 87138d8 commit 0ba50ee
Show file tree
Hide file tree
Showing 16 changed files with 750 additions and 42 deletions.
6 changes: 5 additions & 1 deletion backend/question-service/src/config/multer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,9 @@ import multer from "multer";

const storage = multer.memoryStorage();
const upload = multer({ storage }).array("images[]");
const uploadTestcaseFiles = multer({ storage }).fields([
{ name: "testcaseInputFile", maxCount: 1 },
{ name: "testcaseOutputFile", maxCount: 1 },
]);

export { upload };
export { upload, uploadTestcaseFiles };
99 changes: 94 additions & 5 deletions backend/question-service/src/controllers/questionController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,17 @@ import {
MONGO_OBJ_ID_MALFORMED_MESSAGE,
} from "../utils/constants.ts";

import { upload } from "../config/multer.ts";
import { upload, uploadTestcaseFiles } from "../config/multer.ts";
import { uploadFileToFirebase } from "../utils/utils";
import { QnListSearchFilterParams, RandomQnCriteria } from "../utils/types.ts";

const FIREBASE_TESTCASE_FILES_FOLDER_NAME = "testcaseFiles/";

enum TestcaseFilesUploadRequestTypes {
CREATE = "create",
UPDATE = "update",
}

export const createQuestion = async (
req: Request,
res: Response,
Expand All @@ -34,6 +41,8 @@ export const createQuestion = async (
description,
complexity,
category,
testcaseInputFileUrl,
testcaseOutputFileUrl,
pythonTemplate,
javaTemplate,
cTemplate,
Expand All @@ -59,6 +68,8 @@ export const createQuestion = async (
description,
complexity,
category,
testcaseInputFileUrl,
testcaseOutputFileUrl,
});

await newQuestion.save();
Expand All @@ -77,6 +88,7 @@ export const createQuestion = async (
question: formatQuestionIndivResponse(newQuestion, newQuestionTemplate),
});
} catch (error) {
console.log(error);
res.status(500).json({ message: SERVER_ERROR_MESSAGE, error });
}
};
Expand Down Expand Up @@ -110,6 +122,72 @@ export const createImageLink = async (
});
};

export const createFileLink = async (
req: Request,
res: Response,
): Promise<void> => {
uploadTestcaseFiles(req, res, async (err) => {
if (err) {
return res.status(500).json({
message: "Failed to upload testcase files",
error: err.message,
});
}

const isQuestionCreation =
req.body.requestType === TestcaseFilesUploadRequestTypes.CREATE;

const tcFiles = req.files as {
testcaseInputFile?: Express.Multer.File[];
testcaseOutputFile?: Express.Multer.File[];
};

if (
isQuestionCreation &&
(!tcFiles || !tcFiles.testcaseInputFile || !tcFiles.testcaseOutputFile)
) {
return res
.status(400)
.json({ message: "Missing one or both testcase file(s)" });
}

try {
const uploadPromises = [];

if (tcFiles.testcaseInputFile) {
const inputFile = tcFiles.testcaseInputFile[0] as Express.Multer.File;
uploadPromises.push(
uploadFileToFirebase(inputFile, FIREBASE_TESTCASE_FILES_FOLDER_NAME),
);
} else {
uploadPromises.push(Promise.resolve(null));
}

if (tcFiles.testcaseOutputFile) {
const outputFile = tcFiles.testcaseOutputFile[0] as Express.Multer.File;
uploadPromises.push(
uploadFileToFirebase(outputFile, FIREBASE_TESTCASE_FILES_FOLDER_NAME),
);
} else {
uploadPromises.push(Promise.resolve(null));
}

const [tcInputFileUrl, tcOutputFileUrl] =
await Promise.all(uploadPromises);

return res.status(200).json({
message: "Files uploaded successfully",
urls: {
testcaseInputFileUrl: tcInputFileUrl || "",
testcaseOutputFileUrl: tcOutputFileUrl || "",
},
});
} catch (error) {
return res.status(500).json({ message: "Server error", error });
}
});
};

export const updateQuestion = async (
req: Request,
res: Response,
Expand Down Expand Up @@ -156,9 +234,9 @@ export const updateQuestion = async (
const updatedQuestionTemplate = await QuestionTemplate.findOneAndUpdate(
{ questionId: id },
{
...(pythonTemplate !== undefined && { pythonTemplate }),
...(javaTemplate !== undefined && { javaTemplate }),
...(cTemplate !== undefined && { cTemplate }),
pythonTemplate,
javaTemplate,
cTemplate,
},
{ new: true },
);
Expand Down Expand Up @@ -304,9 +382,18 @@ export const readRandomQuestion = async (
return;
}

const chosenQuestion = randomQuestion[0];

const questionTemplate = await QuestionTemplate.findOne({
questionId: chosenQuestion._id,
});

res.status(200).json({
message: QN_RETRIEVED_MESSAGE,
question: formatQuestionResponse(randomQuestion[0]),
question: formatQuestionIndivResponse(
chosenQuestion,
questionTemplate as IQuestionTemplate,
),
});
} catch (error) {
res.status(500).json({ message: SERVER_ERROR_MESSAGE, error });
Expand Down Expand Up @@ -356,6 +443,8 @@ const formatQuestionIndivResponse = (
description: question.description,
complexity: question.complexity,
categories: question.category,
testcaseInputFileUrl: question.testcaseInputFileUrl,
testcaseOutputFileUrl: question.testcaseOutputFileUrl,
pythonTemplate: questionTemplate ? questionTemplate.pythonTemplate : "",
javaTemplate: questionTemplate ? questionTemplate.javaTemplate : "",
cTemplate: questionTemplate ? questionTemplate.cTemplate : "",
Expand Down
9 changes: 5 additions & 4 deletions backend/question-service/src/models/Question.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ export interface IQuestion extends Document {
description: string;
complexity: string;
category: string[];
testcaseInputFileUrl: string;
testcaseOutputFileUrl: string;
createdAt: Date;
updatedAt: Date;
}
Expand All @@ -18,10 +20,9 @@ const questionSchema: Schema<IQuestion> = new mongoose.Schema(
enum: ["Easy", "Medium", "Hard"],
required: true,
},
category: {
type: [String],
required: true,
},
category: { type: [String], required: true },
testcaseInputFileUrl: { type: String, required: true },
testcaseOutputFileUrl: { type: String, required: true },
},
{ timestamps: true },
);
Expand Down
3 changes: 3 additions & 0 deletions backend/question-service/src/routes/questionRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
readQuestionIndiv,
readCategories,
readRandomQuestion,
createFileLink,
} from "../controllers/questionController.ts";
import { verifyAdminToken } from "../middlewares/basicAccessControl.ts";

Expand All @@ -17,6 +18,8 @@ router.post("/", verifyAdminToken, createQuestion);

router.post("/images", verifyAdminToken, createImageLink);

router.post("/tcfiles", verifyAdminToken, createFileLink);

router.put("/:id", verifyAdminToken, updateQuestion);

router.get("/categories", readCategories);
Expand Down
3 changes: 2 additions & 1 deletion backend/question-service/src/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@ export const checkIsExistingQuestion = async (

export const uploadFileToFirebase = async (
file: Express.Multer.File,
folderName: string = "",
): Promise<string> => {
return new Promise((resolve, reject) => {
const fileName = uuidv4();
const fileName = folderName + uuidv4();
const ref = bucket.file(fileName);

const blobStream = ref.createWriteStream({
Expand Down
14 changes: 14 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"react-router-dom": "^6.3.0",
"react-toastify": "^10.0.5",
"socket.io-client": "^4.8.0",
"uuid": "^11.0.2",
"vite-plugin-svgr": "^4.2.0"
},
"devDependencies": {
Expand Down
132 changes: 132 additions & 0 deletions frontend/src/components/QuestionCodeTemplates/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { HelpOutlined } from "@mui/icons-material";
import {
Box,
IconButton,
Stack,
TextField,
ToggleButton,
ToggleButtonGroup,
Tooltip,
Typography,
} from "@mui/material";
import { useState } from "react";
import { CODE_TEMPLATES_TOOLTIP_MESSAGE } from "../../utils/constants";

interface QuestionCodeTemplatesProps {
codeTemplates: {
[key: string]: string;
};
setCodeTemplates: React.Dispatch<
React.SetStateAction<{
[key: string]: string;
}>
>;
}

const QuestionCodeTemplates: React.FC<QuestionCodeTemplatesProps> = ({
codeTemplates,
setCodeTemplates,
}) => {
const [selectedLanguage, setSelectedLanguage] = useState<string>("python");

const handleLanguageChange = (
_: React.MouseEvent<HTMLElement>,
language: string
) => {
if (language) {
setSelectedLanguage(language);
}
};

const handleCodeChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const { value } = event.target;
setCodeTemplates((prevTemplates) => ({
...prevTemplates,
[selectedLanguage]: value,
}));
};

const handleTabKeys = (event: any) => {

Check failure on line 49 in frontend/src/components/QuestionCodeTemplates/index.tsx

View workflow job for this annotation

GitHub Actions / frontend-ci

Unexpected any. Specify a different type
const { value } = event.target;

if (event.key === "Tab") {
event.preventDefault();

const cursorPosition = event.target.selectionStart;
const cursorEndPosition = event.target.selectionEnd;
const tab = "\t";

event.target.value =
value.substring(0, cursorPosition) +
tab +
value.substring(cursorEndPosition);

event.target.selectionStart = cursorPosition + 1;
event.target.selectionEnd = cursorPosition + 1;
}
};

return (
<Box display="flex" flexDirection="column" marginTop={2}>
<Stack direction="row" alignItems="center">
<Typography variant="h6">Code Templates</Typography>
<Tooltip
title={
<Typography variant="body2">
<span
dangerouslySetInnerHTML={{
__html: CODE_TEMPLATES_TOOLTIP_MESSAGE,
}}
/>
</Typography>
}
placement="right"
arrow
>
<IconButton>
<HelpOutlined fontSize="small" />
</IconButton>
</Tooltip>
</Stack>
<ToggleButtonGroup
value={selectedLanguage}
exclusive
onChange={handleLanguageChange}
sx={{
marginY: 2,
height: 42,
}}
fullWidth
>
<ToggleButton value="python">Python</ToggleButton>
<ToggleButton value="java">Java</ToggleButton>
<ToggleButton value="c">C</ToggleButton>
</ToggleButtonGroup>

<TextField
label={
codeTemplates[selectedLanguage]
? ``
: `${
selectedLanguage.charAt(0).toUpperCase() +
selectedLanguage.slice(1)
} Code Template`
}
variant="outlined"
multiline
rows={8}
sx={{
"& .MuiOutlinedInput-root": {
fontFamily: "monospace",
},
}}
value={codeTemplates[selectedLanguage]}
onChange={handleCodeChange}
onKeyDown={handleTabKeys}
fullWidth
/>
</Box>
);
};

export default QuestionCodeTemplates;
Loading

0 comments on commit 0ba50ee

Please sign in to comment.