diff --git a/controllers/flag.js b/controllers/flag.js index 7ef44bec0..241f5776f 100644 --- a/controllers/flag.js +++ b/controllers/flag.js @@ -13,6 +13,7 @@ var formidable = require('formidable'); //--- Model inclusions var Flag = require('../models/flag').Flag; + var User = require('../models/user').User; var Script = require('../models/script').Script; @@ -109,6 +110,7 @@ exports.flag = function (aReq, aRes, aNext) { }); }); + break; case 'users': username = aReq.params[1]; diff --git a/controllers/script.js b/controllers/script.js index 0e3c66498..c44fda4fc 100644 --- a/controllers/script.js +++ b/controllers/script.js @@ -16,7 +16,6 @@ var SPDX = require('spdx-license-ids'); var Discussion = require('../models/discussion').Discussion; var Group = require('../models/group').Group; var Script = require('../models/script').Script; -var Vote = require('../models/vote').Vote; //--- Controller inclusions var scriptStorage = require('./scriptStorage'); @@ -29,6 +28,7 @@ var getFlaggedListForContent = require('./flag').getFlaggedListForContent; var isSameOrigin = require('../libs/helpers').isSameOrigin; +var voteLib = require('../libs/vote'); var flagLib = require('../libs/flag'); var removeLib = require('../libs/remove'); @@ -233,35 +233,17 @@ var getScriptPageTasks = function (aOptions) { aOptions.voteDownUrl = voteUrl + '/down'; aOptions.unvoteUrl = voteUrl + '/unvote'; - aOptions.voteable = false; - aOptions.votedUp = false; - aOptions.votedDown = false; - - // Can't vote when not logged in or when user owns the script. - if (!authedUser || aOptions.isOwner) { - aCallback(); - return; - } - - Vote.findOne({ - _scriptId: script._id, - _userId: authedUser._id - }, function (aErr, aVoteModel) { - // WARNING: No err handling - - aOptions.voteable = !script.isOwner; - - if (aVoteModel) { - if (aVoteModel.vote) { - aOptions.votedUp = true; + voteLib.voteable(script, authedUser, + function (aCanVote, aAuthor, aVote) { + if (aVote) { + aOptions.votedUp = aVote.vote === true; + aOptions.votedDown = aVote.vote === false; + aOptions.canVote = true; } else { - aOptions.votedDown = true; + aOptions.canVote = aCanVote; } - } - - aCallback(); - }); - + aCallback(); + }); }); // Setup the flagging UI @@ -506,125 +488,3 @@ exports.edit = function (aReq, aRes, aNext) { } }); }; - -// Script voting -exports.vote = function (aReq, aRes, aNext) { - // - var uri = aReq._parsedUrl.pathname.split('/'); - var vote = aReq.params.vote; - var unvote = false; - - var isLib = aReq.params.isLib; - var installNameBase = scriptStorage.getInstallNameBase(aReq); - - // --- - if (uri.length > 5) { - uri.pop(); - } - uri.shift(); - uri.shift(); - uri = '/' + uri.join('/'); - - if (vote === 'up') { - vote = true; - } else if (vote === 'down') { - vote = false; - } else if (vote === 'unvote') { - unvote = true; - } else { - aRes.redirect(uri); - return; - } - - Script.findOne({ - installName: scriptStorage.caseSensitive(installNameBase + - (isLib ? '.js' : '.user.js')) - }, function (aErr, aScript) { - // - var authedUser = aReq.session.user; - - // --- - if (aErr || !aScript) { - aRes.redirect(uri); - return; - } - - Vote.findOne({ _scriptId: aScript._id, _userId: authedUser._id }, - function (aErr, aVoteModel) { - // WARNING: No err handling - - var votes = aScript.votes || 0; - var flags = 0; - var oldVote = null; - - function saveScript() { - if (!flags) { - aScript.save(function (aErr, aScript) { - // WARNING: No err handling - - var script = null; - - if (vote === false) { - script = modelParser.parseScript(aScript); - - // Gently encourage browsing/creating an issue with a down vote - aRes.redirect(script.scriptIssuesPageUri); - } else { - aRes.redirect(uri); - } - }); - return; - } - - flagLib.getAuthor(aScript, function (aAuthor) { - flagLib.saveContent(Script, aScript, aAuthor, flags, false, - function (aFlagged) { - aRes.redirect(uri); - }); - }); - } - - if (!aScript.rating) { - aScript.rating = 0; - } - - if (!aScript.votes) { - aScript.votes = 0; - } - - if (authedUser._id == aScript._authorId || (!aVoteModel && unvote)) { - aRes.redirect(uri); - return; - } else if (!aVoteModel) { - aVoteModel = new Vote({ - vote: vote, - _scriptId: aScript._id, - _userId: authedUser._id - }); - aScript.rating += vote ? 1 : -1; - aScript.votes = votes + 1; - if (vote) { - flags = -1; - } - } else if (unvote) { - oldVote = aVoteModel.vote; - aVoteModel.remove(function () { - aScript.rating += oldVote ? -1 : 1; - aScript.votes = votes <= 0 ? 0 : votes - 1; - if (oldVote) { - flags = 1; - } - saveScript(); - }); - return; - } else if (aVoteModel.vote !== vote) { - aVoteModel.vote = vote; - aScript.rating += vote ? 2 : -2; - flags = vote ? -1 : 1; - } - - aVoteModel.save(saveScript); - } - ); - }); -}; diff --git a/controllers/vote.js b/controllers/vote.js index dc8e7ff5e..746ee05ef 100644 --- a/controllers/vote.js +++ b/controllers/vote.js @@ -8,14 +8,111 @@ var isDbg = require('../libs/debug').isDbg; // //--- Dependency inclusions +var formidable = require('formidable'); //--- Model inclusions +var Script = require('../models/script').Script; //--- Controller inclusions +var scriptStorage = require('./scriptStorage'); + //--- Library inclusions var voteLib = require('../libs/vote'); +var modelParser = require('../libs/modelParser'); + +var statusCodePage = require('../libs/templateHelpers').statusCodePage; + //--- Configuration inclusions //--- + +// Controller for Script voting +exports.vote = function (aReq, aRes, aNext) { + var form = null; + + // Check to make sure multipart form data submission header is present + if (!/multipart\/form-data/.test(aReq.headers['content-type'])) { + statusCodePage(aReq, aRes, aNext, { + statusCode: 400, + statusMessage: 'Missing required header.' + }); + return; + } + + form = new formidable.IncomingForm(); + form.parse(aReq, function (aErr, aFields) { + // WARNING: No err handling + + var vote = aFields.vote; + var unvote = false; + + var type = aReq.params[0]; + var isLib = null; + + var installNameBase = null; + var authedUser = aReq.session.user; + + switch (vote) { + case 'up': + // fallthrough + case 'down': + // fallthrough + case 'un': + break; + default: + statusCodePage(aReq, aRes, aNext, { + statusCode: 400, + statusMessage: 'Missing required field value.' + }); + return; + } + + switch (type) { + case 'libs': + isLib = true; + // fallthrough + case 'scripts': + aReq.params.username = aReq.params[2]; + aReq.params.scriptname = aReq.params[3] + + installNameBase = scriptStorage.getInstallNameBase(aReq); + + Script.findOne({ + installName: scriptStorage.caseSensitive(installNameBase + + (isLib ? '.js' : '.user.js')) + }, function (aErr, aScript) { + var fn = voteLib[vote + 'vote']; + + // --- + if (aErr || !aScript) { + aRes.redirect((isLib ? '/libs/' : '/scripts/') + scriptStorage.getInstallNameBase( + aReq, { encoding: 'uri' })); + return; + } + + fn(aScript, authedUser, function (aErr) { + var script = null; + + if (vote === 'down') { + script = modelParser.parseScript(aScript); + + // Gently encourage browsing/creating an issue with a down vote + aRes.redirect(script.scriptIssuesPageUri); + + } else { + aRes.redirect((isLib ? '/libs/' : '/scripts/') + scriptStorage.getInstallNameBase( + aReq, { encoding: 'uri' })); + } + }); + + }); + + break; + default: + aNext(); + return; + } + }); +}; diff --git a/libs/flag.js b/libs/flag.js index ded8e2aee..38f009ed3 100644 --- a/libs/flag.js +++ b/libs/flag.js @@ -70,7 +70,10 @@ function getFlag(aModel, aContent, aUser, aCallback) { function getAuthor(aContent, aCallback) { User.findOne({ _id: aContent._authorId }, function (aErr, aAuthor) { // Content without an author shouldn't exist - if (aErr || !aAuthor) { return aCallback(null); } + if (aErr || !aAuthor) { + aCallback(null); + return; + } aCallback(aAuthor); }); @@ -152,6 +155,8 @@ function flag(aModel, aContent, aUser, aAuthor, aReason, aCallback) { }); flag.save(function (aErr, aFlag) { + // WARNING: No err handling + if (!aContent.flags) { aContent.flags = {}; } diff --git a/libs/modelParser.js b/libs/modelParser.js index d0a9b8443..19df99e87 100644 --- a/libs/modelParser.js +++ b/libs/modelParser.js @@ -380,6 +380,10 @@ var parseScript = function (aScript) { // Urls: Moderation script.scriptRemovePageUrl = '/remove' + (script.isLib ? '/libs/' : '/scripts/') + script.installNameSlugUrl; + + script.scriptVotePageUrl = '/vote' + (script.isLib ? '/libs/' : '/scripts/') + + script.installNameSlugUrl; + script.scriptFlagPageUrl = '/flag' + (script.isLib ? '/libs/' : '/scripts/') + script.installNameSlugUrl; diff --git a/libs/vote.js b/libs/vote.js index 6108badbb..e4f100db0 100644 --- a/libs/vote.js +++ b/libs/vote.js @@ -5,4 +5,252 @@ var isPro = require('../libs/debug').isPro; var isDev = require('../libs/debug').isDev; var isDbg = require('../libs/debug').isDbg; -// +//--- Model inclusions +var User = require('../models/user').User; +var Vote = require('../models/vote').Vote; +var Script = require('../models/script').Script; + +//--- Library inclusions +var flagLib = require('../libs/flag'); + +//--- + +// Determine whether content can be voted by a user. +function voteable(aScript, aUser, aCallback) { + // No script + if (!aScript) { + aCallback(false); + return; + } + // Not logged in + if (!aUser) { + aCallback(false); + return; + } + + getAuthor(aScript, function (aAuthor) { + // Script without an author shouldn't exist + if (!aAuthor) { + aCallback(false); + return; + } + + // You can't vote on your own Script + if (aAuthor._id == aUser._id) { + aCallback(false); + return; + } + + // Always try to get a Vote + getVote(aScript, aUser, function (aVote) { + aCallback(true, aAuthor, aVote); + return; + }); + }); +} +exports.voteable = voteable; + +function getVote(aScript, aUser, aCallback) { + Vote.findOne({ + '_scriptId': aScript._id, + '_userId': aUser._id + }, function (aErr, aVote) { + if (aErr) { + console.error('DB: getVote failure. aErr :=', aErr); + // fallthrough + } + + aCallback(aErr || (!aVote ? null : aVote)); + }); +} + +function getAuthor(aScript, aCallback) { + User.findOne({ _id: aScript._authorId }, function (aErr, aAuthor) { + // Script without an author shouldn't exist + if (aErr) { + console.error('DB: getAuthor failure. aErr :=', aErr); + aCallback(null); + return; + } + if (!aAuthor) { + console.error('DB: Script has no Author', aScript._id); + aCallback(null); + return; + } + + aCallback(aAuthor); + }); +} +exports.getAuthor = getAuthor; + + +function saveScript(aScript, aAuthor, aFlags, aCallback) { + if (!aScript) { + aCallback(false); + return; + } + + if (!aFlags) { + aScript.save(function (aErr, aScript) { + if (aErr) { + console.error('DB: saveScript failure. aErr :=', aErr); + aCallback('DB: saveScript failed to save flags in voteLib'); + return; + } + + if (!aScript) { + console.error('DB: saveScript failure with no Script'); + aCallback('DB: saveScript failed to save flags in voteLib with no Script'); + return; + } + + aCallback(null); + return; + }); + return; + } + + flagLib.saveContent(Script, aScript, aAuthor, aFlags, false, function (aFlagged) { + aCallback(null); + }); +} +exports.saveScript = saveScript; + +function newVote(aScript, aUser, aAuthor, aCasting, aCallback) { + var vote = new Vote({ + vote: aCasting, + _scriptId: aScript._id, + _userId: aUser._id + }); + + vote.save(function (aErr, aNewVote) { + var flags = null; + + if (aErr) { + console.error('DB: Failed to save new Vote', aErr); + aCallback(false); + return; + } + + if (!aNewVote) { + console.error('DB: New vote not saved. aScript._id :=', aScript._id); + aCallback(false); + return; + } + + aScript.rating += (aCasting ? 1 : -1); + aScript.votes = aScript.votes + (aCasting ? 1 : -1); + if (aCasting) { + flags = -1; + } + + saveScript(aScript, aAuthor, flags, aCallback); + }); +} + +exports.unvote = function (aScript, aUser, aCallback) { + voteable(aScript, aUser, function (aCanVote, aAuthor, aVote) { + var votes = null; + var flags = null; + var casted = null; + + if (!aCanVote || !aVote) { + aCallback(false); + return; + } + if (!aScript.rating) { + aScript.rating = 0; + } + + if (!aScript.votes) { + aScript.votes = 0; + } + + votes = aScript.votes; + flags = 0; + casted = aVote.vote; + + aVote.remove(function (aErr, aRemovedVote) { + if (aErr) { + console.error('DB: Vote removal failure aErr :=', aErr); + aCallback(false); + return; + } + + if (!aRemovedVote) { + console.error('DB: Nothing removed for Vote. aScript._id :=', aScript._id); + aCallback(false); + return; + } + + aScript.rating += (casted ? -1 : 1); + aScript.votes = (votes <= 0 ? 0 : votes - 1); + if (casted) { + flags = 1; + } + + saveScript(aScript, aAuthor, flags, aCallback); + }); + + }); +} + +function vote(aCasting, aScript, aUser, aCallback) { + voteable(aScript, aUser, function (aCanVote, aAuthor, aVote) { + var votes = null; + var flags = 0; + + if (!aCanVote) { + aCallback(false); + return; + } + + if (!aScript.rating) { + aScript.rating = 0; + } + + if (!aScript.votes) { + aScript.votes = 0; + } + + votes = aScript.votes; + + if (aVote) { + if (aVote.vote !== aCasting) { + aVote.vote = aCasting; + aScript.rating += (aCasting ? 2 : -2); + aScript.votes = aScript.votes + (aCasting ? 2 : -2); + flags = aCasting ? -1 : 1; + + } + + aVote.save(function (aErr, aSavedVote) { + if (aErr) { + console.error('DB: Vote saving failure aErr :=', aErr); + aCallback(false); + return; + } + + if (!aSavedVote) { + console.error('DB: Nothing saved for Vote. aScript._id :=', aScript._id); + return; + } + + saveScript(aScript, aAuthor, flags, aCallback); + }); + + } else { + newVote(aScript, aUser, aAuthor, aCasting, aCallback); + } + + }); +} + +exports.upvote = function (aScript, aUser, aCallback) { + vote(true, aScript, aUser, aCallback); +} + +exports.downvote = function (aScript, aUser, aCallback) { + vote(false, aScript, aUser, aCallback); +} + diff --git a/routes.js b/routes.js index ed0be4a42..86b7238c4 100644 --- a/routes.js +++ b/routes.js @@ -15,6 +15,7 @@ var admin = require('./controllers/admin'); var user = require('./controllers/user'); var script = require('./controllers/script'); var flag = require('./controllers/flag'); +var vote = require('./controllers/vote'); var remove = require('./controllers/remove'); var moderation = require('./controllers/moderation'); var group = require('./controllers/group'); @@ -183,12 +184,10 @@ module.exports = function (aApp) { aApp.route('/mod/removed').get(authentication.validateUser, moderation.removedItemListPage); aApp.route('/mod/removed/:id').get(authentication.validateUser, moderation.removedItemPage); - // Vote routes - // TODO: Single vote route + POST - aApp.route('/vote/scripts/:username/:scriptname/:vote').get(authentication.validateUser, script.vote); - aApp.route('/vote/libs/:username/:scriptname/:vote').get(authentication.validateUser, script.lib(script.vote)); + // Vote route + aApp.route(/^\/vote\/(users|scripts|libs)\/((.+?)(?:\/(.+))?)$/).post(authentication.validateUser, vote.vote); - // Flag routes + // Flag route aApp.route(/^\/flag\/(users|scripts|libs)\/((.+?)(?:\/(.+))?)$/).post(authentication.validateUser, flag.flag); // Remove route diff --git a/views/includes/scriptModToolsPanel.html b/views/includes/scriptModToolsPanel.html index 942a65fb0..fe872f6cf 100644 --- a/views/includes/scriptModToolsPanel.html +++ b/views/includes/scriptModToolsPanel.html @@ -10,9 +10,11 @@ {{#script}} {{> includes/flagModelSnippet.html }} {{/script}} + {{#canRemove}} + {{/canRemove}} {{/modTools}} diff --git a/views/includes/scriptUserToolsPanel.html b/views/includes/scriptUserToolsPanel.html index 192ab2d2d..fa053d62a 100644 --- a/views/includes/scriptUserToolsPanel.html +++ b/views/includes/scriptUserToolsPanel.html @@ -2,8 +2,16 @@
- - + {{#canVote}} +
+ + +
+
+ + +
+ {{/canVote}}

Rating: {{script.rating}}

@@ -18,11 +26,11 @@ {{#flagged}}
- +
{{/flagged}} {{^flagged}} - Flag + Flag {{/flagged}}
diff --git a/views/includes/userModToolsPanel.html b/views/includes/userModToolsPanel.html index 8585e0cf6..acca53738 100644 --- a/views/includes/userModToolsPanel.html +++ b/views/includes/userModToolsPanel.html @@ -10,9 +10,11 @@ {{#user}} {{> includes/flagModelSnippet.html }} {{/user}} + {{#canRemove}} + {{/canRemove}}
{{/modTools}}