I am trying to implement a voting system similar to stackoverflow or reddit where a user would only be allowed to vote once on a given post.
After following the advice given here
storing upvotes/downvotes in mongodb
I have created two schemas to store the upvotes and the downvotes. For each user I am keeping track of the posts that user has voted on.
Post Schema :
var postSchema = new Schema({
name: String,
votes: Number,
votetype: Number,
postedBy: { type: String, ref: 'User' },
});
User Schema :
var userSchema = new Schema({
twittername: String,
twitterID: Number,
votedPosts : [{ _id : mongoose.Schema.Types.ObjectId , votetype: Number }]
});
Depending on the the current user each post is going to have a different view, if the user has voted on the post before the upvote button or downvote button is going to be orange (similar to stackoverflow) so I have the following (simplified) backbone model for a post:
var PostModel = Backbone.Model.extend({
urlRoot : '/tweet',
idAttribute: '_id',
defaults:{
name: '',
votes: 0,
votetype: 0,
postedBy : '',
},
upvote: function(){
this.set ({votetype : 1 }, {votes : this.get('votes') + 1});
this.save();
$.ajax({
type: "POST",
url:"/upvote",
data : {postID : this.id , userID : window.userID , vote: 1},
success : function(result){
console.log(result);
},
error: function(jqXHR, textStatus, errorThrown) {
console.log(textStatus, errorThrown);
}
});
},
});
So votetype starts with a "0" if the user hasn't voted on the post before, and its "1" or "-1" depending on the vote. In the upvote function, as I update and save the votetype of that post, I also send a ajax request to add that post to the user's votes posts array in the post controller like the following :
exports.upvote = function(req,res){
var postID = req.body.postID;
var newvotetype = req.body.vote;
User.findOne({twitterID : req.body.userID}, {votedPosts : { $elemMatch: { "_id": postID }}},
function(err, post) {
if (post.votedPosts.length == 0) {
//append to the array
User.update({twitterID : req.body.userID} , { $push : {votedPosts : {_id : postID , votetype: newvotetype}}} ,function (err, user, raw) {
if (err){console.log(err);}
});
console.log(post);
console.log("no one has voted on this before");
}
else {
//update in the existing array
User.update({twitterID : req.body.userID, 'votedPosts._id': postID }, { $set : {'votedPosts.$.votetype' : newvotetype}} ,function (err, user, raw) {
if (err){console.log(err);}
});
}
}
);
res.send("success");
res.end();
};
I might have some bad design decisions but so far it seems like this works fine. Please please tell me if I can make some improvements on my code, or anything else on my design.
Now comes the tricky part. Somehow I have to look through both of these schemas and change the "votetype" of every post before doing a collection.fetch().. I came up with a ugly solution like this :
https://gist.github.com/gorkemyurt/6042558
(i put it in a gits so maybe its more readable, sorry for the ugly code..)
and once I update the vote type of each post depending on the user I pass it to my backbone view, and in my template I do something very basic like:
<div class="post-container">
<div id="arrow-container">
<% if (votetype == 1 ) { %>
<p><img id="arrowup" src="/images/arrow-up-orange.jpg"></p>
<p><img id="arrowdown" src="/images/arrow-down.jpg"></p>
<% } %>
<% if ( votetype == 0 ) { %>
<p><img id="arrowup" src="/images/arrow-up.jpg"></p>
<p><img id="arrowdown" src="/images/arrow-down.jpg"></p>
<% } %>
<% if ( votetype == -1 ) { %>
<p><img id="arrowup" src="/images/arrow-up.jpg"></p>
<p><img id="arrowdown" src="/images/arrow-down-orange.jpg"></p>
<% } %>
</div>
<div id="text-container">
<p><h2><%- name %></h2></p>
<p><%- dateCreated %></p>
<p>Posted by: <%- postedBy %></p>
</div>
</div>
This solution works, but I dont think its really efficient to look up all the posts and all the posts that the user has voted on every-time a user opens the page to render the custom view of posts.. Can anyone think of a better way to do this? I am open to any advice or criticism about my code.. thanks in advance
There're many things that can be improved:
First, you client-side code is a low hanging fruit for an attacker - you do an atomic operation (upvote/downvote) with two requests, and the first request not only sends vote type but also sends total number of votes:
this.set ({votetype : 1 }, {votes : this.get('votes') + 1});
this.save();
// btw this looks broken, the second argument for `set` is options, so
// if you want to set votes, you should move them to the first argument:
this.set ({votetype : 1, votes : this.get('votes') + 1});
But, how your application will respond if the attacker will send 100 or even 1000 votes?
This operation should be atomic, and you should increment votes on the server when you make POST request to the /upvote
endpoint.
Second, you don't really need to store votetype on the post itself - whenever user votes, you change the votetype which is visible for all users but you later hide it with the loop and it just strange to store a votetype of the last vote on the post where you clearly need to have a votetype of the particular user, therefore, you don't need it in the schema and can remote it. You can remove votetype from the posts and you can remove the loop by storing votes history on the post itself, so whenever you want to display a post or a list of posts, you can easily filter the array to contain just the vote of the given user, so you schema will look like this:
var postSchema = new Schema({
name: String,
votesCount: Number,
votes: [{ user_id : mongoose.Schema.Types.ObjectId , type: Number }],
postedBy: { type: String, ref: 'User' },
});
And then you can get a post and filter votes with something like:
Post.findOne({_id:<post-id>)}, function(err, post){
post.vote = post.votes.filter(function(vote){
return vote.user_id === req.body.userID;
})[0].type;
res.send(post)
}
)
// or list of posts
Post.find(function(err, posts){
posts.forEach(function(post){
post.vote = post.votes.filter(function(vote){
return vote.user_id === req.body.userID;
})[0].type;
});
res.send(posts)
}
)
// you can move vote finding logic in a function:
function findVote(post) {
var vote = post.votes.filter(function(vote){
return vote.user_id === req.body.userID;
})[0]
if(vote) return vote.type;
}
If you need to display latest voted posts in the user profile you can filter posts voted by the user:
Post.find({'votes.user_id': req.body.userID}, function(err, posts){
posts.forEach(function(post){
// using findVote defined above
post.vote = findVote(post);
});
res.send(posts)
}
)
Template code on the client side should remain almost the same.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With