acts-as-taggable-on 4.0.0.pre → 7.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +1 -1
  3. data/.travis.yml +28 -15
  4. data/Appraisals +12 -10
  5. data/CHANGELOG.md +200 -71
  6. data/CONTRIBUTING.md +13 -0
  7. data/Gemfile +1 -1
  8. data/README.md +68 -28
  9. data/acts-as-taggable-on.gemspec +2 -6
  10. data/db/migrate/1_acts_as_taggable_on_migration.rb +14 -8
  11. data/db/migrate/2_add_missing_unique_indices.rb +14 -9
  12. data/db/migrate/3_add_taggings_counter_cache_to_tags.rb +9 -4
  13. data/db/migrate/4_add_missing_taggable_index.rb +8 -3
  14. data/db/migrate/5_change_collation_for_tag_names.rb +7 -2
  15. data/db/migrate/6_add_missing_indexes_on_taggings.rb +22 -0
  16. data/gemfiles/activerecord_5.0.gemfile +11 -5
  17. data/gemfiles/activerecord_5.1.gemfile +21 -0
  18. data/gemfiles/activerecord_5.2.gemfile +21 -0
  19. data/gemfiles/activerecord_6.0.gemfile +21 -0
  20. data/gemfiles/activerecord_6.1.gemfile +23 -0
  21. data/lib/acts-as-taggable-on.rb +6 -2
  22. data/lib/acts_as_taggable_on/tag.rb +17 -23
  23. data/lib/acts_as_taggable_on/tag_list.rb +1 -0
  24. data/lib/acts_as_taggable_on/taggable.rb +0 -1
  25. data/lib/acts_as_taggable_on/taggable/cache.rb +38 -34
  26. data/lib/acts_as_taggable_on/taggable/collection.rb +9 -7
  27. data/lib/acts_as_taggable_on/taggable/core.rb +41 -181
  28. data/lib/acts_as_taggable_on/taggable/ownership.rb +16 -5
  29. data/lib/acts_as_taggable_on/taggable/related.rb +1 -1
  30. data/lib/acts_as_taggable_on/taggable/tag_list_type.rb +4 -0
  31. data/lib/acts_as_taggable_on/taggable/tagged_with_query.rb +16 -0
  32. data/lib/acts_as_taggable_on/taggable/tagged_with_query/all_tags_query.rb +111 -0
  33. data/lib/acts_as_taggable_on/taggable/tagged_with_query/any_tags_query.rb +70 -0
  34. data/lib/acts_as_taggable_on/taggable/tagged_with_query/exclude_tags_query.rb +82 -0
  35. data/lib/acts_as_taggable_on/taggable/tagged_with_query/query_base.rb +61 -0
  36. data/lib/acts_as_taggable_on/tagger.rb +3 -3
  37. data/lib/acts_as_taggable_on/tagging.rb +6 -3
  38. data/lib/acts_as_taggable_on/utils.rb +4 -4
  39. data/lib/acts_as_taggable_on/version.rb +1 -2
  40. data/spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb +4 -12
  41. data/spec/acts_as_taggable_on/caching_spec.rb +34 -10
  42. data/spec/acts_as_taggable_on/{taggable/dirty_spec.rb → dirty_spec.rb} +28 -13
  43. data/spec/acts_as_taggable_on/single_table_inheritance_spec.rb +28 -8
  44. data/spec/acts_as_taggable_on/taggable_spec.rb +16 -13
  45. data/spec/acts_as_taggable_on/tagger_spec.rb +2 -2
  46. data/spec/internal/app/models/altered_inheriting_taggable_model.rb +2 -0
  47. data/spec/internal/app/models/cached_model_with_array.rb +6 -0
  48. data/spec/internal/app/models/columns_override_model.rb +5 -0
  49. data/spec/internal/app/models/company.rb +1 -1
  50. data/spec/internal/app/models/inheriting_taggable_model.rb +2 -0
  51. data/spec/internal/app/models/market.rb +1 -1
  52. data/spec/internal/app/models/non_standard_id_taggable_model.rb +1 -1
  53. data/spec/internal/app/models/student.rb +2 -0
  54. data/spec/internal/app/models/taggable_model.rb +1 -0
  55. data/spec/internal/app/models/user.rb +1 -1
  56. data/spec/internal/db/schema.rb +14 -5
  57. data/spec/spec_helper.rb +0 -1
  58. data/spec/support/database.rb +4 -4
  59. metadata +30 -61
  60. data/db/migrate/6_add_missing_indexes.rb +0 -12
  61. data/gemfiles/activerecord_4.0.gemfile +0 -16
  62. data/gemfiles/activerecord_4.1.gemfile +0 -16
  63. data/gemfiles/activerecord_4.2.gemfile +0 -15
  64. data/lib/acts_as_taggable_on/taggable/dirty.rb +0 -36
  65. data/spec/internal/app/models/models.rb +0 -90
