{posts.content}
-diff --git a/public/src/client/flags/list.js b/public/src/client/flags/list.js index bf8b4bc1a8..7647e1db6d 100644 --- a/public/src/client/flags/list.js +++ b/public/src/client/flags/list.js @@ -2,12 +2,13 @@ /* globals define */ -define('forum/flags/list', ['components'], function (components) { +define('forum/flags/list', ['components', 'Chart'], function (components, Chart) { var Flags = {}; Flags.init = function () { Flags.enableFilterForm(); Flags.enableChatButtons(); + Flags.handleGraphs(); }; Flags.enableFilterForm = function () { @@ -38,5 +39,53 @@ define('forum/flags/list', ['components'], function (components) { }); }; + Flags.handleGraphs = function () { + 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 + } + }] + } + } + }); + }; + return Flags; }); diff --git a/src/controllers/mods.js b/src/controllers/mods.js index 62f7b6dd07..39820e60a6 100644 --- a/src/controllers/mods.js +++ b/src/controllers/mods.js @@ -4,6 +4,7 @@ var async = require('async'); var user = require('../user'); var flags = require('../flags'); +var analytics = require('../analytics'); // var adminFlagsController = require('./admin/flags'); var modsController = { @@ -35,13 +36,17 @@ modsController.flags.list = function (req, res, next) { return memo; }, {}); - flags.list(filters, req.uid, function (err, flags) { + async.parallel({ + flags: async.apply(flags.list, filters, req.uid), + analytics: async.apply(analytics.getDailyStatsForSet, 'analytics:flags', Date.now(), 30) + }, function (err, data) { if (err) { return next(err); } res.render('flags/list', { - flags: flags, + flags: data.flags, + analytics: data.analytics, hasFilter: !!Object.keys(filters).length, filters: filters, title: '[[pages:flags]]' diff --git a/src/flags.js b/src/flags.js index aafae593e0..7c56ba4fb5 100644 --- a/src/flags.js +++ b/src/flags.js @@ -267,8 +267,16 @@ Flags.getNotes = function (flagId, callback) { ], callback); }; -Flags.create = function (type, id, uid, reason, callback) { +Flags.create = function (type, id, uid, reason, timestamp, callback) { var targetUid; + var doHistoryAppend = false; + + // timestamp is optional + if (typeof timestamp === 'function' && !callback) { + callback = timestamp; + timestamp = Date.now(); + doHistoryAppend = true; + } async.waterfall([ function (next) { @@ -302,16 +310,17 @@ Flags.create = function (type, id, uid, reason, callback) { targetId: id, description: reason, uid: uid, - datetime: Date.now() + datetime: timestamp }), - 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 + async.apply(db.sortedSetAdd.bind(db), 'flags:datetime', timestamp, flagId), // by time, the default + async.apply(db.sortedSetAdd.bind(db), 'flags:byReporter:' + uid, timestamp, flagId), // by reporter + async.apply(db.sortedSetAdd.bind(db), 'flags:byType:' + type, timestamp, flagId), // by flag type + async.apply(db.setObjectField.bind(db), 'flagHash:flagId', [type, id, uid].join(':'), flagId), // save hash for existence checking + async.apply(analytics.increment, 'flags') // some fancy analytics ]; if (targetUid) { - tasks.push(async.apply(db.sortedSetAdd.bind(db), 'flags:byTargetUid:' + targetUid, Date.now(), flagId)); // by target uid + tasks.push(async.apply(db.sortedSetAdd.bind(db), 'flags:byTargetUid:' + targetUid, timestamp, flagId)); // by target uid } async.parallel(tasks, function (err, data) { @@ -319,7 +328,10 @@ Flags.create = function (type, id, uid, reason, callback) { return next(err); } - Flags.update(flagId, uid, { "state": "open" }); + if (doHistoryAppend) { + Flags.update(flagId, uid, { "state": "open" }); + } + next(null, flagId); }); }, @@ -423,7 +435,7 @@ Flags.update = function (flagId, uid, changeset, callback) { // Retrieve existing flag data to compare for history-saving purposes var fields = ['state', 'assignee']; var tasks = []; - var now = Date.now(); + var now = changeset.datetime || Date.now(); async.waterfall([ async.apply(db.getObjectFields.bind(db), 'flag:' + flagId, fields), @@ -513,16 +525,24 @@ Flags.getHistory = function (flagId, callback) { Flags.appendHistory = function (flagId, uid, changeset, callback) { var payload; + var datetime = changeset.datetime || Date.now(); + delete changeset.datetime; + try { - payload = JSON.stringify([uid, changeset, Date.now()]); + payload = JSON.stringify([uid, changeset, datetime]); } catch (e) { return callback(e); } - db.sortedSetAdd('flag:' + flagId + ':history', Date.now(), payload, callback); + db.sortedSetAdd('flag:' + flagId + ':history', datetime, payload, callback); }; -Flags.appendNote = function (flagId, uid, note, callback) { +Flags.appendNote = function (flagId, uid, note, datetime, callback) { + if (typeof datetime === 'function' && !callback) { + callback = datetime; + datetime = Date.now(); + } + var payload; try { payload = JSON.stringify([uid, note]); @@ -531,9 +551,10 @@ Flags.appendNote = function (flagId, uid, note, callback) { } async.waterfall([ - async.apply(db.sortedSetAdd, 'flag:' + flagId + ':notes', Date.now(), payload), + async.apply(db.sortedSetAdd, 'flag:' + flagId + ':notes', datetime, payload), async.apply(Flags.appendHistory, flagId, uid, { - notes: null + notes: null, + datetime: datetime }) ], callback); }; diff --git a/src/upgrade.js b/src/upgrade.js index 2a605e84f9..29b806d4a7 100644 --- a/src/upgrade.js +++ b/src/upgrade.js @@ -12,7 +12,7 @@ var db = require('./database'), schemaDate, thisSchemaDate, // IMPORTANT: REMEMBER TO UPDATE VALUE OF latestSchema - latestSchema = Date.UTC(2016, 10, 22); + latestSchema = Date.UTC(2016, 11, 7); Upgrade.check = function (callback) { db.get('schemaDate', function (err, value) { @@ -1023,7 +1023,7 @@ Upgrade.upgrade = function (callback) { if (schemaDate < thisSchemaDate) { updatesMade = true; - winston.info('[2016/11/25] Creating sorted sets for pinned topcis'); + winston.info('[2016/11/25] Creating sorted sets for pinned topics'); var topics = require('./topics'); var batch = require('./batch'); @@ -1060,6 +1060,89 @@ Upgrade.upgrade = function (callback) { next(); } }, + function (next) { + thisSchemaDate = Date.UTC(2016, 11, 7); + + if (schemaDate < thisSchemaDate) { + updatesMade = true; + winston.info('[2016/12/07] Migrating flags to new schema (#5232)'); + + var batch = require('./batch'); + var posts = require('./posts'); + var flags = require('./flags'); + var migrated = 0; + + batch.processSortedSet('posts:pid', function (ids, next) { + posts.getPostsByPids(ids, 1, function (err, posts) { + if (err) { + return next(err); + } + + posts = posts.filter(function (post) { + return post.hasOwnProperty('flags'); + }); + + async.each(posts, function (post, next) { + async.parallel({ + uids: async.apply(db.getSortedSetRangeWithScores, 'pid:' + post.pid + ':flag:uids', 0, -1), + reasons: async.apply(db.getSortedSetRange, 'pid:' + post.pid + ':flag:uid:reason', 0, -1) + }, function (err, data) { + if (err) { + return next(err); + } + + // Just take the first entry + var datetime = data.uids[0].score; + var reason = data.reasons[0].split(':')[1]; + var flagObj; + + async.waterfall([ + async.apply(flags.create, 'post', post.pid, data.uids[0].value, reason, datetime), + function (_flagObj, next) { + flagObj = _flagObj; + if (post['flag:state'] || post['flag:assignee']) { + flags.update(flagObj.flagId, 1, { + state: post['flag:state'], + assignee: post['flag:assignee'], + datetime: datetime + }, next); + } else { + setImmediate(next); + } + }, + function (next) { + if (post.hasOwnProperty('flag:notes') && post['flag:notes'].length) { + try { + var history = JSON.parse(post['flag:history']); + history = history.filter(function (event) { + return event.type === 'notes'; + })[0]; + + flags.appendNote(flagObj.flagId, history.uid, post['flag:notes'], history.timestamp, next); + } catch (e) { + next(e); + } + } else { + setImmediate(next); + } + } + ], next); + }); + }, next); + }); + }, function (err) { + if (err) { + return next(err); + } + + winston.info('[2016/12/07] Migrating flags to new schema (#5232) - done'); + Upgrade.update(thisSchemaDate, next); + }); + } else { + winston.info('[2016/12/07] Migrating flags to new schema (#5232) - skipped!'); + next(); + } + } // Add new schema updates here // IMPORTANT: REMEMBER TO UPDATE VALUE OF latestSchema IN LINE 24!!! ], function (err) { diff --git a/src/views/admin/manage/flags.tpl b/src/views/admin/manage/flags.tpl deleted file mode 100644 index a14348ba42..0000000000 --- a/src/views/admin/manage/flags.tpl +++ /dev/null @@ -1,196 +0,0 @@ -
- -
-{posts.content}
---- -
-- - - -
- -- - - - {../user.username} - : "{posts.flagReasons.reason}" -