From e0f859edd2c1f5d1d62f612478e11da8625e94bd Mon Sep 17 00:00:00 2001 From: Abdelkader Boudih Date: Sun, 5 Dec 2021 22:49:37 +0100 Subject: [PATCH] chore: update formatting and style of all files (#1066) --- .github/workflows/spec.yml | 9 +- Appraisals | 14 +- gemfiles/activerecord_6.0.gemfile | 10 +- gemfiles/activerecord_6.1.gemfile | 11 +- gemfiles/activerecord_7.0.gemfile | 11 +- lib/acts_as_taggable_on/default_parser.rb | 18 +- lib/acts_as_taggable_on/engine.rb | 2 + lib/acts_as_taggable_on/generic_parser.rb | 2 + lib/acts_as_taggable_on/tag.rb | 60 +-- lib/acts_as_taggable_on/tag_list.rb | 19 +- lib/acts_as_taggable_on/taggable.rb | 28 +- lib/acts_as_taggable_on/taggable/cache.rb | 126 ++--- .../taggable/collection.rb | 315 ++++++------ lib/acts_as_taggable_on/taggable/core.rb | 484 +++++++++--------- lib/acts_as_taggable_on/taggable/ownership.rb | 208 ++++---- lib/acts_as_taggable_on/taggable/related.rb | 107 ++-- .../taggable/tag_list_type.rb | 8 +- .../taggable/tagged_with_query.rb | 22 +- .../tagged_with_query/all_tags_query.rb | 216 ++++---- .../tagged_with_query/any_tags_query.rb | 110 ++-- .../tagged_with_query/exclude_tags_query.rb | 123 ++--- .../taggable/tagged_with_query/query_base.rb | 100 ++-- lib/acts_as_taggable_on/tagger.rb | 14 +- lib/acts_as_taggable_on/tagging.rb | 10 +- lib/acts_as_taggable_on/tags_helper.rb | 4 +- lib/acts_as_taggable_on/version.rb | 2 + spec/support/database.rb | 69 +-- 27 files changed, 1101 insertions(+), 1001 deletions(-) diff --git a/.github/workflows/spec.yml b/.github/workflows/spec.yml index 87bb97b50..f2e1362f9 100644 --- a/.github/workflows/spec.yml +++ b/.github/workflows/spec.yml @@ -12,10 +12,10 @@ jobs: strategy: matrix: ruby: - - 2.5 - - 2.6 - - 2.7 - 3.0 + - 2.7 + - 2.6 + - 2.5 - head gemfile: - gemfiles/activerecord_6.0.gemfile @@ -29,6 +29,9 @@ jobs: - ruby: truffleruby-head db: postgresql gemfile: gemfiles/activerecord_6.1.gemfile + - ruby: truffleruby-head + db: postgresql + gemfile: gemfiles/activerecord_7.0.gemfile exclude: - ruby: 2.5 gemfile: gemfiles/activerecord_7.0.gemfile diff --git a/Appraisals b/Appraisals index c4270eb89..d4a5436c0 100644 --- a/Appraisals +++ b/Appraisals @@ -1,13 +1,19 @@ - +# frozen_string_literal: true appraise 'activerecord-6.0' do - gem 'activerecord', "~> 6.0.0" + gem 'activerecord', '~> 6.0.0' + gem 'pg' + gem 'mysql2', '~> 0.5' end appraise 'activerecord-6.1' do - gem 'activerecord', "~> 6.1.0" + gem 'activerecord', '~> 6.1.0' + gem 'pg' + gem 'mysql2', '~> 0.5' end appraise 'activerecord-7.0' do - gem 'activerecord', "~> 7.0.0.alpha2" + gem 'activerecord', '~> 7.0.0.alpha2' + gem 'pg' + gem 'mysql2', '~> 0.5' end diff --git a/gemfiles/activerecord_6.0.gemfile b/gemfiles/activerecord_6.0.gemfile index decb9e83d..72eaced98 100644 --- a/gemfiles/activerecord_6.0.gemfile +++ b/gemfiles/activerecord_6.0.gemfile @@ -3,14 +3,8 @@ source "https://rubygems.org" gem "activerecord", "~> 6.0.0" -case ENV["DB"] -when "postgresql" - gem 'pg' -when "mysql" - gem 'mysql2', '~> 0.5' -else - gem 'sqlite3' -end +gem "pg" +gem "mysql2", "~> 0.5" group :local_development do gem "guard" diff --git a/gemfiles/activerecord_6.1.gemfile b/gemfiles/activerecord_6.1.gemfile index 718da068b..0538ded5b 100644 --- a/gemfiles/activerecord_6.1.gemfile +++ b/gemfiles/activerecord_6.1.gemfile @@ -3,14 +3,9 @@ source "https://rubygems.org" gem "activerecord", "~> 6.1.0" -case ENV["DB"] -when "postgresql" - gem 'pg' -when "mysql" - gem 'mysql2', '~> 0.5' -else - gem 'sqlite3' -end +gem "pg" +gem "mysql2", "~> 0.5" + group :local_development do gem "guard" gem "guard-rspec" diff --git a/gemfiles/activerecord_7.0.gemfile b/gemfiles/activerecord_7.0.gemfile index a80449727..b49223955 100644 --- a/gemfiles/activerecord_7.0.gemfile +++ b/gemfiles/activerecord_7.0.gemfile @@ -3,14 +3,9 @@ source "https://rubygems.org" gem "activerecord", "~> 7.0.0.alpha2" -case ENV["DB"] -when "postgresql" - gem 'pg' -when "mysql" - gem 'mysql2', '~> 0.5' -else - gem 'sqlite3' -end +gem "pg" +gem "mysql2", "~> 0.5" + group :local_development do gem "guard" gem "guard-rspec" diff --git a/lib/acts_as_taggable_on/default_parser.rb b/lib/acts_as_taggable_on/default_parser.rb index 5eb6396bb..b022c8124 100644 --- a/lib/acts_as_taggable_on/default_parser.rb +++ b/lib/acts_as_taggable_on/default_parser.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActsAsTaggableOn ## # Returns a new TagList using the given tag string. @@ -6,7 +8,6 @@ module ActsAsTaggableOn # tag_list = ActsAsTaggableOn::DefaultParser.parse("One , Two, Three") # tag_list # ["One", "Two", "Three"] class DefaultParser < GenericParser - def parse string = @tag_list @@ -14,33 +15,32 @@ def parse TagList.new.tap do |tag_list| string = string.to_s.dup - string.gsub!(double_quote_pattern) { + string.gsub!(double_quote_pattern) do # Append the matched tag to the tag list tag_list << Regexp.last_match[2] # Return the matched delimiter ($3) to replace the matched items '' - } + end - string.gsub!(single_quote_pattern) { + string.gsub!(single_quote_pattern) do # Append the matched tag ($2) to the tag list tag_list << Regexp.last_match[2] # Return an empty string to replace the matched items '' - } + end # split the string by the delimiter # and add to the tag_list - tag_list.add(string.split(Regexp.new delimiter)) + tag_list.add(string.split(Regexp.new(delimiter))) end end - # private def delimiter # Parse the quoted tags d = ActsAsTaggableOn.delimiter # Separate multiple delimiters by bitwise operator - d = d.join('|') if d.kind_of?(Array) + d = d.join('|') if d.is_a?(Array) d end @@ -73,7 +73,5 @@ def double_quote_pattern def single_quote_pattern /(\A|#{delimiter})\s*'(.*?)'\s*(?=#{delimiter}\s*|\z)/ end - end - end diff --git a/lib/acts_as_taggable_on/engine.rb b/lib/acts_as_taggable_on/engine.rb index 6c491f8c5..fe3c4004d 100644 --- a/lib/acts_as_taggable_on/engine.rb +++ b/lib/acts_as_taggable_on/engine.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActsAsTaggableOn class Engine < Rails::Engine end diff --git a/lib/acts_as_taggable_on/generic_parser.rb b/lib/acts_as_taggable_on/generic_parser.rb index 68ae578e6..2b6308d0e 100644 --- a/lib/acts_as_taggable_on/generic_parser.rb +++ b/lib/acts_as_taggable_on/generic_parser.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActsAsTaggableOn ## # Returns a new TagList using the given tag string. diff --git a/lib/acts_as_taggable_on/tag.rb b/lib/acts_as_taggable_on/tag.rb index b8340ddf3..6f6e9d023 100644 --- a/lib/acts_as_taggable_on/tag.rb +++ b/lib/acts_as_taggable_on/tag.rb @@ -1,4 +1,5 @@ -# encoding: utf-8 +# frozen_string_literal: true + module ActsAsTaggableOn class Tag < ::ActiveRecord::Base self.table_name = ActsAsTaggableOn.tags_table @@ -31,41 +32,43 @@ def self.named(name) end def self.named_any(list) - clause = list.map { |tag| + clause = list.map do |tag| sanitize_sql_for_named_any(tag).force_encoding('BINARY') - }.join(' OR ') + end.join(' OR ') where(clause) end def self.named_like(name) - clause = ["name #{ActsAsTaggableOn::Utils.like_operator} ? ESCAPE '!'", "%#{ActsAsTaggableOn::Utils.escape_like(name)}%"] + clause = ["name #{ActsAsTaggableOn::Utils.like_operator} ? ESCAPE '!'", + "%#{ActsAsTaggableOn::Utils.escape_like(name)}%"] where(clause) end def self.named_like_any(list) - clause = list.map { |tag| - sanitize_sql(["name #{ActsAsTaggableOn::Utils.like_operator} ? ESCAPE '!'", "%#{ActsAsTaggableOn::Utils.escape_like(tag.to_s)}%"]) - }.join(' OR ') + clause = list.map do |tag| + sanitize_sql(["name #{ActsAsTaggableOn::Utils.like_operator} ? ESCAPE '!'", + "%#{ActsAsTaggableOn::Utils.escape_like(tag.to_s)}%"]) + end.join(' OR ') where(clause) end def self.for_context(context) - joins(:taggings). - where(["#{ActsAsTaggableOn.taggings_table}.context = ?", context]). - select("DISTINCT #{ActsAsTaggableOn.tags_table}.*") + joins(:taggings) + .where(["#{ActsAsTaggableOn.taggings_table}.context = ?", context]) + .select("DISTINCT #{ActsAsTaggableOn.tags_table}.*") end def self.for_tenant(tenant) - joins(:taggings). - where("#{ActsAsTaggableOn.taggings_table}.tenant = ?", tenant.to_s). - select("DISTINCT #{ActsAsTaggableOn.tags_table}.*") + joins(:taggings) + .where("#{ActsAsTaggableOn.taggings_table}.tenant = ?", tenant.to_s) + .select("DISTINCT #{ActsAsTaggableOn.tags_table}.*") end ### CLASS METHODS: def self.find_or_create_with_like_by_name(name) if ActsAsTaggableOn.strict_case_match - self.find_or_create_all_with_like_by_name([name]).first + find_or_create_all_with_like_by_name([name]).first else named_like(name).first || create(name: name) end @@ -78,27 +81,25 @@ def self.find_or_create_all_with_like_by_name(*list) existing_tags = named_any(list) list.map do |tag_name| - begin - tries ||= 3 - comparable_tag_name = comparable_name(tag_name) - existing_tag = existing_tags.find { |tag| comparable_name(tag.name) == comparable_tag_name } - existing_tag || create(name: tag_name) - rescue ActiveRecord::RecordNotUnique - if (tries -= 1).positive? - ActiveRecord::Base.connection.execute 'ROLLBACK' - existing_tags = named_any(list) - retry - end - - raise DuplicateTagError.new("'#{tag_name}' has already been taken") + tries ||= 3 + comparable_tag_name = comparable_name(tag_name) + existing_tag = existing_tags.find { |tag| comparable_name(tag.name) == comparable_tag_name } + existing_tag || create(name: tag_name) + rescue ActiveRecord::RecordNotUnique + if (tries -= 1).positive? + ActiveRecord::Base.connection.execute 'ROLLBACK' + existing_tags = named_any(list) + retry end + + raise DuplicateTagError, "'#{tag_name}' has already been taken" end end ### INSTANCE METHODS: - def ==(object) - super || (object.is_a?(Tag) && name == object.name) + def ==(other) + super || (other.is_a?(Tag) && name == other.name) end def to_s @@ -110,7 +111,6 @@ def count end class << self - private def comparable_name(str) diff --git a/lib/acts_as_taggable_on/tag_list.rb b/lib/acts_as_taggable_on/tag_list.rb index b103665d2..4fb9284ef 100644 --- a/lib/acts_as_taggable_on/tag_list.rb +++ b/lib/acts_as_taggable_on/tag_list.rb @@ -1,10 +1,10 @@ +# frozen_string_literal: true require 'active_support/core_ext/module/delegation' module ActsAsTaggableOn class TagList < Array - attr_accessor :owner - attr_accessor :parser + attr_accessor :owner, :parser def initialize(*args) @parser = ActsAsTaggableOn.default_parser @@ -34,8 +34,8 @@ def <<(obj) # Concatenation --- Returns a new tag list built by concatenating the # two tag lists together to produce a third tag list. - def +(other_tag_list) - TagList.new.add(self).add(other_tag_list) + def +(other) + TagList.new.add(self).add(other) end # Appends the elements of +other_tag_list+ to +self+. @@ -65,12 +65,12 @@ def remove(*names) # tag_list = TagList.new("Round", "Square,Cube") # tag_list.to_s # 'Round, "Square,Cube"' def to_s - tags = frozen? ? self.dup : self + tags = frozen? ? dup : self tags.send(:clean!) tags.map do |name| d = ActsAsTaggableOn.delimiter - d = Regexp.new d.join('|') if d.kind_of? Array + d = Regexp.new d.join('|') if d.is_a? Array name.index(d) ? "\"#{name}\"" : name end.join(ActsAsTaggableOn.glue) end @@ -85,22 +85,19 @@ def clean! map! { |tag| tag.mb_chars.downcase.to_s } if ActsAsTaggableOn.force_lowercase map!(&:parameterize) if ActsAsTaggableOn.force_parameterize - ActsAsTaggableOn.strict_case_match ? uniq! : uniq!{ |tag| tag.downcase } + ActsAsTaggableOn.strict_case_match ? uniq! : uniq!(&:downcase) self end - def extract_and_apply_options!(args) options = args.last.is_a?(Hash) ? args.pop : {} options.assert_valid_keys :parse, :parser - parser = options[:parser] ? options[:parser] : @parser + parser = options[:parser] || @parser args.map! { |a| parser.new(a).parse } if options[:parse] || options[:parser] args.flatten! end - end end - diff --git a/lib/acts_as_taggable_on/taggable.rb b/lib/acts_as_taggable_on/taggable.rb index 91a2270dd..2fea8d56f 100644 --- a/lib/acts_as_taggable_on/taggable.rb +++ b/lib/acts_as_taggable_on/taggable.rb @@ -1,6 +1,7 @@ +# frozen_string_literal: true + module ActsAsTaggableOn module Taggable - def taggable? false end @@ -56,11 +57,10 @@ def acts_as_ordered_taggable_on(*tag_types) def acts_as_taggable_tenant(tenant) if taggable? - self.tenant_column = tenant else class_attribute :tenant_column - self.tenant_column = tenant end + self.tenant_column = tenant # each of these add context-specific methods and must be # called on each call of taggable_on @@ -73,17 +73,17 @@ def acts_as_taggable_tenant(tenant) private - # Make a model taggable on specified contexts - # and optionally preserves the order in which tags are created - # - # Separate methods used above for backwards compatibility - # so that the original acts_as_taggable_on method is unaffected - # as it's not possible to add another argument to the method - # without the tag_types being enclosed in square brackets - # - # NB: method overridden in core module in order to create tag type - # associations and methods after this logic has executed - # + # Make a model taggable on specified contexts + # and optionally preserves the order in which tags are created + # + # Separate methods used above for backwards compatibility + # so that the original acts_as_taggable_on method is unaffected + # as it's not possible to add another argument to the method + # without the tag_types being enclosed in square brackets + # + # NB: method overridden in core module in order to create tag type + # associations and methods after this logic has executed + # def taggable_on(preserve_tag_order, *tag_types) tag_types = tag_types.to_a.flatten.compact.map(&:to_sym) diff --git a/lib/acts_as_taggable_on/taggable/cache.rb b/lib/acts_as_taggable_on/taggable/cache.rb index 46df3fe25..a42c42601 100644 --- a/lib/acts_as_taggable_on/taggable/cache.rb +++ b/lib/acts_as_taggable_on/taggable/cache.rb @@ -1,89 +1,91 @@ -module ActsAsTaggableOn::Taggable - module Cache - def self.included(base) - # When included, conditionally adds tag caching methods when the model - # has any "cached_#{tag_type}_list" column - base.extend Columns - end +# frozen_string_literal: true - module Columns - # ActiveRecord::Base.columns makes a database connection and caches the - # calculated columns hash for the record as @columns. Since we don't - # want to add caching methods until we confirm the presence of a - # caching column, and we don't want to force opening a database - # connection when the class is loaded, here we intercept and cache - # the call to :columns as @acts_as_taggable_on_cache_columns - # to mimic the underlying behavior. While processing this first - # call to columns, we do the caching column check and dynamically add - # the class and instance methods - # FIXME: this method cannot compile in rubinius - def columns - @acts_as_taggable_on_cache_columns ||= begin - db_columns = super - _add_tags_caching_methods if _has_tags_cache_columns?(db_columns) - db_columns - end +module ActsAsTaggableOn + module Taggable + module Cache + def self.included(base) + # When included, conditionally adds tag caching methods when the model + # has any "cached_#{tag_type}_list" column + base.extend Columns end - def reset_column_information - super - @acts_as_taggable_on_cache_columns = nil - end + module Columns + # ActiveRecord::Base.columns makes a database connection and caches the + # calculated columns hash for the record as @columns. Since we don't + # want to add caching methods until we confirm the presence of a + # caching column, and we don't want to force opening a database + # connection when the class is loaded, here we intercept and cache + # the call to :columns as @acts_as_taggable_on_cache_columns + # to mimic the underlying behavior. While processing this first + # call to columns, we do the caching column check and dynamically add + # the class and instance methods + # FIXME: this method cannot compile in rubinius + def columns + @acts_as_taggable_on_cache_columns ||= begin + db_columns = super + _add_tags_caching_methods if _has_tags_cache_columns?(db_columns) + db_columns + end + end - private + def reset_column_information + super + @acts_as_taggable_on_cache_columns = nil + end + + private - # @private - def _has_tags_cache_columns?(db_columns) - db_column_names = db_columns.map(&:name) - tag_types.any? do |context| - db_column_names.include?("cached_#{context.to_s.singularize}_list") + # @private + def _has_tags_cache_columns?(db_columns) + db_column_names = db_columns.map(&:name) + tag_types.any? do |context| + db_column_names.include?("cached_#{context.to_s.singularize}_list") + end end - end - # @private - def _add_tags_caching_methods - send :include, ActsAsTaggableOn::Taggable::Cache::InstanceMethods - extend ActsAsTaggableOn::Taggable::Cache::ClassMethods + # @private + def _add_tags_caching_methods + send :include, ActsAsTaggableOn::Taggable::Cache::InstanceMethods + extend ActsAsTaggableOn::Taggable::Cache::ClassMethods - before_save :save_cached_tag_list + before_save :save_cached_tag_list - initialize_tags_cache + initialize_tags_cache + end end - end - module ClassMethods - def initialize_tags_cache - tag_types.map(&:to_s).each do |tag_type| - class_eval <<-RUBY, __FILE__, __LINE__ + 1 + module ClassMethods + def initialize_tags_cache + tag_types.map(&:to_s).each do |tag_type| + class_eval <<-RUBY, __FILE__, __LINE__ + 1 def self.caching_#{tag_type.singularize}_list? caching_tag_list_on?("#{tag_type}") end - RUBY + RUBY + end end - end - def acts_as_taggable_on(*args) - super(*args) - initialize_tags_cache - end + def acts_as_taggable_on(*args) + super(*args) + initialize_tags_cache + end - def caching_tag_list_on?(context) - column_names.include?("cached_#{context.to_s.singularize}_list") + def caching_tag_list_on?(context) + column_names.include?("cached_#{context.to_s.singularize}_list") + end end - end - module InstanceMethods - def save_cached_tag_list - tag_types.map(&:to_s).each do |tag_type| - if self.class.send("caching_#{tag_type.singularize}_list?") - if tag_list_cache_set_on(tag_type) + module InstanceMethods + def save_cached_tag_list + tag_types.map(&:to_s).each do |tag_type| + if self.class.send("caching_#{tag_type.singularize}_list?") && tag_list_cache_set_on(tag_type) list = tag_list_cache_on(tag_type).to_a.flatten.compact.join("#{ActsAsTaggableOn.delimiter} ") self["cached_#{tag_type.singularize}_list"] = list end end - end - true + true + end end end end diff --git a/lib/acts_as_taggable_on/taggable/collection.rb b/lib/acts_as_taggable_on/taggable/collection.rb index eafc28d05..3d96a82ea 100644 --- a/lib/acts_as_taggable_on/taggable/collection.rb +++ b/lib/acts_as_taggable_on/taggable/collection.rb @@ -1,14 +1,17 @@ -module ActsAsTaggableOn::Taggable - module Collection - def self.included(base) - base.extend ActsAsTaggableOn::Taggable::Collection::ClassMethods - base.initialize_acts_as_taggable_on_collection - end +# frozen_string_literal: true + +module ActsAsTaggableOn + module Taggable + module Collection + def self.included(base) + base.extend ActsAsTaggableOn::Taggable::Collection::ClassMethods + base.initialize_acts_as_taggable_on_collection + end - module ClassMethods - def initialize_acts_as_taggable_on_collection - tag_types.map(&:to_s).each do |tag_type| - class_eval <<-RUBY, __FILE__, __LINE__ + 1 + module ClassMethods + def initialize_acts_as_taggable_on_collection + tag_types.map(&:to_s).each do |tag_type| + class_eval <<-RUBY, __FILE__, __LINE__ + 1 def self.#{tag_type.singularize}_counts(options={}) tag_counts_on('#{tag_type}', options) end @@ -24,166 +27,192 @@ def top_#{tag_type}(limit = 10) def self.top_#{tag_type}(limit = 10) tag_counts_on('#{tag_type}', order: 'count desc', limit: limit.to_i) end - RUBY + RUBY + end end - end - - def acts_as_taggable_on(*args) - super(*args) - initialize_acts_as_taggable_on_collection - end - def tag_counts_on(context, options = {}) - all_tag_counts(options.merge({on: context.to_s})) - end - - def tags_on(context, options = {}) - all_tags(options.merge({on: context.to_s})) - end - - ## - # Calculate the tag names. - # To be used when you don't need tag counts and want to avoid the taggable joins. - # - # @param [Hash] options Options: - # * :start_at - Restrict the tags to those created after a certain time - # * :end_at - Restrict the tags to those created before a certain time - # * :conditions - A piece of SQL conditions to add to the query. Note we don't join the taggable objects for performance reasons. - # * :limit - The maximum number of tags to return - # * :order - A piece of SQL to order by. Eg 'tags.count desc' or 'taggings.created_at desc' - # * :on - Scope the find to only include a certain context - def all_tags(options = {}) - options = options.dup - options.assert_valid_keys :start_at, :end_at, :conditions, :order, :limit, :on - - ## Generate conditions: - options[:conditions] = sanitize_sql(options[:conditions]) if options[:conditions] - - ## Generate scope: - tagging_scope = ActsAsTaggableOn::Tagging.select("#{ActsAsTaggableOn::Tagging.table_name}.tag_id") - tag_scope = ActsAsTaggableOn::Tag.select("#{ActsAsTaggableOn::Tag.table_name}.*").order(options[:order]).limit(options[:limit]) - - # Joins and conditions - tagging_conditions(options).each { |condition| tagging_scope = tagging_scope.where(condition) } - tag_scope = tag_scope.where(options[:conditions]) - - group_columns = "#{ActsAsTaggableOn::Tagging.table_name}.tag_id" - - # Append the current scope to the scope, because we can't use scope(:find) in RoR 3.0 anymore: - tagging_scope = generate_tagging_scope_in_clause(tagging_scope, table_name, primary_key).group(group_columns) - - tag_scope_joins(tag_scope, tagging_scope) - end - - ## - # Calculate the tag counts for all tags. - # - # @param [Hash] options Options: - # * :start_at - Restrict the tags to those created after a certain time - # * :end_at - Restrict the tags to those created before a certain time - # * :conditions - A piece of SQL conditions to add to the query - # * :limit - The maximum number of tags to return - # * :order - A piece of SQL to order by. Eg 'tags.count desc' or 'taggings.created_at desc' - # * :at_least - Exclude tags with a frequency less than the given value - # * :at_most - Exclude tags with a frequency greater than the given value - # * :on - Scope the find to only include a certain context - def all_tag_counts(options = {}) - options = options.dup - options.assert_valid_keys :start_at, :end_at, :conditions, :at_least, :at_most, :order, :limit, :on, :id - - ## Generate conditions: - options[:conditions] = sanitize_sql(options[:conditions]) if options[:conditions] - - ## Generate scope: - tagging_scope = ActsAsTaggableOn::Tagging.select("#{ActsAsTaggableOn::Tagging.table_name}.tag_id, COUNT(#{ActsAsTaggableOn::Tagging.table_name}.tag_id) AS tags_count") - tag_scope = ActsAsTaggableOn::Tag.select("#{ActsAsTaggableOn::Tag.table_name}.*, #{ActsAsTaggableOn::Tagging.table_name}.tags_count AS count").order(options[:order]).limit(options[:limit]) - - # Current model is STI descendant, so add type checking to the join condition - unless descends_from_active_record? - taggable_join = "INNER JOIN #{table_name} ON #{table_name}.#{primary_key} = #{ActsAsTaggableOn::Tagging.table_name}.taggable_id" - taggable_join << " AND #{table_name}.#{inheritance_column} = '#{name}'" - tagging_scope = tagging_scope.joins(taggable_join) + def acts_as_taggable_on(*args) + super(*args) + initialize_acts_as_taggable_on_collection end - # Conditions - tagging_conditions(options).each { |condition| tagging_scope = tagging_scope.where(condition) } - tag_scope = tag_scope.where(options[:conditions]) + def tag_counts_on(context, options = {}) + all_tag_counts(options.merge({ on: context.to_s })) + end - # GROUP BY and HAVING clauses: - having = ["COUNT(#{ActsAsTaggableOn::Tagging.table_name}.tag_id) > 0"] - having.push sanitize_sql(["COUNT(#{ActsAsTaggableOn::Tagging.table_name}.tag_id) >= ?", options.delete(:at_least)]) if options[:at_least] - having.push sanitize_sql(["COUNT(#{ActsAsTaggableOn::Tagging.table_name}.tag_id) <= ?", options.delete(:at_most)]) if options[:at_most] - having = having.compact.join(' AND ') + def tags_on(context, options = {}) + all_tags(options.merge({ on: context.to_s })) + end - group_columns = "#{ActsAsTaggableOn::Tagging.table_name}.tag_id" + ## + # Calculate the tag names. + # To be used when you don't need tag counts and want to avoid the taggable joins. + # + # @param [Hash] options Options: + # * :start_at - Restrict the tags to those created after a certain time + # * :end_at - Restrict the tags to those created before a certain time + # * :conditions - A piece of SQL conditions to add to the query. Note we don't join the taggable objects for performance reasons. + # * :limit - The maximum number of tags to return + # * :order - A piece of SQL to order by. Eg 'tags.count desc' or 'taggings.created_at desc' + # * :on - Scope the find to only include a certain context + def all_tags(options = {}) + options = options.dup + options.assert_valid_keys :start_at, :end_at, :conditions, :order, :limit, :on + + ## Generate conditions: + options[:conditions] = sanitize_sql(options[:conditions]) if options[:conditions] + + ## Generate scope: + tagging_scope = ActsAsTaggableOn::Tagging.select("#{ActsAsTaggableOn::Tagging.table_name}.tag_id") + tag_scope = ActsAsTaggableOn::Tag.select("#{ActsAsTaggableOn::Tag.table_name}.*").order(options[:order]).limit(options[:limit]) + + # Joins and conditions + tagging_conditions(options).each { |condition| tagging_scope = tagging_scope.where(condition) } + tag_scope = tag_scope.where(options[:conditions]) + + group_columns = "#{ActsAsTaggableOn::Tagging.table_name}.tag_id" - unless options[:id] # Append the current scope to the scope, because we can't use scope(:find) in RoR 3.0 anymore: - tagging_scope = generate_tagging_scope_in_clause(tagging_scope, table_name, primary_key) + tagging_scope = generate_tagging_scope_in_clause(tagging_scope, table_name, primary_key).group(group_columns) + + tag_scope_joins(tag_scope, tagging_scope) end - tagging_scope = tagging_scope.group(group_columns).having(having) + ## + # Calculate the tag counts for all tags. + # + # @param [Hash] options Options: + # * :start_at - Restrict the tags to those created after a certain time + # * :end_at - Restrict the tags to those created before a certain time + # * :conditions - A piece of SQL conditions to add to the query + # * :limit - The maximum number of tags to return + # * :order - A piece of SQL to order by. Eg 'tags.count desc' or 'taggings.created_at desc' + # * :at_least - Exclude tags with a frequency less than the given value + # * :at_most - Exclude tags with a frequency greater than the given value + # * :on - Scope the find to only include a certain context + def all_tag_counts(options = {}) + options = options.dup + options.assert_valid_keys :start_at, :end_at, :conditions, :at_least, :at_most, :order, :limit, :on, :id + + ## Generate conditions: + options[:conditions] = sanitize_sql(options[:conditions]) if options[:conditions] + + ## Generate scope: + tagging_scope = ActsAsTaggableOn::Tagging.select("#{ActsAsTaggableOn::Tagging.table_name}.tag_id, COUNT(#{ActsAsTaggableOn::Tagging.table_name}.tag_id) AS tags_count") + tag_scope = ActsAsTaggableOn::Tag.select("#{ActsAsTaggableOn::Tag.table_name}.*, #{ActsAsTaggableOn::Tagging.table_name}.tags_count AS count").order(options[:order]).limit(options[:limit]) + + # Current model is STI descendant, so add type checking to the join condition + unless descends_from_active_record? + taggable_join = "INNER JOIN #{table_name} ON #{table_name}.#{primary_key} = #{ActsAsTaggableOn::Tagging.table_name}.taggable_id" + taggable_join = taggable_join + " AND #{table_name}.#{inheritance_column} = '#{name}'" + tagging_scope = tagging_scope.joins(taggable_join) + end - tag_scope_joins(tag_scope, tagging_scope) - end + # Conditions + tagging_conditions(options).each { |condition| tagging_scope = tagging_scope.where(condition) } + tag_scope = tag_scope.where(options[:conditions]) - def safe_to_sql(relation) - connection.respond_to?(:unprepared_statement) ? connection.unprepared_statement { relation.to_sql } : relation.to_sql - end + # GROUP BY and HAVING clauses: + having = ["COUNT(#{ActsAsTaggableOn::Tagging.table_name}.tag_id) > 0"] + if options[:at_least] + having.push sanitize_sql(["COUNT(#{ActsAsTaggableOn::Tagging.table_name}.tag_id) >= ?", + options.delete(:at_least)]) + end + if options[:at_most] + having.push sanitize_sql(["COUNT(#{ActsAsTaggableOn::Tagging.table_name}.tag_id) <= ?", + options.delete(:at_most)]) + end + having = having.compact.join(' AND ') - private + group_columns = "#{ActsAsTaggableOn::Tagging.table_name}.tag_id" - def generate_tagging_scope_in_clause(tagging_scope, table_name, primary_key) - table_name_pkey = "#{table_name}.#{primary_key}" - if ActsAsTaggableOn::Utils.using_mysql? - # See https://github.com/mbleigh/acts-as-taggable-on/pull/457 for details - scoped_ids = pluck(table_name_pkey) - tagging_scope = tagging_scope.where("#{ActsAsTaggableOn::Tagging.table_name}.taggable_id IN (?)", scoped_ids) - else - tagging_scope = tagging_scope.where("#{ActsAsTaggableOn::Tagging.table_name}.taggable_id IN(#{safe_to_sql(except(:select).select(table_name_pkey))})") - end + unless options[:id] + # Append the current scope to the scope, because we can't use scope(:find) in RoR 3.0 anymore: + tagging_scope = generate_tagging_scope_in_clause(tagging_scope, table_name, primary_key) + end - tagging_scope - end + tagging_scope = tagging_scope.group(group_columns).having(having) + + tag_scope_joins(tag_scope, tagging_scope) + end - def tagging_conditions(options) - tagging_conditions = [] - tagging_conditions.push sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at <= ?", options.delete(:end_at)]) if options[:end_at] - tagging_conditions.push sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at >= ?", options.delete(:start_at)]) if options[:start_at] + def safe_to_sql(relation) + if connection.respond_to?(:unprepared_statement) + connection.unprepared_statement do + relation.to_sql + end + else + relation.to_sql + end + end - taggable_conditions = sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.taggable_type = ?", base_class.name]) - taggable_conditions << sanitize_sql([" AND #{ActsAsTaggableOn::Tagging.table_name}.context = ?", options.delete(:on).to_s]) if options[:on] + private - if options[:id] - if options[:id].is_a? Array - taggable_conditions << sanitize_sql([" AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_id IN (?)", options[:id]]) + def generate_tagging_scope_in_clause(tagging_scope, table_name, primary_key) + table_name_pkey = "#{table_name}.#{primary_key}" + if ActsAsTaggableOn::Utils.using_mysql? + # See https://github.com/mbleigh/acts-as-taggable-on/pull/457 for details + scoped_ids = pluck(table_name_pkey) + tagging_scope = tagging_scope.where("#{ActsAsTaggableOn::Tagging.table_name}.taggable_id IN (?)", + scoped_ids) else - taggable_conditions << sanitize_sql([" AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_id = ?", options[:id]]) + tagging_scope = tagging_scope.where("#{ActsAsTaggableOn::Tagging.table_name}.taggable_id IN(#{safe_to_sql(except(:select).select(table_name_pkey))})") end + + tagging_scope end - tagging_conditions.push taggable_conditions + def tagging_conditions(options) + tagging_conditions = [] + if options[:end_at] + tagging_conditions.push sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at <= ?", + options.delete(:end_at)]) + end + if options[:start_at] + tagging_conditions.push sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at >= ?", + options.delete(:start_at)]) + end - tagging_conditions - end + taggable_conditions = sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.taggable_type = ?", + base_class.name]) + if options[:on] + taggable_conditions << sanitize_sql([" AND #{ActsAsTaggableOn::Tagging.table_name}.context = ?", + options.delete(:on).to_s]) + end - def tag_scope_joins(tag_scope, tagging_scope) - tag_scope = tag_scope.joins("JOIN (#{safe_to_sql(tagging_scope)}) AS #{ActsAsTaggableOn::Tagging.table_name} ON #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.id") - tag_scope.extending(CalculationMethods) + if options[:id] + taggable_conditions << if options[:id].is_a? Array + sanitize_sql([" AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_id IN (?)", + options[:id]]) + else + sanitize_sql([" AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_id = ?", + options[:id]]) + end + end + + tagging_conditions.push taggable_conditions + + tagging_conditions + end + + def tag_scope_joins(tag_scope, tagging_scope) + tag_scope = tag_scope.joins("JOIN (#{safe_to_sql(tagging_scope)}) AS #{ActsAsTaggableOn::Tagging.table_name} ON #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.id") + tag_scope.extending(CalculationMethods) + end end - end - def tag_counts_on(context, options={}) - self.class.tag_counts_on(context, options.merge(id: id)) - end + def tag_counts_on(context, options = {}) + self.class.tag_counts_on(context, options.merge(id: id)) + end - module CalculationMethods - # Rails 5 TODO: Remove options argument as soon we remove support to - # activerecord-deprecated_finders. - # See https://github.com/rails/rails/blob/master/activerecord/lib/active_record/relation/calculations.rb#L38 - def count(column_name = :all, options = {}) - # https://github.com/rails/rails/commit/da9b5d4a8435b744fcf278fffd6d7f1e36d4a4f2 - super(column_name) + module CalculationMethods + # Rails 5 TODO: Remove options argument as soon we remove support to + # activerecord-deprecated_finders. + # See https://github.com/rails/rails/blob/master/activerecord/lib/active_record/relation/calculations.rb#L38 + def count(column_name = :all, _options = {}) + # https://github.com/rails/rails/commit/da9b5d4a8435b744fcf278fffd6d7f1e36d4a4f2 + super(column_name) + end end end end diff --git a/lib/acts_as_taggable_on/taggable/core.rb b/lib/acts_as_taggable_on/taggable/core.rb index 70ba1d7ae..88138e226 100644 --- a/lib/acts_as_taggable_on/taggable/core.rb +++ b/lib/acts_as_taggable_on/taggable/core.rb @@ -1,48 +1,51 @@ +# frozen_string_literal: true + require_relative 'tagged_with_query' require_relative 'tag_list_type' -module ActsAsTaggableOn::Taggable - module Core +module ActsAsTaggableOn + module Taggable + module Core + def self.included(base) + base.extend ActsAsTaggableOn::Taggable::Core::ClassMethods - def self.included(base) - base.extend ActsAsTaggableOn::Taggable::Core::ClassMethods + base.class_eval do + attr_writer :custom_contexts - base.class_eval do - attr_writer :custom_contexts - after_save :save_tags - end + after_save :save_tags + end - base.initialize_acts_as_taggable_on_core - end + base.initialize_acts_as_taggable_on_core + end - module ClassMethods - def initialize_acts_as_taggable_on_core - include taggable_mixin - tag_types.map(&:to_s).each do |tags_type| - tag_type = tags_type.to_s.singularize - context_taggings = "#{tag_type}_taggings".to_sym - context_tags = tags_type.to_sym - taggings_order = (preserve_tag_order? ? "#{ActsAsTaggableOn::Tagging.table_name}.id" : []) - - class_eval do - # when preserving tag order, include order option so that for a 'tags' context - # the associations tag_taggings & tags are always returned in created order - has_many context_taggings, -> { includes(:tag).order(taggings_order).where(context: tags_type) }, - as: :taggable, - class_name: 'ActsAsTaggableOn::Tagging', - dependent: :destroy, - after_add: :dirtify_tag_list, - after_remove: :dirtify_tag_list - - has_many context_tags, -> { order(taggings_order) }, - class_name: 'ActsAsTaggableOn::Tag', - through: context_taggings, - source: :tag - - attribute "#{tags_type.singularize}_list".to_sym, ActsAsTaggableOn::Taggable::TagListType.new - end + module ClassMethods + def initialize_acts_as_taggable_on_core + include taggable_mixin + tag_types.map(&:to_s).each do |tags_type| + tag_type = tags_type.to_s.singularize + context_taggings = "#{tag_type}_taggings".to_sym + context_tags = tags_type.to_sym + taggings_order = (preserve_tag_order? ? "#{ActsAsTaggableOn::Tagging.table_name}.id" : []) + + class_eval do + # when preserving tag order, include order option so that for a 'tags' context + # the associations tag_taggings & tags are always returned in created order + has_many context_taggings, -> { includes(:tag).order(taggings_order).where(context: tags_type) }, + as: :taggable, + class_name: 'ActsAsTaggableOn::Tagging', + dependent: :destroy, + after_add: :dirtify_tag_list, + after_remove: :dirtify_tag_list + + has_many context_tags, -> { order(taggings_order) }, + class_name: 'ActsAsTaggableOn::Tag', + through: context_taggings, + source: :tag + + attribute "#{tags_type.singularize}_list".to_sym, ActsAsTaggableOn::Taggable::TagListType.new + end - taggable_mixin.class_eval <<-RUBY, __FILE__, __LINE__ + 1 + taggable_mixin.class_eval <<-RUBY, __FILE__, __LINE__ + 1 def #{tag_type}_list tag_list_on('#{tags_type}') end @@ -68,263 +71,268 @@ def all_#{tags_type}_list def dirtify_tag_list(tagging) attribute_will_change! tagging.context.singularize+"_list" end - RUBY + RUBY + end + end + + def taggable_on(preserve_tag_order, *tag_types) + super(preserve_tag_order, *tag_types) + initialize_acts_as_taggable_on_core end - end - def taggable_on(preserve_tag_order, *tag_types) - super(preserve_tag_order, *tag_types) - initialize_acts_as_taggable_on_core + # all column names are necessary for PostgreSQL group clause + def grouped_column_names_for(object) + object.column_names.map { |column| "#{object.table_name}.#{column}" }.join(', ') + end + + ## + # Return a scope of objects that are tagged with the specified tags. + # + # @param tags The tags that we want to query for + # @param [Hash] options A hash of options to alter you query: + # * :exclude - if set to true, return objects that are *NOT* tagged with the specified tags + # * :any - if set to true, return objects that are tagged with *ANY* of the specified tags + # * :order_by_matching_tag_count - if set to true and used with :any, sort by objects matching the most tags, descending + # * :match_all - if set to true, return objects that are *ONLY* tagged with the specified tags + # * :owned_by - return objects that are *ONLY* owned by the owner + # * :start_at - Restrict the tags to those created after a certain time + # * :end_at - Restrict the tags to those created before a certain time + # + # Example: + # User.tagged_with(["awesome", "cool"]) # Users that are tagged with awesome and cool + # User.tagged_with(["awesome", "cool"], :exclude => true) # Users that are not tagged with awesome or cool + # User.tagged_with(["awesome", "cool"], :any => true) # Users that are tagged with awesome or cool + # User.tagged_with(["awesome", "cool"], :any => true, :order_by_matching_tag_count => true) # Sort by users who match the most tags, descending + # User.tagged_with(["awesome", "cool"], :match_all => true) # Users that are tagged with just awesome and cool + # User.tagged_with(["awesome", "cool"], :owned_by => foo ) # Users that are tagged with just awesome and cool by 'foo' + # User.tagged_with(["awesome", "cool"], :owned_by => foo, :start_at => Date.today ) # Users that are tagged with just awesome, cool by 'foo' and starting today + def tagged_with(tags, options = {}) + tag_list = ActsAsTaggableOn.default_parser.new(tags).parse + options = options.dup + + return none if tag_list.empty? + + ::ActsAsTaggableOn::Taggable::TaggedWithQuery.build(self, ActsAsTaggableOn::Tag, ActsAsTaggableOn::Tagging, + tag_list, options) + end + + def is_taggable? + true + end + + def taggable_mixin + @taggable_mixin ||= Module.new + end end # all column names are necessary for PostgreSQL group clause def grouped_column_names_for(object) - object.column_names.map { |column| "#{object.table_name}.#{column}" }.join(', ') + self.class.grouped_column_names_for(object) end - ## - # Return a scope of objects that are tagged with the specified tags. - # - # @param tags The tags that we want to query for - # @param [Hash] options A hash of options to alter you query: - # * :exclude - if set to true, return objects that are *NOT* tagged with the specified tags - # * :any - if set to true, return objects that are tagged with *ANY* of the specified tags - # * :order_by_matching_tag_count - if set to true and used with :any, sort by objects matching the most tags, descending - # * :match_all - if set to true, return objects that are *ONLY* tagged with the specified tags - # * :owned_by - return objects that are *ONLY* owned by the owner - # * :start_at - Restrict the tags to those created after a certain time - # * :end_at - Restrict the tags to those created before a certain time - # - # Example: - # User.tagged_with(["awesome", "cool"]) # Users that are tagged with awesome and cool - # User.tagged_with(["awesome", "cool"], :exclude => true) # Users that are not tagged with awesome or cool - # User.tagged_with(["awesome", "cool"], :any => true) # Users that are tagged with awesome or cool - # User.tagged_with(["awesome", "cool"], :any => true, :order_by_matching_tag_count => true) # Sort by users who match the most tags, descending - # User.tagged_with(["awesome", "cool"], :match_all => true) # Users that are tagged with just awesome and cool - # User.tagged_with(["awesome", "cool"], :owned_by => foo ) # Users that are tagged with just awesome and cool by 'foo' - # User.tagged_with(["awesome", "cool"], :owned_by => foo, :start_at => Date.today ) # Users that are tagged with just awesome, cool by 'foo' and starting today - def tagged_with(tags, options = {}) - tag_list = ActsAsTaggableOn.default_parser.new(tags).parse - options = options.dup - - return none if tag_list.empty? - - ::ActsAsTaggableOn::Taggable::TaggedWithQuery.build(self, ActsAsTaggableOn::Tag, ActsAsTaggableOn::Tagging, tag_list, options) + def custom_contexts + @custom_contexts ||= taggings.map(&:context).uniq end def is_taggable? - true + self.class.is_taggable? end - def taggable_mixin - @taggable_mixin ||= Module.new + def add_custom_context(value) + unless custom_contexts.include?(value.to_s) || self.class.tag_types.map(&:to_s).include?(value.to_s) + custom_contexts << value.to_s + end end - end - - # all column names are necessary for PostgreSQL group clause - def grouped_column_names_for(object) - self.class.grouped_column_names_for(object) - end - def custom_contexts - @custom_contexts ||= taggings.map(&:context).uniq - end + def cached_tag_list_on(context) + self["cached_#{context.to_s.singularize}_list"] + end - def is_taggable? - self.class.is_taggable? - end + def tag_list_cache_set_on(context) + variable_name = "@#{context.to_s.singularize}_list" + instance_variable_defined?(variable_name) && instance_variable_get(variable_name) + end - def add_custom_context(value) - custom_contexts << value.to_s unless custom_contexts.include?(value.to_s) or self.class.tag_types.map(&:to_s).include?(value.to_s) - end + def tag_list_cache_on(context) + variable_name = "@#{context.to_s.singularize}_list" + if instance_variable_get(variable_name) + instance_variable_get(variable_name) + elsif cached_tag_list_on(context) && ensure_included_cache_methods! && self.class.caching_tag_list_on?(context) + instance_variable_set(variable_name, ActsAsTaggableOn.default_parser.new(cached_tag_list_on(context)).parse) + else + instance_variable_set(variable_name, ActsAsTaggableOn::TagList.new(tags_on(context).map(&:name))) + end + end - def cached_tag_list_on(context) - self["cached_#{context.to_s.singularize}_list"] - end + def tag_list_on(context) + add_custom_context(context) + tag_list_cache_on(context) + end - def tag_list_cache_set_on(context) - variable_name = "@#{context.to_s.singularize}_list" - instance_variable_defined?(variable_name) && instance_variable_get(variable_name) - end + def all_tags_list_on(context) + variable_name = "@all_#{context.to_s.singularize}_list" + if instance_variable_defined?(variable_name) && instance_variable_get(variable_name) + return instance_variable_get(variable_name) + end - def tag_list_cache_on(context) - variable_name = "@#{context.to_s.singularize}_list" - if instance_variable_get(variable_name) - instance_variable_get(variable_name) - elsif cached_tag_list_on(context) && ensure_included_cache_methods! && self.class.caching_tag_list_on?(context) - instance_variable_set(variable_name, ActsAsTaggableOn.default_parser.new(cached_tag_list_on(context)).parse) - else - instance_variable_set(variable_name, ActsAsTaggableOn::TagList.new(tags_on(context).map(&:name))) + instance_variable_set(variable_name, ActsAsTaggableOn::TagList.new(all_tags_on(context).map(&:name)).freeze) end - end - def tag_list_on(context) - add_custom_context(context) - tag_list_cache_on(context) - end + ## + # Returns all tags of a given context + def all_tags_on(context) + tagging_table_name = ActsAsTaggableOn::Tagging.table_name - def all_tags_list_on(context) - variable_name = "@all_#{context.to_s.singularize}_list" - return instance_variable_get(variable_name) if instance_variable_defined?(variable_name) && instance_variable_get(variable_name) + opts = ["#{tagging_table_name}.context = ?", context.to_s] + scope = base_tags.where(opts) - instance_variable_set(variable_name, ActsAsTaggableOn::TagList.new(all_tags_on(context).map(&:name)).freeze) - end - - ## - # Returns all tags of a given context - def all_tags_on(context) - tagging_table_name = ActsAsTaggableOn::Tagging.table_name + if ActsAsTaggableOn::Utils.using_postgresql? + group_columns = grouped_column_names_for(ActsAsTaggableOn::Tag) + scope.order(Arel.sql("max(#{tagging_table_name}.created_at)")).group(group_columns) + else + scope.group("#{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key}") + end.to_a + end - opts = ["#{tagging_table_name}.context = ?", context.to_s] - scope = base_tags.where(opts) + ## + # Returns all tags that are not owned of a given context + def tags_on(context) + scope = base_tags.where([ + "#{ActsAsTaggableOn::Tagging.table_name}.context = ? AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_id IS NULL", context.to_s + ]) + # when preserving tag order, return tags in created order + # if we added the order to the association this would always apply + scope = scope.order("#{ActsAsTaggableOn::Tagging.table_name}.id") if self.class.preserve_tag_order? + scope + end - if ActsAsTaggableOn::Utils.using_postgresql? - group_columns = grouped_column_names_for(ActsAsTaggableOn::Tag) - scope.order(Arel.sql("max(#{tagging_table_name}.created_at)")).group(group_columns) - else - scope.group("#{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key}") - end.to_a - end + def set_tag_list_on(context, new_list) + add_custom_context(context) - ## - # Returns all tags that are not owned of a given context - def tags_on(context) - scope = base_tags.where(["#{ActsAsTaggableOn::Tagging.table_name}.context = ? AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_id IS NULL", context.to_s]) - # when preserving tag order, return tags in created order - # if we added the order to the association this would always apply - scope = scope.order("#{ActsAsTaggableOn::Tagging.table_name}.id") if self.class.preserve_tag_order? - scope - end + variable_name = "@#{context.to_s.singularize}_list" - def set_tag_list_on(context, new_list) - add_custom_context(context) + parsed_new_list = ActsAsTaggableOn.default_parser.new(new_list).parse - variable_name = "@#{context.to_s.singularize}_list" + instance_variable_set(variable_name, parsed_new_list) + end - parsed_new_list = ActsAsTaggableOn.default_parser.new(new_list).parse + def tagging_contexts + self.class.tag_types.map(&:to_s) + custom_contexts + end - instance_variable_set(variable_name, parsed_new_list) - end + def taggable_tenant + public_send(self.class.tenant_column) if self.class.tenant_column + end - def tagging_contexts - self.class.tag_types.map(&:to_s) + custom_contexts - end + def reload(*args) + self.class.tag_types.each do |context| + instance_variable_set("@#{context.to_s.singularize}_list", nil) + instance_variable_set("@all_#{context.to_s.singularize}_list", nil) + end - def taggable_tenant - if self.class.tenant_column - public_send(self.class.tenant_column) + super(*args) end - end - def reload(*args) - self.class.tag_types.each do |context| - instance_variable_set("@#{context.to_s.singularize}_list", nil) - instance_variable_set("@all_#{context.to_s.singularize}_list", nil) + ## + # Find existing tags or create non-existing tags + def load_tags(tag_list) + ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name(tag_list) end - super(*args) - end - - ## - # Find existing tags or create non-existing tags - def load_tags(tag_list) - ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name(tag_list) - end + def save_tags + tagging_contexts.each do |context| + next unless tag_list_cache_set_on(context) - def save_tags - tagging_contexts.each do |context| - next unless tag_list_cache_set_on(context) - # List of currently assigned tag names - tag_list = tag_list_cache_on(context).uniq + # List of currently assigned tag names + tag_list = tag_list_cache_on(context).uniq - # Find existing tags or create non-existing tags: - tags = find_or_create_tags_from_list_with_context(tag_list, context) + # Find existing tags or create non-existing tags: + tags = find_or_create_tags_from_list_with_context(tag_list, context) - # Tag objects for currently assigned tags - current_tags = tags_on(context) + # Tag objects for currently assigned tags + current_tags = tags_on(context) - # Tag maintenance based on whether preserving the created order of tags - if self.class.preserve_tag_order? - old_tags, new_tags = current_tags - tags, tags - current_tags + # Tag maintenance based on whether preserving the created order of tags + old_tags = current_tags - tags + new_tags = tags - current_tags + if self.class.preserve_tag_order? - shared_tags = current_tags & tags + shared_tags = current_tags & tags - if shared_tags.any? && tags[0...shared_tags.size] != shared_tags - index = shared_tags.each_with_index { |_, i| break i unless shared_tags[i] == tags[i] } + if shared_tags.any? && tags[0...shared_tags.size] != shared_tags + index = shared_tags.each_with_index do |_, i| + break i unless shared_tags[i] == tags[i] + end - # Update arrays of tag objects - old_tags |= current_tags[index...current_tags.size] - new_tags |= current_tags[index...current_tags.size] & shared_tags + # Update arrays of tag objects + old_tags |= current_tags[index...current_tags.size] + new_tags |= current_tags[index...current_tags.size] & shared_tags - # Order the array of tag objects to match the tag list - new_tags = tags.map do |t| - new_tags.find { |n| n.name.downcase == t.name.downcase } - end.compact + # Order the array of tag objects to match the tag list + new_tags = tags.map do |t| + new_tags.find { |n| n.name.downcase == t.name.downcase } + end.compact + end + else + # Delete discarded tags and create new tags end - else - # Delete discarded tags and create new tags - old_tags = current_tags - tags - new_tags = tags - current_tags - end - # Destroy old taggings: - if old_tags.present? - taggings.not_owned.by_context(context).where(tag_id: old_tags).destroy_all - end + # Destroy old taggings: + taggings.not_owned.by_context(context).where(tag_id: old_tags).destroy_all if old_tags.present? - # Create new taggings: - new_tags.each do |tag| - if taggable_tenant - taggings.create!(tag_id: tag.id, context: context.to_s, taggable: self, tenant: taggable_tenant) - else - taggings.create!(tag_id: tag.id, context: context.to_s, taggable: self) + # Create new taggings: + new_tags.each do |tag| + if taggable_tenant + taggings.create!(tag_id: tag.id, context: context.to_s, taggable: self, tenant: taggable_tenant) + else + taggings.create!(tag_id: tag.id, context: context.to_s, taggable: self) + end end end - end - true - end + true + end - private + private - def ensure_included_cache_methods! - self.class.columns - end + def ensure_included_cache_methods! + self.class.columns + end - # Filters the tag lists from the attribute names. - def attributes_for_update(attribute_names) - tag_lists = tag_types.map {|tags_type| "#{tags_type.to_s.singularize}_list"} - super.delete_if {|attr| tag_lists.include? attr } - end + # Filters the tag lists from the attribute names. + def attributes_for_update(attribute_names) + tag_lists = tag_types.map { |tags_type| "#{tags_type.to_s.singularize}_list" } + super.delete_if { |attr| tag_lists.include? attr } + end - # Filters the tag lists from the attribute names. - def attributes_for_create(attribute_names) - tag_lists = tag_types.map {|tags_type| "#{tags_type.to_s.singularize}_list"} - super.delete_if {|attr| tag_lists.include? attr } - end + # Filters the tag lists from the attribute names. + def attributes_for_create(attribute_names) + tag_lists = tag_types.map { |tags_type| "#{tags_type.to_s.singularize}_list" } + super.delete_if { |attr| tag_lists.include? attr } + end - ## - # Override this hook if you wish to subclass {ActsAsTaggableOn::Tag} -- - # context is provided so that you may conditionally use a Tag subclass - # only for some contexts. - # - # @example Custom Tag class for one context - # class Company < ActiveRecord::Base - # acts_as_taggable_on :markets, :locations - # - # def find_or_create_tags_from_list_with_context(tag_list, context) - # if context.to_sym == :markets - # MarketTag.find_or_create_all_with_like_by_name(tag_list) - # else - # super - # end - # end - # - # @param [Array] tag_list Tags to find or create - # @param [Symbol] context The tag context for the tag_list - def find_or_create_tags_from_list_with_context(tag_list, _context) - load_tags(tag_list) + ## + # Override this hook if you wish to subclass {ActsAsTaggableOn::Tag} -- + # context is provided so that you may conditionally use a Tag subclass + # only for some contexts. + # + # @example Custom Tag class for one context + # class Company < ActiveRecord::Base + # acts_as_taggable_on :markets, :locations + # + # def find_or_create_tags_from_list_with_context(tag_list, context) + # if context.to_sym == :markets + # MarketTag.find_or_create_all_with_like_by_name(tag_list) + # else + # super + # end + # end + # + # @param [Array] tag_list Tags to find or create + # @param [Symbol] context The tag context for the tag_list + def find_or_create_tags_from_list_with_context(tag_list, _context) + load_tags(tag_list) + end end end end - diff --git a/lib/acts_as_taggable_on/taggable/ownership.rb b/lib/acts_as_taggable_on/taggable/ownership.rb index 12187aeb2..403985794 100644 --- a/lib/acts_as_taggable_on/taggable/ownership.rb +++ b/lib/acts_as_taggable_on/taggable/ownership.rb @@ -1,136 +1,148 @@ -module ActsAsTaggableOn::Taggable - module Ownership - def self.included(base) - base.extend ActsAsTaggableOn::Taggable::Ownership::ClassMethods +# frozen_string_literal: true - base.class_eval do - after_save :save_owned_tags - end +module ActsAsTaggableOn + module Taggable + module Ownership + def self.included(base) + base.extend ActsAsTaggableOn::Taggable::Ownership::ClassMethods - base.initialize_acts_as_taggable_on_ownership - end + base.class_eval do + after_save :save_owned_tags + end - module ClassMethods - def acts_as_taggable_on(*args) - initialize_acts_as_taggable_on_ownership - super(*args) + base.initialize_acts_as_taggable_on_ownership end - def initialize_acts_as_taggable_on_ownership - tag_types.map(&:to_s).each do |tag_type| - class_eval <<-RUBY, __FILE__, __LINE__ + 1 + module ClassMethods + def acts_as_taggable_on(*args) + initialize_acts_as_taggable_on_ownership + super(*args) + end + + def initialize_acts_as_taggable_on_ownership + tag_types.map(&:to_s).each do |tag_type| + class_eval <<-RUBY, __FILE__, __LINE__ + 1 def #{tag_type}_from(owner) owner_tag_list_on(owner, '#{tag_type}') end - RUBY + RUBY + end + end + end + + def owner_tags(owner) + scope = if owner.nil? + base_tags + else + base_tags.where( + ActsAsTaggableOn::Tagging.table_name.to_s => { + tagger_id: owner.id, + tagger_type: owner.class.base_class.to_s + } + ) + end + + # when preserving tag order, return tags in created order + # if we added the order to the association this would always apply + if self.class.preserve_tag_order? + scope.order("#{ActsAsTaggableOn::Tagging.table_name}.id") + else + scope end end - end - def owner_tags(owner) - if owner.nil? - scope = base_tags - else - scope = base_tags.where( - "#{ActsAsTaggableOn::Tagging.table_name}" => { - tagger_id: owner.id, - tagger_type: owner.class.base_class.to_s + def owner_tags_on(owner, context) + owner_tags(owner).where( + ActsAsTaggableOn::Tagging.table_name.to_s => { + context: context } ) end - # when preserving tag order, return tags in created order - # if we added the order to the association this would always apply - if self.class.preserve_tag_order? - scope.order("#{ActsAsTaggableOn::Tagging.table_name}.id") - else - scope + def cached_owned_tag_list_on(context) + variable_name = "@owned_#{context}_list" + (instance_variable_defined?(variable_name) && instance_variable_get(variable_name)) || instance_variable_set( + variable_name, {} + ) end - end - - def owner_tags_on(owner, context) - owner_tags(owner).where( - "#{ActsAsTaggableOn::Tagging.table_name}" => { - context: context - } - ) - end - - def cached_owned_tag_list_on(context) - variable_name = "@owned_#{context}_list" - (instance_variable_defined?(variable_name) && instance_variable_get(variable_name)) || instance_variable_set(variable_name, {}) - end - - def owner_tag_list_on(owner, context) - add_custom_context(context) - cache = cached_owned_tag_list_on(context) + def owner_tag_list_on(owner, context) + add_custom_context(context) - cache[owner] ||= ActsAsTaggableOn::TagList.new(*owner_tags_on(owner, context).map(&:name)) - end - - def set_owner_tag_list_on(owner, context, new_list) - add_custom_context(context) - - cache = cached_owned_tag_list_on(context) - - cache[owner] = ActsAsTaggableOn.default_parser.new(new_list).parse - end + cache = cached_owned_tag_list_on(context) - def reload(*args) - self.class.tag_types.each do |context| - instance_variable_set("@owned_#{context}_list", nil) + cache[owner] ||= ActsAsTaggableOn::TagList.new(*owner_tags_on(owner, context).map(&:name)) end - super(*args) - end - - def save_owned_tags - tagging_contexts.each do |context| - cached_owned_tag_list_on(context).each do |owner, tag_list| + def set_owner_tag_list_on(owner, context, new_list) + add_custom_context(context) - # Find existing tags or create non-existing tags: - tags = find_or_create_tags_from_list_with_context(tag_list.uniq, context) + cache = cached_owned_tag_list_on(context) - # Tag objects for owned tags - owned_tags = owner_tags_on(owner, context).to_a + cache[owner] = ActsAsTaggableOn.default_parser.new(new_list).parse + end - # Tag maintenance based on whether preserving the created order of tags - if self.class.preserve_tag_order? - old_tags, new_tags = owned_tags - tags, tags - owned_tags + def reload(*args) + self.class.tag_types.each do |context| + instance_variable_set("@owned_#{context}_list", nil) + end - shared_tags = owned_tags & tags + super(*args) + end - if shared_tags.any? && tags[0...shared_tags.size] != shared_tags - index = shared_tags.each_with_index { |_, i| break i unless shared_tags[i] == tags[i] } + def save_owned_tags + tagging_contexts.each do |context| + cached_owned_tag_list_on(context).each do |owner, tag_list| + # Find existing tags or create non-existing tags: + tags = find_or_create_tags_from_list_with_context(tag_list.uniq, context) - # Update arrays of tag objects - old_tags |= owned_tags.from(index) - new_tags |= owned_tags.from(index) & shared_tags + # Tag objects for owned tags + owned_tags = owner_tags_on(owner, context).to_a - # Order the array of tag objects to match the tag list - new_tags = tags.map { |t| new_tags.find { |n| n.name.downcase == t.name.downcase } }.compact - end - else - # Delete discarded tags and create new tags + # Tag maintenance based on whether preserving the created order of tags old_tags = owned_tags - tags new_tags = tags - owned_tags - end + if self.class.preserve_tag_order? + + shared_tags = owned_tags & tags + + if shared_tags.any? && tags[0...shared_tags.size] != shared_tags + index = shared_tags.each_with_index do |_, i| + break i unless shared_tags[i] == tags[i] + end + + # Update arrays of tag objects + old_tags |= owned_tags.from(index) + new_tags |= owned_tags.from(index) & shared_tags + + # Order the array of tag objects to match the tag list + new_tags = tags.map do |t| + new_tags.find do |n| + n.name.downcase == t.name.downcase + end + end.compact + end + else + # Delete discarded tags and create new tags + end - # Find all taggings that belong to the taggable (self), are owned by the owner, - # have the correct context, and are removed from the list. - ActsAsTaggableOn::Tagging.where(taggable_id: id, taggable_type: self.class.base_class.to_s, - tagger_type: owner.class.base_class.to_s, tagger_id: owner.id, - tag_id: old_tags, context: context).destroy_all if old_tags.present? + # Find all taggings that belong to the taggable (self), are owned by the owner, + # have the correct context, and are removed from the list. + if old_tags.present? + ActsAsTaggableOn::Tagging.where(taggable_id: id, taggable_type: self.class.base_class.to_s, + tagger_type: owner.class.base_class.to_s, tagger_id: owner.id, + tag_id: old_tags, context: context).destroy_all + end - # Create new taggings: - new_tags.each do |tag| - taggings.create!(tag_id: tag.id, context: context.to_s, tagger: owner, taggable: self) + # Create new taggings: + new_tags.each do |tag| + taggings.create!(tag_id: tag.id, context: context.to_s, tagger: owner, taggable: self) + end end end - end - true + true + end end end end diff --git a/lib/acts_as_taggable_on/taggable/related.rb b/lib/acts_as_taggable_on/taggable/related.rb index 7a9755108..de9a739af 100644 --- a/lib/acts_as_taggable_on/taggable/related.rb +++ b/lib/acts_as_taggable_on/taggable/related.rb @@ -1,14 +1,17 @@ -module ActsAsTaggableOn::Taggable - module Related - def self.included(base) - base.extend ActsAsTaggableOn::Taggable::Related::ClassMethods - base.initialize_acts_as_taggable_on_related - end +# frozen_string_literal: true + +module ActsAsTaggableOn + module Taggable + module Related + def self.included(base) + base.extend ActsAsTaggableOn::Taggable::Related::ClassMethods + base.initialize_acts_as_taggable_on_related + end - module ClassMethods - def initialize_acts_as_taggable_on_related - tag_types.map(&:to_s).each do |tag_type| - class_eval <<-RUBY, __FILE__, __LINE__ + 1 + module ClassMethods + def initialize_acts_as_taggable_on_related + tag_types.map(&:to_s).each do |tag_type| + class_eval <<-RUBY, __FILE__, __LINE__ + 1 def find_related_#{tag_type}(options = {}) related_tags_for('#{tag_type}', self.class, options) end @@ -17,55 +20,65 @@ def find_related_#{tag_type}(options = {}) def find_related_#{tag_type}_for(klass, options = {}) related_tags_for('#{tag_type}', klass, options) end - RUBY + RUBY + end end - end - def acts_as_taggable_on(*args) - super(*args) - initialize_acts_as_taggable_on_related + def acts_as_taggable_on(*args) + super(*args) + initialize_acts_as_taggable_on_related + end end - end - def find_matching_contexts(search_context, result_context, options = {}) - matching_contexts_for(search_context.to_s, result_context.to_s, self.class, options) - end + def find_matching_contexts(search_context, result_context, options = {}) + matching_contexts_for(search_context.to_s, result_context.to_s, self.class, options) + end - def find_matching_contexts_for(klass, search_context, result_context, options = {}) - matching_contexts_for(search_context.to_s, result_context.to_s, klass, options) - end + def find_matching_contexts_for(klass, search_context, result_context, options = {}) + matching_contexts_for(search_context.to_s, result_context.to_s, klass, options) + end - def matching_contexts_for(search_context, result_context, klass, options = {}) - tags_to_find = tags_on(search_context).map { |t| t.name } - related_where(klass, ["#{exclude_self(klass, id)} #{klass.table_name}.#{klass.primary_key} = #{ActsAsTaggableOn::Tagging.table_name}.taggable_id AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = '#{klass.base_class}' AND #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key} AND #{ActsAsTaggableOn::Tag.table_name}.name IN (?) AND #{ActsAsTaggableOn::Tagging.table_name}.context = ?", tags_to_find, result_context]) - end + def matching_contexts_for(search_context, result_context, klass, _options = {}) + tags_to_find = tags_on(search_context).map(&:name) + related_where(klass, + [ + "#{exclude_self(klass, + id)} #{klass.table_name}.#{klass.primary_key} = #{ActsAsTaggableOn::Tagging.table_name}.taggable_id AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = '#{klass.base_class}' AND #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key} AND #{ActsAsTaggableOn::Tag.table_name}.name IN (?) AND #{ActsAsTaggableOn::Tagging.table_name}.context = ?", tags_to_find, result_context + ]) + end - def related_tags_for(context, klass, options = {}) - tags_to_ignore = Array.wrap(options[:ignore]).map(&:to_s) || [] - tags_to_find = tags_on(context).map { |t| t.name }.reject { |t| tags_to_ignore.include? t } - related_where(klass, ["#{exclude_self(klass, id)} #{klass.table_name}.#{klass.primary_key} = #{ActsAsTaggableOn::Tagging.table_name}.taggable_id AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = '#{klass.base_class}' AND #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key} AND #{ActsAsTaggableOn::Tag.table_name}.name IN (?) AND #{ActsAsTaggableOn::Tagging.table_name}.context = ?", tags_to_find, context]) - end + def related_tags_for(context, klass, options = {}) + tags_to_ignore = Array.wrap(options[:ignore]).map(&:to_s) || [] + tags_to_find = tags_on(context).map(&:name).reject { |t| tags_to_ignore.include? t } + related_where(klass, + [ + "#{exclude_self(klass, + id)} #{klass.table_name}.#{klass.primary_key} = #{ActsAsTaggableOn::Tagging.table_name}.taggable_id AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = '#{klass.base_class}' AND #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key} AND #{ActsAsTaggableOn::Tag.table_name}.name IN (?) AND #{ActsAsTaggableOn::Tagging.table_name}.context = ?", tags_to_find, context + ]) + end - private + private - def exclude_self(klass, id) - "#{klass.arel_table[klass.primary_key].not_eq(id).to_sql} AND" if [self.class.base_class, self.class].include? klass - end + def exclude_self(klass, id) + "#{klass.arel_table[klass.primary_key].not_eq(id).to_sql} AND" if [self.class.base_class, + self.class].include? klass + end - def group_columns(klass) - if ActsAsTaggableOn::Utils.using_postgresql? - grouped_column_names_for(klass) - else - "#{klass.table_name}.#{klass.primary_key}" + def group_columns(klass) + if ActsAsTaggableOn::Utils.using_postgresql? + grouped_column_names_for(klass) + else + "#{klass.table_name}.#{klass.primary_key}" + end end - end - def related_where(klass, conditions) - klass.select("#{klass.table_name}.*, COUNT(#{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key}) AS count") - .from("#{klass.table_name}, #{ActsAsTaggableOn::Tag.table_name}, #{ActsAsTaggableOn::Tagging.table_name}") - .group(group_columns(klass)) - .order('count DESC') - .where(conditions) + def related_where(klass, conditions) + klass.select("#{klass.table_name}.*, COUNT(#{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key}) AS count") + .from("#{klass.table_name}, #{ActsAsTaggableOn::Tag.table_name}, #{ActsAsTaggableOn::Tagging.table_name}") + .group(group_columns(klass)) + .order('count DESC') + .where(conditions) + end end end end diff --git a/lib/acts_as_taggable_on/taggable/tag_list_type.rb b/lib/acts_as_taggable_on/taggable/tag_list_type.rb index d53a1e3dd..2f8cf6466 100644 --- a/lib/acts_as_taggable_on/taggable/tag_list_type.rb +++ b/lib/acts_as_taggable_on/taggable/tag_list_type.rb @@ -1,4 +1,8 @@ -module ActsAsTaggableOn::Taggable - class TagListType < ActiveModel::Type::Value +# frozen_string_literal: true + +module ActsAsTaggableOn + module Taggable + class TagListType < ActiveModel::Type::Value + end end end diff --git a/lib/acts_as_taggable_on/taggable/tagged_with_query.rb b/lib/acts_as_taggable_on/taggable/tagged_with_query.rb index a2b2613c6..c74f164e7 100644 --- a/lib/acts_as_taggable_on/taggable/tagged_with_query.rb +++ b/lib/acts_as_taggable_on/taggable/tagged_with_query.rb @@ -1,16 +1,22 @@ +# frozen_string_literal: true + require_relative 'tagged_with_query/query_base' require_relative 'tagged_with_query/exclude_tags_query' require_relative 'tagged_with_query/any_tags_query' require_relative 'tagged_with_query/all_tags_query' -module ActsAsTaggableOn::Taggable::TaggedWithQuery - def self.build(taggable_model, tag_model, tagging_model, tag_list, options) - if options[:exclude].present? - ExcludeTagsQuery.new(taggable_model, tag_model, tagging_model, tag_list, options).build - elsif options[:any].present? - AnyTagsQuery.new(taggable_model, tag_model, tagging_model, tag_list, options).build - else - AllTagsQuery.new(taggable_model, tag_model, tagging_model, tag_list, options).build +module ActsAsTaggableOn + module Taggable + module TaggedWithQuery + def self.build(taggable_model, tag_model, tagging_model, tag_list, options) + if options[:exclude].present? + ExcludeTagsQuery.new(taggable_model, tag_model, tagging_model, tag_list, options).build + elsif options[:any].present? + AnyTagsQuery.new(taggable_model, tag_model, tagging_model, tag_list, options).build + else + AllTagsQuery.new(taggable_model, tag_model, tagging_model, tag_list, options).build + end + end end end end diff --git a/lib/acts_as_taggable_on/taggable/tagged_with_query/all_tags_query.rb b/lib/acts_as_taggable_on/taggable/tagged_with_query/all_tags_query.rb index 8e5455096..5789b7505 100644 --- a/lib/acts_as_taggable_on/taggable/tagged_with_query/all_tags_query.rb +++ b/lib/acts_as_taggable_on/taggable/tagged_with_query/all_tags_query.rb @@ -1,111 +1,115 @@ -module ActsAsTaggableOn::Taggable::TaggedWithQuery - class AllTagsQuery < QueryBase - def build - taggable_model.joins(each_tag_in_list) - .group(by_taggable) - .having(tags_that_matches_count) - .order(order_conditions) - .readonly(false) - end - - private - - def each_tag_in_list - arel_join = taggable_arel_table - - tag_list.each do |tag| - tagging_alias = tagging_arel_table.alias(tagging_alias(tag)) - arel_join = arel_join - .join(tagging_alias) - .on(on_conditions(tag, tagging_alias)) - end - - if options[:match_all].present? - arel_join = arel_join - .join(tagging_arel_table, Arel::Nodes::OuterJoin) - .on( - match_all_on_conditions - ) - end - - return arel_join.join_sources - end - - def on_conditions(tag, tagging_alias) - on_condition = tagging_alias[:taggable_id].eq(taggable_arel_table[taggable_model.primary_key]) - .and(tagging_alias[:taggable_type].eq(taggable_model.base_class.name)) - .and( - tagging_alias[:tag_id].in( - tag_arel_table.project(tag_arel_table[:id]).where(tag_match_type(tag)) +# frozen_string_literal: true + +module ActsAsTaggableOn + module Taggable + module TaggedWithQuery + class AllTagsQuery < QueryBase + def build + taggable_model.joins(each_tag_in_list) + .group(by_taggable) + .having(tags_that_matches_count) + .order(order_conditions) + .readonly(false) + end + + private + + def each_tag_in_list + arel_join = taggable_arel_table + + tag_list.each do |tag| + tagging_alias = tagging_arel_table.alias(tagging_alias(tag)) + arel_join = arel_join + .join(tagging_alias) + .on(on_conditions(tag, tagging_alias)) + end + + if options[:match_all].present? + arel_join = arel_join + .join(tagging_arel_table, Arel::Nodes::OuterJoin) + .on( + match_all_on_conditions + ) + end + + arel_join.join_sources + end + + def on_conditions(tag, tagging_alias) + on_condition = tagging_alias[:taggable_id].eq(taggable_arel_table[taggable_model.primary_key]) + .and(tagging_alias[:taggable_type].eq(taggable_model.base_class.name)) + .and( + tagging_alias[:tag_id].in( + tag_arel_table.project(tag_arel_table[:id]).where(tag_match_type(tag)) + ) + ) + + if options[:start_at].present? + on_condition = on_condition.and(tagging_alias[:created_at].gteq(options[:start_at])) + end + + if options[:end_at].present? + on_condition = on_condition.and(tagging_alias[:created_at].lteq(options[:end_at])) + end + + on_condition = on_condition.and(tagging_alias[:context].eq(options[:on])) if options[:on].present? + + if (owner = options[:owned_by]).present? + on_condition = on_condition.and(tagging_alias[:tagger_id].eq(owner.id)) + .and(tagging_alias[:tagger_type].eq(owner.class.base_class.to_s)) + end + + on_condition + end + + def match_all_on_conditions + on_condition = tagging_arel_table[:taggable_id].eq(taggable_arel_table[taggable_model.primary_key]) + .and(tagging_arel_table[:taggable_type].eq(taggable_model.base_class.name)) + + if options[:start_at].present? + on_condition = on_condition.and(tagging_arel_table[:created_at].gteq(options[:start_at])) + end + + if options[:end_at].present? + on_condition = on_condition.and(tagging_arel_table[:created_at].lteq(options[:end_at])) + end + + on_condition = on_condition.and(tagging_arel_table[:context].eq(options[:on])) if options[:on].present? + + on_condition + end + + def by_taggable + return [] if options[:match_all].blank? + + taggable_arel_table[taggable_model.primary_key] + end + + def tags_that_matches_count + return [] if options[:match_all].blank? + + taggable_model.find_by_sql(tag_arel_table.project(Arel.star.count).where(tags_match_type).to_sql) + + tagging_arel_table[:taggable_id].count.eq( + tag_arel_table.project(Arel.star.count).where(tags_match_type) ) - ) - - if options[:start_at].present? - on_condition = on_condition.and(tagging_alias[:created_at].gteq(options[:start_at])) - end - - if options[:end_at].present? - on_condition = on_condition.and(tagging_alias[:created_at].lteq(options[:end_at])) - end - - if options[:on].present? - on_condition = on_condition.and(tagging_alias[:context].eq(options[:on])) - end - - if (owner = options[:owned_by]).present? - on_condition = on_condition.and(tagging_alias[:tagger_id].eq(owner.id)) - .and(tagging_alias[:tagger_type].eq(owner.class.base_class.to_s)) - end - - on_condition - end - - def match_all_on_conditions - on_condition = tagging_arel_table[:taggable_id].eq(taggable_arel_table[taggable_model.primary_key]) - .and(tagging_arel_table[:taggable_type].eq(taggable_model.base_class.name)) - - if options[:start_at].present? - on_condition = on_condition.and(tagging_arel_table[:created_at].gteq(options[:start_at])) - end - - if options[:end_at].present? - on_condition = on_condition.and(tagging_arel_table[:created_at].lteq(options[:end_at])) + end + + def order_conditions + order_by = [] + if options[:order_by_matching_tag_count].present? && options[:match_all].blank? + order_by << tagging_arel_table.project(tagging_arel_table[Arel.star].count.as('taggings_count')).order('taggings_count DESC').to_sql + end + + order_by << options[:order] if options[:order].present? + order_by.join(', ') + end + + def tagging_alias(tag) + alias_base_name = taggable_model.base_class.name.downcase + adjust_taggings_alias("#{alias_base_name[0..11]}_taggings_#{ActsAsTaggableOn::Utils.sha_prefix(tag)}") + end end - - if options[:on].present? - on_condition = on_condition.and(tagging_arel_table[:context].eq(options[:on])) - end - - on_condition - end - - def by_taggable - return [] unless options[:match_all].present? - - taggable_arel_table[taggable_model.primary_key] - end - - def tags_that_matches_count - return [] unless options[:match_all].present? - - taggable_model.find_by_sql(tag_arel_table.project(Arel.star.count).where(tags_match_type).to_sql) - - tagging_arel_table[:taggable_id].count.eq( - tag_arel_table.project(Arel.star.count).where(tags_match_type) - ) - end - - def order_conditions - order_by = [] - order_by << tagging_arel_table.project(tagging_arel_table[Arel.star].count.as('taggings_count')).order('taggings_count DESC').to_sql if options[:order_by_matching_tag_count].present? && options[:match_all].blank? - - order_by << options[:order] if options[:order].present? - order_by.join(', ') - end - - def tagging_alias(tag) - alias_base_name = taggable_model.base_class.name.downcase - adjust_taggings_alias("#{alias_base_name[0..11]}_taggings_#{ActsAsTaggableOn::Utils.sha_prefix(tag)}") end end end diff --git a/lib/acts_as_taggable_on/taggable/tagged_with_query/any_tags_query.rb b/lib/acts_as_taggable_on/taggable/tagged_with_query/any_tags_query.rb index b8c280b06..260de3d9b 100644 --- a/lib/acts_as_taggable_on/taggable/tagged_with_query/any_tags_query.rb +++ b/lib/acts_as_taggable_on/taggable/tagged_with_query/any_tags_query.rb @@ -1,70 +1,74 @@ -module ActsAsTaggableOn::Taggable::TaggedWithQuery - class AnyTagsQuery < QueryBase - def build - taggable_model.select(all_fields) - .where(model_has_at_least_one_tag) - .order(Arel.sql(order_conditions)) - .readonly(false) - end +# frozen_string_literal: true - private +module ActsAsTaggableOn + module Taggable + module TaggedWithQuery + class AnyTagsQuery < QueryBase + def build + taggable_model.select(all_fields) + .where(model_has_at_least_one_tag) + .order(Arel.sql(order_conditions)) + .readonly(false) + end - def all_fields - taggable_arel_table[Arel.star] - end + private - def model_has_at_least_one_tag - tagging_arel_table.project(Arel.star).where(at_least_one_tag).exists - end + def all_fields + taggable_arel_table[Arel.star] + end - def at_least_one_tag - exists_contition = tagging_arel_table[:taggable_id].eq(taggable_arel_table[taggable_model.primary_key]) - .and(tagging_arel_table[:taggable_type].eq(taggable_model.base_class.name)) - .and( - tagging_arel_table[:tag_id].in( - tag_arel_table.project(tag_arel_table[:id]).where(tags_match_type) - ) - ) + def model_has_at_least_one_tag + tagging_arel_table.project(Arel.star).where(at_least_one_tag).exists + end - if options[:start_at].present? - exists_contition = exists_contition.and(tagging_arel_table[:created_at].gteq(options[:start_at])) - end + def at_least_one_tag + exists_contition = tagging_arel_table[:taggable_id].eq(taggable_arel_table[taggable_model.primary_key]) + .and(tagging_arel_table[:taggable_type].eq(taggable_model.base_class.name)) + .and( + tagging_arel_table[:tag_id].in( + tag_arel_table.project(tag_arel_table[:id]).where(tags_match_type) + ) + ) - if options[:end_at].present? - exists_contition = exists_contition.and(tagging_arel_table[:created_at].lteq(options[:end_at])) - end + if options[:start_at].present? + exists_contition = exists_contition.and(tagging_arel_table[:created_at].gteq(options[:start_at])) + end - if options[:on].present? - exists_contition = exists_contition.and(tagging_arel_table[:context].eq(options[:on])) - end + if options[:end_at].present? + exists_contition = exists_contition.and(tagging_arel_table[:created_at].lteq(options[:end_at])) + end - if (owner = options[:owned_by]).present? - exists_contition = exists_contition.and(tagging_arel_table[:tagger_id].eq(owner.id)) - .and(tagging_arel_table[:tagger_type].eq(owner.class.base_class.to_s)) - end + if options[:on].present? + exists_contition = exists_contition.and(tagging_arel_table[:context].eq(options[:on])) + end - exists_contition - end + if (owner = options[:owned_by]).present? + exists_contition = exists_contition.and(tagging_arel_table[:tagger_id].eq(owner.id)) + .and(tagging_arel_table[:tagger_type].eq(owner.class.base_class.to_s)) + end - def order_conditions - order_by = [] - if options[:order_by_matching_tag_count].present? - order_by << "(SELECT count(*) FROM #{tagging_model.table_name} WHERE #{at_least_one_tag.to_sql}) desc" - end + exists_contition + end - order_by << options[:order] if options[:order].present? - order_by.join(', ') - end + def order_conditions + order_by = [] + if options[:order_by_matching_tag_count].present? + order_by << "(SELECT count(*) FROM #{tagging_model.table_name} WHERE #{at_least_one_tag.to_sql}) desc" + end - def alias_name(tag_list) - alias_base_name = taggable_model.base_class.name.downcase - taggings_context = options[:on] ? "_#{options[:on]}" : '' + order_by << options[:order] if options[:order].present? + order_by.join(', ') + end - taggings_alias = adjust_taggings_alias( - "#{alias_base_name[0..4]}#{taggings_context[0..6]}_taggings_#{ActsAsTaggableOn::Utils.sha_prefix(tag_list.join('_'))}" - ) + def alias_name(tag_list) + alias_base_name = taggable_model.base_class.name.downcase + taggings_context = options[:on] ? "_#{options[:on]}" : '' - taggings_alias + adjust_taggings_alias( + "#{alias_base_name[0..4]}#{taggings_context[0..6]}_taggings_#{ActsAsTaggableOn::Utils.sha_prefix(tag_list.join('_'))}" + ) + end + end end end end diff --git a/lib/acts_as_taggable_on/taggable/tagged_with_query/exclude_tags_query.rb b/lib/acts_as_taggable_on/taggable/tagged_with_query/exclude_tags_query.rb index 8ec94b0bd..1201ea47f 100644 --- a/lib/acts_as_taggable_on/taggable/tagged_with_query/exclude_tags_query.rb +++ b/lib/acts_as_taggable_on/taggable/tagged_with_query/exclude_tags_query.rb @@ -1,82 +1,85 @@ -module ActsAsTaggableOn::Taggable::TaggedWithQuery - class ExcludeTagsQuery < QueryBase - def build - taggable_model.joins(owning_to_tagger) - .where(tags_not_in_list) - .having(tags_that_matches_count) - .readonly(false) - end - - private - - def tags_not_in_list - return taggable_arel_table[:id].not_in( +# frozen_string_literal: true + +module ActsAsTaggableOn + module Taggable + module TaggedWithQuery + class ExcludeTagsQuery < QueryBase + def build + taggable_model.joins(owning_to_tagger) + .where(tags_not_in_list) + .having(tags_that_matches_count) + .readonly(false) + end + + private + + def tags_not_in_list + taggable_arel_table[:id].not_in( tagging_arel_table .project(tagging_arel_table[:taggable_id]) .join(tag_arel_table) .on( - tagging_arel_table[:tag_id].eq(tag_arel_table[:id]) - .and(tagging_arel_table[:taggable_type].eq(taggable_model.base_class.name)) - .and(tags_match_type) - ) + tagging_arel_table[:tag_id].eq(tag_arel_table[:id]) + .and(tagging_arel_table[:taggable_type].eq(taggable_model.base_class.name)) + .and(tags_match_type) + ) ) - # FIXME: missing time scope, this is also missing in the original implementation - end + # FIXME: missing time scope, this is also missing in the original implementation + end + def owning_to_tagger + return [] if options[:owned_by].blank? - def owning_to_tagger - return [] unless options[:owned_by].present? + owner = options[:owned_by] - owner = options[:owned_by] + arel_join = taggable_arel_table + .join(tagging_arel_table) + .on( + tagging_arel_table[:tagger_id].eq(owner.id) + .and(tagging_arel_table[:tagger_type].eq(owner.class.base_class.to_s)) + .and(tagging_arel_table[:taggable_id].eq(taggable_arel_table[taggable_model.primary_key])) + .and(tagging_arel_table[:taggable_type].eq(taggable_model.base_class.name)) + ) - arel_join = taggable_arel_table - .join(tagging_arel_table) - .on( - tagging_arel_table[:tagger_id].eq(owner.id) - .and(tagging_arel_table[:tagger_type].eq(owner.class.base_class.to_s)) - .and(tagging_arel_table[:taggable_id].eq(taggable_arel_table[taggable_model.primary_key])) - .and(tagging_arel_table[:taggable_type].eq(taggable_model.base_class.name)) - ) + if options[:match_all].present? + arel_join = arel_join + .join(tagging_arel_table, Arel::Nodes::OuterJoin) + .on( + match_all_on_conditions + ) + end - if options[:match_all].present? - arel_join = arel_join - .join(tagging_arel_table, Arel::Nodes::OuterJoin) - .on( - match_all_on_conditions - ) - end + arel_join.join_sources + end - return arel_join.join_sources - end + def match_all_on_conditions + on_condition = tagging_arel_table[:taggable_id].eq(taggable_arel_table[taggable_model.primary_key]) + .and(tagging_arel_table[:taggable_type].eq(taggable_model.base_class.name)) - def match_all_on_conditions - on_condition = tagging_arel_table[:taggable_id].eq(taggable_arel_table[taggable_model.primary_key]) - .and(tagging_arel_table[:taggable_type].eq(taggable_model.base_class.name)) + if options[:start_at].present? + on_condition = on_condition.and(tagging_arel_table[:created_at].gteq(options[:start_at])) + end - if options[:start_at].present? - on_condition = on_condition.and(tagging_arel_table[:created_at].gteq(options[:start_at])) - end + if options[:end_at].present? + on_condition = on_condition.and(tagging_arel_table[:created_at].lteq(options[:end_at])) + end - if options[:end_at].present? - on_condition = on_condition.and(tagging_arel_table[:created_at].lteq(options[:end_at])) - end + on_condition = on_condition.and(tagging_arel_table[:context].eq(options[:on])) if options[:on].present? - if options[:on].present? - on_condition = on_condition.and(tagging_arel_table[:context].eq(options[:on])) - end + on_condition + end - on_condition - end + def tags_that_matches_count + return [] if options[:match_all].blank? - def tags_that_matches_count - return [] unless options[:match_all].present? + taggable_model.find_by_sql(tag_arel_table.project(Arel.star.count).where(tags_match_type).to_sql) - taggable_model.find_by_sql(tag_arel_table.project(Arel.star.count).where(tags_match_type).to_sql) - - tagging_arel_table[:taggable_id].count.eq( - tag_arel_table.project(Arel.star.count).where(tags_match_type) - ) + tagging_arel_table[:taggable_id].count.eq( + tag_arel_table.project(Arel.star.count).where(tags_match_type) + ) + end + end end end end diff --git a/lib/acts_as_taggable_on/taggable/tagged_with_query/query_base.rb b/lib/acts_as_taggable_on/taggable/tagged_with_query/query_base.rb index 36c9d97a2..8c7f09f81 100644 --- a/lib/acts_as_taggable_on/taggable/tagged_with_query/query_base.rb +++ b/lib/acts_as_taggable_on/taggable/tagged_with_query/query_base.rb @@ -1,61 +1,69 @@ -module ActsAsTaggableOn::Taggable::TaggedWithQuery - class QueryBase - def initialize(taggable_model, tag_model, tagging_model, tag_list, options) - @taggable_model = taggable_model - @tag_model = tag_model - @tagging_model = tagging_model - @tag_list = tag_list - @options = options - end +# frozen_string_literal: true - private +module ActsAsTaggableOn + module Taggable + module TaggedWithQuery + class QueryBase + def initialize(taggable_model, tag_model, tagging_model, tag_list, options) + @taggable_model = taggable_model + @tag_model = tag_model + @tagging_model = tagging_model + @tag_list = tag_list + @options = options + end - attr_reader :taggable_model, :tag_model, :tagging_model, :tag_list, :options + private - def taggable_arel_table - @taggable_arel_table ||= taggable_model.arel_table - end + attr_reader :taggable_model, :tag_model, :tagging_model, :tag_list, :options - def tag_arel_table - @tag_arel_table ||= tag_model.arel_table - end + def taggable_arel_table + @taggable_arel_table ||= taggable_model.arel_table + end - def tagging_arel_table - @tagging_arel_table ||=tagging_model.arel_table - end + def tag_arel_table + @tag_arel_table ||= tag_model.arel_table + end - def tag_match_type(tag) - matches_attribute = tag_arel_table[:name] - matches_attribute = matches_attribute.lower unless ActsAsTaggableOn.strict_case_match + def tagging_arel_table + @tagging_arel_table ||= tagging_model.arel_table + end - if options[:wild].present? - matches_attribute.matches("%#{escaped_tag(tag)}%", "!", ActsAsTaggableOn.strict_case_match) - else - matches_attribute.matches(escaped_tag(tag), "!", ActsAsTaggableOn.strict_case_match) - end - end + def tag_match_type(tag) + matches_attribute = tag_arel_table[:name] + matches_attribute = matches_attribute.lower unless ActsAsTaggableOn.strict_case_match - def tags_match_type - matches_attribute = tag_arel_table[:name] - matches_attribute = matches_attribute.lower unless ActsAsTaggableOn.strict_case_match + if options[:wild].present? + matches_attribute.matches("%#{escaped_tag(tag)}%", '!', ActsAsTaggableOn.strict_case_match) + else + matches_attribute.matches(escaped_tag(tag), '!', ActsAsTaggableOn.strict_case_match) + end + end - if options[:wild].present? - matches_attribute.matches_any(tag_list.map{|tag| "%#{escaped_tag(tag)}%"}, "!", ActsAsTaggableOn.strict_case_match) - else - matches_attribute.matches_any(tag_list.map{|tag| "#{escaped_tag(tag)}"}, "!", ActsAsTaggableOn.strict_case_match) - end - end + def tags_match_type + matches_attribute = tag_arel_table[:name] + matches_attribute = matches_attribute.lower unless ActsAsTaggableOn.strict_case_match - def escaped_tag(tag) - tag = tag.downcase unless ActsAsTaggableOn.strict_case_match - ActsAsTaggableOn::Utils.escape_like(tag) - end + if options[:wild].present? + matches_attribute.matches_any(tag_list.map do |tag| + "%#{escaped_tag(tag)}%" + end, '!', ActsAsTaggableOn.strict_case_match) + else + matches_attribute.matches_any(tag_list.map do |tag| + escaped_tag(tag).to_s + end, '!', ActsAsTaggableOn.strict_case_match) + end + end + + def escaped_tag(tag) + tag = tag.downcase unless ActsAsTaggableOn.strict_case_match + ActsAsTaggableOn::Utils.escape_like(tag) + end - def adjust_taggings_alias(taggings_alias) - if taggings_alias.size > 75 - taggings_alias = 'taggings_alias_' + Digest::SHA1.hexdigest(taggings_alias) + def adjust_taggings_alias(taggings_alias) + taggings_alias = "taggings_alias_#{Digest::SHA1.hexdigest(taggings_alias)}" if taggings_alias.size > 75 + taggings_alias + end end - taggings_alias end end end diff --git a/lib/acts_as_taggable_on/tagger.rb b/lib/acts_as_taggable_on/tagger.rb index 28487e353..1611efdc1 100644 --- a/lib/acts_as_taggable_on/tagger.rb +++ b/lib/acts_as_taggable_on/tagger.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActsAsTaggableOn module Tagger def self.included(base) @@ -13,7 +15,7 @@ module ClassMethods # class User < ActiveRecord::Base # acts_as_tagger # end - def acts_as_tagger(opts={}) + def acts_as_tagger(opts = {}) class_eval do owned_taggings_scope = opts.delete(:scope) @@ -54,14 +56,16 @@ module InstanceMethods # # Example: # @user.tag(@photo, :with => "paris, normandy", :on => :locations) - def tag(taggable, opts={}) + def tag(taggable, opts = {}) opts.reverse_merge!(force: true) skip_save = opts.delete(:skip_save) return false unless taggable.respond_to?(:is_taggable?) && taggable.is_taggable? - fail 'You need to specify a tag context using :on' unless opts.key?(:on) - fail 'You need to specify some tags using :with' unless opts.key?(:with) - fail "No context :#{opts[:on]} defined in #{taggable.class}" unless opts[:force] || taggable.tag_types.include?(opts[:on]) + raise 'You need to specify a tag context using :on' unless opts.key?(:on) + raise 'You need to specify some tags using :with' unless opts.key?(:with) + unless opts[:force] || taggable.tag_types.include?(opts[:on]) + raise "No context :#{opts[:on]} defined in #{taggable.class}" + end taggable.set_owner_tag_list_on(self, opts[:on].to_s, opts[:with]) taggable.save unless skip_save diff --git a/lib/acts_as_taggable_on/tagging.rb b/lib/acts_as_taggable_on/tagging.rb index c759462bb..1bdb8a575 100644 --- a/lib/acts_as_taggable_on/tagging.rb +++ b/lib/acts_as_taggable_on/tagging.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + module ActsAsTaggableOn - class Tagging < ::ActiveRecord::Base #:nodoc: + class Tagging < ::ActiveRecord::Base # :nodoc: self.table_name = ActsAsTaggableOn.taggings_table DEFAULT_CONTEXT = 'tags' @@ -19,7 +21,7 @@ class Tagging < ::ActiveRecord::Base #:nodoc: validates_presence_of :context validates_presence_of :tag_id - validates_uniqueness_of :tag_id, scope: [:taggable_type, :taggable_id, :context, :tagger_id, :tagger_type] + validates_uniqueness_of :tag_id, scope: %i[taggable_type taggable_id context tagger_id tagger_type] after_destroy :remove_unused_tags @@ -29,8 +31,8 @@ def remove_unused_tags if ActsAsTaggableOn.remove_unused_tags if ActsAsTaggableOn.tags_counter tag.destroy if tag.reload.taggings_count.zero? - else - tag.destroy if tag.reload.taggings.none? + elsif tag.reload.taggings.none? + tag.destroy end end end diff --git a/lib/acts_as_taggable_on/tags_helper.rb b/lib/acts_as_taggable_on/tags_helper.rb index bcbeb19ef..71121e2f4 100644 --- a/lib/acts_as_taggable_on/tags_helper.rb +++ b/lib/acts_as_taggable_on/tags_helper.rb @@ -1,10 +1,12 @@ +# frozen_string_literal: true + module ActsAsTaggableOn module TagsHelper # See the wiki for an example using tag_cloud. def tag_cloud(tags, classes) return [] if tags.empty? - max_count = tags.sort_by(&:taggings_count).last.taggings_count.to_f + max_count = tags.max_by(&:taggings_count).taggings_count.to_f tags.each do |tag| index = ((tag.taggings_count / max_count) * (classes.size - 1)) diff --git a/lib/acts_as_taggable_on/version.rb b/lib/acts_as_taggable_on/version.rb index 1cf7519d3..7a9e5c78d 100644 --- a/lib/acts_as_taggable_on/version.rb +++ b/lib/acts_as_taggable_on/version.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActsAsTaggableOn VERSION = '9.0.0' end diff --git a/spec/support/database.rb b/spec/support/database.rb index efa686635..2a48ae9f2 100644 --- a/spec/support/database.rb +++ b/spec/support/database.rb @@ -1,41 +1,46 @@ +# frozen_string_literal: true + # set adapter to use, default is sqlite3 # to use an alternative adapter run => rake spec DB='postgresql' db_name = ENV['DB'] || 'sqlite3' -database_yml = File.expand_path('../../internal/config/database.yml', __FILE__) - -if File.exist?(database_yml) +database_yml = File.expand_path('../internal/config/database.yml', __dir__) - ActiveRecord::Base.configurations = YAML.load_file(database_yml) - ActiveRecord::Base.logger = Logger.new(File.join(File.dirname(__FILE__), '../debug.log')) - ActiveRecord::Base.logger.level = ENV['TRAVIS'] ? ::Logger::ERROR : ::Logger::DEBUG - ActiveRecord::Migration.verbose = false - if ActiveRecord.version >= Gem::Version.new('7.0.0.alpha2') - ActiveRecord.default_timezone = :utc - config = ActiveRecord::Base.configurations.configs_for(env_name: db_name) - else - ActiveRecord::Base.default_timezone = :utc - config = ActiveSupport::HashWithIndifferentAccess.new(ActiveRecord::Base.configurations[db_name]) - end +unless File.exist?(database_yml) + raise "Please create #{database_yml} first to configure your database. Take a look at: #{database_yml}.sample" +end - begin - ActiveRecord::Base.establish_connection(db_name.to_sym) - ActiveRecord::Base.connection - rescue - case db_name - when /mysql/ - ActiveRecord::Base.establish_connection(config.merge('database' => nil)) - ActiveRecord::Base.connection.create_database(config['database'], {charset: 'utf8', collation: 'utf8_unicode_ci'}) - when 'postgresql' - ActiveRecord::Base.establish_connection(config.merge('database' => 'postgres', 'schema_search_path' => 'public')) - ActiveRecord::Base.connection.create_database(config['database'], config.merge('encoding' => 'utf8')) - end +ActiveRecord::Base.configurations = YAML.load_file(database_yml) +ActiveRecord::Base.logger = Logger.new(File.join(File.dirname(__FILE__), '../debug.log')) +ActiveRecord::Base.logger.level = ENV['CI'] ? ::Logger::ERROR : ::Logger::DEBUG +ActiveRecord::Migration.verbose = false +if ActiveRecord.version >= Gem::Version.new('7.0.0.alpha2') + ActiveRecord.default_timezone = :utc +else + ActiveRecord::Base.default_timezone = :utc +end +config = if ActiveRecord.version >= Gem::Version.new('6.1.0') + ActiveRecord::Base.configurations.configs_for(env_name: db_name) + else + ActiveSupport::HashWithIndifferentAccess.new(ActiveRecord::Base.configurations[db_name]) + end - ActiveRecord::Base.establish_connection(config) +begin + ActiveRecord::Base.establish_connection(db_name.to_sym) + ActiveRecord::Base.connection +rescue StandardError + case db_name + when /mysql/ + ActiveRecord::Base.establish_connection(config.merge('database' => nil)) + ActiveRecord::Base.connection.create_database(config['database'], + { charset: 'utf8', collation: 'utf8_unicode_ci' }) + when 'postgresql' + ActiveRecord::Base.establish_connection(config.merge('database' => 'postgres', + 'schema_search_path' => 'public')) + ActiveRecord::Base.connection.create_database(config['database'], config.merge('encoding' => 'utf8')) end - require File.dirname(__FILE__) + '/../internal/db/schema.rb' - Dir[File.dirname(__dir__) + '/internal/app/models/*.rb'].each { |f| require f } - -else - fail "Please create #{database_yml} first to configure your database. Take a look at: #{database_yml}.sample" + ActiveRecord::Base.establish_connection(config) end + +require "#{File.dirname(__FILE__)}/../internal/db/schema.rb" +Dir["#{File.dirname(__dir__)}/internal/app/models/*.rb"].each { |f| require f }