@@ -0,0 +1,21 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "activerecord", "~> 5.2.0"
4
+ case ENV["DB"]
5
+ when "postgresql"
6
+ gem 'pg'
7
+ when "mysql"
8
+ gem 'mysql2', '~> 0.3'
9
+ else
10
+ gem "sqlite3", "~> 1.3", "< 1.4"
11
+ end
12
+
13
+ group :local_development do
14
+ gem "guard"
15
+ gem "guard-rspec"
16
+ gem "appraisal"
17
+ gem "rake"
18
+ gem "byebug", platforms: [:mri]
19
+ end
20
+
21
+ gemspec path: "../"
@@ -0,0 +1,21 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "activerecord", "~> 6.0.0"
4
+ case ENV["DB"]
5
+ when "postgresql"
6
+ gem 'pg'
7
+ when "mysql"
8
+ gem 'mysql2', '~> 0.4'
9
+ else
10
+ gem 'sqlite3'
11
+ end
12
+
13
+ group :local_development do
14
+ gem "guard"
15
+ gem "guard-rspec"
16
+ gem "appraisal"
17
+ gem "rake"
18
+ gem "byebug", platforms: [:mri]
19
+ end
20
+
21
+ gemspec path: "../"
@@ -0,0 +1,23 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activerecord", "~> 6.1.0"
6
+ case ENV["DB"]
7
+ when "postgresql"
8
+ gem 'pg'
9
+ when "mysql"
10
+ gem 'mysql2', '~> 0.5'
11
+ else
12
+ gem 'sqlite3'
13
+ end
14
+
15
+ group :local_development do
16
+ gem "guard"
17
+ gem "guard-rspec"
18
+ gem "appraisal"
19
+ gem "rake"
20
+ gem "byebug", platforms: [:mri]
21
+ end
22
+
23
+ gemspec path: "../"
@@ -31,6 +31,7 @@ module ActsAsTaggableOn
31
31
  autoload :Dirty
32
32
  autoload :Ownership
33
33
  autoload :Related
34
+ autoload :TagListType
34
35
  end
35
36
 
36
37
  autoload :Utils
@@ -57,13 +58,14 @@ module ActsAsTaggableOn
57
58
  def self.glue
58
59
  setting = @configuration.delimiter
59
60
  delimiter = setting.kind_of?(Array) ? setting[0] : setting
60
- delimiter.ends_with?(' ') ? delimiter : "#{delimiter} "
61
+ delimiter.end_with?(' ') ? delimiter : "#{delimiter} "
61
62
  end
62
63
 
63
64
  class Configuration
64
65
  attr_accessor :force_lowercase, :force_parameterize,
65
66
  :remove_unused_tags, :default_parser,
66
- :tags_counter
67
+ :tags_counter, :tags_table,
68
+ :taggings_table
67
69
  attr_reader :delimiter, :strict_case_match
68
70
 
69
71
  def initialize
@@ -75,6 +77,8 @@ module ActsAsTaggableOn
75
77
  @tags_counter = true
76
78
  @default_parser = DefaultParser
77
79
  @force_binary_collation = false
80
+ @tags_table = :tags
81
+ @taggings_table = :taggings
78
82
  end
79
83
 
80
84
  def strict_case_match=(force_cs)
@@ -1,6 +1,7 @@
1
1
  # encoding: utf-8
2
2
  module ActsAsTaggableOn
3
3
  class Tag < ::ActiveRecord::Base
4
+ self.table_name = ActsAsTaggableOn.tags_table
4
5
 
