mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-10-26 08:36:12 +01:00
feat: store topic tags in topic hash (#9656)
* feat: store topic tags in topic hash breaking: remove color info from tags (use css) * fix: remove unused tag modal * fix: tag search
This commit is contained in:
committed by
GitHub
parent
0d3f74b762
commit
4a56388ec3
@@ -2,7 +2,6 @@
|
|||||||
"none": "Your forum does not have any topics with tags yet.",
|
"none": "Your forum does not have any topics with tags yet.",
|
||||||
"bg-color": "Background Colour",
|
"bg-color": "Background Colour",
|
||||||
"text-color": "Text Colour",
|
"text-color": "Text Colour",
|
||||||
"create-modify": "Create & Modify Tags",
|
|
||||||
"description": "Select tags by clicking or dragging, use <code>CTRL</code> to select multiple tags.",
|
"description": "Select tags by clicking or dragging, use <code>CTRL</code> to select multiple tags.",
|
||||||
"create": "Create Tag",
|
"create": "Create Tag",
|
||||||
"modify": "Modify Tags",
|
"modify": "Modify Tags",
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ define('admin/manage/tags', [
|
|||||||
|
|
||||||
handleCreate();
|
handleCreate();
|
||||||
handleSearch();
|
handleSearch();
|
||||||
handleModify();
|
|
||||||
handleRename();
|
handleRename();
|
||||||
handleDeleteSelected();
|
handleDeleteSelected();
|
||||||
};
|
};
|
||||||
@@ -82,54 +81,6 @@ define('admin/manage/tags', [
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleModify() {
|
|
||||||
$('#modify').on('click', function () {
|
|
||||||
var tagsToModify = $('.tag-row.ui-selected');
|
|
||||||
if (!tagsToModify.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var firstTag = $(tagsToModify[0]);
|
|
||||||
bootbox.dialog({
|
|
||||||
title: '[[admin/manage/tags:alerts.editing]]',
|
|
||||||
message: firstTag.find('.tag-modal').html(),
|
|
||||||
buttons: {
|
|
||||||
success: {
|
|
||||||
label: 'Save',
|
|
||||||
className: 'btn-primary save',
|
|
||||||
callback: function () {
|
|
||||||
var modal = $('.bootbox');
|
|
||||||
var resetColors = modal.find('#reset-colors').is(':checked');
|
|
||||||
var bgColor = resetColors ? '' : modal.find('[data-name="bgColor"]').val();
|
|
||||||
var color = resetColors ? '' : modal.find('[data-name="color"]').val();
|
|
||||||
|
|
||||||
var data = [];
|
|
||||||
tagsToModify.each(function (idx, tag) {
|
|
||||||
tag = $(tag);
|
|
||||||
data.push({
|
|
||||||
value: tag.attr('data-tag'),
|
|
||||||
color: color,
|
|
||||||
bgColor: bgColor,
|
|
||||||
});
|
|
||||||
|
|
||||||
tag.find('[data-name="bgColor"]').val(bgColor);
|
|
||||||
tag.find('[data-name="color"]').val(color);
|
|
||||||
tag.find('.tag-item').css('background-color', bgColor).css('color', color);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.emit('admin.tags.update', data, function (err) {
|
|
||||||
if (err) {
|
|
||||||
return app.alertError(err.message);
|
|
||||||
}
|
|
||||||
app.alertSuccess('[[admin/manage/tags:alerts.update-success]]');
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleRename() {
|
function handleRename() {
|
||||||
$('#rename').on('click', function () {
|
$('#rename').on('click', function () {
|
||||||
var tagsToModify = $('.tag-row.ui-selected');
|
var tagsToModify = $('.tag-row.ui-selected');
|
||||||
|
|||||||
@@ -89,8 +89,11 @@ Topics.addTags = async (req, res) => {
|
|||||||
if (!await privileges.topics.canEdit(req.params.tid, req.user.uid)) {
|
if (!await privileges.topics.canEdit(req.params.tid, req.user.uid)) {
|
||||||
return helpers.formatApiResponse(403, res);
|
return helpers.formatApiResponse(403, res);
|
||||||
}
|
}
|
||||||
|
const cid = await topics.getTopicField(req.params.tid, 'cid');
|
||||||
|
await topics.validateTags(req.body.tags, cid, req.user.uid, req.params.tid);
|
||||||
|
const tags = await topics.filterTags(req.body.tags);
|
||||||
|
|
||||||
await topics.createTags(req.body.tags, req.params.tid, Date.now());
|
await topics.addTags(tags, [req.params.tid]);
|
||||||
helpers.formatApiResponse(200, res);
|
helpers.formatApiResponse(200, res);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -93,7 +93,6 @@ privsAdmin.socketMap = {
|
|||||||
'admin.user.invite': 'admin:users',
|
'admin.user.invite': 'admin:users',
|
||||||
|
|
||||||
'admin.tags.create': 'admin:tags',
|
'admin.tags.create': 'admin:tags',
|
||||||
'admin.tags.update': 'admin:tags',
|
|
||||||
'admin.tags.rename': 'admin:tags',
|
'admin.tags.rename': 'admin:tags',
|
||||||
'admin.tags.deleteTags': 'admin:tags',
|
'admin.tags.deleteTags': 'admin:tags',
|
||||||
|
|
||||||
|
|||||||
@@ -155,18 +155,15 @@ async function getUsers(uids, data) {
|
|||||||
async function getTopics(tids, data) {
|
async function getTopics(tids, data) {
|
||||||
const topicsData = await topics.getTopicsData(tids);
|
const topicsData = await topics.getTopicsData(tids);
|
||||||
const cids = _.uniq(topicsData.map(topic => topic && topic.cid));
|
const cids = _.uniq(topicsData.map(topic => topic && topic.cid));
|
||||||
const [categories, tags] = await Promise.all([
|
const categories = await getCategories(cids, data);
|
||||||
getCategories(cids, data),
|
|
||||||
getTags(tids, data),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const cidToCategory = _.zipObject(cids, categories);
|
const cidToCategory = _.zipObject(cids, categories);
|
||||||
topicsData.forEach((topic, index) => {
|
topicsData.forEach((topic) => {
|
||||||
if (topic && categories && cidToCategory[topic.cid]) {
|
if (topic && categories && cidToCategory[topic.cid]) {
|
||||||
topic.category = cidToCategory[topic.cid];
|
topic.category = cidToCategory[topic.cid];
|
||||||
}
|
}
|
||||||
if (topic && tags && tags[index]) {
|
if (topic && topic.tags) {
|
||||||
topic.tags = tags[index];
|
topic.tags = topic.tags.map(tag => tag.value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -186,13 +183,6 @@ async function getCategories(cids, data) {
|
|||||||
return await db.getObjectsFields(cids.map(cid => `category:${cid}`), categoryFields);
|
return await db.getObjectsFields(cids.map(cid => `category:${cid}`), categoryFields);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getTags(tids, data) {
|
|
||||||
if (Array.isArray(data.hasTags) && data.hasTags.length) {
|
|
||||||
return await topics.getTopicsTags(tids);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function filterByPostcount(posts, postCount, repliesFilter) {
|
function filterByPostcount(posts, postCount, repliesFilter) {
|
||||||
postCount = parseInt(postCount, 10);
|
postCount = parseInt(postCount, 10);
|
||||||
if (postCount) {
|
if (postCount) {
|
||||||
|
|||||||
@@ -12,14 +12,6 @@ Tags.create = async function (socket, data) {
|
|||||||
await topics.createEmptyTag(data.tag);
|
await topics.createEmptyTag(data.tag);
|
||||||
};
|
};
|
||||||
|
|
||||||
Tags.update = async function (socket, data) {
|
|
||||||
if (!Array.isArray(data)) {
|
|
||||||
throw new Error('[[error:invalid-data]]');
|
|
||||||
}
|
|
||||||
|
|
||||||
await topics.updateTags(data);
|
|
||||||
};
|
|
||||||
|
|
||||||
Tags.rename = async function (socket, data) {
|
Tags.rename = async function (socket, data) {
|
||||||
if (!Array.isArray(data)) {
|
if (!Array.isArray(data)) {
|
||||||
throw new Error('[[error:invalid-data]]');
|
throw new Error('[[error:invalid-data]]');
|
||||||
|
|||||||
@@ -34,6 +34,11 @@ module.exports = function (Topics) {
|
|||||||
postcount: 0,
|
postcount: 0,
|
||||||
viewcount: 0,
|
viewcount: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (Array.isArray(data.tags) && data.tags.length) {
|
||||||
|
topicData.tags = data.tags.join(',');
|
||||||
|
}
|
||||||
|
|
||||||
const result = await plugins.hooks.fire('filter:topic.create', { topic: topicData, data: data });
|
const result = await plugins.hooks.fire('filter:topic.create', { topic: topicData, data: data });
|
||||||
topicData = result.topic;
|
topicData = result.topic;
|
||||||
await db.setObject(`topic:${topicData.tid}`, topicData);
|
await db.setObject(`topic:${topicData.tid}`, topicData);
|
||||||
@@ -79,6 +84,7 @@ module.exports = function (Topics) {
|
|||||||
}
|
}
|
||||||
Topics.checkTitle(data.title);
|
Topics.checkTitle(data.title);
|
||||||
await Topics.validateTags(data.tags, data.cid, uid);
|
await Topics.validateTags(data.tags, data.cid, uid);
|
||||||
|
data.tags = await Topics.filterTags(data.tags, data.cid);
|
||||||
Topics.checkContent(data.content);
|
Topics.checkContent(data.content);
|
||||||
|
|
||||||
const [categoryExists, canCreate, canTag] = await Promise.all([
|
const [categoryExists, canCreate, canTag] = await Promise.all([
|
||||||
|
|||||||
@@ -126,4 +126,12 @@ function modifyTopic(topic, fields) {
|
|||||||
if (fields.includes('teaserPid') || !fields.length) {
|
if (fields.includes('teaserPid') || !fields.length) {
|
||||||
topic.teaserPid = topic.teaserPid || null;
|
topic.teaserPid = topic.teaserPid || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (fields.includes('tags') || !fields.length) {
|
||||||
|
const tags = String(topic.tags || '');
|
||||||
|
topic.tags = tags.split(',').filter(Boolean).map(tag => ({
|
||||||
|
value: tag,
|
||||||
|
valueEscaped: validator.escape(String(tag)),
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,9 +116,8 @@ Topics.getTopicsByTids = async function (tids, options) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const [result, tags, hasRead, isIgnored, bookmarks, callerSettings] = await Promise.all([
|
const [result, hasRead, isIgnored, bookmarks, callerSettings] = await Promise.all([
|
||||||
loadTopics(),
|
loadTopics(),
|
||||||
Topics.getTopicsTagsObjects(tids),
|
|
||||||
Topics.hasReadTopics(tids, uid),
|
Topics.hasReadTopics(tids, uid),
|
||||||
Topics.isIgnoring(tids, uid),
|
Topics.isIgnoring(tids, uid),
|
||||||
Topics.getUserBookmarks(tids, uid),
|
Topics.getUserBookmarks(tids, uid),
|
||||||
@@ -136,8 +135,6 @@ Topics.getTopicsByTids = async function (tids, options) {
|
|||||||
topic.user.displayname = topic.user.username;
|
topic.user.displayname = topic.user.username;
|
||||||
}
|
}
|
||||||
topic.teaser = result.teasers[i] || null;
|
topic.teaser = result.teasers[i] || null;
|
||||||
topic.tags = tags[i];
|
|
||||||
|
|
||||||
topic.isOwner = topic.uid === parseInt(uid, 10);
|
topic.isOwner = topic.uid === parseInt(uid, 10);
|
||||||
topic.ignored = isIgnored[i];
|
topic.ignored = isIgnored[i];
|
||||||
topic.unread = parseInt(uid, 10) > 0 && !hasRead[i] && !isIgnored[i];
|
topic.unread = parseInt(uid, 10) > 0 && !hasRead[i] && !isIgnored[i];
|
||||||
@@ -180,7 +177,7 @@ Topics.getTopicWithPosts = async function (topicData, set, uid, start, stop, rev
|
|||||||
social.getActivePostSharing(),
|
social.getActivePostSharing(),
|
||||||
getDeleter(topicData),
|
getDeleter(topicData),
|
||||||
getMerger(topicData),
|
getMerger(topicData),
|
||||||
getRelated(topicData, uid),
|
Topics.getRelatedTopics(topicData, uid),
|
||||||
Topics.thumbs.load([topicData]),
|
Topics.thumbs.load([topicData]),
|
||||||
Topics.events.get(topicData.tid, uid),
|
Topics.events.get(topicData.tid, uid),
|
||||||
]);
|
]);
|
||||||
@@ -268,12 +265,6 @@ async function getMerger(topicData) {
|
|||||||
return merger;
|
return merger;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getRelated(topicData, uid) {
|
|
||||||
const tags = await Topics.getTopicTagsObjects(topicData.tid);
|
|
||||||
topicData.tags = tags;
|
|
||||||
return await Topics.getRelatedTopics(topicData, uid);
|
|
||||||
}
|
|
||||||
|
|
||||||
Topics.getMainPost = async function (tid, uid) {
|
Topics.getMainPost = async function (tid, uid) {
|
||||||
const mainPosts = await Topics.getMainPosts([tid], uid);
|
const mainPosts = await Topics.getMainPosts([tid], uid);
|
||||||
return Array.isArray(mainPosts) && mainPosts.length ? mainPosts[0] : null;
|
return Array.isArray(mainPosts) && mainPosts.length ? mainPosts[0] : null;
|
||||||
|
|||||||
@@ -19,23 +19,23 @@ module.exports = function (Topics) {
|
|||||||
if (!Array.isArray(tags) || !tags.length) {
|
if (!Array.isArray(tags) || !tags.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const result = await plugins.hooks.fire('filter:tags.filter', { tags: tags, tid: tid });
|
|
||||||
tags = _.uniq(result.tags)
|
|
||||||
.map(tag => utils.cleanUpTag(tag, meta.config.maximumTagLength))
|
|
||||||
.filter(tag => tag && tag.length >= (meta.config.minimumTagLength || 3));
|
|
||||||
|
|
||||||
tags = await filterCategoryTags(tags, tid);
|
|
||||||
const cid = await Topics.getTopicField(tid, 'cid');
|
const cid = await Topics.getTopicField(tid, 'cid');
|
||||||
const topicSets = tags.map(tag => `tag:${tag}:topics`).concat(
|
const topicSets = tags.map(tag => `tag:${tag}:topics`).concat(
|
||||||
tags.map(tag => `cid:${cid}:tag:${tag}:topics`)
|
tags.map(tag => `cid:${cid}:tag:${tag}:topics`)
|
||||||
);
|
);
|
||||||
await Promise.all([
|
await db.sortedSetsAdd(topicSets, timestamp, tid);
|
||||||
db.setAdd(`topic:${tid}:tags`, tags),
|
|
||||||
db.sortedSetsAdd(topicSets, timestamp, tid),
|
|
||||||
]);
|
|
||||||
cache.del(`topic:${tid}:tags`);
|
|
||||||
await Topics.updateCategoryTagsCount([cid], tags);
|
await Topics.updateCategoryTagsCount([cid], tags);
|
||||||
await Promise.all(tags.map(tag => updateTagCount(tag)));
|
await Promise.all(tags.map(updateTagCount));
|
||||||
|
};
|
||||||
|
|
||||||
|
Topics.filterTags = async function (tags, cid) {
|
||||||
|
const result = await plugins.hooks.fire('filter:tags.filter', { tags: tags, cid: cid });
|
||||||
|
tags = _.uniq(result.tags)
|
||||||
|
.map(tag => utils.cleanUpTag(tag, meta.config.maximumTagLength))
|
||||||
|
.filter(tag => tag && tag.length >= (meta.config.minimumTagLength || 3));
|
||||||
|
|
||||||
|
return await filterCategoryTags(tags, cid);
|
||||||
};
|
};
|
||||||
|
|
||||||
Topics.updateCategoryTagsCount = async function (cids, tags) {
|
Topics.updateCategoryTagsCount = async function (cids, tags) {
|
||||||
@@ -92,8 +92,7 @@ module.exports = function (Topics) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
async function filterCategoryTags(tags, tid) {
|
async function filterCategoryTags(tags, cid) {
|
||||||
const cid = await Topics.getTopicField(tid, 'cid');
|
|
||||||
const tagWhitelist = await categories.getTagWhitelist([cid]);
|
const tagWhitelist = await categories.getTagWhitelist([cid]);
|
||||||
if (!Array.isArray(tagWhitelist[0]) || !tagWhitelist[0].length) {
|
if (!Array.isArray(tagWhitelist[0]) || !tagWhitelist[0].length) {
|
||||||
return tags;
|
return tags;
|
||||||
@@ -116,15 +115,6 @@ module.exports = function (Topics) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Topics.updateTags = async function (data) {
|
|
||||||
await async.eachSeries(data, async (tagData) => {
|
|
||||||
await db.setObject(`tag:${tagData.value}`, {
|
|
||||||
color: tagData.color,
|
|
||||||
bgColor: tagData.bgColor,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
Topics.renameTags = async function (data) {
|
Topics.renameTags = async function (data) {
|
||||||
await async.eachSeries(data, async (tagData) => {
|
await async.eachSeries(data, async (tagData) => {
|
||||||
await renameTag(tagData.value, tagData.newName);
|
await renameTag(tagData.value, tagData.newName);
|
||||||
@@ -136,19 +126,12 @@ module.exports = function (Topics) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
newTagName = utils.cleanUpTag(newTagName, meta.config.maximumTagLength);
|
newTagName = utils.cleanUpTag(newTagName, meta.config.maximumTagLength);
|
||||||
const targetExists = await db.isSortedSetMember('tags:topic:count', newTagName);
|
|
||||||
await Topics.createEmptyTag(newTagName);
|
await Topics.createEmptyTag(newTagName);
|
||||||
const allCids = {};
|
const allCids = {};
|
||||||
const tagData = await db.getObject(`tag:${tag}`);
|
|
||||||
if (tagData && !targetExists) {
|
|
||||||
await db.setObject(`tag:${newTagName}`, {
|
|
||||||
color: tagData.color,
|
|
||||||
bgColor: tagData.bgColor,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await batch.processSortedSet(`tag:${tag}:topics`, async (tids) => {
|
await batch.processSortedSet(`tag:${tag}:topics`, async (tids) => {
|
||||||
const topicData = await Topics.getTopicsFields(tids, ['tid', 'cid']);
|
const topicData = await Topics.getTopicsFields(tids, ['tid', 'cid', 'tags']);
|
||||||
const cids = topicData.map(t => t.cid);
|
const cids = topicData.map(t => t.cid);
|
||||||
topicData.forEach((t) => { allCids[t.cid] = true; });
|
topicData.forEach((t) => { allCids[t.cid] = true; });
|
||||||
const scores = await db.sortedSetScores(`tag:${tag}:topics`, tids);
|
const scores = await db.sortedSetScores(`tag:${tag}:topics`, tids);
|
||||||
@@ -162,11 +145,18 @@ module.exports = function (Topics) {
|
|||||||
));
|
));
|
||||||
await db.sortedSetRemove(cids.map(cid => `cid:${cid}:tag:${tag}:topics`), tids);
|
await db.sortedSetRemove(cids.map(cid => `cid:${cid}:tag:${tag}:topics`), tids);
|
||||||
|
|
||||||
// update topic:<tid>:tags
|
// update 'tags' field in topic hash
|
||||||
const keys = tids.map(tid => `topic:${tid}:tags`);
|
topicData.forEach((topic) => {
|
||||||
await db.setsRemove(keys, tag);
|
topic.tags = topic.tags.map(tagItem => tagItem.value);
|
||||||
await db.setsAdd(keys, newTagName);
|
const index = topic.tags.indexOf(tag);
|
||||||
cache.del(keys);
|
if (index !== -1) {
|
||||||
|
topic.tags.splice(index, 1, newTagName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await db.setObjectBulk(
|
||||||
|
topicData.map(t => `topic:${t.tid}`),
|
||||||
|
topicData.map(t => ({ tags: t.tags.join(',') }))
|
||||||
|
);
|
||||||
}, {});
|
}, {});
|
||||||
await Topics.deleteTag(tag);
|
await Topics.deleteTag(tag);
|
||||||
await updateTagCount(newTagName);
|
await updateTagCount(newTagName);
|
||||||
@@ -235,9 +225,11 @@ module.exports = function (Topics) {
|
|||||||
if (!tids.length) {
|
if (!tids.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const keys = tids.map(tid => `topic:${tid}:tags`);
|
|
||||||
await db.setsRemove(keys, tag);
|
await db.deleteObjectFields(
|
||||||
cache.del(keys);
|
tids.map(tid => `topic:${tid}`),
|
||||||
|
['tags'],
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,11 +283,8 @@ module.exports = function (Topics) {
|
|||||||
if (!tags.length) {
|
if (!tags.length) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const tagData = await db.getObjects(tags.map(tag => `tag:${tag.value}`));
|
tags.forEach((tag) => {
|
||||||
tags.forEach((tag, index) => {
|
|
||||||
tag.valueEscaped = validator.escape(String(tag.value));
|
tag.valueEscaped = validator.escape(String(tag.value));
|
||||||
tag.color = tagData[index] ? tagData[index].color : '';
|
|
||||||
tag.bgColor = tagData[index] ? tagData[index].bgColor : '';
|
|
||||||
});
|
});
|
||||||
return tags;
|
return tags;
|
||||||
};
|
};
|
||||||
@@ -306,24 +295,8 @@ module.exports = function (Topics) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Topics.getTopicsTags = async function (tids) {
|
Topics.getTopicsTags = async function (tids) {
|
||||||
const cachedData = {};
|
const topicTagData = await Topics.getTopicsFields(tids, ['tags']);
|
||||||
const uncachedKeys = cache.getUnCachedKeys(
|
return tids.map((tid, i) => topicTagData[i].tags.map(tagData => tagData.value));
|
||||||
tids.map(tid => `topic:${tid}:tags`),
|
|
||||||
cachedData
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!uncachedKeys.length) {
|
|
||||||
return tids.map(tid => cachedData[`topic:${tid}:tags`].slice());
|
|
||||||
}
|
|
||||||
|
|
||||||
const tagData = await db.getSetsMembers(
|
|
||||||
uncachedKeys,
|
|
||||||
);
|
|
||||||
uncachedKeys.forEach((uncachedKey, index) => {
|
|
||||||
cachedData[uncachedKey] = tagData[index];
|
|
||||||
cache.set(uncachedKey, tagData[index]);
|
|
||||||
});
|
|
||||||
return tids.map(tid => cachedData[`topic:${tid}:tags`].slice());
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Topics.getTopicTagsObjects = async function (tid) {
|
Topics.getTopicTagsObjects = async function (tid) {
|
||||||
@@ -349,65 +322,80 @@ module.exports = function (Topics) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Topics.addTags = async function (tags, tids) {
|
Topics.addTags = async function (tags, tids) {
|
||||||
const topicData = await Topics.getTopicsFields(tids, ['tid', 'cid', 'timestamp']);
|
const topicData = await Topics.getTopicsFields(tids, ['tid', 'cid', 'timestamp', 'tags']);
|
||||||
const sets = tids.map(tid => `topic:${tid}:tags`);
|
const bulkAdd = [];
|
||||||
for (let i = 0; i < tags.length; i++) {
|
const bulkSet = [];
|
||||||
/* eslint-disable no-await-in-loop */
|
topicData.forEach((t) => {
|
||||||
const bulkAdd = [];
|
const topicTags = t.tags.map(tagItem => tagItem.value);
|
||||||
topicData.forEach((t) => {
|
tags.forEach((tag) => {
|
||||||
bulkAdd.push([`tag:${tags[i]}:topics`, t.timestamp, t.tid]);
|
bulkAdd.push([`tag:${tag}:topics`, t.timestamp, t.tid]);
|
||||||
bulkAdd.push([`cid:${t.cid}:tag:${tags[i]}:topics`, t.timestamp, t.tid]);
|
bulkAdd.push([`cid:${t.cid}:tag:${tag}:topics`, t.timestamp, t.tid]);
|
||||||
|
if (!topicTags.includes(tag)) {
|
||||||
|
topicTags.push(tag);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
await Promise.all([
|
bulkSet.push({ tags: topicTags.join(',') });
|
||||||
db.setsAdd(sets, tags[i]),
|
});
|
||||||
db.sortedSetAddBulk(bulkAdd),
|
await Promise.all([
|
||||||
]);
|
db.sortedSetAddBulk(bulkAdd),
|
||||||
await updateTagCount(tags[i]);
|
db.setObjectBulk(
|
||||||
}
|
topicData.map(t => `topic:${t.tid}`),
|
||||||
|
bulkSet,
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await Promise.all(tags.map(updateTagCount));
|
||||||
await Topics.updateCategoryTagsCount(_.uniq(topicData.map(t => t.cid)), tags);
|
await Topics.updateCategoryTagsCount(_.uniq(topicData.map(t => t.cid)), tags);
|
||||||
cache.del(sets);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Topics.removeTags = async function (tags, tids) {
|
Topics.removeTags = async function (tags, tids) {
|
||||||
const topicData = await Topics.getTopicsFields(tids, ['tid', 'cid']);
|
const topicData = await Topics.getTopicsFields(tids, ['tid', 'cid', 'tags']);
|
||||||
const sets = tids.map(tid => `topic:${tid}:tags`);
|
const bulkRemove = [];
|
||||||
for (let i = 0; i < tags.length; i++) {
|
const bulkSet = [];
|
||||||
/* eslint-disable no-await-in-loop */
|
|
||||||
const bulkRemove = [];
|
topicData.forEach((t) => {
|
||||||
topicData.forEach((t) => {
|
const topicTags = t.tags.map(tagItem => tagItem.value);
|
||||||
bulkRemove.push([`tag:${tags[i]}:topics`, t.tid]);
|
tags.forEach((tag) => {
|
||||||
bulkRemove.push([`cid:${t.cid}:tag:${tags[i]}:topics`, t.tid]);
|
bulkRemove.push([`tag:${tag}:topics`, t.tid]);
|
||||||
|
bulkRemove.push([`cid:${t.cid}:tag:${tag}:topics`, t.tid]);
|
||||||
|
if (topicTags.includes(tag)) {
|
||||||
|
topicTags.splice(topicTags.indexOf(tag), 1);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
await Promise.all([
|
bulkSet.push({ tags: topicTags.join(',') });
|
||||||
db.setsRemove(sets, tags[i]),
|
});
|
||||||
db.sortedSetRemoveBulk(bulkRemove),
|
await Promise.all([
|
||||||
]);
|
db.sortedSetRemoveBulk(bulkRemove),
|
||||||
await updateTagCount(tags[i]);
|
db.setObjectBulk(
|
||||||
}
|
topicData.map(t => `topic:${t.tid}`),
|
||||||
|
bulkSet,
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await Promise.all(tags.map(updateTagCount));
|
||||||
await Topics.updateCategoryTagsCount(_.uniq(topicData.map(t => t.cid)), tags);
|
await Topics.updateCategoryTagsCount(_.uniq(topicData.map(t => t.cid)), tags);
|
||||||
cache.del(sets);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Topics.updateTopicTags = async function (tid, tags) {
|
Topics.updateTopicTags = async function (tid, tags) {
|
||||||
await Topics.deleteTopicTags(tid);
|
await Topics.deleteTopicTags(tid);
|
||||||
const timestamp = await Topics.getTopicField(tid, 'timestamp');
|
const cid = await Topics.getTopicField(tid, 'cid');
|
||||||
await Topics.createTags(tags, tid, timestamp);
|
|
||||||
|
tags = await Topics.filterTags(tags, cid);
|
||||||
|
await Topics.addTags(tags, [tid]);
|
||||||
};
|
};
|
||||||
|
|
||||||
Topics.deleteTopicTags = async function (tid) {
|
Topics.deleteTopicTags = async function (tid) {
|
||||||
const [tags, cid] = await Promise.all([
|
const topicData = await Topics.getTopicFields(tid, ['cid', 'tags']);
|
||||||
Topics.getTopicTags(tid),
|
const { cid } = topicData;
|
||||||
Topics.getTopicField(tid, 'cid'),
|
const tags = topicData.tags.map(tagItem => tagItem.value);
|
||||||
]);
|
await db.deleteObjectField(`topic:${tid}`, 'tags');
|
||||||
await db.delete(`topic:${tid}:tags`);
|
|
||||||
cache.del(`topic:${tid}:tags`);
|
|
||||||
|
|
||||||
const sets = tags.map(tag => `tag:${tag}:topics`)
|
const sets = tags.map(tag => `tag:${tag}:topics`)
|
||||||
.concat(tags.map(tag => `cid:${cid}:tag:${tag}:topics`));
|
.concat(tags.map(tag => `cid:${cid}:tag:${tag}:topics`));
|
||||||
await db.sortedSetsRemove(sets, tid);
|
await db.sortedSetsRemove(sets, tid);
|
||||||
|
|
||||||
await Topics.updateCategoryTagsCount([cid], tags);
|
await Topics.updateCategoryTagsCount([cid], tags);
|
||||||
await Promise.all(tags.map(tag => updateTagCount(tag)));
|
await Promise.all(tags.map(updateTagCount));
|
||||||
};
|
};
|
||||||
|
|
||||||
Topics.searchTags = async function (data) {
|
Topics.searchTags = async function (data) {
|
||||||
|
|||||||
@@ -11,10 +11,16 @@ module.exports = {
|
|||||||
method: async function () {
|
method: async function () {
|
||||||
const { progress } = this;
|
const { progress } = this;
|
||||||
|
|
||||||
|
async function getTopicsTags(tids) {
|
||||||
|
return await db.getSetsMembers(
|
||||||
|
tids.map(tid => `topic:${tid}:tags`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
await batch.processSortedSet('topics:tid', async (tids) => {
|
await batch.processSortedSet('topics:tid', async (tids) => {
|
||||||
const [topicData, tags] = await Promise.all([
|
const [topicData, tags] = await Promise.all([
|
||||||
topics.getTopicsFields(tids, ['tid', 'cid', 'timestamp']),
|
topics.getTopicsFields(tids, ['tid', 'cid', 'timestamp']),
|
||||||
topics.getTopicsTags(tids),
|
getTopicsTags(tids),
|
||||||
]);
|
]);
|
||||||
const topicsWithTags = topicData.map((t, i) => {
|
const topicsWithTags = topicData.map((t, i) => {
|
||||||
t.tags = tags[i];
|
t.tags = tags[i];
|
||||||
|
|||||||
37
src/upgrades/1.18.0/topic_tags_refactor.js
Normal file
37
src/upgrades/1.18.0/topic_tags_refactor.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const db = require('../../database');
|
||||||
|
const batch = require('../../batch');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
name: 'Store tags in topic hash',
|
||||||
|
timestamp: Date.UTC(2021, 8, 9),
|
||||||
|
method: async function () {
|
||||||
|
const { progress } = this;
|
||||||
|
|
||||||
|
async function getTopicsTags(tids) {
|
||||||
|
return await db.getSetsMembers(
|
||||||
|
tids.map(tid => `topic:${tid}:tags`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await batch.processSortedSet('topics:tid', async (tids) => {
|
||||||
|
const tags = await getTopicsTags(tids);
|
||||||
|
|
||||||
|
const topicsWithTags = tids.map((tid, i) => {
|
||||||
|
const topic = { tid: tid };
|
||||||
|
topic.tags = tags[i];
|
||||||
|
return topic;
|
||||||
|
}).filter(t => t && t.tags.length);
|
||||||
|
|
||||||
|
await db.setObjectBulk(
|
||||||
|
topicsWithTags.map(t => `topic:${t.tid}`),
|
||||||
|
topicsWithTags.map(t => ({ tags: t.tags.join(',') }))
|
||||||
|
);
|
||||||
|
progress.incr(tids.length);
|
||||||
|
}, {
|
||||||
|
batch: 500,
|
||||||
|
progress: progress,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -3,7 +3,6 @@
|
|||||||
<div class="panel panel-default tag-management">
|
<div class="panel panel-default tag-management">
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<div class="alert alert-info">
|
<div class="alert alert-info">
|
||||||
<strong>[[admin/manage/tags:create-modify]]</strong>
|
|
||||||
<p>[[admin/manage/tags:description]]</p>
|
<p>[[admin/manage/tags:description]]</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -22,21 +21,6 @@
|
|||||||
<span class="mdl-chip__text">{tags.valueEscaped}</span>
|
<span class="mdl-chip__text">{tags.valueEscaped}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="tag-modal hidden">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="bgColor">[[admin/manage/tags:bg-color]]</label>
|
|
||||||
<input type="color" id="bgColor" placeholder="#ffffff" data-name="bgColor" value="{tags.bgColor}" class="form-control category_bgColor" />
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="color">[[admin/manage/tags:text-color]]</label>
|
|
||||||
<input type="color" id="color" placeholder="#a2a2a2" data-name="color" value="{tags.color}" class="form-control category_color" />
|
|
||||||
</div>
|
|
||||||
<div class="checkbox">
|
|
||||||
<label>
|
|
||||||
<input id="reset-colors" type="checkbox"> <strong>[[admin/manage/tags:reset-colors]]</strong>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<!-- END tags -->
|
<!-- END tags -->
|
||||||
</div>
|
</div>
|
||||||
@@ -48,7 +32,6 @@
|
|||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<button class="btn btn-primary btn-block" id="create">[[admin/manage/tags:create]]</button>
|
<button class="btn btn-primary btn-block" id="create">[[admin/manage/tags:create]]</button>
|
||||||
<button class="btn btn-primary btn-block" id="modify">[[admin/manage/tags:modify]]</button>
|
|
||||||
<button class="btn btn-primary btn-block" id="rename">[[admin/manage/tags:rename]]</button>
|
<button class="btn btn-primary btn-block" id="rename">[[admin/manage/tags:rename]]</button>
|
||||||
<button class="btn btn-warning btn-block" id="deleteSelected">[[admin/manage/tags:delete]]</button>
|
<button class="btn btn-warning btn-block" id="deleteSelected">[[admin/manage/tags:delete]]</button>
|
||||||
<hr />
|
<hr />
|
||||||
|
|||||||
101
test/topics.js
101
test/topics.js
@@ -1824,9 +1824,9 @@ describe('Topic\'s', () => {
|
|||||||
assert.equal(data.matchCount, 3);
|
assert.equal(data.matchCount, 3);
|
||||||
assert.equal(data.pageCount, 1);
|
assert.equal(data.pageCount, 1);
|
||||||
const tagData = [
|
const tagData = [
|
||||||
{ value: 'nodebb', valueEscaped: 'nodebb', color: '', bgColor: '', score: 3 },
|
{ value: 'nodebb', valueEscaped: 'nodebb', score: 3 },
|
||||||
{ value: 'nodejs', valueEscaped: 'nodejs', color: '', bgColor: '', score: 1 },
|
{ value: 'nodejs', valueEscaped: 'nodejs', score: 1 },
|
||||||
{ value: 'nosql', valueEscaped: 'nosql', color: '', bgColor: '', score: 1 },
|
{ value: 'nosql', valueEscaped: 'nosql', score: 1 },
|
||||||
];
|
];
|
||||||
assert.deepEqual(data.tags, tagData);
|
assert.deepEqual(data.tags, tagData);
|
||||||
|
|
||||||
@@ -1893,66 +1893,24 @@ describe('Topic\'s', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should error if data is invalid', (done) => {
|
|
||||||
socketAdmin.tags.update({ uid: adminUid }, null, (err) => {
|
|
||||||
assert.equal(err.message, '[[error:invalid-data]]');
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should error if data is not an array', (done) => {
|
it('should rename tags', async () => {
|
||||||
socketAdmin.tags.update({ uid: adminUid }, {
|
const result1 = await topics.post({ uid: adminUid, tags: ['plugins'], title: 'topic tagged with plugins', content: 'topic 1 content', cid: topic.categoryId });
|
||||||
bgColor: '#ff0000',
|
const result2 = await topics.post({ uid: adminUid, tags: ['plugin'], title: 'topic tagged with plugin', content: 'topic 2 content', cid: topic.categoryId });
|
||||||
color: '#00ff00',
|
const data1 = await topics.getTopicData(result2.topicData.tid);
|
||||||
}, (err) => {
|
|
||||||
assert.equal(err.message, '[[error:invalid-data]]');
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should update tag', (done) => {
|
await socketAdmin.tags.rename({ uid: adminUid }, [{
|
||||||
socketAdmin.tags.update({ uid: adminUid }, [{
|
value: 'plugin',
|
||||||
value: 'emptytag',
|
newName: 'plugins',
|
||||||
bgColor: '#ff0000',
|
}]);
|
||||||
color: '#00ff00',
|
|
||||||
}], (err) => {
|
|
||||||
assert.ifError(err);
|
|
||||||
db.getObject('tag:emptytag', (err, data) => {
|
|
||||||
assert.ifError(err);
|
|
||||||
assert.equal(data.bgColor, '#ff0000');
|
|
||||||
assert.equal(data.color, '#00ff00');
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should rename tags', (done) => {
|
const tids = await topics.getTagTids('plugins', 0, -1);
|
||||||
async.series({
|
assert.strictEqual(tids.length, 2);
|
||||||
topic1: function (next) {
|
const tags = await topics.getTopicTags(result2.topicData.tid);
|
||||||
topics.post({ uid: adminUid, tags: ['plugins'], title: 'topic tagged with plugins', content: 'topic 1 content', cid: topic.categoryId }, next);
|
|
||||||
},
|
const data = await topics.getTopicData(result2.topicData.tid);
|
||||||
topic2: function (next) {
|
assert.strictEqual(tags.length, 1);
|
||||||
topics.post({ uid: adminUid, tags: ['plugin'], title: 'topic tagged with plugin', content: 'topic 2 content', cid: topic.categoryId }, next);
|
assert.strictEqual(tags[0], 'plugins');
|
||||||
},
|
|
||||||
}, (err, result) => {
|
|
||||||
assert.ifError(err);
|
|
||||||
socketAdmin.tags.rename({ uid: adminUid }, [{
|
|
||||||
value: 'plugin',
|
|
||||||
newName: 'plugins',
|
|
||||||
}], (err) => {
|
|
||||||
assert.ifError(err);
|
|
||||||
topics.getTagTids('plugins', 0, -1, (err, tids) => {
|
|
||||||
assert.ifError(err);
|
|
||||||
assert.equal(tids.length, 2);
|
|
||||||
topics.getTopicTags(result.topic2.topicData.tid, (err, tags) => {
|
|
||||||
assert.ifError(err);
|
|
||||||
assert.equal(tags.length, 1);
|
|
||||||
assert.equal(tags[0], 'plugins');
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return related topics', (done) => {
|
it('should return related topics', (done) => {
|
||||||
@@ -2108,18 +2066,17 @@ describe('Topic\'s', () => {
|
|||||||
await topics.post({ uid: adminUid, tags: ['cattag1'], title: title, content: 'topic 1 content', cid: cid });
|
await topics.post({ uid: adminUid, tags: ['cattag1'], title: title, content: 'topic 1 content', cid: cid });
|
||||||
let result = await topics.getCategoryTagsData(cid, 0, -1);
|
let result = await topics.getCategoryTagsData(cid, 0, -1);
|
||||||
assert.deepStrictEqual(result, [
|
assert.deepStrictEqual(result, [
|
||||||
{ value: 'cattag1', score: 3, bgColor: '', color: '', valueEscaped: 'cattag1' },
|
{ value: 'cattag1', score: 3, valueEscaped: 'cattag1' },
|
||||||
{ value: 'cattag2', score: 2, bgColor: '', color: '', valueEscaped: 'cattag2' },
|
{ value: 'cattag2', score: 2, valueEscaped: 'cattag2' },
|
||||||
{ value: 'cattag3', score: 1, bgColor: '', color: '', valueEscaped: 'cattag3' },
|
{ value: 'cattag3', score: 1, valueEscaped: 'cattag3' },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// after purging values should update properly
|
// after purging values should update properly
|
||||||
await topics.purge(postResult.topicData.tid, adminUid);
|
await topics.purge(postResult.topicData.tid, adminUid);
|
||||||
result = await topics.getCategoryTagsData(cid, 0, -1);
|
result = await topics.getCategoryTagsData(cid, 0, -1);
|
||||||
|
|
||||||
assert.deepStrictEqual(result, [
|
assert.deepStrictEqual(result, [
|
||||||
{ value: 'cattag1', score: 2, bgColor: '', color: '', valueEscaped: 'cattag1' },
|
{ value: 'cattag1', score: 2, valueEscaped: 'cattag1' },
|
||||||
{ value: 'cattag2', score: 1, bgColor: '', color: '', valueEscaped: 'cattag2' },
|
{ value: 'cattag2', score: 1, valueEscaped: 'cattag2' },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2138,11 +2095,11 @@ describe('Topic\'s', () => {
|
|||||||
let result1 = await topics.getCategoryTagsData(cid1, 0, -1);
|
let result1 = await topics.getCategoryTagsData(cid1, 0, -1);
|
||||||
let result2 = await topics.getCategoryTagsData(cid2, 0, -1);
|
let result2 = await topics.getCategoryTagsData(cid2, 0, -1);
|
||||||
assert.deepStrictEqual(result1, [
|
assert.deepStrictEqual(result1, [
|
||||||
{ value: 'movedtag1', score: 2, bgColor: '', color: '', valueEscaped: 'movedtag1' },
|
{ value: 'movedtag1', score: 2, valueEscaped: 'movedtag1' },
|
||||||
{ value: 'movedtag2', score: 1, bgColor: '', color: '', valueEscaped: 'movedtag2' },
|
{ value: 'movedtag2', score: 1, valueEscaped: 'movedtag2' },
|
||||||
]);
|
]);
|
||||||
assert.deepStrictEqual(result2, [
|
assert.deepStrictEqual(result2, [
|
||||||
{ value: 'movedtag2', score: 1, bgColor: '', color: '', valueEscaped: 'movedtag2' },
|
{ value: 'movedtag2', score: 1, valueEscaped: 'movedtag2' },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// after moving values should update properly
|
// after moving values should update properly
|
||||||
@@ -2151,11 +2108,11 @@ describe('Topic\'s', () => {
|
|||||||
result1 = await topics.getCategoryTagsData(cid1, 0, -1);
|
result1 = await topics.getCategoryTagsData(cid1, 0, -1);
|
||||||
result2 = await topics.getCategoryTagsData(cid2, 0, -1);
|
result2 = await topics.getCategoryTagsData(cid2, 0, -1);
|
||||||
assert.deepStrictEqual(result1, [
|
assert.deepStrictEqual(result1, [
|
||||||
{ value: 'movedtag1', score: 1, bgColor: '', color: '', valueEscaped: 'movedtag1' },
|
{ value: 'movedtag1', score: 1, valueEscaped: 'movedtag1' },
|
||||||
]);
|
]);
|
||||||
assert.deepStrictEqual(result2, [
|
assert.deepStrictEqual(result2, [
|
||||||
{ value: 'movedtag2', score: 2, bgColor: '', color: '', valueEscaped: 'movedtag2' },
|
{ value: 'movedtag2', score: 2, valueEscaped: 'movedtag2' },
|
||||||
{ value: 'movedtag1', score: 1, bgColor: '', color: '', valueEscaped: 'movedtag1' },
|
{ value: 'movedtag1', score: 1, valueEscaped: 'movedtag1' },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user