Skip to content

Commit 0ba50ee

Browse files
authored
Merge pull request #93 from jolynloh/feature/admin-add-test-cases
Add and store test cases and code templates
2 parents 73012f8 + 87138d8 commit 0ba50ee

File tree

16 files changed

+750
-42
lines changed

16 files changed

+750
-42
lines changed

backend/question-service/src/config/multer.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,9 @@ import multer from "multer";
22

33
const storage = multer.memoryStorage();
44
const upload = multer({ storage }).array("images[]");
5+
const uploadTestcaseFiles = multer({ storage }).fields([
6+
{ name: "testcaseInputFile", maxCount: 1 },
7+
{ name: "testcaseOutputFile", maxCount: 1 },
8+
]);
59

6-
export { upload };
10+
export { upload, uploadTestcaseFiles };

backend/question-service/src/controllers/questionController.ts

Lines changed: 94 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,17 @@ import {
2020
MONGO_OBJ_ID_MALFORMED_MESSAGE,
2121
} from "../utils/constants.ts";
2222

23-
import { upload } from "../config/multer.ts";
23+
import { upload, uploadTestcaseFiles } from "../config/multer.ts";
2424
import { uploadFileToFirebase } from "../utils/utils";
2525
import { QnListSearchFilterParams, RandomQnCriteria } from "../utils/types.ts";
2626

27+
const FIREBASE_TESTCASE_FILES_FOLDER_NAME = "testcaseFiles/";
28+
29+
enum TestcaseFilesUploadRequestTypes {
30+
CREATE = "create",
31+
UPDATE = "update",
32+
}
33+
2734
export const createQuestion = async (
2835
req: Request,
2936
res: Response,
@@ -34,6 +41,8 @@ export const createQuestion = async (
3441
description,
3542
complexity,
3643
category,
44+
testcaseInputFileUrl,
45+
testcaseOutputFileUrl,
3746
pythonTemplate,
3847
javaTemplate,
3948
cTemplate,
@@ -59,6 +68,8 @@ export const createQuestion = async (
5968
description,
6069
complexity,
6170
category,
71+
testcaseInputFileUrl,
72+
testcaseOutputFileUrl,
6273
});
6374

6475
await newQuestion.save();
@@ -77,6 +88,7 @@ export const createQuestion = async (
7788
question: formatQuestionIndivResponse(newQuestion, newQuestionTemplate),
7889
});
7990
} catch (error) {
91+
console.log(error);
8092
res.status(500).json({ message: SERVER_ERROR_MESSAGE, error });
8193
}
8294
};
@@ -110,6 +122,72 @@ export const createImageLink = async (
110122
});
111123
};
112124