5
6
  ### ASSOCIATIONS:
6
7
 
@@ -9,7 +10,7 @@ module ActsAsTaggableOn
9
10
  ### VALIDATIONS:
10
11
 
11
12
  validates_presence_of :name
12
- validates_uniqueness_of :name, if: :validates_name_uniqueness?
13
+ validates_uniqueness_of :name, if: :validates_name_uniqueness?, case_sensitive: true
13
14
  validates_length_of :name, maximum: 255
14
15
 
15
16
  # monkey patch this method if don't need name uniqueness validation
@@ -50,8 +51,8 @@ module ActsAsTaggableOn
50
51
 
51
52
  def self.for_context(context)
52
53
  joins(:taggings).
53
- where(["taggings.context = ?", context]).
54
- select("DISTINCT tags.*")
54
+ where(["#{ActsAsTaggableOn.taggings_table}.context = ?", context]).
55
+ select("DISTINCT #{ActsAsTaggableOn.tags_table}.*")
55
56
  end
56
57
 
57
58
  ### CLASS METHODS:
@@ -70,16 +71,19 @@ module ActsAsTaggableOn
70
71
  return [] if list.empty?
71
72
 
72
73
  existing_tags = named_any(list)
73
-
74
74
  list.map do |tag_name|
75
- comparable_tag_name = comparable_name(tag_name)
76
- existing_tag = existing_tags.find { |tag| comparable_name(tag.name) == comparable_tag_name }
77
75
  begin
76
+ tries ||= 3
77
+ comparable_tag_name = comparable_name(tag_name)
78
+ existing_tag = existing_tags.find { |tag| comparable_name(tag.name) == comparable_tag_name }
78
79
  existing_tag || create(name: tag_name)
79
80
  rescue ActiveRecord::RecordNotUnique
80
- # Postgres aborts the current transaction with
81
- # PG::InFailedSqlTransaction: ERROR: current transaction is aborted, commands ignored until end of transaction block
82
- # so we have to rollback this transaction
81
+ if (tries -= 1).positive?
82
+ ActiveRecord::Base.connection.execute 'ROLLBACK'
83
+ existing_tags = named_any(list)
84
+ retry
85
+ end
86
+
83
87
  raise DuplicateTagError.new("'#{tag_name}' has already been taken")
84
88
  end
85
89
  end
@@ -101,8 +105,6 @@ module ActsAsTaggableOn
101
105
 
102
106
  class << self
103
107
 
104
-
105
-
106
108
  private
107
109
 
108
110
  def comparable_name(str)
@@ -117,20 +119,12 @@ module ActsAsTaggableOn
117
119
  ActsAsTaggableOn::Utils.using_mysql? ? 'BINARY ' : nil
118
120
  end
119
121
 
120
- def unicode_downcase(string)
121
- if ActiveSupport::Multibyte::Unicode.respond_to?(:downcase)
122
- ActiveSupport::Multibyte::Unicode.downcase(string)
123
- else
124
- ActiveSupport::Multibyte::Chars.new(string).downcase.to_s
125
- end
122
+ def as_8bit_ascii(string)
123
+ string.to_s.mb_chars
126
124
  end
127
125
 
128
- def as_8bit_ascii(string)
129
- if defined?(Encoding)
130
- string.to_s.dup.force_encoding('BINARY')
131
- else
132
- string.to_s.mb_chars
133
- end
126
+ def unicode_downcase(string)
127
+ as_8bit_ascii(string).downcase
134
128
  end
135
129
 
136
130
  def sanitize_sql_for_named_any(tag)
@@ -41,6 +41,7 @@ module ActsAsTaggableOn
41
41
  # Appends the elements of +other_tag_list+ to +self+.
42
42
  def concat(other_tag_list)
43
43
  super(other_tag_list).send(:clean!)
44
+ self
44
45
  end
45
46
 
46
47
  ##
@@ -96,7 +96,6 @@ module ActsAsTaggableOn
96
96
  include Cache
97
97
  include Ownership
98
98
  include Related
99
- include Dirty
100
99
  end
101
100
  end
102
101
  end
@@ -3,47 +3,51 @@ module ActsAsTaggableOn::Taggable
3
3
  def self.included(base)
