From 789a3736af9b4b1608a8f0dc42a85545f28f927c Mon Sep 17 00:00:00 2001
From: barisusakli <barisusakli@gmail.com>
Date: Mon, 4 Jul 2016 17:49:02 +0300
Subject: [PATCH] closes #4820

---
 public/language/en_GB/pages.json |   1 +
 public/language/en_GB/users.json |   1 +
 public/src/admin/manage/flags.js |  45 +++++++-
 public/src/client/users.js       |  11 +-
 src/controllers/admin/flags.js   |  27 +++--
 src/controllers/admin/users.js   |   4 +
 src/controllers/users.js         |  41 ++++---
 src/posts/flags.js               | 179 ++++++++++++++++++-------------
 src/routes/admin.js              |   1 +
 src/routes/index.js              |   1 +
 src/socket.io/admin/user.js      |  18 +---
 src/socket.io/user/search.js     |   1 +
 src/user.js                      |   3 +-
 src/user/admin.js                |  10 +-
 src/user/search.js               |  20 ++--
 src/views/admin/manage/flags.tpl |  72 +++++++------
 src/views/admin/manage/users.tpl |   3 +-
 17 files changed, 270 insertions(+), 168 deletions(-)

diff --git a/public/language/en_GB/pages.json b/public/language/en_GB/pages.json
index bbdf3cdc47..2fe5b5dbac 100644
--- a/public/language/en_GB/pages.json
+++ b/public/language/en_GB/pages.json
@@ -13,6 +13,7 @@
 	"users/sort-posts": "Users with the most posts",
 	"users/sort-reputation": "Users with the most reputation",
 	"users/banned": "Banned Users",
+	"users/most-flags": "Most flagged users",
 	"users/search": "User Search",
 
 	"notifications": "Notifications",
diff --git a/public/language/en_GB/users.json b/public/language/en_GB/users.json
index e693bf6333..5c9d8b93a4 100644
--- a/public/language/en_GB/users.json
+++ b/public/language/en_GB/users.json
@@ -2,6 +2,7 @@
 	"latest_users": "Latest Users",
 	"top_posters": "Top Posters",
 	"most_reputation": "Most Reputation",
+	"most_flags": "Most Flags",
 	"search": "Search",
 	"enter_username": "Enter a username to search",
 	"load_more": "Load More",
diff --git a/public/src/admin/manage/flags.js b/public/src/admin/manage/flags.js
index eaa839522a..e1ccde9c00 100644
--- a/public/src/admin/manage/flags.js
+++ b/public/src/admin/manage/flags.js
@@ -1,11 +1,12 @@
 "use strict";
