mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-12-21 16:00:26 +01:00
closes #5273
This commit is contained in:
@@ -8,44 +8,9 @@ define('admin/manage/category', [
|
|||||||
'autocomplete'
|
'autocomplete'
|
||||||
], function (uploader, iconSelect, colorpicker, autocomplete) {
|
], function (uploader, iconSelect, colorpicker, autocomplete) {
|
||||||
var Category = {};
|
var Category = {};
|
||||||
|
var modified_categories = {};
|
||||||
|
|
||||||
Category.init = function () {
|
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 () {
|
$('.blockclass, form.category select').each(function () {
|
||||||
var $this = $(this);
|
var $this = $(this);
|
||||||
@@ -85,7 +50,28 @@ define('admin/manage/category', [
|
|||||||
|
|
||||||
$('[data-name="bgColor"], [data-name="color"]').each(enableColorPicker);
|
$('[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) {
|
$('.purge').on('click', function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
@@ -171,8 +157,37 @@ define('admin/manage/category', [
|
|||||||
});
|
});
|
||||||
|
|
||||||
Category.setupPrivilegeTable();
|
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 () {
|
Category.setupPrivilegeTable = function () {
|
||||||
$('.privilege-table-container').on('change', 'input[type="checkbox"]', function () {
|
$('.privilege-table-container').on('change', 'input[type="checkbox"]', function () {
|
||||||
var checkboxEl = $(this),
|
var checkboxEl = $(this),
|
||||||
|
|||||||
@@ -101,7 +101,7 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
cleanUpTag: function (tag, maxLength) {
|
cleanUpTag: function (tag, maxLength) {
|
||||||
if (typeof tag !== 'string' || !tag.length ) {
|
if (typeof tag !== 'string' || !tag.length) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -141,6 +141,9 @@ var privileges = require('./privileges');
|
|||||||
parents: function (next) {
|
parents: function (next) {
|
||||||
Categories.getParents(cids, next);
|
Categories.getParents(cids, next);
|
||||||
},
|
},
|
||||||
|
tagWhitelist: function (next) {
|
||||||
|
Categories.getTagWhitelist(cids, next);
|
||||||
|
},
|
||||||
hasRead: function (next) {
|
hasRead: function (next) {
|
||||||
Categories.hasReadCategories(cids, uid, next);
|
Categories.hasReadCategories(cids, uid, next);
|
||||||
}
|
}
|
||||||
@@ -149,22 +152,28 @@ var privileges = require('./privileges');
|
|||||||
return callback(err);
|
return callback(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
var categories = results.categories;
|
|
||||||
var hasRead = results.hasRead;
|
|
||||||
uid = parseInt(uid, 10);
|
uid = parseInt(uid, 10);
|
||||||
for(var i = 0; i < results.categories.length; ++i) {
|
results.categories.forEach(function (category, i) {
|
||||||
if (categories[i]) {
|
if (category) {
|
||||||
categories[i]['unread-class'] = (parseInt(categories[i].topic_count, 10) === 0 || (hasRead[i] && uid !== 0)) ? '' : 'unread';
|
category['unread-class'] = (parseInt(category.topic_count, 10) === 0 || (results.hasRead[i] && uid !== 0)) ? '' : 'unread';
|
||||||
categories[i].children = results.children[i];
|
category.children = results.children[i];
|
||||||
categories[i].parent = results.parents[i] || undefined;
|
category.parent = results.parents[i] || undefined;
|
||||||
calculateTopicPostCount(categories[i]);
|
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) {
|
function calculateTopicPostCount(category) {
|
||||||
if (!category) {
|
if (!category) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ module.exports = function (Categories) {
|
|||||||
'cid:' + cid + ':read_by_uid',
|
'cid:' + cid + ':read_by_uid',
|
||||||
'cid:' + cid + ':ignorers',
|
'cid:' + cid + ':ignorers',
|
||||||
'cid:' + cid + ':children',
|
'cid:' + cid + ':children',
|
||||||
|
'cid:' + cid + ':tag:whitelist',
|
||||||
'category:' + cid
|
'category:' + cid
|
||||||
], next);
|
], next);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var async = require('async');
|
var async = require('async');
|
||||||
|
|
||||||
var db = require('../database');
|
var db = require('../database');
|
||||||
|
var meta = require('../meta');
|
||||||
var utils = require('../../public/src/utils');
|
var utils = require('../../public/src/utils');
|
||||||
var translator = require('../../public/src/modules/translator');
|
var translator = require('../../public/src/modules/translator');
|
||||||
var plugins = require('../plugins');
|
var plugins = require('../plugins');
|
||||||
@@ -66,6 +68,8 @@ module.exports = function (Categories) {
|
|||||||
function updateCategoryField(cid, key, value, callback) {
|
function updateCategoryField(cid, key, value, callback) {
|
||||||
if (key === 'parentCid') {
|
if (key === 'parentCid') {
|
||||||
return updateParent(cid, value, callback);
|
return updateParent(cid, value, callback);
|
||||||
|
} else if (key === 'tagWhitelist') {
|
||||||
|
return updateTagWhitelist(cid, value, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
async.waterfall([
|
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) {
|
function updateOrder(cid, order, callback) {
|
||||||
async.waterfall([
|
async.waterfall([
|
||||||
function (next) {
|
function (next) {
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ module.exports = function (Meta) {
|
|||||||
acpSource += '\n@import "..' + path.sep + 'public/less/generics.less";\n';
|
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/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/css/smoothness/jquery-ui.css";';
|
||||||
|
acpSource += '\n@import (inline) "..' + path.sep + 'public/vendor/jquery/bootstrap-tagsinput/bootstrap-tagsinput.css";';
|
||||||
|
|
||||||
minify(acpSource, paths, 'acpCache', callback);
|
minify(acpSource, paths, 'acpCache', callback);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,29 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var async = require('async');
|
var async = require('async');
|
||||||
|
var db = require('../../database');
|
||||||
var topics = require('../../topics');
|
var topics = require('../../topics');
|
||||||
var utils = require('../../../public/src/utils');
|
var utils = require('../../../public/src/utils');
|
||||||
|
|
||||||
module.exports = function (SocketTopics) {
|
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) {
|
SocketTopics.autocompleteTags = function (socket, data, callback) {
|
||||||
topics.autocompleteTags(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;
|
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) {
|
var keys = tags.map(function (tag) {
|
||||||
return 'tag:' + tag + ':topics';
|
return 'tag:' + tag + ':topics';
|
||||||
});
|
});
|
||||||
@@ -39,15 +43,35 @@ module.exports = function (Topics) {
|
|||||||
async.apply(db.setAdd, 'topic:' + tid + ':tags', tags),
|
async.apply(db.setAdd, 'topic:' + tid + ':tags', tags),
|
||||||
async.apply(db.sortedSetsAdd, keys, timestamp, tid)
|
async.apply(db.sortedSetsAdd, keys, timestamp, tid)
|
||||||
], function (err) {
|
], function (err) {
|
||||||
if (err) {
|
next(err);
|
||||||
return next(err);
|
|
||||||
}
|
|
||||||
async.each(tags, updateTagCount, next);
|
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
function (next) {
|
||||||
|
async.each(tags, updateTagCount, next);
|
||||||
}
|
}
|
||||||
], callback);
|
], 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) {
|
Topics.createEmptyTag = function (tag, callback) {
|
||||||
if (!tag) {
|
if (!tag) {
|
||||||
return callback(new Error('[[error:invalid-tag]]'));
|
return callback(new Error('[[error:invalid-tag]]'));
|
||||||
@@ -273,7 +297,7 @@ module.exports = function (Topics) {
|
|||||||
if (plugins.hasListeners('filter:topics.searchTags')) {
|
if (plugins.hasListeners('filter:topics.searchTags')) {
|
||||||
plugins.fireHook('filter:topics.searchTags', {data: data}, next);
|
plugins.fireHook('filter:topics.searchTags', {data: data}, next);
|
||||||
} else {
|
} else {
|
||||||
findMatches(data.query, next);
|
findMatches(data.query, 0, next);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
function (result, next) {
|
function (result, next) {
|
||||||
@@ -295,7 +319,7 @@ module.exports = function (Topics) {
|
|||||||
if (plugins.hasListeners('filter:topics.autocompleteTags')) {
|
if (plugins.hasListeners('filter:topics.autocompleteTags')) {
|
||||||
plugins.fireHook('filter:topics.autocompleteTags', {data: data}, next);
|
plugins.fireHook('filter:topics.autocompleteTags', {data: data}, next);
|
||||||
} else {
|
} else {
|
||||||
findMatches(data.query, next);
|
findMatches(data.query, data.cid, next);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
function (result, next) {
|
function (result, next) {
|
||||||
@@ -304,10 +328,21 @@ module.exports = function (Topics) {
|
|||||||
], callback);
|
], callback);
|
||||||
};
|
};
|
||||||
|
|
||||||
function findMatches(query, callback) {
|
function findMatches(query, cid, callback) {
|
||||||
async.waterfall([
|
async.waterfall([
|
||||||
function (next) {
|
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) {
|
function (tags, next) {
|
||||||
query = query.toLowerCase();
|
query = query.toLowerCase();
|
||||||
|
|||||||
@@ -59,6 +59,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</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>
|
||||||
</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