125+
export const createFileLink = async (
126+
req: Request,
127+
res: Response,
128+
): Promise<void> => {
129+
uploadTestcaseFiles(req, res, async (err) => {
130+
if (err) {
131+
return res.status(500).json({
132+
message: "Failed to upload testcase files",
133+
error: err.message,
134+
});
135+
}
136+
137+
const isQuestionCreation =
138+
req.body.requestType === TestcaseFilesUploadRequestTypes.CREATE;
139+
140+
const tcFiles = req.files as {
141+
testcaseInputFile?: Express.Multer.File[];
142+
testcaseOutputFile?: Express.Multer.File[];
143+
};
144+
145+
if (
146+
isQuestionCreation &&
147+
(!tcFiles || !tcFiles.testcaseInputFile || !tcFiles.testcaseOutputFile)
148+
) {
149+
return res
150+
.status(400)
151+
.json({ message: "Missing one or both testcase file(s)" });
152+
}
153+
154+
try {
155+
const uploadPromises = [];
156+
157+
if (tcFiles.testcaseInputFile) {
158+
const inputFile = tcFiles.testcaseInputFile[0] as Express.Multer.File;
159+
uploadPromises.push(
160+
uploadFileToFirebase(inputFile, FIREBASE_TESTCASE_FILES_FOLDER_NAME),
161+
);
162+
} else {
163+
uploadPromises.push(Promise.resolve(null));
164+
}
165+
166+
if (tcFiles.testcaseOutputFile) {
167+
const outputFile = tcFiles.testcaseOutputFile[0] as Express.Multer.File;
168+
uploadPromises.push(
169+
uploadFileToFirebase(outputFile, FIREBASE_TESTCASE_FILES_FOLDER_NAME),
170+
);
171+
} else {
172+
uploadPromises.push(Promise.resolve(null));
173+
}
174+
175+
const [tcInputFileUrl, tcOutputFileUrl] =
176+
await Promise.all(uploadPromises);
177+
178+
return res.status(200).json({
179+
message: "Files uploaded successfully",
180+
urls: {
181+
testcaseInputFileUrl: tcInputFileUrl || "",
182+
testcaseOutputFileUrl: tcOutputFileUrl || "",
183+
},
184+
});
185+
} catch (error) {
186+
return res.status(500).json({ message: "Server error", error });
187+
}
188+
});
189+
};
190+
113191
export const updateQuestion = async (
114192
req: Request,
115193
res: Response,
@@ -156,9 +234,9 @@ export const updateQuestion = async (
156234
const updatedQuestionTemplate = await QuestionTemplate.findOneAndUpdate(
157235
{ questionId: id },
158236
{
159-
...(pythonTemplate !== undefined && { pythonTemplate }),
160-
...(javaTemplate !== undefined && { javaTemplate }),
161-
...(cTemplate !== undefined && { cTemplate }),
237+
pythonTemplate,
238+
javaTemplate,
239+
cTemplate,
162240
},
163241
{ new: true },
164242
);
@@ -304,9 +382,18 @@ export const readRandomQuestion = async (
304382
return;
305383
}
306384

385+
const chosenQuestion = randomQuestion[0];
386+
387+
const questionTemplate = await QuestionTemplate.findOne({
388+
questionId: chosenQuestion._id,
389+
});
390+
307391
res.status(200).json({
308392
message: QN_RETRIEVED_MESSAGE,
309-
question: formatQuestionResponse(randomQuestion[0]),
393+
question: formatQuestionIndivResponse(
394+
chosenQuestion,
395+
questionTemplate as IQuestionTemplate,
396+
),
310397
});
311398
} catch (error) {
312399
res.status(500).json({ message: SERVER_ERROR_MESSAGE, error });
@@ -356,6 +443,8 @@ const formatQuestionIndivResponse = (
356443
description: question.description,
357444
complexity: question.complexity,
358445
categories: question.category,
446+
testcaseInputFileUrl: question.testcaseInputFileUrl,
447+
testcaseOutputFileUrl: question.testcaseOutputFileUrl,
359448
pythonTemplate: questionTemplate ? questionTemplate.pythonTemplate : "",
360449
javaTemplate: questionTemplate ? questionTemplate.javaTemplate : "",
361450
cTemplate: questionTemplate ? questionTemplate.cTemplate : "",

backend/question-service/src/models/Question.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ export interface IQuestion extends Document {
55
description: string;
66
complexity: string;
77
category: string[];
8+
testcaseInputFileUrl: string;
9+
testcaseOutputFileUrl: string;
810
createdAt: Date;
911
updatedAt: Date;
1012
}
@@ -18,10 +20,9 @@ const questionSchema: Schema<IQuestion> = new mongoose.Schema(
1820
enum: ["Easy", "Medium", "Hard"],
1921
required: true,
2022
},
21-
category: {
22-
type: [String],
23-
required: true,
24-
},
23+
category: { type: [String], required: true },
24+
testcaseInputFileUrl: { type: String, required: true },
25+
testcaseOutputFileUrl: { type: String, required: true },
2526
},
2627
{ timestamps: true },
2728
);

backend/question-service/src/routes/questionRoutes.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
readQuestionIndiv,
99
readCategories,
1010
readRandomQuestion,
11+
createFileLink,
1112
} from "../controllers/questionController.ts";
1213
import { verifyAdminToken } from "../middlewares/basicAccessControl.ts";
1314

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

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

21+
router.post("/tcfiles", verifyAdminToken, createFileLink);
22+
2023
router.put("/:id", verifyAdminToken, updateQuestion);
2124

2225
router.get("/categories", readCategories);

