mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-11-02 12:05:57 +01:00
closes #5273
This commit is contained in:
@@ -8,44 +8,9 @@ define('admin/manage/category', [
|
||||
'autocomplete'
|
||||
], function (uploader, iconSelect, colorpicker, autocomplete) {
|
||||
var Category = {};
|
||||
var modified_categories = {};
|
||||
|
||||
Category.init = function () {
|
||||
var modified_categories = {};
|
||||
|
||||
function modified(el) {
|
||||
var cid = $(el).parents('form').attr('data-cid');
|
||||
|
||||
if (cid) {
|
||||
modified_categories[cid] = modified_categories[cid] || {};
|
||||
modified_categories[cid][$(el).attr('data-name')] = $(el).val();
|
||||
|
||||
app.flags = app.flags || {};
|
||||
app.flags._unsaved = true;
|
||||
}
|
||||
}
|
||||
|
||||
function save(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if(Object.keys(modified_categories).length) {
|
||||
socket.emit('admin.categories.update', modified_categories, function (err, result) {
|
||||
if (err) {
|
||||
return app.alertError(err.message);
|
||||
}
|
||||
|
||||
if (result && result.length) {
|
||||
app.flags._unsaved = false;
|
||||
app.alert({
|
||||
title: 'Updated Categories',
|
||||
message: 'Category IDs ' + result.join(', ') + ' was successfully updated.',
|
||||
type: 'success',
|
||||
timeout: 2000
|
||||
});
|
||||
}
|
||||
});
|
||||
modified_categories = {};
|
||||
}
|
||||
}
|
||||
|
||||
$('.blockclass, form.category select').each(function () {
|
||||
var $this = $(this);
|
||||
@@ -85,7 +50,28 @@ define('admin/manage/category', [
|
||||
|
||||
$('[data-name="bgColor"], [data-name="color"]').each(enableColorPicker);
|
||||
|
||||
$('#save').on('click', save);
|
||||
$('#save').on('click', function () {
|
||||
if (Object.keys(modified_categories).length) {
|
||||
socket.emit('admin.categories.update', modified_categories, function (err, result) {
|
||||
if (err) {
|
||||
return app.alertError(err.message);
|
||||
}
|
||||
|
||||
if (result && result.length) {
|
||||
app.flags._unsaved = false;
|
||||
app.alert({
|
||||
title: 'Updated Categories',
|
||||
message: 'Category IDs ' + result.join(', ') + ' was successfully updated.',
|
||||
type: 'success',
|
||||
timeout: 2000
|
||||
});
|
||||
}
|
||||
});
|
||||
modified_categories = {};
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
$('.purge').on('click', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -171,8 +157,37 @@ define('admin/manage/category', [
|
||||
});
|
||||
|
||||
Category.setupPrivilegeTable();
|
||||
|
||||
handleTags();
|
||||
};
|
||||
|
||||
function modified(el) {
|
||||
var cid = ajaxify.data.category.cid;
|
||||
|
||||
if (cid) {
|
||||
modified_categories[cid] = modified_categories[cid] || {};
|
||||
modified_categories[cid][$(el).attr('data-name')] = $(el).val();
|
||||
|
||||
app.flags = app.flags || {};
|
||||
app.flags._unsaved = true;
|
||||
}
|
||||
}
|
||||
|
||||
function handleTags() {
|
||||
var tagEl = $('#tag-whitelist');
|
||||
tagEl.tagsinput({
|
||||
confirmKeys: [13, 44],
|
||||
trimValue: true
|
||||
});
|
||||
|
||||
ajaxify.data.category.tagWhitelist.forEach(function (tag) {
|
||||
tagEl.tagsinput('add', tag);
|
||||
});
|
||||
tagEl.on('itemAdded itemRemoved', function (event) {
|
||||
modified(tagEl);
|
||||
});
|
||||
}
|
||||
|
||||
Category.setupPrivilegeTable = function () {
|
||||
$('.privilege-table-container').on('change', 'input[type="checkbox"]', function () {
|
||||
var checkboxEl = $(this),
|
||||
|
||||
@@ -101,7 +101,7 @@
|
||||
},
|
||||
|
||||
cleanUpTag: function (tag, maxLength) {
|
||||
if (typeof tag !== 'string' || !tag.length ) {
|
||||
if (typeof tag !== 'string' || !tag.length) {
|
||||
return '';
|
||||
}
|
||||
|
||||
|
||||
@@ -141,6 +141,9 @@ var privileges = require('./privileges');
|
||||
parents: function (next) {
|
||||
Categories.getParents(cids, next);
|
||||
},
|
||||
tagWhitelist: function (next) {
|
||||
Categories.getTagWhitelist(cids, next);
|
||||
},
|
||||
hasRead: function (next) {
|
||||
Categories.hasReadCategories(cids, uid, next);
|
||||
}
|
||||
@@ -149,22 +152,28 @@ var privileges = require('./privileges');
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
var categories = results.categories;
|
||||
var hasRead = results.hasRead;
|
||||
uid = parseInt(uid, 10);
|
||||
for(var i = 0; i < results.categories.length; ++i) {
|
||||
if (categories[i]) {
|
||||
categories[i]['unread-class'] = (parseInt(categories[i].topic_count, 10) === 0 || (hasRead[i] && uid !== 0)) ? '' : 'unread';
|
||||
categories[i].children = results.children[i];
|
||||
categories[i].parent = results.parents[i] || undefined;
|
||||
calculateTopicPostCount(categories[i]);
|
||||
results.categories.forEach(function (category, i) {
|
||||
if (category) {
|
||||
category['unread-class'] = (parseInt(category.topic_count, 10) === 0 || (results.hasRead[i] && uid !== 0)) ? '' : 'unread';
|
||||
category.children = results.children[i];
|
||||
category.parent = results.parents[i] || undefined;
|
||||
category.tagWhitelist = results.tagWhitelist[i];
|
||||
calculateTopicPostCount(category);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
callback(null, categories);
|
||||
callback(null, results.categories);
|
||||
});
|
||||
};
|
||||
|
||||
Categories.getTagWhitelist = function (cids, callback) {
|
||||
var keys = cids.map(function (cid) {
|
||||
return 'cid:' + cid + ':tag:whitelist';
|
||||
});
|
||||
db.getSortedSetsMembers(keys, callback);
|
||||
};
|
||||
|
||||
function calculateTopicPostCount(category) {
|
||||
if (!category) {
|
||||
return;
|
||||
|
||||
@@ -54,6 +54,7 @@ module.exports = function (Categories) {
|
||||
'cid:' + cid + ':read_by_uid',
|
||||
'cid:' + cid + ':ignorers',
|
||||
'cid:' + cid + ':children',
|
||||
'cid:' + cid + ':tag:whitelist',
|
||||
'category:' + cid
|
||||
], next);
|
||||
},
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
var db = require('../database');
|
||||
var meta = require('../meta');
|
||||
var utils = require('../../public/src/utils');
|
||||
var translator = require('../../public/src/modules/translator');
|
||||
var plugins = require('../plugins');
|
||||
@@ -66,6 +68,8 @@ module.exports = function (Categories) {
|
||||
function updateCategoryField(cid, key, value, callback) {
|
||||
if (key === 'parentCid') {
|
||||
return updateParent(cid, value, callback);
|
||||
} else if (key === 'tagWhitelist') {
|
||||
return updateTagWhitelist(cid, value, callback);
|
||||
}
|
||||
|
||||
async.waterfall([
|
||||
@@ -112,6 +116,25 @@ module.exports = function (Categories) {
|
||||
});
|
||||
}
|
||||
|
||||
function updateTagWhitelist(cid, tags, callback) {
|
||||
tags = tags.split(',');
|
||||
tags = tags.map(function (tag) {
|
||||
return utils.cleanUpTag(tag, meta.config.maximumTagLength);
|
||||
}).filter(Boolean);
|
||||
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
db.delete('cid:' + cid + ':tag:whitelist', next);
|
||||
},
|
||||
function (next) {
|
||||
var scores = tags.map(function (tag, index) {
|
||||
return index;
|
||||
});
|
||||
db.sortedSetAdd('cid:' + cid + ':tag:whitelist', scores, tags, next);
|
||||
}
|
||||
], callback);
|
||||
}
|
||||
|
||||
function updateOrder(cid, order, callback) {
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
|
||||
@@ -78,6 +78,7 @@ module.exports = function (Meta) {
|
||||
acpSource += '\n@import "..' + path.sep + 'public/less/generics.less";\n';
|
||||
acpSource += '\n@import (inline) "..' + path.sep + 'public/vendor/colorpicker/colorpicker.css";\n';
|
||||
acpSource += '\n@import (inline) "..' + path.sep + 'public/vendor/jquery/css/smoothness/jquery-ui.css";';
|
||||
acpSource += '\n@import (inline) "..' + path.sep + 'public/vendor/jquery/bootstrap-tagsinput/bootstrap-tagsinput.css";';
|
||||
|
||||
minify(acpSource, paths, 'acpCache', callback);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,29 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
var db = require('../../database');
|
||||
var topics = require('../../topics');
|
||||
var utils = require('../../../public/src/utils');
|
||||
|
||||
module.exports = function (SocketTopics) {
|
||||
|
||||
SocketTopics.isTagAllowed = function (socket, data, callback) {
|
||||
if (!data || !data.cid || !data.tag) {
|
||||
return callback(new Error('[[error:invalid-data]]'));
|
||||
}
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
db.getSortedSetRange('cid:' + data.cid + ':tag:whitelist', 0, -1, next);
|
||||
},
|
||||
function (tagWhitelist, next) {
|
||||
if (!tagWhitelist.length) {
|
||||
return next(null, true);
|
||||
}
|
||||
next(null, tagWhitelist.indexOf(data.tag) !== -1);
|
||||
}
|
||||
], callback);
|
||||
};
|
||||
|
||||
SocketTopics.autocompleteTags = function (socket, data, callback) {
|
||||
topics.autocompleteTags(data, callback);
|
||||
};
|
||||
|
||||
@@ -31,6 +31,10 @@ module.exports = function (Topics) {
|
||||
return tag && tag.length >= (meta.config.minimumTagLength || 3) && array.indexOf(tag) === index;
|
||||
});
|
||||
|
||||
filterCategoryTags(tags, tid, next);
|
||||
},
|
||||
function (_tags, next) {
|
||||
tags = _tags;
|
||||
var keys = tags.map(function (tag) {
|
||||
return 'tag:' + tag + ':topics';
|
||||
});
|
||||
@@ -39,15 +43,35 @@ module.exports = function (Topics) {
|
||||
async.apply(db.setAdd, 'topic:' + tid + ':tags', tags),
|
||||
async.apply(db.sortedSetsAdd, keys, timestamp, tid)
|
||||
], function (err) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
async.each(tags, updateTagCount, next);
|
||||
next(err);
|
||||
});
|
||||
},
|
||||
function (next) {
|
||||
async.each(tags, updateTagCount, next);
|
||||
}
|
||||
], callback);
|
||||
};
|
||||
|
||||
function filterCategoryTags(tags, tid, callback) {
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
Topics.getTopicField(tid, 'cid', next);
|
||||
},
|
||||
function (cid, next) {
|
||||
db.getSortedSetRange('cid:' + cid + ':tag:whitelist', 0, -1, next);
|
||||
},
|
||||
function (tagWhitelist, next) {
|
||||
if (!tagWhitelist.length) {
|
||||
return next(null, tags);
|
||||
}
|
||||
tags = tags.filter(function (tag) {
|
||||
return tagWhitelist.indexOf(tag) !== -1;
|
||||
});
|
||||
next(null, tags);
|
||||
}
|
||||
], callback);
|
||||
}
|
||||
|
||||
Topics.createEmptyTag = function (tag, callback) {
|
||||
if (!tag) {
|
||||
return callback(new Error('[[error:invalid-tag]]'));
|
||||
@@ -273,7 +297,7 @@ module.exports = function (Topics) {
|
||||
if (plugins.hasListeners('filter:topics.searchTags')) {
|
||||
plugins.fireHook('filter:topics.searchTags', {data: data}, next);
|
||||
} else {
|
||||
findMatches(data.query, next);
|
||||
findMatches(data.query, 0, next);
|
||||
}
|
||||
},
|
||||
function (result, next) {
|
||||
@@ -295,7 +319,7 @@ module.exports = function (Topics) {
|
||||
if (plugins.hasListeners('filter:topics.autocompleteTags')) {
|
||||
plugins.fireHook('filter:topics.autocompleteTags', {data: data}, next);
|
||||
} else {
|
||||
findMatches(data.query, next);
|
||||
findMatches(data.query, data.cid, next);
|
||||
}
|
||||
},
|
||||
function (result, next) {
|
||||
@@ -304,10 +328,21 @@ module.exports = function (Topics) {
|
||||
], callback);
|
||||
};
|
||||
|
||||
function findMatches(query, callback) {
|
||||
function findMatches(query, cid, callback) {
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
db.getSortedSetRevRange('tags:topic:count', 0, -1, next);
|
||||
if (parseInt(cid, 10)) {
|
||||
db.getSortedSetRange('cid:' + cid + ':tag:whitelist', 0, -1, next);
|
||||
} else {
|
||||
setImmediate(next, null, []);
|
||||
}
|
||||
},
|
||||
function (tagWhitelist, next) {
|
||||
if (tagWhitelist.length) {
|
||||
setImmediate(next, null, tagWhitelist);
|
||||
} else {
|
||||
db.getSortedSetRevRange('tags:topic:count', 0, -1, next);
|
||||
}
|
||||
},
|
||||
function (tags, next) {
|
||||
query = query.toLowerCase();
|
||||
|
||||
@@ -59,6 +59,10 @@
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<label for="tag-whitelist">Tag Whitelist</label><br />
|
||||
<input id="tag-whitelist" type="text" class="form-control" placeholder="Enter category tags here" data-name="tagWhitelist" value="" />
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -541,7 +541,80 @@ describe('Categories', function () {
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('tag whitelist', function () {
|
||||
var cid;
|
||||
var socketTopics = require('../src/socket.io/topics');
|
||||
before(function (done) {
|
||||
Categories.create({
|
||||
name: 'test'
|
||||
}, function (err, category) {
|
||||
assert.ifError(err);
|
||||
cid = category.cid;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should error if data is invalid', function (done) {
|
||||
socketTopics.isTagAllowed({uid: posterUid}, null, function (err) {
|
||||
assert.equal(err.message, '[[error:invalid-data]]');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return true if category whitelist is empty', function (done) {
|
||||
socketTopics.isTagAllowed({uid: posterUid}, {tag: 'notallowed', cid: cid}, function (err, allowed) {
|
||||
assert.ifError(err);
|
||||
assert(allowed);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should add tags to category whitelist', function (done) {
|
||||
var data = {};
|
||||
data[cid] = {
|
||||
tagWhitelist: 'nodebb,jquery,javascript'
|
||||
};
|
||||
Categories.update(data, function (err) {
|
||||
assert.ifError(err);
|
||||
db.getSortedSetRange('cid:' + cid + ':tag:whitelist', 0, -1, function (err, tagWhitelist) {
|
||||
assert.ifError(err);
|
||||
assert.deepEqual(['nodebb', 'jquery', 'javascript'], tagWhitelist);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should return false if category whitelist does not have tag', function (done) {
|
||||
socketTopics.isTagAllowed({uid: posterUid}, {tag: 'notallowed', cid: cid}, function (err, allowed) {
|
||||
assert.ifError(err);
|
||||
assert(!allowed);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return true if category whitelist has tag', function (done) {
|
||||
socketTopics.isTagAllowed({uid: posterUid}, {tag: 'nodebb', cid: cid}, function (err, allowed) {
|
||||
assert.ifError(err);
|
||||
assert(allowed);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should post a topic with only allowed tags', function (done) {
|
||||
Topics.post({
|
||||
uid: posterUid,
|
||||
cid: cid,
|
||||
title: 'Test Topic Title',
|
||||
content: 'The content of test topic',
|
||||
tags: ['nodebb', 'jquery', 'notallowed']
|
||||
}, function (err, data) {
|
||||
assert.ifError(err);
|
||||
assert.equal(data.topicData.tags.length, 2);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user