From d9d60c20bd743abd0a027efef474916278e3d793 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 1 Dec 2016 11:42:06 -0500 Subject: [PATCH] flag updating and note appending, #5232 --- public/language/en-GB/flags.json | 4 +- public/src/client/flags/detail.js | 60 +++++++++++ public/src/client/topic/flag.js | 2 +- src/flags.js | 156 ++++++--------------------- src/socket.io/flags.js | 167 ++++++++++++++++++++++++++++ src/socket.io/index.js | 6 +- src/socket.io/posts.js | 1 - src/socket.io/posts/flag.js | 173 ------------------------------ 8 files changed, 270 insertions(+), 299 deletions(-) create mode 100644 public/src/client/flags/detail.js create mode 100644 src/socket.io/flags.js delete mode 100644 src/socket.io/posts/flag.js diff --git a/public/language/en-GB/flags.json b/public/language/en-GB/flags.json index d266e2c4db..0c1f32b88e 100644 --- a/public/language/en-GB/flags.json +++ b/public/language/en-GB/flags.json @@ -14,5 +14,7 @@ "state-wip": "Work in Progress", "state-resolved": "Resolved", "state-rejected": "Rejected", - "no-assignee": "Not Assigned" + "no-assignee": "Not Assigned", + "updated": "Flag Details Updated", + "note-added": "Note Added" } \ No newline at end of file diff --git a/public/src/client/flags/detail.js b/public/src/client/flags/detail.js new file mode 100644 index 0000000000..d23a1941b5 --- /dev/null +++ b/public/src/client/flags/detail.js @@ -0,0 +1,60 @@ +'use strict'; + +/* globals define */ + +define('forum/flags/detail', ['components'], function (components) { + var Flags = {}; + + Flags.init = function () { + // Update attributes + $('#state').val(ajaxify.data.state).removeAttr('disabled'); + $('#assignee').val(ajaxify.data.assignee).removeAttr('disabled'); + + $('[data-action]').on('click', function () { + var action = this.getAttribute('data-action'); + + switch (action) { + case 'update': + socket.emit('flags.update', { + flagId: ajaxify.data.flagId, + data: $('#attributes').serializeArray() + }, function (err) { + if (err) { + return app.alertError(err.message); + } else { + app.alertSuccess('[[flags:updated]]'); + } + }); + break; + + case 'appendNote': + socket.emit('flags.appendNote', { + flagId: ajaxify.data.flagId, + note: document.getElementById('note').value + }, function (err, notes) { + if (err) { + return app.alertError(err.message); + } else { + app.alertSuccess('[[flags:note-added]]'); + Flags.reloadNotes(notes); + } + }); + break; + } + }); + }; + + Flags.reloadNotes = function (notes) { + templates.parse('flags/detail', 'notes', { + notes: notes + }, function (html) { + var wrapperEl = components.get('flag/notes'); + wrapperEl.empty(); + wrapperEl.html(html); + wrapperEl.find('span.timeago').timeago(); + document.getElementById('note').value = ''; + }); + }; + + return Flags; +}); diff --git a/public/src/client/topic/flag.js b/public/src/client/topic/flag.js index 78b1dd5d2a..046585ae68 100644 --- a/public/src/client/topic/flag.js +++ b/public/src/client/topic/flag.js @@ -48,7 +48,7 @@ define('forum/topic/flag', [], function () { if (!pid || !reason) { return; } - socket.emit('posts.flag', {pid: pid, reason: reason}, function (err) { + socket.emit('flags.create', {pid: pid, reason: reason}, function (err) { if (err) { return app.alertError(err.message); } diff --git a/src/flags.js b/src/flags.js index 99039aa175..323a2a1e07 100644 --- a/src/flags.js +++ b/src/flags.js @@ -373,138 +373,52 @@ Flags.dismissByUid = function (uid, callback) { }); }; -// New method signature (type, id, flagObj, callback) and name (.update()) -// uid used in history string, which should be rewritten too. -Flags.update = function (uid, pid, flagObj, callback) { +Flags.update = function (flagId, uid, changeset, callback) { // Retrieve existing flag data to compare for history-saving purposes - var changes = []; - var changeset = {}; - var prop; + var fields = ['state', 'assignee']; - posts.getPostData(pid, function (err, postData) { - if (err) { - return callback(err); - } - - // Track new additions - for(prop in flagObj) { - if (flagObj.hasOwnProperty(prop) && !postData.hasOwnProperty('flag:' + prop) && flagObj[prop].length) { - changes.push(prop); - } - } - - // Track changed items - for(prop in postData) { - if ( - postData.hasOwnProperty(prop) && prop.startsWith('flag:') && - flagObj.hasOwnProperty(prop.slice(5)) && - postData[prop] !== flagObj[prop.slice(5)] - ) { - changes.push(prop.slice(5)); - } - } - - changeset = changes.reduce(function (memo, prop) { - memo['flag:' + prop] = flagObj[prop]; - return memo; - }, {}); - - // Append changes to history string - if (changes.length) { - try { - var history = JSON.parse(postData['flag:history'] || '[]'); - - changes.forEach(function (property) { - switch(property) { - case 'assignee': // intentional fall-through - case 'state': - history.unshift({ - uid: uid, - type: property, - value: flagObj[property], - timestamp: Date.now() - }); - break; - - case 'notes': - history.unshift({ - uid: uid, - type: property, - timestamp: Date.now() - }); + async.waterfall([ + async.apply(db.getObjectFields.bind(db), 'flag:' + flagId, fields), + function (current, next) { + for(var prop in changeset) { + if (changeset.hasOwnProperty(prop)) { + if (current[prop] === changeset[prop]) { + delete changeset[prop]; } - }); + } + } - changeset['flag:history'] = JSON.stringify(history); - } catch (e) { - winston.warn('[flags/update] Unable to deserialise post flag history, likely malformed data'); + if (!Object.keys(changeset).length) { + // No changes + return next(); } - } - // Save flag data into post hash - if (changes.length) { - posts.setPostFields(pid, changeset, callback); - } else { - setImmediate(callback); + async.parallel([ + // Save new object to db (upsert) + async.apply(db.setObject, 'flag:' + flagId, changeset), + // Append history + async.apply(Flags.appendHistory, flagId, uid, Object.keys(changeset)) + ], next); } - }); + ], callback); }; -// To be rewritten and deprecated -Flags.expandFlagHistory = function (posts, callback) { - // Expand flag history - async.map(posts, function (post, next) { - var history; - try { - history = JSON.parse(post['flag:history'] || '[]'); - } catch (e) { - winston.warn('[flags/get] Unable to deserialise post flag history, likely malformed data'); - return callback(e); - } - - async.map(history, function (event, next) { - event.timestampISO = new Date(event.timestamp).toISOString(); - - async.parallel([ - function (next) { - user.getUserFields(event.uid, ['username', 'picture'], function (err, userData) { - if (err) { - return next(err); - } +Flags.appendHistory = function (flagId, uid, changeset, callback) { + return callback(); +}; - event.user = userData; - next(); - }); - }, - function (next) { - if (event.type === 'assignee') { - user.getUserField(parseInt(event.value, 10), 'username', function (err, username) { - if (err) { - return next(err); - } - - event.label = username || 'Unknown user'; - next(null); - }); - } else if (event.type === 'state') { - event.label = '[[topic:flag_manage_state_' + event.value + ']]'; - setImmediate(next); - } else { - setImmediate(next); - } - } - ], function (err) { - next(err, event); - }); - }, function (err, history) { - if (err) { - return next(err); - } +Flags.appendNote = function (flagId, uid, note, callback) { + var payload; + try { + payload = JSON.stringify([uid, note]); + } catch (e) { + return callback(e); + } - post['flag:history'] = history; - next(null, post); - }); - }, callback); + async.waterfall([ + async.apply(db.sortedSetAdd, 'flag:' + flagId + ':notes', Date.now(), payload), + async.apply(Flags.getNotes, flagId) + ], callback); }; module.exports = Flags; \ No newline at end of file diff --git a/src/socket.io/flags.js b/src/socket.io/flags.js new file mode 100644 index 0000000000..28db7551a6 --- /dev/null +++ b/src/socket.io/flags.js @@ -0,0 +1,167 @@ +'use strict'; + +var async = require('async'); +var S = require('string'); + +var user = require('../user'); +var groups = require('../groups'); +var posts = require('../posts'); +var topics = require('../topics'); +var privileges = require('../privileges'); +var notifications = require('../notifications'); +var plugins = require('../plugins'); +var meta = require('../meta'); +var utils = require('../../public/src/utils'); +var flags = require('../flags'); + +var SocketFlags = {}; + +SocketFlags.create = function (socket, data, callback) { + if (!socket.uid) { + return callback(new Error('[[error:not-logged-in]]')); + } + + if (!data || !data.pid || !data.reason) { + return callback(new Error('[[error:invalid-data]]')); + } + + var flaggingUser = {}; + var post; + + async.waterfall([ + function (next) { + posts.getPostFields(data.pid, ['pid', 'tid', 'uid', 'content', 'deleted'], next); + }, + function (postData, next) { + if (parseInt(postData.deleted, 10) === 1) { + return next(new Error('[[error:post-deleted]]')); + } + + post = postData; + topics.getTopicFields(post.tid, ['title', 'cid'], next); + }, + function (topicData, next) { + post.topic = topicData; + + async.parallel({ + isAdminOrMod: function (next) { + privileges.categories.isAdminOrMod(post.topic.cid, socket.uid, next); + }, + userData: function (next) { + user.getUserFields(socket.uid, ['username', 'reputation', 'banned'], next); + } + }, next); + }, + function (user, next) { + var minimumReputation = utils.isNumber(meta.config['privileges:flag']) ? parseInt(meta.config['privileges:flag'], 10) : 1; + if (!user.isAdminOrMod && parseInt(user.userData.reputation, 10) < minimumReputation) { + return next(new Error('[[error:not-enough-reputation-to-flag]]')); + } + + if (parseInt(user.banned, 10) === 1) { + return next(new Error('[[error:user-banned]]')); + } + + flaggingUser = user.userData; + flaggingUser.uid = socket.uid; + + flags.create('post', post.pid, socket.uid, data.reason, next); + }, + function (next) { + async.parallel({ + post: function (next) { + posts.parsePost(post, next); + }, + admins: function (next) { + groups.getMembers('administrators', 0, -1, next); + }, + globalMods: function (next) { + groups.getMembers('Global Moderators', 0, -1, next); + }, + moderators: function (next) { + groups.getMembers('cid:' + post.topic.cid + ':privileges:mods', 0, -1, next); + } + }, next); + }, + function (results, next) { + var title = S(post.topic.title).decodeHTMLEntities().s; + var titleEscaped = title.replace(/%/g, '%').replace(/,/g, ','); + + notifications.create({ + bodyShort: '[[notifications:user_flagged_post_in, ' + flaggingUser.username + ', ' + titleEscaped + ']]', + bodyLong: post.content, + pid: data.pid, + path: '/post/' + data.pid, + nid: 'post_flag:' + data.pid + ':uid:' + socket.uid, + from: socket.uid, + mergeId: 'notifications:user_flagged_post_in|' + data.pid, + topicTitle: post.topic.title + }, function (err, notification) { + if (err || !notification) { + return next(err); + } + + plugins.fireHook('action:post.flag', {post: post, reason: data.reason, flaggingUser: flaggingUser}); + notifications.push(notification, results.admins.concat(results.moderators).concat(results.globalMods), next); + }); + } + ], callback); +}; + +SocketFlags.update = function (socket, data, callback) { + if (!data || !(data.flagId && data.data)) { + return callback(new Error('[[error:invalid-data]]')); + } + + var payload = {}; + + async.waterfall([ + function (next) { + async.parallel([ + async.apply(user.isAdminOrGlobalMod, socket.uid), + async.apply(user.isModeratorOfAnyCategory, socket.uid) + ], function (err, results) { + next(err, results[0] || results[1]); + }); + }, + function (allowed, next) { + if (!allowed) { + return next(new Error('[[no-privileges]]')); + } + + // Translate form data into object + payload = data.data.reduce(function (memo, cur) { + memo[cur.name] = cur.value; + return memo; + }, payload); + + flags.update(data.flagId, socket.uid, payload, next); + } + ], callback); +}; + +SocketFlags.appendNote = function (socket, data, callback) { + if (!data || !(data.flagId && data.note)) { + return callback(new Error('[[error:invalid-data]]')); + } + + async.waterfall([ + function (next) { + async.parallel([ + async.apply(user.isAdminOrGlobalMod, socket.uid), + async.apply(user.isModeratorOfAnyCategory, socket.uid) + ], function (err, results) { + next(err, results[0] || results[1]); + }); + }, + function (allowed, next) { + if (!allowed) { + return next(new Error('[[no-privileges]]')); + } + + flags.appendNote(data.flagId, socket.uid, data.note, next); + } + ], callback); +}; + +module.exports = SocketFlags; diff --git a/src/socket.io/index.js b/src/socket.io/index.js index 38e73c1cc1..43157f19ad 100644 --- a/src/socket.io/index.js +++ b/src/socket.io/index.js @@ -123,8 +123,10 @@ var ratelimit = require('../middleware/ratelimit'); } function requireModules() { - var modules = ['admin', 'categories', 'groups', 'meta', 'modules', - 'notifications', 'plugins', 'posts', 'topics', 'user', 'blacklist' + var modules = [ + 'admin', 'categories', 'groups', 'meta', 'modules', + 'notifications', 'plugins', 'posts', 'topics', 'user', + 'blacklist', 'flags' ]; modules.forEach(function (module) { diff --git a/src/socket.io/posts.js b/src/socket.io/posts.js index fe729a5c11..b5309dd3cf 100644 --- a/src/socket.io/posts.js +++ b/src/socket.io/posts.js @@ -20,7 +20,6 @@ require('./posts/move')(SocketPosts); require('./posts/votes')(SocketPosts); require('./posts/bookmarks')(SocketPosts); require('./posts/tools')(SocketPosts); -require('./posts/flag')(SocketPosts); SocketPosts.reply = function (socket, data, callback) { if (!data || !data.tid || !data.content) { diff --git a/src/socket.io/posts/flag.js b/src/socket.io/posts/flag.js deleted file mode 100644 index 88b47058d1..0000000000 --- a/src/socket.io/posts/flag.js +++ /dev/null @@ -1,173 +0,0 @@ -'use strict'; - -var async = require('async'); -var S = require('string'); - -var user = require('../../user'); -var groups = require('../../groups'); -var posts = require('../../posts'); -var topics = require('../../topics'); -var privileges = require('../../privileges'); -var notifications = require('../../notifications'); -var plugins = require('../../plugins'); -var meta = require('../../meta'); -var utils = require('../../../public/src/utils'); -var flags = require('../../flags'); - -module.exports = function (SocketPosts) { - - SocketPosts.flag = function (socket, data, callback) { - if (!socket.uid) { - return callback(new Error('[[error:not-logged-in]]')); - } - - if (!data || !data.pid || !data.reason) { - return callback(new Error('[[error:invalid-data]]')); - } - - var flaggingUser = {}; - var post; - - async.waterfall([ - function (next) { - posts.getPostFields(data.pid, ['pid', 'tid', 'uid', 'content', 'deleted'], next); - }, - function (postData, next) { - if (parseInt(postData.deleted, 10) === 1) { - return next(new Error('[[error:post-deleted]]')); - } - - post = postData; - topics.getTopicFields(post.tid, ['title', 'cid'], next); - }, - function (topicData, next) { - post.topic = topicData; - - async.parallel({ - isAdminOrMod: function (next) { - privileges.categories.isAdminOrMod(post.topic.cid, socket.uid, next); - }, - userData: function (next) { - user.getUserFields(socket.uid, ['username', 'reputation', 'banned'], next); - } - }, next); - }, - function (user, next) { - var minimumReputation = utils.isNumber(meta.config['privileges:flag']) ? parseInt(meta.config['privileges:flag'], 10) : 1; - if (!user.isAdminOrMod && parseInt(user.userData.reputation, 10) < minimumReputation) { - return next(new Error('[[error:not-enough-reputation-to-flag]]')); - } - - if (parseInt(user.banned, 10) === 1) { - return next(new Error('[[error:user-banned]]')); - } - - flaggingUser = user.userData; - flaggingUser.uid = socket.uid; - - flags.create('post', post.pid, socket.uid, data.reason, next); - }, - function (next) { - async.parallel({ - post: function (next) { - posts.parsePost(post, next); - }, - admins: function (next) { - groups.getMembers('administrators', 0, -1, next); - }, - globalMods: function (next) { - groups.getMembers('Global Moderators', 0, -1, next); - }, - moderators: function (next) { - groups.getMembers('cid:' + post.topic.cid + ':privileges:mods', 0, -1, next); - } - }, next); - }, - function (results, next) { - var title = S(post.topic.title).decodeHTMLEntities().s; - var titleEscaped = title.replace(/%/g, '%').replace(/,/g, ','); - - notifications.create({ - bodyShort: '[[notifications:user_flagged_post_in, ' + flaggingUser.username + ', ' + titleEscaped + ']]', - bodyLong: post.content, - pid: data.pid, - path: '/post/' + data.pid, - nid: 'post_flag:' + data.pid + ':uid:' + socket.uid, - from: socket.uid, - mergeId: 'notifications:user_flagged_post_in|' + data.pid, - topicTitle: post.topic.title - }, function (err, notification) { - if (err || !notification) { - return next(err); - } - - plugins.fireHook('action:post.flag', {post: post, reason: data.reason, flaggingUser: flaggingUser}); - notifications.push(notification, results.admins.concat(results.moderators).concat(results.globalMods), next); - }); - } - ], callback); - }; - - SocketPosts.dismissFlag = function (socket, pid, callback) { - if (!pid || !socket.uid) { - return callback(new Error('[[error:invalid-data]]')); - } - async.waterfall([ - function (next) { - user.isAdminOrGlobalMod(socket.uid, next); - }, - function (isAdminOrGlobalModerator, next) { - if (!isAdminOrGlobalModerator) { - return next(new Error('[[no-privileges]]')); - } - flags.dismiss(pid, next); - } - ], callback); - }; - - SocketPosts.dismissAllFlags = function (socket, data, callback) { - async.waterfall([ - function (next) { - user.isAdminOrGlobalMod(socket.uid, next); - }, - function (isAdminOrGlobalModerator, next) { - if (!isAdminOrGlobalModerator) { - return next(new Error('[[no-privileges]]')); - } - flags.dismissAll(next); - } - ], callback); - }; - - SocketPosts.updateFlag = function (socket, data, callback) { - if (!data || !(data.pid && data.data)) { - return callback(new Error('[[error:invalid-data]]')); - } - - var payload = {}; - - async.waterfall([ - function (next) { - async.parallel([ - async.apply(user.isAdminOrGlobalMod, socket.uid), - async.apply(user.isModeratorOfAnyCategory, socket.uid) - ], function (err, results) { - next(err, results[0] || results[1]); - }); - }, - function (allowed, next) { - if (!allowed) { - return next(new Error('[[no-privileges]]')); - } - - // Translate form data into object - payload = data.data.reduce(function (memo, cur) { - memo[cur.name] = cur.value; - return memo; - }, payload); - - flags.update(socket.uid, data.pid, payload, next); - } - ], callback); - }; -};