FEATURE: restrict tags to be used in a category
This commit is contained in:
parent
26f25fc0d9
commit
6796b15857
|
@ -0,0 +1,4 @@
|
|||
import { buildCategoryPanel } from 'discourse/components/edit-category-panel';
|
||||
|
||||
export default buildCategoryPanel('tags', {
|
||||
});
|
|
@ -6,7 +6,7 @@ function formatTag(t) {
|
|||
|
||||
export default Ember.TextField.extend({
|
||||
classNameBindings: [':tag-chooser'],
|
||||
attributeBindings: ['tabIndex'],
|
||||
attributeBindings: ['tabIndex', 'placeholderKey', 'categoryId'],
|
||||
|
||||
_setupTags: function() {
|
||||
const tags = this.get('tags') || [];
|
||||
|
@ -25,7 +25,7 @@ export default Ember.TextField.extend({
|
|||
|
||||
this.$().select2({
|
||||
tags: true,
|
||||
placeholder: I18n.t('tagging.choose_for_topic'),
|
||||
placeholder: I18n.t(this.get('placeholderKey') || 'tagging.choose_for_topic'),
|
||||
maximumInputLength: this.siteSettings.max_tag_length,
|
||||
maximumSelectionSize: this.siteSettings.max_tags_per_topic,
|
||||
initSelection(element, callback) {
|
||||
|
@ -78,7 +78,7 @@ export default Ember.TextField.extend({
|
|||
url: Discourse.getURL("/tags/filter/search"),
|
||||
dataType: 'json',
|
||||
data: function (term) {
|
||||
return { q: term, limit: self.siteSettings.max_tag_search_results, filterForInput: true };
|
||||
return { q: term, limit: self.siteSettings.max_tag_search_results, filterForInput: true, categoryId: self.get('categoryId') };
|
||||
},
|
||||
results: function (data) {
|
||||
if (self.siteSettings.tags_sort_alphabetically) {
|
||||
|
|
|
@ -86,7 +86,8 @@ const Category = RestModel.extend({
|
|||
allow_badges: this.get('allow_badges'),
|
||||
custom_fields: this.get('custom_fields'),
|
||||
topic_template: this.get('topic_template'),
|
||||
suppress_from_homepage: this.get('suppress_from_homepage')
|
||||
suppress_from_homepage: this.get('suppress_from_homepage'),
|
||||
allowed_tags: this.get('allowed_tags')
|
||||
},
|
||||
type: this.get('id') ? 'PUT' : 'POST'
|
||||
});
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
<section class="field">
|
||||
<p>{{i18n 'category.tags_allowed_tags'}}</p>
|
||||
{{tag-chooser placeholderKey="category.tags_placeholder" tags=category.allowed_tags}}
|
||||
</section>
|
|
@ -98,7 +98,7 @@
|
|||
<div class='submit-panel'>
|
||||
{{plugin-outlet "composer-fields-below"}}
|
||||
{{#if canEditTags}}
|
||||
{{tag-chooser tags=model.tags tabIndex="4"}}
|
||||
{{tag-chooser tags=model.tags tabIndex="4" categoryId=model.categoryId}}
|
||||
{{/if}}
|
||||
<button {{action "save"}} tabindex="5" class="btn btn-primary create {{if disableSubmit 'disabled'}}" title="{{i18n 'composer.title'}}">{{{model.saveIcon}}}{{model.saveText}}</button>
|
||||
<a href {{action "cancel"}} class='cancel' tabindex="6">{{i18n 'cancel'}}</a>
|
||||
|
|
|
@ -7,6 +7,9 @@
|
|||
{{edit-category-tab panels=panels selectedTab=selectedTab tab="settings"}}
|
||||
{{edit-category-tab panels=panels selectedTab=selectedTab tab="images"}}
|
||||
{{edit-category-tab panels=panels selectedTab=selectedTab tab="topic-template"}}
|
||||
{{#if siteSettings.tagging_enabled}}
|
||||
{{edit-category-tab panels=panels selectedTab=selectedTab tab="tags"}}
|
||||
{{/if}}
|
||||
</ul>
|
||||
|
||||
<div class="modal-body">
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
|
||||
{{#if canEditTags}}
|
||||
<br>
|
||||
{{tag-chooser tags=buffered.tags}}
|
||||
{{tag-chooser tags=buffered.tags categoryId=buffered.category_id}}
|
||||
{{/if}}
|
||||
|
||||
{{plugin-outlet "edit-topic"}}
|
||||
|
|
|
@ -194,7 +194,8 @@ class CategoriesController < ApplicationController
|
|||
:allow_badges,
|
||||
:topic_template,
|
||||
:custom_fields => [params[:custom_fields].try(:keys)],
|
||||
:permissions => [*p.try(:keys)])
|
||||
:permissions => [*p.try(:keys)],
|
||||
:allowed_tags => [])
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -104,18 +104,15 @@ class TagsController < ::ApplicationController
|
|||
end
|
||||
|
||||
def search
|
||||
query = self.class.tags_by_count(guardian, params.slice(:limit))
|
||||
term = params[:q]
|
||||
if term.present?
|
||||
term.gsub!(/[^a-z0-9\.\-\_]*/, '')
|
||||
term.gsub!("_", "\\_")
|
||||
query = query.where('tags.name like ?', "%#{term}%")
|
||||
end
|
||||
|
||||
if params[:filterForInput] && !guardian.is_staff?
|
||||
staff_tag_names = SiteSetting.staff_tags.split("|")
|
||||
query = query.where('tags.name NOT IN (?)', staff_tag_names) if staff_tag_names.present?
|
||||
end
|
||||
query = DiscourseTagging.filter_allowed_tags(
|
||||
self.class.tags_by_count(guardian, params.slice(:limit)),
|
||||
guardian,
|
||||
{
|
||||
for_input: params[:filterForInput],
|
||||
term: params[:q],
|
||||
category: params[:categoryId] ? Category.find_by_id(params[:categoryId]) : nil
|
||||
}
|
||||
)
|
||||
|
||||
tags = query.count.map {|t, c| { id: t, text: t, count: c } }
|
||||
|
||||
|
|
|
@ -55,6 +55,9 @@ class Category < ActiveRecord::Base
|
|||
belongs_to :parent_category, class_name: 'Category'
|
||||
has_many :subcategories, class_name: 'Category', foreign_key: 'parent_category_id'
|
||||
|
||||
has_many :category_tags
|
||||
has_many :tags, through: :category_tags
|
||||
|
||||
scope :latest, ->{ order('topic_count desc') }
|
||||
|
||||
scope :secured, ->(guardian = nil) {
|
||||
|
@ -312,6 +315,12 @@ SQL
|
|||
end
|
||||
end
|
||||
|
||||
def allowed_tags=(tag_names)
|
||||
if self.tags.pluck(:name).sort != tag_names.sort
|
||||
self.tags = Tag.where(name: tag_names).all
|
||||
end
|
||||
end
|
||||
|
||||
def downcase_email
|
||||
self.email_in = (email_in || "").strip.downcase.presence
|
||||
end
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
class CategoryTag < ActiveRecord::Base
|
||||
belongs_to :category
|
||||
belongs_to :tag
|
||||
end
|
|
@ -1,8 +1,13 @@
|
|||
class Tag < ActiveRecord::Base
|
||||
validates :name, presence: true, uniqueness: true
|
||||
|
||||
has_many :tag_users # notification settings
|
||||
|
||||
has_many :topic_tags, dependent: :destroy
|
||||
has_many :topics, through: :topic_tags
|
||||
has_many :tag_users
|
||||
|
||||
has_many :category_tags, dependent: :destroy
|
||||
has_many :categories, through: :category_tags
|
||||
|
||||
def self.tags_by_count_query(opts={})
|
||||
q = TopicTag.joins(:tag, :topic).group("topic_tags.tag_id, tags.name").order('count_all DESC')
|
||||
|
|
|
@ -13,7 +13,8 @@ class CategorySerializer < BasicCategorySerializer
|
|||
:cannot_delete_reason,
|
||||
:is_special,
|
||||
:allow_badges,
|
||||
:custom_fields
|
||||
:custom_fields,
|
||||
:allowed_tags
|
||||
|
||||
def group_permissions
|
||||
@group_permissions ||= begin
|
||||
|
@ -77,4 +78,12 @@ class CategorySerializer < BasicCategorySerializer
|
|||
(user && CategoryUser.where(user: user, category: object).first.try(:notification_level))
|
||||
end
|
||||
|
||||
def include_allowed_tags?
|
||||
SiteSetting.tagging_enabled
|
||||
end
|
||||
|
||||
def allowed_tags
|
||||
object.tags.pluck(:name)
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -1716,6 +1716,9 @@ en:
|
|||
general: 'General'
|
||||
settings: 'Settings'
|
||||
topic_template: "Topic Template"
|
||||
tags: "Tags"
|
||||
tags_allowed_tags: "Tags that can only be used in this category:"
|
||||
tags_placeholder: "(Optional) list of allowed tags"
|
||||
delete: 'Delete Category'
|
||||
create: 'New Category'
|
||||
create_long: 'Create a new category'
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
class CreateCategoryTags < ActiveRecord::Migration
|
||||
def change
|
||||
create_table :category_tags do |t|
|
||||
t.references :category, null: false
|
||||
t.references :tag, null: false
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
add_index :category_tags, [:category_id, :tag_id], name: "idx_category_tags_ix1", unique: true
|
||||
add_index :category_tags, [:tag_id, :category_id], name: "idx_category_tags_ix2", unique: true
|
||||
end
|
||||
end
|
|
@ -32,12 +32,14 @@ module DiscourseTagging
|
|||
end
|
||||
|
||||
if tag_names.present?
|
||||
tags = Tag.where(name: tag_names).all
|
||||
if tags.size < tag_names.size
|
||||
existing_names = tags.map(&:name)
|
||||
category = topic.category
|
||||
tags = filter_allowed_tags(Tag.where(name: tag_names), guardian, { for_input: true, category: category }).to_a
|
||||
|
||||
if tags.size < tag_names.size && (category.nil? || category.tags.count == 0)
|
||||
tag_names.each do |name|
|
||||
next if existing_names.include?(name)
|
||||
tags << Tag.create(name: name)
|
||||
unless Tag.where(name: name).exists?
|
||||
tags << Tag.create(name: name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -52,6 +54,34 @@ module DiscourseTagging
|
|||
true
|
||||
end
|
||||
|
||||
# Options:
|
||||
# term: a search term to filter tags by name
|
||||
# for_input: result is for an input field, so only show permitted tags
|
||||
# category: a Category to which the object being tagged belongs
|
||||
def self.filter_allowed_tags(query, guardian, opts={})
|
||||
term = opts[:term]
|
||||
if term.present?
|
||||
term.gsub!(/[^a-z0-9\.\-\_]*/, '')
|
||||
term.gsub!("_", "\\_")
|
||||
query = query.where('tags.name like ?', "%#{term}%")
|
||||
end
|
||||
|
||||
if opts[:for_input]
|
||||
unless guardian.is_staff?
|
||||
staff_tag_names = SiteSetting.staff_tags.split("|")
|
||||
query = query.where('tags.name NOT IN (?)', staff_tag_names) if staff_tag_names.present?
|
||||
end
|
||||
|
||||
if opts[:category] && opts[:category].tags.count > 0
|
||||
query = query.where("tags.id IN (SELECT tag_id FROM category_tags WHERE category_id = ?)", opts[:category].id)
|
||||
elsif CategoryTag.exists?
|
||||
query = query.where("tags.id NOT IN (SELECT tag_id FROM category_tags)")
|
||||
end
|
||||
end
|
||||
|
||||
query
|
||||
end
|
||||
|
||||
def self.auto_notify_for(tags, topic)
|
||||
TagUser.auto_watch_new_topic(topic, tags)
|
||||
TagUser.auto_track_new_topic(topic, tags)
|
||||
|
@ -78,17 +108,16 @@ module DiscourseTagging
|
|||
|
||||
return unless tags.present?
|
||||
|
||||
tags.map! {|t| clean_tag(t) }
|
||||
tags.delete_if {|t| t.blank? }
|
||||
tags.uniq!
|
||||
tag_names = tags.map {|t| clean_tag(t) }
|
||||
tag_names.delete_if {|t| t.blank? }
|
||||
tag_names.uniq!
|
||||
|
||||
# If the user can't create tags, remove any tags that don't already exist
|
||||
# TODO: this is doing a full count, it should just check first or use a cache
|
||||
unless guardian.can_create_tag?
|
||||
tags = Tag.where(name: tags).pluck(:name)
|
||||
tag_names = Tag.where(name: tag_names).pluck(:name)
|
||||
end
|
||||
|
||||
return tags[0...SiteSetting.max_tags_per_topic]
|
||||
return tag_names[0...SiteSetting.max_tags_per_topic]
|
||||
end
|
||||
|
||||
def self.notification_key(tag_id)
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
# encoding: UTF-8
|
||||
|
||||
require 'rails_helper'
|
||||
require_dependency 'post_creator'
|
||||
|
||||
describe "category tag restrictions" do
|
||||
let!(:tag1) { Fabricate(:tag) }
|
||||
let!(:tag2) { Fabricate(:tag) }
|
||||
let!(:tag3) { Fabricate(:tag) }
|
||||
let!(:tag4) { Fabricate(:tag) }
|
||||
|
||||
let(:user) { Fabricate(:user) }
|
||||
let(:admin) { Fabricate(:admin) }
|
||||
|
||||
before do
|
||||
SiteSetting.tagging_enabled = true
|
||||
SiteSetting.min_trust_to_create_tag = 0
|
||||
SiteSetting.min_trust_level_to_tag_topics = 0
|
||||
end
|
||||
|
||||
context "tags restricted to one category" do
|
||||
let(:category_with_tags) { Fabricate(:category) }
|
||||
let(:other_category) { Fabricate(:category) }
|
||||
|
||||
before do
|
||||
category_with_tags.tags = [tag1, tag2]
|
||||
end
|
||||
|
||||
it "tags belonging to that category can only be used there" do
|
||||
post = create_post(category: category_with_tags, tags: [tag1.name, tag2.name, tag3.name])
|
||||
expect(post.topic.tags.map(&:name).sort).to eq([tag1.name, tag2.name].sort)
|
||||
|
||||
post = create_post(category: other_category, tags: [tag1.name, tag2.name, tag3.name])
|
||||
expect(post.topic.tags.map(&:name)).to eq([tag3.name])
|
||||
end
|
||||
|
||||
it "search can show only permitted tags" do
|
||||
expect(DiscourseTagging.filter_allowed_tags(Tag.all, Guardian.new(user)).count).to eq(Tag.count)
|
||||
expect(DiscourseTagging.filter_allowed_tags(Tag.all, Guardian.new(user), {for_input: true, category: category_with_tags}).pluck(:name).sort).to eq([tag1.name, tag2.name].sort)
|
||||
expect(DiscourseTagging.filter_allowed_tags(Tag.all, Guardian.new(user), {for_input: true}).pluck(:name).sort).to eq([tag3.name, tag4.name].sort)
|
||||
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.map(&:name)).to eq([tag1.name])
|
||||
post = create_post(category: category_with_tags, tags: [tag1.name, "newtag"], user: admin)
|
||||
expect(post.topic.tags.map(&:name)).to eq([tag1.name])
|
||||
end
|
||||
|
||||
it "can create new tags in a non-restricted category" do
|
||||
post = create_post(category: other_category, tags: [tag3.name, "newtag"])
|
||||
expect(post.topic.tags.map(&:name).sort).to eq([tag3.name, "newtag"].sort)
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue