Skip to content

Commit

Permalink
Merge branch 'challenges' into challenges-leaderboard
Browse files Browse the repository at this point in the history
  • Loading branch information
jyorien committed Feb 1, 2024
2 parents 12ac52e + e9d24f7 commit 4d7e262
Show file tree
Hide file tree
Showing 34 changed files with 3,615 additions and 359 deletions.
3 changes: 2 additions & 1 deletion apps/challenges/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"supertest": "^6.3.4",
"ts-jest": "^29.1.1",
"ts-node": "^10.9.2",
"turbo": "^1.10.1"
"turbo": "^1.10.1",
"zod": "^3.22.4"
},
"devDependencies": {
"@types/express": "^4.17.21",
Expand Down
2 changes: 1 addition & 1 deletion apps/challenges/src/controllers/questionaire.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,8 +192,8 @@ const QuestionController = {
getActiveQuestions,
setQuestion,
updateQuestion,
deleteQuestion,
// submitAnswer
deleteQuestion
};

export { QuestionController as default };
56 changes: 25 additions & 31 deletions apps/challenges/src/controllers/season.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Request, Response } from "express";
import asyncHandler from "express-async-handler";

import { isValidObjectId } from "../utils/db";
import { isValidObjectId, paramsSchema } from "../utils/db";
import SeasonService from "../service/seasonService";
import isValidDate from "../utils/checkObjectProperties";
import { z } from "zod";

interface CreateSeasonRequest {
title: string;
Expand Down Expand Up @@ -141,45 +142,37 @@ const getUserAllSeasonRankings = asyncHandler(async (req: Request, res: Response
// @route GET /api/seasons/:seasonID/rankings
// @access Public
const getSeasonRankings = asyncHandler(async (req: Request, res: Response) => {
const { seasonID } = req.params;
const { page, limit } = req.query;

if (!isValidObjectId(seasonID)) {
res.status(400).json({ message: 'Invalid request' });
return;
}
if((page != null && limit == null) || (page == null && limit != null)){
res.status(400).json({ message: 'Invalid request' });
return;
}
if (page != null && typeof page !== "string") {
res.status(400).json({ message: 'Invalid request' });
return;
}
if (limit != null && typeof limit !== "string") {
res.status(400).json({ message: 'Invalid request' });
return;
}

const _page = parseInt((page) as string);
const _limit = parseInt((limit) as string);
try {
const {rankings , rankingsCount} = await SeasonService.getSeasonRankingsByPagination(seasonID, _page, _limit);
const seasonID = paramsSchema.parse(req.params);
const querySchema = z.object({
page: z.number().int().min(0).optional(),
limit: z.number().int().min(1).optional()
}).refine(
data => (data.page && data.limit) && (!data.page && !data.limit),
{ message: "Invalid request" }
);
const { page, limit } = querySchema.parse(req.query);

const { rankings, rankingsCount } = await SeasonService.getSeasonRankingsByPagination(seasonID, page!, limit!);
const metaData = {
page: _page,
limit: _limit,
pageCount: Math.ceil(rankingsCount / _limit) || 0,
page: page,
limit: limit,
pageCount: Math.ceil(rankingsCount / limit!) || 0,
itemCount: rankingsCount || 0,
links: getLinks(seasonID, _page, _limit, rankingsCount)
links: getLinks(seasonID, page!, limit!, rankingsCount)
}
res.setHeader("access-control-expose-headers", "pagination");
res.setHeader("pagination", JSON.stringify(metaData));
res.status(200).json({
rankings: rankings,
_metaData: metaData
});
} catch (error) {
res.status(500).json({ message: 'Internal Server Error' });
} catch (err) {
if(err instanceof z.ZodError){
res.status(400).json({ message: 'Invalid request' });
}else{
res.status(500).json({ message: 'Internal Server Error' });
}
}
});

Expand Down Expand Up @@ -251,7 +244,8 @@ const SeasonController = {
getSeasonRankings,
getUserSeasonRanking,
getUserAllSeasonRankings,
updateSeasonRankings
updateSeasonRankings,
getLinks
};

export { SeasonController as default };
176 changes: 176 additions & 0 deletions apps/challenges/src/controllers/submission.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import { Request, Response } from "express";
const asyncHandler = require('express-async-handler');
const Question = require('../model/question');
const Submission = require('../model/submission');
const Leaderboard = require('../model/leaderboard');
import { isValidObjectId } from "../utils/db";


// @desc Get submissions
// @route GET /api/submission
// @access Public
const getSubmissions = asyncHandler(async (req: Request, res: Response) => {
const submissions = await Submission.find({})
res.status(200).json(submissions)
})

// @desc Get submission
// @route GET /api/submission/:id
// @access Public
const getSubmission = asyncHandler(async (req: Request, res: Response) => {
const submissionId = req.params.id;

if (!isValidObjectId(submissionId)) {
return res.status(400).json({ message: 'Invalid submission ID' });
}

try {
const submission = await Submission.findById(submissionId);

if (!submission) {
return res.status(404).json({ message: 'Submission not found' });
}

res.status(200).json(submission);
} catch (error) {
console.error(error);
res.status(500).json({ message: 'Internal Server Error' });
}
})

// @desc Set submission
// @route POST /api/submission
// @access Private
const setSubmission = asyncHandler(async (req: Request, res: Response) => {
const questionId = req.body.question;

if (!isValidObjectId(questionId)) {
return res.status(400).json({ message: 'Invalid question ID' });
}

try {
const question = await Question.findById(questionId);

// cooldown period of 3 seconds per user per question
// if (question.submissions.find((submission: any) => submission.user == req.body.user && new Date(submission.createdAt) > new Date(new Date().getTime() - 3 * 1000))) {
// return res.status(400).json({ message: 'Cooldown period of 10 seconds per user' });
// }

if (!question) {
return res.status(404).json({ message: 'Question not found' });
}

if (!question.active) {
return res.status(400).json({ message: 'Question is not active' });
}

if (new Date(question.expiry) < new Date()) {
return res.status(400).json({ message: 'Question has expired' });
}

const submission = await Submission.create({
user: req.body.user,
leaderboard: req.body.leaderboard,
answer: req.body.answer,
correct: req.body.answer === question.answer,
points_awarded: req.body.answer === question.answer ? question.points : 0,
question: questionId,
attempt: question.submissions.find((submission: any) => submission.user == req.body.user) ? question.submissions.find((submission: any) => submission.user == req.body.user).attempt + 1 : 1
});

// Update question submissions array using $push and $inc submission counts
await Question.findByIdAndUpdate(questionId, {
$push: { submissions: submission._id },
$inc: { submissions_count: 1, correct_submissions_count: req.body.answer === question.answer ? 1 : 0 } },
{ new: true });

// Retrieve user and update points of the entry in the leaderboard
const leaderboard = await Leaderboard.findOne({ _id: req.body.leaderboard });
const ranking = leaderboard.rankings.find((ranking: any) => ranking.user == req.body.user);
if (!ranking) {
await Leaderboard.findByIdAndUpdate(req.body.leaderboard, { $push: { rankings: { user: req.body.user, points: submission.points_awarded } } }, { new: true });
} else {
// if there is previous submission for the same question, remove the points from the previous submission and add the points from the new submission
const prevSubmission = await Submission.find({ user: req.body.user, question: questionId, _id: { $ne: submission._id } });
if (prevSubmission.length > 0) {
// get the highest points awarded for the question
const highestPoints = Math.max(...prevSubmission.map((submission: any) => submission.points_awarded));
await Leaderboard.findByIdAndUpdate(req.body.leaderboard, { $set: { 'rankings.$[elem].points': ranking.points - highestPoints + submission.points_awarded } }, { arrayFilters: [{ 'elem.user': req.body.user }], new: true });
} else {
await Leaderboard.findByIdAndUpdate(req.body.leaderboard, { $set: { 'rankings.$[elem].points': ranking.points + submission.points_awarded } }, { arrayFilters: [{ 'elem.user': req.body.user }], new: true });
}
}

res.status(201).json({ message: 'Answer submitted' });

} catch (error) {
res.status(500).json({ message: 'Internal Server Error' });
}
})

// @desc Update submission
// @route PUT /api/submission/:id
// @access Private
const updateSubmission = asyncHandler(async (req: Request, res: Response) => {
const submissionId = req.params.id;

if (!isValidObjectId(submissionId)) {
return res.status(400).json({ message: 'Invalid submission ID' });
}

try {
const submission = await Submission.findById(submissionId);

if (!submission) {
return res.status(404).json({ message: 'Submission not found' });
}

const updatedSubmission = await Submission.findByIdAndUpdate(submissionId, req.body, { new: true });

// Re-evaluate the points awarded



res.status(200).json(updatedSubmission);
} catch (error) {
console.error(error);
res.status(500).json({ message: 'Internal Server Error' });
}
})

// @desc Delete submission
// @route DELETE /api/submission/:id
// @access Private
const deleteSubmission = asyncHandler(async (req: Request, res: Response) => {
const submissionId = req.params.id;

if (!isValidObjectId(submissionId)) {
return res.status(400).json({ message: 'Invalid submission ID' });
}

try {
const submission = await Submission.findById(submissionId);

if (!submission) {
return res.status(404).json({ message: 'Submission not found' });
}

await submission.remove()

res.status(200).json({message: 'Submission deleted'});
} catch (error) {
console.error(error);
res.status(500).json({ message: 'Internal Server Error' });
}
})


const SubmissionController = {
getSubmissions,
getSubmission,
setSubmission,
updateSubmission,
deleteSubmission
};

export { SubmissionController as default };
2 changes: 2 additions & 0 deletions apps/challenges/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import express, { Express, Request, Response } from "express";
import dotenv from "dotenv";
import SeasonRouter from "./routes/seasons";
import QuestionaireRouter from "./routes/questionaire";
import SubmissionRouter from "./routes/submission";
import UserRouter from "./routes/user";
import connectDB from "./config/db";
dotenv.config({ path: "../.env"});
Expand All @@ -26,6 +27,7 @@ app.get("/ping", (req: Request, res: Response) => {
});
app.use("/api/seasons", SeasonRouter);
app.use('/api/question', QuestionaireRouter);
app.use('/api/submission', SubmissionRouter);
app.use('/api/user', UserRouter);

if (process.env.NODE_ENV !== 'test') {
Expand Down
10 changes: 10 additions & 0 deletions apps/challenges/src/model/question.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ export interface QuestionModel {
points: number;
answer: string;
submissions: Array<mongoose.Types.ObjectId>;
submissions_count: number;
correct_submissions_count: number;
active: boolean;
}

Expand Down Expand Up @@ -45,6 +47,14 @@ const questionSchema: Schema<QuestionModel> = new Schema({
type: [mongoose.Types.ObjectId],
ref: 'Submission'
},
submissions_count: {
type: Number,
default: 0
},
correct_submissions_count: {
type: Number,
default: 0
},
active: {
type: Boolean,
default: true
Expand Down
6 changes: 6 additions & 0 deletions apps/challenges/src/model/submission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export interface SubmissionModel {
correct?: boolean;
points_awarded?: number;
question?: mongoose.Types.ObjectId;
attempt?: number;
}

const submissionSchema: Schema<SubmissionModel> = new Schema({
Expand All @@ -32,8 +33,13 @@ const submissionSchema: Schema<SubmissionModel> = new Schema({
},
question: {
type: mongoose.Schema.Types.ObjectId,
required: true,
ref: 'Question',
},
attempt: {
type: Number,
default: 1,
},
}, {
timestamps: true,
});
Expand Down
4 changes: 1 addition & 3 deletions apps/challenges/src/routes/questionaire.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
const express = require('express');
const router = express.Router();
import QuestionController from "../controllers/questionaire";

import SubmissionController from "../controllers/submission";

router.route('/').get(QuestionController.getQuestions).post(QuestionController.setQuestion);
router.route('/active').get(QuestionController.getActiveQuestions);
router.route('/:id').get(QuestionController.getQuestion).delete(QuestionController.deleteQuestion).put(QuestionController.updateQuestion);

// router.route('/submit/:id').post(QuestionController.submitAnswer);

export { router as default };
8 changes: 8 additions & 0 deletions apps/challenges/src/routes/submission.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const express = require('express');
const router = express.Router();
import SubmissionController from "../controllers/submission";

router.route('/').get(SubmissionController.getSubmissions).post(SubmissionController.setSubmission);
router.route('/:id').get(SubmissionController.getSubmission).delete(SubmissionController.deleteSubmission).put(SubmissionController.updateSubmission);

export { router as default };
Loading

0 comments on commit 4d7e262

Please sign in to comment.