From 6f86621e30b4cca862074f3e5f4e9753fd2df446 Mon Sep 17 00:00:00 2001 From: barisusakli Date: Sat, 8 Oct 2016 19:09:48 +0300 Subject: [PATCH] closes #5096 --- public/language/en_GB/error.json | 4 +- public/language/en_GB/pages.json | 2 +- public/language/en_GB/topic.json | 6 +- public/language/en_GB/user.json | 2 +- public/src/client/account/bookmarks.js | 17 ++ public/src/client/account/favourites.js | 17 -- public/src/client/topic/events.js | 26 +- public/src/client/topic/postTools.js | 10 +- public/src/modules/components.js | 4 +- src/controllers/accounts/posts.js | 10 +- src/posts.js | 2 + src/posts/bookmarks.js | 107 ++++++++ src/posts/create.js | 1 - src/posts/delete.js | 10 +- src/{favourites.js => posts/votes.js} | 331 +++++++++--------------- src/routes/accounts.js | 2 +- src/socket.io/posts.js | 9 +- src/socket.io/posts/bookmarks.js | 16 ++ src/socket.io/posts/favourites.js | 162 ------------ src/socket.io/posts/helpers.js | 71 +++++ src/socket.io/posts/tools.js | 9 +- src/socket.io/posts/votes.js | 92 +++++++ src/topics/create.js | 2 +- src/topics/posts.js | 9 +- src/upgrade.js | 59 ++++- src/user/delete.js | 20 +- test/posts.js | 136 ++++++++++ 27 files changed, 676 insertions(+), 460 deletions(-) create mode 100644 public/src/client/account/bookmarks.js delete mode 100644 public/src/client/account/favourites.js create mode 100644 src/posts/bookmarks.js rename src/{favourites.js => posts/votes.js} (65%) create mode 100644 src/socket.io/posts/bookmarks.js delete mode 100644 src/socket.io/posts/favourites.js create mode 100644 src/socket.io/posts/helpers.js create mode 100644 src/socket.io/posts/votes.js create mode 100644 test/posts.js diff --git a/public/language/en_GB/error.json b/public/language/en_GB/error.json index a7f47b9ac3..5a0cc50e52 100644 --- a/public/language/en_GB/error.json +++ b/public/language/en_GB/error.json @@ -88,8 +88,8 @@ "file-too-big": "Maximum allowed file size is %1 kB - please upload a smaller file", "guest-upload-disabled": "Guest uploading has been disabled", - "already-favourited": "You have already bookmarked this post", - "already-unfavourited": "You have already unbookmarked this post", + "already-bookmarked": "You have already bookmarked this post", + "already-unbookmarked": "You have already unbookmarked this post", "cant-ban-other-admins": "You can't ban other admins!", "cant-remove-last-admin": "You are the only administrator. Add another user as an administrator before removing yourself as admin", diff --git a/public/language/en_GB/pages.json b/public/language/en_GB/pages.json index c9b2014d79..52d9f2b4e3 100644 --- a/public/language/en_GB/pages.json +++ b/public/language/en_GB/pages.json @@ -41,7 +41,7 @@ "account/posts": "Posts made by %1", "account/topics": "Topics created by %1", "account/groups": "%1's Groups", - "account/favourites": "%1's Bookmarked Posts", + "account/bookmarks": "%1's Bookmarked Posts", "account/settings": "User Settings", "account/watched": "Topics watched by %1", "account/upvoted": "Posts upvoted by %1", diff --git a/public/language/en_GB/topic.json b/public/language/en_GB/topic.json index 36daa3d044..af49c4763e 100644 --- a/public/language/en_GB/topic.json +++ b/public/language/en_GB/topic.json @@ -104,9 +104,9 @@ "confirm_move": "Move", "confirm_fork": "Fork", - "favourite": "Bookmark", - "favourites": "Bookmarks", - "favourites.has_no_favourites": "You haven't bookmarked any posts yet.", + "bookmark": "Bookmark", + "bookmarks": "Bookmarks", + "bookmarks.has_no_bookmarks": "You haven't bookmarked any posts yet.", "loading_more_posts": "Loading More Posts", "move_topic": "Move Topic", diff --git a/public/language/en_GB/user.json b/public/language/en_GB/user.json index 10a21ff03e..fd6f11e250 100644 --- a/public/language/en_GB/user.json +++ b/public/language/en_GB/user.json @@ -25,7 +25,7 @@ "profile": "Profile", "profile_views": "Profile views", "reputation": "Reputation", - "favourites":"Bookmarks", + "bookmarks":"Bookmarks", "watched": "Watched", "followers": "Followers", "following": "Following", diff --git a/public/src/client/account/bookmarks.js b/public/src/client/account/bookmarks.js new file mode 100644 index 0000000000..d1aca2080e --- /dev/null +++ b/public/src/client/account/bookmarks.js @@ -0,0 +1,17 @@ +'use strict'; + +/* globals define */ + +define('forum/account/bookmarks', ['forum/account/header', 'forum/account/posts'], function(header, posts) { + var Bookmarks = {}; + + Bookmarks.init = function() { + header.init(); + + $('[component="post/content"] img:not(.not-responsive)').addClass('img-responsive'); + + posts.handleInfiniteScroll('posts.loadMoreBookmarks', 'account/bookmarks'); + }; + + return Bookmarks; +}); diff --git a/public/src/client/account/favourites.js b/public/src/client/account/favourites.js deleted file mode 100644 index 9d732418c4..0000000000 --- a/public/src/client/account/favourites.js +++ /dev/null @@ -1,17 +0,0 @@ -'use strict'; - -/* globals define, app, utils */ - -define('forum/account/favourites', ['forum/account/header', 'forum/account/posts'], function(header, posts) { - var Favourites = {}; - - Favourites.init = function() { - header.init(); - - $('[component="post/content"] img:not(.not-responsive)').addClass('img-responsive'); - - posts.handleInfiniteScroll('posts.loadMoreFavourites', 'account/favourites'); - }; - - return Favourites; -}); diff --git a/public/src/client/topic/events.js b/public/src/client/topic/events.js index 4d8434adb8..bba3cb0dcb 100644 --- a/public/src/client/topic/events.js +++ b/public/src/client/topic/events.js @@ -16,7 +16,7 @@ define('forum/topic/events', [ var events = { 'event:user_status_change': onUserStatusChange, 'event:voted': updatePostVotesAndUserReputation, - 'event:favourited': updateFavouriteCount, + 'event:bookmarked': updateBookmarkCount, 'event:topic_deleted': threadTools.setDeleteState, 'event:topic_restored': threadTools.setDeleteState, @@ -36,8 +36,8 @@ define('forum/topic/events', [ 'event:post_deleted': togglePostDeleteState, 'event:post_restored': togglePostDeleteState, - 'posts.favourite': togglePostFavourite, - 'posts.unfavourite': togglePostFavourite, + 'posts.bookmark': togglePostBookmark, + 'posts.unbookmark': togglePostBookmark, 'posts.upvote': togglePostVote, 'posts.downvote': togglePostVote, @@ -69,15 +69,15 @@ define('forum/topic/events', [ } function updatePostVotesAndUserReputation(data) { - var votes = components.get('post/vote-count', data.post.pid), - reputationElements = $('.reputation[data-uid="' + data.post.uid + '"]'); + var votes = components.get('post/vote-count', data.post.pid); + var reputationElements = $('.reputation[data-uid="' + data.post.uid + '"]'); votes.html(data.post.votes).attr('data-votes', data.post.votes); reputationElements.html(data.user.reputation).attr('data-reputation', data.user.reputation); } - function updateFavouriteCount(data) { - $('[data-pid="' + data.post.pid + '"] .favouriteCount').html(data.post.reputation).attr('data-favourites', data.post.reputation); + function updateBookmarkCount(data) { + $('[data-pid="' + data.post.pid + '"] .bookmarkCount').html(data.post.bookmarks).attr('data-bookmarks', data.post.bookmarks); } function onTopicPurged() { @@ -198,17 +198,17 @@ define('forum/topic/events', [ } } - function togglePostFavourite(data) { - var favBtn = $('[data-pid="' + data.post.pid + '"] [component="post/favourite"]'); + function togglePostBookmark(data) { + var el = $('[data-pid="' + data.post.pid + '"] [component="post/bookmark"]'); - if (!favBtn.length) { + if (!el.length) { return; } - favBtn.attr('data-favourited', data.isFavourited); + el.attr('data-bookmarked', data.isBookmarked); - favBtn.find('[component="post/favourite/on"]').toggleClass('hidden', !data.isFavourited); - favBtn.find('[component="post/favourite/off"]').toggleClass('hidden', data.isFavourited); + el.find('[component="post/bookmark/on"]').toggleClass('hidden', !data.isBookmarked); + el.find('[component="post/bookmark/off"]').toggleClass('hidden', data.isBookmarked); } function togglePostVote(data) { diff --git a/public/src/client/topic/postTools.js b/public/src/client/topic/postTools.js index 86fe03ce03..b5095384da 100644 --- a/public/src/client/topic/postTools.js +++ b/public/src/client/topic/postTools.js @@ -52,7 +52,7 @@ define('forum/topic/postTools', ['share', 'navigator', 'components', 'translator PostTools.toggle = function(pid, isDeleted) { var postEl = components.get('post', 'pid', pid); - postEl.find('[component="post/quote"], [component="post/favourite"], [component="post/reply"], [component="post/flag"], [component="user/chat"]') + postEl.find('[component="post/quote"], [component="post/bookmark"], [component="post/reply"], [component="post/flag"], [component="user/chat"]') .toggleClass('hidden', isDeleted); postEl.find('[component="post/delete"]').toggleClass('hidden', isDeleted); @@ -149,8 +149,8 @@ define('forum/topic/postTools', ['share', 'navigator', 'components', 'translator }); }); - postContainer.on('click', '[component="post/favourite"]', function() { - favouritePost($(this), getData($(this), 'data-pid')); + postContainer.on('click', '[component="post/bookmark"]', function() { + bookmarkPost($(this), getData($(this), 'data-pid')); }); postContainer.on('click', '[component="post/upvote"]', function() { @@ -330,8 +330,8 @@ define('forum/topic/postTools', ['share', 'navigator', 'components', 'translator return selectionText; } - function favouritePost(button, pid) { - var method = button.attr('data-favourited') === 'false' ? 'posts.favourite' : 'posts.unfavourite'; + function bookmarkPost(button, pid) { + var method = button.attr('data-bookmarked') === 'false' ? 'posts.bookmark' : 'posts.unbookmark'; socket.emit(method, { pid: pid, diff --git a/public/src/modules/components.js b/public/src/modules/components.js index 58773d9821..4a08e4073c 100644 --- a/public/src/modules/components.js +++ b/public/src/modules/components.js @@ -28,8 +28,8 @@ define('components', function() { 'post/vote-count': function(pid) { return components.core.post('pid', pid).find('[component="post/vote-count"]'); }, - 'post/favourite-count': function(pid) { - return components.core.post('pid', pid).find('[component="post/favourite-count"]'); + 'post/bookmark-count': function(pid) { + return components.core.post('pid', pid).find('[component="post/bookmark-count"]'); }, 'user/postcount': function(uid) { diff --git a/src/controllers/accounts/posts.js b/src/controllers/accounts/posts.js index 08737f0cde..53450c9673 100644 --- a/src/controllers/accounts/posts.js +++ b/src/controllers/accounts/posts.js @@ -13,14 +13,14 @@ var accountHelpers = require('./helpers'); var postsController = {}; -postsController.getFavourites = function(req, res, next) { +postsController.getBookmarks = function(req, res, next) { var data = { - template: 'account/favourites', - set: 'favourites', + template: 'account/bookmarks', + set: 'bookmarks', type: 'posts', - noItemsFoundKey: '[[topic:favourites.has_no_favourites]]', + noItemsFoundKey: '[[topic:bookmarks.has_no_bookmarks]]', method: posts.getPostSummariesFromSet, - crumb: '[[user:favourites]]' + crumb: '[[user:bookmarks]]' }; getFromUserSet(data, req, res, next); }; diff --git a/src/posts.js b/src/posts.js index 7062052433..3a08272f70 100644 --- a/src/posts.js +++ b/src/posts.js @@ -23,6 +23,8 @@ var plugins = require('./plugins'); require('./posts/recent')(Posts); require('./posts/flags')(Posts); require('./posts/tools')(Posts); + require('./posts/votes')(Posts); + require('./posts/bookmarks')(Posts); Posts.exists = function(pid, callback) { db.isSortedSetMember('posts:pid', pid, callback); diff --git a/src/posts/bookmarks.js b/src/posts/bookmarks.js new file mode 100644 index 0000000000..14bf585647 --- /dev/null +++ b/src/posts/bookmarks.js @@ -0,0 +1,107 @@ +'use strict'; + +var async = require('async'); + +var db = require('../database'); +var plugins = require('../plugins'); + +module.exports = function(Posts) { + + Posts.bookmark = function (pid, uid, callback) { + toggleBookmark('bookmark', pid, uid, callback); + }; + + Posts.unbookmark = function(pid, uid, callback) { + toggleBookmark('unbookmark', pid, uid, callback); + }; + + function toggleBookmark(type, pid, uid, callback) { + if (!parseInt(uid, 10)) { + return callback(new Error('[[error:not-logged-in]]')); + } + var isBookmarking = type === 'bookmark'; + + async.parallel({ + owner: function(next) { + Posts.getPostField(pid, 'uid', next); + }, + postData: function(next) { + Posts.getPostFields(pid, ['pid', 'uid'], next); + }, + hasBookmarked: function(next) { + Posts.hasBookmarked(pid, uid, next); + } + }, function(err, results) { + if (err) { + return callback(err); + } + + if (isBookmarking && results.hasBookmarked) { + return callback(new Error('[[error:already-bookmarked]]')); + } + + if (!isBookmarking && !results.hasBookmarked) { + return callback(new Error('[[error:already-unbookmarked]]')); + } + + async.waterfall([ + function(next) { + if (isBookmarking) { + db.sortedSetAdd('uid:' + uid + ':bookmarks', Date.now(), pid, next); + } else { + db.sortedSetRemove('uid:' + uid + ':bookmarks', pid, next); + } + }, + function(next) { + db[isBookmarking ? 'setAdd' : 'setRemove']('pid:' + pid + ':users_bookmarked', uid, next); + }, + function(next) { + db.setCount('pid:' + pid + ':users_bookmarked', next); + }, + function(count, next) { + results.postData.bookmarks = count; + Posts.setPostField(pid, 'bookmarks', count, next); + } + ], function(err) { + if (err) { + return callback(err); + } + + var current = results.hasBookmarked ? 'bookmarked' : 'unbookmarked'; + + plugins.fireHook('action:post.' + type, { + pid: pid, + uid: uid, + owner: results.owner, + current: current + }); + + callback(null, { + post: results.postData, + isBookmarked: isBookmarking + }); + }); + }); + } + + Posts.hasBookmarked = function(pid, uid, callback) { + if (!parseInt(uid, 10)) { + if (Array.isArray(pid)) { + callback(null, pid.map(function() { return false; })); + } else { + callback(null, false); + } + return; + } + + if (Array.isArray(pid)) { + var sets = pid.map(function(pid) { + return 'pid:' + pid + ':users_bookmarked'; + }); + + db.isMemberOfSets(sets, uid, callback); + } else { + db.isSetMember('pid:' + pid + ':users_bookmarked', uid, callback); + } + }; +}; diff --git a/src/posts/create.js b/src/posts/create.js index 8884a3d07e..29391bda5f 100644 --- a/src/posts/create.js +++ b/src/posts/create.js @@ -38,7 +38,6 @@ module.exports = function(Posts) { 'tid': tid, 'content': content, 'timestamp': timestamp, - 'reputation': 0, 'editor': '', 'edited': 0, 'deleted': 0 diff --git a/src/posts/delete.js b/src/posts/delete.js index 100a39eeb8..522f6855ab 100644 --- a/src/posts/delete.js +++ b/src/posts/delete.js @@ -132,7 +132,7 @@ module.exports = function(Posts) { deletePostFromCategoryRecentPosts(pid, next); }, function (next) { - deletePostFromUsersFavourites(pid, next); + deletePostFromUsersBookmarks(pid, next); }, function (next) { deletePostFromUsersVotes(pid, next); @@ -219,14 +219,14 @@ module.exports = function(Posts) { }); } - function deletePostFromUsersFavourites(pid, callback) { - db.getSetMembers('pid:' + pid + ':users_favourited', function(err, uids) { + function deletePostFromUsersBookmarks(pid, callback) { + db.getSetMembers('pid:' + pid + ':users_bookmarked', function(err, uids) { if (err) { return callback(err); } var sets = uids.map(function(uid) { - return 'uid:' + uid + ':favourites'; + return 'uid:' + uid + ':bookmarks'; }); db.sortedSetsRemove(sets, pid, function(err) { @@ -234,7 +234,7 @@ module.exports = function(Posts) { return callback(err); } - db.delete('pid:' + pid + ':users_favourited', callback); + db.delete('pid:' + pid + ':users_bookmarked', callback); }); }); } diff --git a/src/favourites.js b/src/posts/votes.js similarity index 65% rename from src/favourites.js rename to src/posts/votes.js index 543b41af98..f089e62e4b 100644 --- a/src/favourites.js +++ b/src/posts/votes.js @@ -1,104 +1,17 @@ -"use strict"; +'use strict'; var async = require('async'); -var db = require('./database'); -var posts = require('./posts'); -var user = require('./user'); -var plugins = require('./plugins'); -var meta = require('./meta'); -(function (Favourites) { +var meta = require('../meta'); +var db = require('../database'); +var user = require('../user'); +var plugins = require('../plugins'); - var votesInProgress = {}; - - function vote(type, unvote, pid, uid, callback) { - uid = parseInt(uid, 10); - - if (uid === 0) { - return callback(new Error('[[error:not-logged-in]]')); - } - - posts.getPostFields(pid, ['pid', 'uid', 'tid'], function (err, postData) { - if (err) { - return callback(err); - } - - var now = Date.now(); - - if (type === 'upvote' && !unvote) { - db.sortedSetAdd('uid:' + uid + ':upvote', now, pid); - } else { - db.sortedSetRemove('uid:' + uid + ':upvote', pid); - } - - if (type === 'upvote' || unvote) { - db.sortedSetRemove('uid:' + uid + ':downvote', pid); - } else { - db.sortedSetAdd('uid:' + uid + ':downvote', now, pid); - } - - user[type === 'upvote' ? 'incrementUserFieldBy' : 'decrementUserFieldBy'](postData.uid, 'reputation', 1, function (err, newreputation) { - if (err) { - return callback(err); - } - - if (parseInt(postData.uid, 10)) { - db.sortedSetAdd('users:reputation', newreputation, postData.uid); - } - - adjustPostVotes(postData, uid, type, unvote, function(err) { - callback(err, { - user: { - reputation: newreputation - }, - post: postData, - upvote: type === 'upvote' && !unvote, - downvote: type === 'downvote' && !unvote - }); - }); - }); - }); - } - - function adjustPostVotes(postData, uid, type, unvote, callback) { - var notType = (type === 'upvote' ? 'downvote' : 'upvote'); - - async.series([ - function(next) { - if (unvote) { - db.setRemove('pid:' + postData.pid + ':' + type, uid, next); - } else { - db.setAdd('pid:' + postData.pid + ':' + type, uid, next); - } - }, - function(next) { - db.setRemove('pid:' + postData.pid + ':' + notType, uid, next); - } - ], function(err) { - if (err) { - return callback(err); - } +module.exports = function(Posts) { - async.parallel({ - upvotes: function(next) { - db.setCount('pid:' + postData.pid + ':upvote', next); - }, - downvotes: function(next) { - db.setCount('pid:' + postData.pid + ':downvote', next); - } - }, function(err, results) { - if (err) { - return callback(err); - } - postData.upvotes = parseInt(results.upvotes, 10); - postData.downvotes = parseInt(results.downvotes, 10); - postData.votes = postData.upvotes - postData.downvotes; - posts.updatePostVoteCount(postData, callback); - }); - }); - } + var votesInProgress = {}; - Favourites.upvote = function(pid, uid, callback) { + Posts.upvote = function(pid, uid, callback) { if (parseInt(meta.config['reputation:disabled'], 10) === 1) { return callback(new Error('[[error:reputation-system-disabled]]')); } @@ -115,7 +28,7 @@ var meta = require('./meta'); }); }; - Favourites.downvote = function(pid, uid, callback) { + Posts.downvote = function(pid, uid, callback) { if (parseInt(meta.config['reputation:disabled'], 10) === 1) { return callback(new Error('[[error:reputation-system-disabled]]')); } @@ -136,7 +49,7 @@ var meta = require('./meta'); }); }; - Favourites.unvote = function(pid, uid, callback) { + Posts.unvote = function(pid, uid, callback) { if (voteInProgress(pid, uid)) { return callback(new Error('[[error:already-voting-for-this-post]]')); } @@ -149,6 +62,50 @@ var meta = require('./meta'); }); }; + Posts.hasVoted = function(pid, uid, callback) { + if (!parseInt(uid, 10)) { + return callback(null, {upvoted: false, downvoted: false}); + } + + db.isMemberOfSets(['pid:' + pid + ':upvote', 'pid:' + pid + ':downvote'], uid, function(err, hasVoted) { + if (err) { + return callback(err); + } + + callback (null, {upvoted: hasVoted[0], downvoted: hasVoted[1]}); + }); + }; + + Posts.getVoteStatusByPostIDs = function(pids, uid, callback) { + if (!parseInt(uid, 10)) { + var data = pids.map(function() { return false; }); + return callback(null, {upvotes: data, downvotes: data}); + } + var upvoteSets = []; + var downvoteSets = []; + + for (var i=0; i 6) { - otherCount = uids.length - 5; - uids = uids.slice(0, 5); - } - user.getUsernamesByUids(uids, function(err, usernames) { - next(err, { - otherCount: otherCount, - usernames: usernames - }); - }); - }, callback); - }); - }; - - SocketPosts.upvote = function(socket, data, callback) { - favouriteCommand(socket, 'upvote', 'voted', 'notifications:upvoted_your_post_in', data, callback); - }; - - SocketPosts.downvote = function(socket, data, callback) { - favouriteCommand(socket, 'downvote', 'voted', '', data, callback); - }; - - SocketPosts.unvote = function(socket, data, callback) { - favouriteCommand(socket, 'unvote', 'voted', '', data, callback); - }; - - SocketPosts.favourite = function(socket, data, callback) { - favouriteCommand(socket, 'favourite', 'favourited', '', data, callback); - }; - - SocketPosts.unfavourite = function(socket, data, callback) { - favouriteCommand(socket, 'unfavourite', 'favourited', '', data, callback); - }; - - function favouriteCommand(socket, command, eventName, notification, data, callback) { - if (!socket.uid) { - return callback(new Error('[[error:not-logged-in]]')) - } - if (!data || !data.pid || !data.room_id) { - return callback(new Error('[[error:invalid-data]]')); - } - async.parallel({ - exists: function(next) { - posts.exists(data.pid, next); - }, - deleted: function(next) { - posts.getPostField(data.pid, 'deleted', next); - } - }, function(err, results) { - if (err || !results.exists) { - return callback(err || new Error('[[error:invalid-pid]]')); - } - - if (parseInt(results.deleted, 10) === 1) { - return callback(new Error('[[error:post-deleted]]')); - } - - /* - hooks: - filter:post.upvote - filter:post.downvote - filter:post.unvote - filter:post.favourite - filter:post.unfavourite - */ - plugins.fireHook('filter:post.' + command, {data: data, uid: socket.uid}, function(err, filteredData) { - if (err) { - return callback(err); - } - - executeFavouriteCommand(socket, command, eventName, notification, filteredData.data, callback); - }); - }); - } - - function executeFavouriteCommand(socket, command, eventName, notification, data, callback) { - favourites[command](data.pid, socket.uid, function(err, result) { - if (err) { - return callback(err); - } - - if (result && eventName) { - socket.emit('posts.' + command, result); - websockets.in(data.room_id).emit('event:' + eventName, result); - } - - if (result && notification) { - socketHelpers.sendNotificationToPostOwner(data.pid, socket.uid, command, notification); - } else if (result && command === 'unvote') { - socketHelpers.rescindUpvoteNotification(data.pid, socket.uid); - } - callback(); - }); - } -}; \ No newline at end of file diff --git a/src/socket.io/posts/helpers.js b/src/socket.io/posts/helpers.js new file mode 100644 index 0000000000..ad5d46c906 --- /dev/null +++ b/src/socket.io/posts/helpers.js @@ -0,0 +1,71 @@ +'use strict'; + + +var async = require('async'); +var posts = require('../../posts'); +var plugins = require('../../plugins'); +var websockets = require('../index'); +var socketHelpers = require('../helpers'); + +var helpers = module.exports; + +helpers.postCommand = function(socket, command, eventName, notification, data, callback) { + if (!socket.uid) { + return callback(new Error('[[error:not-logged-in]]')); + } + if (!data || !data.pid || !data.room_id) { + return callback(new Error('[[error:invalid-data]]')); + } + async.parallel({ + exists: function(next) { + posts.exists(data.pid, next); + }, + deleted: function(next) { + posts.getPostField(data.pid, 'deleted', next); + } + }, function(err, results) { + if (err || !results.exists) { + return callback(err || new Error('[[error:invalid-pid]]')); + } + + if (parseInt(results.deleted, 10) === 1) { + return callback(new Error('[[error:post-deleted]]')); + } + + /* + hooks: + filter:post.upvote + filter:post.downvote + filter:post.unvote + filter:post.bookmark + filter:post.unbookmark + */ + plugins.fireHook('filter:post.' + command, {data: data, uid: socket.uid}, function(err, filteredData) { + if (err) { + return callback(err); + } + + executeCommand(socket, command, eventName, notification, filteredData.data, callback); + }); + }); +}; + +function executeCommand(socket, command, eventName, notification, data, callback) { + posts[command](data.pid, socket.uid, function(err, result) { + if (err) { + return callback(err); + } + + if (result && eventName) { + socket.emit('posts.' + command, result); + websockets.in(data.room_id).emit('event:' + eventName, result); + } + + if (result && notification) { + socketHelpers.sendNotificationToPostOwner(data.pid, socket.uid, command, notification); + } else if (result && command === 'unvote') { + socketHelpers.rescindUpvoteNotification(data.pid, socket.uid); + } + callback(); + }); +} \ No newline at end of file diff --git a/src/socket.io/posts/tools.js b/src/socket.io/posts/tools.js index 91e8dca241..fcd076f124 100644 --- a/src/socket.io/posts/tools.js +++ b/src/socket.io/posts/tools.js @@ -12,7 +12,6 @@ var socketTopics = require('../topics'); var privileges = require('../../privileges'); var plugins = require('../../plugins'); var social = require('../../social'); -var favourites = require('../../favourites'); module.exports = function(SocketPosts) { @@ -23,7 +22,7 @@ module.exports = function(SocketPosts) { async.parallel({ posts: function(next) { - posts.getPostFields(data.pid, ['deleted', 'reputation', 'uid'], next); + posts.getPostFields(data.pid, ['deleted', 'bookmarks', 'uid'], next); }, isAdminOrMod: function(next) { privileges.categories.isAdminOrMod(data.cid, socket.uid, next); @@ -34,8 +33,8 @@ module.exports = function(SocketPosts) { canDelete: function(next) { privileges.posts.canDelete(data.pid, socket.uid, next); }, - favourited: function(next) { - favourites.getFavouritesByPostIDs([data.pid], socket.uid, next); + bookmarked: function(next) { + posts.hasBookmarked(data.pid, socket.uid, next); }, tools: function(next) { plugins.fireHook('filter:post.tools', {pid: data.pid, uid: socket.uid, tools: []}, next); @@ -50,7 +49,7 @@ module.exports = function(SocketPosts) { results.posts.tools = results.tools.tools; results.posts.deleted = parseInt(results.posts.deleted, 10) === 1; - results.posts.favourited = results.favourited[0]; + results.posts.bookmarked = results.bookmarked; results.posts.selfPost = socket.uid && socket.uid === parseInt(results.posts.uid, 10); results.posts.display_edit_tools = results.canEdit.flag; results.posts.display_delete_tools = results.canDelete.flag; diff --git a/src/socket.io/posts/votes.js b/src/socket.io/posts/votes.js new file mode 100644 index 0000000000..2a31ce87a5 --- /dev/null +++ b/src/socket.io/posts/votes.js @@ -0,0 +1,92 @@ +'use strict'; + +var async = require('async'); + +var db = require('../../database'); +var user = require('../../user'); +var posts = require('../../posts'); +var privileges = require('../../privileges'); +var helpers = require('./helpers'); + +module.exports = function(SocketPosts) { + + SocketPosts.getVoters = function(socket, data, callback) { + if (!data || !data.pid || !data.cid) { + return callback(new Error('[[error:invalid-data]]')); + } + + async.waterfall([ + function (next) { + privileges.categories.isAdminOrMod(data.cid, socket.uid, next); + }, + function (isAdminOrMod, next) { + if (!isAdminOrMod) { + return next(new Error('[[error:no-privileges]]')); + } + + async.parallel({ + upvoteUids: function(next) { + db.getSetMembers('pid:' + data.pid + ':upvote', next); + }, + downvoteUids: function(next) { + db.getSetMembers('pid:' + data.pid + ':downvote', next); + } + }, next); + }, + function (results, next) { + async.parallel({ + upvoters: function(next) { + user.getUsersFields(results.upvoteUids, ['username', 'userslug', 'picture'], next); + }, + upvoteCount: function(next) { + next(null, results.upvoteUids.length); + }, + downvoters: function(next) { + user.getUsersFields(results.downvoteUids, ['username', 'userslug', 'picture'], next); + }, + downvoteCount: function(next) { + next(null, results.downvoteUids.length); + } + }, next); + } + ], callback); + }; + + SocketPosts.getUpvoters = function(socket, pids, callback) { + if (!Array.isArray(pids)) { + return callback(new Error('[[error:invalid-data]]')); + } + + posts.getUpvotedUidsByPids(pids, function(err, data) { + if (err || !Array.isArray(data) || !data.length) { + return callback(err, []); + } + + async.map(data, function(uids, next) { + var otherCount = 0; + if (uids.length > 6) { + otherCount = uids.length - 5; + uids = uids.slice(0, 5); + } + user.getUsernamesByUids(uids, function(err, usernames) { + next(err, { + otherCount: otherCount, + usernames: usernames + }); + }); + }, callback); + }); + }; + + SocketPosts.upvote = function(socket, data, callback) { + helpers.postCommand(socket, 'upvote', 'voted', 'notifications:upvoted_your_post_in', data, callback); + }; + + SocketPosts.downvote = function(socket, data, callback) { + helpers.postCommand(socket, 'downvote', 'voted', '', data, callback); + }; + + SocketPosts.unvote = function(socket, data, callback) { + helpers.postCommand(socket, 'unvote', 'voted', '', data, callback); + }; +}; \ No newline at end of file diff --git a/src/topics/create.js b/src/topics/create.js index 85491a0643..de50ac1cae 100644 --- a/src/topics/create.js +++ b/src/topics/create.js @@ -310,8 +310,8 @@ module.exports = function(Topics) { postData.user.username = validator.escape(String(data.handle)); } - postData.favourited = false; postData.votes = 0; + postData.bookmarked = false; postData.display_edit_tools = true; postData.display_delete_tools = true; postData.display_moderator_tools = true; diff --git a/src/topics/posts.js b/src/topics/posts.js index 3c49fa29ee..658eac69c1 100644 --- a/src/topics/posts.js +++ b/src/topics/posts.js @@ -7,7 +7,6 @@ var validator = require('validator'); var db = require('../database'); var user = require('../user'); -var favourites = require('../favourites'); var posts = require('../posts'); var meta = require('../meta'); @@ -60,11 +59,11 @@ module.exports = function(Topics) { } async.parallel({ - favourites: function(next) { - favourites.getFavouritesByPostIDs(pids, uid, next); + bookmarks: function(next) { + posts.hasBookmarked(pids, uid, next); }, voteData: function(next) { - favourites.getVoteStatusByPostIDs(pids, uid, next); + posts.getVoteStatusByPostIDs(pids, uid, next); }, userData: function(next) { var uids = []; @@ -120,7 +119,7 @@ module.exports = function(Topics) { postObj.deleted = parseInt(postObj.deleted, 10) === 1; postObj.user = parseInt(postObj.uid, 10) ? results.userData[postObj.uid] : _.clone(results.userData[postObj.uid]); postObj.editor = postObj.editor ? results.editors[postObj.editor] : null; - postObj.favourited = results.favourites[i]; + postObj.bookmarked = results.bookmarks[i]; postObj.upvoted = results.voteData.upvotes[i]; postObj.downvoted = results.voteData.downvotes[i]; postObj.votes = postObj.votes || 0; diff --git a/src/upgrade.js b/src/upgrade.js index c6fbe767e3..791aefacae 100644 --- a/src/upgrade.js +++ b/src/upgrade.js @@ -10,7 +10,7 @@ var db = require('./database'), schemaDate, thisSchemaDate, // IMPORTANT: REMEMBER TO UPDATE VALUE OF latestSchema - latestSchema = Date.UTC(2016, 8, 22); + latestSchema = Date.UTC(2016, 9, 8); Upgrade.check = function(callback) { db.get('schemaDate', function(err, value) { @@ -850,6 +850,63 @@ Upgrade.upgrade = function(callback) { winston.info('[2016/09/22] Setting category recent tids - skipped!'); next(); } + }, + function(next) { + function upgradePosts(next) { + var batch = require('./batch'); + + batch.processSortedSet('posts:pid', function(ids, next) { + async.each(ids, function(id, next) { + console.log('processing pid ' + id); + async.waterfall([ + function(next) { + db.rename('pid:' + id + ':users_favourited', 'pid:' + id + ':users_bookmarked', next); + }, + function(next) { + db.getObjectField('post:' + id, 'reputation', next); + }, + function(reputation, next) { + if (parseInt(reputation, 10)) { + db.setObjectField('post:' + id, 'bookmarks', reputation, next); + } else { + next(); + } + }, + function(next) { + db.deleteObjectField('post:' + id, 'reputation', next); + } + ], next); + }, next); + }, {}, next); + } + + function upgradeUsers(next) { + var batch = require('./batch'); + + batch.processSortedSet('users:joindate', function(ids, next) { + async.each(ids, function(id, next) { + console.log('processing uid ' + id); + db.rename('uid:' + id + ':favourites', 'uid:' + id + ':bookmarks', next); + }, next); + }, {}, next); + } + + thisSchemaDate = Date.UTC(2016, 9, 8); + + if (schemaDate < thisSchemaDate || 1) { + updatesMade = true; + winston.info('[2016/10/8] favourite -> bookmark refactor'); + async.series([upgradePosts, upgradeUsers], function(err) { + if (err) { + return next(err); + } + winston.info('[2016/08/05] favourite- bookmark refactor done!'); + Upgrade.update(thisSchemaDate, next); + }); + } else { + winston.info('[2016/10/8] favourite -> bookmark refactor - skipped!'); + next(); + } } // Add new schema updates here // IMPORTANT: REMEMBER TO UPDATE VALUE OF latestSchema IN LINE 24!!! diff --git a/src/user/delete.js b/src/user/delete.js index 66d351cdf2..1e88c90a5a 100644 --- a/src/user/delete.js +++ b/src/user/delete.js @@ -1,13 +1,13 @@ 'use strict'; -var async = require('async'), - db = require('../database'), - posts = require('../posts'), - topics = require('../topics'), - favourites = require('../favourites'), - groups = require('../groups'), - plugins = require('../plugins'), - batch = require('../batch'); +var async = require('async'); + +var db = require('../database'); +var posts = require('../posts'); +var topics = require('../topics'); +var groups = require('../groups'); +var plugins = require('../plugins'); +var batch = require('../batch'); module.exports = function(User) { @@ -107,7 +107,7 @@ module.exports = function(User) { var keys = [ 'uid:' + uid + ':notifications:read', 'uid:' + uid + ':notifications:unread', - 'uid:' + uid + ':favourites', + 'uid:' + uid + ':bookmarks', 'uid:' + uid + ':followed_tids', 'uid:' + uid + ':ignored_tids', 'user:' + uid + ':settings', @@ -151,7 +151,7 @@ module.exports = function(User) { }); async.eachSeries(pids, function(pid, next) { - favourites.unvote(pid, uid, next); + posts.unvote(pid, uid, next); }, next); } ], function(err) { diff --git a/test/posts.js b/test/posts.js new file mode 100644 index 0000000000..eda66ce409 --- /dev/null +++ b/test/posts.js @@ -0,0 +1,136 @@ +'use strict'; +/*global require, before, after*/ + +var assert = require('assert'); +var async = require('async'); + +var db = require('./mocks/databasemock'); +var topics = require('../src/topics'); +var posts = require('../src/posts'); +var categories = require('../src/categories'); +var user = require('../src/user'); + +describe('Post\'s', function() { + var voterUid; + var voteeUid; + var postData; + + before(function(done) { + async.parallel({ + voterUid: function(next) { + user.create({username: 'upvoter'}, next); + }, + voteeUid: function(next) { + user.create({username: 'upvotee'}, next); + }, + category: function(next) { + categories.create({ + name: 'Test Category', + description: 'Test category created by testing script' + }, next); + } + }, function(err, results) { + if (err) { + return done(err); + } + + voterUid = results.voterUid; + voteeUid = results.voteeUid; + + topics.post({ + uid: results.voteeUid, + cid: results.category.cid, + title: 'Test Topic Title', + content: 'The content of test topic' + }, function(err, data) { + if (err) { + return done(err); + } + postData = data.postData; + done(); + }); + }); + }); + + describe('voting', function() { + + it('should upvote a post', function(done) { + posts.upvote(postData.pid, voterUid, function(err, result) { + assert.ifError(err); + assert.equal(result.post.upvotes, 1); + assert.equal(result.post.downvotes, 0); + assert.equal(result.post.votes, 1); + assert.equal(result.user.reputation, 1); + posts.hasVoted(postData.pid, voterUid, function(err, data) { + assert.ifError(err); + assert.equal(data.upvoted, true); + assert.equal(data.downvoted, false); + done(); + }); + }); + }); + + it('should unvote a post', function(done) { + posts.unvote(postData.pid, voterUid, function(err, result) { + assert.ifError(err); + assert.equal(result.post.upvotes, 0); + assert.equal(result.post.downvotes, 0); + assert.equal(result.post.votes, 0); + assert.equal(result.user.reputation, 0); + posts.hasVoted(postData.pid, voterUid, function(err, data) { + assert.ifError(err); + assert.equal(data.upvoted, false); + assert.equal(data.downvoted, false); + done(); + }); + }); + }); + + it('should downvote a post', function(done) { + posts.downvote(postData.pid, voterUid, function(err, result) { + assert.ifError(err); + assert.equal(result.post.upvotes, 0); + assert.equal(result.post.downvotes, 1); + assert.equal(result.post.votes, -1); + assert.equal(result.user.reputation, -1); + posts.hasVoted(postData.pid, voterUid, function(err, data) { + assert.ifError(err); + assert.equal(data.upvoted, false); + assert.equal(data.downvoted, true); + done(); + }); + }); + }); + }); + + describe('bookmarking', function() { + it('should bookmark a post', function(done) { + posts.bookmark(postData.pid, voterUid, function(err, data) { + assert.ifError(err); + assert.equal(data.isBookmarked, true); + posts.hasBookmarked(postData.pid, voterUid, function(err, hasBookmarked) { + assert.ifError(err); + assert.equal(hasBookmarked, true); + done(); + }); + }); + }); + + it('should unbookmark a post', function(done) { + posts.unbookmark(postData.pid, voterUid, function(err, data) { + assert.ifError(err); + assert.equal(data.isBookmarked, false); + posts.hasBookmarked([postData.pid], voterUid, function(err, hasBookmarked) { + assert.ifError(err); + assert.equal(hasBookmarked[0], false); + done(); + }); + }); + }); + }); + + + after(function(done) { + db.flushdb(done); + }); +});