From 2c140c2107deab5912e481188da70df372b6da56 Mon Sep 17 00:00:00 2001
From: barisusakli <barisusakli@gmail.com>
Date: Fri, 16 Sep 2016 14:20:07 +0300
Subject: [PATCH] ability to filter flags by category

---
 public/src/admin/manage/flags.js |  2 --
 src/categories.js                | 33 ++++++++++++++++++
 src/controllers/admin/flags.js   | 59 +++++++++++++++++++++++++-------
 src/controllers/search.js        | 50 +++++----------------------
 src/plugins.js                   |  1 +
 src/posts/flags.js               | 53 +++++++++-------------------
 src/routes/debug.js              |  9 ++---
 src/views/admin/manage/flags.tpl | 18 ++++++++--
 8 files changed, 126 insertions(+), 99 deletions(-)

diff --git a/public/src/admin/manage/flags.js b/public/src/admin/manage/flags.js
index ccf13b789a..15417faa30 100644
--- a/public/src/admin/manage/flags.js
+++ b/public/src/admin/manage/flags.js
@@ -12,8 +12,6 @@ define('admin/manage/flags', [
 	Flags.init = function() {
 		$('.post-container .content img:not(.not-responsive)').addClass('img-responsive');
 
-		var params = utils.params();
-		$('#flag-sort-by').val(params.sortBy);
 		autocomplete.user($('#byUsername'));
 
 		handleDismiss();
diff --git a/src/categories.js b/src/categories.js
index 3233f2dbcd..b5291b2f8b 100644
--- a/src/categories.js
+++ b/src/categories.js
@@ -305,6 +305,39 @@ var privileges = require('./privileges');
 		return tree;
 	};
 
+	Categories.buildForSelect = function(uid, callback) {
+		function recursive(category, categoriesData, level) {
+			if (category.link) {
+				return;
+			}
+
+			var bullet = level ? '&bull; ' : '';
+			category.value = category.cid;
+			category.text = level + bullet + category.name
+			categoriesData.push(category);
+
+			category.children.forEach(function(child) {
+				recursive(child, categoriesData, '&nbsp;&nbsp;&nbsp;&nbsp;' + level);
+			});
+		}
+		Categories.getCategoriesByPrivilege('cid:0:children', uid, 'read', function(err, categories) {
+			if (err) {
+				return callback(err);
+			}
+
+			var categoriesData = [];
+
+			categories = categories.filter(function(category) {
+				return category && !category.link && !parseInt(category.parentCid, 10);
+			});
+
+			categories.forEach(function(category) {
+				recursive(category, categoriesData, '');
+			});
+			callback(null, categoriesData);
+		});
+	};
+
 	Categories.getIgnorers = function(cid, start, stop, callback) {
 		db.getSortedSetRevRange('cid:' + cid + ':ignorers', start, stop, callback);
 	};
diff --git a/src/controllers/admin/flags.js b/src/controllers/admin/flags.js
index 54a52aa319..4b44b1d951 100644
--- a/src/controllers/admin/flags.js
+++ b/src/controllers/admin/flags.js
@@ -3,28 +3,26 @@
 var async = require('async');
 var posts = require('../../posts');
 var user = require('../../user');
+var categories = require('../../categories');
 var analytics = require('../../analytics');
 var pagination = require('../../pagination');
 
 var flagsController = {};
 
+var itemsPerPage = 20;
+
 flagsController.get = function(req, res, next) {
-	var sortBy = req.query.sortBy || 'count';
 	var byUsername = req.query.byUsername || '';
-
+	var cid = req.query.cid || 0;
+	var sortBy = req.query.sortBy || 'count';
 	var page = parseInt(req.query.page, 10) || 1;
-	var itemsPerPage = 20;
-	var start = (page - 1) * itemsPerPage;
-	var stop = start + itemsPerPage - 1;
 
 	async.parallel({
+		categories: function(next) {
+			categories.buildForSelect(req.uid, next);
+		},
 		flagData: function(next) {
-			if (byUsername) {
-				posts.getUserFlags(byUsername, sortBy, req.uid, start, stop, next);
-			} else {
-				var set = sortBy === 'count' ? 'posts:flags:count' : 'posts:flagged';
-				posts.getFlags(set, req.uid, start, stop, next);
-			}
+			getFlagData(req, next);
 		},
 		analytics: function(next) {
 			analytics.getDailyStatsForSet('analytics:flags', Date.now(), 30, next);
@@ -47,12 +45,18 @@ flagsController.get = function(req, res, next) {
 
 		var pageCount = Math.max(1, Math.ceil(results.flagData.count / itemsPerPage));
 
+		results.categories.forEach(function(category) {
+			category.selected = parseInt(category.cid, 10) === parseInt(cid, 10);
+		});
+
 		var data = {
 			posts: results.flagData.posts,
 			assignees: results.assignees,
 			analytics: results.analytics,
-			next: stop + 1,
+			categories: results.categories,
 			byUsername: byUsername,
+			sortByCount: sortBy === 'count',
+			sortByTime: sortBy === 'time',
 			pagination: pagination.create(page, pageCount, req.query),
 			title: '[[pages:flagged-posts]]'
 		};
@@ -60,5 +64,36 @@ flagsController.get = function(req, res, next) {
 	});
 };
 
+function getFlagData(req, callback) {
+	var sortBy = req.query.sortBy || 'count';
+	var byUsername = req.query.byUsername || '';
+	var cid = req.query.cid || 0;
+	var page = parseInt(req.query.page, 10) || 1;
+	var start = (page - 1) * itemsPerPage;
+	var stop = start + itemsPerPage - 1;
+
+	var sets = [sortBy === 'count' ? 'posts:flags:count' : 'posts:flagged'];
+	if (cid) {
+		sets.push('cid:' + cid + ':pids');
+	}
+
+	async.waterfall([
+		function(next) {
+			if (byUsername) {
+				user.getUidByUsername(byUsername, next);
+			} else {
+				process.nextTick(next, null, 0);
+			}
+		},
+		function(uid, next) {
+			if (uid) {
+				sets.push('uid:' + uid + ':flag:pids');
+			}
+
+			posts.getFlags(sets, req.uid, start, stop, next);
+		}
+	], callback);
+}
+
 
 module.exports = flagsController;
diff --git a/src/controllers/search.js b/src/controllers/search.js
index 2466f17670..8c016c759c 100644
--- a/src/controllers/search.js
+++ b/src/controllers/search.js
@@ -45,14 +45,20 @@ searchController.search = function(req, res, next) {
 	};
 
 	async.parallel({
-		categories: async.apply(buildCategories, req.uid),
+		categories: async.apply(categories.buildForSelect, req.uid),
 		search: async.apply(search.search, data)
 	}, function(err, results) {
 		if (err) {
 			return next(err);
 		}
+
+		var categoriesData = [
+			{value: 'all', text: '[[unread:all_categories]]'},
+			{value: 'watched', text: '[[category:watched-categories]]'}
+		].concat(results.categories);
+
 		var searchData = results.search;
-		searchData.categories = results.categories;
+		searchData.categories = categoriesData;
 		searchData.categoriesCount = results.categories.length;
 		searchData.pagination = pagination.create(page, searchData.pageCount, req.query);
 		searchData.showAsPosts = !req.query.showAs || req.query.showAs === 'posts';
@@ -65,44 +71,4 @@ searchController.search = function(req, res, next) {
 	});
 };
 
-function buildCategories(uid, callback) {
-	categories.getCategoriesByPrivilege('cid:0:children', uid, 'read', function(err, categories) {
-		if (err) {
-			return callback(err);
-		}
-
-		var categoriesData = [
-			{value: 'all', text: '[[unread:all_categories]]'},
-			{value: 'watched', text: '[[category:watched-categories]]'}
-		];
-
-		categories = categories.filter(function(category) {
-			return category && !category.link && !parseInt(category.parentCid, 10);
-		});
-
-		categories.forEach(function(category) {
-			recursive(category, categoriesData, '');
-		});
-		callback(null, categoriesData);
-	});
-}
-
-
-function recursive(category, categoriesData, level) {
-	if (category.link) {
-		return;
-	}
-
-	var bullet = level ? '&bull; ' : '';
-
-	categoriesData.push({
-		value: category.cid,
-		text: level + bullet + category.name
-	});
-
-	category.children.forEach(function(child) {
-		recursive(child, categoriesData, '&nbsp;&nbsp;&nbsp;&nbsp;' + level);
-	});
-}
-
 module.exports = searchController;
diff --git a/src/plugins.js b/src/plugins.js
index 0d6bb02fb5..af3bc887c9 100644
--- a/src/plugins.js
+++ b/src/plugins.js
@@ -260,6 +260,7 @@ var middleware;
 		}, function(err, res, body) {
 			if (err) {
 				winston.error('Error parsing plugins : ' + err.message);
+				return callback(err);
 			}
 
 			Plugins.normalise(body, callback);
diff --git a/src/posts/flags.js b/src/posts/flags.js
index 566a03ae21..0d4a782ee8 100644
--- a/src/posts/flags.js
+++ b/src/posts/flags.js
@@ -154,14 +154,23 @@ module.exports = function(Posts) {
 	};
 
 	Posts.getFlags = function(set, uid, start, stop, callback) {
+		set = set.length > 1 ? set : set[0];
 		async.parallel({
 			count: function(next) {
-				db.sortedSetCard(set, next);
+				if (Array.isArray(set)) {
+					db.sortedSetIntersectCard(set, next);
+				} else {
+					db.sortedSetCard(set, next);
+				}
 			},
 			posts: function(next) {
 				async.waterfall([
 					function (next) {
-						db.getSortedSetRevRange(set, start, stop, next);
+						if (Array.isArray(set)) {
+							db.getSortedSetRevIntersect({sets: set, start: start, stop: stop, aggregate: 'MAX'}, next);
+						} else {
+							db.getSortedSetRevRange(set, start, stop, next);
+						}
 					},
 					function (pids, next) {
 						getFlaggedPostsWithReasons(pids, uid, next);
@@ -171,35 +180,6 @@ module.exports = function(Posts) {
 		}, callback);
 	};
 
-	Posts.getUserFlags = function(byUsername, sortBy, callerUID, start, stop, callback) {
-		var count = 0;
-		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) {
-				count = pids.length;
-				getFlaggedPostsWithReasons(pids, callerUID, next);
-			},
-			function(posts, next) {
-				if (sortBy === 'count') {
-					posts.sort(function(a, b) {
-						return b.flags - a.flags;
-					});
-				}
-
-				next(null, {posts: posts.slice(start, stop === -1 ? undefined : stop + 1), count: count});
-			}
-		], callback);
-	};
-
 	function getFlaggedPostsWithReasons(pids, uid, callback) {
 		async.waterfall([
 			function (next) {
@@ -229,8 +209,6 @@ module.exports = function(Posts) {
 					}
 
 					results.posts.forEach(function(post, index) {
-						var history;
-
 						if (post) {
 							post.flagReasons = reasons[index];
 						}
@@ -352,11 +330,12 @@ module.exports = function(Posts) {
 	Posts.expandFlagHistory = function(posts, callback) {
 		// Expand flag history
 		async.map(posts, function(post, next) {
+			var history;
 			try {
-				var history = JSON.parse(post['flag:history'] || '[]');
+				history = JSON.parse(post['flag:history'] || '[]');
 			} catch (e) {
 				winston.warn('[posts/getFlags] Unable to deserialise post flag history, likely malformed data');
-				callback(e);
+				return callback(e);
 			}
 
 			async.map(history, function(event, next) {
@@ -392,7 +371,7 @@ module.exports = function(Posts) {
 					}
 				], function(err) {
 					next(err, event);
-				})
+				});
 			}, function(err, history) {
 				if (err) {
 					return next(err);
@@ -402,5 +381,5 @@ module.exports = function(Posts) {
 				next(null, post);
 			});
 		}, callback);
-	}
+	};
 };
diff --git a/src/routes/debug.js b/src/routes/debug.js
index a2191d8ffa..87c536c4c6 100644
--- a/src/routes/debug.js
+++ b/src/routes/debug.js
@@ -3,10 +3,11 @@
 var express = require('express');
 var nconf = require('nconf');
 var winston = require('winston');
-var user = require('./../user');
-var categories = require('./../categories');
-var topics = require('./../topics');
-var posts = require('./../posts');
+var user = require('../user');
+var categories = require('../categories');
+var topics = require('../topics');
+var posts = require('../posts');
+var db = require('../database');
 
 module.exports = function(app, middleware, controllers) {
 	var router = express.Router();
diff --git a/src/views/admin/manage/flags.tpl b/src/views/admin/manage/flags.tpl
index 4a87588467..54209a279e 100644
--- a/src/views/admin/manage/flags.tpl
+++ b/src/views/admin/manage/flags.tpl
@@ -24,13 +24,27 @@
 				</div>
 			</div>
 
+			<div class="form-group">
+				<div>
+					<div>
+						<label>Category</label>
+						<select class="form-control" id="category-selector" name="cid">
+							<option value="">[[unread:all_categories]]</option>
+							<!-- BEGIN categories -->
+							<option value="{categories.cid}" <!-- IF categories.selected -->selected<!-- ENDIF categories.selected -->>{categories.text}</option>
+							<!-- END categories -->
+						</select>
+					</div>
+				</div>
+			</div>
+
 			<div class="form-group">
 				<label>Sort By</label>
 				<div>
 					<div>
 						<select id="flag-sort-by" class="form-control" name="sortBy">
-							<option value="count">Most Flags</option>
-							<option value="time">Most Recent</option>
+							<option value="count" <!-- IF sortByCount -->selected<!-- ENDIF sortByCount -->>Most Flags</option>
+							<option value="time" <!-- IF sortByTime -->selected<!-- ENDIF sortByTime -->>Most Recent</option>
 						</select>
 					</div>
 				</div>