diff --git a/public/src/admin/manage/flags.js b/public/src/admin/manage/flags.js index c6dc4b11af..880094af20 100644 --- a/public/src/admin/manage/flags.js +++ b/public/src/admin/manage/flags.js @@ -1,12 +1,21 @@ "use strict"; /*global define, socket, app, admin, utils, bootbox, RELATIVE_PATH*/ -define('admin/manage/flags', ['forum/infinitescroll', 'admin/modules/selectable'], function(infinitescroll, selectable) { +define('admin/manage/flags', [ + 'forum/infinitescroll', + 'admin/modules/selectable', + 'autocomplete' +], function(infinitescroll, selectable, autocomplete) { + var Flags = {}; Flags.init = function() { $('.post-container .content img').addClass('img-responsive'); + var params = utils.params(); + $('#flag-sort-by').val(params.sortBy); + autocomplete.user($('#byUsername')); + handleDismiss(); handleDismissAll(); handleDelete(); @@ -69,8 +78,15 @@ define('admin/manage/flags', ['forum/infinitescroll', 'admin/modules/selectable' if (direction < 0 && !$('.flags').length) { return; } + var params = utils.params(); + var sortBy = params.sortBy || 'count'; + var byUsername = params.byUsername || ''; - infinitescroll.loadMore('admin.getMoreFlags', $('[data-next]').attr('data-next'), function(data, done) { + infinitescroll.loadMore('admin.getMoreFlags', { + byUsername: byUsername, + sortBy: sortBy, + after: $('[data-next]').attr('data-next') + }, function(data, done) { if (data.posts && data.posts.length) { infinitescroll.parseAndTranslate('admin/manage/flags', 'posts', {posts: data.posts}, function(html) { $('[data-next]').attr('data-next', data.next); diff --git a/public/src/client/search.js b/public/src/client/search.js index 95f49b60f8..0af3d56be2 100644 --- a/public/src/client/search.js +++ b/public/src/client/search.js @@ -2,7 +2,7 @@ /* globals app, define, utils, socket*/ -define('forum/search', ['search'], function(searchModule) { +define('forum/search', ['search', 'autocomplete'], function(searchModule, autocomplete) { var Search = {}; Search.init = function() { @@ -156,25 +156,7 @@ define('forum/search', ['search'], function(searchModule) { } function enableAutoComplete() { - var input = $('#posted-by-user'); - input.autocomplete({ - delay: 100, - source: function(request, response) { - socket.emit('user.search', {query: request.term}, function(err, result) { - if (err) { - return app.alertError(err.message); - } - - if (result && result.users) { - var names = result.users.map(function(user) { - return user && user.username; - }); - response(names); - } - $('.ui-autocomplete a').attr('data-ajaxify', 'false'); - }); - } - }); + autocomplete.user($('#posted-by-user')); } return Search; diff --git a/public/src/modules/autocomplete.js b/public/src/modules/autocomplete.js new file mode 100644 index 0000000000..d8b2311b98 --- /dev/null +++ b/public/src/modules/autocomplete.js @@ -0,0 +1,31 @@ + +'use strict'; + +/* globals define, socket, app */ + +define('autocomplete', function() { + var module = {}; + + module.user = function (input) { + input.autocomplete({ + delay: 100, + source: function(request, response) { + socket.emit('user.search', {query: request.term}, function(err, result) { + if (err) { + return app.alertError(err.message); + } + + if (result && result.users) { + var names = result.users.map(function(user) { + return user && user.username; + }); + response(names); + } + $('.ui-autocomplete a').attr('data-ajaxify', 'false'); + }); + } + }); + }; + + return module; +}); diff --git a/src/controllers/admin.js b/src/controllers/admin.js index f03b7fdb88..162f9595ac 100644 --- a/src/controllers/admin.js +++ b/src/controllers/admin.js @@ -163,14 +163,24 @@ adminController.tags.get = function(req, res, next) { }; adminController.flags.get = function(req, res, next) { - var uid = req.user ? parseInt(req.user.uid, 10) : 0; - posts.getFlags(uid, 0, 19, function(err, posts) { + function done(err, posts) { if (err) { return next(err); } + res.render('admin/manage/flags', {posts: posts, next: end + 1, byUsername: byUsername}); + } + var uid = req.user ? parseInt(req.user.uid, 10) : 0; + var sortBy = req.query.sortBy || 'count'; + var byUsername = req.query.byUsername || ''; + var start = 0; + var end = 19; - res.render('admin/manage/flags', {posts: posts, next: 20}); - }); + if (byUsername) { + posts.getUserFlags(byUsername, sortBy, uid, start, end, done); + } else { + var set = sortBy === 'count' ? 'posts:flags:count' : 'posts:flagged'; + posts.getFlags(set, uid, start, end, done); + } }; adminController.database.get = function(req, res, next) { diff --git a/src/posts/delete.js b/src/posts/delete.js index cc1a8c8b5b..f3fab5bd87 100644 --- a/src/posts/delete.js +++ b/src/posts/delete.js @@ -29,7 +29,7 @@ module.exports = function(Posts) { removeFromCategoryRecentPosts(pid, postData.tid, next); }, function(next) { - db.sortedSetRemove('posts:flagged', pid, next); + Posts.dismissFlags(pid, next); } ], function(err) { callback(err, postData); @@ -125,6 +125,9 @@ module.exports = function(Posts) { }, function(next) { db.sortedSetsRemove(['posts:pid', 'posts:flagged'], pid, next); + }, + function(next) { + Posts.dismissFlags(pid, next); } ], function(err) { if (err) { diff --git a/src/posts/flags.js b/src/posts/flags.js index c3b591b001..388b707d3d 100644 --- a/src/posts/flags.js +++ b/src/posts/flags.js @@ -3,22 +3,52 @@ 'use strict'; var async = require('async'), - db = require('../database'); + db = require('../database'), + user = require('../user'); module.exports = function(Posts) { - Posts.flag = function(pid, callback) { - Posts.exists(pid, function(err, exists) { - if (err || !exists) { + + Posts.flag = function(post, uid, callback) { + async.parallel({ + hasFlagged: async.apply(hasFlagged, post.pid, uid), + exists: async.apply(Posts.exists, post.pid) + }, function(err, results) { + if (err || !results.exists) { return callback(err || new Error('[[error:no-post]]')); } + if (results.hasFlagged) { + return callback(new Error('[[error:already-flagged]]')); + } + var now = Date.now(); + async.parallel([ function(next) { - db.sortedSetAdd('posts:flagged', Date.now(), pid, next); + db.sortedSetAdd('posts:flagged', now, post.pid, next); }, function(next) { - db.incrObjectField('post:' + pid, 'flags', next); + db.sortedSetIncrBy('posts:flags:count', 1, post.pid, next); + }, + function(next) { + db.incrObjectField('post:' + post.pid, 'flags', next); + }, + function(next) { + db.sortedSetAdd('pid:' + post.pid + ':flag:uids', now, uid, next); + }, + function(next) { + if (parseInt(post.uid, 10)) { + db.sortedSetAdd('uid:' + post.uid + ':flag:pids', now, post.pid, next); + } else { + next(); + } + }, + function(next) { + if (parseInt(post.uid, 10)) { + db.setAdd('uid:' + post.uid + ':flagged_by', uid, next); + } else { + next(); + } } ], function(err, results) { callback(err); @@ -26,14 +56,31 @@ module.exports = function(Posts) { }); }; + function hasFlagged(pid, uid, callback) { + db.isSortedSetMember('pid:' + pid + ':flag:uids', uid, callback); + } + Posts.dismissFlag = function(pid, callback) { async.parallel([ function(next) { - db.sortedSetRemove('posts:flagged', pid, next); + db.getObjectField('post:' + pid, 'uid', function(err, uid) { + if (err) { + return next(err); + } + + db.sortedSetsRemove([ + 'posts:flagged', + 'posts:flags:count', + 'uid:' + uid + ':flag:pids' + ], pid, next); + }); }, function(next) { db.deleteObjectField('post:' + pid, 'flags', next); - } + }, + function(next) { + db.delete('pid:' + pid + ':flag:uids', next); + } ], function(err, results) { callback(err); }); @@ -43,8 +90,8 @@ module.exports = function(Posts) { db.delete('posts:flagged', callback); }; - Posts.getFlags = function(uid, start, end, callback) { - db.getSortedSetRevRange('posts:flagged', start, end, function(err, pids) { + Posts.getFlags = function(set, uid, start, end, callback) { + db.getSortedSetRevRange(set, start, end, function(err, pids) { if (err) { return callback(err); } @@ -52,4 +99,29 @@ module.exports = function(Posts) { Posts.getPostSummaryByPids(pids, uid, {stripTags: false, extraFields: ['flags']}, callback); }); }; + + Posts.getUserFlags = function(byUsername, sortBy, callerUID, start, end, callback) { + async.waterfall([ + function(next) { + user.getUidByUsername(byUsername, next); + }, + function(uid, next) { + if (!uid) { + return next(null, []); + } + db.getSortedSetRevRange('uid:' + uid + ':flag:pids', 0, -1, next); + }, + function(pids, next) { + Posts.getPostSummaryByPids(pids, callerUID, {stripTags: false, extraFields: ['flags']}, next); + }, + function(posts, next) { + if (sortBy === 'count') { + posts.sort(function(a, b) { + return b.flags - a.flags; + }); + } + next(null, posts.slice(start, end)); + } + ], callback); + }; }; diff --git a/src/socket.io/admin.js b/src/socket.io/admin.js index a770af5c53..b954e52da7 100644 --- a/src/socket.io/admin.js +++ b/src/socket.io/admin.js @@ -311,14 +311,24 @@ SocketAdmin.dismissAllFlags = function(socket, data, callback) { posts.dismissAllFlags(callback); }; -SocketAdmin.getMoreFlags = function(socket, after, callback) { - if (!parseInt(after, 10)) { +SocketAdmin.getMoreFlags = function(socket, data, callback) { + if (!data || !parseInt(data.after, 10)) { return callback('[[error:invalid-data]]'); } - after = parseInt(after, 10); - posts.getFlags(socket.uid, after, after + 19, function(err, posts) { - callback(err, {posts: posts, next: after + 20}); - }); + var sortBy = data.sortBy || 'count'; + var byUsername = data.byUsername || ''; + var start = parseInt(data.after, 10); + var end = start + 19; + if (byUsername) { + posts.getUserFlags(byUsername, sortBy, socket.uid, start, end, function(err, posts) { + callback(err, {posts: posts, next: end + 1}); + }); + } else { + var set = sortBy === 'count' ? 'posts:flags:count' : 'posts:flagged'; + posts.getFlags(set, socket.uid, start, end, function(err, posts) { + callback(err, {posts: posts, next: end + 1}); + }); + } }; SocketAdmin.takeHeapSnapshot = function(socket, data, callback) { diff --git a/src/socket.io/posts.js b/src/socket.io/posts.js index a2de8ca5d6..0b5ab9b418 100644 --- a/src/socket.io/posts.js +++ b/src/socket.io/posts.js @@ -422,14 +422,14 @@ SocketPosts.flag = function(socket, pid, callback) { return next(new Error('[[error:not-enough-reputation-to-flag]]')); } userName = userData.username; - posts.getPostFields(pid, ['tid', 'uid', 'content', 'deleted'], next); + posts.getPostFields(pid, ['pid', 'tid', 'uid', 'content', 'deleted'], next); }, function(postData, next) { if (parseInt(postData.deleted, 10) === 1) { return next(new Error('[[error:post-deleted]]')); } post = postData; - posts.flag(pid, next); + posts.flag(post, socket.uid, next); }, function(next) { topics.getTopicFields(post.tid, ['title', 'cid'], next); @@ -462,14 +462,7 @@ SocketPosts.flag = function(socket, pid, callback) { } notifications.push(notification, results.admins.concat(results.moderators), next); }); - }, - function(next) { - if (!parseInt(post.uid, 10)) { - return next(); - } - - db.setAdd('uid:' + post.uid + ':flagged_by', socket.uid, next); - } + } ], callback); }; diff --git a/src/user/delete.js b/src/user/delete.js index 3440e810a4..1c1e9c2974 100644 --- a/src/user/delete.js +++ b/src/user/delete.js @@ -80,7 +80,7 @@ module.exports = function(User) { 'uid:' + uid + ':topics', 'uid:' + uid + ':posts', 'uid:' + uid + ':chats', 'uid:' + uid + ':chats:unread', 'uid:' + uid + ':upvote', 'uid:' + uid + ':downvote', - 'uid:' + uid + ':ignored:cids' + 'uid:' + uid + ':ignored:cids', 'uid:' + uid + ':flag:pids' ]; db.deleteAll(keys, next); }, diff --git a/src/views/admin/manage/flags.tpl b/src/views/admin/manage/flags.tpl index 23a5d6f796..a87f450a47 100644 --- a/src/views/admin/manage/flags.tpl +++ b/src/views/admin/manage/flags.tpl @@ -2,42 +2,69 @@
Flags
-
- - - No flagged posts! - - - -
-
-
- - - - - - {posts.user.username} - -
-

{posts.content}

-

+
+ +
+ +
+ + No flagged posts! + + + +
+
+
+ + + + + + {posts.user.username} + +
+

{posts.content}

+

+
+ + + Posted in {posts.category.name}, • + Read More + + +
+
+ + {posts.flags} + + +

+
+
-