-/*global define, socket, app, admin, utils, bootbox, RELATIVE_PATH*/
+/*global define, socket, app, utils, bootbox, ajaxify*/
 
 define('admin/manage/flags', [
 	'forum/infinitescroll',
 	'admin/modules/selectable',
-	'autocomplete'
-], function(infinitescroll, selectable, autocomplete) {
+	'autocomplete',
+	'Chart'
+], function(infinitescroll, selectable, autocomplete, Chart) {
 
 	var	Flags = {};
 
@@ -20,6 +21,7 @@ define('admin/manage/flags', [
 		handleDismissAll();
 		handleDelete();
 		handleInfiniteScroll();
+		handleGraphs();
 	};
 
 	function handleDismiss() {
@@ -101,5 +103,42 @@ define('admin/manage/flags', [
 		});
 	}
 
+	function handleGraphs() {
+		var dailyCanvas = document.getElementById('flags:daily');
+		var dailyLabels = utils.getDaysArray().map(function(text, idx) {
+			return idx % 3 ? '' : text;
+		});
+
+		if (utils.isMobile()) {
+			Chart.defaults.global.showTooltips = false;
+		}
+		var data = {
+			'flags:daily': {
+				labels: dailyLabels,
+				datasets: [
+					{
+						label: "",
+						fillColor: "rgba(151,187,205,0.2)",
+						strokeColor: "rgba(151,187,205,1)",
+						pointColor: "rgba(151,187,205,1)",
+						pointStrokeColor: "#fff",
+						pointHighlightFill: "#fff",
+						pointHighlightStroke: "rgba(151,187,205,1)",
+						data: ajaxify.data.analytics
+					}
+				]
+			}
+		};
+
+
+
+		dailyCanvas.width = $(dailyCanvas).parent().width();
+		new Chart(dailyCanvas.getContext('2d')).Line(data['flags:daily'], {
+			responsive: true,
+			animation: false
+		});
+
+	}
+
 	return Flags;
 });
\ No newline at end of file
diff --git a/public/src/client/users.js b/public/src/client/users.js
index d90d3ca6ac..fe083a13e2 100644
--- a/public/src/client/users.js
+++ b/public/src/client/users.js
@@ -102,14 +102,15 @@ define('forum/users', ['translator'], function(translator) {
 		if (!username) {
 			return loadPage(page);
 		}
-
+		var activeSection = getActiveSection();
 		socket.emit('user.search', {
 			query: username,
 			page: page,
 			searchBy: 'username',
 			sortBy: $('.search select').val() || getSortBy(),
-			onlineOnly: $('.search .online-only').is(':checked') || (getActiveSection() === 'online'),
-			bannedOnly: getActiveSection() === 'banned'
+			onlineOnly: $('.search .online-only').is(':checked') || (activeSection === 'online'),
+			bannedOnly:  activeSection === 'banned',
+			flaggedOnly: activeSection === 'flagged'
 		}, function(err, data) {
 			if (err) {
 				return app.alertError(err.message);
@@ -168,8 +169,8 @@ define('forum/users', ['translator'], function(translator) {
 	}
 
 	function getActiveSection() {
-		var url = window.location.href,
-			parts = url.split('/');
+		var url = window.location.href;
+		var parts = url.split('/');
 		return parts[parts.length - 1];
 	}
 
diff --git a/src/controllers/admin/flags.js b/src/controllers/admin/flags.js
index cb2f830365..340eda0a9d 100644
--- a/src/controllers/admin/flags.js
+++ b/src/controllers/admin/flags.js
@@ -2,6 +2,7 @@
 
 var async = require('async');
 var posts = require('../../posts');
+var analytics = require('../../analytics');
 
 var flagsController = {};
 
@@ -13,20 +14,28 @@ flagsController.get = function(req, res, next) {
 
 	async.waterfall([
 		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);
-			}
+			async.parallel({
+				posts: 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);
+					}
+				},
+				analytics: function(next) {
+					analytics.getDailyStatsForSet('analytics:flags', Date.now(), 30, next);
+				}
+			}, next);
 		}
-	], function (err, posts) {
+	], function (err, results) {
 		if (err) {
 			return next(err);
 		}
 		var data = {
-			posts: posts, 
-			next: stop + 1, 
+			posts: results.posts,
+			analytics: results.analytics,
+			next: stop + 1,
 			byUsername: byUsername,
 			title: '[[pages:flagged-posts]]'
 		};
diff --git a/src/controllers/admin/users.js b/src/controllers/admin/users.js
index 1b833c2709..e682045fce 100644
--- a/src/controllers/admin/users.js
+++ b/src/controllers/admin/users.js
@@ -28,6 +28,10 @@ usersController.noPosts = function(req, res, next) {
 	getUsersByScore('users:postcount', 'noposts', 0, 0, req, res, next);
 };
 
+usersController.flagged = function(req, res, next) {
+	getUsersByScore('users:flags', 'mostflags', 1, '+inf', req, res, next);
+};
+
 usersController.inactive = function(req, res, next) {
 	var timeRange = 1000 * 60 * 60 * 24 * 30 * (parseInt(req.query.months, 10) || 3);
 	var cutoff = Date.now() - timeRange;
diff --git a/src/controllers/users.js b/src/controllers/users.js
index c84de75b5b..4ee7e5a9ec 100644
--- a/src/controllers/users.js
+++ b/src/controllers/users.js
@@ -69,6 +69,20 @@ usersController.getBannedUsers = function(req, res, next) {
 	});
 };
 
+usersController.getFlaggedUsers = function(req, res, next) {
+	usersController.getUsers('users:flags', req.uid, req.query.page, function(err, userData) {
+		if (err) {
+			return next(err);
+		}
+
+		if (!userData.isAdminOrGlobalMod) {
+			return next();
+		}
+
+		render(req, res, userData, next);
+	});
+};
+
 usersController.renderUsersPage = function(set, req, res, next) {
 	usersController.getUsers(set, req.uid, req.query.page, function(err, userData) {
 		if (err) {
@@ -79,23 +93,16 @@ usersController.renderUsersPage = function(set, req, res, next) {
 };
 
 usersController.getUsers = function(set, uid, page, callback) {
-	var setToTitles = {
-		'users:postcount': '[[pages:users/sort-posts]]',
-		'users:reputation': '[[pages:users/sort-reputation]]',
-		'users:joindate': '[[pages:users/latest]]',
-		'users:online': '[[pages:users/online]]',
-		'users:banned': '[[pages:users/banned]]'
-	};
-
-	var setToCrumbs = {
-		'users:postcount': '[[users:top_posters]]',
-		'users:reputation': '[[users:most_reputation]]',
-		'users:joindate': '[[global:users]]',
-		'users:online': '[[global:online]]',
-		'users:banned': '[[user:banned]]'
+	var setToData = {
+		'users:postcount': {title: '[[pages:users/sort-posts]]', crumb: '[[users:top_posters]]'},
+		'users:reputation': {title: '[[pages:users/sort-reputation]]', crumb: '[[users:most_reputation]]'},
+		'users:joindate': {title: '[[pages:users/latest]]', crumb: '[[global:users]]'},
+		'users:online': {title: '[[pages:users/online]]', crumb: '[[global:online]]'},
+		'users:banned': {title: '[[pages:users/banned]]', crumb: '[[user:banned]]'},
+		'users:flags': {title: '[[pages:users/most-flags]]', crumb: '[[users:most_flags]]'},
 	};
 
-	var breadcrumbs = [{text: setToCrumbs[set]}];
+	var breadcrumbs = [{text: setToData[set].crumb}];
 
 	if (set !== 'users:joindate') {
 		breadcrumbs.unshift({text: '[[global:users]]', url: '/users'});
@@ -127,7 +134,7 @@ usersController.getUsers = function(set, uid, page, callback) {
 			users: results.usersData.users,
 			pagination: pagination.create(page, pageCount),
 			userCount: results.usersData.count,
-			title: setToTitles[set] || '[[pages:users/latest]]',
+			title: setToData[set].title || '[[pages:users/latest]]',
 			breadcrumbs: helpers.buildBreadcrumbs(breadcrumbs),
 			setName: set,
 			isAdminOrGlobalMod: results.isAdministrator || results.isGlobalMod
@@ -148,6 +155,8 @@ usersController.getUsersAndCount = function(set, uid, start, stop, callback) {
 				db.sortedSetCount('users:online', now - 300000, '+inf', next);
 			} else if (set === 'users:banned') {
 				db.sortedSetCard('users:banned', next);
+			} else if (set === 'users:flags') {
+				db.sortedSetCard('users:flags', next);
 			} else {
 				db.getObjectField('global', 'userCount', next);
 			}
diff --git a/src/posts/flags.js b/src/posts/flags.js
index cf2b37bf0c..9eae966cda 100644
--- a/src/posts/flags.js
+++ b/src/posts/flags.js
@@ -5,7 +5,7 @@
 var async = require('async');
 var db = require('../database');
 var user = require('../user');
-
+var analytics = require('../analytics');
 
 module.exports = function(Posts) {
 
@@ -13,52 +13,59 @@ module.exports = function(Posts) {
 		if (!parseInt(uid, 10) || !reason) {
 			return 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', now, post.pid, next);
-				},
-				function(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) {
-					db.sortedSetAdd('pid:' + post.pid + ':flag:uid:reason', 0, uid + ':' + reason, 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();
-					}
+		async.waterfall([
+			function(next) {
+				async.parallel({
+					hasFlagged: async.apply(hasFlagged, post.pid, uid),
+					exists: async.apply(Posts.exists, post.pid)
+				}, next);
+			},
+			function(results, next) {
+				if (!results.exists) {
+					return next(new Error('[[error:no-post]]'));
 				}
-			], function(err) {
-				callback(err);
-			});
+
+				if (results.hasFlagged) {
+					return next(new Error('[[error:already-flagged]]'));
+				}
+
+				var now = Date.now();
+				async.parallel([
+					function(next) {
+						db.sortedSetAdd('posts:flagged', now, post.pid, next);
+					},
+					function(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) {
+						db.sortedSetAdd('pid:' + post.pid + ':flag:uid:reason', 0, uid + ':' + reason, next);
+					},
+					function(next) {
+						if (parseInt(post.uid, 10)) {
+							async.parallel([
+								async.apply(db.sortedSetIncrBy, 'users:flags', 1, post.uid),
+								async.apply(db.incrObjectField, 'user:' + post.uid, 'flags'),
+								async.apply(db.sortedSetAdd, 'uid:' + post.uid + ':flag:pids', now, post.pid)
+							], next);
+						} else {
+							next();
+						}
+					}
+				], next);
+			}
+		], function(err) {
+			if (err) {
+				return callback(err);
+			}
+			analytics.increment('flags');
+			callback();
 		});
 	};
 
@@ -67,41 +74,58 @@ module.exports = function(Posts) {
 	}
 
 	Posts.dismissFlag = function(pid, callback) {
-		async.parallel([
+		async.waterfall([
 			function(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);
-				});
+				db.getObjectFields('post:' + pid, ['pid', 'uid', 'flags'], next);
 			},
-			function(next) {
-				async.series([
+			function(postData, next) {
+				if (!postData.pid) {
+					return callback();
+				}
+				async.parallel([
 					function(next) {
-						db.getSortedSetRange('pid:' + pid + ':flag:uids', 0, -1, function(err, uids) {
-							async.each(uids, function(uid, next) {
-								var nid = 'post_flag:' + pid + ':uid:' + uid;
+						if (parseInt(postData.uid, 10)) {
+							if (parseInt(postData.flags, 10) > 0) {
 								async.parallel([
-									async.apply(db.delete, 'notifications:' + nid),
-									async.apply(db.sortedSetRemove, 'notifications', 'post_flag:' + pid + ':uid:' + uid)
+									async.apply(db.sortedSetIncrBy, 'users:flags', -postData.flags, postData.uid),
+									async.apply(db.incrObjectFieldBy, 'user:' + postData.uid, 'flags', -postData.flags)
 								], next);
-							}, next);
-						});
+							} else {
+								next();
+							}
+						}
 					},
-					async.apply(db.delete, 'pid:' + pid + ':flag:uids')
+					function(next) {
+						db.sortedSetsRemove([
+							'posts:flagged',
+							'posts:flags:count',
+							'uid:' + postData.uid + ':flag:pids'
+						], pid, next);
+					},
+					function(next) {
+						async.series([
+							function(next) {
+								db.getSortedSetRange('pid:' + pid + ':flag:uids', 0, -1, function(err, uids) {
+									async.each(uids, function(uid, next) {
+										var nid = 'post_flag:' + pid + ':uid:' + uid;
+										async.parallel([
+											async.apply(db.delete, 'notifications:' + nid),
+											async.apply(db.sortedSetRemove, 'notifications', 'post_flag:' + pid + ':uid:' + uid)
+										], next);
+									}, next);
+								});
+							},
+							async.apply(db.delete, 'pid:' + pid + ':flag:uids')
+						], next);
+					},
+					async.apply(db.deleteObjectField, 'post:' + pid, 'flags'),
+					async.apply(db.delete, 'pid:' + pid + ':flag:uid:reason')
 				], next);
 			},
-			async.apply(db.deleteObjectField, 'post:' + pid, 'flags'),
-			async.apply(db.delete, 'pid:' + pid + ':flag:uid:reason')
-		], function(err) {
-			callback(err);
-		});
+			function(results, next) {
+				db.sortedSetsRemoveRangeByScore(['users:flags'], '-inf', 0, next);
+			}
+		], callback);
 	};
 
 	Posts.dismissAllFlags = function(callback) {
@@ -109,7 +133,16 @@ module.exports = function(Posts) {
 			if (err) {
 				return callback(err);
 			}
-			async.eachLimit(pids, 50, Posts.dismissFlag, callback);
+			async.eachSeries(pids, Posts.dismissFlag, callback);
+		});
+	};
+
+	Posts.dismissUserFlags = function(uid, callback) {
+		db.getSortedSetRange('uid:' + uid + ':flag:pids', 0, -1, function(err, pids) {
+			if (err) {
+				return callback(err);
+			}
+			async.eachSeries(pids, Posts.dismissFlag, callback);
 		});
 	};
 
diff --git a/src/routes/admin.js b/src/routes/admin.js
index 521f0194bf..543b9e8489 100644
--- a/src/routes/admin.js
+++ b/src/routes/admin.js
@@ -65,6 +65,7 @@ function addRoutes(router, middleware, controllers) {
 	router.get('/manage/users/not-validated', middlewares, controllers.admin.users.notValidated);
 	router.get('/manage/users/no-posts', middlewares, controllers.admin.users.noPosts);
 	router.get('/manage/users/inactive', middlewares, controllers.admin.users.inactive);
+	router.get('/manage/users/flagged', middlewares, controllers.admin.users.flagged);
 	router.get('/manage/users/banned', middlewares, controllers.admin.users.banned);
 	router.get('/manage/registration', middlewares, controllers.admin.users.registrationQueue);
 
diff --git a/src/routes/index.js b/src/routes/index.js
index 06c9706856..1e51f5d59e 100644
--- a/src/routes/index.js
+++ b/src/routes/index.js
@@ -75,6 +75,7 @@ function userRoutes(app, middleware, controllers) {
 	setupPageRoute(app, '/users/sort-posts', middleware, middlewares, controllers.users.getUsersSortedByPosts);
 	setupPageRoute(app, '/users/sort-reputation', middleware, middlewares, controllers.users.getUsersSortedByReputation);
 	setupPageRoute(app, '/users/banned', middleware, middlewares, controllers.users.getBannedUsers);
+	setupPageRoute(app, '/users/flagged', middleware, middlewares, controllers.users.getFlaggedUsers);
 }
 
 function groupRoutes(app, middleware, controllers) {
diff --git a/src/socket.io/admin/user.js b/src/socket.io/admin/user.js
index 2633425dc4..ed7b4b378d 100644
--- a/src/socket.io/admin/user.js
+++ b/src/socket.io/admin/user.js
@@ -185,25 +185,15 @@ User.search = function(socket, data, callback) {
 			return user && user.uid;
 		});
 
-		async.parallel({
-			users: function(next) {
-				user.getUsersFields(uids, ['email'], next);
-			},
-			flagCounts: function(next) {
-				var sets = uids.map(function(uid) {
-					return 'uid:' + uid + ':flagged_by';
-				});
-				db.setsCount(sets, next);
-			}
-		}, function(err, results) {
+		user.getUsersFields(uids, ['email', 'flags'], function(err, userInfo) {
 			if (err) {
 				return callback(err);
 			}
 
 			userData.forEach(function(user, index) {
-				if (user) {
-					user.email = (results.users[index] && results.users[index].email) || '';
-					user.flags = results.flagCounts[index] || 0;
+				if (user && userInfo[index]) {
+					user.email = userInfo[index].email || '';
+					user.flags = userInfo[index].flags || 0;
 				}
 			});
 
diff --git a/src/socket.io/user/search.js b/src/socket.io/user/search.js
index ecef1127c2..76b1daa3a7 100644
--- a/src/socket.io/user/search.js
+++ b/src/socket.io/user/search.js
@@ -20,6 +20,7 @@ module.exports = function(SocketUser) {
 			sortBy: data.sortBy,
 			onlineOnly: data.onlineOnly,
 			bannedOnly: data.bannedOnly,
+			flaggedOnly: data.flaggedOnly,
 			uid: socket.uid
 		}, function(err, result) {
 			if (err) {
diff --git a/src/user.js b/src/user.js
index 3303d04e79..b4557abfb0 100644
--- a/src/user.js
+++ b/src/user.js
@@ -89,7 +89,8 @@ var utils = require('../public/src/utils');
 	};
 
 	User.getUsers = function(uids, uid, callback) {
-		var fields = ['uid', 'username', 'userslug', 'picture', 'status', 'banned', 'joindate', 'postcount', 'reputation', 'email:confirmed', 'lastonline'];
+		var fields = ['uid', 'username', 'userslug', 'picture', 'status', 'flags',
+			'banned', 'joindate', 'postcount', 'reputation', 'email:confirmed', 'lastonline'];
 
 		async.waterfall([
 			function (next) {
diff --git a/src/user/admin.js b/src/user/admin.js
index c402eb7e7a..59ec0e0979 100644
--- a/src/user/admin.js
+++ b/src/user/admin.js
@@ -3,6 +3,7 @@
 
 var async = require('async');
 var db = require('../database');
+var posts = require('../posts');
 var plugins = require('../plugins');
 
 module.exports = function(User) {
@@ -89,7 +90,6 @@ module.exports = function(User) {
 	};
 
 	User.unban = function(uid, callback) {
-		db.delete('uid:' + uid + ':flagged_by');
 		async.waterfall([
 			function (next) {
 				User.setUserField(uid, 'banned', 0, next);
@@ -108,9 +108,9 @@ module.exports = function(User) {
 		if (!Array.isArray(uids) || !uids.length) {
 			return callback();
 		}
-		var keys = uids.map(function(uid) {
-			return 'uid:' + uid + ':flagged_by';
-		});
-		db.deleteAll(keys, callback);
+
+		async.eachSeries(uids, function(uid, next) {
+			posts.dismissUserFlags(uid, next);
+		}, callback);
 	};
 };
diff --git a/src/user/search.js b/src/user/search.js
index b2a1e83a80..6bb7084b1f 100644
--- a/src/user/search.js
+++ b/src/user/search.js
@@ -1,10 +1,10 @@
 
 'use strict';
 
-var async = require('async'),
-	meta = require('../meta'),
-	plugins = require('../plugins'),
-	db = require('../database');
+var async = require('async');
+var meta = require('../meta');
+var plugins = require('../plugins');
+var db = require('../database');
 
 module.exports = function(User) {
 
@@ -84,7 +84,7 @@ module.exports = function(User) {
 	function filterAndSortUids(uids, data, callback) {
 		var sortBy = data.sortBy || 'joindate';
 
-		var fields = ['uid', 'status', 'lastonline', 'banned', sortBy];
+		var fields = ['uid', 'status', 'lastonline', 'banned', 'flags', sortBy];
 
 		User.getUsersFields(uids, fields, function(err, userData) {
 			if (err) {
@@ -96,13 +96,19 @@ module.exports = function(User) {
 					return user && user.status !== 'offline' && (Date.now() - parseInt(user.lastonline, 10) < 300000);
 				});
 			}
-			
-			if(data.bannedOnly) {
+
+			if (data.bannedOnly) {
 				userData = userData.filter(function(user) {
 					return user && user.banned;
 				});
 			}
 
+			if (data.flaggedOnly) {
+				userData = userData.filter(function(user) {
+					return user && parseInt(user.flags, 10) > 0;
+				});
+			}
+
 			sortUsers(userData, sortBy);
 
 			uids = userData.map(function(user) {
diff --git a/src/views/admin/manage/flags.tpl b/src/views/admin/manage/flags.tpl
index a7a9253163..1bd1726b97 100644
--- a/src/views/admin/manage/flags.tpl
+++ b/src/views/admin/manage/flags.tpl
@@ -1,32 +1,48 @@
 <div class="flags">
-	<div class="col-lg-9">
-		<div data-next="{next}">
-			<form id="flag-search" method="GET" action="flags">
-				<div class="form-group">
-					<div class="row">
-						<div class="col-md-6">
-							<label>Flags by user</label>
-							<input type="text" class="form-control" id="byUsername" placeholder="Search flagged posts by username" name="byUsername" value="{byUsername}">
-						</div>
+
+	<div class="col-lg-12">
+
+		<div class="text-center">
+			<div class="panel panel-default">
+				<div class="panel-body">
+					<div><canvas id="flags:daily" height="250"></canvas></div>
+					<p>
+
+					</p>
+				</div>
+				<div class="panel-footer"><small>Daily flags</small></div>
+			</div>
+		</div>
+
+		<form id="flag-search" method="GET" action="flags">
+			<div class="form-group">
+				<div>
+					<div>
+						<label>Flags by user</label>
+						<input type="text" class="form-control" id="byUsername" placeholder="Search flagged posts by username" name="byUsername" value="{byUsername}">
 					</div>
 				</div>
+			</div>
 
-				<div class="form-group">
-					<label>Sort By</label>
-					<div class="row">
-						<div class="col-md-6">
-							<select id="flag-sort-by" class="form-control" name="sortBy">
-								<option value="count">Most Flags</option>
-								<option value="time">Most Recent</option>
-							</select>
-						</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>
+						</select>
 					</div>
 				</div>
+			</div>
+
+			<button type="submit" class="btn btn-primary">Search</button>
+			<button class="btn btn-primary" id="dismissAll">Dismiss All</button>
+		</form>
 
-				<button type="submit" class="btn btn-primary">Search</button>
-			</form>
-			<br />
-			<hr/>
+		<hr/>
+
+		<div data-next="{next}">
 
 			<div class="post-container" data-next="{next}">
 				<!-- IF !posts.length -->
@@ -53,7 +69,6 @@
 								</a>
 								<div class="content">
 									<p>{posts.content}</p>
-									<p class="fade-out"></p>
 								</div>
 								<small>
 									<span class="pull-right">
@@ -92,15 +107,4 @@
 			</div>
 		</div>
 	</div>
-
-	<div class="col-lg-3 acp-sidebar">
-		<div class="panel panel-default">
-			<div class="panel-heading">Flags Control Panel</div>
-			<div class="panel-body">
-				<div>
-					<button class="btn btn-primary" id="dismissAll">Dismiss All</button>
-				</div>
-			</div>
-		</div>
-	</div>
 </div>
diff --git a/src/views/admin/manage/users.tpl b/src/views/admin/manage/users.tpl
index 644bde38f6..6af10174bb 100644
--- a/src/views/admin/manage/users.tpl
+++ b/src/views/admin/manage/users.tpl
@@ -8,6 +8,7 @@
 					<li><a href='{config.relative_path}/admin/manage/users/not-validated'>Not validated</a></li>
 					<li><a href='{config.relative_path}/admin/manage/users/no-posts'>No Posts</a></li>
 					<li><a href='{config.relative_path}/admin/manage/users/inactive'>Inactive</a></li>
+					<li><a href='{config.relative_path}/admin/manage/users/flagged'>Most Flags</a></li>
 					<li><a href='{config.relative_path}/admin/manage/users/banned'>Banned</a></li>
 					<li><a href='{config.relative_path}/admin/manage/users/search'>User Search</a></li>
 
@@ -84,7 +85,7 @@
 						posts {users.postcount}
 
 						<!-- IF users.flags -->
-						<div><small><span><i class="fa fa-flag"></i> {users.flags}</span></small></div>
+						<div><small><span><i class="fa fa-flag"></i> <a href="{config.relative_path}/admin/manage/flags?byUsername={users.username}">{users.flags}</a></span></small></div>
 						<!-- ENDIF users.flags -->
 					</div>
 					<!-- END users -->