mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-11-01 19:46:01 +01:00
Merge branch 'flagging-refactor' into develop
This commit is contained in:
62
public/language/en-GB/flags.json
Normal file
62
public/language/en-GB/flags.json
Normal file
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"state": "State",
|
||||
"reporter": "Reporter",
|
||||
"reported-at": "Reported At",
|
||||
"description": "Description",
|
||||
"no-flags": "Hooray! No flags found.",
|
||||
"assignee": "Assignee",
|
||||
"update": "Update",
|
||||
"updated": "Updated",
|
||||
"target-purged": "The content this flag referred to has been purged and is no longer available.",
|
||||
|
||||
"quick-filters": "Quick Filters",
|
||||
"filter-active": "There are one or more filters active in this list of flags",
|
||||
"filter-reset": "Remove Filters",
|
||||
"filters": "Filter Options",
|
||||
"filter-reporterId": "Reporter UID",
|
||||
"filter-targetUid": "Flagged UID",
|
||||
"filter-type": "Flag Type",
|
||||
"filter-type-all": "All Content",
|
||||
"filter-type-post": "Post",
|
||||
"filter-state": "State",
|
||||
"filter-assignee": "Assignee UID",
|
||||
"filter-cid": "Category",
|
||||
"filter-quick-mine": "Assigned to me",
|
||||
"filter-cid-all": "All categories",
|
||||
"apply-filters": "Apply Filters",
|
||||
|
||||
"quick-links": "Quick Links",
|
||||
"flagged-user": "Flagged User",
|
||||
"reporter": "Reporting User",
|
||||
"view-profile": "View Profile",
|
||||
"start-new-chat": "Start New Chat",
|
||||
"go-to-target": "View Flag Target",
|
||||
|
||||
"user-view": "View Profile",
|
||||
"user-edit": "Edit Profile",
|
||||
|
||||
"notes": "Flag Notes",
|
||||
"add-note": "Add Note",
|
||||
"no-notes": "No shared notes.",
|
||||
|
||||
"history": "Flag History",
|
||||
"back": "Back to Flags List",
|
||||
"no-history": "No flag history.",
|
||||
|
||||
"state": "State",
|
||||
"state-all": "All states",
|
||||
"state-open": "New/Open",
|
||||
"state-wip": "Work in Progress",
|
||||
"state-resolved": "Resolved",
|
||||
"state-rejected": "Rejected",
|
||||
"no-assignee": "Not Assigned",
|
||||
"note-added": "Note Added",
|
||||
|
||||
"modal-title": "Report Inappropriate Content",
|
||||
"modal-body": "Please specify your reason for flagging %1 %2 for review. Alternatively, use one of the quick report buttons if applicable.",
|
||||
"modal-reason-spam": "Spam",
|
||||
"modal-reason-offensive": "Offensive",
|
||||
"modal-reason-custom": "Reason for reporting this content...",
|
||||
"modal-submit": "Submit Report",
|
||||
"modal-submit-success": "Content has been flagged for moderation."
|
||||
}
|
||||
@@ -21,6 +21,9 @@
|
||||
"user_flagged_post_in": "<strong>%1</strong> flagged a post in <strong>%2</strong>",
|
||||
"user_flagged_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> flagged a post in <strong>%3</strong>",
|
||||
"user_flagged_post_in_multiple": "<strong>%1</strong> and %2 others flagged a post in <strong>%3</strong>",
|
||||
"user_flagged_user": "<strong>%1</strong> flagged a user profile (%2)",
|
||||
"user_flagged_user_dual": "<strong>%1</strong> and <strong>%2</strong> flagged a user profile (%3)",
|
||||
"user_flagged_user_multiple": "<strong>%1</strong> and %2 others flagged a user profile (%3)",
|
||||
"user_posted_to" : "<strong>%1</strong> has posted a reply to: <strong>%2</strong>",
|
||||
"user_posted_to_dual" : "<strong>%1</strong> and <strong>%2</strong> have posted replies to: <strong>%3</strong>",
|
||||
"user_posted_to_multiple" : "<strong>%1</strong> and %2 others have posted replies to: <strong>%3</strong>",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"popular-month": "Popular topics this month",
|
||||
"popular-alltime": "All time popular topics",
|
||||
"recent": "Recent Topics",
|
||||
"flagged-posts": "Flagged Posts",
|
||||
"flagged-content": "Flagged Content",
|
||||
"ip-blacklist": "IP Blacklist",
|
||||
|
||||
"users/online": "Online Users",
|
||||
@@ -32,6 +32,9 @@
|
||||
"chats": "Chats",
|
||||
"chat": "Chatting with %1",
|
||||
|
||||
"flags": "Flags",
|
||||
"flag-details": "Flag %1 Details",
|
||||
|
||||
"account/edit": "Editing \"%1\"",
|
||||
"account/edit/password": "Editing password of \"%1\"",
|
||||
"account/edit/username": "Editing username of \"%1\"",
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
"link": "Link",
|
||||
"share": "Share",
|
||||
"tools": "Tools",
|
||||
"flag": "Flag",
|
||||
"locked": "Locked",
|
||||
"pinned": "Pinned",
|
||||
"moved": "Moved",
|
||||
@@ -36,22 +35,6 @@
|
||||
"bookmark_instructions" : "Click here to return to the last read post in this thread.",
|
||||
|
||||
"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.",
|
||||
|
||||
@@ -153,10 +136,5 @@
|
||||
"stale.create": "Create a new topic",
|
||||
"stale.reply_anyway": "Reply to this topic anyway",
|
||||
|
||||
"link_back": "Re: [%1](%2)\n\n",
|
||||
|
||||
"spam": "Spam",
|
||||
"offensive": "Offensive",
|
||||
"custom-flag-reason": "Enter a flagging reason"
|
||||
|
||||
"link_back": "Re: [%1](%2)\n\n"
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
"chat": "Chat",
|
||||
"chat_with": "Continue chat with %1",
|
||||
"new_chat_with": "Start new chat with %1",
|
||||
"flag-profile": "Flag Profile",
|
||||
"follow": "Follow",
|
||||
"unfollow": "Unfollow",
|
||||
"more": "More",
|
||||
|
||||
@@ -107,6 +107,12 @@
|
||||
}
|
||||
|
||||
&.avatar-lg {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
.user-icon-style(64px, 4rem);
|
||||
}
|
||||
|
||||
&.avatar-xl {
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
.user-icon-style(128px, 7.5rem);
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
@@ -128,15 +128,6 @@ define('admin/manage/users', ['translator'], function (translator) {
|
||||
socket.emit('admin.user.resetLockouts', uids, done('Lockout(s) reset!'));
|
||||
});
|
||||
|
||||
$('.reset-flags').on('click', function () {
|
||||
var uids = getSelectedUids();
|
||||
if (!uids.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
socket.emit('admin.user.resetFlags', uids, done('Flags(s) reset!'));
|
||||
});
|
||||
|
||||
$('.admin-user').on('click', function () {
|
||||
var uids = getSelectedUids();
|
||||
if (!uids.length) {
|
||||
|
||||
@@ -49,6 +49,7 @@ define('forum/account/header', [
|
||||
components.get('account/ban').on('click', banAccount);
|
||||
components.get('account/unban').on('click', unbanAccount);
|
||||
components.get('account/delete').on('click', deleteAccount);
|
||||
components.get('account/flag').on('click', flagAccount);
|
||||
};
|
||||
|
||||
function hidePrivateLinks() {
|
||||
@@ -167,6 +168,15 @@ define('forum/account/header', [
|
||||
});
|
||||
}
|
||||
|
||||
function flagAccount() {
|
||||
require(['flags'], function (flags) {
|
||||
flags.showFlagModal({
|
||||
type: 'user',
|
||||
id: ajaxify.data.uid
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function removeCover() {
|
||||
socket.emit('user.removeCover', {
|
||||
uid: ajaxify.data.uid
|
||||
|
||||
77
public/src/client/flags/detail.js
Normal file
77
public/src/client/flags/detail.js
Normal file
@@ -0,0 +1,77 @@
|
||||
'use strict';
|
||||
|
||||
/* globals define */
|
||||
|
||||
define('forum/flags/detail', ['forum/flags/list', 'components', 'translator'], function (FlagsList, components, translator) {
|
||||
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, history) {
|
||||
if (err) {
|
||||
return app.alertError(err.message);
|
||||
} else {
|
||||
app.alertSuccess('[[flags:updated]]');
|
||||
Flags.reloadHistory(history);
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case 'appendNote':
|
||||
socket.emit('flags.appendNote', {
|
||||
flagId: ajaxify.data.flagId,
|
||||
note: document.getElementById('note').value
|
||||
}, function (err, payload) {
|
||||
if (err) {
|
||||
return app.alertError(err.message);
|
||||
} else {
|
||||
app.alertSuccess('[[flags:note-added]]');
|
||||
Flags.reloadNotes(payload.notes);
|
||||
Flags.reloadHistory(payload.history);
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
FlagsList.enableFilterForm();
|
||||
};
|
||||
|
||||
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 = '';
|
||||
});
|
||||
};
|
||||
|
||||
Flags.reloadHistory = function (history) {
|
||||
templates.parse('flags/detail', 'history', {
|
||||
history: history
|
||||
}, function (html) {
|
||||
translator.translate(html, function (translated) {
|
||||
var wrapperEl = components.get('flag/history');
|
||||
wrapperEl.empty();
|
||||
wrapperEl.html(translated);
|
||||
wrapperEl.find('span.timeago').timeago();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return Flags;
|
||||
});
|
||||
86
public/src/client/flags/list.js
Normal file
86
public/src/client/flags/list.js
Normal file
@@ -0,0 +1,86 @@
|
||||
'use strict';
|
||||
|
||||
/* globals define */
|
||||
|
||||
define('forum/flags/list', ['components', 'Chart'], function (components, Chart) {
|
||||
var Flags = {};
|
||||
|
||||
Flags.init = function () {
|
||||
Flags.enableFilterForm();
|
||||
Flags.enableChatButtons();
|
||||
Flags.handleGraphs();
|
||||
};
|
||||
|
||||
Flags.enableFilterForm = function () {
|
||||
var filtersEl = components.get('flags/filters');
|
||||
|
||||
// Parse ajaxify data to set form values to reflect current filters
|
||||
for (var filter in ajaxify.data.filters) {
|
||||
filtersEl.find('[name="' + filter + '"]').val(ajaxify.data.filters[filter]);
|
||||
}
|
||||
|
||||
filtersEl.find('button').on('click', function () {
|
||||
var payload = filtersEl.serializeArray().filter(function (item) {
|
||||
return !!item.value;
|
||||
});
|
||||
ajaxify.go('flags?' + $.param(payload));
|
||||
});
|
||||
};
|
||||
|
||||
Flags.enableChatButtons = function () {
|
||||
$('[data-chat]').on('click', function () {
|
||||
app.newChat(this.getAttribute('data-chat'));
|
||||
});
|
||||
};
|
||||
|
||||
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,
|
||||
stepSize: 1
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return Flags;
|
||||
});
|
||||
@@ -167,10 +167,11 @@ define('forum/topic/postTools', ['share', 'navigator', 'components', 'translator
|
||||
|
||||
postContainer.on('click', '[component="post/flag"]', function () {
|
||||
var pid = getData($(this), 'data-pid');
|
||||
var username = getData($(this), 'data-username');
|
||||
var userslug = getData($(this), 'data-userslug');
|
||||
require(['forum/topic/flag'], function (flag) {
|
||||
flag.showFlagModal(pid, username, userslug);
|
||||
require(['flags'], function (flags) {
|
||||
flags.showFlagModal({
|
||||
type: 'post',
|
||||
id: pid
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -2,18 +2,13 @@
|
||||
|
||||
/* globals define, app, socket, templates */
|
||||
|
||||
define('forum/topic/flag', [], function () {
|
||||
|
||||
define('flags', [], function () {
|
||||
var Flag = {},
|
||||
flagModal,
|
||||
flagCommit;
|
||||
|
||||
Flag.showFlagModal = function (pid, username, userslug) {
|
||||
parseModal({
|
||||
pid: pid,
|
||||
username: username,
|
||||
userslug: userslug
|
||||
}, function (html) {
|
||||
Flag.showFlagModal = function (data) {
|
||||
parseModal(data, function (html) {
|
||||
flagModal = $(html);
|
||||
|
||||
flagModal.on('hidden.bs.modal', function () {
|
||||
@@ -23,11 +18,11 @@ define('forum/topic/flag', [], function () {
|
||||
flagCommit = flagModal.find('#flag-post-commit');
|
||||
|
||||
flagModal.on('click', '.flag-reason', function () {
|
||||
flagPost(pid, $(this).text());
|
||||
createFlag(data.type, data.id, $(this).text());
|
||||
});
|
||||
|
||||
flagCommit.on('click', function () {
|
||||
flagPost(pid, flagModal.find('#flag-reason-custom').val());
|
||||
createFlag(data.type, data.id, flagModal.find('#flag-reason-custom').val());
|
||||
});
|
||||
|
||||
flagModal.modal('show');
|
||||
@@ -37,24 +32,24 @@ define('forum/topic/flag', [], function () {
|
||||
};
|
||||
|
||||
function parseModal(tplData, callback) {
|
||||
templates.parse('partials/modals/flag_post_modal', tplData, function (html) {
|
||||
templates.parse('partials/modals/flag_modal', tplData, function (html) {
|
||||
require(['translator'], function (translator) {
|
||||
translator.translate(html, callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function flagPost(pid, reason) {
|
||||
if (!pid || !reason) {
|
||||
function createFlag(type, id, reason) {
|
||||
if (!type || !id || !reason) {
|
||||
return;
|
||||
}
|
||||
socket.emit('posts.flag', {pid: pid, reason: reason}, function (err) {
|
||||
socket.emit('flags.create', {type: type, id: id, reason: reason}, function (err) {
|
||||
if (err) {
|
||||
return app.alertError(err.message);
|
||||
}
|
||||
|
||||
flagModal.modal('hide');
|
||||
app.alertSuccess('[[topic:flag_success]]');
|
||||
app.alertSuccess('[[flags:modal-submit-success]]');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -16,17 +16,21 @@ var uniquevisitors = 0;
|
||||
|
||||
var isCategory = /^(?:\/api)?\/category\/(\d+)/;
|
||||
|
||||
new cronJob('*/10 * * * *', function () {
|
||||
new cronJob('*/10 * * * * *', function () {
|
||||
Analytics.writeData();
|
||||
}, null, true);
|
||||
|
||||
Analytics.increment = function (keys) {
|
||||
Analytics.increment = function (keys, callback) {
|
||||
keys = Array.isArray(keys) ? keys : [keys];
|
||||
|
||||
keys.forEach(function (key) {
|
||||
counters[key] = counters[key] || 0;
|
||||
++counters[key];
|
||||
});
|
||||
|
||||
if (typeof callback === 'function') {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
Analytics.pageView = function (payload) {
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
var async = require('async');
|
||||
var validator = require('validator');
|
||||
|
||||
var posts = require('../../posts');
|
||||
var user = require('../../user');
|
||||
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');
|
||||
}
|
||||
|
||||
posts.getFlags(sets, cid, req.uid, start, stop, next);
|
||||
}
|
||||
], callback);
|
||||
}
|
||||
|
||||
|
||||
module.exports = flagsController;
|
||||
@@ -3,24 +3,92 @@
|
||||
var async = require('async');
|
||||
|
||||
var user = require('../user');
|
||||
var adminFlagsController = require('./admin/flags');
|
||||
var categories = require('../categories');
|
||||
var flags = require('../flags');
|
||||
var analytics = require('../analytics');
|
||||
|
||||
var modsController = {};
|
||||
var modsController = {
|
||||
flags: {}
|
||||
};
|
||||
|
||||
modsController.flagged = function (req, res, next) {
|
||||
modsController.flags.list = function (req, res, next) {
|
||||
async.parallel({
|
||||
isAdminOrGlobalMod: async.apply(user.isAdminOrGlobalMod, req.uid),
|
||||
moderatedCids: async.apply(user.getModeratedCids, req.uid)
|
||||
}, function (err, results) {
|
||||
if (err || !(results.isAdminOrGlobalMod || !!results.moderatedCids.length)) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
} else if (!(results.isAdminOrGlobalMod || !!results.moderatedCids.length)) {
|
||||
return next(new Error('[[error:no-privileges]]'));
|
||||
}
|
||||
|
||||
if (!results.isAdminOrGlobalMod && results.moderatedCids.length) {
|
||||
res.locals.cids = results.moderatedCids;
|
||||
}
|
||||
|
||||
adminFlagsController.get(req, res, next);
|
||||
// Parse query string params for filters
|
||||
var valid = ['assignee', 'state', 'reporterId', 'type', 'targetUid', 'cid', 'quick'];
|
||||
var filters = valid.reduce(function (memo, cur) {
|
||||
if (req.query.hasOwnProperty(cur)) {
|
||||
memo[cur] = req.query[cur];
|
||||
}
|
||||
|
||||
return memo;
|
||||
}, {});
|
||||
|
||||
async.parallel({
|
||||
flags: async.apply(flags.list, filters, req.uid),
|
||||
analytics: async.apply(analytics.getDailyStatsForSet, 'analytics:flags', Date.now(), 30),
|
||||
categories: async.apply(categories.buildForSelect, req.uid)
|
||||
}, function (err, data) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
// Minimal returned set for templates.js
|
||||
data.categories = data.categories.reduce(function (memo, cur) {
|
||||
memo[cur.cid] = cur.name;
|
||||
return memo;
|
||||
}, {});
|
||||
|
||||
res.render('flags/list', {
|
||||
flags: data.flags,
|
||||
analytics: data.analytics,
|
||||
categories: data.categories,
|
||||
hasFilter: !!Object.keys(filters).length,
|
||||
filters: filters,
|
||||
title: '[[pages:flags]]'
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
modsController.flags.detail = function (req, res, next) {
|
||||
async.parallel({
|
||||
isAdminOrGlobalMod: async.apply(user.isAdminOrGlobalMod, req.uid),
|
||||
moderatedCids: async.apply(user.getModeratedCids, req.uid),
|
||||
flagData: async.apply(flags.get, req.params.flagId),
|
||||
assignees: async.apply(user.getAdminsandGlobalModsandModerators)
|
||||
}, function (err, results) {
|
||||
if (err || !results.flagData) {
|
||||
return next(err || new Error('[[error:invalid-data]]'));
|
||||
} else if (!(results.isAdminOrGlobalMod || !!results.moderatedCids.length)) {
|
||||
return next(new Error('[[error:no-privileges]]'));
|
||||
}
|
||||
|
||||
res.render('flags/detail', Object.assign(results.flagData, {
|
||||
assignees: results.assignees,
|
||||
type_bool: ['post', 'user', 'empty'].reduce(function (memo, cur) {
|
||||
if (cur !== 'empty') {
|
||||
memo[cur] = results.flagData.type === cur && !!Object.keys(results.flagData.target).length;
|
||||
} else {
|
||||
memo[cur] = !Object.keys(results.flagData.target).length;
|
||||
}
|
||||
|
||||
return memo;
|
||||
}, {}),
|
||||
title: '[[pages:flag-details, ' + req.params.flagId + ']]'
|
||||
}));
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
620
src/flags.js
Normal file
620
src/flags.js
Normal file
@@ -0,0 +1,620 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
var winston = require('winston');
|
||||
var db = require('./database');
|
||||
var user = require('./user');
|
||||
var groups = require('./groups');
|
||||
var meta = require('./meta');
|
||||
var notifications = require('./notifications');
|
||||
var analytics = require('./analytics');
|
||||
var topics = require('./topics');
|
||||
var posts = require('./posts');
|
||||
var privileges = require('./privileges');
|
||||
var plugins = require('./plugins');
|
||||
var utils = require('../public/src/utils');
|
||||
var _ = require('underscore');
|
||||
var S = require('string');
|
||||
|
||||
var Flags = {};
|
||||
|
||||
Flags.get = function (flagId, callback) {
|
||||
async.waterfall([
|
||||
// First stage
|
||||
async.apply(async.parallel, {
|
||||
base: async.apply(db.getObject.bind(db), 'flag:' + flagId),
|
||||
history: async.apply(Flags.getHistory, flagId),
|
||||
notes: async.apply(Flags.getNotes, flagId)
|
||||
}),
|
||||
function (data, next) {
|
||||
// Second stage
|
||||
async.parallel({
|
||||
userObj: async.apply(user.getUserFields, data.base.uid, ['username', 'userslug', 'picture']),
|
||||
targetObj: async.apply(Flags.getTarget, data.base.type, data.base.targetId, data.base.uid)
|
||||
}, function (err, payload) {
|
||||
// Final object return construction
|
||||
next(err, Object.assign(data.base, {
|
||||
datetimeISO: new Date(parseInt(data.base.datetime, 10)).toISOString(),
|
||||
target_readable: data.base.type.charAt(0).toUpperCase() + data.base.type.slice(1) + ' ' + data.base.targetId,
|
||||
target: payload.targetObj,
|
||||
history: data.history,
|
||||
notes: data.notes,
|
||||
reporter: payload.userObj
|
||||
}));
|
||||
});
|
||||
}
|
||||
], callback);
|
||||
};
|
||||
|
||||
Flags.list = function (filters, uid, callback) {
|
||||
if (typeof filters === 'function' && !uid && !callback) {
|
||||
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 'state':
|
||||
sets.push('flags:byState:' + filters[type]);
|
||||
break;
|
||||
|
||||
case 'reporterId':
|
||||
sets.push('flags:byReporter:' + filters[type]);
|
||||
break;
|
||||
|
||||
case 'assignee':
|
||||
sets.push('flags:byAssignee:' + filters[type]);
|
||||
break;
|
||||
|
||||
case 'targetUid':
|
||||
sets.push('flags:byTargetUid:' + filters[type]);
|
||||
break;
|
||||
|
||||
case 'cid':
|
||||
sets.push('flags:byCid:' + filters[type]);
|
||||
break;
|
||||
|
||||
case 'quick':
|
||||
switch (filters.quick) {
|
||||
case 'mine':
|
||||
sets.push('flags:byAssignee:' + uid);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
sets = sets.length ? sets : ['flags:datetime']; // No filter default
|
||||
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
if (sets.length === 1) {
|
||||
db.getSortedSetRevRange(sets[0], 0, -1, next);
|
||||
} else {
|
||||
db.getSortedSetRevIntersect({sets: sets, start: 0, stop: -1, aggregate: 'MAX'}, next);
|
||||
}
|
||||
},
|
||||
function (flagIds, next) {
|
||||
async.map(flagIds, function (flagId, next) {
|
||||
async.waterfall([
|
||||
async.apply(db.getObject, 'flag:' + flagId),
|
||||
function (flagObj, next) {
|
||||
user.getUserFields(flagObj.uid, ['username', 'picture'], function (err, userObj) {
|
||||
next(err, Object.assign(flagObj, {
|
||||
reporter: {
|
||||
username: userObj.username,
|
||||
picture: userObj.picture,
|
||||
'icon:bgColor': userObj['icon:bgColor'],
|
||||
'icon:text': userObj['icon:text']
|
||||
}
|
||||
}));
|
||||
});
|
||||
}
|
||||
], function (err, flagObj) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
switch(flagObj.state) {
|
||||
case 'open':
|
||||
flagObj.labelClass = 'info';
|
||||
break;
|
||||
case 'wip':
|
||||
flagObj.labelClass = 'warning';
|
||||
break;
|
||||
case 'resolved':
|
||||
flagObj.labelClass = 'success';
|
||||
break;
|
||||
case 'rejected':
|
||||
flagObj.labelClass = 'danger';
|
||||
break;
|
||||
}
|
||||
|
||||
next(null, Object.assign(flagObj, {
|
||||
target_readable: flagObj.type.charAt(0).toUpperCase() + flagObj.type.slice(1) + ' ' + flagObj.targetId,
|
||||
datetimeISO: new Date(parseInt(flagObj.datetime, 10)).toISOString()
|
||||
}));
|
||||
});
|
||||
}, next);
|
||||
}
|
||||
], callback);
|
||||
};
|
||||
|
||||
Flags.validate = function (payload, callback) {
|
||||
async.parallel({
|
||||
targetExists: async.apply(Flags.targetExists, payload.type, payload.id),
|
||||
target: async.apply(Flags.getTarget, payload.type, payload.id, payload.uid),
|
||||
reporter: async.apply(user.getUserData, payload.uid)
|
||||
}, function (err, data) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (data.target.deleted) {
|
||||
return callback(new Error('[[error:post-deleted]]'));
|
||||
} else if (parseInt(data.reporter.banned, 10)) {
|
||||
return callback(new Error('[[error:user-banned]]'));
|
||||
}
|
||||
|
||||
switch (payload.type) {
|
||||
case 'post':
|
||||
privileges.posts.canEdit(payload.id, payload.uid, function (err, editable) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
var minimumReputation = utils.isNumber(meta.config['privileges:flag']) ? parseInt(meta.config['privileges:flag'], 10) : 1;
|
||||
// Check if reporter meets rep threshold (or can edit the target post, in which case threshold does not apply)
|
||||
if (!editable.flag && parseInt(data.reporter.reputation, 10) < minimumReputation) {
|
||||
return callback(new Error('[[error:not-enough-reputation-to-flag]]'));
|
||||
}
|
||||
|
||||
callback();
|
||||
});
|
||||
break;
|
||||
|
||||
case 'user':
|
||||
privileges.users.canEdit(payload.uid, payload.id, function (err, editable) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
var minimumReputation = utils.isNumber(meta.config['privileges:flag']) ? parseInt(meta.config['privileges:flag'], 10) : 1;
|
||||
// Check if reporter meets rep threshold (or can edit the target user, in which case threshold does not apply)
|
||||
if (!editable && parseInt(data.reporter.reputation, 10) < minimumReputation) {
|
||||
return callback(new Error('[[error:not-enough-reputation-to-flag]]'));
|
||||
}
|
||||
|
||||
callback();
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
callback(new Error('[[error:invalid-data]]'));
|
||||
break;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Flags.getNotes = function (flagId, callback) {
|
||||
async.waterfall([
|
||||
async.apply(db.getSortedSetRevRangeWithScores.bind(db), 'flag:' + flagId + ':notes', 0, -1),
|
||||
function (notes, next) {
|
||||
var uids = [];
|
||||
var noteObj;
|
||||
notes = notes.map(function (note) {
|
||||
try {
|
||||
noteObj = JSON.parse(note.value);
|
||||
uids.push(noteObj[0]);
|
||||
return {
|
||||
uid: noteObj[0],
|
||||
content: noteObj[1],
|
||||
datetime: note.score,
|
||||
datetimeISO: new Date(parseInt(note.score, 10)).toISOString()
|
||||
};
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
});
|
||||
next(null, notes, uids);
|
||||
},
|
||||
function (notes, uids, next) {
|
||||
user.getUsersFields(uids, ['username', 'userslug', 'picture'], function (err, users) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
next(null, notes.map(function (note, idx) {
|
||||
note.user = users[idx];
|
||||
return note;
|
||||
}));
|
||||
});
|
||||
}
|
||||
], callback);
|
||||
};
|
||||
|
||||
Flags.create = function (type, id, uid, reason, timestamp, callback) {
|
||||
var targetUid;
|
||||
var targetCid;
|
||||
var doHistoryAppend = false;
|
||||
|
||||
// timestamp is optional
|
||||
if (typeof timestamp === 'function' && !callback) {
|
||||
callback = timestamp;
|
||||
timestamp = Date.now();
|
||||
doHistoryAppend = true;
|
||||
}
|
||||
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
async.parallel([
|
||||
// Sanity checks
|
||||
async.apply(Flags.exists, type, id, uid),
|
||||
async.apply(Flags.targetExists, type, id),
|
||||
|
||||
// Extra data for zset insertion
|
||||
async.apply(Flags.getTargetUid, type, id),
|
||||
async.apply(Flags.getTargetCid, type, id)
|
||||
], function (err, checks) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
targetUid = checks[2] || null;
|
||||
targetCid = checks[3] || null;
|
||||
|
||||
if (checks[0]) {
|
||||
return next(new Error('[[error:already-flagged]]'));
|
||||
} else if (!checks[1]) {
|
||||
return next(new Error('[[error:invalid-data]]'));
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
},
|
||||
async.apply(db.incrObjectField, 'global', 'nextFlagId'),
|
||||
function (flagId, next) {
|
||||
var tasks = [
|
||||
async.apply(db.setObject.bind(db), 'flag:' + flagId, {
|
||||
flagId: flagId,
|
||||
type: type,
|
||||
targetId: id,
|
||||
description: reason,
|
||||
uid: uid,
|
||||
datetime: timestamp
|
||||
}),
|
||||
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.sortedSetAdd.bind(db), 'flags:hash', flagId, [type, id, uid].join(':')), // save zset for duplicate checking
|
||||
async.apply(analytics.increment, 'flags') // some fancy analytics
|
||||
];
|
||||
|
||||
if (targetUid) {
|
||||
tasks.push(async.apply(db.sortedSetAdd.bind(db), 'flags:byTargetUid:' + targetUid, timestamp, flagId)); // by target uid
|
||||
}
|
||||
if (targetCid) {
|
||||
tasks.push(async.apply(db.sortedSetAdd.bind(db), 'flags:byCid:' + targetCid, timestamp, flagId)); // by target uid
|
||||
}
|
||||
|
||||
async.parallel(tasks, function (err, data) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (doHistoryAppend) {
|
||||
Flags.update(flagId, uid, { "state": "open" });
|
||||
}
|
||||
|
||||
next(null, flagId);
|
||||
});
|
||||
},
|
||||
async.apply(Flags.get)
|
||||
], callback);
|
||||
};
|
||||
|
||||
Flags.exists = function (type, id, uid, callback) {
|
||||
db.isSortedSetMember('flags:hash', [type, id, uid].join(':'), callback);
|
||||
};
|
||||
|
||||
Flags.getTarget = function (type, id, uid, callback) {
|
||||
async.waterfall([
|
||||
async.apply(Flags.targetExists, type, id),
|
||||
function (exists, next) {
|
||||
if (exists) {
|
||||
switch (type) {
|
||||
case 'post':
|
||||
async.waterfall([
|
||||
async.apply(posts.getPostsByPids, [id], uid),
|
||||
function (posts, next) {
|
||||
topics.addPostData(posts, uid, next);
|
||||
}
|
||||
], function (err, posts) {
|
||||
next(err, posts[0]);
|
||||
});
|
||||
break;
|
||||
|
||||
case 'user':
|
||||
user.getUsersData([id], function (err, users) {
|
||||
next(err, users ? users[0] : undefined);
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
next(new Error('[[error:invalid-data]]'));
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// Target used to exist (otherwise flag creation'd fail), but no longer
|
||||
next(null, {});
|
||||
}
|
||||
}
|
||||
], callback);
|
||||
};
|
||||
|
||||
Flags.targetExists = function (type, id, callback) {
|
||||
switch (type) {
|
||||
case 'post':
|
||||
posts.exists(id, callback);
|
||||
break;
|
||||
|
||||
case 'user':
|
||||
user.exists(id, callback);
|
||||
break;
|
||||
|
||||
default:
|
||||
callback(new Error('[[error:invalid-data]]'));
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
Flags.getTargetUid = function (type, id, callback) {
|
||||
switch (type) {
|
||||
case 'post':
|
||||
posts.getPostField(id, 'uid', callback);
|
||||
break;
|
||||
|
||||
default:
|
||||
setImmediate(callback, null, id);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
Flags.getTargetCid = function (type, id, callback) {
|
||||
switch (type) {
|
||||
case 'post':
|
||||
posts.getCidByPid(id, callback);
|
||||
break;
|
||||
|
||||
default:
|
||||
setImmediate(callback, null, id);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
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 = changeset.datetime || 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];
|
||||
} else {
|
||||
// Add tasks as necessary
|
||||
switch (prop) {
|
||||
case 'state':
|
||||
tasks.push(async.apply(db.sortedSetAdd.bind(db), 'flags:byState:' + changeset[prop], now, flagId));
|
||||
tasks.push(async.apply(db.sortedSetRemove.bind(db), 'flags:byState:' + current[prop], flagId));
|
||||
break;
|
||||
|
||||
case 'assignee':
|
||||
tasks.push(async.apply(db.sortedSetAdd.bind(db), 'flags:byAssignee:' + changeset[prop], now, flagId));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!Object.keys(changeset).length) {
|
||||
// No changes
|
||||
return next();
|
||||
}
|
||||
|
||||
// Save new object to db (upsert)
|
||||
tasks.push(async.apply(db.setObject, 'flag:' + flagId, changeset));
|
||||
// Append history
|
||||
tasks.push(async.apply(Flags.appendHistory, flagId, uid, changeset));
|
||||
|
||||
async.parallel(tasks, function (err, data) {
|
||||
return next(err);
|
||||
});
|
||||
}
|
||||
], callback);
|
||||
};
|
||||
|
||||
Flags.getHistory = function (flagId, callback) {
|
||||
var history;
|
||||
var uids = [];
|
||||
async.waterfall([
|
||||
async.apply(db.getSortedSetRevRangeWithScores.bind(db), 'flag:' + flagId + ':history', 0, -1),
|
||||
function (_history, next) {
|
||||
history = _history.map(function (entry) {
|
||||
try {
|
||||
entry.value = JSON.parse(entry.value);
|
||||
} catch (e) {
|
||||
return callback(e);
|
||||
}
|
||||
|
||||
uids.push(entry.value[0]);
|
||||
|
||||
// Deserialise changeset
|
||||
var changeset = entry.value[1];
|
||||
if (changeset.hasOwnProperty('state')) {
|
||||
changeset.state = changeset.state === undefined ? '' : '[[flags:state-' + changeset.state + ']]';
|
||||
}
|
||||
|
||||
return {
|
||||
uid: entry.value[0],
|
||||
fields: changeset,
|
||||
datetime: entry.score,
|
||||
datetimeISO: new Date(parseInt(entry.score, 10)).toISOString()
|
||||
};
|
||||
});
|
||||
|
||||
user.getUsersFields(uids, ['username', 'userslug', 'picture'], next);
|
||||
}
|
||||
], function (err, users) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
// Append user data to each history event
|
||||
history = history.map(function (event, idx) {
|
||||
event.user = users[idx];
|
||||
return event;
|
||||
});
|
||||
|
||||
callback(null, history);
|
||||
});
|
||||
};
|
||||
|
||||
Flags.appendHistory = function (flagId, uid, changeset, callback) {
|
||||
var payload;
|
||||
var datetime = changeset.datetime || Date.now();
|
||||
delete changeset.datetime;
|
||||
|
||||
try {
|
||||
payload = JSON.stringify([uid, changeset, datetime]);
|
||||
} catch (e) {
|
||||
return callback(e);
|
||||
}
|
||||
|
||||
db.sortedSetAdd('flag:' + flagId + ':history', datetime, payload, 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]);
|
||||
} catch (e) {
|
||||
return callback(e);
|
||||
}
|
||||
|
||||
async.waterfall([
|
||||
async.apply(db.sortedSetAdd, 'flag:' + flagId + ':notes', datetime, payload),
|
||||
async.apply(Flags.appendHistory, flagId, uid, {
|
||||
notes: null,
|
||||
datetime: datetime
|
||||
})
|
||||
], callback);
|
||||
};
|
||||
|
||||
Flags.notify = function (flagObj, uid, callback) {
|
||||
// Notify administrators, mods, and other associated people
|
||||
if (!callback) {
|
||||
callback = function () {};
|
||||
}
|
||||
|
||||
switch (flagObj.type) {
|
||||
case 'post':
|
||||
async.parallel({
|
||||
post: function (next) {
|
||||
async.waterfall([
|
||||
async.apply(posts.getPostData, flagObj.targetId),
|
||||
async.apply(posts.parsePost)
|
||||
], next);
|
||||
},
|
||||
title: async.apply(topics.getTitleByPid, flagObj.targetId),
|
||||
admins: async.apply(groups.getMembers, 'administrators', 0, -1),
|
||||
globalMods: async.apply(groups.getMembers, 'Global Moderators', 0, -1),
|
||||
moderators: function (next) {
|
||||
async.waterfall([
|
||||
async.apply(posts.getCidByPid, flagObj.targetId),
|
||||
function (cid, next) {
|
||||
groups.getMembers('cid:' + cid + ':privileges:mods', 0, -1, next);
|
||||
}
|
||||
], next);
|
||||
}
|
||||
}, function (err, results) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
var title = S(results.title).decodeHTMLEntities().s;
|
||||
var titleEscaped = title.replace(/%/g, '%').replace(/,/g, ',');
|
||||
|
||||
notifications.create({
|
||||
bodyShort: '[[notifications:user_flagged_post_in, ' + flagObj.reporter.username + ', ' + titleEscaped + ']]',
|
||||
bodyLong: flagObj.description,
|
||||
pid: flagObj.targetId,
|
||||
path: '/post/' + flagObj.targetId,
|
||||
nid: 'flag:post:' + flagObj.targetId + ':uid:' + uid,
|
||||
from: uid,
|
||||
mergeId: 'notifications:user_flagged_post_in|' + flagObj.targetId,
|
||||
topicTitle: results.title
|
||||
}, function (err, notification) {
|
||||
if (err || !notification) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
plugins.fireHook('action:flag.create', {
|
||||
flag: flagObj
|
||||
});
|
||||
notifications.push(notification, results.admins.concat(results.moderators).concat(results.globalMods), callback);
|
||||
});
|
||||
});
|
||||
break;
|
||||
|
||||
case 'user':
|
||||
async.parallel({
|
||||
admins: async.apply(groups.getMembers, 'administrators', 0, -1),
|
||||
globalMods: async.apply(groups.getMembers, 'Global Moderators', 0, -1),
|
||||
}, function (err, results) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
notifications.create({
|
||||
bodyShort: '[[notifications:user_flagged_user, ' + flagObj.reporter.username + ', ' + flagObj.target.username + ']]',
|
||||
bodyLong: flagObj.description,
|
||||
path: '/uid/' + flagObj.targetId,
|
||||
nid: 'flag:user:' + flagObj.targetId + ':uid:' + uid,
|
||||
from: uid,
|
||||
mergeId: 'notifications:user_flagged_user|' + flagObj.targetId
|
||||
}, function (err, notification) {
|
||||
if (err || !notification) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
plugins.fireHook('action:flag.create', {
|
||||
flag: flagObj
|
||||
});
|
||||
notifications.push(notification, results.admins.concat(results.globalMods), callback);
|
||||
});
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
callback(new Error('[[error:invalid-data]]'));
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = Flags;
|
||||
@@ -49,7 +49,6 @@ module.exports = function (Meta) {
|
||||
'public/src/client/unread.js',
|
||||
'public/src/client/topic.js',
|
||||
'public/src/client/topic/events.js',
|
||||
'public/src/client/topic/flag.js',
|
||||
'public/src/client/topic/fork.js',
|
||||
'public/src/client/topic/move.js',
|
||||
'public/src/client/topic/posts.js',
|
||||
@@ -72,7 +71,8 @@ module.exports = function (Meta) {
|
||||
'public/src/modules/taskbar.js',
|
||||
'public/src/modules/helpers.js',
|
||||
'public/src/modules/sounds.js',
|
||||
'public/src/modules/string.js'
|
||||
'public/src/modules/string.js',
|
||||
'public/src/modules/flags.js'
|
||||
],
|
||||
|
||||
// modules listed below are routed through express (/src/modules) so they can be defined anonymously
|
||||
|
||||
@@ -415,6 +415,7 @@ var utils = require('../public/src/utils');
|
||||
'notifications:user_started_following_you',
|
||||
'notifications:user_posted_to',
|
||||
'notifications:user_flagged_post_in',
|
||||
'notifications:user_flagged_user',
|
||||
'new_register'
|
||||
],
|
||||
isolated, differentiators, differentiator, modifyIndex, set;
|
||||
@@ -462,6 +463,7 @@ var utils = require('../public/src/utils');
|
||||
case 'notifications:user_started_following_you':
|
||||
case 'notifications:user_posted_to':
|
||||
case 'notifications:user_flagged_post_in':
|
||||
case 'notifications:user_flagged_user':
|
||||
var usernames = set.map(function (notifObj) {
|
||||
return notifObj && notifObj.user && notifObj.user.username;
|
||||
}).filter(function (username, idx, array) {
|
||||
|
||||
@@ -7,7 +7,8 @@ module.exports = function (Plugins) {
|
||||
Plugins.deprecatedHooks = {
|
||||
'filter:user.custom_fields': null, // remove in v1.1.0
|
||||
'filter:post.save': 'filter:post.create',
|
||||
'filter:user.profileLinks': 'filter:user.profileMenu'
|
||||
'filter:user.profileLinks': 'filter:user.profileMenu',
|
||||
'action:post.flag': 'action:flag.create'
|
||||
};
|
||||
/*
|
||||
`data` is an object consisting of (* is required):
|
||||
|
||||
@@ -21,7 +21,6 @@ var plugins = require('./plugins');
|
||||
require('./posts/category')(Posts);
|
||||
require('./posts/summary')(Posts);
|
||||
require('./posts/recent')(Posts);
|
||||
require('./posts/flags')(Posts);
|
||||
require('./posts/tools')(Posts);
|
||||
require('./posts/votes')(Posts);
|
||||
require('./posts/bookmarks')(Posts);
|
||||
|
||||
@@ -8,6 +8,7 @@ var topics = require('../topics');
|
||||
var user = require('../user');
|
||||
var notifications = require('../notifications');
|
||||
var plugins = require('../plugins');
|
||||
var flags = require('../flags');
|
||||
|
||||
module.exports = function (Posts) {
|
||||
|
||||
@@ -143,9 +144,6 @@ module.exports = function (Posts) {
|
||||
},
|
||||
function (next) {
|
||||
db.sortedSetsRemove(['posts:pid', 'posts:flagged'], pid, next);
|
||||
},
|
||||
function (next) {
|
||||
Posts.dismissFlag(pid, next);
|
||||
}
|
||||
], function (err) {
|
||||
if (err) {
|
||||
|
||||
@@ -1,417 +0,0 @@
|
||||
|
||||
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
var winston = require('winston');
|
||||
var db = require('../database');
|
||||
var user = require('../user');
|
||||
var analytics = require('../analytics');
|
||||
|
||||
module.exports = function (Posts) {
|
||||
|
||||
Posts.flag = function (post, uid, reason, callback) {
|
||||
if (!parseInt(uid, 10) || !reason) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
async.parallel({
|
||||
hasFlagged: async.apply(Posts.isFlaggedByUser, post.pid, uid),
|
||||
exists: async.apply(Posts.exists, post.pid)
|
||||
}, next);
|
||||
},
|
||||
function (results, next) {
|
||||
if (!results.exists) {
|
||||
return next(new Error('[[error:no-post]]'));
|
||||
}
|
||||
|
||||
if (results.hasFlagged) {
|
||||
return next(new Error('[[error:already-flagged]]'));
|
||||
}
|
||||
|
||||
var now = Date.now();
|
||||
async.parallel([
|
||||
function (next) {
|
||||
db.sortedSetAdd('posts:flagged', now, post.pid, next);
|
||||
},
|
||||
function (next) {
|
||||
db.sortedSetIncrBy('posts:flags:count', 1, post.pid, next);
|
||||
},
|
||||
function (next) {
|
||||
db.incrObjectField('post:' + post.pid, 'flags', next);
|
||||
},
|
||||
function (next) {
|
||||
db.sortedSetAdd('pid:' + post.pid + ':flag:uids', now, uid, next);
|
||||
},
|
||||
function (next) {
|
||||
db.sortedSetAdd('pid:' + post.pid + ':flag:uid:reason', 0, uid + ':' + reason, next);
|
||||
},
|
||||
function (next) {
|
||||
if (parseInt(post.uid, 10)) {
|
||||
async.parallel([
|
||||
async.apply(db.sortedSetIncrBy, 'users:flags', 1, post.uid),
|
||||
async.apply(db.incrObjectField, 'user:' + post.uid, 'flags'),
|
||||
async.apply(db.sortedSetAdd, 'uid:' + post.uid + ':flag:pids', now, post.pid)
|
||||
], next);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
], next);
|
||||
},
|
||||
function (data, next) {
|
||||
openNewFlag(post.pid, uid, next);
|
||||
}
|
||||
], function (err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
analytics.increment('flags');
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
function openNewFlag(pid, uid, callback) {
|
||||
db.sortedSetScore('posts:flags:count', pid, function (err, count) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
if (count === 1) { // Only update state on new flag
|
||||
Posts.updateFlagData(uid, pid, {
|
||||
state: 'open'
|
||||
}, callback);
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Posts.isFlaggedByUser = function (pid, uid, callback) {
|
||||
db.isSortedSetMember('pid:' + pid + ':flag:uids', uid, callback);
|
||||
};
|
||||
|
||||
Posts.dismissFlag = function (pid, callback) {
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
db.getObjectFields('post:' + pid, ['pid', 'uid', 'flags'], next);
|
||||
},
|
||||
function (postData, next) {
|
||||
if (!postData.pid) {
|
||||
return callback();
|
||||
}
|
||||
async.parallel([
|
||||
function (next) {
|
||||
if (parseInt(postData.uid, 10)) {
|
||||
if (parseInt(postData.flags, 10) > 0) {
|
||||
async.parallel([
|
||||
async.apply(db.sortedSetIncrBy, 'users:flags', -postData.flags, postData.uid),
|
||||
async.apply(db.incrObjectFieldBy, 'user:' + postData.uid, 'flags', -postData.flags)
|
||||
], next);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
},
|
||||
function (next) {
|
||||
db.sortedSetsRemove([
|
||||
'posts:flagged',
|
||||
'posts:flags:count',
|
||||
'uid:' + postData.uid + ':flag:pids'
|
||||
], pid, next);
|
||||
},
|
||||
function (next) {
|
||||
async.series([
|
||||
function (next) {
|
||||
db.getSortedSetRange('pid:' + pid + ':flag:uids', 0, -1, function (err, uids) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
async.each(uids, function (uid, next) {
|
||||
var nid = 'post_flag:' + pid + ':uid:' + uid;
|
||||
async.parallel([
|
||||
async.apply(db.delete, 'notifications:' + nid),
|
||||
async.apply(db.sortedSetRemove, 'notifications', 'post_flag:' + pid + ':uid:' + uid)
|
||||
], next);
|
||||
}, next);
|
||||
});
|
||||
},
|
||||
async.apply(db.delete, 'pid:' + pid + ':flag:uids')
|
||||
], next);
|
||||
},
|
||||
async.apply(db.deleteObjectField, 'post:' + pid, 'flags'),
|
||||
async.apply(db.delete, 'pid:' + pid + ':flag:uid:reason'),
|
||||
async.apply(db.deleteObjectFields, 'post:' + pid, ['flag:state', 'flag:assignee', 'flag:notes', 'flag:history'])
|
||||
], next);
|
||||
},
|
||||
function (results, next) {
|
||||
db.sortedSetsRemoveRangeByScore(['users:flags'], '-inf', 0, next);
|
||||
}
|
||||
], callback);
|
||||
};
|
||||
|
||||
Posts.dismissAllFlags = function (callback) {
|
||||
db.getSortedSetRange('posts:flagged', 0, -1, function (err, pids) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
async.eachSeries(pids, Posts.dismissFlag, callback);
|
||||
});
|
||||
};
|
||||
|
||||
Posts.dismissUserFlags = function (uid, callback) {
|
||||
db.getSortedSetRange('uid:' + uid + ':flag:pids', 0, -1, function (err, pids) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
async.eachSeries(pids, Posts.dismissFlag, callback);
|
||||
});
|
||||
};
|
||||
|
||||
Posts.getFlags = function (set, cid, uid, start, stop, callback) {
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
if (Array.isArray(set)) {
|
||||
db.getSortedSetRevIntersect({sets: set, start: start, stop: -1, aggregate: 'MAX'}, next);
|
||||
} else {
|
||||
db.getSortedSetRevRange(set, start, -1, next);
|
||||
}
|
||||
},
|
||||
function (pids, next) {
|
||||
if (cid) {
|
||||
Posts.filterPidsByCid(pids, cid, next);
|
||||
} else {
|
||||
process.nextTick(next, null, pids);
|
||||
}
|
||||
},
|
||||
function (pids, next) {
|
||||
getFlaggedPostsWithReasons(pids, uid, next);
|
||||
},
|
||||
function (posts, next) {
|
||||
var count = posts.length;
|
||||
var end = stop - start + 1;
|
||||
next(null, {posts: posts.slice(0, stop === -1 ? undefined : end), count: count});
|
||||
}
|
||||
], callback);
|
||||
};
|
||||
|
||||
function getFlaggedPostsWithReasons(pids, uid, callback) {
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
async.parallel({
|
||||
uidsReasons: function (next) {
|
||||
async.map(pids, function (pid, next) {
|
||||
db.getSortedSetRange('pid:' + pid + ':flag:uid:reason', 0, -1, next);
|
||||
}, next);
|
||||
},
|
||||
posts: function (next) {
|
||||
Posts.getPostSummaryByPids(pids, uid, {stripTags: false, extraFields: ['flags', 'flag:assignee', 'flag:state', 'flag:notes', 'flag:history']}, next);
|
||||
}
|
||||
}, next);
|
||||
},
|
||||
function (results, next) {
|
||||
async.map(results.uidsReasons, function (uidReasons, next) {
|
||||
async.map(uidReasons, function (uidReason, next) {
|
||||
var uid = uidReason.split(':')[0];
|
||||
var reason = uidReason.substr(uidReason.indexOf(':') + 1);
|
||||
user.getUserFields(uid, ['username', 'userslug', 'picture'], function (err, userData) {
|
||||
next(err, {user: userData, reason: reason});
|
||||
});
|
||||
}, next);
|
||||
}, function (err, reasons) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
results.posts.forEach(function (post, index) {
|
||||
if (post) {
|
||||
post.flagReasons = reasons[index];
|
||||
}
|
||||
});
|
||||
|
||||
next(null, results.posts);
|
||||
});
|
||||
},
|
||||
async.apply(Posts.expandFlagHistory),
|
||||
function (posts, next) {
|
||||
// Parse out flag data into its own object inside each post hash
|
||||
async.map(posts, function (postObj, next) {
|
||||
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];
|
||||
}
|
||||
}
|
||||
|
||||
if (postObj.flagData.assignee) {
|
||||
user.getUserFields(parseInt(postObj.flagData.assignee, 10), ['username', 'picture'], function (err, userData) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
postObj.flagData.assigneeUser = userData;
|
||||
next(null, postObj);
|
||||
});
|
||||
} else {
|
||||
setImmediate(next.bind(null, null, postObj));
|
||||
}
|
||||
}, next);
|
||||
}
|
||||
], 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) && 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()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
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
|
||||
if (changes.length) {
|
||||
Posts.setPostFields(pid, changeset, callback);
|
||||
} else {
|
||||
setImmediate(callback);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Posts.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('[posts/getFlags] 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);
|
||||
}
|
||||
|
||||
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);
|
||||
};
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -40,7 +40,8 @@ function mainRoutes(app, middleware, controllers) {
|
||||
}
|
||||
|
||||
function modRoutes(app, middleware, controllers) {
|
||||
setupPageRoute(app, '/posts/flags', middleware, [], controllers.mods.flagged);
|
||||
setupPageRoute(app, '/flags', middleware, [], controllers.mods.flags.list);
|
||||
setupPageRoute(app, '/flags/:flagId', middleware, [], controllers.mods.flags.detail);
|
||||
}
|
||||
|
||||
function globalModRoutes(app, middleware, controllers) {
|
||||
|
||||
@@ -69,14 +69,6 @@ User.resetLockouts = function (socket, uids, callback) {
|
||||
async.each(uids, user.auth.resetLockout, callback);
|
||||
};
|
||||
|
||||
User.resetFlags = function (socket, uids, callback) {
|
||||
if (!Array.isArray(uids)) {
|
||||
return callback(new Error('[[error:invalid-data]]'));
|
||||
}
|
||||
|
||||
user.resetFlags(uids, callback);
|
||||
};
|
||||
|
||||
User.validateEmail = function (socket, uids, callback) {
|
||||
if (!Array.isArray(uids)) {
|
||||
return callback(new Error('[[error:invalid-data]]'));
|
||||
|
||||
111
src/socket.io/flags.js
Normal file
111
src/socket.io/flags.js
Normal file
@@ -0,0 +1,111 @@
|
||||
'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.type || !data.id || !data.reason) {
|
||||
return callback(new Error('[[error:invalid-data]]'));
|
||||
}
|
||||
|
||||
async.waterfall([
|
||||
async.apply(flags.validate, {
|
||||
uid: socket.uid,
|
||||
type: data.type,
|
||||
id: data.id
|
||||
}),
|
||||
function (next) {
|
||||
// If we got here, then no errors occurred
|
||||
flags.create(data.type, data.id, socket.uid, data.reason, next);
|
||||
}
|
||||
], function (err, flagObj) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
flags.notify(flagObj, socket.uid);
|
||||
callback(null, flagObj);
|
||||
});
|
||||
};
|
||||
|
||||
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);
|
||||
},
|
||||
async.apply(flags.getHistory, data.flagId)
|
||||
], 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);
|
||||
},
|
||||
function (next) {
|
||||
async.parallel({
|
||||
"notes": async.apply(flags.getNotes, data.flagId),
|
||||
"history": async.apply(flags.getHistory, data.flagId)
|
||||
}, next);
|
||||
}
|
||||
], callback);
|
||||
};
|
||||
|
||||
module.exports = SocketFlags;
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -1,172 +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');
|
||||
|
||||
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;
|
||||
|
||||
posts.flag(post, 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]]'));
|
||||
}
|
||||
posts.dismissFlag(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]]'));
|
||||
}
|
||||
posts.dismissAllFlags(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);
|
||||
|
||||
posts.updateFlagData(socket.uid, data.pid, payload, next);
|
||||
}
|
||||
], callback);
|
||||
};
|
||||
};
|
||||
133
src/upgrade.js
133
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) {
|
||||
@@ -455,52 +455,6 @@ Upgrade.upgrade = function (callback) {
|
||||
next();
|
||||
}
|
||||
},
|
||||
function (next) {
|
||||
thisSchemaDate = Date.UTC(2016, 3, 29);
|
||||
|
||||
if (schemaDate < thisSchemaDate) {
|
||||
updatesMade = true;
|
||||
winston.info('[2016/04/29] Dismiss flags from deleted topics');
|
||||
|
||||
var posts = require('./posts'),
|
||||
topics = require('./topics');
|
||||
|
||||
var pids, tids;
|
||||
|
||||
async.waterfall([
|
||||
async.apply(db.getSortedSetRange, 'posts:flagged', 0, -1),
|
||||
function (_pids, next) {
|
||||
pids = _pids;
|
||||
posts.getPostsFields(pids, ['tid'], next);
|
||||
},
|
||||
function (_tids, next) {
|
||||
tids = _tids.map(function (a) {
|
||||
return a.tid;
|
||||
});
|
||||
|
||||
topics.getTopicsFields(tids, ['deleted'], next);
|
||||
},
|
||||
function (state, next) {
|
||||
var toDismiss = state.map(function (a, idx) {
|
||||
return parseInt(a.deleted, 10) === 1 ? pids[idx] : null;
|
||||
}).filter(Boolean);
|
||||
|
||||
winston.info('[2016/04/29] ' + toDismiss.length + ' dismissable flags found');
|
||||
async.each(toDismiss, posts.dismissFlag, next);
|
||||
}
|
||||
], function (err) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
winston.info('[2016/04/29] Dismiss flags from deleted topics done');
|
||||
Upgrade.update(thisSchemaDate, next);
|
||||
});
|
||||
} else {
|
||||
winston.info('[2016/04/29] Dismiss flags from deleted topics skipped!');
|
||||
next();
|
||||
}
|
||||
},
|
||||
function (next) {
|
||||
thisSchemaDate = Date.UTC(2016, 4, 28);
|
||||
|
||||
@@ -1022,7 +976,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');
|
||||
@@ -1059,6 +1013,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) {
|
||||
|
||||
@@ -6,6 +6,7 @@ var db = require('../database');
|
||||
var posts = require('../posts');
|
||||
var plugins = require('../plugins');
|
||||
var winston = require('winston');
|
||||
var flags = require('../flags');
|
||||
|
||||
module.exports = function (User) {
|
||||
|
||||
@@ -55,14 +56,4 @@ module.exports = function (User) {
|
||||
}
|
||||
], callback);
|
||||
};
|
||||
|
||||
User.resetFlags = function (uids, callback) {
|
||||
if (!Array.isArray(uids) || !uids.length) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
async.eachSeries(uids, function (uid, next) {
|
||||
posts.dismissUserFlags(uid, next);
|
||||
}, callback);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -34,11 +34,20 @@ module.exports = function (User) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!Array.isArray(uids) || !uids.length) {
|
||||
// Eliminate duplicates and build ref table
|
||||
var uniqueUids = uids.filter(function (uid, index) {
|
||||
return index === uids.indexOf(uid);
|
||||
});
|
||||
var ref = uniqueUids.reduce(function (memo, cur, idx) {
|
||||
memo[cur] = idx;
|
||||
return memo;
|
||||
}, {});
|
||||
|
||||
if (!Array.isArray(uniqueUids) || !uniqueUids.length) {
|
||||
return callback(null, []);
|
||||
}
|
||||
|
||||
var keys = uids.map(function (uid) {
|
||||
var keys = uniqueUids.map(function (uid) {
|
||||
return 'user:' + uid;
|
||||
});
|
||||
|
||||
@@ -60,6 +69,10 @@ module.exports = function (User) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
users = uids.map(function (uid) {
|
||||
return users[ref[uid]];
|
||||
});
|
||||
|
||||
modifyUserData(users, fieldsToRemove, callback);
|
||||
});
|
||||
};
|
||||
@@ -80,7 +93,16 @@ module.exports = function (User) {
|
||||
return callback(null, []);
|
||||
}
|
||||
|
||||
var keys = uids.map(function (uid) {
|
||||
// Eliminate duplicates and build ref table
|
||||
var uniqueUids = uids.filter(function (uid, index) {
|
||||
return index === uids.indexOf(uid);
|
||||
});
|
||||
var ref = uniqueUids.reduce(function (memo, cur, idx) {
|
||||
memo[cur] = idx;
|
||||
return memo;
|
||||
}, {});
|
||||
|
||||
var keys = uniqueUids.map(function (uid) {
|
||||
return 'user:' + uid;
|
||||
});
|
||||
|
||||
@@ -89,6 +111,10 @@ module.exports = function (User) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
users = uids.map(function (uid) {
|
||||
return users[ref[uid]];
|
||||
});
|
||||
|
||||
modifyUserData(users, [], callback);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,196 +0,0 @@
|
||||
<div class="flags">
|
||||
|
||||
<div class="col-lg-12">
|
||||
|
||||
<div class="text-center">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-body">
|
||||
<div><canvas id="flags:daily" height="250"></canvas></div>
|
||||
<p>
|
||||
|
||||
</p>
|
||||
</div>
|
||||
<div class="panel-footer"><small>Daily flags</small></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="flag-search" method="GET" action="flags">
|
||||
<div class="form-group">
|
||||
<div>
|
||||
<div>
|
||||
<label>Flags by user</label>
|
||||
<input type="text" class="form-control" id="byUsername" placeholder="Search flagged posts by username" name="byUsername" value="{byUsername}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div>
|
||||
<div>
|
||||
<label>Category</label>
|
||||
<select class="form-control" id="category-selector" name="cid">
|
||||
<option value="">[[unread:all_categories]]</option>
|
||||
<!-- BEGIN categories -->
|
||||
<option value="{categories.cid}" <!-- IF categories.selected -->selected<!-- ENDIF categories.selected -->>{categories.text}</option>
|
||||
<!-- END categories -->
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Sort By</label>
|
||||
<div>
|
||||
<div>
|
||||
<select id="flag-sort-by" class="form-control" name="sortBy">
|
||||
<option value="count" <!-- IF sortByCount -->selected<!-- ENDIF sortByCount -->>Most Flags</option>
|
||||
<option value="time" <!-- IF sortByTime -->selected<!-- ENDIF sortByTime -->>Most Recent</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Search</button>
|
||||
<button class="btn btn-primary" id="dismissAll">Dismiss All</button>
|
||||
</form>
|
||||
|
||||
<hr/>
|
||||
|
||||
<div 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!
|
||||
</div>
|
||||
<!-- ENDIF !posts.length -->
|
||||
|
||||
<!-- BEGIN posts -->
|
||||
<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}">
|
||||
<!-- IF ../flagData.assignee -->
|
||||
<div class="pull-right">
|
||||
<!-- IF ../flagData.assigneeUser.picture -->
|
||||
<img class="avatar avatar-xs" title="{../flagData.assigneeUser.username}" src="{../flagData.assigneeUser.picture}">
|
||||
<!-- ELSE -->
|
||||
<div class="avatar avatar-xs" title="{../flagData.assigneeUser.username}" style="background-color: {../flagData.assigneeUser.icon:bgColor};">{../flagData.assigneeUser.icon:text}</div>
|
||||
<!-- ENDIF ../flagData.assigneeUser.picture -->
|
||||
</div>
|
||||
<!-- ENDIF ../flagData.assignee -->
|
||||
<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>
|
||||
[[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 title="{posts.user.username}" 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>
|
||||
</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> •
|
||||
<a href="{config.relative_path}/post/{posts.pid}" 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">
|
||||
<!-- 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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- END posts -->
|
||||
<!-- IMPORT partials/paginator.tpl -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -19,7 +19,6 @@
|
||||
<li><a href="#" class="ban-user-temporary"><i class="fa fa-fw fa-clock-o"></i> Ban User(s) Temporarily</a></li>
|
||||
<li><a href="#" class="unban-user"><i class="fa fa-fw fa-comment-o"></i> Unban User(s)</a></li>
|
||||
<li><a href="#" class="reset-lockout"><i class="fa fa-fw fa-unlock"></i> Reset Lockout</a></li>
|
||||
<li><a href="#" class="reset-flags"><i class="fa fa-fw fa-flag"></i> Reset Flags</a></li>
|
||||
<li class="divider"></li>
|
||||
<li><a href="#" class="delete-user"><i class="fa fa-fw fa-trash-o"></i> Delete User(s)</a></li>
|
||||
<li><a href="#" class="delete-user-and-content"><i class="fa fa-fw fa-trash-o"></i> Delete User(s) and Content</a></li>
|
||||
|
||||
524
test/flags.js
Normal file
524
test/flags.js
Normal file
@@ -0,0 +1,524 @@
|
||||
'use strict';
|
||||
/*globals require, before, after, describe, it*/
|
||||
|
||||
var assert = require('assert');
|
||||
var async = require('async');
|
||||
|
||||
var db = require('./mocks/databasemock');
|
||||
var Flags = require('../src/flags');
|
||||
var Categories = require('../src/categories');
|
||||
var Topics = require('../src/topics');
|
||||
var Posts = require('../src/posts');
|
||||
var User = require('../src/user');
|
||||
var Groups = require('../src/groups');
|
||||
var Meta = require('../src/meta');
|
||||
|
||||
describe('Flags', function () {
|
||||
before(function (done) {
|
||||
// Create some stuff to flag
|
||||
async.waterfall([
|
||||
async.apply(User.create, {username: 'testUser', password: 'abcdef', email: 'b@c.com'}),
|
||||
function (uid, next) {
|
||||
Categories.create({
|
||||
name: 'test category'
|
||||
}, function (err, category) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
Topics.post({
|
||||
cid: category.cid,
|
||||
uid: uid,
|
||||
title: 'Topic to flag',
|
||||
content: 'This is flaggable content'
|
||||
}, next);
|
||||
});
|
||||
},
|
||||
function (topicData, next) {
|
||||
User.create({
|
||||
username: 'testUser2', password: 'abcdef', email: 'c@d.com'
|
||||
}, next);
|
||||
},
|
||||
function (uid, next) {
|
||||
Groups.join('administrators', uid, next);
|
||||
},
|
||||
function (next) {
|
||||
User.create({
|
||||
username: 'unprivileged', password: 'abcdef', email: 'd@e.com'
|
||||
}, next);
|
||||
}
|
||||
], done);
|
||||
});
|
||||
|
||||
describe('.create()', function () {
|
||||
it('should create a flag and return its data', function (done) {
|
||||
Flags.create('post', 1, 1, 'Test flag', function (err, flagData) {
|
||||
assert.ifError(err);
|
||||
var compare = {
|
||||
flagId: 1,
|
||||
uid: 1,
|
||||
targetId: 1,
|
||||
type: 'post',
|
||||
description: 'Test flag'
|
||||
};
|
||||
|
||||
for(var key in compare) {
|
||||
if (compare.hasOwnProperty(key)) {
|
||||
assert.ok(flagData[key]);
|
||||
assert.equal(flagData[key], compare[key]);
|
||||
}
|
||||
}
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should add the flag to the byCid zset for category 1 if it is of type post', function (done) {
|
||||
db.isSortedSetMember('flags:byCid:' + 1, 1, function (err, isMember) {
|
||||
assert.ifError(err);
|
||||
assert.ok(isMember);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('.exists()', function () {
|
||||
it('should return Boolean True if a flag matching the flag hash already exists', function (done) {
|
||||
Flags.exists('post', 1, 1, function (err, exists) {
|
||||
assert.ifError(err);
|
||||
assert.strictEqual(true, exists);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return Boolean False if a flag matching the flag hash does not already exists', function (done) {
|
||||
Flags.exists('post', 1, 2, function (err, exists) {
|
||||
assert.ifError(err);
|
||||
assert.strictEqual(false, exists);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('.targetExists()', function () {
|
||||
it('should return Boolean True if the targeted element exists', function (done) {
|
||||
Flags.targetExists('post', 1, function (err, exists) {
|
||||
assert.ifError(err);
|
||||
assert.strictEqual(true, exists);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return Boolean False if the targeted element does not exist', function (done) {
|
||||
Flags.targetExists('post', 15, function (err, exists) {
|
||||
assert.ifError(err);
|
||||
assert.strictEqual(false, exists);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('.get()', function () {
|
||||
it('should retrieve and display a flag\'s data', function (done) {
|
||||
Flags.get(1, function (err, flagData) {
|
||||
assert.ifError(err);
|
||||
var compare = {
|
||||
flagId: 1,
|
||||
uid: 1,
|
||||
targetId: 1,
|
||||
type: 'post',
|
||||
description: 'Test flag',
|
||||
state: 'open'
|
||||
};
|
||||
|
||||
for(var key in compare) {
|
||||
if (compare.hasOwnProperty(key)) {
|
||||
assert.ok(flagData[key]);
|
||||
assert.equal(flagData[key], compare[key]);
|
||||
}
|
||||
}
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('.list()', function () {
|
||||
it('should show a list of flags (with one item)', function (done) {
|
||||
Flags.list({}, 1, function (err, flags) {
|
||||
assert.ifError(err);
|
||||
assert.ok(Array.isArray(flags));
|
||||
assert.equal(flags.length, 1);
|
||||
|
||||
Flags.get(flags[0].flagId, function (err, flagData) {
|
||||
assert.ifError(err);
|
||||
assert.equal(flags[0].flagId, flagData.flagId);
|
||||
assert.equal(flags[0].description, flagData.description);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('(with filters)', function () {
|
||||
it('should return a filtered list of flags if said filters are passed in', function (done) {
|
||||
Flags.list({
|
||||
state: 'open'
|
||||
}, 1, function (err, flags) {
|
||||
assert.ifError(err);
|
||||
assert.ok(Array.isArray(flags));
|
||||
assert.strictEqual(1, parseInt(flags[0].flagId, 10));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return no flags if a filter with no matching flags is used', function (done) {
|
||||
Flags.list({
|
||||
state: 'rejected'
|
||||
}, 1, function (err, flags) {
|
||||
assert.ifError(err);
|
||||
assert.ok(Array.isArray(flags));
|
||||
assert.strictEqual(0, flags.length);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a flag when filtered by cid 1', function (done) {
|
||||
Flags.list({
|
||||
cid: 1
|
||||
}, 1, function (err, flags) {
|
||||
assert.ifError(err);
|
||||
assert.ok(Array.isArray(flags));
|
||||
assert.strictEqual(1, flags.length);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('shouldn\'t return a flag when filtered by cid 2', function (done) {
|
||||
Flags.list({
|
||||
cid: 2
|
||||
}, 1, function (err, flags) {
|
||||
assert.ifError(err);
|
||||
assert.ok(Array.isArray(flags));
|
||||
assert.strictEqual(0, flags.length);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('.update()', function () {
|
||||
it('should alter a flag\'s various attributes and persist them to the database', function (done) {
|
||||
Flags.update(1, 1, {
|
||||
"state": "wip",
|
||||
"assignee": 1
|
||||
}, function (err) {
|
||||
assert.ifError(err);
|
||||
db.getObjectFields('flag:1', ['state', 'assignee'], function (err, data) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
assert.strictEqual('wip', data.state);
|
||||
assert.ok(!isNaN(parseInt(data.assignee, 10)));
|
||||
assert.strictEqual(1, parseInt(data.assignee, 10));
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should persist to the flag\'s history', function (done) {
|
||||
Flags.getHistory(1, function (err, history) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
history.forEach(function (change) {
|
||||
switch (change.attribute) {
|
||||
case 'state':
|
||||
assert.strictEqual('[[flags:state-wip]]', change.value);
|
||||
break;
|
||||
|
||||
case 'assignee':
|
||||
assert.strictEqual(1, change.value);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('.getTarget()', function () {
|
||||
it('should return a post\'s data if queried with type "post"', function (done) {
|
||||
Flags.getTarget('post', 1, 1, function (err, data) {
|
||||
assert.ifError(err);
|
||||
var compare = {
|
||||
uid: 1,
|
||||
pid: 1,
|
||||
content: 'This is flaggable content'
|
||||
};
|
||||
|
||||
for(var key in compare) {
|
||||
if (compare.hasOwnProperty(key)) {
|
||||
assert.ok(data[key]);
|
||||
assert.equal(data[key], compare[key]);
|
||||
}
|
||||
}
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a user\'s data if queried with type "user"', function (done) {
|
||||
Flags.getTarget('user', 1, 1, function (err, data) {
|
||||
assert.ifError(err);
|
||||
var compare = {
|
||||
uid: 1,
|
||||
username: 'testUser',
|
||||
email: 'b@c.com'
|
||||
};
|
||||
|
||||
for(var key in compare) {
|
||||
if (compare.hasOwnProperty(key)) {
|
||||
assert.ok(data[key]);
|
||||
assert.equal(data[key], compare[key]);
|
||||
}
|
||||
}
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a plain object with no properties if the target no longer exists', function (done) {
|
||||
Flags.getTarget('user', 15, 1, function (err, data) {
|
||||
assert.ifError(err);
|
||||
assert.strictEqual(0, Object.keys(data).length);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('.validate()', function () {
|
||||
it('should error out if type is post and post is deleted', function (done) {
|
||||
Posts.delete(1, 1, function (err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
Flags.validate({
|
||||
type: 'post',
|
||||
id: 1,
|
||||
uid: 1
|
||||
}, function (err) {
|
||||
assert.ok(err);
|
||||
assert.strictEqual('[[error:post-deleted]]', err.message);
|
||||
Posts.restore(1, 1, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should not pass validation if flag threshold is set and user rep does not meet it', function (done) {
|
||||
Meta.configs.set('privileges:flag', '50', function (err) {
|
||||
assert.ifError(err);
|
||||
|
||||
Flags.validate({
|
||||
type: 'post',
|
||||
id: 1,
|
||||
uid: 3
|
||||
}, function (err) {
|
||||
assert.ok(err);
|
||||
assert.strictEqual('[[error:not-enough-reputation-to-flag]]', err.message);
|
||||
Meta.configs.set('privileges:flag', 0, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('.appendNote()', function () {
|
||||
it('should add a note to a flag', function (done) {
|
||||
Flags.appendNote(1, 1, 'this is my note', function (err) {
|
||||
assert.ifError(err);
|
||||
|
||||
db.getSortedSetRange('flag:1:notes', 0, -1, function (err, notes) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
assert.strictEqual('[1,"this is my note"]', notes[0]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should be a JSON string', function (done) {
|
||||
db.getSortedSetRange('flag:1:notes', 0, -1, function (err, notes) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
try {
|
||||
JSON.parse(notes[0]);
|
||||
} catch (e) {
|
||||
assert.ifError(e);
|
||||
}
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('.getNotes()', function () {
|
||||
before(function (done) {
|
||||
// Add a second note
|
||||
Flags.appendNote(1, 1, 'this is the second note', done);
|
||||
});
|
||||
|
||||
it('return should match a predefined spec', function (done) {
|
||||
Flags.getNotes(1, function (err, notes) {
|
||||
assert.ifError(err);
|
||||
var compare = {
|
||||
uid: 1,
|
||||
content: 'this is my note'
|
||||
};
|
||||
|
||||
var data = notes[1];
|
||||
for(var key in compare) {
|
||||
if (compare.hasOwnProperty(key)) {
|
||||
assert.ok(data[key]);
|
||||
assert.strictEqual(data[key], compare[key]);
|
||||
}
|
||||
}
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should retrieve a list of notes, from newest to oldest', function (done) {
|
||||
Flags.getNotes(1, function (err, notes) {
|
||||
assert.ifError(err);
|
||||
assert(notes[0].datetime > notes[1].datetime);
|
||||
assert.strictEqual('this is the second note', notes[0].content);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('.appendHistory()', function () {
|
||||
var entries;
|
||||
before(function (done) {
|
||||
db.sortedSetCard('flag:1:history', function (err, count) {
|
||||
entries = count;
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should add a new entry into a flag\'s history', function (done) {
|
||||
Flags.appendHistory(1, 1, {
|
||||
state: 'rejected'
|
||||
}, function (err) {
|
||||
assert.ifError(err);
|
||||
|
||||
Flags.getHistory(1, function (err, history) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
assert.strictEqual(entries + 1, history.length);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('.getHistory()', function () {
|
||||
it('should retrieve a flag\'s history', function (done) {
|
||||
Flags.getHistory(1, function (err, history) {
|
||||
assert.ifError(err);
|
||||
assert.strictEqual(history[0].fields.state, '[[flags:state-rejected]]');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('(websockets)', function () {
|
||||
var SocketFlags = require('../src/socket.io/flags.js');
|
||||
var tid, pid, flag;
|
||||
|
||||
before(function (done) {
|
||||
Topics.post({
|
||||
cid: 1,
|
||||
uid: 1,
|
||||
title: 'Another topic',
|
||||
content: 'This is flaggable content'
|
||||
}, function (err, topic) {
|
||||
tid = topic.postData.tid;
|
||||
pid = topic.postData.pid;
|
||||
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.create()', function () {
|
||||
it('should create a flag with no errors', function (done) {
|
||||
SocketFlags.create({ uid: 2 }, {
|
||||
type: 'post',
|
||||
id: pid,
|
||||
reason: 'foobar'
|
||||
}, function (err, flagObj) {
|
||||
flag = flagObj;
|
||||
assert.ifError(err);
|
||||
|
||||
Flags.exists('post', pid, 1, function (err, exists) {
|
||||
assert.ifError(err);
|
||||
assert(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('.update()', function () {
|
||||
it('should update a flag\'s properties', function (done) {
|
||||
SocketFlags.update({ uid: 2 }, {
|
||||
flagId: 2,
|
||||
data: [{
|
||||
name: 'state',
|
||||
value: 'wip'
|
||||
}]
|
||||
}, function (err, history) {
|
||||
assert.ifError(err);
|
||||
assert(Array.isArray(history));
|
||||
assert(history[0].fields.hasOwnProperty('state'));
|
||||
assert.strictEqual('[[flags:state-wip]]', history[0].fields.state);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('.appendNote()', function () {
|
||||
it('should append a note to the flag', function (done) {
|
||||
SocketFlags.appendNote({ uid: 2 }, {
|
||||
flagId: 2,
|
||||
note: 'lorem ipsum dolor sit amet'
|
||||
}, function (err, data) {
|
||||
assert.ifError(err);
|
||||
assert(data.hasOwnProperty('notes'));
|
||||
assert(Array.isArray(data.notes));
|
||||
assert.strictEqual('lorem ipsum dolor sit amet', data.notes[0].content);
|
||||
assert.strictEqual(2, data.notes[0].uid);
|
||||
|
||||
assert(data.hasOwnProperty('history'));
|
||||
assert(Array.isArray(data.history));
|
||||
assert.strictEqual(1, Object.keys(data.history[0].fields).length);
|
||||
assert(data.history[0].fields.hasOwnProperty('notes'));
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
after(function (done) {
|
||||
db.emptydb(done);
|
||||
});
|
||||
});
|
||||
215
test/posts.js
215
test/posts.js
@@ -418,221 +418,6 @@ describe('Post\'s', function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe('flagging a post', function () {
|
||||
var meta = require('../src/meta');
|
||||
var socketPosts = require('../src/socket.io/posts');
|
||||
it('should fail to flag a post due to low reputation', function (done) {
|
||||
meta.config['privileges:flag'] = 10;
|
||||
flagPost(function (err) {
|
||||
assert.equal(err.message, '[[error:not-enough-reputation-to-flag]]');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should flag a post', function (done) {
|
||||
meta.config['privileges:flag'] = -1;
|
||||
flagPost(function (err) {
|
||||
assert.ifError(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return nothing without a uid or a reason', function (done) {
|
||||
socketPosts.flag({uid: 0}, {pid: postData.pid, reason: 'reason'}, function (err) {
|
||||
assert.equal(err.message, '[[error:not-logged-in]]');
|
||||
socketPosts.flag({uid: voteeUid}, {}, function (err) {
|
||||
assert.equal(err.message, '[[error:invalid-data]]');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should return an error without an existing post', function (done) {
|
||||
socketPosts.flag({uid: voteeUid}, {pid: 12312312, reason: 'reason'}, function (err) {
|
||||
assert.equal(err.message, '[[error:no-post]]');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return an error if the flag already exists', function (done) {
|
||||
socketPosts.flag({uid: voteeUid}, {pid: postData.pid, reason: 'reason'}, function (err) {
|
||||
assert.equal(err.message, '[[error:already-flagged]]');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function flagPost(next) {
|
||||
var socketPosts = require('../src/socket.io/posts');
|
||||
socketPosts.flag({uid: voteeUid}, {pid: postData.pid, reason: 'reason'}, next);
|
||||
}
|
||||
|
||||
describe('get flag data', function () {
|
||||
it('should see the flagged post', function (done) {
|
||||
posts.isFlaggedByUser(postData.pid, voteeUid, function (err, hasFlagged) {
|
||||
assert.ifError(err);
|
||||
assert(hasFlagged);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return the flagged post data', function (done) {
|
||||
posts.getFlags('posts:flagged', cid, voteeUid, 0, -1, function (err, flagData) {
|
||||
assert.ifError(err);
|
||||
assert(flagData.posts);
|
||||
assert(flagData.count);
|
||||
assert.equal(flagData.count, 1);
|
||||
assert.equal(flagData.posts.length, 1);
|
||||
assert(flagData.posts[0].flagReasons);
|
||||
assert.equal(flagData.posts[0].flagReasons.length, 1);
|
||||
assert.strictEqual(flagData.posts[0].flagReasons[0].reason, 'reason');
|
||||
assert(flagData.posts[0].flagData);
|
||||
assert.strictEqual(flagData.posts[0].flagData.state, 'open');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('updating a flag', function () {
|
||||
var socketPosts = require('../src/socket.io/posts');
|
||||
|
||||
it('should update a flag', function (done) {
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
socketPosts.updateFlag({uid: globalModUid}, {
|
||||
pid: postData.pid,
|
||||
data: [
|
||||
{name: 'assignee', value: `${globalModUid}`},
|
||||
{name: 'notes', value: 'notes'}
|
||||
]
|
||||
}, function (err) {
|
||||
assert.ifError(err);
|
||||
posts.getFlags('posts:flagged', cid, globalModUid, 0, -1, function (err, flagData) {
|
||||
assert.ifError(err);
|
||||
assert(flagData.posts);
|
||||
assert.equal(flagData.posts.length, 1);
|
||||
assert.deepEqual({
|
||||
assignee: flagData.posts[0].flagData.assignee,
|
||||
notes: flagData.posts[0].flagData.notes,
|
||||
state: flagData.posts[0].flagData.state,
|
||||
labelClass: flagData.posts[0].flagData.labelClass
|
||||
}, {
|
||||
assignee: `${globalModUid}`,
|
||||
notes: 'notes',
|
||||
state: 'open',
|
||||
labelClass: 'info'
|
||||
});
|
||||
next();
|
||||
});
|
||||
});
|
||||
}, function (next) {
|
||||
posts.updateFlagData(globalModUid, postData.pid, {
|
||||
state: 'rejected'
|
||||
}, function (err) {
|
||||
assert.ifError(err);
|
||||
posts.getFlags('posts:flagged', cid, globalModUid, 0, -1, function (err, flagData) {
|
||||
assert.ifError(err);
|
||||
assert(flagData.posts);
|
||||
assert.equal(flagData.posts.length, 1);
|
||||
assert.deepEqual({
|
||||
state: flagData.posts[0].flagData.state,
|
||||
labelClass: flagData.posts[0].flagData.labelClass
|
||||
}, {
|
||||
state: 'rejected',
|
||||
labelClass: 'danger'
|
||||
});
|
||||
next();
|
||||
});
|
||||
});
|
||||
}, function (next) {
|
||||
posts.updateFlagData(globalModUid, postData.pid, {
|
||||
state: 'wip'
|
||||
}, function (err) {
|
||||
assert.ifError(err);
|
||||
posts.getFlags('posts:flagged', cid, globalModUid, 0, -1, function (err, flagData) {
|
||||
assert.ifError(err);
|
||||
assert(flagData.posts);
|
||||
assert.equal(flagData.posts.length, 1);
|
||||
assert.deepEqual({
|
||||
state: flagData.posts[0].flagData.state,
|
||||
labelClass: flagData.posts[0].flagData.labelClass
|
||||
}, {
|
||||
state: 'wip',
|
||||
labelClass: 'warning'
|
||||
});
|
||||
next();
|
||||
});
|
||||
});
|
||||
}, function (next) {
|
||||
posts.updateFlagData(globalModUid, postData.pid, {
|
||||
state: 'resolved'
|
||||
}, function (err) {
|
||||
assert.ifError(err);
|
||||
posts.getFlags('posts:flagged', cid, globalModUid, 0, -1, function (err, flagData) {
|
||||
assert.ifError(err);
|
||||
assert(flagData.posts);
|
||||
assert.equal(flagData.posts.length, 1);
|
||||
assert.deepEqual({
|
||||
state: flagData.posts[0].flagData.state,
|
||||
labelClass: flagData.posts[0].flagData.labelClass
|
||||
}, {
|
||||
state: 'resolved',
|
||||
labelClass: 'success'
|
||||
});
|
||||
next();
|
||||
});
|
||||
});
|
||||
}
|
||||
], done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('dismissing a flag', function () {
|
||||
var socketPosts = require('../src/socket.io/posts');
|
||||
|
||||
it('should dismiss a flag', function (done) {
|
||||
socketPosts.dismissFlag({uid: globalModUid}, postData.pid, function (err) {
|
||||
assert.ifError(err);
|
||||
posts.isFlaggedByUser(postData.pid, voteeUid, function (err, hasFlagged) {
|
||||
assert.ifError(err);
|
||||
assert(!hasFlagged);
|
||||
flagPost(function (err) {
|
||||
assert.ifError(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should dismiss all of a user\'s flags', function (done) {
|
||||
posts.dismissUserFlags(voteeUid, function (err) {
|
||||
assert.ifError(err);
|
||||
posts.isFlaggedByUser(postData.pid, voteeUid, function (err, hasFlagged) {
|
||||
assert.ifError(err);
|
||||
assert(!hasFlagged);
|
||||
flagPost(function (err) {
|
||||
assert.ifError(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should dismiss all flags', function (done) {
|
||||
socketPosts.dismissAllFlags({uid: globalModUid}, {}, function (err) {
|
||||
assert.ifError(err);
|
||||
posts.isFlaggedByUser(postData.pid, voteeUid, function (err, hasFlagged) {
|
||||
assert.ifError(err);
|
||||
assert(!hasFlagged);
|
||||
flagPost(function (err) {
|
||||
assert.ifError(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPostSummaryByPids', function () {
|
||||
it('should return empty array for empty pids', function (done) {
|
||||
posts.getPostSummaryByPids([], 0, {}, function (err, data) {
|
||||
|
||||
@@ -245,16 +245,6 @@ describe('socket.io', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('should reset flags', function (done) {
|
||||
var socketAdmin = require('../src/socket.io/admin');
|
||||
socketAdmin.user.resetFlags({uid: adminUid}, [regularUid], function (err) {
|
||||
assert.ifError(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
describe('validation emails', function () {
|
||||
var socketAdmin = require('../src/socket.io/admin');
|
||||
var meta = require('../src/meta');
|
||||
|
||||
Reference in New Issue
Block a user