mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-11-16 18:56:15 +01:00
Category tags (#8938)
* feat: wip category tags * fix: tag search * feat: remove debug * fix: returns of searchTags and autocomplete * fix: alpha sort * fix: redis * fix: delete zsets on category purge, fix another test * fix: test
This commit is contained in:
committed by
GitHub
parent
792e9e703e
commit
d2888d1d1f
@@ -28,6 +28,7 @@ module.exports = function (Categories) {
|
|||||||
async function purgeCategory(cid) {
|
async function purgeCategory(cid) {
|
||||||
await db.sortedSetRemove('categories:cid', cid);
|
await db.sortedSetRemove('categories:cid', cid);
|
||||||
await removeFromParent(cid);
|
await removeFromParent(cid);
|
||||||
|
await deleteTags(cid);
|
||||||
await db.deleteAll([
|
await db.deleteAll([
|
||||||
'cid:' + cid + ':tids',
|
'cid:' + cid + ':tids',
|
||||||
'cid:' + cid + ':tids:pinned',
|
'cid:' + cid + ':tids:pinned',
|
||||||
@@ -71,4 +72,10 @@ module.exports = function (Categories) {
|
|||||||
'cid:' + cid + ':tag:whitelist',
|
'cid:' + cid + ':tag:whitelist',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function deleteTags(cid) {
|
||||||
|
const tags = await db.getSortedSetMembers('cid:' + cid + ':tags');
|
||||||
|
await db.deleteAll(tags.map(tag => 'cid:' + cid + ':tag:' + tag + ':topics'));
|
||||||
|
await db.delete('cid:' + cid + ':tags');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -24,13 +24,17 @@ tagsController.getTag = async function (req, res) {
|
|||||||
breadcrumbs: helpers.buildBreadcrumbs([{ text: '[[tags:tags]]', url: '/tags' }, { text: tag }]),
|
breadcrumbs: helpers.buildBreadcrumbs([{ text: '[[tags:tags]]', url: '/tags' }, { text: tag }]),
|
||||||
title: '[[pages:tag, ' + tag + ']]',
|
title: '[[pages:tag, ' + tag + ']]',
|
||||||
};
|
};
|
||||||
const settings = await user.getSettings(req.uid);
|
const [settings, cids] = await Promise.all([
|
||||||
|
user.getSettings(req.uid),
|
||||||
|
categories.getCidsByPrivilege('categories:cid', req.uid, 'topics:read'),
|
||||||
|
]);
|
||||||
const start = Math.max(0, (page - 1) * settings.topicsPerPage);
|
const start = Math.max(0, (page - 1) * settings.topicsPerPage);
|
||||||
const stop = start + settings.topicsPerPage - 1;
|
const stop = start + settings.topicsPerPage - 1;
|
||||||
const states = [categories.watchStates.watching, categories.watchStates.notwatching, categories.watchStates.ignoring];
|
const states = [categories.watchStates.watching, categories.watchStates.notwatching, categories.watchStates.ignoring];
|
||||||
|
|
||||||
const [topicCount, tids, categoriesData] = await Promise.all([
|
const [topicCount, tids, categoriesData] = await Promise.all([
|
||||||
topics.getTagTopicCount(tag),
|
topics.getTagTopicCount(tag, cids),
|
||||||
topics.getTagTids(tag, start, stop),
|
topics.getTagTidsByCids(tag, cids, start, stop),
|
||||||
helpers.getCategoriesByStates(req.uid, '', states),
|
helpers.getCategoriesByStates(req.uid, '', states),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -59,9 +63,10 @@ tagsController.getTag = async function (req, res) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
tagsController.getTags = async function (req, res) {
|
tagsController.getTags = async function (req, res) {
|
||||||
|
const cids = await categories.getCidsByPrivilege('categories:cid', req.uid, 'topics:read');
|
||||||
const [canSearch, tags] = await Promise.all([
|
const [canSearch, tags] = await Promise.all([
|
||||||
privileges.global.can('search:tags', req.uid),
|
privileges.global.can('search:tags', req.uid),
|
||||||
topics.getTags(0, 99),
|
topics.getCategoryTagsData(cids, 0, 99),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
res.render('tags', {
|
res.render('tags', {
|
||||||
|
|||||||
@@ -16,11 +16,20 @@ module.exports = function (SocketTopics) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
SocketTopics.autocompleteTags = async function (socket, data) {
|
SocketTopics.autocompleteTags = async function (socket, data) {
|
||||||
return await topics.autocompleteTags(data);
|
if (data.cid) {
|
||||||
|
const canRead = await privileges.categories.can('topics:read', data.cid, socket.uid);
|
||||||
|
if (!canRead) {
|
||||||
|
throw new Error('[[error:no-privileges]]');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data.cids = await categories.getCidsByPrivilege('categories:cid', socket.uid, 'topics:read');
|
||||||
|
const result = await topics.autocompleteTags(data);
|
||||||
|
return result.map(tag => tag.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
SocketTopics.searchTags = async function (socket, data) {
|
SocketTopics.searchTags = async function (socket, data) {
|
||||||
return await searchTags(socket.uid, topics.searchTags, data);
|
const result = await searchTags(socket.uid, topics.searchTags, data);
|
||||||
|
return result.map(tag => tag.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
SocketTopics.searchAndLoadTags = async function (socket, data) {
|
SocketTopics.searchAndLoadTags = async function (socket, data) {
|
||||||
@@ -32,6 +41,13 @@ module.exports = function (SocketTopics) {
|
|||||||
if (!allowed) {
|
if (!allowed) {
|
||||||
throw new Error('[[error:no-privileges]]');
|
throw new Error('[[error:no-privileges]]');
|
||||||
}
|
}
|
||||||
|
if (data.cid) {
|
||||||
|
const canRead = await privileges.categories.can('topics:read', data.cid, uid);
|
||||||
|
if (!canRead) {
|
||||||
|
throw new Error('[[error:no-privileges]]');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data.cids = await categories.getCidsByPrivilege('categories:cid', uid, 'topics:read');
|
||||||
return await method(data);
|
return await method(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,8 +58,8 @@ module.exports = function (SocketTopics) {
|
|||||||
|
|
||||||
const start = parseInt(data.after, 10);
|
const start = parseInt(data.after, 10);
|
||||||
const stop = start + 99;
|
const stop = start + 99;
|
||||||
const tags = await topics.getTags(start, stop);
|
const cids = await categories.getCidsByPrivilege('categories:cid', socket.uid, 'topics:read');
|
||||||
|
const tags = await topics.getCategoryTagsData(cids, start, stop);
|
||||||
return { tags: tags.filter(Boolean), nextStart: stop + 1 };
|
return { tags: tags.filter(Boolean), nextStart: stop + 1 };
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -24,14 +24,42 @@ module.exports = function (Topics) {
|
|||||||
.filter(tag => tag && tag.length >= (meta.config.minimumTagLength || 3));
|
.filter(tag => tag && tag.length >= (meta.config.minimumTagLength || 3));
|
||||||
|
|
||||||
tags = await filterCategoryTags(tags, tid);
|
tags = await filterCategoryTags(tags, tid);
|
||||||
|
const cid = await Topics.getTopicField(tid, 'cid');
|
||||||
|
const topicSets = tags.map(tag => 'tag:' + tag + ':topics').concat(
|
||||||
|
tags.map(tag => 'cid:' + cid + ':tag:' + tag + ':topics')
|
||||||
|
);
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
db.setAdd('topic:' + tid + ':tags', tags),
|
db.setAdd('topic:' + tid + ':tags', tags),
|
||||||
db.sortedSetsAdd(tags.map(tag => 'tag:' + tag + ':topics'), timestamp, tid),
|
db.sortedSetsAdd(topicSets, timestamp, tid),
|
||||||
]);
|
]);
|
||||||
|
await Topics.updateCategoryTagsCount([cid], tags);
|
||||||
await Promise.all(tags.map(tag => updateTagCount(tag)));
|
await Promise.all(tags.map(tag => updateTagCount(tag)));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Topics.updateCategoryTagsCount = async function (cids, tags) {
|
||||||
|
await Promise.all(cids.map(async (cid) => {
|
||||||
|
const counts = await db.sortedSetsCard(
|
||||||
|
tags.map(tag => 'cid:' + cid + ':tag:' + tag + ':topics')
|
||||||
|
);
|
||||||
|
const set = 'cid:' + cid + ':tags';
|
||||||
|
|
||||||
|
const bulkAdd = tags.filter((tag, index) => counts[index] > 0)
|
||||||
|
.map((tag, index) => [set, counts[index], tag]);
|
||||||
|
|
||||||
|
const bulkRemove = tags.filter((tag, index) => counts[index] <= 0)
|
||||||
|
.map(tag => [set, tag]);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
db.sortedSetAddBulk(bulkAdd),
|
||||||
|
db.sortedSetRemoveBulk(bulkRemove),
|
||||||
|
]);
|
||||||
|
}));
|
||||||
|
|
||||||
|
await db.sortedSetsRemoveRangeByScore(
|
||||||
|
cids.map(cid => 'cid:' + cid + ':tags'), '-inf', 0
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
Topics.validateTags = async function (tags, cid) {
|
Topics.validateTags = async function (tags, cid) {
|
||||||
if (!Array.isArray(tags)) {
|
if (!Array.isArray(tags)) {
|
||||||
throw new Error('[[error:invalid-data]]');
|
throw new Error('[[error:invalid-data]]');
|
||||||
@@ -91,6 +119,7 @@ module.exports = function (Topics) {
|
|||||||
newTagName = utils.cleanUpTag(newTagName, meta.config.maximumTagLength);
|
newTagName = utils.cleanUpTag(newTagName, meta.config.maximumTagLength);
|
||||||
const targetExists = await db.isSortedSetMember('tags:topic:count', newTagName);
|
const targetExists = await db.isSortedSetMember('tags:topic:count', newTagName);
|
||||||
await Topics.createEmptyTag(newTagName);
|
await Topics.createEmptyTag(newTagName);
|
||||||
|
const allCids = {};
|
||||||
const tagData = await db.getObject('tag:' + tag);
|
const tagData = await db.getObject('tag:' + tag);
|
||||||
if (tagData && !targetExists) {
|
if (tagData && !targetExists) {
|
||||||
await db.setObject('tag:' + newTagName, {
|
await db.setObject('tag:' + newTagName, {
|
||||||
@@ -100,15 +129,28 @@ module.exports = function (Topics) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await batch.processSortedSet('tag:' + tag + ':topics', async function (tids) {
|
await batch.processSortedSet('tag:' + tag + ':topics', async function (tids) {
|
||||||
|
const topicData = await Topics.getTopicsFields(tids, ['tid', 'cid']);
|
||||||
|
const cids = topicData.map(t => t.cid);
|
||||||
|
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);
|
||||||
|
// update tag:<tag>:topics
|
||||||
await db.sortedSetAdd('tag:' + newTagName + ':topics', scores, tids);
|
await db.sortedSetAdd('tag:' + newTagName + ':topics', scores, tids);
|
||||||
const keys = tids.map(tid => 'topic:' + tid + ':tags');
|
|
||||||
await db.sortedSetRemove('tag:' + tag + ':topics', tids);
|
await db.sortedSetRemove('tag:' + tag + ':topics', tids);
|
||||||
|
|
||||||
|
// update cid:<cid>:tag:<tag>:topics
|
||||||
|
await db.sortedSetAddBulk(topicData.map(
|
||||||
|
(t, index) => ['cid:' + t.cid + ':tag:' + newTagName + ':topics', scores[index], t.tid]
|
||||||
|
));
|
||||||
|
await db.sortedSetRemove(cids.map(cid => 'cid:' + cid + ':tag:' + tag + ':topics'), tids);
|
||||||
|
|
||||||
|
// update topic:<tid>:tags
|
||||||
|
const keys = tids.map(tid => 'topic:' + tid + ':tags');
|
||||||
await db.setsRemove(keys, tag);
|
await db.setsRemove(keys, tag);
|
||||||
await db.setsAdd(keys, newTagName);
|
await db.setsAdd(keys, newTagName);
|
||||||
}, {});
|
}, {});
|
||||||
await Topics.deleteTag(tag);
|
await Topics.deleteTag(tag);
|
||||||
await updateTagCount(newTagName);
|
await updateTagCount(newTagName);
|
||||||
|
await Topics.updateCategoryTagsCount(Object.keys(allCids), [newTagName]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateTagCount(tag) {
|
async function updateTagCount(tag) {
|
||||||
@@ -123,9 +165,24 @@ module.exports = function (Topics) {
|
|||||||
return payload.tids;
|
return payload.tids;
|
||||||
};
|
};
|
||||||
|
|
||||||
Topics.getTagTopicCount = async function (tag) {
|
Topics.getTagTidsByCids = async function (tag, cids, start, stop) {
|
||||||
const count = await db.sortedSetCard('tag:' + tag + ':topics');
|
const keys = cids.map(cid => 'cid:' + cid + ':tag:' + tag + ':topics');
|
||||||
const payload = await plugins.hooks.fire('filter:topics.getTagTopicCount', { tag, count });
|
const tids = await db.getSortedSetRevRange(keys, start, stop);
|
||||||
|
const payload = await plugins.hooks.fire('filter:topics.getTagTidsByCids', { tag, cids, start, stop, tids });
|
||||||
|
return payload.tids;
|
||||||
|
};
|
||||||
|
|
||||||
|
Topics.getTagTopicCount = async function (tag, cids = []) {
|
||||||
|
let count = 0;
|
||||||
|
if (cids.length) {
|
||||||
|
count = await db.sortedSetsCardSum(
|
||||||
|
cids.map(cid => 'cid:' + cid + ':tag:' + tag + ':topics')
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
count = await db.sortedSetCard('tag:' + tag + ':topics');
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = await plugins.hooks.fire('filter:topics.getTagTopicCount', { tag, count, cids });
|
||||||
return payload.count;
|
return payload.count;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -138,7 +195,18 @@ module.exports = function (Topics) {
|
|||||||
await db.deleteAll(keys);
|
await db.deleteAll(keys);
|
||||||
await db.sortedSetRemove('tags:topic:count', tags);
|
await db.sortedSetRemove('tags:topic:count', tags);
|
||||||
cache.del('tags:topic:count');
|
cache.del('tags:topic:count');
|
||||||
await db.deleteAll(tags.map(tag => 'tag:' + tag));
|
const cids = await categories.getAllCidsFromSet('categories:cid');
|
||||||
|
|
||||||
|
await db.sortedSetRemove(cids.map(cid => 'cid:' + cid + ':tags'), tags);
|
||||||
|
|
||||||
|
const deleteKeys = [];
|
||||||
|
tags.forEach((tag) => {
|
||||||
|
deleteKeys.push('tag:' + tag);
|
||||||
|
cids.forEach((cid) => {
|
||||||
|
deleteKeys.push('cid:' + cid + ':tag:' + tag + ':topics');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await db.deleteAll(deleteKeys);
|
||||||
};
|
};
|
||||||
|
|
||||||
async function removeTagsFromTopics(tags) {
|
async function removeTagsFromTopics(tags) {
|
||||||
@@ -157,12 +225,46 @@ module.exports = function (Topics) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Topics.getTags = async function (start, stop) {
|
Topics.getTags = async function (start, stop) {
|
||||||
const tags = await db.getSortedSetRevRangeWithScores('tags:topic:count', start, stop);
|
return await getFromSet('tags:topic:count', start, stop);
|
||||||
|
};
|
||||||
|
|
||||||
|
Topics.getCategoryTags = async function (cids, start, stop) {
|
||||||
|
if (Array.isArray(cids)) {
|
||||||
|
return await db.getSortedSetRevUnion({
|
||||||
|
sets: cids.map(cid => 'cid:' + cid + ':tags'),
|
||||||
|
start,
|
||||||
|
stop,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return await db.getSortedSetRevRange('cid:' + cids + ':tags', start, stop);
|
||||||
|
};
|
||||||
|
|
||||||
|
Topics.getCategoryTagsData = async function (cids, start, stop) {
|
||||||
|
return await getFromSet(
|
||||||
|
Array.isArray(cids) ? cids.map(cid => 'cid:' + cid + ':tags') : 'cid:' + cids + ':tags',
|
||||||
|
start,
|
||||||
|
stop
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
async function getFromSet(set, start, stop) {
|
||||||
|
let tags;
|
||||||
|
if (Array.isArray(set)) {
|
||||||
|
tags = await db.getSortedSetRevUnion({
|
||||||
|
sets: set,
|
||||||
|
start,
|
||||||
|
stop,
|
||||||
|
withScores: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
tags = await db.getSortedSetRevRangeWithScores(set, start, stop);
|
||||||
|
}
|
||||||
|
|
||||||
const payload = await plugins.hooks.fire('filter:tags.getAll', {
|
const payload = await plugins.hooks.fire('filter:tags.getAll', {
|
||||||
tags: tags,
|
tags: tags,
|
||||||
});
|
});
|
||||||
return await Topics.getTagData(payload.tags);
|
return await Topics.getTagData(payload.tags);
|
||||||
};
|
}
|
||||||
|
|
||||||
Topics.getTagData = async function (tags) {
|
Topics.getTagData = async function (tags) {
|
||||||
if (!tags.length) {
|
if (!tags.length) {
|
||||||
@@ -199,22 +301,13 @@ module.exports = function (Topics) {
|
|||||||
const uniqueTopicTags = _.uniq(_.flatten(topicTags));
|
const uniqueTopicTags = _.uniq(_.flatten(topicTags));
|
||||||
|
|
||||||
const tags = uniqueTopicTags.map(tag => ({ value: tag }));
|
const tags = uniqueTopicTags.map(tag => ({ value: tag }));
|
||||||
|
const tagData = await Topics.getTagData(tags);
|
||||||
const [tagData, counts] = await Promise.all([
|
|
||||||
Topics.getTagData(tags),
|
|
||||||
db.sortedSetScores('tags:topic:count', uniqueTopicTags),
|
|
||||||
]);
|
|
||||||
|
|
||||||
tagData.forEach(function (tag, index) {
|
|
||||||
tag.score = counts[index] ? counts[index] : 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
const tagDataMap = _.zipObject(uniqueTopicTags, tagData);
|
const tagDataMap = _.zipObject(uniqueTopicTags, tagData);
|
||||||
|
|
||||||
topicTags.forEach(function (tags, index) {
|
topicTags.forEach(function (tags, index) {
|
||||||
if (Array.isArray(tags)) {
|
if (Array.isArray(tags)) {
|
||||||
topicTags[index] = tags.map(tag => tagDataMap[tag]);
|
topicTags[index] = tags.map(tag => tagDataMap[tag]);
|
||||||
topicTags[index].sort((tag1, tag2) => tag2.score - tag1.score);
|
topicTags[index].sort((tag1, tag2) => tag2.value - tag1.value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -222,28 +315,41 @@ module.exports = function (Topics) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Topics.addTags = async function (tags, tids) {
|
Topics.addTags = async function (tags, tids) {
|
||||||
const topicData = await Topics.getTopicsFields(tids, ['timestamp']);
|
const topicData = await Topics.getTopicsFields(tids, ['tid', 'cid', 'timestamp']);
|
||||||
const sets = tids.map(tid => 'topic:' + tid + ':tags');
|
const sets = tids.map(tid => 'topic:' + tid + ':tags');
|
||||||
for (let i = 0; i < tags.length; i++) {
|
for (let i = 0; i < tags.length; i++) {
|
||||||
/* eslint-disable no-await-in-loop */
|
/* eslint-disable no-await-in-loop */
|
||||||
|
const bulkAdd = [];
|
||||||
|
topicData.forEach((t) => {
|
||||||
|
bulkAdd.push(['tag:' + tags[i] + ':topics', t.timestamp, t.tid]);
|
||||||
|
bulkAdd.push(['cid:' + t.cid + ':tag:' + tags[i] + ':topics', t.timestamp, t.tid]);
|
||||||
|
});
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
db.setsAdd(sets, tags[i]),
|
db.setsAdd(sets, tags[i]),
|
||||||
db.sortedSetAdd('tag:' + tags[i] + ':topics', topicData.map(t => t.timestamp), tids),
|
db.sortedSetAddBulk(bulkAdd),
|
||||||
]);
|
]);
|
||||||
await updateTagCount(tags[i]);
|
await updateTagCount(tags[i]);
|
||||||
}
|
}
|
||||||
|
await Topics.updateCategoryTagsCount(_.uniq(topicData.map(t => t.cid)), tags);
|
||||||
};
|
};
|
||||||
|
|
||||||
Topics.removeTags = async function (tags, tids) {
|
Topics.removeTags = async function (tags, tids) {
|
||||||
|
const topicData = await Topics.getTopicsFields(tids, ['tid', 'cid']);
|
||||||
const sets = tids.map(tid => 'topic:' + tid + ':tags');
|
const sets = tids.map(tid => 'topic:' + tid + ':tags');
|
||||||
for (let i = 0; i < tags.length; i++) {
|
for (let i = 0; i < tags.length; i++) {
|
||||||
/* eslint-disable no-await-in-loop */
|
/* eslint-disable no-await-in-loop */
|
||||||
|
const bulkRemove = [];
|
||||||
|
topicData.forEach((t) => {
|
||||||
|
bulkRemove.push(['tag:' + tags[i] + ':topics', t.tid]);
|
||||||
|
bulkRemove.push(['cid:' + t.cid + ':tag:' + tags[i] + ':topics', t.tid]);
|
||||||
|
});
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
db.setsRemove(sets, tags[i]),
|
db.setsRemove(sets, tags[i]),
|
||||||
db.sortedSetRemove('tag:' + tags[i] + ':topics', tids),
|
db.sortedSetRemoveBulk(bulkRemove),
|
||||||
]);
|
]);
|
||||||
await updateTagCount(tags[i]);
|
await updateTagCount(tags[i]);
|
||||||
}
|
}
|
||||||
|
await Topics.updateCategoryTagsCount(_.uniq(topicData.map(t => t.cid)), tags);
|
||||||
};
|
};
|
||||||
|
|
||||||
Topics.updateTopicTags = async function (tid, tags) {
|
Topics.updateTopicTags = async function (tid, tags) {
|
||||||
@@ -253,10 +359,17 @@ module.exports = function (Topics) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Topics.deleteTopicTags = async function (tid) {
|
Topics.deleteTopicTags = async function (tid) {
|
||||||
const tags = await Topics.getTopicTags(tid);
|
const [tags, cid] = await Promise.all([
|
||||||
|
Topics.getTopicTags(tid),
|
||||||
|
Topics.getTopicField(tid, 'cid'),
|
||||||
|
]);
|
||||||
await db.delete('topic:' + tid + ':tags');
|
await db.delete('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'));
|
||||||
await db.sortedSetsRemove(sets, tid);
|
await db.sortedSetsRemove(sets, tid);
|
||||||
|
|
||||||
|
await Topics.updateCategoryTagsCount([cid], tags);
|
||||||
await Promise.all(tags.map(tag => updateTagCount(tag)));
|
await Promise.all(tags.map(tag => updateTagCount(tag)));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -268,7 +381,7 @@ module.exports = function (Topics) {
|
|||||||
if (plugins.hooks.hasListeners('filter:topics.searchTags')) {
|
if (plugins.hooks.hasListeners('filter:topics.searchTags')) {
|
||||||
result = await plugins.hooks.fire('filter:topics.searchTags', { data: data });
|
result = await plugins.hooks.fire('filter:topics.searchTags', { data: data });
|
||||||
} else {
|
} else {
|
||||||
result = await findMatches(data.query, 0);
|
result = await findMatches(data);
|
||||||
}
|
}
|
||||||
result = await plugins.hooks.fire('filter:tags.search', { data: data, matches: result.matches });
|
result = await plugins.hooks.fire('filter:tags.search', { data: data, matches: result.matches });
|
||||||
return result.matches;
|
return result.matches;
|
||||||
@@ -282,7 +395,7 @@ module.exports = function (Topics) {
|
|||||||
if (plugins.hooks.hasListeners('filter:topics.autocompleteTags')) {
|
if (plugins.hooks.hasListeners('filter:topics.autocompleteTags')) {
|
||||||
result = await plugins.hooks.fire('filter:topics.autocompleteTags', { data: data });
|
result = await plugins.hooks.fire('filter:topics.autocompleteTags', { data: data });
|
||||||
} else {
|
} else {
|
||||||
result = await findMatches(data.query, data.cid);
|
result = await findMatches(data);
|
||||||
}
|
}
|
||||||
return result.matches;
|
return result.matches;
|
||||||
};
|
};
|
||||||
@@ -292,19 +405,28 @@ module.exports = function (Topics) {
|
|||||||
if (cached !== undefined) {
|
if (cached !== undefined) {
|
||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
const tags = await db.getSortedSetRevRange('tags:topic:count', 0, -1);
|
const tags = await db.getSortedSetRevRangeWithScores('tags:topic:count', 0, -1);
|
||||||
cache.set('tags:topic:count', tags);
|
cache.set('tags:topic:count', tags);
|
||||||
return tags;
|
return tags;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function findMatches(query, cid) {
|
async function findMatches(data) {
|
||||||
|
let query = data.query;
|
||||||
let tagWhitelist = [];
|
let tagWhitelist = [];
|
||||||
if (parseInt(cid, 10)) {
|
if (parseInt(data.cid, 10)) {
|
||||||
tagWhitelist = await categories.getTagWhitelist([cid]);
|
tagWhitelist = await categories.getTagWhitelist([data.cid]);
|
||||||
}
|
}
|
||||||
let tags = [];
|
let tags = [];
|
||||||
if (Array.isArray(tagWhitelist[0]) && tagWhitelist[0].length) {
|
if (Array.isArray(tagWhitelist[0]) && tagWhitelist[0].length) {
|
||||||
tags = tagWhitelist[0];
|
const scores = await db.sortedSetScores('cid:' + data.cid + ':tags', tagWhitelist[0]);
|
||||||
|
tags = tagWhitelist[0].map((tag, index) => ({ value: tag, score: scores[index] }));
|
||||||
|
} else if (data.cids) {
|
||||||
|
tags = await db.getSortedSetRevUnion({
|
||||||
|
sets: data.cids.map(cid => 'cid:' + cid + ':tags'),
|
||||||
|
start: 0,
|
||||||
|
stop: -1,
|
||||||
|
withScores: true,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
tags = await getAllTags();
|
tags = await getAllTags();
|
||||||
}
|
}
|
||||||
@@ -313,15 +435,22 @@ module.exports = function (Topics) {
|
|||||||
|
|
||||||
const matches = [];
|
const matches = [];
|
||||||
for (let i = 0; i < tags.length; i += 1) {
|
for (let i = 0; i < tags.length; i += 1) {
|
||||||
if (tags[i].toLowerCase().startsWith(query)) {
|
if (tags[i].value && tags[i].value.toLowerCase().startsWith(query)) {
|
||||||
matches.push(tags[i]);
|
matches.push(tags[i]);
|
||||||
if (matches.length > 19) {
|
if (matches.length > 39) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
matches.sort();
|
matches.sort(function (a, b) {
|
||||||
|
if (a.value < b.value) {
|
||||||
|
return -1;
|
||||||
|
} else if (a.value > b.value) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
return { matches: matches };
|
return { matches: matches };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,12 +465,11 @@ module.exports = function (Topics) {
|
|||||||
return searchResult;
|
return searchResult;
|
||||||
}
|
}
|
||||||
const tags = await Topics.searchTags(data);
|
const tags = await Topics.searchTags(data);
|
||||||
const [counts, tagData] = await Promise.all([
|
|
||||||
db.sortedSetScores('tags:topic:count', tags),
|
const tagData = await Topics.getTagData(tags.map(tag => ({ value: tag.value })));
|
||||||
Topics.getTagData(tags.map(tag => ({ value: tag }))),
|
|
||||||
]);
|
|
||||||
tagData.forEach(function (tag, index) {
|
tagData.forEach(function (tag, index) {
|
||||||
tag.score = counts[index];
|
tag.score = tags[index].score;
|
||||||
});
|
});
|
||||||
tagData.sort((a, b) => b.score - a.score);
|
tagData.sort((a, b) => b.score - a.score);
|
||||||
searchResult.tags = tagData;
|
searchResult.tags = tagData;
|
||||||
|
|||||||
@@ -213,6 +213,7 @@ module.exports = function (Topics) {
|
|||||||
if (cid === topicData.cid) {
|
if (cid === topicData.cid) {
|
||||||
throw new Error('[[error:cant-move-topic-to-same-category]]');
|
throw new Error('[[error:cant-move-topic-to-same-category]]');
|
||||||
}
|
}
|
||||||
|
const tags = await Topics.getTopicTags(tid);
|
||||||
await db.sortedSetsRemove([
|
await db.sortedSetsRemove([
|
||||||
'cid:' + topicData.cid + ':tids',
|
'cid:' + topicData.cid + ':tids',
|
||||||
'cid:' + topicData.cid + ':tids:pinned',
|
'cid:' + topicData.cid + ':tids:pinned',
|
||||||
@@ -221,6 +222,7 @@ module.exports = function (Topics) {
|
|||||||
'cid:' + topicData.cid + ':tids:lastposttime',
|
'cid:' + topicData.cid + ':tids:lastposttime',
|
||||||
'cid:' + topicData.cid + ':recent_tids',
|
'cid:' + topicData.cid + ':recent_tids',
|
||||||
'cid:' + topicData.cid + ':uid:' + topicData.uid + ':tids',
|
'cid:' + topicData.cid + ':uid:' + topicData.uid + ':tids',
|
||||||
|
...tags.map(tag => 'cid:' + topicData.cid + ':tag:' + tag + ':topics'),
|
||||||
], tid);
|
], tid);
|
||||||
|
|
||||||
topicData.postcount = topicData.postcount || 0;
|
topicData.postcount = topicData.postcount || 0;
|
||||||
@@ -229,6 +231,7 @@ module.exports = function (Topics) {
|
|||||||
const bulk = [
|
const bulk = [
|
||||||
['cid:' + cid + ':tids:lastposttime', topicData.lastposttime, tid],
|
['cid:' + cid + ':tids:lastposttime', topicData.lastposttime, tid],
|
||||||
['cid:' + cid + ':uid:' + topicData.uid + ':tids', topicData.timestamp, tid],
|
['cid:' + cid + ':uid:' + topicData.uid + ':tids', topicData.timestamp, tid],
|
||||||
|
...tags.map(tag => ['cid:' + cid + ':tag:' + tag + ':topics', topicData.timestamp, tid]),
|
||||||
];
|
];
|
||||||
if (topicData.pinned) {
|
if (topicData.pinned) {
|
||||||
bulk.push(['cid:' + cid + ':tids:pinned', Date.now(), tid]);
|
bulk.push(['cid:' + cid + ':tids:pinned', Date.now(), tid]);
|
||||||
@@ -251,6 +254,7 @@ module.exports = function (Topics) {
|
|||||||
cid: cid,
|
cid: cid,
|
||||||
oldCid: oldCid,
|
oldCid: oldCid,
|
||||||
}),
|
}),
|
||||||
|
Topics.updateCategoryTagsCount([oldCid, cid], tags),
|
||||||
]);
|
]);
|
||||||
const hookData = _.clone(data);
|
const hookData = _.clone(data);
|
||||||
hookData.fromCid = oldCid;
|
hookData.fromCid = oldCid;
|
||||||
|
|||||||
50
src/upgrades/1.15.3/category_tags.js
Normal file
50
src/upgrades/1.15.3/category_tags.js
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const async = require('async');
|
||||||
|
const db = require('../../database');
|
||||||
|
const batch = require('../../batch');
|
||||||
|
const topics = require('../../topics');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
name: 'Create category tags sorted sets',
|
||||||
|
timestamp: Date.UTC(2020, 10, 23),
|
||||||
|
method: async function () {
|
||||||
|
const progress = this.progress;
|
||||||
|
const cache = {};
|
||||||
|
async function categoryTagCount(cid, tag) {
|
||||||
|
if (cache[cid] && cache[cid][tag]) {
|
||||||
|
return cache[cid][tag];
|
||||||
|
}
|
||||||
|
const count = await db.sortedSetIntersectCard(
|
||||||
|
['cid:' + cid + ':tids:lastposttime', 'tag:' + tag + ':topics']
|
||||||
|
);
|
||||||
|
cache[cid] = cache[cid] || {};
|
||||||
|
cache[cid][tag] = count;
|
||||||
|
}
|
||||||
|
|
||||||
|
await batch.processSortedSet('topics:tid', async function (tids) {
|
||||||
|
await async.eachSeries(tids, async function (tid) {
|
||||||
|
const [topicData, tags] = await Promise.all([
|
||||||
|
topics.getTopicFields(tid, ['cid', 'timestamp']),
|
||||||
|
topics.getTopicTags(tid),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (tags.length) {
|
||||||
|
const cid = topicData.cid;
|
||||||
|
await async.eachSeries(tags, async function (tag) {
|
||||||
|
const count = await categoryTagCount(cid, tag);
|
||||||
|
if (count > 0) {
|
||||||
|
await db.sortedSetAdd('cid:' + cid + ':tags', count, tag);
|
||||||
|
await db.sortedSetAdd('cid:' + cid + ':tag:' + tag + ':topics', topicData.timestamp, tid);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
progress.incr();
|
||||||
|
});
|
||||||
|
}, {
|
||||||
|
batch: 500,
|
||||||
|
progress: progress,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1939,17 +1939,41 @@ describe('Topic\'s', function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should delete category tag as well', async function () {
|
||||||
|
const category = await categories.create({ name: 'delete category' });
|
||||||
|
const cid = category.cid;
|
||||||
|
await topics.post({ uid: adminUid, tags: ['willbedeleted', 'notthis'], title: 'tag topic', content: 'topic 1 content', cid: cid });
|
||||||
|
let categoryTags = await topics.getCategoryTags(cid, 0, -1);
|
||||||
|
assert(categoryTags.includes('willbedeleted'));
|
||||||
|
assert(categoryTags.includes('notthis'));
|
||||||
|
await topics.deleteTags(['willbedeleted']);
|
||||||
|
categoryTags = await topics.getCategoryTags(cid, 0, -1);
|
||||||
|
assert(!categoryTags.includes('willbedeleted'));
|
||||||
|
assert(categoryTags.includes('notthis'));
|
||||||
|
});
|
||||||
|
|
||||||
it('should add and remove tags from topics properly', async () => {
|
it('should add and remove tags from topics properly', async () => {
|
||||||
const result = await topics.post({ uid: adminUid, tags: ['tag4', 'tag2', 'tag1', 'tag3'], title: 'tag topic', content: 'topic 1 content', cid: topic.categoryId });
|
const category = await categories.create({ name: 'add/remove category' });
|
||||||
|
const cid = category.cid;
|
||||||
|
const result = await topics.post({ uid: adminUid, tags: ['tag4', 'tag2', 'tag1', 'tag3'], title: 'tag topic', content: 'topic 1 content', cid: cid });
|
||||||
const tid = result.topicData.tid;
|
const tid = result.topicData.tid;
|
||||||
|
|
||||||
let tags = await topics.getTopicTags(tid);
|
let tags = await topics.getTopicTags(tid);
|
||||||
|
let categoryTags = await topics.getCategoryTags(cid, 0, -1);
|
||||||
assert.deepStrictEqual(tags, ['tag1', 'tag2', 'tag3', 'tag4']);
|
assert.deepStrictEqual(tags, ['tag1', 'tag2', 'tag3', 'tag4']);
|
||||||
|
assert.deepStrictEqual(categoryTags.sort(), ['tag1', 'tag2', 'tag3', 'tag4']);
|
||||||
|
|
||||||
await topics.addTags(['tag7', 'tag6', 'tag5'], [tid]);
|
await topics.addTags(['tag7', 'tag6', 'tag5'], [tid]);
|
||||||
tags = await topics.getTopicTags(tid);
|
tags = await topics.getTopicTags(tid);
|
||||||
|
categoryTags = await topics.getCategoryTags(cid, 0, -1);
|
||||||
assert.deepStrictEqual(tags, ['tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6', 'tag7']);
|
assert.deepStrictEqual(tags, ['tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6', 'tag7']);
|
||||||
|
assert.deepStrictEqual(categoryTags.sort(), ['tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6', 'tag7']);
|
||||||
|
|
||||||
await topics.removeTags(['tag1', 'tag3', 'tag5', 'tag7'], [tid]);
|
await topics.removeTags(['tag1', 'tag3', 'tag5', 'tag7'], [tid]);
|
||||||
tags = await topics.getTopicTags(tid);
|
tags = await topics.getTopicTags(tid);
|
||||||
|
categoryTags = await topics.getCategoryTags(cid, 0, -1);
|
||||||
assert.deepStrictEqual(tags, ['tag2', 'tag4', 'tag6']);
|
assert.deepStrictEqual(tags, ['tag2', 'tag4', 'tag6']);
|
||||||
|
assert.deepStrictEqual(categoryTags.sort(), ['tag2', 'tag4', 'tag6']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should respect minTags', async () => {
|
it('should respect minTags', async () => {
|
||||||
@@ -2003,6 +2027,66 @@ describe('Topic\'s', function () {
|
|||||||
assert.equal(err.message, '[[error:too-many-tags, ' + maxTags + ']]');
|
assert.equal(err.message, '[[error:too-many-tags, ' + maxTags + ']]');
|
||||||
await db.deleteObjectField('category:' + topic.categoryId, 'maxTags');
|
await db.deleteObjectField('category:' + topic.categoryId, 'maxTags');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should create and delete category tags properly', async () => {
|
||||||
|
const category = await categories.create({ name: 'tag category 2' });
|
||||||
|
const cid = category.cid;
|
||||||
|
const title = 'test title';
|
||||||
|
const postResult = await topics.post({ uid: adminUid, tags: ['cattag1', 'cattag2', 'cattag3'], title: title, content: 'topic 1 content', cid: cid });
|
||||||
|
await topics.post({ uid: adminUid, tags: ['cattag1', 'cattag2'], 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);
|
||||||
|
assert.deepStrictEqual(result, [
|
||||||
|
{ value: 'cattag1', score: 3, bgColor: '', color: '', valueEscaped: 'cattag1' },
|
||||||
|
{ value: 'cattag2', score: 2, bgColor: '', color: '', valueEscaped: 'cattag2' },
|
||||||
|
{ value: 'cattag3', score: 1, bgColor: '', color: '', valueEscaped: 'cattag3' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// after purging values should update properly
|
||||||
|
await topics.purge(postResult.topicData.tid, adminUid);
|
||||||
|
result = await topics.getCategoryTagsData(cid, 0, -1);
|
||||||
|
|
||||||
|
assert.deepStrictEqual(result, [
|
||||||
|
{ value: 'cattag1', score: 2, bgColor: '', color: '', valueEscaped: 'cattag1' },
|
||||||
|
{ value: 'cattag2', score: 1, bgColor: '', color: '', valueEscaped: 'cattag2' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update counts correctly if topic is moved between categories', async function () {
|
||||||
|
const category1 = await categories.create({ name: 'tag category 2' });
|
||||||
|
const category2 = await categories.create({ name: 'tag category 2' });
|
||||||
|
const cid1 = category1.cid;
|
||||||
|
const cid2 = category2.cid;
|
||||||
|
|
||||||
|
const title = 'test title';
|
||||||
|
const postResult = await topics.post({ uid: adminUid, tags: ['movedtag1', 'movedtag2'], title: title, content: 'topic 1 content', cid: cid1 });
|
||||||
|
|
||||||
|
await topics.post({ uid: adminUid, tags: ['movedtag1'], title: title, content: 'topic 1 content', cid: cid1 });
|
||||||
|
await topics.post({ uid: adminUid, tags: ['movedtag2'], title: title, content: 'topic 1 content', cid: cid2 });
|
||||||
|
|
||||||
|
let result1 = await topics.getCategoryTagsData(cid1, 0, -1);
|
||||||
|
let result2 = await topics.getCategoryTagsData(cid2, 0, -1);
|
||||||
|
assert.deepStrictEqual(result1, [
|
||||||
|
{ value: 'movedtag1', score: 2, bgColor: '', color: '', valueEscaped: 'movedtag1' },
|
||||||
|
{ value: 'movedtag2', score: 1, bgColor: '', color: '', valueEscaped: 'movedtag2' },
|
||||||
|
]);
|
||||||
|
assert.deepStrictEqual(result2, [
|
||||||
|
{ value: 'movedtag2', score: 1, bgColor: '', color: '', valueEscaped: 'movedtag2' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// after moving values should update properly
|
||||||
|
await topics.tools.move(postResult.topicData.tid, { cid: cid2, uid: adminUid });
|
||||||
|
|
||||||
|
result1 = await topics.getCategoryTagsData(cid1, 0, -1);
|
||||||
|
result2 = await topics.getCategoryTagsData(cid2, 0, -1);
|
||||||
|
assert.deepStrictEqual(result1, [
|
||||||
|
{ value: 'movedtag1', score: 1, bgColor: '', color: '', valueEscaped: 'movedtag1' },
|
||||||
|
]);
|
||||||
|
assert.deepStrictEqual(result2, [
|
||||||
|
{ value: 'movedtag2', score: 2, bgColor: '', color: '', valueEscaped: 'movedtag2' },
|
||||||
|
{ value: 'movedtag1', score: 1, bgColor: '', color: '', valueEscaped: 'movedtag1' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('follow/unfollow', function () {
|
describe('follow/unfollow', function () {
|
||||||
|
|||||||
Reference in New Issue
Block a user