mirror of
https://github.com/NodeBB/NodeBB.git
synced 2026-07-07 07:33:15 +02:00
upgrade script and graphs for #5232
This commit is contained in:
@@ -2,12 +2,13 @@
|
||||
|
||||
/* globals define */
|
||||
|
||||
define('forum/flags/list', ['components'], function (components) {
|
||||
define('forum/flags/list', ['components', 'Chart'], function (components, Chart) {
|
||||
var Flags = {};
|
||||
|
||||
Flags.init = function () {
|
||||
Flags.enableFilterForm();
|
||||
Flags.enableChatButtons();
|
||||
Flags.handleGraphs();
|
||||
};
|
||||
|
||||
Flags.enableFilterForm = function () {
|
||||
@@ -38,5 +39,53 @@ define('forum/flags/list', ['components'], function (components) {
|
||||
});
|
||||
};
|
||||
|
||||
Flags.handleGraphs = function () {
|
||||
var dailyCanvas = document.getElementById('flags:daily');
|
||||
var dailyLabels = utils.getDaysArray().map(function (text, idx) {
|
||||
return idx % 3 ? '' : text;
|
||||
});
|
||||
|
||||
if (utils.isMobile()) {
|
||||
Chart.defaults.global.tooltips.enabled = false;
|
||||
}
|
||||
var data = {
|
||||
'flags:daily': {
|
||||
labels: dailyLabels,
|
||||
datasets: [
|
||||
{
|
||||
label: "",
|
||||
backgroundColor: "rgba(151,187,205,0.2)",
|
||||
borderColor: "rgba(151,187,205,1)",
|
||||
pointBackgroundColor: "rgba(151,187,205,1)",
|
||||
pointHoverBackgroundColor: "#fff",
|
||||
pointBorderColor: "#fff",
|
||||
pointHoverBorderColor: "rgba(151,187,205,1)",
|
||||
data: ajaxify.data.analytics
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
dailyCanvas.width = $(dailyCanvas).parent().width();
|
||||
new Chart(dailyCanvas.getContext('2d'), {
|
||||
type: 'line',
|
||||
data: data['flags:daily'],
|
||||
options: {
|
||||
responsive: true,
|
||||
animation: false,
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
scales: {
|
||||
yAxes: [{
|
||||
ticks: {
|
||||
beginAtZero: true
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return Flags;
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ var async = require('async');
|
||||
|
||||
var user = require('../user');
|
||||
var flags = require('../flags');
|
||||
var analytics = require('../analytics');
|
||||
// var adminFlagsController = require('./admin/flags');
|
||||
|
||||
var modsController = {
|
||||
@@ -35,13 +36,17 @@ modsController.flags.list = function (req, res, next) {
|
||||
return memo;
|
||||
}, {});
|
||||
|
||||
flags.list(filters, req.uid, function (err, flags) {
|
||||
async.parallel({
|
||||
flags: async.apply(flags.list, filters, req.uid),
|
||||
analytics: async.apply(analytics.getDailyStatsForSet, 'analytics:flags', Date.now(), 30)
|
||||
}, function (err, data) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.render('flags/list', {
|
||||
flags: flags,
|
||||
flags: data.flags,
|
||||
analytics: data.analytics,
|
||||
hasFilter: !!Object.keys(filters).length,
|
||||
filters: filters,
|
||||
title: '[[pages:flags]]'
|
||||
|
||||
49
src/flags.js
49
src/flags.js
@@ -267,8 +267,16 @@ Flags.getNotes = function (flagId, callback) {
|
||||
], callback);
|
||||
};
|
||||
|
||||
Flags.create = function (type, id, uid, reason, callback) {
|
||||
Flags.create = function (type, id, uid, reason, timestamp, callback) {
|
||||
var targetUid;
|
||||
var doHistoryAppend = false;
|
||||
|
||||
// timestamp is optional
|
||||
if (typeof timestamp === 'function' && !callback) {
|
||||
callback = timestamp;
|
||||
timestamp = Date.now();
|
||||
doHistoryAppend = true;
|
||||
}
|
||||
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
@@ -302,16 +310,17 @@ Flags.create = function (type, id, uid, reason, callback) {
|
||||
targetId: id,
|
||||
description: reason,
|
||||
uid: uid,
|
||||
datetime: Date.now()
|
||||
datetime: timestamp
|
||||
}),
|
||||
async.apply(db.sortedSetAdd.bind(db), 'flags:datetime', Date.now(), flagId), // by time, the default
|
||||
async.apply(db.sortedSetAdd.bind(db), 'flags:byReporter:' + uid, Date.now(), flagId), // by reporter
|
||||
async.apply(db.sortedSetAdd.bind(db), 'flags:byType:' + type, Date.now(), flagId), // by flag type
|
||||
async.apply(db.setObjectField.bind(db), 'flagHash:flagId', [type, id, uid].join(':'), flagId) // save hash for existence checking
|
||||
async.apply(db.sortedSetAdd.bind(db), 'flags:datetime', timestamp, flagId), // by time, the default
|
||||
async.apply(db.sortedSetAdd.bind(db), 'flags:byReporter:' + uid, timestamp, flagId), // by reporter
|
||||
async.apply(db.sortedSetAdd.bind(db), 'flags:byType:' + type, timestamp, flagId), // by flag type
|
||||
async.apply(db.setObjectField.bind(db), 'flagHash:flagId', [type, id, uid].join(':'), flagId), // save hash for existence checking
|
||||
async.apply(analytics.increment, 'flags') // some fancy analytics
|
||||
];
|
||||
|
||||
if (targetUid) {
|
||||
tasks.push(async.apply(db.sortedSetAdd.bind(db), 'flags:byTargetUid:' + targetUid, Date.now(), flagId)); // by target uid
|
||||
tasks.push(async.apply(db.sortedSetAdd.bind(db), 'flags:byTargetUid:' + targetUid, timestamp, flagId)); // by target uid
|
||||
}
|
||||
|
||||
async.parallel(tasks, function (err, data) {
|
||||
@@ -319,7 +328,10 @@ Flags.create = function (type, id, uid, reason, callback) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
Flags.update(flagId, uid, { "state": "open" });
|
||||
if (doHistoryAppend) {
|
||||
Flags.update(flagId, uid, { "state": "open" });
|
||||
}
|
||||
|
||||
next(null, flagId);
|
||||
});
|
||||
},
|
||||
@@ -423,7 +435,7 @@ Flags.update = function (flagId, uid, changeset, callback) {
|
||||
// Retrieve existing flag data to compare for history-saving purposes
|
||||
var fields = ['state', 'assignee'];
|
||||
var tasks = [];
|
||||
var now = Date.now();
|
||||
var now = changeset.datetime || Date.now();
|
||||
|
||||
async.waterfall([
|
||||
async.apply(db.getObjectFields.bind(db), 'flag:' + flagId, fields),
|
||||
@@ -513,16 +525,24 @@ Flags.getHistory = function (flagId, callback) {
|
||||
|
||||
Flags.appendHistory = function (flagId, uid, changeset, callback) {
|
||||
var payload;
|
||||
var datetime = changeset.datetime || Date.now();
|
||||
delete changeset.datetime;
|
||||
|
||||
try {
|
||||
payload = JSON.stringify([uid, changeset, Date.now()]);
|
||||
payload = JSON.stringify([uid, changeset, datetime]);
|
||||
} catch (e) {
|
||||
return callback(e);
|
||||
}
|
||||
|
||||
db.sortedSetAdd('flag:' + flagId + ':history', Date.now(), payload, callback);
|
||||
db.sortedSetAdd('flag:' + flagId + ':history', datetime, payload, callback);
|
||||
};
|
||||
|
||||
Flags.appendNote = function (flagId, uid, note, callback) {
|
||||
Flags.appendNote = function (flagId, uid, note, datetime, callback) {
|
||||
if (typeof datetime === 'function' && !callback) {
|
||||
callback = datetime;
|
||||
datetime = Date.now();
|
||||
}
|
||||
|
||||
var payload;
|
||||
try {
|
||||
payload = JSON.stringify([uid, note]);
|
||||
@@ -531,9 +551,10 @@ Flags.appendNote = function (flagId, uid, note, callback) {
|
||||
}
|
||||
|
||||
async.waterfall([
|
||||
async.apply(db.sortedSetAdd, 'flag:' + flagId + ':notes', Date.now(), payload),
|
||||
async.apply(db.sortedSetAdd, 'flag:' + flagId + ':notes', datetime, payload),
|
||||
async.apply(Flags.appendHistory, flagId, uid, {
|
||||
notes: null
|
||||
notes: null,
|
||||
datetime: datetime
|
||||
})
|
||||
], callback);
|
||||
};
|
||||
|
||||
@@ -12,7 +12,7 @@ var db = require('./database'),
|
||||
schemaDate, thisSchemaDate,
|
||||
|
||||
// IMPORTANT: REMEMBER TO UPDATE VALUE OF latestSchema
|
||||
latestSchema = Date.UTC(2016, 10, 22);
|
||||
latestSchema = Date.UTC(2016, 11, 7);
|
||||
|
||||
Upgrade.check = function (callback) {
|
||||
db.get('schemaDate', function (err, value) {
|
||||
@@ -1023,7 +1023,7 @@ Upgrade.upgrade = function (callback) {
|
||||
|
||||
if (schemaDate < thisSchemaDate) {
|
||||
updatesMade = true;
|
||||
winston.info('[2016/11/25] Creating sorted sets for pinned topcis');
|
||||
winston.info('[2016/11/25] Creating sorted sets for pinned topics');
|
||||
|
||||
var topics = require('./topics');
|
||||
var batch = require('./batch');
|
||||
@@ -1060,6 +1060,89 @@ Upgrade.upgrade = function (callback) {
|
||||
next();
|
||||
}
|
||||
},
|
||||
function (next) {
|
||||
thisSchemaDate = Date.UTC(2016, 11, 7);
|
||||
|
||||
if (schemaDate < thisSchemaDate) {
|
||||
updatesMade = true;
|
||||
winston.info('[2016/12/07] Migrating flags to new schema (#5232)');
|
||||
|
||||
var batch = require('./batch');
|
||||
var posts = require('./posts');
|
||||
var flags = require('./flags');
|
||||
var migrated = 0;
|
||||
|
||||
batch.processSortedSet('posts:pid', function (ids, next) {
|
||||
posts.getPostsByPids(ids, 1, function (err, posts) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
posts = posts.filter(function (post) {
|
||||
return post.hasOwnProperty('flags');
|
||||
});
|
||||
|
||||
async.each(posts, function (post, next) {
|
||||
async.parallel({
|
||||
uids: async.apply(db.getSortedSetRangeWithScores, 'pid:' + post.pid + ':flag:uids', 0, -1),
|
||||
reasons: async.apply(db.getSortedSetRange, 'pid:' + post.pid + ':flag:uid:reason', 0, -1)
|
||||
}, function (err, data) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
// Just take the first entry
|
||||
var datetime = data.uids[0].score;
|
||||
var reason = data.reasons[0].split(':')[1];
|
||||
var flagObj;
|
||||
|
||||
async.waterfall([
|
||||
async.apply(flags.create, 'post', post.pid, data.uids[0].value, reason, datetime),
|
||||
function (_flagObj, next) {
|
||||
flagObj = _flagObj;
|
||||
if (post['flag:state'] || post['flag:assignee']) {
|
||||
flags.update(flagObj.flagId, 1, {
|
||||
state: post['flag:state'],
|
||||
assignee: post['flag:assignee'],
|
||||
datetime: datetime
|
||||
}, next);
|
||||
} else {
|
||||
setImmediate(next);
|
||||
}
|
||||
},
|
||||
function (next) {
|
||||
if (post.hasOwnProperty('flag:notes') && post['flag:notes'].length) {
|
||||
try {
|
||||
var history = JSON.parse(post['flag:history']);
|
||||
history = history.filter(function (event) {
|
||||
return event.type === 'notes';
|
||||
})[0];
|
||||
|
||||
flags.appendNote(flagObj.flagId, history.uid, post['flag:notes'], history.timestamp, next);
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
} else {
|
||||
setImmediate(next);
|
||||
}
|
||||
}
|
||||
], next);
|
||||
});
|
||||
}, next);
|
||||
});
|
||||
}, function (err) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
winston.info('[2016/12/07] Migrating flags to new schema (#5232) - done');
|
||||
Upgrade.update(thisSchemaDate, next);
|
||||
});
|
||||
} else {
|
||||
winston.info('[2016/12/07] Migrating flags to new schema (#5232) - skipped!');
|
||||
next();
|
||||
}
|
||||
}
|
||||
// Add new schema updates here
|
||||
// IMPORTANT: REMEMBER TO UPDATE VALUE OF latestSchema IN LINE 24!!!
|
||||
], function (err) {
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user