Merge remote-tracking branch 'origin/master' into develop

v1.18.x
Julian Lam 8 years ago
commit 29c7ae7645

@ -1,7 +1,7 @@
/* /*
NodeBB - A better forum platform for the modern web NodeBB - A better forum platform for the modern web
https://github.com/NodeBB/NodeBB/ https://github.com/NodeBB/NodeBB/
Copyright (C) 2013-2016 NodeBB Inc. Copyright (C) 2013-2017 NodeBB Inc.
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by

@ -13,7 +13,7 @@
"start": "node loader.js", "start": "node loader.js",
"lint": "eslint --cache .", "lint": "eslint --cache .",
"pretest": "npm run lint", "pretest": "npm run lint",
"test": "istanbul cover node_modules/mocha/bin/_mocha -- -R dot", "test": "istanbul cover node_modules/mocha/bin/_mocha -- -R spec",
"coveralls": "istanbul cover _mocha --report lcovonly -- -R dot && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage" "coveralls": "istanbul cover _mocha --report lcovonly -- -R dot && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage"
}, },
"dependencies": { "dependencies": {
@ -61,7 +61,7 @@
"nodebb-plugin-spam-be-gone": "0.4.10", "nodebb-plugin-spam-be-gone": "0.4.10",
"nodebb-rewards-essentials": "0.0.9", "nodebb-rewards-essentials": "0.0.9",
"nodebb-theme-lavender": "3.0.15", "nodebb-theme-lavender": "3.0.15",
"nodebb-theme-persona": "4.1.90", "nodebb-theme-persona": "4.1.93",
"nodebb-theme-vanilla": "5.1.57", "nodebb-theme-vanilla": "5.1.57",
"nodebb-widget-essentials": "2.0.13", "nodebb-widget-essentials": "2.0.13",
"nodemailer": "2.6.4", "nodemailer": "2.6.4",

@ -20,6 +20,7 @@
"plugin-item.themes": "Themes", "plugin-item.themes": "Themes",
"plugin-item.deactivate": "Deactivate", "plugin-item.deactivate": "Deactivate",
"plugin-item.activate": "Activate", "plugin-item.activate": "Activate",
"plugin-item.install": "Install",
"plugin-item.uninstall": "Uninstall", "plugin-item.uninstall": "Uninstall",
"plugin-item.settings": "Settings", "plugin-item.settings": "Settings",
"plugin-item.installed": "Installed", "plugin-item.installed": "Installed",

@ -39,7 +39,7 @@
"section-appearance": "Appearance", "section-appearance": "Appearance",
"appearance/themes": "Themes", "appearance/themes": "Themes",
"appearance/skins": "Skins", "appearance/skins": "Skins",
"appearance/customise": "Custom HTML & CSS", "appearance/customise": "Custom HTML & CSS",
"section-extend": "Extend", "section-extend": "Extend",
"extend/plugins": "Plugins", "extend/plugins": "Plugins",

@ -95,6 +95,7 @@
} }
url = [config.relative_path, url].join('/'); url = [config.relative_path, url].join('/');
var fallback;
$('#main-menu li').removeClass('active'); $('#main-menu li').removeClass('active');
$('#main-menu a').removeClass('active').filter('[href="' + url + '"]').each(function () { $('#main-menu a').removeClass('active').filter('[href="' + url + '"]').each(function () {
@ -102,36 +103,35 @@
menu menu
.parent().addClass('active') .parent().addClass('active')
.parents('.menu-item').addClass('active'); .parents('.menu-item').addClass('active');
fallback = menu.text();
var match = menu.attr('href').match(/admin\/((.+?)\/.+?)$/);
if (!match) {
return;
}
var str = '[[admin/menu:' + match[1] + ']]';
if (match[2] === 'settings') {
str = translator.compile('admin/menu:settings.page-title', str);
}
translator.translate(str, function (text) {
$('#main-page-title').text(text);
});
}); });
var title = url; var mainTitle;
if (/admin\/general\/dashboard$/.test(title)) { var pageTitle;
title = '[[admin/menu:general/dashboard]]'; if (/admin\/general\/dashboard$/.test(url)) {
mainTitle = pageTitle = '[[admin/menu:general/dashboard]]';
} else if (/admin\/plugins\//.test(url)) {
mainTitle = fallback;
pageTitle = '[[admin/menu:section-plugins]] > ' + mainTitle;
} else { } else {
title = title.match(/admin\/(.+?)\/(.+?)$/); var matches = url.match(/admin\/(.+?)\/(.+?)$/);
title = '[[admin/menu:section-' + mainTitle = '[[admin/menu:' + matches[1] + '/' + matches[2] + ']]';
(title[1] === 'development' ? 'advanced' : title[1]) + pageTitle = '[[admin/menu:section-' +
']]' + (title[2] ? (' > [[admin/menu:' + (matches[1] === 'development' ? 'advanced' : matches[1]) +
title[1] + '/' + title[2] + ']]') : ''); ']]' + (matches[2] ? (' > ' + mainTitle) : '');
if (matches[2] === 'settings') {
mainTitle = translator.compile('admin/menu:settings.page-title', mainTitle);
}
} }
title = '[[admin/admin:acp-title, ' + title + ']]'; pageTitle = translator.compile('admin/admin:acp-title', pageTitle);
translator.translate(title, function (title) { translator.translate(pageTitle, function (title) {
document.title = title.replace(/>/g, '>'); document.title = title.replace(/>/g, '>');
}); });
translator.translate(mainTitle, function (text) {
$('#main-page-title').text(text);
});
}); });
} }

@ -5,7 +5,7 @@ define('forum/category', [
'forum/infinitescroll', 'forum/infinitescroll',
'share', 'share',
'navigator', 'navigator',
'forum/categoryTools', 'forum/category/tools',
'sort', 'sort',
'components', 'components',
'translator', 'translator',

@ -4,7 +4,12 @@
/* globals define, app, socket, bootbox, ajaxify */ /* globals define, app, socket, bootbox, ajaxify */
define('forum/categoryTools', ['forum/topic/move', 'topicSelect', 'components', 'translator'], function (move, topicSelect, components, translator) { define('forum/category/tools', [
'forum/topic/move',
'topicSelect',
'components',
'translator'
], function (move, topicSelect, components, translator) {
var CategoryTools = {}; var CategoryTools = {};
@ -13,6 +18,8 @@ define('forum/categoryTools', ['forum/topic/move', 'topicSelect', 'components',
topicSelect.init(updateDropdownOptions); topicSelect.init(updateDropdownOptions);
handlePinnedTopicSort();
components.get('topic/delete').on('click', function () { components.get('topic/delete').on('click', function () {
categoryCommand('delete', topicSelect.getSelectedTids()); categoryCommand('delete', topicSelect.getSelectedTids());
return false; return false;
@ -235,5 +242,30 @@ define('forum/categoryTools', ['forum/topic/move', 'topicSelect', 'components',
getTopicEl(data.tid).remove(); getTopicEl(data.tid).remove();
} }
function handlePinnedTopicSort() {
if (!ajaxify.data.privileges.isAdminOrMod) {
return;
}
app.loadJQueryUI(function () {
$('[component="category"]').sortable({
items: '[component="category/topic"].pinned',
update: function () {
var data = [];
var pinnedTopics = $('[component="category/topic"].pinned');
pinnedTopics.each(function (index, element) {
data.push({tid: $(element).attr('data-tid'), order: pinnedTopics.length - index - 1});
});
socket.emit('topics.orderPinnedTopics', data, function (err) {
if (err) {
return app.alertError(err.message);
}
});
}
});
});
}
return CategoryTools; return CategoryTools;
}); });

@ -111,7 +111,7 @@ module.exports = function (Categories) {
Categories.onNewPostMade = function (cid, pinned, postData, callback) { Categories.onNewPostMade = function (cid, pinned, postData, callback) {
if (!cid || !postData) { if (!cid || !postData) {
return callback(); return setImmediate(callback);
} }
async.parallel([ async.parallel([
@ -123,17 +123,23 @@ module.exports = function (Categories) {
}, },
function (next) { function (next) {
if (parseInt(pinned, 10) === 1) { if (parseInt(pinned, 10) === 1) {
next(); return setImmediate(next);
} else {
db.sortedSetAdd('cid:' + cid + ':tids', postData.timestamp, postData.tid, next);
} }
},
async.parallel([
function (next) { function (next) {
Categories.updateRecentTid(cid, postData.tid, next); db.sortedSetAdd('cid:' + cid + ':tids', postData.timestamp, postData.tid, next);
}, },
function (next) { function (next) {
db.sortedSetIncrBy('cid:' + cid + ':tids:posts', 1, postData.tid, next); db.sortedSetIncrBy('cid:' + cid + ':tids:posts', 1, postData.tid, next);
} }
], function (err) {
next(err);
});
},
function (next) {
Categories.updateRecentTid(cid, postData.tid, next);
}
], callback); ], callback);
}; };

@ -57,7 +57,7 @@ module.exports = function (Meta) {
'public/src/client/topic/threadTools.js', 'public/src/client/topic/threadTools.js',
'public/src/client/categories.js', 'public/src/client/categories.js',
'public/src/client/category.js', 'public/src/client/category.js',
'public/src/client/categoryTools.js', 'public/src/client/category/tools.js',
'public/src/modules/translator.js', 'public/src/modules/translator.js',
'public/src/modules/notifications.js', 'public/src/modules/notifications.js',

@ -39,6 +39,11 @@ module.exports = function (Plugins) {
}; };
Plugins.prepareForBuild = function (callback) { Plugins.prepareForBuild = function (callback) {
Plugins.cssFiles.length = 0;
Plugins.lessFiles.length = 0;
Plugins.clientScripts.length = 0;
Plugins.acpScripts.length = 0;
async.waterfall([ async.waterfall([
async.apply(Plugins.getPluginPaths), async.apply(Plugins.getPluginPaths),
function (paths, next) { function (paths, next) {

@ -65,8 +65,8 @@ module.exports = function (Posts) {
} }
post.user = results.users[post.uid]; post.user = results.users[post.uid];
post.topic = results.topics[post.tid]; post.topic = results.topics[post.tid];
post.category = results.categories[post.topic.cid]; post.category = post.topic && results.categories[post.topic.cid];
post.isMainPost = parseInt(post.pid, 10) === parseInt(post.topic.mainPid, 10); post.isMainPost = post.topic && parseInt(post.pid, 10) === parseInt(post.topic.mainPid, 10);
post.deleted = parseInt(post.deleted, 10) === 1; post.deleted = parseInt(post.deleted, 10) === 1;
post.upvotes = parseInt(post.upvotes, 10) || 0; post.upvotes = parseInt(post.upvotes, 10) || 0;
post.downvotes = parseInt(post.downvotes, 10) || 0; post.downvotes = parseInt(post.downvotes, 10) || 0;

@ -150,7 +150,7 @@ module.exports = function (SocketPosts) {
return callback(new Error('[[error:cant-purge-main-post]]')); return callback(new Error('[[error:cant-purge-main-post]]'));
} }
if (results.isMain && results.isLast) { if (results.isMain && results.isLast) {
deleteTopicOf(data.pid, socket, next); return deleteTopicOf(data.pid, socket, next);
} }
setImmediate(next); setImmediate(next);
}, },

@ -121,4 +121,12 @@ module.exports = function (SocketTopics) {
], callback); ], callback);
} }
SocketTopics.orderPinnedTopics = function (socket, data, callback) {
if (!Array.isArray(data)) {
return callback(new Error('[[error:invalid-data]]'));
}
topics.tools.orderPinnedTopics(socket.uid, data, callback);
};
}; };

@ -1,6 +1,7 @@
'use strict'; 'use strict';
var async = require('async'); var async = require('async');
var _ = require('underscore');
var db = require('../database'); var db = require('../database');
var categories = require('../categories'); var categories = require('../categories');
@ -184,13 +185,13 @@ module.exports = function (Topics) {
async.parallel([ async.parallel([
async.apply(db.sortedSetAdd, 'cid:' + topicData.cid + ':tids:pinned', Date.now(), tid), async.apply(db.sortedSetAdd, 'cid:' + topicData.cid + ':tids:pinned', Date.now(), tid),
async.apply(db.sortedSetRemove, 'cid:' + topicData.cid + ':tids', tid), async.apply(db.sortedSetRemove, 'cid:' + topicData.cid + ':tids', tid),
async.apply(db.sortedSetRemove, 'cid:' + topicData.cid + ':tids:posts', tid), async.apply(db.sortedSetRemove, 'cid:' + topicData.cid + ':tids:posts', tid)
], next); ], next);
} else { } else {
async.parallel([ async.parallel([
async.apply(db.sortedSetRemove, 'cid:' + topicData.cid + ':tids:pinned', tid), async.apply(db.sortedSetRemove, 'cid:' + topicData.cid + ':tids:pinned', tid),
async.apply(db.sortedSetAdd, 'cid:' + topicData.cid + ':tids', topicData.lastposttime, tid), async.apply(db.sortedSetAdd, 'cid:' + topicData.cid + ':tids', topicData.lastposttime, tid),
async.apply(db.sortedSetAdd, 'cid:' + topicData.cid + ':tids:posts', topicData.postcount, tid), async.apply(db.sortedSetAdd, 'cid:' + topicData.cid + ':tids:posts', topicData.postcount, tid)
], next); ], next);
} }
} }
@ -211,6 +212,49 @@ module.exports = function (Topics) {
], callback); ], callback);
} }
topicTools.orderPinnedTopics = function (uid, data, callback) {
var cid;
async.waterfall([
function (next) {
var tids = data.map(function (topic) {
return topic && topic.tid;
});
Topics.getTopicsFields(tids, ['cid'], next);
},
function (topicData, next) {
var uniqueCids = _.unique(topicData.map(function (topicData) {
return topicData && parseInt(topicData.cid, 10);
}));
if (uniqueCids.length > 1 || !uniqueCids.length || !uniqueCids[0]) {
return next(new Error('[[error:invalid-data]]'));
}
cid = uniqueCids[0];
privileges.categories.isAdminOrMod(cid, uid, next);
},
function (isAdminOrMod, next) {
if (!isAdminOrMod) {
return next(new Error('[[error:no-privileges]]'));
}
async.eachSeries(data, function (topicData, next) {
async.waterfall([
function (next) {
db.isSortedSetMember('cid:' + cid + ':tids:pinned', topicData.tid, next);
},
function (isPinned, next) {
if (isPinned) {
db.sortedSetAdd('cid:' + cid + ':tids:pinned', topicData.order, topicData.tid, next);
} else {
setImmediate(next);
}
}
], next);
}, next);
}
], callback);
};
topicTools.move = function (tid, cid, uid, callback) { topicTools.move = function (tid, cid, uid, callback) {
var topic; var topic;
async.waterfall([ async.waterfall([

@ -28,9 +28,7 @@
<a target="_blank" href="{config.relative_path}/api/admin/users/csv" class="btn btn-primary pull-right">[[admin/manage/users:download-csv]]</a> <a target="_blank" href="{config.relative_path}/api/admin/users/csv" class="btn btn-primary pull-right">[[admin/manage/users:download-csv]]</a>
<!-- IF inviteOnly --> <!-- IF inviteOnly -->
<!-- IF loggedIn -->
<button component="user/invite" class="btn btn-success form-control"><i class="fa fa-users"></i> [[admin/manage/users:invite]]</button> <button component="user/invite" class="btn btn-success form-control"><i class="fa fa-users"></i> [[admin/manage/users:invite]]</button>
<!-- ENDIF loggedIn -->
<!-- ENDIF inviteOnly --> <!-- ENDIF inviteOnly -->
<button id="createUser" class="btn btn-primary pull-right">[[admin/manage/users:new]]</button> <button id="createUser" class="btn btn-primary pull-right">[[admin/manage/users:new]]</button>

@ -1,2 +1,2 @@
--reporter dot --reporter dot
--timeout 10000 --timeout 15000

@ -101,6 +101,7 @@ describe('Plugins', function () {
var latest; var latest;
var pluginName = 'nodebb-plugin-imgur'; var pluginName = 'nodebb-plugin-imgur';
it('should install a plugin', function (done) { it('should install a plugin', function (done) {
this.timeout(20000);
plugins.toggleInstall(pluginName, '1.0.16', function (err, pluginData) { plugins.toggleInstall(pluginName, '1.0.16', function (err, pluginData) {
assert.ifError(err); assert.ifError(err);

@ -185,32 +185,38 @@ describe('Post\'s', function () {
}); });
describe('delete/restore/purge', function () { describe('delete/restore/purge', function () {
var tid; function createTopicWithReply(callback) {
var mainPid;
var replyPid;
var socketPosts = require('../src/socket.io/posts');
before(function (done) {
topics.post({ topics.post({
uid: voterUid, uid: voterUid,
cid: cid, cid: cid,
title: 'topic to delete/restore/purge', title: 'topic to delete/restore/purge',
content: 'A post to delete/restore/purge' content: 'A post to delete/restore/purge'
}, function (err, data) { }, function (err, topicPostData) {
assert.ifError(err); assert.ifError(err);
tid = data.topicData.tid;
mainPid = data.postData.pid;
topics.reply({ topics.reply({
uid: voterUid, uid: voterUid,
tid: topicData.tid, tid: topicPostData.topicData.tid,
timestamp: Date.now(), timestamp: Date.now(),
content: 'A post to delete/restore and purge' content: 'A post to delete/restore and purge'
}, function (err, data) { }, function (err, replyData) {
assert.ifError(err); assert.ifError(err);
replyPid = data.pid; callback(topicPostData, replyData);
privileges.categories.give(['purge'], cid, 'registered-users', done);
}); });
}); });
}
var tid;
var mainPid;
var replyPid;
var socketPosts = require('../src/socket.io/posts');
before(function (done) {
createTopicWithReply(function (topicPostData, replyData) {
tid = topicPostData.topicData.tid;
mainPid = topicPostData.postData.pid;
replyPid = replyData.pid;
privileges.categories.give(['purge'], cid, 'registered-users', done);
});
}); });
it('should error with invalid data', function (done) { it('should error with invalid data', function (done) {
@ -242,32 +248,53 @@ describe('Post\'s', function () {
}); });
}); });
it('should delete posts and topic', function (done) { it('should delete posts', function (done) {
socketPosts.deletePosts({uid: globalModUid}, {pids: [replyPid, mainPid], tid: tid}, function (err) { socketPosts.deletePosts({uid: globalModUid}, {pids: [replyPid, mainPid], tid: tid}, function (err) {
assert.ifError(err); assert.ifError(err);
topics.getTopicField(tid, 'deleted', function (err, deleted) { posts.getPostField(replyPid, 'deleted', function (err, deleted) {
assert.ifError(err);
assert.equal(parseInt(deleted, 10), 1);
posts.getPostField(mainPid, 'deleted', function (err, deleted) {
assert.ifError(err); assert.ifError(err);
assert.equal(parseInt(deleted, 10), 1); assert.equal(parseInt(deleted, 10), 1);
done(); done();
}); });
}); });
}); });
});
it('should purge posts', function (done) { it('should delete topic if last main post is deleted', function (done) {
var socketTopics = require('../src/socket.io/topics'); topics.post({uid: voterUid, cid: cid, title: 'test topic', content: 'test topic'}, function (err, data) {
socketTopics.restore({uid: globalModUid}, {tids: [tid], cid: cid}, function (err) {
assert.ifError(err); assert.ifError(err);
socketPosts.purgePosts({uid: voterUid}, {pids: [replyPid, mainPid], tid: tid}, function (err) { socketPosts.deletePosts({uid: globalModUid}, {pids: [data.postData.pid], tid: data.topicData.tid}, function (err) {
assert.ifError(err);
topics.getTopicField(data.topicData.tid, 'deleted', function (err, deleted) {
assert.ifError(err);
assert.equal(parseInt(deleted, 10), 1);
done();
});
});
});
});
it('should purge posts and delete topic', function (done) {
createTopicWithReply(function (topicPostData, replyData) {
socketPosts.purgePosts({uid: voterUid}, {pids: [replyData.pid, topicPostData.postData.pid], tid: topicPostData.topicData.tid}, function (err) {
assert.ifError(err); assert.ifError(err);
posts.exists('post:' + replyPid, function (err, exists) { posts.exists('post:' + replyData.pid, function (err, exists) {
assert.ifError(err); assert.ifError(err);
assert.equal(exists, false); assert.equal(exists, false);
topics.getTopicField(topicPostData.topicData.tid, 'deleted', function (err, deleted) {
assert.ifError(err);
assert.equal(parseInt(deleted, 10), 1);
done(); done();
}); });
}); });
}); });
}); });
}); });
});
describe('edit', function () { describe('edit', function () {
var pid; var pid;

@ -45,8 +45,6 @@ describe('Topic\'s', function () {
done(); done();
}); });
}); });
}); });
describe('.post', function () { describe('.post', function () {
@ -362,6 +360,98 @@ describe('Topic\'s', function () {
}); });
}); });
describe('order pinned topics', function () {
var tid1;
var tid2;
var tid3;
before(function (done) {
function createTopic(callback) {
topics.post({
uid: topic.userId,
title: 'topic for test',
content: 'topic content',
cid: topic.categoryId
}, callback);
}
async.series({
topic1: function (next) {
createTopic(next);
},
topic2: function (next) {
createTopic(next);
},
topic3: function (next) {
createTopic(next);
}
}, function (err, results) {
assert.ifError(err);
tid1 = results.topic1.topicData.tid;
tid2 = results.topic2.topicData.tid;
tid3 = results.topic3.topicData.tid;
async.series([
function (next) {
topics.tools.pin(tid1, adminUid, next);
},
function (next) {
topics.tools.pin(tid2, adminUid, next);
}
], done);
});
});
var socketTopics = require('../src/socket.io/topics');
it('should error with invalid data', function (done) {
socketTopics.orderPinnedTopics({uid: adminUid}, null, function (err) {
assert.equal(err.message, '[[error:invalid-data]]');
done();
});
});
it('should error with invalid data', function (done) {
socketTopics.orderPinnedTopics({uid: adminUid}, [null, null], function (err) {
assert.equal(err.message, '[[error:invalid-data]]');
done();
});
});
it('should error with unprivileged user', function (done) {
socketTopics.orderPinnedTopics({uid: 0}, [{tid: tid1}, {tid: tid2}], function (err) {
assert.equal(err.message, '[[error:no-privileges]]');
done();
});
});
it('should not do anything if topics are not pinned', function (done) {
socketTopics.orderPinnedTopics({uid: adminUid}, [{tid: tid3}], function (err) {
assert.ifError(err);
db.isSortedSetMember('cid:' + topic.categoryId + ':tids:pinned', tid3, function (err, isMember) {
assert.ifError(err);
assert(!isMember);
done();
});
});
});
it('should order pinned topics', function (done) {
db.getSortedSetRevRange('cid:' + topic.categoryId + ':tids:pinned', 0, -1, function (err, pinnedTids) {
assert.ifError(err);
assert.equal(pinnedTids[0], tid2);
assert.equal(pinnedTids[1], tid1);
socketTopics.orderPinnedTopics({uid: adminUid}, [{tid: tid1, order: 1}, {tid: tid2, order: 0}], function (err) {
assert.ifError(err);
db.getSortedSetRevRange('cid:' + topic.categoryId + ':tids:pinned', 0, -1, function (err, pinnedTids) {
assert.ifError(err);
assert.equal(pinnedTids[0], tid1);
assert.equal(pinnedTids[1], tid2);
done();
});
});
});
});
});
describe('.ignore', function () { describe('.ignore', function () {
var newTid; var newTid;
var uid; var uid;

Loading…
Cancel
Save