From 0482fb2998d87788bf91a98955c209e3c5989887 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Wed, 24 Jun 2020 15:31:49 -0400 Subject: [PATCH 01/10] feat: use tags partial instead of post_bar --- public/src/client/topic/events.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/src/client/topic/events.js b/public/src/client/topic/events.js index edfa5068b5..e7cb002723 100644 --- a/public/src/client/topic/events.js +++ b/public/src/client/topic/events.js @@ -149,7 +149,7 @@ define('forum/topic/events', [ }); if (data.topic.tags && tagsUpdated(data.topic.tags)) { - Benchpress.parse('partials/post_bar', 'tags', { tags: data.topic.tags }, function (html) { + Benchpress.parse('partials/topic/tags', { tags: data.topic.tags }, function (html) { var tags = $('.tags'); tags.fadeOut(250, function () { From 272b4992c230d26e54d84d2d0fc0b4971506c6f9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 24 Jun 2020 15:34:47 -0400 Subject: [PATCH 02/10] fix(deps): update dependency nodebb-theme-vanilla to v11.1.30 (#8435) Co-authored-by: Renovate Bot --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index f18e7d0b4d..5622e2f2fe 100644 --- a/install/package.json +++ b/install/package.json @@ -92,7 +92,7 @@ "nodebb-theme-lavender": "5.0.11", "nodebb-theme-persona": "10.1.54", "nodebb-theme-slick": "1.2.29", - "nodebb-theme-vanilla": "11.1.29", + "nodebb-theme-vanilla": "11.1.30", "nodebb-widget-essentials": "4.1.0", "nodemailer": "^6.4.6", "passport": "^0.4.1", From a860a793efe8e6c67f04111a28984d65fb9efcde Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 24 Jun 2020 15:34:59 -0400 Subject: [PATCH 03/10] fix(deps): update dependency nodebb-theme-persona to v10.1.55 (#8434) Co-authored-by: Renovate Bot --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index 5622e2f2fe..d8ec05405c 100644 --- a/install/package.json +++ b/install/package.json @@ -90,7 +90,7 @@ "nodebb-plugin-spam-be-gone": "0.7.2", "nodebb-rewards-essentials": "0.1.3", "nodebb-theme-lavender": "5.0.11", - "nodebb-theme-persona": "10.1.54", + "nodebb-theme-persona": "10.1.55", "nodebb-theme-slick": "1.2.29", "nodebb-theme-vanilla": "11.1.30", "nodebb-widget-essentials": "4.1.0", From 6a8f54fd6bdf65581a3d4868fdd57a130a0734e5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 24 Jun 2020 15:35:08 -0400 Subject: [PATCH 04/10] fix(deps): update dependency winston to v3.3.3 (#8431) Co-authored-by: Renovate Bot --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index d8ec05405c..4f34e79450 100644 --- a/install/package.json +++ b/install/package.json @@ -126,7 +126,7 @@ "toobusy-js": "^0.5.1", "uglify-es": "^3.3.9", "validator": "13.1.1", - "winston": "3.3.2", + "winston": "3.3.3", "xml": "^1.0.1", "xregexp": "^4.3.0", "zxcvbn": "^4.4.2" From 3349274439941fd5e3174e1ff25b6800da55c000 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 24 Jun 2020 15:35:20 -0400 Subject: [PATCH 05/10] chore(deps): update dependency eslint to v7.3.1 (#8417) Co-authored-by: Renovate Bot --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index 4f34e79450..d38bd6e1d2 100644 --- a/install/package.json +++ b/install/package.json @@ -136,7 +136,7 @@ "@commitlint/cli": "9.0.1", "@commitlint/config-angular": "9.0.1", "coveralls": "3.1.0", - "eslint": "7.2.0", + "eslint": "7.3.1", "eslint-config-airbnb-base": "14.1.0", "eslint-plugin-import": "2.21.1", "grunt": "1.1.0", From ed4b5caf2cae7e83e96e483cd377f34b8189ec44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Wed, 24 Jun 2020 16:02:01 -0400 Subject: [PATCH 06/10] fix: copy settings showing empty category selection --- public/src/admin/manage/category.js | 78 ++++++++++++++++------------- 1 file changed, 42 insertions(+), 36 deletions(-) diff --git a/public/src/admin/manage/category.js b/public/src/admin/manage/category.js index 15c70655e0..f4c75c38e6 100644 --- a/public/src/admin/manage/category.js +++ b/public/src/admin/manage/category.js @@ -124,49 +124,55 @@ define('admin/manage/category', [ }); $('.copy-settings').on('click', function () { - Benchpress.parse('admin/partials/categories/copy-settings', { - categories: ajaxify.data.allCategories, - }, function (html) { - var selectedCid; - var modal = bootbox.dialog({ - title: '[[modules:composer.select_category]]', - message: html, - buttons: { - save: { - label: '[[modules:bootbox.confirm]]', - className: 'btn-primary', - callback: function () { - if (!selectedCid) { - return; - } + socket.emit('categories.getSelectCategories', {}, function (err, allCategories) { + if (err) { + return app.alertError(err.message); + } - socket.emit('admin.categories.copySettingsFrom', { - fromCid: selectedCid, - toCid: ajaxify.data.category.cid, - copyParent: modal.find('#copyParent').prop('checked'), - }, function (err) { - if (err) { - return app.alertError(err.message); + Benchpress.parse('admin/partials/categories/copy-settings', { + categories: allCategories, + }, function (html) { + var selectedCid; + var modal = bootbox.dialog({ + title: '[[modules:composer.select_category]]', + message: html, + buttons: { + save: { + label: '[[modules:bootbox.confirm]]', + className: 'btn-primary', + callback: function () { + if (!selectedCid || parseInt(selectedCid, 10) === parseInt(ajaxify.data.category.cid, 10)) { + return; } - modal.modal('hide'); - app.alertSuccess('[[admin/manage/categories:alert.copy-success]]'); - ajaxify.refresh(); - }); - return false; + socket.emit('admin.categories.copySettingsFrom', { + fromCid: selectedCid, + toCid: ajaxify.data.category.cid, + copyParent: modal.find('#copyParent').prop('checked'), + }, function (err) { + if (err) { + return app.alertError(err.message); + } + + modal.modal('hide'); + app.alertSuccess('[[admin/manage/categories:alert.copy-success]]'); + ajaxify.refresh(); + }); + return false; + }, }, }, - }, - }); - modal.find('.modal-footer button').prop('disabled', true); - categorySelector.init(modal.find('[component="category-selector"]'), function (selectedCategory) { - selectedCid = selectedCategory && selectedCategory.cid; - if (selectedCid) { - modal.find('.modal-footer button').prop('disabled', false); - } + }); + modal.find('.modal-footer button').prop('disabled', true); + categorySelector.init(modal.find('[component="category-selector"]'), function (selectedCategory) { + selectedCid = selectedCategory && selectedCategory.cid; + if (selectedCid) { + modal.find('.modal-footer button').prop('disabled', false); + } + }); }); + return false; }); - return false; }); $('.upload-button').on('click', function () { From c718b7293ecb76533b5914ef531e0f51f30f634a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Wed, 24 Jun 2020 16:02:57 -0400 Subject: [PATCH 07/10] feat: #3783, min/max tags per category --- .../components/schemas/CategoryObject.yaml | 8 ++- public/openapi/read.yaml | 8 +++ src/categories/create.js | 2 + src/categories/data.js | 21 +++++++- src/posts/edit.js | 2 + src/socket.io/posts/edit.js | 4 -- src/topics/create.js | 2 +- src/topics/index.js | 2 + src/topics/tags.js | 15 +++++- src/views/admin/manage/category.tpl | 26 ++++++++-- test/topics.js | 52 +++++++++++++++++++ 11 files changed, 130 insertions(+), 12 deletions(-) diff --git a/public/openapi/components/schemas/CategoryObject.yaml b/public/openapi/components/schemas/CategoryObject.yaml index fa8af2d41d..1599975b3c 100644 --- a/public/openapi/components/schemas/CategoryObject.yaml +++ b/public/openapi/components/schemas/CategoryObject.yaml @@ -62,4 +62,10 @@ CategoryObject: description: The number of posts in the category totalTopicCount: type: number - description: The number of topics in the category \ No newline at end of file + description: The number of topics in the category + minTags: + type: number + description: Minimum tags per topic in this category + maxTags: + type: number + description: Maximum tags per topic in this category \ No newline at end of file diff --git a/public/openapi/read.yaml b/public/openapi/read.yaml index 11d072ed54..8d8dadd8e8 100644 --- a/public/openapi/read.yaml +++ b/public/openapi/read.yaml @@ -3130,6 +3130,10 @@ paths: type: number totalTopicCount: type: number + minTags: + type: number + maxTags: + type: number /api/categories: get: tags: @@ -3655,6 +3659,10 @@ paths: type: array items: type: string + minTags: + type: number + maxTags: + type: number thread_tools: type: array items: diff --git a/src/categories/create.js b/src/categories/create.js index 83b84b023e..b724ef031b 100644 --- a/src/categories/create.js +++ b/src/categories/create.js @@ -143,6 +143,8 @@ module.exports = function (Categories) { destination.class = source.class; destination.image = source.image; destination.imageClass = source.imageClass; + destination.minTags = source.minTags; + destination.maxTags = source.maxTags; if (copyParent) { destination.parentCid = source.parentCid || 0; diff --git a/src/categories/data.js b/src/categories/data.js index 47b4a9e2b9..832ed4ca97 100644 --- a/src/categories/data.js +++ b/src/categories/data.js @@ -1,12 +1,14 @@ 'use strict'; -var validator = require('validator'); +const validator = require('validator'); -var db = require('../database'); +const db = require('../database'); +const meta = require('../meta'); const intFields = [ 'cid', 'parentCid', 'disabled', 'isSection', 'order', 'topic_count', 'post_count', 'numRecentReplies', + 'minTags', 'maxTags', ]; module.exports = function (Categories) { @@ -59,6 +61,21 @@ function modifyCategory(category, fields) { return; } + if (!fields.length || fields.includes('minTags')) { + const useDefault = !category.hasOwnProperty('minTags') || + category.minTags === null || + category.minTags === '' || + !parseInt(category.minTags, 10); + category.minTags = useDefault ? meta.config.minimumTagsPerTopic : category.minTags; + } + if (!fields.length || fields.includes('maxTags')) { + const useDefault = !category.hasOwnProperty('maxTags') || + category.maxTags === null || + category.maxTags === '' || + !parseInt(category.maxTags, 10); + category.maxTags = useDefault ? meta.config.maximumTagsPerTopic : category.maxTags; + } + db.parseIntFields(category, intFields, fields); if (category.hasOwnProperty('name')) { diff --git a/src/posts/edit.js b/src/posts/edit.js index 67d0b898e5..7adc5ba2de 100644 --- a/src/posts/edit.js +++ b/src/posts/edit.js @@ -117,6 +117,8 @@ module.exports = function (Posts) { throw new Error('[[error:no-privileges]]'); } } + await topics.validateTags(data.tags, topicData.cid); + const results = await plugins.fireHook('filter:topic.edit', { req: data.req, topic: newTopicData, data: data }); await db.setObject('topic:' + tid, results.topic); await topics.updateTopicTags(tid, data.tags); diff --git a/src/socket.io/posts/edit.js b/src/socket.io/posts/edit.js index 6499ea3eea..c4dfa1365a 100644 --- a/src/socket.io/posts/edit.js +++ b/src/socket.io/posts/edit.js @@ -25,10 +25,6 @@ module.exports = function (SocketPosts) { throw new Error('[[error:title-too-short, ' + meta.config.minimumTitleLength + ']]'); } else if (data.title && data.title.length > meta.config.maximumTitleLength) { throw new Error('[[error:title-too-long, ' + meta.config.maximumTitleLength + ']]'); - } else if (data.tags && data.tags.length < meta.config.minimumTagsPerTopic) { - throw new Error('[[error:not-enough-tags, ' + meta.config.minimumTagsPerTopic + ']]'); - } else if (data.tags && data.tags.length > meta.config.maximumTagsPerTopic) { - throw new Error('[[error:too-many-tags, ' + meta.config.maximumTagsPerTopic + ']]'); } else if (meta.config.minimumPostLength !== 0 && contentLen < meta.config.minimumPostLength) { throw new Error('[[error:content-too-short, ' + meta.config.minimumPostLength + ']]'); } else if (contentLen > meta.config.maximumPostLength) { diff --git a/src/topics/create.js b/src/topics/create.js index 884446a549..6943e4204d 100644 --- a/src/topics/create.js +++ b/src/topics/create.js @@ -68,7 +68,7 @@ module.exports = function (Topics) { data.content = utils.rtrim(data.content); } check(data.title, meta.config.minimumTitleLength, meta.config.maximumTitleLength, 'title-too-short', 'title-too-long'); - check(data.tags, meta.config.minimumTagsPerTopic, meta.config.maximumTagsPerTopic, 'not-enough-tags', 'too-many-tags'); + await Topics.validateTags(data.tags, data.cid); check(data.content, meta.config.minimumPostLength, meta.config.maximumPostLength, 'content-too-short', 'content-too-long'); const [categoryExists, canCreate, canTag] = await Promise.all([ diff --git a/src/topics/index.js b/src/topics/index.js index 6a63680d89..6d44360565 100644 --- a/src/topics/index.js +++ b/src/topics/index.js @@ -160,6 +160,8 @@ Topics.getTopicWithPosts = async function (topicData, set, uid, start, stop, rev topicData.posts = posts; topicData.category = category; topicData.tagWhitelist = tagWhitelist[0]; + topicData.minTags = category.minTags; + topicData.maxTags = category.maxTags; topicData.thread_tools = threadTools.tools; topicData.isFollowing = followData[0].following; topicData.isNotFollowing = !followData[0].following && !followData[0].ignoring; diff --git a/src/topics/tags.js b/src/topics/tags.js index 88a8f3a69b..db49719e14 100644 --- a/src/topics/tags.js +++ b/src/topics/tags.js @@ -19,7 +19,7 @@ module.exports = function (Topics) { return; } const result = await plugins.fireHook('filter:tags.filter', { tags: tags, tid: tid }); - tags = _.uniq(result.tags).slice(0, meta.config.maximumTagsPerTopic || 5) + tags = _.uniq(result.tags) .map(tag => utils.cleanUpTag(tag, meta.config.maximumTagLength)) .filter(tag => tag && tag.length >= (meta.config.minimumTagLength || 3)); @@ -32,6 +32,19 @@ module.exports = function (Topics) { await Promise.all(tags.map(tag => updateTagCount(tag))); }; + Topics.validateTags = async function (tags, cid) { + if (!Array.isArray(tags)) { + throw new Error('[[error:invalid-data]]'); + } + tags = _.uniq(tags); + const categoryData = await categories.getCategoryFields(cid, ['minTags', 'maxTags']); + if (tags.length < parseInt(categoryData.minTags, 10)) { + throw new Error('[[error:not-enough-tags, ' + categoryData.minTags + ']]'); + } else if (tags.length > parseInt(categoryData.maxTags, 10)) { + throw new Error('[[error:too-many-tags, ' + categoryData.maxTags + ']]'); + } + }; + async function filterCategoryTags(tags, tid) { const cid = await Topics.getTopicField(tid, 'cid'); const tagWhitelist = await categories.getTagWhitelist([cid]); diff --git a/src/views/admin/manage/category.tpl b/src/views/admin/manage/category.tpl index 776bb0e1de..1d862db40e 100644 --- a/src/views/admin/manage/category.tpl +++ b/src/views/admin/manage/category.tpl @@ -95,9 +95,29 @@ -
-
- +
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+
+ +
diff --git a/test/topics.js b/test/topics.js index 06c25afa1b..4aede4c9d9 100644 --- a/test/topics.js +++ b/test/topics.js @@ -1782,6 +1782,58 @@ describe('Topic\'s', function () { tags = await topics.getTopicTags(tid); assert.deepStrictEqual(tags, ['tag2', 'tag4', 'tag6']); }); + + it('should respect minTags', async () => { + const oldValue = meta.config.minimumTagsPerTopic; + meta.config.minimumTagsPerTopic = 2; + let err; + try { + await topics.post({ uid: adminUid, tags: ['tag4'], title: 'tag topic', content: 'topic 1 content', cid: topic.categoryId }); + } catch (_err) { + err = _err; + } + assert.equal(err.message, '[[error:not-enough-tags, ' + meta.config.minimumTagsPerTopic + ']]'); + meta.config.minimumTagsPerTopic = oldValue; + }); + + it('should respect maxTags', async () => { + const oldValue = meta.config.maximumTagsPerTopic; + meta.config.maximumTagsPerTopic = 2; + let err; + try { + await topics.post({ uid: adminUid, tags: ['tag1', 'tag2', 'tag3'], title: 'tag topic', content: 'topic 1 content', cid: topic.categoryId }); + } catch (_err) { + err = _err; + } + assert.equal(err.message, '[[error:too-many-tags, ' + meta.config.maximumTagsPerTopic + ']]'); + meta.config.maximumTagsPerTopic = oldValue; + }); + + it('should respect minTags per category', async () => { + const minTags = 2; + await categories.setCategoryField(topic.categoryId, 'minTags', minTags); + let err; + try { + await topics.post({ uid: adminUid, tags: ['tag4'], title: 'tag topic', content: 'topic 1 content', cid: topic.categoryId }); + } catch (_err) { + err = _err; + } + assert.equal(err.message, '[[error:not-enough-tags, ' + minTags + ']]'); + await db.deleteObjectField('category:' + topic.categoryId, 'minTags'); + }); + + it('should respect maxTags per category', async () => { + const maxTags = 2; + await categories.setCategoryField(topic.categoryId, 'maxTags', maxTags); + let err; + try { + await topics.post({ uid: adminUid, tags: ['tag1', 'tag2', 'tag3'], title: 'tag topic', content: 'topic 1 content', cid: topic.categoryId }); + } catch (_err) { + err = _err; + } + assert.equal(err.message, '[[error:too-many-tags, ' + maxTags + ']]'); + await db.deleteObjectField('category:' + topic.categoryId, 'maxTags'); + }); }); describe('follow/unfollow', function () { From c1991abe6fb2330aec6feda14df8181afaf832d2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 24 Jun 2020 16:04:28 -0400 Subject: [PATCH 08/10] fix(deps): update dependency nodebb-plugin-composer-default to v6.3.44 (#8436) Co-authored-by: Renovate Bot --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index d38bd6e1d2..ffae5c5c68 100644 --- a/install/package.json +++ b/install/package.json @@ -80,7 +80,7 @@ "@nodebb/mubsub": "^1.6.0", "@nodebb/socket.io-adapter-mongo": "3.0.0", "nconf": "^0.10.0", - "nodebb-plugin-composer-default": "6.3.43", + "nodebb-plugin-composer-default": "6.3.44", "nodebb-plugin-dbsearch": "4.0.7", "nodebb-plugin-emoji": "^3.3.0", "nodebb-plugin-emoji-android": "2.0.0", From 0d112b3605c0d03ebbb5e10ec1596f91a29d4c06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Wed, 24 Jun 2020 16:10:06 -0400 Subject: [PATCH 09/10] refactor: make code climate happier? --- src/categories/data.js | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/categories/data.js b/src/categories/data.js index 832ed4ca97..07aa9eb3b9 100644 --- a/src/categories/data.js +++ b/src/categories/data.js @@ -56,25 +56,23 @@ module.exports = function (Categories) { }; }; +function defaultMinMaxTags(category, fields, fieldName, defaultField) { + if (!fields.length || fields.includes(fieldName)) { + const useDefault = !category.hasOwnProperty(fieldName) || + category[fieldName] === null || + category[fieldName] === '' || + !parseInt(category[fieldName], 10); + category[fieldName] = useDefault ? meta.config[defaultField] : category[fieldName]; + } +} + function modifyCategory(category, fields) { if (!category) { return; } - if (!fields.length || fields.includes('minTags')) { - const useDefault = !category.hasOwnProperty('minTags') || - category.minTags === null || - category.minTags === '' || - !parseInt(category.minTags, 10); - category.minTags = useDefault ? meta.config.minimumTagsPerTopic : category.minTags; - } - if (!fields.length || fields.includes('maxTags')) { - const useDefault = !category.hasOwnProperty('maxTags') || - category.maxTags === null || - category.maxTags === '' || - !parseInt(category.maxTags, 10); - category.maxTags = useDefault ? meta.config.maximumTagsPerTopic : category.maxTags; - } + defaultMinMaxTags(category, fields, 'minTags', 'minimumTagsPerTopic'); + defaultMinMaxTags(category, fields, 'maxTags', 'maximumTagsPerTopic'); db.parseIntFields(category, intFields, fields); From bffb830d8754344f7bf8cc819e7985cd3318d49b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Wed, 24 Jun 2020 16:20:50 -0400 Subject: [PATCH 10/10] feat: add missing translation key --- public/language/en-GB/admin/manage/categories.json | 1 + src/views/admin/manage/category.tpl | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/public/language/en-GB/admin/manage/categories.json b/public/language/en-GB/admin/manage/categories.json index 1eda6ea080..a8949c654b 100644 --- a/public/language/en-GB/admin/manage/categories.json +++ b/public/language/en-GB/admin/manage/categories.json @@ -11,6 +11,7 @@ "num-recent-replies": "# of Recent Replies", "ext-link": "External Link", "is-section": "Treat this category as a section", + "tag-whitelist": "Tag Whitelist", "upload-image": "Upload Image", "delete-image": "Remove", "category-image": "Category Image", diff --git a/src/views/admin/manage/category.tpl b/src/views/admin/manage/category.tpl index 1d862db40e..7d06aaf77a 100644 --- a/src/views/admin/manage/category.tpl +++ b/src/views/admin/manage/category.tpl @@ -115,8 +115,8 @@
-
- +
+