diff --git a/src/categories/delete.js b/src/categories/delete.js index 74395f0658..42b2bacdce 100644 --- a/src/categories/delete.js +++ b/src/categories/delete.js @@ -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'); + } }; diff --git a/src/controllers/tags.js b/src/controllers/tags.js index 1484a12a37..5d01f365fe 100644 --- a/src/controllers/tags.js +++ b/src/controllers/tags.js @@ -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', { diff --git a/src/socket.io/topics/tags.js b/src/socket.io/topics/tags.js index 19fc9fc884..14863af61f 100644 --- a/src/socket.io/topics/tags.js +++ b/src/socket.io/topics/tags.js @@ -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 }; }; }; diff --git a/src/topics/tags.js b/src/topics/tags.js index c68b7b9594..33be8f7756 100644 --- a/src/topics/tags.js +++ b/src/topics/tags.js @@ -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::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::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::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; diff --git a/src/topics/tools.js b/src/topics/tools.js index c25e5c6bc0..bc23e55ccb 100644 --- a/src/topics/tools.js +++ b/src/topics/tools.js @@ -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; diff --git a/src/upgrades/1.15.3/category_tags.js b/src/upgrades/1.15.3/category_tags.js new file mode 100644 index 0000000000..15fc0c86ca --- /dev/null +++ b/src/upgrades/1.15.3/category_tags.js @@ -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, + }); + }, +}; diff --git a/test/topics.js b/test/topics.js index adf516521a..855612b1e1 100644 --- a/test/topics.js +++ b/test/topics.js @@ -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 () {