mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-11-16 10:46:14 +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) {
|
||||
await db.sortedSetRemove('categories:cid', cid);
|
||||
await removeFromParent(cid);
|
||||
await deleteTags(cid);
|
||||
await db.deleteAll([
|
||||
'cid:' + cid + ':tids',
|
||||
'cid:' + cid + ':tids:pinned',
|
||||
@@ -71,4 +72,10 @@ module.exports = function (Categories) {
|
||||
'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 }]),
|
||||
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 stop = start + settings.topicsPerPage - 1;
|
||||
const states = [categories.watchStates.watching, categories.watchStates.notwatching, categories.watchStates.ignoring];
|
||||
|
||||
const [topicCount, tids, categoriesData] = await Promise.all([
|
||||
topics.getTagTopicCount(tag),
|
||||
topics.getTagTids(tag, start, stop),
|
||||
topics.getTagTopicCount(tag, cids),
|
||||
topics.getTagTidsByCids(tag, cids, start, stop),
|
||||
helpers.getCategoriesByStates(req.uid, '', states),
|
||||
]);
|
||||
|
||||
@@ -59,9 +63,10 @@ tagsController.getTag = 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([
|
||||
privileges.global.can('search:tags', req.uid),
|
||||
topics.getTags(0, 99),
|
||||
topics.getCategoryTagsData(cids, 0, 99),
|
||||
]);
|
||||
|
||||
res.render('tags', {
|
||||
|
||||
@@ -16,11 +16,20 @@ module.exports = function (SocketTopics) {
|
||||
};
|
||||
|
||||
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) {
|
||||
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) {
|
||||
@@ -32,6 +41,13 @@ module.exports = function (SocketTopics) {
|
||||
if (!allowed) {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -42,8 +58,8 @@ module.exports = function (SocketTopics) {
|
||||
|
||||
const start = parseInt(data.after, 10);
|
||||
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 };
|
||||
};
|
||||
};
|
||||
|
||||
@@ -24,14 +24,42 @@ module.exports = function (Topics) {
|
||||
.filter(tag => tag && tag.length >= (meta.config.minimumTagLength || 3));
|
||||
|
||||
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([
|
||||
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)));
|
||||
};
|
||||
|
||||
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) {
|
||||
if (!Array.isArray(tags)) {
|
||||
throw new Error('[[error:invalid-data]]');
|
||||
@@ -91,6 +119,7 @@ module.exports = function (Topics) {
|
||||
newTagName = utils.cleanUpTag(newTagName, meta.config.maximumTagLength);
|
||||
const targetExists = await db.isSortedSetMember('tags:topic:count', newTagName);
|
||||
await Topics.createEmptyTag(newTagName);
|
||||
const allCids = {};
|
||||
const tagData = await db.getObject('tag:' + tag);
|
||||
if (tagData && !targetExists) {
|
||||
await db.setObject('tag:' + newTagName, {
|
||||
@@ -100,15 +129,28 @@ module.exports = function (Topics) {
|
||||
}
|
||||
|
||||
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);
|
||||
// update tag:<tag>:topics
|
||||
await db.sortedSetAdd('tag:' + newTagName + ':topics', scores, tids);
|
||||
const keys = tids.map(tid => 'topic:' + tid + ':tags');
|
||||
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.setsAdd(keys, newTagName);
|
||||
}, {});
|
||||
await Topics.deleteTag(tag);
|
||||
await updateTagCount(newTagName);
|
||||
await Topics.updateCategoryTagsCount(Object.keys(allCids), [newTagName]);
|
||||
}
|
||||
|
||||
async function updateTagCount(tag) {
|
||||
@@ -123,9 +165,24 @@ module.exports = function (Topics) {
|
||||
return payload.tids;
|
||||
};
|
||||
|
||||
Topics.getTagTopicCount = async function (tag) {
|
||||
const count = await db.sortedSetCard('tag:' + tag + ':topics');
|
||||
const payload = await plugins.hooks.fire('filter:topics.getTagTopicCount', { tag, count });
|
||||
Topics.getTagTidsByCids = async function (tag, cids, start, stop) {
|
||||
const keys = cids.map(cid => 'cid:' + cid + ':tag:' + tag + ':topics');
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -138,7 +195,18 @@ module.exports = function (Topics) {
|
||||
await db.deleteAll(keys);
|
||||
await db.sortedSetRemove('tags:topic:count', tags);
|
||||
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) {
|
||||
@@ -157,12 +225,46 @@ module.exports = function (Topics) {
|
||||
};
|
||||
|
||||
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', {
|
||||
tags: tags,
|
||||
});
|
||||
return await Topics.getTagData(payload.tags);
|
||||
};
|
||||
}
|
||||
|
||||
Topics.getTagData = async function (tags) {
|
||||
if (!tags.length) {
|
||||
@@ -199,22 +301,13 @@ module.exports = function (Topics) {
|
||||
const uniqueTopicTags = _.uniq(_.flatten(topicTags));
|
||||
|
||||
const tags = uniqueTopicTags.map(tag => ({ value: tag }));
|
||||
|
||||
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 tagData = await Topics.getTagData(tags);
|
||||
const tagDataMap = _.zipObject(uniqueTopicTags, tagData);
|
||||
|
||||
topicTags.forEach(function (tags, index) {
|
||||
if (Array.isArray(tags)) {
|
||||
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) {
|
||||
const topicData = await Topics.getTopicsFields(tids, ['timestamp']);
|
||||
const topicData = await Topics.getTopicsFields(tids, ['tid', 'cid', 'timestamp']);
|
||||
const sets = tids.map(tid => 'topic:' + tid + ':tags');
|
||||
for (let i = 0; i < tags.length; i++) {
|
||||
/* 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([
|
||||
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 Topics.updateCategoryTagsCount(_.uniq(topicData.map(t => t.cid)), tags);
|
||||
};
|
||||
|
||||
Topics.removeTags = async function (tags, tids) {
|
||||
const topicData = await Topics.getTopicsFields(tids, ['tid', 'cid']);
|
||||
const sets = tids.map(tid => 'topic:' + tid + ':tags');
|
||||
for (let i = 0; i < tags.length; i++) {
|
||||
/* 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([
|
||||
db.setsRemove(sets, tags[i]),
|
||||
db.sortedSetRemove('tag:' + tags[i] + ':topics', tids),
|
||||
db.sortedSetRemoveBulk(bulkRemove),
|
||||
]);
|
||||
await updateTagCount(tags[i]);
|
||||
}
|
||||
await Topics.updateCategoryTagsCount(_.uniq(topicData.map(t => t.cid)), tags);
|
||||
};
|
||||
|
||||
Topics.updateTopicTags = async function (tid, tags) {
|
||||
@@ -253,10 +359,17 @@ module.exports = function (Topics) {
|
||||
};
|
||||
|
||||
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');
|
||||
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 Topics.updateCategoryTagsCount([cid], tags);
|
||||
await Promise.all(tags.map(tag => updateTagCount(tag)));
|
||||
};
|
||||
|
||||
@@ -268,7 +381,7 @@ module.exports = function (Topics) {
|
||||
if (plugins.hooks.hasListeners('filter:topics.searchTags')) {
|
||||
result = await plugins.hooks.fire('filter:topics.searchTags', { data: data });
|
||||
} else {
|
||||
result = await findMatches(data.query, 0);
|
||||
result = await findMatches(data);
|
||||
}
|
||||
result = await plugins.hooks.fire('filter:tags.search', { data: data, matches: result.matches });
|
||||
return result.matches;
|
||||
@@ -282,7 +395,7 @@ module.exports = function (Topics) {
|
||||
if (plugins.hooks.hasListeners('filter:topics.autocompleteTags')) {
|
||||
result = await plugins.hooks.fire('filter:topics.autocompleteTags', { data: data });
|
||||
} else {
|
||||
result = await findMatches(data.query, data.cid);
|
||||
result = await findMatches(data);
|
||||
}
|
||||
return result.matches;
|
||||
};
|
||||
@@ -292,19 +405,28 @@ module.exports = function (Topics) {
|
||||
if (cached !== undefined) {
|
||||
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);
|
||||
return tags;
|
||||
}
|
||||
|
||||
async function findMatches(query, cid) {
|
||||
async function findMatches(data) {
|
||||
let query = data.query;
|
||||
let tagWhitelist = [];
|
||||
if (parseInt(cid, 10)) {
|
||||
tagWhitelist = await categories.getTagWhitelist([cid]);
|
||||
if (parseInt(data.cid, 10)) {
|
||||
tagWhitelist = await categories.getTagWhitelist([data.cid]);
|
||||
}
|
||||
let tags = [];
|
||||
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 {
|
||||
tags = await getAllTags();
|
||||
}
|
||||
@@ -313,15 +435,22 @@ module.exports = function (Topics) {
|
||||
|
||||
const matches = [];
|
||||
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]);
|
||||
if (matches.length > 19) {
|
||||
if (matches.length > 39) {
|
||||
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 };
|
||||
}
|
||||
|
||||
@@ -336,12 +465,11 @@ module.exports = function (Topics) {
|
||||
return searchResult;
|
||||
}
|
||||
const tags = await Topics.searchTags(data);
|
||||
const [counts, tagData] = await Promise.all([
|
||||
db.sortedSetScores('tags:topic:count', tags),
|
||||
Topics.getTagData(tags.map(tag => ({ value: tag }))),
|
||||
]);
|
||||
|
||||
const tagData = await Topics.getTagData(tags.map(tag => ({ value: tag.value })));
|
||||
|
||||
tagData.forEach(function (tag, index) {
|
||||
tag.score = counts[index];
|
||||
tag.score = tags[index].score;
|
||||
});
|
||||
tagData.sort((a, b) => b.score - a.score);
|
||||
searchResult.tags = tagData;
|
||||
|
||||
@@ -213,6 +213,7 @@ module.exports = function (Topics) {
|
||||
if (cid === topicData.cid) {
|
||||
throw new Error('[[error:cant-move-topic-to-same-category]]');
|
||||
}
|
||||
const tags = await Topics.getTopicTags(tid);
|
||||
await db.sortedSetsRemove([
|
||||
'cid:' + topicData.cid + ':tids',
|
||||
'cid:' + topicData.cid + ':tids:pinned',
|
||||
@@ -221,6 +222,7 @@ module.exports = function (Topics) {
|
||||
'cid:' + topicData.cid + ':tids:lastposttime',
|
||||
'cid:' + topicData.cid + ':recent_tids',
|
||||
'cid:' + topicData.cid + ':uid:' + topicData.uid + ':tids',
|
||||
...tags.map(tag => 'cid:' + topicData.cid + ':tag:' + tag + ':topics'),
|
||||
], tid);
|
||||
|
||||
topicData.postcount = topicData.postcount || 0;
|
||||
@@ -229,6 +231,7 @@ module.exports = function (Topics) {
|
||||
const bulk = [
|
||||
['cid:' + cid + ':tids:lastposttime', topicData.lastposttime, tid],
|
||||
['cid:' + cid + ':uid:' + topicData.uid + ':tids', topicData.timestamp, tid],
|
||||
...tags.map(tag => ['cid:' + cid + ':tag:' + tag + ':topics', topicData.timestamp, tid]),
|
||||
];
|
||||
if (topicData.pinned) {
|
||||
bulk.push(['cid:' + cid + ':tids:pinned', Date.now(), tid]);
|
||||
@@ -251,6 +254,7 @@ module.exports = function (Topics) {
|
||||
cid: cid,
|
||||
oldCid: oldCid,
|
||||
}),
|
||||
Topics.updateCategoryTagsCount([oldCid, cid], tags),
|
||||
]);
|
||||
const hookData = _.clone(data);
|
||||
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 () => {
|
||||
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;
|
||||
|
||||
let tags = await topics.getTopicTags(tid);
|
||||
let categoryTags = await topics.getCategoryTags(cid, 0, -1);
|
||||
assert.deepStrictEqual(tags, ['tag1', 'tag2', 'tag3', 'tag4']);
|
||||
assert.deepStrictEqual(categoryTags.sort(), ['tag1', 'tag2', 'tag3', 'tag4']);
|
||||
|
||||
await topics.addTags(['tag7', 'tag6', 'tag5'], [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(categoryTags.sort(), ['tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6', 'tag7']);
|
||||
|
||||
await topics.removeTags(['tag1', 'tag3', 'tag5', 'tag7'], [tid]);
|
||||
tags = await topics.getTopicTags(tid);
|
||||
categoryTags = await topics.getCategoryTags(cid, 0, -1);
|
||||
assert.deepStrictEqual(tags, ['tag2', 'tag4', 'tag6']);
|
||||
assert.deepStrictEqual(categoryTags.sort(), ['tag2', 'tag4', 'tag6']);
|
||||
});
|
||||
|
||||
it('should respect minTags', async () => {
|
||||
@@ -2003,6 +2027,66 @@ describe('Topic\'s', function () {
|
||||
assert.equal(err.message, '[[error:too-many-tags, ' + 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 () {
|
||||
|
||||
Reference in New Issue
Block a user