FEATURE: Mixed case tagging (#6454)

- By default, behaviour is not changed: tags are made lowercase upon creation and edit.

- If force_lowercase_tags is disabled, then mixed case tags are allowed.

- Tags must remain case-insensitively unique. This is enforced by ActiveRecord and Postgres.

- A migration is added to provide a `UNIQUE` index on `lower(name)`. Migration includes a safety to correct any current tags that do not meet the criteria.

- A `where_name` scope is added to `models/tag.rb`, to allow easy case-insensitive lookups. This is used instead of `Tag.where(name: "blah")`.

- URLs remain lowercase. Mixed case URLs are functional, but have the lowercase equivalent as the canonical.
This commit is contained in:
David Taylor
2018-10-05 10:23:52 +01:00
committed by GitHub
parent 8430ea927e
commit 9bf522f227
23 changed files with 137 additions and 43 deletions

View File

@@ -39,7 +39,7 @@ module DiscourseTagging
# guardian is explicitly nil cause we don't want to strip all
# staff tags that already passed validation
tags = filter_allowed_tags(
Tag.where(name: tag_names),
Tag.where_name(tag_names),
nil, # guardian
for_topic: true,
category: category,
@@ -48,7 +48,7 @@ module DiscourseTagging
if tags.size < tag_names.size && (category.nil? || (category.tags.count == 0 && category.tag_groups.count == 0))
tag_names.each do |name|
unless Tag.where(name: name).exists?
unless Tag.where_name(name).exists?
tags << Tag.create(name: name)
end
end
@@ -82,8 +82,7 @@ module DiscourseTagging
# for_topic: results are for tagging a topic
# selected_tags: an array of tag names that are in the current selection
def self.filter_allowed_tags(query, guardian, opts = {})
selected_tag_ids = opts[:selected_tags] ? Tag.where(name: opts[:selected_tags]).pluck(:id) : []
selected_tag_ids = opts[:selected_tags] ? Tag.where_name(opts[:selected_tags]).pluck(:id) : []
if !opts[:for_topic] && !selected_tag_ids.empty?
query = query.where('tags.id NOT IN (?)', selected_tag_ids)
@@ -92,8 +91,8 @@ module DiscourseTagging
term = opts[:term]
if term.present?
term.gsub!("_", "\\_")
term = clean_tag(term)
query = query.where('tags.name like ?', "%#{term}%")
term = clean_tag(term).downcase
query = query.where('lower(tags.name) like ?', "%#{term}%")
end
# Filters for category-specific tags:
@@ -203,7 +202,8 @@ module DiscourseTagging
end
def self.clean_tag(tag)
tag.downcase.strip
tag.downcase! if SiteSetting.force_lowercase_tags
tag.strip
.gsub(/\s+/, '-').squeeze('-')
.gsub(TAGS_FILTER_REGEXP, '')[0...SiteSetting.max_tag_length]
end
@@ -212,7 +212,7 @@ module DiscourseTagging
return [] unless guardian.can_tag_topics? && tags_arg.present?
tag_names = Tag.where(name: tags_arg).pluck(:name)
tag_names = Tag.where_name(tags_arg).pluck(:name)
if guardian.can_create_tag?
tag_names += (tags_arg - tag_names).map { |t| clean_tag(t) }
@@ -226,7 +226,7 @@ module DiscourseTagging
def self.add_or_create_tags_by_name(taggable, tag_names_arg, opts = {})
tag_names = DiscourseTagging.tags_for_saving(tag_names_arg, Guardian.new(Discourse.system_user), opts) || []
if taggable.tags.pluck(:name).sort != tag_names.sort
taggable.tags = Tag.where(name: tag_names).all
taggable.tags = Tag.where_name(tag_names).all
if taggable.tags.size < tag_names.size
new_tag_names = tag_names - taggable.tags.map(&:name)
new_tag_names.each do |name|