4
4
  # When included, conditionally adds tag caching methods when the model
5
5
  # has any "cached_#{tag_type}_list" column
6
- base.instance_eval do
7
- # @private
8
- def _has_tags_cache_columns?(db_columns)
9
- db_column_names = db_columns.map(&:name)
10
- tag_types.any? do |context|
11
- db_column_names.include?("cached_#{context.to_s.singularize}_list")
12
- end
6
+ base.extend Columns
7
+ end
8
+
9
+ module Columns
10
+ # ActiveRecord::Base.columns makes a database connection and caches the
11
+ # calculated columns hash for the record as @columns. Since we don't
12
+ # want to add caching methods until we confirm the presence of a
13
+ # caching column, and we don't want to force opening a database
14
+ # connection when the class is loaded, here we intercept and cache
15
+ # the call to :columns as @acts_as_taggable_on_cache_columns
16
+ # to mimic the underlying behavior. While processing this first
17
+ # call to columns, we do the caching column check and dynamically add
18
+ # the class and instance methods
19
+ # FIXME: this method cannot compile in rubinius
20
+ def columns
21
+ @acts_as_taggable_on_cache_columns ||= begin
22
+ db_columns = super
23
+ _add_tags_caching_methods if _has_tags_cache_columns?(db_columns)
24
+ db_columns
13
25
  end
26
+ end
14
27
 
15
- # @private
16
- def _add_tags_caching_methods
17
- send :include, ActsAsTaggableOn::Taggable::Cache::InstanceMethods
18
- extend ActsAsTaggableOn::Taggable::Cache::ClassMethods
28
+ def reset_column_information
29
+ super
30
+ @acts_as_taggable_on_cache_columns = nil
31
+ end
19
32
 
20
- before_save :save_cached_tag_list
33
+ private
21
34
 
22
- initialize_tags_cache
35
+ # @private
36
+ def _has_tags_cache_columns?(db_columns)
37
+ db_column_names = db_columns.map(&:name)
38
+ tag_types.any? do |context|
39
+ db_column_names.include?("cached_#{context.to_s.singularize}_list")
23
40
  end
41
+ end
24
42
 
25
- # ActiveRecord::Base.columns makes a database connection and caches the
26
- # calculated columns hash for the record as @columns. Since we don't
27
- # want to add caching methods until we confirm the presence of a
28
- # caching column, and we don't want to force opening a database
29
- # connection when the class is loaded, here we intercept and cache
30
- # the call to :columns as @acts_as_taggable_on_cache_columns
31
- # to mimic the underlying behavior. While processing this first
32
- # call to columns, we do the caching column check and dynamically add
33
- # the class and instance methods
34
- # FIXME: this method cannot compile in rubinius
35
- def columns
36
- @acts_as_taggable_on_cache_columns ||= begin
37
- db_columns = super
38
- _add_tags_caching_methods if _has_tags_cache_columns?(db_columns)
39
- db_columns
40
- end
41
- end
43
+ # @private
44
+ def _add_tags_caching_methods
45
+ send :include, ActsAsTaggableOn::Taggable::Cache::InstanceMethods
46
+ extend ActsAsTaggableOn::Taggable::Cache::ClassMethods
42
47
 
43
- def reset_column_information
44
- super
45
- @acts_as_taggable_on_cache_columns = nil
46
- end
48
+ before_save :save_cached_tag_list
49
+
50
+ initialize_tags_cache
47
51
  end
48
52
  end
49
53
 
@@ -94,16 +94,18 @@ module ActsAsTaggableOn::Taggable
94
94
  ## Generate conditions:
95
95
  options[:conditions] = sanitize_sql(options[:conditions]) if options[:conditions]
96
96
 
97
- ## Generate joins:
98
- taggable_join = "INNER JOIN #{table_name} ON #{table_name}.#{primary_key} = #{ActsAsTaggableOn::Tagging.table_name}.taggable_id"
99
- taggable_join << " AND #{table_name}.#{inheritance_column} = '#{name}'" unless descends_from_active_record? # Current model is STI descendant, so add type checking to the join condition
100
-
101
97
  ## Generate scope:
