mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-11-11 00:15:46 +01:00
wrapped up basic functionality of list and detail for flags, filter support. #5232
This commit is contained in:
@@ -9,6 +9,13 @@
|
|||||||
"update": "Update",
|
"update": "Update",
|
||||||
"updated": "Updated",
|
"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",
|
"notes": "Flag Notes",
|
||||||
"add-note": "Add Note",
|
"add-note": "Add Note",
|
||||||
"no-notes": "No shared notes.",
|
"no-notes": "No shared notes.",
|
||||||
|
|||||||
@@ -37,12 +37,6 @@
|
|||||||
|
|
||||||
"flag_title": "Flag this post for moderation",
|
"flag_title": "Flag this post for moderation",
|
||||||
"flag_success": "This post has been flagged 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.",
|
"deleted_message": "This topic has been deleted. Only users with topic management privileges can see it.",
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
|
||||||
});
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
/* globals define */
|
/* 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 = {};
|
var Flags = {};
|
||||||
|
|
||||||
Flags.init = function () {
|
Flags.init = function () {
|
||||||
@@ -44,6 +44,8 @@ define('forum/flags/detail', ['components', 'translator'], function (components,
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
FlagsList.enableFilterForm();
|
||||||
};
|
};
|
||||||
|
|
||||||
Flags.reloadNotes = function (notes) {
|
Flags.reloadNotes = function (notes) {
|
||||||
|
|||||||
28
public/src/client/flags/list.js
Normal file
28
public/src/client/flags/list.js
Normal file
@@ -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;
|
||||||
|
});
|
||||||
@@ -4,7 +4,6 @@ var adminController = {
|
|||||||
dashboard: require('./admin/dashboard'),
|
dashboard: require('./admin/dashboard'),
|
||||||
categories: require('./admin/categories'),
|
categories: require('./admin/categories'),
|
||||||
tags: require('./admin/tags'),
|
tags: require('./admin/tags'),
|
||||||
flags: require('./admin/flags'),
|
|
||||||
blacklist: require('./admin/blacklist'),
|
blacklist: require('./admin/blacklist'),
|
||||||
groups: require('./admin/groups'),
|
groups: require('./admin/groups'),
|
||||||
appearance: require('./admin/appearance'),
|
appearance: require('./admin/appearance'),
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -25,7 +25,17 @@ modsController.flags.list = function (req, res, next) {
|
|||||||
res.locals.cids = results.moderatedCids;
|
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) {
|
if (err) {
|
||||||
return next(err);
|
return next(err);
|
||||||
}
|
}
|
||||||
|
|||||||
31
src/flags.js
31
src/flags.js
@@ -53,8 +53,31 @@ Flags.list = function (filters, callback) {
|
|||||||
filters = {};
|
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.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) {
|
function (flagIds, next) {
|
||||||
async.map(flagIds, function (flagId, next) {
|
async.map(flagIds, function (flagId, next) {
|
||||||
async.waterfall([
|
async.waterfall([
|
||||||
@@ -197,8 +220,10 @@ Flags.create = function (type, id, uid, reason, callback) {
|
|||||||
uid: uid,
|
uid: uid,
|
||||||
datetime: Date.now()
|
datetime: Date.now()
|
||||||
})),
|
})),
|
||||||
async.apply(db.sortedSetAdd.bind(db), 'flags:datetime', Date.now(), flagId),
|
async.apply(db.sortedSetAdd.bind(db), 'flags:datetime', Date.now(), flagId), // by time, the default
|
||||||
async.apply(db.setObjectField.bind(db), 'flagHash:flagId', [type, id, uid].join(':'), flagId)
|
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) {
|
], function (err, data) {
|
||||||
if (err) {
|
if (err) {
|
||||||
return next(err);
|
return next(err);
|
||||||
|
|||||||
@@ -56,7 +56,6 @@ function addRoutes(router, middleware, controllers) {
|
|||||||
router.get('/manage/categories/:category_id/analytics', middlewares, controllers.admin.categories.getAnalytics);
|
router.get('/manage/categories/:category_id/analytics', middlewares, controllers.admin.categories.getAnalytics);
|
||||||
|
|
||||||
router.get('/manage/tags', middlewares, controllers.admin.tags.get);
|
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/ip-blacklist', middlewares, controllers.admin.blacklist.get);
|
||||||
|
|
||||||
router.get('/manage/users', middlewares, controllers.admin.users.sortByJoinDate);
|
router.get('/manage/users', middlewares, controllers.admin.users.sortByJoinDate);
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ SocketFlags.create = function (socket, data, callback) {
|
|||||||
|
|
||||||
flags.create('post', post.pid, socket.uid, data.reason, next);
|
flags.create('post', post.pid, socket.uid, data.reason, next);
|
||||||
},
|
},
|
||||||
function (next) {
|
function (flagObj, next) {
|
||||||
async.parallel({
|
async.parallel({
|
||||||
post: function (next) {
|
post: function (next) {
|
||||||
posts.parsePost(post, next);
|
posts.parsePost(post, next);
|
||||||
|
|||||||
Reference in New Issue
Block a user