upgrade script and graphs for #5232

v1.18.x
Julian Lam 8 years ago
parent 5d63c0aa3b
commit ebc9abd773

@ -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]]'

@ -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>
&nbsp;[[topic:flag_manage_title, {posts.category.name}]]
<small><span class="timeago" title="{posts.timestampISO}"></span></small>
</a>
</h4>
</div>
<div id="flag-pid-{posts.pid}" class="panel-collapse collapse" role="tabpanel">
<div class="panel-body">
<div class="row" data-pid="{posts.pid}" data-tid="{posts.topic.tid}">
<div class="col-sm-8">
<div class="well flag-post-body">
<a href="{config.relative_path}/user/{../user.userslug}">
<!-- IF ../user.picture -->
<img title="{posts.user.username}" src="{../user.picture}">
<!-- ELSE -->
<div 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> &bull;
<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>
Loading…
Cancel
Save