v1.18.x
barisusakli 9 years ago
commit 8bffabeee4

@ -36,6 +36,22 @@
"flag_title": "Flag this post for moderation",
"flag_success": "This post has been flagged for moderation.",
"flag_manage_title": "Flagged post in %1",
"flag_manage_history": "Action History",
"flag_manage_no_history": "No event history to report",
"flag_manage_assignee": "Assignee",
"flag_manage_state": "State",
"flag_manage_state_open": "New/Open",
"flag_manage_state_wip": "Work in Progress",
"flag_manage_state_resolved": "Resolved",
"flag_manage_state_rejected": "Rejected",
"flag_manage_notes": "Shared Notes",
"flag_manage_update": "Update Flag Status",
"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.",
"following_topic.message": "You will now be receiving notifications when somebody posts to this topic.",

@ -31,4 +31,8 @@
.user-icon-style(24px, 1.5rem);
}
}
[component="posts/flag/history"] .avatar {
margin-right: 1rem;
}
}

@ -4,8 +4,9 @@
define('admin/manage/flags', [
'forum/infinitescroll',
'autocomplete',
'Chart'
], function(infinitescroll, autocomplete, Chart) {
'Chart',
'components'
], function(infinitescroll, autocomplete, Chart, components) {
var Flags = {};
@ -21,6 +22,10 @@ define('admin/manage/flags', [
handleDelete();
handleInfiniteScroll();
handleGraphs();
updateFlagDetails(ajaxify.data.posts);
components.get('posts/flags').on('click', '[component="posts/flag/update"]', updateFlag);
};
function handleDismiss() {
@ -89,10 +94,14 @@ define('admin/manage/flags', [
after: $('[data-next]').attr('data-next')
}, function(data, done) {
if (data.posts && data.posts.length) {
app.parseAndTranslate('admin/manage/flags', 'posts', {posts: data.posts}, function(html) {
app.parseAndTranslate('admin/manage/flags', 'posts', {
posts: data.posts,
assignees: ajaxify.data.assignees
}, function(html) {
$('[data-next]').attr('data-next', data.next);
$('.post-container').append(html);
html.find('img:not(.not-responsive)').addClass('img-responsive');
updateFlagDetails(data.posts);
done();
});
} else {
@ -150,5 +159,44 @@ define('admin/manage/flags', [
});
}
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,6 +2,7 @@
var async = require('async');
var posts = require('../../posts');
var user = require('../../user');
var analytics = require('../../analytics');
var flagsController = {};
@ -25,15 +26,32 @@ flagsController.get = function(req, res, next) {
},
analytics: function(next) {
analytics.getDailyStatsForSet('analytics:flags', Date.now(), 30, next);
}
},
assignees: async.apply(user.getAdminsandGlobalMods)
}, next);
}
], 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) {
var keep = ['uid', 'username'];
for(var prop in userObj) {
if (userObj.hasOwnProperty(prop)) {
if (keep.indexOf(prop) === -1) {
delete userObj[prop];
}
}
}
return userObj;
});
var data = {
posts: results.posts,
assignees: results.assignees,
analytics: results.analytics,
next: stop + 1,
byUsername: byUsername,

@ -3,6 +3,7 @@
'use strict';
var async = require('async');
var winston = require('winston');
var db = require('../database');
var user = require('../user');
var analytics = require('../analytics');
@ -171,7 +172,7 @@ module.exports = function(Posts) {
}, next);
},
posts: function(next) {
Posts.getPostSummaryByPids(pids, uid, {stripTags: false, extraFields: ['flags']}, next);
Posts.getPostSummaryByPids(pids, uid, {stripTags: false, extraFields: ['flags', 'flag:assignee', 'flag:state', 'flag:notes', 'flag:history']}, next);
}
}, next);
},
@ -190,6 +191,8 @@ module.exports = function(Posts) {
}
results.posts.forEach(function(post, index) {
var history;
if (post) {
post.flagReasons = reasons[index];
}
@ -197,6 +200,42 @@ module.exports = function(Posts) {
next(null, results.posts);
});
},
async.apply(Posts.expandFlagHistory),
function(posts, next) {
// Parse out flag data into its own object inside each post hash
posts = posts.map(function(postObj) {
for(var prop in postObj) {
postObj.flagData = postObj.flagData || {};
if (postObj.hasOwnProperty(prop) && prop.startsWith('flag:')) {
postObj.flagData[prop.slice(5)] = postObj[prop];
if (prop === 'flag:state') {
switch(postObj[prop]) {
case 'open':
postObj.flagData.labelClass = 'info';
break;
case 'wip':
postObj.flagData.labelClass = 'warning';
break;
case 'resolved':
postObj.flagData.labelClass = 'success';
break;
case 'rejected':
postObj.flagData.labelClass = 'danger';
break;
}
}
delete postObj[prop];
}
}
return postObj;
});
setImmediate(next.bind(null, null, posts));
}
], callback);
}
@ -226,4 +265,130 @@ module.exports = function(Posts) {
}
], callback);
};
Posts.updateFlagData = function(uid, pid, flagObj, callback) {
// Retrieve existing flag data to compare for history-saving purposes
var changes = [];
var changeset = {};
var prop;
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)) {
changes.push(prop);
}
// Generate changeset for object modification
if (flagObj.hasOwnProperty(prop)) {
changeset['flag:' + prop] = flagObj[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));
}
}
// 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()
});
}
});
changeset['flag:history'] = JSON.stringify(history);
} catch (e) {
winston.warn('[posts/updateFlagData] Unable to deserialise post flag history, likely malformed data');
}
}
// Save flag data into post hash
Posts.setPostFields(pid, changeset, callback);
});
};
Posts.expandFlagHistory = function(posts, callback) {
// Expand flag history
async.map(posts, function(post, next) {
try {
var history = JSON.parse(post['flag:history'] || '[]');
} catch (e) {
winston.warn('[posts/getFlags] Unable to deserialise post flag history, likely malformed data');
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);
}
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);
}
post['flag:history'] = history;
next(null, post);
});
}, callback);
}
};