102
98
  tagging_scope = ActsAsTaggableOn::Tagging.select("#{ActsAsTaggableOn::Tagging.table_name}.tag_id, COUNT(#{ActsAsTaggableOn::Tagging.table_name}.tag_id) AS tags_count")
103
99
  tag_scope = ActsAsTaggableOn::Tag.select("#{ActsAsTaggableOn::Tag.table_name}.*, #{ActsAsTaggableOn::Tagging.table_name}.tags_count AS count").order(options[:order]).limit(options[:limit])
104
100
 
105
- # Joins and conditions
106
- tagging_scope = tagging_scope.joins(taggable_join)
101
+ # Current model is STI descendant, so add type checking to the join condition
102
+ unless descends_from_active_record?
103
+ taggable_join = "INNER JOIN #{table_name} ON #{table_name}.#{primary_key} = #{ActsAsTaggableOn::Tagging.table_name}.taggable_id"
104
+ taggable_join << " AND #{table_name}.#{inheritance_column} = '#{name}'"
105
+ tagging_scope = tagging_scope.joins(taggable_join)
106
+ end
107
+
108
+ # Conditions
107
109
  tagging_conditions(options).each { |condition| tagging_scope = tagging_scope.where(condition) }
108
110
  tag_scope = tag_scope.where(options[:conditions])
109
111
 
@@ -174,7 +176,7 @@ module ActsAsTaggableOn::Taggable
174
176
  # See https://github.com/rails/rails/blob/master/activerecord/lib/active_record/relation/calculations.rb#L38
175
177
  def count(column_name = :all, options = {})
176
178
  # https://github.com/rails/rails/commit/da9b5d4a8435b744fcf278fffd6d7f1e36d4a4f2
177
- ActsAsTaggableOn::Utils.active_record5? ? super(column_name) : super(column_name, options)
179
+ super(column_name)
178
180
  end
179
181
  end
180
182
  end
@@ -1,5 +1,9 @@
1
+ require_relative 'tagged_with_query'
2
+ require_relative 'tag_list_type'
3
+
1
4
  module ActsAsTaggableOn::Taggable
2
5
  module Core
6
+
3
7
  def self.included(base)
4
8
  base.extend ActsAsTaggableOn::Taggable::Core::ClassMethods
5
9
 
@@ -25,13 +29,17 @@ module ActsAsTaggableOn::Taggable
25
29
  # the associations tag_taggings & tags are always returned in created order
26
30
  has_many context_taggings, -> { includes(:tag).order(taggings_order).where(context: tags_type) },
27
31
  as: :taggable,
28
- class_name: ActsAsTaggableOn::Tagging,
29
- dependent: :destroy
32
+ class_name: 'ActsAsTaggableOn::Tagging',
33
+ dependent: :destroy,
34
+ after_add: :dirtify_tag_list,
35
+ after_remove: :dirtify_tag_list
30
36
 
31
37
  has_many context_tags, -> { order(taggings_order) },
32
- class_name: ActsAsTaggableOn::Tag,
38
+ class_name: 'ActsAsTaggableOn::Tag',
33
39
  through: context_taggings,
34
40
  source: :tag
41
+
42
+ attribute "#{tags_type.singularize}_list".to_sym, ActsAsTaggableOn::Taggable::TagListType.new
35
43
  end
36
44
 
37
45
  taggable_mixin.class_eval <<-RUBY, __FILE__, __LINE__ + 1
@@ -40,12 +48,30 @@ module ActsAsTaggableOn::Taggable
40
48
  end
41
49
 
42
50
  def #{tag_type}_list=(new_tags)
