Skip to content

Commit

Permalink
Feat/challenges add user to res (#148)
Browse files Browse the repository at this point in the history
* feat: add getSeasonQuestion function

* fix: return correct itemcount and pagecount when generating pagination metadata

* test: add test file for pagination and getSeasonQuestion

* fix: modify some function to use cont-serv-repo architecture

* fix: add get season question endpoint
  • Loading branch information
BoonHianLim authored Mar 2, 2024
1 parent 5df66c8 commit f9bac61
Show file tree
Hide file tree
Showing 21 changed files with 543 additions and 133 deletions.
16 changes: 14 additions & 2 deletions apps/challenges/src/config/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as dotenv from "dotenv";
import { ConnectionOptions } from "tls";
dotenv.config();

const connectDB = async () => {
export const connectDB = async () => {
try {
const mongoURL = process.env.MONGO_URI || 'mongodb://localhost:27017';
const conn = await moongose.connect(mongoURL, {
Expand All @@ -17,4 +17,16 @@ const connectDB = async () => {
}
}

export { connectDB as default };
export const connectTestDB = async () => {
try {
const mongoURL = process.env.MONGO_URI || 'mongodb://localhost:27017';
const conn = await moongose.connect(mongoURL, {
useNewUrlParser: true,
dbName: process.env.MONGO_TEST_DATABSE_NAME || 'test',
} as ConnectionOptions);
console.log(`MongoDB Connected: ${mongoURL}`);
} catch (error) {
console.log(error);
process.exit(1)
}
}
5 changes: 3 additions & 2 deletions apps/challenges/src/controllers/questionaire.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Request, Response } from "express";
const asyncHandler = require('express-async-handler');
const Question = require('../model/question');
const Submission = require('../model/submission');
import Question from '../model/question';
import Submission from '../model/submission';
import Season from "../model/season";
import { isValidObjectId } from "../utils/db";

Expand Down Expand Up @@ -59,6 +59,7 @@ const setQuestion = asyncHandler(async (req: Request, res: Response) => {
question_title: req.body.question_title,
question_desc: req.body.question_desc,
question_date: req.body.question_date,
seasonID: req.body.season_id,
expiry: req.body.expiry,
points: req.body.points,
answer: req.body.answer,
Expand Down
67 changes: 28 additions & 39 deletions apps/challenges/src/controllers/season.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const getSeasons = asyncHandler(async (req: Request, res: Response) => {

try {
const seasons = await SeasonService.getSeasonsByDate(_startDate, _endDate);

res.status(200).json({
seasons: seasons,
});
Expand Down Expand Up @@ -105,42 +106,6 @@ const createSeason = asyncHandler(async (req: Request, res: Response) => {
}
});

/*
const getUserSeasonRanking = asyncHandler(async (req: Request, res: Response) => {
const { seasonID, userID } = req.params;
if (!isValidObjectId(seasonID) || !isValidObjectId(userID)) {
res.status(400).json({ message: 'Invalid request' });
return;
}
try {
const ranking = await SeasonService.getUserSeasonRanking(seasonID, userID);
res.status(200).json({
ranking: ranking
});
} catch (error) {
const err = error as Error;
res.status(500).json({ message: err.message });
}
});
const getUserAllSeasonRankings = asyncHandler(async (req: Request, res: Response) => {
const { userID } = req.params;
if (!isValidObjectId(userID)) {
res.status(400).json({ message: 'Invalid request' });
return;
}
try {
const rankings = await SeasonService.getUserAllSeasonRankings(userID);
res.status(200).json(rankings);
} catch (error) {
const err = error as Error;
res.status(500).json({ message: err.message });
}
});
*/

// @desc Get season rankings
// @route GET /api/seasons/:seasonID/rankings
// @access Public
Expand Down Expand Up @@ -181,8 +146,11 @@ const getSeasonRankings = asyncHandler(async (req: Request, res: Response) => {

try{
const { rankings, rankingsCount } = await SeasonService.getSeasonRankingsByPagination(seasonID, page!, limit!);

isNonNegativeInteger.parse(rankingsCount);
const metaData = generatePaginationMetaData(`/api/seasons/${seasonID}/rankings`, page!, limit!, Math.ceil(rankingsCount / limit) - 1);
const maxPageIndex = rankingsCount == 0 ? 0 : Math.ceil(rankingsCount / limit) - 1

const metaData = generatePaginationMetaData(`/api/seasons/${seasonID}/rankings`, page!, limit!, maxPageIndex, rankingsCount);
res.setHeader("access-control-expose-headers", "pagination");
res.setHeader("pagination", JSON.stringify(metaData));
res.status(200).json({
Expand All @@ -195,14 +163,35 @@ const getSeasonRankings = asyncHandler(async (req: Request, res: Response) => {
}
});

const getSeasonQuestions = asyncHandler(async (req: Request, res: Response) => {
try {
const seasonID = zodIsValidObjectId.parse(req.params.seasonID);

const season = await SeasonService.getSeasonByID(seasonID);
if (!season) {
res.status(404).json({ message: 'Season not found' });
return;
}

const questions = await SeasonService.getSeasonQuestions(seasonID);

res.status(200).json(questions);
} catch (err) {
if (err instanceof z.ZodError) {
res.status(400).json({ message: 'Invalid request' });
} else {
res.status(500).json({ message: 'Internal Server Error' });
}
}
});

const SeasonController = {
getSeasons,
getActiveSeasons,
getSeasonByID,
createSeason,
getSeasonRankings,
// getUserSeasonRanking,
// getUserAllSeasonRankings,
getSeasonQuestions,
};

export { SeasonController as default };
67 changes: 13 additions & 54 deletions apps/challenges/src/controllers/submission.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { Request, Response } from "express";
const asyncHandler = require('express-async-handler');
const Question = require('../model/question');
const Submission = require('../model/submission');
import Question from '../model/question';
import Submission from '../model/submission';
import Season from "../model/season";
import { isValidObjectId } from "../utils/db";
import QuestionService from "../service/questionService";
import SubmissionService from "../service/submissionService";
import { isValidCreateSubmissionRequest } from "../utils/validator";
import mongoose from 'mongoose';


// @desc Get submissions
Expand Down Expand Up @@ -50,60 +54,15 @@ const setSubmission = asyncHandler(async (req: Request, res: Response) => {
}

try {
const question = await Question.findById(questionId);
const submission = isValidCreateSubmissionRequest.parse(req.body);
const createSubmissionReq = {
question: new mongoose.Types.ObjectId(submission.question),
user: new mongoose.Types.ObjectId(submission.user),
answer: submission.answer
};

// 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' });
// }
await SubmissionService.createSubmission(createSubmissionReq);

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 Season.findOne({ _id: req.body.leaderboard });
// TODO: Update leaderboard rankings
/*
const ranking = leaderboard?.rankings.find((ranking: any) => ranking.user == req.body.user);
if (!ranking) {
await Season.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 Season.findByIdAndUpdate(req.body.leaderboard, { $set: { 'rankings.$[elem].points': ranking.points - highestPoints + submission.points_awarded } }, { arrayFilters: [{ 'elem.user': req.body.user }], new: true });
} else {
await Season.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) {
Expand Down
2 changes: 1 addition & 1 deletion apps/challenges/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ 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";
import { connectDB } from "./config/db";
import { CronJob } from "cron";
import { rankingCalculation } from "./tasks/rankingCalculation";
dotenv.config({ path: "../.env"});
Expand Down
6 changes: 6 additions & 0 deletions apps/challenges/src/model/question.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export interface QuestionModel {
question_title: string;
question_desc: string;
question_date: Date;
seasonID: mongoose.Types.ObjectId;
expiry: Date;
points: number;
answer: string;
Expand All @@ -31,6 +32,11 @@ const questionSchema: Schema<QuestionModel> = new Schema({
type: Date,
required: [true, 'Please add a question date']
},
seasonID: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Season',
required: [true, 'Please add a season ID']
},
expiry: {
type: Date,
required: [true, 'Please add an expiry date']
Expand Down
5 changes: 5 additions & 0 deletions apps/challenges/src/model/response.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface GeneralResp {
status: number;
message: string;
data?: any;
}
9 changes: 9 additions & 0 deletions apps/challenges/src/model/season.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
import mongoose, { Schema } from 'mongoose';
import { QuestionModel } from './question';

export interface GetSeasonResp {
id: mongoose.Types.ObjectId;
title: string;
startDate: Date;
endDate: Date;
questions: QuestionModel[];
}

export interface SeasonModel {
_id: mongoose.Types.ObjectId;
Expand Down
25 changes: 13 additions & 12 deletions apps/challenges/src/model/submission.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import mongoose, { Schema, model } from 'mongoose';

export interface CreateSubmissionReq {
user: mongoose.Types.ObjectId;
question: mongoose.Types.ObjectId;
answer: string;
}

export interface SubmissionModel {
user: mongoose.Types.ObjectId;
seasonID: mongoose.Types.ObjectId;
question: mongoose.Types.ObjectId;
answer: string;
correct?: boolean;
points_awarded?: number;
question?: mongoose.Types.ObjectId;
attempt?: number;
}

const submissionSchema: Schema<SubmissionModel> = new Schema({
Expand All @@ -19,7 +24,12 @@ const submissionSchema: Schema<SubmissionModel> = new Schema({
seasonID: {
type: mongoose.Schema.Types.ObjectId,
required: true,
ref: 'Leaderboard',
ref: 'Season',
},
question: {
type: mongoose.Schema.Types.ObjectId,
required: true,
ref: 'Question',
},
answer: {
type: String,
Expand All @@ -31,15 +41,6 @@ const submissionSchema: Schema<SubmissionModel> = new Schema({
points_awarded: {
type: Number,
},
question: {
type: mongoose.Schema.Types.ObjectId,
required: true,
ref: 'Question',
},
attempt: {
type: Number,
default: 1,
},
}, {
timestamps: true,
});
Expand Down
46 changes: 46 additions & 0 deletions apps/challenges/src/repo/questionRepo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import Question, { QuestionModel } from "../model/question"
import mongoose from 'mongoose';

const getQuestionByID = async (
questionID: mongoose.Types.ObjectId,
): Promise<QuestionModel | null> => {
const question = await Question.findOne({
_id: questionID
});
return question;
}

const updateQuestionByID = async (
questionID: mongoose.Types.ObjectId,
questionModel: QuestionModel
): Promise<QuestionModel | null> => {
const question = await Question.findOneAndUpdate({
_id: questionID
}, questionModel, { new: true });
return question;
}

const updateQuestionSubmissions = async (
questionID: mongoose.Types.ObjectId,
submissionID: mongoose.Types.ObjectId,
isCorrect: boolean
): Promise<QuestionModel | null> => {
const question = await Question.findOneAndUpdate({
_id: questionID
}, {
$push: { submissions: submissionID },
$inc: {
submissions_count: 1,
correct_submissions_count: isCorrect ? 1 : 0
}
}, { new: true });
return question;
}

const QuestionRepo = {
getQuestionByID,
updateQuestionByID,
updateQuestionSubmissions
}

export { QuestionRepo as default }
Loading

0 comments on commit f9bac61

Please sign in to comment.