@ -163,7 +163,35 @@ module.exports = function(SocketPosts) {
},
function (posts, next) {
next(null, {posts: posts, next: stop + 1});
},
}
], callback);
};
SocketPosts.updateFlag = function(socket, data, callback) {
if (!data || !(data.pid && data.data)) {
return callback('[[error:invalid-data]]');
}
var payload = {};
async.waterfall([
function (next) {
user.isAdminOrGlobalMod(socket.uid, next);
},
function (isAdminOrGlobalModerator, next) {
if (!isAdminOrGlobalModerator) {
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);
next(null, socket.uid, data.pid, payload);
},
async.apply(posts.updateFlagData)
], callback);
}
};

@ -2,6 +2,7 @@
var async = require('async');
var groups = require('./groups');
var plugins = require('./plugins');
var db = require('./database');
var topics = require('./topics');
@ -260,6 +261,19 @@ var utils = require('../public/src/utils');
});
};
User.getAdminsandGlobalMods = function(callback) {
async.parallel({
admins: async.apply(groups.getMembers, 'administrators', 0, -1),
mods: async.apply(groups.getMembers, 'Global Moderators', 0, -1)
}, function(err, results) {
if (err) {
return callback(err);
}
User.getUsersData(results.admins.concat(results.mods), callback);
});
};
User.addInterstitials = function(callback) {
plugins.registerHook('core', {
hook: 'filter:register.interstitial',

@ -44,7 +44,7 @@
<div data-next="{next}">
<div class="post-container" data-next="{next}">
<div component="posts/flags" class="panel-group post-container" id="accordion" role="tablist" aria-multiselectable="true" data-next="{next}">
<!-- IF !posts.length -->
<div class="alert alert-success">
No flagged posts!
@ -52,54 +52,117 @@
<!-- ENDIF !posts.length -->
<!-- BEGIN posts -->
<div class="row" data-pid="{posts.pid}" data-tid="{posts.topic.tid}">
<div class="col-sm-8">
<div class="panel panel-default">
<div class="panel-body flag-post-body">
<a href="{config.relative_path}/user/{../user.userslug}">
<!-- IF ../user.picture -->
<img title="{posts.user.username}" src="{../user.picture}">
<!-- ELSE -->
<div class="user-icon" style="background-color: {../user.icon:bgColor};">{../user.icon:text}</div>
<!-- ENDIF ../user.picture -->
</a>
<div class="panel panel-default" component="posts/flag" data-pid="{../pid}">
<div class="panel-heading" role="tab">
<h4 class="panel-title">
<a role="button" data-toggle="collapse" data-parent="#accordion" href="#flag-pid-{posts.pid}" aria-expanded="true" aria-controls="flag-pid-{posts.pid}">
<span class="label <!-- IF ../flagData.labelClass -->label-{../flagData.labelClass}<!-- ELSE -->label-info<!-- ENDIF ../flagData.labelClass -->">[[topic:flag_manage_state_<!-- IF ../flagData.state -->{../flagData.state}<!-- ELSE -->open<!-- ENDIF ../flagData.state -->]]</span>
&nbsp;[[topic:flag_manage_title, {posts.category.name}]]
<small><span class="timeago" title="{posts.timestampISO}"></span></small>
</a>
</h4>
</div>
<div id="flag-pid-{posts.pid}" class="panel-collapse collapse" role="tabpanel">
<div class="panel-body">
<div class="row" data-pid="{posts.pid}" data-tid="{posts.topic.tid}">
<div class="col-sm-8">
<div class="well flag-post-body">
<a href="{config.relative_path}/user/{../user.userslug}">
<!-- IF ../user.picture -->
<img title="{posts.user.username}" src="{../user.picture}">
<!-- ELSE -->
<div class="user-icon" style="background-color: {../user.icon:bgColor};">{../user.icon:text}</div>
<!-- ENDIF ../user.picture -->
</a>
<a href="{config.relative_path}/user/{../user.userslug}">
<strong><span>{../user.username}</span></strong>
</a>
<div class="content">
<p>{posts.content}</p>
<a href="{config.relative_path}/user/{../user.userslug}">
<strong><span>{../user.username}</span></strong>
</a>
<div class="content">
<p>{posts.content}</p>
</div>
<small>
<span class="pull-right">
Posted in <a href="{config.relative_path}/category/{posts.category.slug}" target="_blank"><i class="fa {posts.category.icon}"></i> {posts.category.name}</a>, <span class="timeago" title="{posts.timestampISO}"></span> &bull;
<a href="{config.relative_path}/topic/{posts.topic.slug}/{posts.index}" target="_blank">Read More</a>
</span>
</small>
</div>
</div>
<div class="col-sm-4">
<i class="fa fa-flag"></i> This post has been flagged {posts.flags} time(s):
<blockquote class="flag-reporters">
<ul>
<!-- BEGIN posts.flagReasons -->
<li>
<a target="_blank" href="{config.relative_path}/user/{../user.userslug}">
<!-- IF ../user.picture -->
<img src="{../user.picture}" />
<!-- ELSE -->
<div class="user-icon" style="background-color: {../user.icon:bgColor};">{../user.icon:text}</div>
<!-- ENDIF ../user.picture -->
{../user.username}
</a>: "{posts.flagReasons.reason}"
</li>
<!-- END posts.flagReasons -->
</ul>
</blockquote>
<div class="btn-group">
<button class="btn btn-sm btn-success dismiss">Dismiss this Flag</button>
<button class="btn btn-sm btn-danger delete">Delete the Post</button>
</div>
</div>
</div>
<hr />
<div class="row">
<div class="col-sm-6">
<form role="form">
<div class="form-group">
<label for="{posts.pid}-assignee">[[topic:flag_manage_assignee]]</label>
<select class="form-control" id="{posts.pid}-assignee" name="assignee">
<option value="">No Assignee</option>
<!-- BEGIN assignees -->
<option value="{assignees.uid}">{assignees.username}</option>
<!-- END assignees -->
</select>
</div>
<div class="form-group">
<label for="{posts.pid}-state">[[topic:flag_manage_state]]</label>
<select class="form-control" id="{posts.pid}-state" name="state">
<option value="open">[[topic:flag_manage_state_open]]</option>
<option value="wip">[[topic:flag_manage_state_wip]]</option>
<option value="resolved">[[topic:flag_manage_state_resolved]]</option>
<option value="rejected">[[topic:flag_manage_state_rejected]]</option>
</select>
</div>
<div class="form-group">
<label for="{posts.pid}-notes">[[topic:flag_manage_notes]]</label>
<textarea class="form-control" id="{posts.pid}-notes" name="notes"></textarea>
</div>
<button type="button" component="posts/flag/update" class="btn btn-sm btn-primary btn-block">[[topic:flag_manage_update]]</button>
</form>
</div>
<div class="col-sm-6">
<h5>[[topic:flag_manage_history]]</h5>
<!-- IF !posts.flagData.history.length -->
<div class="alert alert-info">[[topic:flag_manage_no_history]]</div>
<!-- ELSE -->
<ul class="list-group" component="posts/flag/history">
<!-- BEGIN posts.flagData.history -->
<li class="list-group-item">
<div class="pull-right"><small><span class="timeago" title="{posts.flagData.history.timestampISO}"></span></small></div>
<!-- IF ../user.picture -->
<img class="avatar avatar-sm avatar-rounded" src="{../user.picture}" title="{../user.username}" />
<!-- ELSE -->
<div class="avatar avatar-sm avatar-rounded" style="background-color: {../user.icon:bgColor};" title="{../user.username}">{../user.icon:text}</div>
<!-- ENDIF ../user.picture -->
[[topic:flag_manage_history_{posts.flagData.history.type}, {posts.flagData.history.label}]]
</li>
<!-- END posts.flagData.history -->
</ul>
<!-- ENDIF !posts.flagData.history.length -->
</div>
<small>
<span class="pull-right">
Posted in <a href="{config.relative_path}/category/{posts.category.slug}" target="_blank"><i class="fa {posts.category.icon}"></i> {posts.category.name}</a>, <span class="timeago" title="{posts.timestampISO}"></span> &bull;
<a href="{config.relative_path}/topic/{posts.topic.slug}/{posts.index}" target="_blank">Read More</a>
</span>
</small>
</div>
</div>
</div>
<div class="col-sm-4">
<i class="fa fa-flag"></i> This post has been flagged {posts.flags} time(s):
<blockquote class="flag-reporters">
<ul>
<!-- BEGIN posts.flagReasons -->
<li>
<a target="_blank" href="{config.relative_path}/user/{../user.userslug}">
<!-- IF ../user.picture -->
<img src="{../user.picture}" />
<!-- ELSE -->
<div class="user-icon" style="background-color: {../user.icon:bgColor};">{../user.icon:text}</div>
<!-- ENDIF ../user.picture -->
{../user.username}
</a>: "{../reason}"
</li>
<!-- END posts.flagReasons -->
</ul>
</blockquote>
<div class="btn-group">
<button class="btn btn-sm btn-success dismiss">Dismiss this Flag</button>
<button class="btn btn-sm btn-danger delete">Delete the Post</button>
</div>
</div>
</div>

Loading…
Cancel
Save