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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+