From 824da5d30d789a391e8d008342851a2584d52304 Mon Sep 17 00:00:00 2001 From: Eivind Dalholt Date: Wed, 27 Sep 2023 19:02:57 +0200 Subject: [PATCH 1/8] fix: improve votation fetch performance --- backend/controllers/votation.ts | 31 ++++--------------------------- 1 file changed, 4 insertions(+), 27 deletions(-) diff --git a/backend/controllers/votation.ts b/backend/controllers/votation.ts index 5ff0079..7cda9cb 100644 --- a/backend/controllers/votation.ts +++ b/backend/controllers/votation.ts @@ -50,20 +50,7 @@ export async function getAllVotations(req: RequestWithNtnuiNo, res: Response) { continue; } - const optionList: OptionType[] = []; - - for (const optionID of vote.options) { - const id = optionID; - const option = await Option.findById(id); - if (option) { - const newOption = new Option({ - _id: id, - title: option.title, - voteCount: option.voteCount, - }); - optionList.push(newOption); - } - } + const optionList = await Option.find({ _id: { $in: vote.options } }); let isActive = false; if (assembly.currentVotation) { @@ -142,19 +129,9 @@ export async function getCurrentVotation( return res.status(200).json(null); } - const optionList: LimitedOptionType[] = []; - - for (const optionID of vote.options) { - const id = optionID; - const option = await Option.findById(id); - if (option) { - const newOption: LimitedOptionType = { - _id: id, - title: option.title, - }; - optionList.push(newOption); - } - } + const optionList: LimitedOptionType[] = await Option.find({ + _id: { $in: vote.options }, + }).select("_id title"); const votationResponse: LimitedVoteResponseType = { _id: assembly.currentVotation, From c4311c236510a0257831af0719026c444aa4da25 Mon Sep 17 00:00:00 2001 From: Eivind Dalholt Date: Wed, 27 Sep 2023 23:35:25 +0200 Subject: [PATCH 2/8] fix: aggregate options and votations --- backend/controllers/votation.ts | 69 +++++++++++++++------------------ 1 file changed, 32 insertions(+), 37 deletions(-) diff --git a/backend/controllers/votation.ts b/backend/controllers/votation.ts index 7cda9cb..22a98c5 100644 --- a/backend/controllers/votation.ts +++ b/backend/controllers/votation.ts @@ -29,49 +29,44 @@ export async function getAllVotations(req: RequestWithNtnuiNo, res: Response) { (membership) => membership.organizer && membership.groupSlug == group ) ) { - const listOfVotations: VoteResponseType[] = []; - const assembly = await Assembly.findById(group); if (!assembly) { return res .status(400) - .json({ message: "No assembly with the given group found " }); + .json({ message: "No assembly found on the given group found " }); } - const voteIDs = assembly.votes; - - for (const voteID of voteIDs) { - if (!Types.ObjectId.isValid(String(voteID))) { - continue; - } - const vote = await Votation.findById(voteID); - - if (!vote) { - continue; - } - - const optionList = await Option.find({ _id: { $in: vote.options } }); - - let isActive = false; - if (assembly.currentVotation) { - isActive = vote._id.equals(assembly.currentVotation); - } - - const votationResponse: VoteResponseType = { - _id: vote._id, - title: vote.title, - caseNumber: vote.caseNumber, - voteText: vote.voteText, - voted: vote.voted.length, - options: optionList, - isActive: isActive, - maximumOptions: vote.maximumOptions, - isFinished: vote.isFinished, - numberParticipants: vote.numberParticipants, - }; - - listOfVotations.push(votationResponse); - } + const listOfVotations: VoteResponseType[] = await Votation.aggregate([ + { + $match: { + _id: { $in: assembly.votes }, + }, + }, + { + $lookup: { + from: "options", // The name of the Option collection + localField: "options", + foreignField: "_id", + as: "options", + }, + }, + { + $project: { + _id: 1, + title: 1, + caseNumber: 1, + voteText: 1, + voted: { $size: "$voted" }, + maximumOptions: 1, + isFinished: 1, + numberParticipants: 1, + options: 1, + isActive: { + $eq: ["$_id", assembly.currentVotation], // Compare _id with assembly.CurrentVotation + }, + }, + }, + ]); return res.status(200).json(listOfVotations); } From cd9431463864d7a3fe401c5e82e653b8af5d887c Mon Sep 17 00:00:00 2001 From: Eivind Dalholt Date: Thu, 28 Sep 2023 00:47:18 +0200 Subject: [PATCH 3/8] fix: improve createVotation --- backend/controllers/votation.ts | 110 +++++++++++++++++--------------- backend/models/vote.ts | 3 +- 2 files changed, 61 insertions(+), 52 deletions(-) diff --git a/backend/controllers/votation.ts b/backend/controllers/votation.ts index 22a98c5..fe6f98d 100644 --- a/backend/controllers/votation.ts +++ b/backend/controllers/votation.ts @@ -33,7 +33,7 @@ export async function getAllVotations(req: RequestWithNtnuiNo, res: Response) { if (!assembly) { return res .status(400) - .json({ message: "No assembly found on the given group found " }); + .json({ message: "No assembly found on the given group found" }); } const listOfVotations: VoteResponseType[] = await Votation.aggregate([ @@ -44,7 +44,7 @@ export async function getAllVotations(req: RequestWithNtnuiNo, res: Response) { }, { $lookup: { - from: "options", // The name of the Option collection + from: "options", // The name of the collection containing the option documents (for joining data). localField: "options", foreignField: "_id", as: "options", @@ -56,7 +56,7 @@ export async function getAllVotations(req: RequestWithNtnuiNo, res: Response) { title: 1, caseNumber: 1, voteText: 1, - voted: { $size: "$voted" }, + voted: { $size: "$voted" }, // Count the number of elements in the array to get the number of votes. maximumOptions: 1, isFinished: 1, numberParticipants: 1, @@ -166,8 +166,12 @@ export async function createVotation(req: RequestWithNtnuiNo, res: Response) { (membership) => membership.organizer && membership.groupSlug == group ) ) { - const tempOptionTitles: OptionType[] = []; - + const assembly = await Assembly.findById(group); + if (!(assembly && title && Number.isFinite(caseNumber))) { + return res + .status(400) + .json({ message: "Error with groupID or case number" }); + } if (options) { if (!Array.isArray(options)) { return res @@ -175,57 +179,61 @@ export async function createVotation(req: RequestWithNtnuiNo, res: Response) { .json({ message: "Options is not on correct format" }); } - for (const optionID of options) { - const title = optionID; - const newOption = new Option({ - title: title, - voteCount: 0, - }); - const feedback = await Option.create(newOption); - tempOptionTitles.push(feedback); - } - } - - const newVotation = new Votation({ - title: title, - caseNumber: caseNumber, - isFinished: false, - options: tempOptionTitles, - voteText: voteText, - maximumOptions: maximumOptions, - }); - - const assembly = await Assembly.findById(group); - if (assembly && title && Number.isFinite(caseNumber)) { - const votationFeedback = await Votation.create(newVotation); - const assemblyFeedback = await Assembly.findByIdAndUpdate(group, { - $push: { - votes: votationFeedback, - }, - }); - if (assemblyFeedback) { - return res.status(200).json({ - message: "Votation successfully created", - vote_id: newVotation._id, + const newOptions = options.map((title) => ({ + title: title, + })); + + // Insert the array of Options and create a new Votation + try { + await Option.insertMany( + newOptions, + async (error: any, options: OptionType[]) => { + if (error) { + console.error(error); + return res.status(500).json({ + message: "Error while creating options", + }); + } else { + console.log("Options inserted successfully:", options); + + // Extract the _id values from the inserted documents + const insertedIDs: OptionType[] = options.map( + (option: OptionType) => option._id + ); + + const newVotation = await Votation.create({ + title: title, + caseNumber: caseNumber, + isFinished: false, + options: insertedIDs, + voteText: voteText, + maximumOptions: maximumOptions, + }); + + await Assembly.findByIdAndUpdate(group, { + $push: { + votes: newVotation._id, + }, + }); + + return res.status(200).json({ + message: "Votation successfully created", + vote_id: newVotation._id, + }); + } + } + ); + } catch (error) { + console.error(error); + return res.status(500).json({ + message: "Error while creating votation", }); } - } else if (!Number.isFinite(caseNumber)) { - return res - .status(400) - .json({ message: "Votation is missing casenumber" }); - } else { - return res - .status(400) - .json( - title == undefined - ? { message: "Votation is missing title" } - : { message: "Assembly not found" } - ); } } + } else { + return res.status(401).json({ message: "Unauthorized" }); } - - return res.status(401).json({ message: "Unauthorized" }); } export async function activateVotationStatus( diff --git a/backend/models/vote.ts b/backend/models/vote.ts index 6170018..40c6a16 100644 --- a/backend/models/vote.ts +++ b/backend/models/vote.ts @@ -9,7 +9,8 @@ export const optionSchema = new Schema( }, voteCount: { type: Number, - required: true, + required: false, + default: 0, }, }, { collection: "options", _id: true } From a1319416f5c35fa4509ef85ff34a31fcf3335614 Mon Sep 17 00:00:00 2001 From: Eivind Dalholt Date: Thu, 28 Sep 2023 16:21:58 +0200 Subject: [PATCH 4/8] fix: improve update and delete Votation --- backend/controllers/votation.ts | 71 ++++++++++----------------------- 1 file changed, 20 insertions(+), 51 deletions(-) diff --git a/backend/controllers/votation.ts b/backend/controllers/votation.ts index fe6f98d..01cb9d8 100644 --- a/backend/controllers/votation.ts +++ b/backend/controllers/votation.ts @@ -194,8 +194,6 @@ export async function createVotation(req: RequestWithNtnuiNo, res: Response) { message: "Error while creating options", }); } else { - console.log("Options inserted successfully:", options); - // Extract the _id values from the inserted documents const insertedIDs: OptionType[] = options.map( (option: OptionType) => option._id @@ -254,11 +252,6 @@ export async function activateVotationStatus( (membership) => membership.organizer && membership.groupSlug == group ) ) { - if (!Types.ObjectId.isValid(voteId)) { - return res - .status(400) - .json({ message: "No votation with the given ID found" }); - } const vote = await Votation.findById(voteId); const assembly = await Assembly.findById(group); @@ -268,13 +261,7 @@ export async function activateVotationStatus( .json({ message: "No votation with the given ID found " }); } - if (!assembly) { - return res - .status(400) - .json({ message: "No assembly with the given group found " }); - } - - if (!assembly.isActive) { + if (!assembly || !assembly.isActive) { return res .status(400) .json({ message: "No active assembly with the given group found " }); @@ -299,19 +286,9 @@ export async function activateVotationStatus( }); // Add votaton and options to an element for sending to participants. - const optionList: LimitedOptionType[] = []; - - for (const optionID of vote.options) { - const id = optionID; - const option = await Option.findById(id); - if (option) { - const newOption: LimitedOptionType = { - _id: id, - title: option.title, - }; - optionList.push(newOption); - } - } + const optionList: LimitedOptionType[] = await Option.find({ + _id: { $in: vote.options }, + }).select("_id title"); const votationResponse: LimitedVoteResponseType = { _id: voteId, @@ -334,6 +311,8 @@ export async function activateVotationStatus( ); }); + // Set number of participants to the number of active participants. + // This is used to store the number of logged in users when the votation was held. await Votation.findByIdAndUpdate(voteId, { $set: { numberParticipants: req.body.numberParticipants, @@ -371,7 +350,7 @@ export async function deactivateVotationStatus( if (!assembly || !assembly.currentVotation) { return res .status(400) - .json({ message: "There is no current votation ongoing" }); + .json({ message: "There are currently no votation ongoing" }); } const voteId = assembly.currentVotation; @@ -380,7 +359,7 @@ export async function deactivateVotationStatus( if (!vote) { return res .status(400) - .json({ message: "No votation with the given ID found " }); + .json({ message: "No votation with the given ID found" }); } await Assembly.findByIdAndUpdate(group, { @@ -456,11 +435,7 @@ export async function deleteVotation(req: RequestWithNtnuiNo, res: Response) { } } - for (const optionID of vote.options) { - const oldOptionId = optionID; - await Option.findByIdAndDelete(oldOptionId); - } - + await Option.deleteMany({ _id: { $in: vote.options } }); await Votation.findByIdAndDelete(voteId); await Assembly.findByIdAndUpdate(group, { @@ -532,13 +507,9 @@ export async function editVotation(req: RequestWithNtnuiNo, res: Response) { }); } - for (const optionID of vote.options) { - const oldOptionId = optionID; - await Option.findByIdAndDelete(oldOptionId); - } - - const tempOptionTitles: OptionType[] = []; + await Option.deleteMany({ _id: { $in: vote.options } }); + let insertedOptionIDs: OptionType[] = []; if (options) { if (!Array.isArray(options)) { return res @@ -546,23 +517,21 @@ export async function editVotation(req: RequestWithNtnuiNo, res: Response) { .json({ message: "Options is not on correct format" }); } - for (let i = 0; i < options.length; i++) { - const title: string = options[i]; - - const newOption = new Option({ + const newOptions = await Option.insertMany( + options.map((title) => ({ title: title, - voteCount: 0, - }); - const feedback = await Option.create(newOption); - tempOptionTitles.push(feedback); - } + })) + ); + + // Extract the _id values from the inserted documents + insertedOptionIDs = newOptions.map((option: OptionType) => option._id); } await Votation.findByIdAndUpdate(voteId, { $set: { title: !title ? vote.title : title, voteText: !voteText ? vote.voteText : voteText, - options: !options ? vote.options : tempOptionTitles, + options: !options ? vote.options : insertedOptionIDs, caseNumber: !Number.isFinite(caseNumber) ? vote.caseNumber : caseNumber, @@ -690,7 +659,7 @@ export async function submitVote(req: RequestWithNtnuiNo, res: Response) { // Notify organizers of new vote notifyOrganizers(group, JSON.stringify({ voteSubmitted: 1 })); - return res.status(200).json({ message: "Successfully submited vote" }); + return res.status(200).json({ message: "Successfully submitted vote" }); } } From 233fce3c7c1d4cb854bed43a5100c7479c394cad Mon Sep 17 00:00:00 2001 From: Eivind Dalholt Date: Fri, 29 Sep 2023 13:37:31 +0200 Subject: [PATCH 5/8] chore: clean up currentVotation --- backend/controllers/votation.ts | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/backend/controllers/votation.ts b/backend/controllers/votation.ts index 01cb9d8..dc2fb74 100644 --- a/backend/controllers/votation.ts +++ b/backend/controllers/votation.ts @@ -99,28 +99,20 @@ export async function getCurrentVotation( return res.status(200).json(null); } - const participants: number[] = assembly.participants; - - if (!participants.includes(user._id)) { + if (!assembly.participants.includes(user._id)) { return res .status(400) .json({ message: "This user is not a part of the assembly" }); } - if (!Types.ObjectId.isValid(assembly.currentVotation.toString())) { - return res - .status(400) - .json({ message: "No votation with the given ID found " }); - } - const vote = await Votation.findById(assembly.currentVotation); if (!vote) { - return res.status(400).json({ message: "No votation found" }); + return res + .status(500) + .json({ message: "There was an error getting the votation" }); } - const voted: number[] = vote.voted; - - if (voted.includes(user._id)) { + if (vote.voted.includes(user._id)) { return res.status(200).json(null); } From 04b0ad0848784c55c5cc1bb9e3af348ded594382 Mon Sep 17 00:00:00 2001 From: Eivind Dalholt Date: Fri, 29 Sep 2023 13:41:39 +0200 Subject: [PATCH 6/8] core: remove unneccecary if --- backend/controllers/votation.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/backend/controllers/votation.ts b/backend/controllers/votation.ts index dc2fb74..6f0642d 100644 --- a/backend/controllers/votation.ts +++ b/backend/controllers/votation.ts @@ -142,16 +142,12 @@ export async function createVotation(req: RequestWithNtnuiNo, res: Response) { const group = req.body.group; const title = req.body.title; - let voteText = req.body.voteText; + const voteText = req.body.voteText || ""; const caseNumber = req.body.caseNumber; const options: [] = req.body.options; const maximumOptions = req.body.maximumOptions || 1; const user = await User.findById(req.ntnuiNo); - if (!voteText) { - voteText = ""; - } - if (user) { if ( user.groups.some( From 54014858f45e93a6727fa4511c8b6d23e0e671db Mon Sep 17 00:00:00 2001 From: Eivind Dalholt Date: Fri, 29 Sep 2023 14:23:25 +0200 Subject: [PATCH 7/8] fix: improve submit vote --- backend/controllers/votation.ts | 35 ++++++++------------------------- 1 file changed, 8 insertions(+), 27 deletions(-) diff --git a/backend/controllers/votation.ts b/backend/controllers/votation.ts index 6f0642d..b0210cb 100644 --- a/backend/controllers/votation.ts +++ b/backend/controllers/votation.ts @@ -559,20 +559,8 @@ export async function submitVote(req: RequestWithNtnuiNo, res: Response) { .status(400) .json({ message: "Options is not on correct format" }); } - } - - for (const optionID of optionIDs) { - if (!Types.ObjectId.isValid(optionID)) { - return res - .status(400) - .json({ message: "No option with the given ID found" }); - } - const option = await Option.findById(optionID); - if (!option) { - return res - .status(400) - .json({ message: "No option with the given ID found " }); - } + } else { + return res.status(400).json({ message: "No vote provided" }); } const vote = await Votation.findById(voteId); @@ -614,17 +602,13 @@ export async function submitVote(req: RequestWithNtnuiNo, res: Response) { .json({ message: "You can not vote on this votation" }); } - const participants: number[] = assembly.participants; - - if (!participants.includes(user._id)) { + if (!assembly.participants.includes(user._id)) { return res .status(400) .json({ message: "This user is not a part of the assembly" }); } - const voted: number[] = vote.voted; - - if (voted.indexOf(user._id) !== -1) { + if (vote.voted.includes(user._id)) { return res .status(400) .json({ message: "This user have already voted" }); @@ -635,13 +619,10 @@ export async function submitVote(req: RequestWithNtnuiNo, res: Response) { }, }); - for (const optionID of optionIDs) { - await Option.findByIdAndUpdate(optionID, { - $inc: { - voteCount: 1, - }, - }); - } + await Option.updateMany( + { _id: { $in: optionIDs } }, + { $inc: { voteCount: 1 } } + ); } // Notify organizers of new vote From df65ba0f5d30de8dbe0262406f52e065e7b583df Mon Sep 17 00:00:00 2001 From: Eivind Dalholt Date: Fri, 10 Nov 2023 17:47:49 +0100 Subject: [PATCH 8/8] fix: maximum one vote per option --- backend/controllers/votation.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/backend/controllers/votation.ts b/backend/controllers/votation.ts index b0210cb..a548108 100644 --- a/backend/controllers/votation.ts +++ b/backend/controllers/votation.ts @@ -543,7 +543,7 @@ export async function submitVote(req: RequestWithNtnuiNo, res: Response) { const group = req.body.group; const voteId = req.body.voteId; const user = await User.findById(req.ntnuiNo); - const optionIDs: string[] = req.body.optionIDs; + let optionIDs: string[] = req.body.optionIDs; if (user) { if (user.groups.some((membership) => membership.groupSlug == group)) { @@ -563,6 +563,9 @@ export async function submitVote(req: RequestWithNtnuiNo, res: Response) { return res.status(400).json({ message: "No vote provided" }); } + // Remove duplicates (maximum one vote per option) + optionIDs = [...new Set(optionIDs)]; + const vote = await Votation.findById(voteId); const assembly = await Assembly.findById(group);