backend/question-service/src/utils/utils.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@ export const checkIsExistingQuestion = async (
2121

2222
export const uploadFileToFirebase = async (
2323
file: Express.Multer.File,
24+
folderName: string = "",
2425
): Promise<string> => {
2526
return new Promise((resolve, reject) => {
26-
const fileName = uuidv4();
27+
const fileName = folderName + uuidv4();
2728
const ref = bucket.file(fileName);
2829

2930
const blobStream = ref.createWriteStream({

frontend/package-lock.json

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"react-router-dom": "^6.3.0",
3232
"react-toastify": "^10.0.5",
3333
"socket.io-client": "^4.8.0",
34+
"uuid": "^11.0.2",
3435
"vite-plugin-svgr": "^4.2.0"
3536
},
3637
"devDependencies": {
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { HelpOutlined } from "@mui/icons-material";
2+
import {
3+
Box,
4+
IconButton,
5+
Stack,
6+
TextField,
7+
ToggleButton,
8+
ToggleButtonGroup,
9+
Tooltip,
10+
Typography,
11+
} from "@mui/material";
12+
import { useState } from "react";
13+
import { CODE_TEMPLATES_TOOLTIP_MESSAGE } from "../../utils/constants";
14+
15+
interface QuestionCodeTemplatesProps {
16+
codeTemplates: {
17+
[key: string]: string;
18+
};
19+
setCodeTemplates: React.Dispatch<
20+
React.SetStateAction<{
21+
[key: string]: string;
22+
}>
23+
>;
24+
}
25+
26+
const QuestionCodeTemplates: React.FC<QuestionCodeTemplatesProps> = ({
27+
codeTemplates,
28+
setCodeTemplates,
29+
}) => {
30+
const [selectedLanguage, setSelectedLanguage] = useState<string>("python");
31+
32+
const handleLanguageChange = (
33+
_: React.MouseEvent<HTMLElement>,
34+
language: string
35+
) => {
36+
if (language) {
37+
setSelectedLanguage(language);
38+
}
39+
};
40+
41+
const handleCodeChange = (event: React.ChangeEvent<HTMLInputElement>) => {
42+
const { value } = event.target;
43+
setCodeTemplates((prevTemplates) => ({
44+
...prevTemplates,
45+
[selectedLanguage]: value,
46+
}));
47+
};
48+
49+
const handleTabKeys = (event: any) => {
50+
const { value } = event.target;
51+
52+
if (event.key === "Tab") {
53+
event.preventDefault();
54+
55+
const cursorPosition = event.target.selectionStart;
56+
const cursorEndPosition = event.target.selectionEnd;
57+
const tab = "\t";
58+
59+
event.target.value =
60+
value.substring(0, cursorPosition) +
61+
tab +
62+
value.substring(cursorEndPosition);
63+
64+
event.target.selectionStart = cursorPosition + 1;
65+
event.target.selectionEnd = cursorPosition + 1;
66+
}
67+
};
68+
69+
return (
70+
<Box display="flex" flexDirection="column" marginTop={2}>
71+
<Stack direction="row" alignItems="center">
72+
<Typography variant="h6">Code Templates</Typography>
73+
<Tooltip
74+
title={
75+
<Typography variant="body2">
76+
<span
77+
dangerouslySetInnerHTML={{
78+
__html: CODE_TEMPLATES_TOOLTIP_MESSAGE,
79+
}}
80+
/>
81+
</Typography>
82+
}
83+
placement="right"
84+
arrow
85+
>
86+
<IconButton>
87+
<HelpOutlined fontSize="small" />
88+
</IconButton>
89+
</Tooltip>
90+
</Stack>
91+
<ToggleButtonGroup
92+
value={selectedLanguage}
93+
exclusive
94+
onChange={handleLanguageChange}
95+
sx={{
96+
marginY: 2,
97+
height: 42,
98+
}}
99+
fullWidth
100+
>
101+
<ToggleButton value="python">Python</ToggleButton>
102+
<ToggleButton value="java">Java</ToggleButton>
103+
<ToggleButton value="c">C</ToggleButton>
104+
</ToggleButtonGroup>
105+
106+
<TextField
107+
label={
108+
codeTemplates[selectedLanguage]
109+
? ``
110+
: `${
111+
selectedLanguage.charAt(0).toUpperCase() +
112+
selectedLanguage.slice(1)
113+
} Code Template`
114+
}
115+
variant="outlined"
116+
multiline
117+
rows={8}
118+
sx={{
119+
"& .MuiOutlinedInput-root": {
120+
fontFamily: "monospace",
121+
},
122+
}}
123+
value={codeTemplates[selectedLanguage]}
124+
onChange={handleCodeChange}
125+
onKeyDown={handleTabKeys}
126+
fullWidth
127+
/>
128+
</Box>
129+
);
130+
};
131+
132+
export default QuestionCodeTemplates;

0 commit comments

Comments
 (0)