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
v1.18.x
Barış Soner Uşaklı 4 years ago committed by GitHub
parent 792e9e703e
commit d2888d1d1f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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;

@ -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 () {

Loading…
Cancel
Save