51
+ parsed_new_list = ActsAsTaggableOn.default_parser.new(new_tags).parse
52
+
53
+ if self.class.preserve_tag_order? || (parsed_new_list.sort != #{tag_type}_list.sort)
54
+ if ActsAsTaggableOn::Utils.legacy_activerecord?
55
+ set_attribute_was("#{tag_type}_list", #{tag_type}_list)
56
+ else
57
+ unless #{tag_type}_list_changed?
58
+ @attributes["#{tag_type}_list"] = ActiveModel::Attribute.from_user("#{tag_type}_list", #{tag_type}_list, ActsAsTaggableOn::Taggable::TagListType.new)
59
+ end
60
+ end
61
+ write_attribute("#{tag_type}_list", parsed_new_list)
62
+ end
63
+
43
64
  set_tag_list_on('#{tags_type}', new_tags)
44
65
  end
45
66
 
46
67
  def all_#{tags_type}_list
47
68
  all_tags_list_on('#{tags_type}')
48
69
  end
70
+
71
+ private
72
+ def dirtify_tag_list(tagging)
73
+ attribute_will_change! tagging.context.singularize+"_list"
74
+ end
49
75
  RUBY
50
76
  end
51
77
  end
@@ -84,173 +110,19 @@ module ActsAsTaggableOn::Taggable
84
110
  def tagged_with(tags, options = {})
85
111
  tag_list = ActsAsTaggableOn.default_parser.new(tags).parse
86
112
  options = options.dup
87
- empty_result = where('1 = 0')
88
-
89
- return empty_result if tag_list.empty?
90
-
91
- joins = []
92
- conditions = []
93
- having = []
94
- select_clause = []
95
- order_by = []
96
-
97
- context = options.delete(:on)
98
- owned_by = options.delete(:owned_by)
99
- alias_base_name = undecorated_table_name.gsub('.', '_')
100
- # FIXME use ActiveRecord's connection quote_column_name
101
- quote = ActsAsTaggableOn::Utils.using_postgresql? ? '"' : ''
102
-
103
- if options.delete(:exclude)
104
- if options.delete(:wild)
105
- tags_conditions = tag_list.map { |t| sanitize_sql(["#{ActsAsTaggableOn::Tag.table_name}.name #{ActsAsTaggableOn::Utils.like_operator} ? ESCAPE '!'", "%#{ActsAsTaggableOn::Utils.escape_like(t)}%"]) }.join(' OR ')
106
- else
107
- tags_conditions = tag_list.map { |t| sanitize_sql(["#{ActsAsTaggableOn::Tag.table_name}.name #{ActsAsTaggableOn::Utils.like_operator} ?", t]) }.join(' OR ')
108
- end
109
-
110
- conditions << "#{table_name}.#{primary_key} NOT IN (SELECT #{ActsAsTaggableOn::Tagging.table_name}.taggable_id FROM #{ActsAsTaggableOn::Tagging.table_name} JOIN #{ActsAsTaggableOn::Tag.table_name} ON #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key} AND (#{tags_conditions}) WHERE #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = #{quote_value(base_class.name, nil)})"
111
-
112
- if owned_by
113
- joins << "JOIN #{ActsAsTaggableOn::Tagging.table_name}" +
114
- " ON #{ActsAsTaggableOn::Tagging.table_name}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" +
115
- " AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = #{quote_value(base_class.name, nil)}" +
116
- " AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_id = #{quote_value(owned_by.id, nil)}" +
117
- " AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_type = #{quote_value(owned_by.class.base_class.to_s, nil)}"
118
-
119
- joins << " AND " + sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
120
- joins << " AND " + sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
121
- end
122
-
123
- elsif any = options.delete(:any)
124
- # get tags, drop out if nothing returned (we need at least one)
125
- tags = if options.delete(:wild)
126
- ActsAsTaggableOn::Tag.named_like_any(tag_list)
127
- else
128
- ActsAsTaggableOn::Tag.named_any(tag_list)
129
- end
130
-
131
- return empty_result if tags.length == 0
132
-
133
- # setup taggings alias so we can chain, ex: items_locations_taggings_awesome_cool_123
134
- # avoid ambiguous column name
135
- taggings_context = context ? "_#{context}" : ''
136
-
137
- taggings_alias = adjust_taggings_alias(
138
- "#{alias_base_name[0..4]}#{taggings_context[0..6]}_taggings_#{ActsAsTaggableOn::Utils.sha_prefix(tags.map(&:name).join('_'))}"
139
- )
140
-
141
- tagging_cond = "#{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
142
- " WHERE #{taggings_alias}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" +
143
- " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name, nil)}"
144
-
145
- tagging_cond << " AND " + sanitize_sql(["#{taggings_alias}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
146
- tagging_cond << " AND " + sanitize_sql(["#{taggings_alias}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
147
-
148
- tagging_cond << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
149
-
150
- # don't need to sanitize sql, map all ids and join with OR logic
151
- tag_ids = tags.map { |t| quote_value(t.id, nil) }.join(', ')
152
- tagging_cond << " AND #{taggings_alias}.tag_id in (#{tag_ids})"
153
- select_clause << " #{table_name}.*" unless context and tag_types.one?
154
-
155
- if owned_by
156
- tagging_cond << ' AND ' +
157
- sanitize_sql([
158
- "#{taggings_alias}.tagger_id = ? AND #{taggings_alias}.tagger_type = ?",
159
- owned_by.id,
160
- owned_by.class.base_class.to_s
161
- ])
162
- end
163
-
164
- conditions << "EXISTS (SELECT 1 FROM #{tagging_cond})"
165
- if options.delete(:order_by_matching_tag_count)
166
- order_by << "(SELECT count(*) FROM #{tagging_cond}) desc"
167
- end
168
- else
169
- tags = ActsAsTaggableOn::Tag.named_any(tag_list)
170
-
171
- return empty_result unless tags.length == tag_list.length
172
-
173
- tags.each do |tag|
174
- taggings_alias = adjust_taggings_alias("#{alias_base_name[0..11]}_taggings_#{ActsAsTaggableOn::Utils.sha_prefix(tag.name)}")
175
- tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" \
176
- " ON #{taggings_alias}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" +
177
- " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name, nil)}" +
178
- " AND #{taggings_alias}.tag_id = #{quote_value(tag.id, nil)}"
179
-
180
- tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
181
- tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
182
-
183
- tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
184
-
185
- if owned_by
186
- tagging_join << ' AND ' +
187
- sanitize_sql([
188
- "#{taggings_alias}.tagger_id = ? AND #{taggings_alias}.tagger_type = ?",
189
- owned_by.id,
190
- owned_by.class.base_class.to_s
191
- ])
192
- end
193
113
 
194
- joins << tagging_join
195
- end
196
- end
197
-
198
- group ||= [] # Rails interprets this as a no-op in the group() call below
199
- if options.delete(:order_by_matching_tag_count)
200
- select_clause << "#{table_name}.*, COUNT(#{taggings_alias}.tag_id) AS #{taggings_alias}_count"
201
- group_columns = ActsAsTaggableOn::Utils.using_postgresql? ? grouped_column_names_for(self) : "#{table_name}.#{primary_key}"
202
- group = group_columns
203
- order_by << "#{taggings_alias}_count DESC"
204
-
205
- elsif options.delete(:match_all)
206
- taggings_alias, _ = adjust_taggings_alias("#{alias_base_name}_taggings_group"), "#{alias_base_name}_tags_group"
207
- joins << "LEFT OUTER JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" \
208
- " ON #{taggings_alias}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" \
209
- " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name, nil)}"
210
-
211
- joins << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
212
- joins << " AND " + sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
213
- joins << " AND " + sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
214
-
215
- group_columns = ActsAsTaggableOn::Utils.using_postgresql? ? grouped_column_names_for(self) : "#{table_name}.#{primary_key}"
216
- group = group_columns
217
- having = "COUNT(#{taggings_alias}.taggable_id) = #{tags.size}"
218
- end
219
-
220
- order_by << options[:order] if options[:order].present?
114
+ return none if tag_list.empty?
221
115
 
222
- query = self
223
- query = self.select(select_clause.join(',')) unless select_clause.empty?
224
- query.joins(joins.join(' '))
225
- .where(conditions.join(' AND '))
226
- .group(group)
227
- .having(having)
228
- .order(order_by.join(', '))
229
- .readonly(false)
116
+ ::ActsAsTaggableOn::Taggable::TaggedWithQuery.build(self, ActsAsTaggableOn::Tag, ActsAsTaggableOn::Tagging, tag_list, options)
230
117
  end
231
118
 
232
119
  def is_taggable?
233
120
  true
234
121
  end
235
122
 
236
- def adjust_taggings_alias(taggings_alias)
237
- if taggings_alias.size > 75
238
- taggings_alias = 'taggings_alias_' + Digest::SHA1.hexdigest(taggings_alias)
239
- end
240
- taggings_alias
241
- end
242
-
243
123
  def taggable_mixin
244
124
  @taggable_mixin ||= Module.new
245
125
  end
246
-
247
- private
248
-
249
- # Rails 5 has merged sanitize and quote_value
250
- # See https://github.com/rails/rails/blob/master/activerecord/lib/active_record/sanitization.rb#L10
251
- def quote_value(value, column = nil)
252
- ActsAsTaggableOn::Utils.active_record5? ? super(value) : super(value, column)
253
- end
254
126
  end
255
127
 
256
128
  # all column names are necessary for PostgreSQL group clause
@@ -283,7 +155,7 @@ module ActsAsTaggableOn::Taggable
283
155
  variable_name = "@#{context.to_s.singularize}_list"
284
156
  if instance_variable_get(variable_name)
285
157
  instance_variable_get(variable_name)
286
- elsif cached_tag_list_on(context) && self.class.caching_tag_list_on?(context)
158
+ elsif cached_tag_list_on(context) && ensure_included_cache_methods! && self.class.caching_tag_list_on?(context)
287
159
  instance_variable_set(variable_name, ActsAsTaggableOn.default_parser.new(cached_tag_list_on(context)).parse)
288
160
  else
289
161
  instance_variable_set(variable_name, ActsAsTaggableOn::TagList.new(tags_on(context).map(&:name)))
@@ -312,7 +184,7 @@ module ActsAsTaggableOn::Taggable
312
184
 
313
185
  if ActsAsTaggableOn::Utils.using_postgresql?
314
186
  group_columns = grouped_column_names_for(ActsAsTaggableOn::Tag)
315
- scope.order("max(#{tagging_table_name}.created_at)").group(group_columns)
187
+ scope.order(Arel.sql("max(#{tagging_table_name}.created_at)")).group(group_columns)
316
188
  else
317
189
  scope.group("#{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key}")
318
190
  end.to_a
@@ -332,33 +204,16 @@ module ActsAsTaggableOn::Taggable
332
204
  add_custom_context(context)
333
205
 
334
206
  variable_name = "@#{context.to_s.singularize}_list"
335
- process_dirty_object(context, new_list) unless custom_contexts.include?(context.to_s)
336
207
 
337
- instance_variable_set(variable_name, ActsAsTaggableOn.default_parser.new(new_list).parse)
208
+ parsed_new_list = ActsAsTaggableOn.default_parser.new(new_list).parse
209
+
210
+ instance_variable_set(variable_name, parsed_new_list)
338
211
  end
339
212
 
340
213
  def tagging_contexts
341
214
  self.class.tag_types.map(&:to_s) + custom_contexts
342
215
  end
343
216
 
344
- def process_dirty_object(context, new_list)
345
- value = new_list.is_a?(Array) ? ActsAsTaggableOn::TagList.new(new_list) : new_list
346
- attrib = "#{context.to_s.singularize}_list"
347
-
348
- if changed_attributes.include?(attrib)
349
- # The attribute already has an unsaved change.
350
- old = changed_attributes[attrib]
351
- @changed_attributes.delete(attrib) if old.to_s == value.to_s
352
- else
353
- old = tag_list_on(context)
354
- if self.class.preserve_tag_order
355
- @changed_attributes[attrib] = old if old.to_s != value.to_s
356
- else
357
- @changed_attributes[attrib] = old.to_s if old.sort != ActsAsTaggableOn.default_parser.new(value).parse.sort
358
- end
359
- end
360
- end
361
-
362
217
  def reload(*args)
363
218
  self.class.tag_types.each do |context|
364
219
  instance_variable_set("@#{context.to_s.singularize}_list", nil)
@@ -426,6 +281,10 @@ module ActsAsTaggableOn::Taggable
426
281
 
427
282
  private
428
283
 
284
+ def ensure_included_cache_methods!
285
+ self.class.columns
286
+ end
287
+
429
288
  # Filters the tag lists from the attribute names.
430
289
  def attributes_for_update(attribute_names)
431
290
  tag_lists = tag_types.map {|tags_type| "#{tags_type.to_s.singularize}_list"}
@@ -462,3 +321,4 @@ module ActsAsTaggableOn::Taggable
462
321
  end
463
322
  end
464
323
  end
324
+