FEATURE: ability to restrict some tags to a category while allowing all others

A new checkbox has been added to the Tags tab of the category settings modal
which is used when some tags and/or tag groups are restricted to the category,
and all other unrestricted tags should also be allowed.
Default is the same as the previous behaviour: only allow the specified set of
tags and tag groups in the category.
This commit is contained in:
Neil Lalonde 2019-04-04 16:35:31 -04:00
parent 9faebfcfe0
commit 83996fc8ea
10 changed files with 159 additions and 29 deletions

View File

@ -1,3 +1,11 @@
import { buildCategoryPanel } from "discourse/components/edit-category-panel";
import computed from "ember-addons/ember-computed-decorators";
export default buildCategoryPanel("tags", {});
export default buildCategoryPanel("tags", {
allowedTagsEmpty: Ember.computed.empty("category.allowed_tags"),
allowedTagGroupsEmpty: Ember.computed.empty("category.allowed_tag_groups"),
disableAllowGlobalTags: Ember.computed.and(
"allowedTagsEmpty",
"allowedTagGroupsEmpty"
)
});

View File

@ -118,6 +118,7 @@ const Category = RestModel.extend({
all_topics_wiki: this.get("all_topics_wiki"),
allowed_tags: this.get("allowed_tags"),
allowed_tag_groups: this.get("allowed_tag_groups"),
allow_global_tags: this.get("allow_global_tags"),
sort_order: this.get("sort_order"),
sort_ascending: this.get("sort_ascending"),
topic_featured_link_allowed: this.get("topic_featured_link_allowed"),

View File

@ -11,6 +11,14 @@
<section class="field">
<label for="category-allowed-tag-groups">{{i18n 'category.tags_allowed_tag_groups'}}</label>
{{tag-group-chooser id="category-allowed-tag-groups" tagGroups=category.allowed_tag_groups}}
{{#link-to 'tagGroups'}}{{i18n 'category.manage_tag_groups_link'}}{{/link-to}}
</section>
<section class='field'>
<label>
{{input type="checkbox" checked=category.allow_global_tags id="allow_global_tags" disabled=disableAllowGlobalTags}}
{{i18n 'category.allow_global_tags_label'}}
</label>
</section>
<section class='field'>

View File

@ -295,6 +295,7 @@ class CategoriesController < ApplicationController
:minimum_required_tags,
:navigate_to_first_post_after_read,
:search_priority,
:allow_global_tags,
custom_fields: [params[:custom_fields].try(:keys)],
permissions: [*p.try(:keys)],
allowed_tags: [],

View File

@ -18,6 +18,7 @@ class CategorySerializer < BasicCategorySerializer
:custom_fields,
:allowed_tags,
:allowed_tag_groups,
:allow_global_tags,
:topic_featured_link_allowed,
:search_priority

View File

@ -2483,11 +2483,13 @@ en:
settings: "Settings"
topic_template: "Topic Template"
tags: "Tags"
tags_allowed_tags: "Only allow these tags to be used in this category:"
tags_allowed_tag_groups: "Only allow tags from these groups to be used in this category:"
tags_allowed_tags: "Restrict these tags to this category:"
tags_allowed_tag_groups: "Restrict these tag groups to this category:"
tags_placeholder: "(Optional) list of allowed tags"
tags_tab_description: "Tags and tag groups specified here will only be available in this category and other categories that also specify them. They won't be available for use in other categories."
tag_groups_placeholder: "(Optional) list of allowed tag groups"
manage_tag_groups_link: "Manage tag groups here."
allow_global_tags_label: "Also allow other tags"
topic_featured_link_allowed: "Allow featured links in this category"
delete: "Delete Category"
create: "New Category"

View File

@ -0,0 +1,5 @@
class AddAllowGlobalTagsToCategories < ActiveRecord::Migration[5.2]
def change
add_column :categories, :allow_global_tags, :boolean, default: false, null: false
end
end

View File

@ -100,21 +100,35 @@ module DiscourseTagging
category = opts[:category]
if category && (category.tags.count > 0 || category.tag_groups.count > 0)
if category.tags.count > 0 && category.tag_groups.count > 0
tag_group_ids = category.tag_groups.pluck(:id)
query = query.where(
"tags.id IN (SELECT tag_id FROM category_tags WHERE category_id = ?
if category.allow_global_tags
# Select tags that:
# * are restricted to the given category
# * belong to no tag groups and aren't restricted to other categories
# * belong to tag groups that are not restricted to any categories
query = query.where(<<~SQL, category.tag_groups.pluck(:id), category.id)
tags.id IN (
SELECT t.id FROM tags t
LEFT JOIN category_tags ct ON t.id = ct.tag_id
LEFT JOIN (
SELECT xtgm.tag_id, xtgm.tag_group_id
FROM tag_group_memberships xtgm
INNER JOIN category_tag_groups ctg
ON xtgm.tag_group_id = ctg.tag_group_id
) AS tgm ON t.id = tgm.tag_id
WHERE (tgm.tag_group_id IS NULL AND ct.category_id IS NULL)
OR tgm.tag_group_id IN (?)
OR ct.category_id = ?
)
SQL
else
# Select only tags that are restricted to the given category
query = query.where(<<~SQL, category.id, category.tag_groups.pluck(:id))
tags.id IN (
SELECT tag_id FROM category_tags WHERE category_id = ?
UNION
SELECT tag_id FROM tag_group_memberships WHERE tag_group_id IN (?))",
category.id, tag_group_ids
)
elsif category.tags.count > 0
query = query.where("tags.id IN (SELECT tag_id FROM category_tags WHERE category_id = ?)", category.id)
else # category.tag_groups.count > 0
tag_group_ids = category.tag_groups.pluck(:id)
query = query.where("tags.id IN (SELECT tag_id FROM tag_group_memberships WHERE tag_group_id IN (?))", tag_group_ids)
SELECT tag_id FROM tag_group_memberships WHERE tag_group_id IN (?)
)
SQL
end
elsif opts[:for_input] || opts[:for_topic] || category
# exclude tags that are restricted to other categories

View File

@ -28,8 +28,8 @@ describe "category tag restrictions" do
end
context "tags restricted to one category" do
let(:category_with_tags) { Fabricate(:category) }
let(:other_category) { Fabricate(:category) }
let!(:category_with_tags) { Fabricate(:category) }
let!(:other_category) { Fabricate(:category) }
before do
category_with_tags.tags = [tag1, tag2]
@ -49,8 +49,13 @@ describe "category tag restrictions" do
expect(filter_allowed_tags(for_input: true)).to contain_exactly(tag3, tag4)
expect(filter_allowed_tags(for_input: true, category: category_with_tags, selected_tags: [tag1.name])).to contain_exactly(tag2)
expect(filter_allowed_tags(for_input: true, category: category_with_tags, selected_tags: [tag1.name], term: 'tag')).to contain_exactly(tag2)
expect(filter_allowed_tags(for_input: true, category: other_category)).to contain_exactly(tag3, tag4)
expect(filter_allowed_tags(for_input: true, category: other_category, selected_tags: [tag3.name])).to contain_exactly(tag4)
expect(filter_allowed_tags(for_input: true, category: other_category, selected_tags: [tag3.name], term: 'tag')).to contain_exactly(tag4)
end
it "can't create new tags in a restricted category" do
post = create_post(category: category_with_tags, tags: [tag1.name, "newtag"])
expect(post.topic.tags).to contain_exactly(tag1)
@ -67,21 +72,44 @@ describe "category tag restrictions" do
expect { other_category.update(allowed_tags: ['newtag']) }.to change { Tag.count }.by(1)
expect { other_category.update(allowed_tags: [tag1.name, 'tag-stuff', tag2.name, 'another-tag']) }.to change { Tag.count }.by(2)
end
context 'category allows other tags to be used' do
before do
category_with_tags.update_attributes!(allow_global_tags: true)
end
it "search can show the permitted tags" do
expect(filter_allowed_tags.count).to eq(Tag.count)
expect(filter_allowed_tags(for_input: true, category: category_with_tags)).to contain_exactly(tag1, tag2, tag3, tag4)
expect(filter_allowed_tags(for_input: true)).to contain_exactly(tag3, tag4)
expect(filter_allowed_tags(for_input: true, category: category_with_tags, selected_tags: [tag1.name])).to contain_exactly(tag2, tag3, tag4)
expect(filter_allowed_tags(for_input: true, category: category_with_tags, selected_tags: [tag1.name], term: 'tag')).to contain_exactly(tag2, tag3, tag4)
expect(filter_allowed_tags(for_input: true, category: other_category)).to contain_exactly(tag3, tag4)
expect(filter_allowed_tags(for_input: true, category: other_category, selected_tags: [tag3.name])).to contain_exactly(tag4)
expect(filter_allowed_tags(for_input: true, category: other_category, selected_tags: [tag3.name], term: 'tag')).to contain_exactly(tag4)
end
it "works if no tags are restricted to the category" do
other_category.update_attributes!(allow_global_tags: true)
expect(filter_allowed_tags(for_input: true, category: other_category)).to contain_exactly(tag3, tag4)
expect(filter_allowed_tags(for_input: true, category: other_category, selected_tags: [tag3.name])).to contain_exactly(tag4)
expect(filter_allowed_tags(for_input: true, category: other_category, selected_tags: [tag3.name], term: 'tag')).to contain_exactly(tag4)
end
end
end
context "tag groups restricted to a category" do
let!(:tag_group1) { Fabricate(:tag_group) }
let(:category) { Fabricate(:category) }
let(:other_category) { Fabricate(:category) }
let!(:category) { Fabricate(:category) }
let!(:other_category) { Fabricate(:category) }
before do
tag_group1.tags = [tag1, tag2]
category.allowed_tag_groups = [tag_group1.name]
category.reload
end
it "tags in the group are used by category tag restrictions" do
category.allowed_tag_groups = [tag_group1.name]
category.reload
expect(filter_allowed_tags(for_input: true, category: category)).to contain_exactly(tag1, tag2)
expect(filter_allowed_tags(for_input: true)).to contain_exactly(tag3, tag4)
@ -92,7 +120,6 @@ describe "category tag restrictions" do
end
it "groups and individual tags can be mixed" do
category.allowed_tag_groups = [tag_group1.name]
category.allowed_tags = [tag4.name]
category.reload
@ -102,11 +129,72 @@ describe "category tag restrictions" do
end
it "enforces restrictions when creating a topic" do
category.allowed_tag_groups = [tag_group1.name]
category.reload
post = create_post(category: category, tags: [tag1.name, "newtag"])
expect(post.topic.tags.map(&:name)).to eq([tag1.name])
end
context 'category allows other tags to be used' do
before do
category.update_attributes!(allow_global_tags: true)
end
it 'filters tags correctly' do
expect(filter_allowed_tags(for_input: true, category: category)).to contain_exactly(tag1, tag2, tag3, tag4)
expect(filter_allowed_tags(for_input: true)).to contain_exactly(tag3, tag4)
expect(filter_allowed_tags(for_input: true, category: other_category)).to contain_exactly(tag3, tag4)
tag_group1.tags = [tag2, tag3, tag4]
expect(filter_allowed_tags(for_input: true, category: category)).to contain_exactly(tag1, tag2, tag3, tag4)
expect(filter_allowed_tags(for_input: true)).to contain_exactly(tag1)
expect(filter_allowed_tags(for_input: true, category: other_category)).to contain_exactly(tag1)
end
it "works if no tags are restricted to the category" do
other_category.update_attributes!(allow_global_tags: true)
expect(filter_allowed_tags(for_input: true, category: other_category)).to contain_exactly(tag3, tag4)
tag_group1.tags = [tag2, tag3, tag4]
expect(filter_allowed_tags(for_input: true, category: other_category)).to contain_exactly(tag1)
end
context 'another category has restricted tags using groups' do
let(:category2) { Fabricate(:category) }
let(:tag_group2) { Fabricate(:tag_group) }
before do
tag_group2.tags = [tag2, tag3]
category2.allowed_tag_groups = [tag_group2.name]
category2.reload
end
it 'filters tags correctly' do
expect(filter_allowed_tags(for_input: true, category: category2)).to contain_exactly(tag2, tag3)
expect(filter_allowed_tags(for_input: true)).to contain_exactly(tag4)
expect(filter_allowed_tags(for_input: true, category: other_category)).to contain_exactly(tag4)
expect(filter_allowed_tags(for_input: true, category: category)).to contain_exactly(tag1, tag2, tag4)
end
it "doesn't care about tags in a group that isn't used in a category" do
unused_tag_group = Fabricate(:tag_group)
unused_tag_group.tags = [tag4]
expect(filter_allowed_tags(for_input: true, category: category2)).to contain_exactly(tag2, tag3)
expect(filter_allowed_tags(for_input: true)).to contain_exactly(tag4)
expect(filter_allowed_tags(for_input: true, category: other_category)).to contain_exactly(tag4)
expect(filter_allowed_tags(for_input: true, category: category)).to contain_exactly(tag1, tag2, tag4) # tag4 missing
end
end
context 'another category has restricted tags' do
let(:category2) { Fabricate(:category) }
it "doesn't filter tags that are also restricted in another category" do
category2.tags = [tag2, tag3]
expect(filter_allowed_tags(for_input: true, category: category2)).to contain_exactly(tag2, tag3)
expect(filter_allowed_tags(for_input: true)).to contain_exactly(tag4)
expect(filter_allowed_tags(for_input: true, category: other_category)).to contain_exactly(tag4)
expect(filter_allowed_tags(for_input: true, category: category)).to contain_exactly(tag1, tag2, tag4) # tag2 missing
end
end
end
end
context "tag groups with parent tag" do

View File

@ -311,7 +311,7 @@ describe CategoriesController do
end
describe "success" do
it "updates the group correctly" do
it "updates attributes correctly" do
readonly = CategoryGroup.permission_types[:readonly]
create_post = CategoryGroup.permission_types[:create_post]
@ -328,7 +328,8 @@ describe CategoriesController do
custom_fields: {
"dancing" => "frogs"
},
minimum_required_tags: ""
minimum_required_tags: "",
allow_global_tags: 'true'
}
expect(response.status).to eq(200)
@ -342,6 +343,7 @@ describe CategoriesController do
expect(category.auto_close_hours).to eq(72)
expect(category.custom_fields).to eq("dancing" => "frogs")
expect(category.minimum_required_tags).to eq(0)
expect(category.allow_global_tags).to eq(true)
end
it 'logs the changes correctly' do