diff --git a/public/language/en-GB/flags.json b/public/language/en-GB/flags.json index 0cdc3fbfb9..cdc3c4069e 100644 --- a/public/language/en-GB/flags.json +++ b/public/language/en-GB/flags.json @@ -9,6 +9,13 @@ "update": "Update", "updated": "Updated", + "filters": "Filter Options", + "filter-reporterId": "Reporter UID", + "filter-type": "Flag Type", + "filter-type-all": "All Content", + "filter-type-post": "Post", + "apply-filters": "Apply Filters", + "notes": "Flag Notes", "add-note": "Add Note", "no-notes": "No shared notes.", diff --git a/public/language/en-GB/topic.json b/public/language/en-GB/topic.json index ce7f35f816..4ae208076e 100644 --- a/public/language/en-GB/topic.json +++ b/public/language/en-GB/topic.json @@ -37,12 +37,6 @@ "flag_title": "Flag this post for moderation", "flag_success": "This post has been flagged for moderation.", - "flag_manage_history": "Action History", - "flag_manage_no_history": "No event history to report", - "flag_manage_history_assignee": "Assigned to %1", - "flag_manage_history_state": "Updated state to %1", - "flag_manage_history_notes": "Updated flag notes", - "flag_manage_saved": "Flag Details Updated", "deleted_message": "This topic has been deleted. Only users with topic management privileges can see it.", diff --git a/public/src/admin/manage/flags.js b/public/src/admin/manage/flags.js deleted file mode 100644 index fb8b32d602..0000000000 --- a/public/src/admin/manage/flags.js +++ /dev/null @@ -1,172 +0,0 @@ -"use strict"; -/*global define, socket, app, utils, bootbox, ajaxify*/ - -define('admin/manage/flags', [ - 'autocomplete', - 'Chart', - 'components' -], function (autocomplete, Chart, components) { - - var Flags = {}; - - Flags.init = function () { - $('.post-container .content img:not(.not-responsive)').addClass('img-responsive'); - - autocomplete.user($('#byUsername')); - - handleDismiss(); - handleDismissAll(); - handleDelete(); - handleGraphs(); - - updateFlagDetails(ajaxify.data.posts); - - components.get('posts/flags').on('click', '[component="posts/flag/update"]', updateFlag); - - // Open flag as indicated in location bar - if (window.location.hash.startsWith('#flag-pid-')) { - $(window.location.hash).collapse('toggle'); - } - }; - - function handleDismiss() { - $('.flags').on('click', '.dismiss', function () { - var btn = $(this); - var pid = btn.parents('[data-pid]').attr('data-pid'); - - socket.emit('posts.dismissFlag', pid, function (err) { - done(err, btn); - }); - }); - } - - function handleDismissAll() { - $('#dismissAll').on('click', function () { - socket.emit('posts.dismissAllFlags', function (err) { - if (err) { - return app.alertError(err.message); - } - - ajaxify.refresh(); - }); - return false; - }); - } - - function handleDelete() { - $('.flags').on('click', '.delete', function () { - var btn = $(this); - bootbox.confirm('Do you really want to delete this post?', function (confirm) { - if (!confirm) { - return; - } - var pid = btn.parents('[data-pid]').attr('data-pid'); - var tid = btn.parents('[data-pid]').attr('data-tid'); - socket.emit('posts.delete', {pid: pid, tid: tid}, function (err) { - done(err, btn); - }); - }); - }); - } - - function done(err, btn) { - if (err) { - return app.alertError(err.messaage); - } - btn.parents('[data-pid]').fadeOut(function () { - $(this).remove(); - if (!$('.flags [data-pid]').length) { - $('.post-container').text('No flagged posts!'); - } - }); - } - - 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.tooltips.enabled = false; - } - var data = { - 'flags:daily': { - labels: dailyLabels, - datasets: [ - { - label: "", - backgroundColor: "rgba(151,187,205,0.2)", - borderColor: "rgba(151,187,205,1)", - pointBackgroundColor: "rgba(151,187,205,1)", - pointHoverBackgroundColor: "#fff", - pointBorderColor: "#fff", - pointHoverBorderColor: "rgba(151,187,205,1)", - data: ajaxify.data.analytics - } - ] - } - }; - - dailyCanvas.width = $(dailyCanvas).parent().width(); - new Chart(dailyCanvas.getContext('2d'), { - type: 'line', - data: data['flags:daily'], - options: { - responsive: true, - animation: false, - legend: { - display: false - }, - scales: { - yAxes: [{ - ticks: { - beginAtZero: true - } - }] - } - } - }); - } - - function updateFlagDetails(source) { - // As the flag details are returned in the API, update the form controls to show the correct data - - // Create reference hash for use in this method - source = source.reduce(function (memo, cur) { - memo[cur.pid] = cur.flagData; - return memo; - }, {}); - - components.get('posts/flag').each(function (idx, el) { - var pid = el.getAttribute('data-pid'); - var el = $(el); - - if (source[pid]) { - for(var prop in source[pid]) { - if (source[pid].hasOwnProperty(prop)) { - el.find('[name="' + prop + '"]').val(source[pid][prop]); - } - } - } - }); - } - - function updateFlag() { - var pid = $(this).parents('[component="posts/flag"]').attr('data-pid'); - var formData = $($(this).parents('form').get(0)).serializeArray(); - - socket.emit('posts.updateFlag', { - pid: pid, - data: formData - }, function (err) { - if (err) { - return app.alertError(err.message); - } else { - app.alertSuccess('[[topic:flag_manage_saved]]'); - } - }); - } - - return Flags; -}); \ No newline at end of file diff --git a/public/src/client/flags/detail.js b/public/src/client/flags/detail.js index aef21ec925..e20f05dba9 100644 --- a/public/src/client/flags/detail.js +++ b/public/src/client/flags/detail.js @@ -2,7 +2,7 @@ /* globals define */ -define('forum/flags/detail', ['components', 'translator'], function (components, translator) { +define('forum/flags/detail', ['forum/flags/list', 'components', 'translator'], function (FlagsList, components, translator) { var Flags = {}; Flags.init = function () { @@ -44,6 +44,8 @@ define('forum/flags/detail', ['components', 'translator'], function (components, break; } }); + + FlagsList.enableFilterForm(); }; Flags.reloadNotes = function (notes) { diff --git a/public/src/client/flags/list.js b/public/src/client/flags/list.js new file mode 100644 index 0000000000..6111372150 --- /dev/null +++ b/public/src/client/flags/list.js @@ -0,0 +1,28 @@ +'use strict'; + +/* globals define */ + +define('forum/flags/list', ['components'], function (components) { + var Flags = {}; + + Flags.init = function () { + Flags.enableFilterForm(); + }; + + Flags.enableFilterForm = function () { + var filtersEl = components.get('flags/filters'); + + filtersEl.find('button').on('click', function () { + var payload = filtersEl.serializeArray(); + var qs = payload.map(function (filter) { + if (filter.value) { + return filter.name + '=' + filter.value; + } + }).filter(Boolean).join('&'); + + ajaxify.go('flags?' + qs); + }) + }; + + return Flags; +}); diff --git a/src/controllers/admin.js b/src/controllers/admin.js index 7f622466cd..c58def03cf 100644 --- a/src/controllers/admin.js +++ b/src/controllers/admin.js @@ -4,7 +4,6 @@ var adminController = { dashboard: require('./admin/dashboard'), categories: require('./admin/categories'), tags: require('./admin/tags'), - flags: require('./admin/flags'), blacklist: require('./admin/blacklist'), groups: require('./admin/groups'), appearance: require('./admin/appearance'), diff --git a/src/controllers/admin/flags.js b/src/controllers/admin/flags.js deleted file mode 100644 index 80c31ba60a..0000000000 --- a/src/controllers/admin/flags.js +++ /dev/null @@ -1,104 +0,0 @@ -"use strict"; - -var async = require('async'); -var validator = require('validator'); - -var posts = require('../../posts'); -var user = require('../../user'); -var flags = require('../../flags'); -var categories = require('../../categories'); -var analytics = require('../../analytics'); -var pagination = require('../../pagination'); - -var flagsController = {}; - -var itemsPerPage = 20; - -flagsController.get = function (req, res, next) { - 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; - - async.parallel({ - categories: function (next) { - categories.buildForSelect(req.uid, next); - }, - flagData: function (next) { - getFlagData(req, res, next); - }, - analytics: function (next) { - analytics.getDailyStatsForSet('analytics:flags', Date.now(), 30, next); - }, - assignees: async.apply(user.getAdminsandGlobalModsandModerators) - }, function (err, results) { - if (err) { - return next(err); - } - - // Minimise data set for assignees so tjs does less work - results.assignees = results.assignees.map(function (userObj) { - return { - uid: userObj.uid, - username: userObj.username - }; - }); - - // If res.locals.cids is populated, then slim down the categories list - if (res.locals.cids) { - results.categories = results.categories.filter(function (category) { - return res.locals.cids.indexOf(String(category.cid)) !== -1; - }); - } - - 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, - categories: results.categories, - byUsername: validator.escape(String(byUsername)), - sortByCount: sortBy === 'count', - sortByTime: sortBy === 'time', - pagination: pagination.create(page, pageCount, req.query), - title: '[[pages:flagged-posts]]' - }; - res.render('admin/manage/flags', data); - }); -}; - -function getFlagData(req, res, callback) { - var sortBy = req.query.sortBy || 'count'; - var byUsername = req.query.byUsername || ''; - var cid = req.query.cid || res.locals.cids || 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']; - - 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'); - } - - flags.get(sets, cid, req.uid, start, stop, next); - } - ], callback); -} - - -module.exports = flagsController; diff --git a/src/controllers/mods.js b/src/controllers/mods.js index 012ffde3c6..759de90ef1 100644 --- a/src/controllers/mods.js +++ b/src/controllers/mods.js @@ -25,7 +25,17 @@ modsController.flags.list = function (req, res, next) { res.locals.cids = results.moderatedCids; } - flags.list({}, function (err, flags) { + // Parse query string params for filters + var valid = ['reporterId', 'type']; + var filters = valid.reduce(function (memo, cur) { + if (req.query.hasOwnProperty(cur)) { + memo[cur] = req.query[cur]; + } + + return memo; + }, {}); + + flags.list(filters, function (err, flags) { if (err) { return next(err); } diff --git a/src/flags.js b/src/flags.js index 58b230af06..6744d8ad59 100644 --- a/src/flags.js +++ b/src/flags.js @@ -53,8 +53,31 @@ Flags.list = function (filters, callback) { filters = {}; } + var sets = []; + if (Object.keys(filters).length > 0) { + for (var type in filters) { + switch (type) { + case 'type': + sets.push('flags:byType:' + filters[type]); + break; + + case 'reporterId': + sets.push('flags:byReporter:' + filters[type]); + break; + } + } + + } + sets = sets.length ? sets : ['flags:datetime']; // No filter default + async.waterfall([ - async.apply(db.getSortedSetRevRange.bind(db), 'flags:datetime', 0, 19), + function (next) { + if (sets.length === 1) { + db.getSortedSetRevRange(sets[0], 0, 19, next); + } else { + db.getSortedSetRevIntersect({sets: sets, start: 0, stop: -1, aggregate: 'MAX'}, next); + } + }, function (flagIds, next) { async.map(flagIds, function (flagId, next) { async.waterfall([ @@ -197,8 +220,10 @@ Flags.create = function (type, id, uid, reason, callback) { uid: uid, datetime: Date.now() })), - async.apply(db.sortedSetAdd.bind(db), 'flags:datetime', Date.now(), flagId), - async.apply(db.setObjectField.bind(db), 'flagHash:flagId', [type, id, uid].join(':'), flagId) + async.apply(db.sortedSetAdd.bind(db), 'flags:datetime', Date.now(), flagId), // by time, the default + async.apply(db.sortedSetAdd.bind(db), 'flags:byReporter:' + uid, Date.now(), flagId), // by reporter + async.apply(db.sortedSetAdd.bind(db), 'flags:byType:' + type, Date.now(), flagId), // by flag type + async.apply(db.setObjectField.bind(db), 'flagHash:flagId', [type, id, uid].join(':'), flagId) // save hash for existence checking ], function (err, data) { if (err) { return next(err); diff --git a/src/routes/admin.js b/src/routes/admin.js index 0611eede70..53b12fc4b8 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -56,7 +56,6 @@ function addRoutes(router, middleware, controllers) { router.get('/manage/categories/:category_id/analytics', middlewares, controllers.admin.categories.getAnalytics); router.get('/manage/tags', middlewares, controllers.admin.tags.get); - router.get('/manage/flags', middlewares, controllers.admin.flags.get); router.get('/manage/ip-blacklist', middlewares, controllers.admin.blacklist.get); router.get('/manage/users', middlewares, controllers.admin.users.sortByJoinDate); diff --git a/src/socket.io/flags.js b/src/socket.io/flags.js index 8b66dd094c..e6fb0be116 100644 --- a/src/socket.io/flags.js +++ b/src/socket.io/flags.js @@ -67,7 +67,7 @@ SocketFlags.create = function (socket, data, callback) { flags.create('post', post.pid, socket.uid, data.reason, next); }, - function (next) { + function (flagObj, next) { async.parallel({ post: function (next) { posts.parsePost(post, next);