Change hashtags to preserve first-used casing (#11416)
This commit is contained in:
		| @ -148,12 +148,9 @@ class ActivityPub::Activity::Create < ActivityPub::Activity | |||||||
|   def process_hashtag(tag) |   def process_hashtag(tag) | ||||||
|     return if tag['name'].blank? |     return if tag['name'].blank? | ||||||
|  |  | ||||||
|     hashtag = tag['name'].gsub(/\A#/, '').mb_chars.downcase |     Tag.find_or_create_by_names(tag['name']) do |hashtag| | ||||||
|     hashtag = Tag.where(name: hashtag).first_or_create!(name: hashtag) |       @tags << hashtag unless @tags.include?(hashtag) | ||||||
|  |     end | ||||||
|     return if @tags.include?(hashtag) |  | ||||||
|  |  | ||||||
|     @tags << hashtag |  | ||||||
|   rescue ActiveRecord::RecordInvalid |   rescue ActiveRecord::RecordInvalid | ||||||
|     nil |     nil | ||||||
|   end |   end | ||||||
|  | |||||||
| @ -20,7 +20,7 @@ class Tag < ApplicationRecord | |||||||
|   HASHTAG_NAME_RE = '([[:word:]_][[:word:]_·]*[[:alpha:]_·][[:word:]_·]*[[:word:]_])|([[:word:]_]*[[:alpha:]][[:word:]_]*)' |   HASHTAG_NAME_RE = '([[:word:]_][[:word:]_·]*[[:alpha:]_·][[:word:]_·]*[[:word:]_])|([[:word:]_]*[[:alpha:]][[:word:]_]*)' | ||||||
|   HASHTAG_RE = /(?:^|[^\/\)\w])#(#{HASHTAG_NAME_RE})/i |   HASHTAG_RE = /(?:^|[^\/\)\w])#(#{HASHTAG_NAME_RE})/i | ||||||
|  |  | ||||||
|   validates :name, presence: true, uniqueness: true, format: { with: /\A(#{HASHTAG_NAME_RE})\z/i } |   validates :name, presence: true, format: { with: /\A(#{HASHTAG_NAME_RE})\z/i } | ||||||
|  |  | ||||||
|   scope :discoverable, -> { joins(:account_tag_stat).where(AccountTagStat.arel_table[:accounts_count].gt(0)).where(account_tag_stats: { hidden: false }).order(Arel.sql('account_tag_stats.accounts_count desc')) } |   scope :discoverable, -> { joins(:account_tag_stat).where(AccountTagStat.arel_table[:accounts_count].gt(0)).where(account_tag_stats: { hidden: false }).order(Arel.sql('account_tag_stats.accounts_count desc')) } | ||||||
|   scope :hidden, -> { where(account_tag_stats: { hidden: true }) } |   scope :hidden, -> { where(account_tag_stats: { hidden: true }) } | ||||||
| @ -64,22 +64,48 @@ class Tag < ApplicationRecord | |||||||
|   end |   end | ||||||
|  |  | ||||||
|   class << self |   class << self | ||||||
|     def search_for(term, limit = 5, offset = 0) |     def find_or_create_by_names(name_or_names) | ||||||
|       pattern = sanitize_sql_like(term.strip) + '%' |       Array(name_or_names).map(&method(:normalize)).uniq.map do |normalized_name| | ||||||
|  |         tag = matching_name(normalized_name).first || create(name: normalized_name) | ||||||
|  |  | ||||||
|       Tag.where('lower(name) like lower(?)', pattern) |         yield tag if block_given? | ||||||
|  |  | ||||||
|  |         tag | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     def search_for(term, limit = 5, offset = 0) | ||||||
|  |       pattern = sanitize_sql_like(normalize(term.strip)) + '%' | ||||||
|  |  | ||||||
|  |       Tag.where(arel_table[:name].lower.matches(pattern.downcase)) | ||||||
|          .order(:name) |          .order(:name) | ||||||
|          .limit(limit) |          .limit(limit) | ||||||
|          .offset(offset) |          .offset(offset) | ||||||
|     end |     end | ||||||
|  |  | ||||||
|     def find_normalized(name) |     def find_normalized(name) | ||||||
|       find_by(name: name.mb_chars.downcase.to_s) |       matching_name(name).first | ||||||
|     end |     end | ||||||
|  |  | ||||||
|     def find_normalized!(name) |     def find_normalized!(name) | ||||||
|       find_normalized(name) || raise(ActiveRecord::RecordNotFound) |       find_normalized(name) || raise(ActiveRecord::RecordNotFound) | ||||||
|     end |     end | ||||||
|  |  | ||||||
|  |     def matching_name(name_or_names) | ||||||
|  |       names = Array(name_or_names).map { |name| normalize(name).downcase } | ||||||
|  |  | ||||||
|  |       if names.size == 1 | ||||||
|  |         where(arel_table[:name].lower.eq(names.first)) | ||||||
|  |       else | ||||||
|  |         where(arel_table[:name].lower.in(names)) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |  | ||||||
|  |     private | ||||||
|  |  | ||||||
|  |     def normalize(str) | ||||||
|  |       str.gsub(/\A#/, '').mb_chars.to_s | ||||||
|  |     end | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   private |   private | ||||||
|  | |||||||
| @ -14,7 +14,7 @@ class HashtagQueryService < BaseService | |||||||
|  |  | ||||||
|   private |   private | ||||||
|  |  | ||||||
|   def tags_for(tags) |   def tags_for(names) | ||||||
|     Tag.where(name: tags.map(&:downcase)) if tags.presence |     Tag.matching_name(names) if names.presence | ||||||
|   end |   end | ||||||
| end | end | ||||||
|  | |||||||
| @ -5,9 +5,7 @@ class ProcessHashtagsService < BaseService | |||||||
|     tags    = Extractor.extract_hashtags(status.text) if status.local? |     tags    = Extractor.extract_hashtags(status.text) if status.local? | ||||||
|     records = [] |     records = [] | ||||||
|  |  | ||||||
|     tags.map { |str| str.mb_chars.downcase }.uniq(&:to_s).each do |name| |     Tag.find_or_create_by_names(tags) do |tag| | ||||||
|       tag = Tag.where(name: name).first_or_create(name: name) |  | ||||||
|  |  | ||||||
|       status.tags << tag |       status.tags << tag | ||||||
|       records << tag |       records << tag | ||||||
|  |  | ||||||
|  | |||||||
| @ -0,0 +1,15 @@ | |||||||
|  | class AddCaseInsensitiveIndexToTags < ActiveRecord::Migration[5.2] | ||||||
|  |   disable_ddl_transaction! | ||||||
|  |  | ||||||
|  |   def up | ||||||
|  |     safety_assured { execute 'CREATE UNIQUE INDEX CONCURRENTLY index_tags_on_name_lower ON tags (lower(name))' } | ||||||
|  |     remove_index :tags, name: 'index_tags_on_name' | ||||||
|  |     remove_index :tags, name: 'hashtag_search_index' | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   def down | ||||||
|  |     add_index :tags, :name, unique: true, algorithm: :concurrently | ||||||
|  |     safety_assured { execute 'CREATE INDEX CONCURRENTLY hashtag_search_index ON tags (name text_pattern_ops)' } | ||||||
|  |     remove_index :tags, name: 'index_tags_on_name_lower' | ||||||
|  |   end | ||||||
|  | end | ||||||
| @ -10,7 +10,7 @@ | |||||||
| # | # | ||||||
| # It's strongly recommended that you check this file into your version control system. | # It's strongly recommended that you check this file into your version control system. | ||||||
|  |  | ||||||
| ActiveRecord::Schema.define(version: 2019_07_15_164535) do | ActiveRecord::Schema.define(version: 2019_07_26_175042) do | ||||||
|  |  | ||||||
|   # These are extensions that must be enabled in order to support this database |   # These are extensions that must be enabled in order to support this database | ||||||
|   enable_extension "plpgsql" |   enable_extension "plpgsql" | ||||||
| @ -652,8 +652,7 @@ ActiveRecord::Schema.define(version: 2019_07_15_164535) do | |||||||
|     t.string "name", default: "", null: false |     t.string "name", default: "", null: false | ||||||
|     t.datetime "created_at", null: false |     t.datetime "created_at", null: false | ||||||
|     t.datetime "updated_at", null: false |     t.datetime "updated_at", null: false | ||||||
|     t.index "lower((name)::text) text_pattern_ops", name: "hashtag_search_index" |     t.index "lower((name)::text)", name: "index_tags_on_name_lower", unique: true | ||||||
|     t.index ["name"], name: "index_tags_on_name", unique: true |  | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   create_table "tombstones", force: :cascade do |t| |   create_table "tombstones", force: :cascade do |t| | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user