acts-as-taggable-on 3.5.0 → 8.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (77) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/spec.yml +95 -0
  3. data/.gitignore +2 -0
  4. data/Appraisals +12 -9
  5. data/CHANGELOG.md +212 -70
  6. data/CONTRIBUTING.md +13 -0
  7. data/Gemfile +2 -1
  8. data/README.md +128 -25
  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 +15 -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/db/migrate/7_add_tenant_to_taggings.rb +16 -0
  17. data/gemfiles/activerecord_5.0.gemfile +21 -0
  18. data/gemfiles/activerecord_5.1.gemfile +21 -0
  19. data/gemfiles/activerecord_5.2.gemfile +21 -0
  20. data/gemfiles/activerecord_6.0.gemfile +21 -0
  21. data/gemfiles/activerecord_6.1.gemfile +23 -0
  22. data/lib/acts-as-taggable-on.rb +22 -14
  23. data/lib/acts_as_taggable_on/engine.rb +0 -1
  24. data/lib/acts_as_taggable_on/tag.rb +27 -23
  25. data/lib/acts_as_taggable_on/tag_list.rb +3 -13
  26. data/lib/acts_as_taggable_on/taggable/cache.rb +39 -35
  27. data/lib/acts_as_taggable_on/taggable/collection.rb +14 -9
  28. data/lib/acts_as_taggable_on/taggable/core.rb +59 -184
  29. data/lib/acts_as_taggable_on/taggable/ownership.rb +19 -8
  30. data/lib/acts_as_taggable_on/taggable/related.rb +1 -1
  31. data/lib/acts_as_taggable_on/taggable/tag_list_type.rb +4 -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/taggable/tagged_with_query.rb +16 -0
  37. data/lib/acts_as_taggable_on/taggable.rb +18 -1
  38. data/lib/acts_as_taggable_on/tagger.rb +12 -11
  39. data/lib/acts_as_taggable_on/tagging.rb +9 -14
  40. data/lib/acts_as_taggable_on/utils.rb +4 -5
  41. data/lib/acts_as_taggable_on/version.rb +1 -2
  42. data/spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb +14 -13
  43. data/spec/acts_as_taggable_on/acts_as_tagger_spec.rb +1 -1
  44. data/spec/acts_as_taggable_on/caching_spec.rb +55 -9
  45. data/spec/acts_as_taggable_on/{taggable/dirty_spec.rb → dirty_spec.rb} +28 -13
  46. data/spec/acts_as_taggable_on/single_table_inheritance_spec.rb +28 -8
  47. data/spec/acts_as_taggable_on/tag_list_spec.rb +27 -1
  48. data/spec/acts_as_taggable_on/tag_spec.rb +31 -1
  49. data/spec/acts_as_taggable_on/taggable_spec.rb +40 -19
  50. data/spec/acts_as_taggable_on/tagger_spec.rb +2 -2
  51. data/spec/acts_as_taggable_on/tagging_spec.rb +87 -7
  52. data/spec/internal/app/models/altered_inheriting_taggable_model.rb +2 -0
  53. data/spec/internal/app/models/cached_model_with_array.rb +6 -0
  54. data/spec/internal/app/models/columns_override_model.rb +5 -0
  55. data/spec/internal/app/models/company.rb +1 -1
  56. data/spec/internal/app/models/inheriting_taggable_model.rb +2 -0
  57. data/spec/internal/app/models/market.rb +1 -1
  58. data/spec/internal/app/models/non_standard_id_taggable_model.rb +1 -1
  59. data/spec/internal/app/models/student.rb +2 -0
  60. data/spec/internal/app/models/taggable_model.rb +3 -0
  61. data/spec/internal/app/models/user.rb +1 -1
  62. data/spec/internal/config/database.yml.sample +4 -8
  63. data/spec/internal/db/schema.rb +23 -7
  64. data/spec/spec_helper.rb +0 -1
  65. data/spec/support/database.rb +5 -11
  66. metadata +27 -75
  67. data/.travis.yml +0 -40
  68. data/UPGRADING.md +0 -8
  69. data/gemfiles/activerecord_3.2.gemfile +0 -15
  70. data/gemfiles/activerecord_4.0.gemfile +0 -15
  71. data/gemfiles/activerecord_4.1.gemfile +0 -15
  72. data/gemfiles/activerecord_4.2.gemfile +0 -16
  73. data/lib/acts_as_taggable_on/compatibility.rb +0 -35
  74. data/lib/acts_as_taggable_on/tag_list_parser.rb +0 -21
  75. data/lib/acts_as_taggable_on/taggable/dirty.rb +0 -36
  76. data/spec/acts_as_taggable_on/tag_list_parser_spec.rb +0 -46
  77. data/spec/internal/app/models/models.rb +0 -90
@@ -1,4 +1,3 @@
1
- require 'rails/engine'
2
1
  module ActsAsTaggableOn
3
2
  class Engine < Rails::Engine
4
3
  end
@@ -1,8 +1,7 @@
1
1
  # encoding: utf-8
2
2
  module ActsAsTaggableOn
3
3
  class Tag < ::ActiveRecord::Base
4
-
5
- attr_accessible :name if defined?(ActiveModel::MassAssignmentSecurity)
4
+ self.table_name = ActsAsTaggableOn.tags_table
6
5
 
7
6
  ### ASSOCIATIONS:
8
7
 
@@ -11,7 +10,7 @@ module ActsAsTaggableOn
11
10
  ### VALIDATIONS:
12
11
 
13
12
  validates_presence_of :name
14
- validates_uniqueness_of :name, if: :validates_name_uniqueness?
13
+ validates_uniqueness_of :name, if: :validates_name_uniqueness?, case_sensitive: true
15
14
  validates_length_of :name, maximum: 255
16
15
 
17
16
  # monkey patch this method if don't need name uniqueness validation
@@ -50,6 +49,18 @@ module ActsAsTaggableOn
50
49
  where(clause)
51
50
  end
52
51
 
52
+ def self.for_context(context)
53
+ joins(:taggings).
54
+ where(["#{ActsAsTaggableOn.taggings_table}.context = ?", context]).
55
+ select("DISTINCT #{ActsAsTaggableOn.tags_table}.*")
56
+ end
57
+
58
+ def self.for_tenant(tenant)
59
+ joins(:taggings).
60
+ where("#{ActsAsTaggableOn.taggings_table}.tenant = ?", tenant.to_s).
61
+ select("DISTINCT #{ActsAsTaggableOn.tags_table}.*")
62
+ end
63
+
53
64
  ### CLASS METHODS:
54
65
 
55
66
  def self.find_or_create_with_like_by_name(name)
@@ -66,16 +77,19 @@ module ActsAsTaggableOn
66
77
  return [] if list.empty?
67
78
 
68
79
  existing_tags = named_any(list)
69
-
70
80
  list.map do |tag_name|
71
- comparable_tag_name = comparable_name(tag_name)
72
- existing_tag = existing_tags.find { |tag| comparable_name(tag.name) == comparable_tag_name }
73
81
  begin
82
+ tries ||= 3
83
+ comparable_tag_name = comparable_name(tag_name)
84
+ existing_tag = existing_tags.find { |tag| comparable_name(tag.name) == comparable_tag_name }
74
85
  existing_tag || create(name: tag_name)
75
86
  rescue ActiveRecord::RecordNotUnique
76
- # Postgres aborts the current transaction with
77
- # PG::InFailedSqlTransaction: ERROR: current transaction is aborted, commands ignored until end of transaction block
78
- # so we have to rollback this transaction
87
+ if (tries -= 1).positive?
88
+ ActiveRecord::Base.connection.execute 'ROLLBACK'
89
+ existing_tags = named_any(list)
90
+ retry
91
+ end
92
+
79
93
  raise DuplicateTagError.new("'#{tag_name}' has already been taken")
80
94
  end
81
95
  end
@@ -97,8 +111,6 @@ module ActsAsTaggableOn
97
111
 
98
112
  class << self
99
113
 
100
-
101
-
102
114
  private
103
115
 
104
116
  def comparable_name(str)
@@ -113,20 +125,12 @@ module ActsAsTaggableOn
113
125
  ActsAsTaggableOn::Utils.using_mysql? ? 'BINARY ' : nil
114
126
  end
115
127
 
116
- def unicode_downcase(string)
117
- if ActiveSupport::Multibyte::Unicode.respond_to?(:downcase)
118
- ActiveSupport::Multibyte::Unicode.downcase(string)
119
- else
120
- ActiveSupport::Multibyte::Chars.new(string).downcase.to_s
121
- end
128
+ def as_8bit_ascii(string)
129
+ string.to_s.mb_chars
122
130
  end
123
131
 
124
- def as_8bit_ascii(string)
125
- if defined?(Encoding)
126
- string.to_s.dup.force_encoding('BINARY')
127
- else
128
- string.to_s.mb_chars
129
- end
132
+ def unicode_downcase(string)
133
+ as_8bit_ascii(string).downcase
130
134
  end
131
135
 
132
136
  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
  ##
@@ -84,7 +85,8 @@ module ActsAsTaggableOn
84
85
  map! { |tag| tag.mb_chars.downcase.to_s } if ActsAsTaggableOn.force_lowercase
85
86
  map!(&:parameterize) if ActsAsTaggableOn.force_parameterize
86
87
 
87
- uniq!
88
+ ActsAsTaggableOn.strict_case_match ? uniq! : uniq!{ |tag| tag.downcase }
89
+ self
88
90
  end
89
91
 
90
92
 
@@ -99,18 +101,6 @@ module ActsAsTaggableOn
99
101
  args.flatten!
100
102
  end
101
103
 
102
-
103
- ## DEPRECATED
104
- def self.from(string)
105
- ActiveRecord::Base.logger.warn <<WARNING
106
- ActsAsTaggableOn::TagList.from is deprecated \
107
- and will be removed from v4.0+, use \
108
- ActsAsTaggableOn::DefaultParser.new instead
109
- WARNING
110
- @parser.new(string).parse
111
- end
112
-
113
-
114
104
  end
115
105
  end
116
106
 
@@ -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
 
@@ -73,7 +77,7 @@ module ActsAsTaggableOn::Taggable
73
77
  tag_types.map(&:to_s).each do |tag_type|
74
78
  if self.class.send("caching_#{tag_type.singularize}_list?")
75
79
  if tag_list_cache_set_on(tag_type)
76
- list = tag_list_cache_on(tag_type).to_a.flatten.compact.join(', ')
80
+ list = tag_list_cache_on(tag_type).to_a.flatten.compact.join("#{ActsAsTaggableOn.delimiter} ")
77
81
  self["cached_#{tag_type.singularize}_list"] = list
78
82
  end
79
83
  end
@@ -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
 
@@ -138,7 +140,7 @@ module ActsAsTaggableOn::Taggable
138
140
  scoped_ids = pluck(table_name_pkey)
139
141
  tagging_scope = tagging_scope.where("#{ActsAsTaggableOn::Tagging.table_name}.taggable_id IN (?)", scoped_ids)
140
142
  else
141
- tagging_scope = tagging_scope.where("#{ActsAsTaggableOn::Tagging.table_name}.taggable_id IN(#{safe_to_sql(select(table_name_pkey))})")
143
+ tagging_scope = tagging_scope.where("#{ActsAsTaggableOn::Tagging.table_name}.taggable_id IN(#{safe_to_sql(except(:select).select(table_name_pkey))})")
142
144
  end
143
145
 
144
146
  tagging_scope
@@ -169,9 +171,12 @@ module ActsAsTaggableOn::Taggable
169
171
  end
170
172
 
171
173
  module CalculationMethods
172
- def count(column_name=:all, options = {})
174
+ # Rails 5 TODO: Remove options argument as soon we remove support to
175
+ # activerecord-deprecated_finders.
176
+ # See https://github.com/rails/rails/blob/master/activerecord/lib/active_record/relation/calculations.rb#L38
177
+ def count(column_name = :all, options = {})
173
178
  # https://github.com/rails/rails/commit/da9b5d4a8435b744fcf278fffd6d7f1e36d4a4f2
174
- super
179
+ super(column_name)
175
180
  end
176
181
  end
177
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
 
@@ -23,18 +27,19 @@ module ActsAsTaggableOn::Taggable
23
27
  class_eval do
24
28
  # when preserving tag order, include order option so that for a 'tags' context
25
29
  # the associations tag_taggings & tags are always returned in created order
26
- has_many_with_taggable_compatibility context_taggings, as: :taggable,
27
- dependent: :destroy,
28
- class_name: 'ActsAsTaggableOn::Tagging',
29
- order: taggings_order,
30
- conditions: {context: tags_type},
31
- include: :tag
32
-
33
- has_many_with_taggable_compatibility context_tags, through: context_taggings,
34
- source: :tag,
35
- class_name: 'ActsAsTaggableOn::Tag',
36
- order: taggings_order
37
-
30
+ has_many context_taggings, -> { includes(:tag).order(taggings_order).where(context: tags_type) },
31
+ as: :taggable,
32
+ class_name: 'ActsAsTaggableOn::Tagging',
33
+ dependent: :destroy,
34
+ after_add: :dirtify_tag_list,
35
+ after_remove: :dirtify_tag_list
36
+
37
+ has_many context_tags, -> { order(taggings_order) },
38
+ class_name: 'ActsAsTaggableOn::Tag',
39
+ through: context_taggings,
40
+ source: :tag
41
+
42
+ attribute "#{tags_type.singularize}_list".to_sym, ActsAsTaggableOn::Taggable::TagListType.new
38
43
  end
39
44
 
40
45
  taggable_mixin.class_eval <<-RUBY, __FILE__, __LINE__ + 1
@@ -43,12 +48,30 @@ module ActsAsTaggableOn::Taggable
43
48
  end
44
49
 
45
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
+
46
64
  set_tag_list_on('#{tags_type}', new_tags)
47
65
  end
48
66
 
49
67
  def all_#{tags_type}_list
50
68
  all_tags_list_on('#{tags_type}')
51
69
  end
70
+
71
+ private
72
+ def dirtify_tag_list(tagging)
73
+ attribute_will_change! tagging.context.singularize+"_list"
74
+ end
52
75
  RUBY
53
76
  end
54
77
  end
@@ -87,162 +110,16 @@ module ActsAsTaggableOn::Taggable
87
110
  def tagged_with(tags, options = {})
88
111
  tag_list = ActsAsTaggableOn.default_parser.new(tags).parse
89
112
  options = options.dup
90
- empty_result = where('1 = 0')
91
-
92
- return empty_result if tag_list.empty?
93
-
94
- joins = []
95
- conditions = []
96
- having = []
97
- select_clause = []
98
- order_by = []
99
-
100
- context = options.delete(:on)
101
- owned_by = options.delete(:owned_by)
102
- alias_base_name = undecorated_table_name.gsub('.', '_')
103
- # FIXME use ActiveRecord's connection quote_column_name
104
- quote = ActsAsTaggableOn::Utils.using_postgresql? ? '"' : ''
105
-
106
- if options.delete(:exclude)
107
- if options.delete(:wild)
108
- 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 ')
109
- else
110
- tags_conditions = tag_list.map { |t| sanitize_sql(["#{ActsAsTaggableOn::Tag.table_name}.name #{ActsAsTaggableOn::Utils.like_operator} ?", t]) }.join(' OR ')
111
- end
112
-
113
- 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)})"
114
-
115
- if owned_by
116
- joins << "JOIN #{ActsAsTaggableOn::Tagging.table_name}" +
117
- " ON #{ActsAsTaggableOn::Tagging.table_name}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" +
118
- " AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = #{quote_value(base_class.name, nil)}" +
119
- " AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_id = #{quote_value(owned_by.id, nil)}" +
120
- " AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_type = #{quote_value(owned_by.class.base_class.to_s, nil)}"
121
-
122
- joins << " AND " + sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
123
- joins << " AND " + sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
124
- end
125
-
126
- elsif any = options.delete(:any)
127
- # get tags, drop out if nothing returned (we need at least one)
128
- tags = if options.delete(:wild)
129
- ActsAsTaggableOn::Tag.named_like_any(tag_list)
130
- else
131
- ActsAsTaggableOn::Tag.named_any(tag_list)
132
- end
133
-
134
- return empty_result if tags.length == 0
135
-
136
- # setup taggings alias so we can chain, ex: items_locations_taggings_awesome_cool_123
137
- # avoid ambiguous column name
138
- taggings_context = context ? "_#{context}" : ''
139
-
140
- taggings_alias = adjust_taggings_alias(
141
- "#{alias_base_name[0..4]}#{taggings_context[0..6]}_taggings_#{ActsAsTaggableOn::Utils.sha_prefix(tags.map(&:name).join('_'))}"
142
- )
143
-
144
- tagging_cond = "#{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
145
- " WHERE #{taggings_alias}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" +
146
- " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name, nil)}"
147
-
148
- tagging_cond << " AND " + sanitize_sql(["#{taggings_alias}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
149
- tagging_cond << " AND " + sanitize_sql(["#{taggings_alias}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
150
-
151
- tagging_cond << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
152
-
153
- # don't need to sanitize sql, map all ids and join with OR logic
154
- tag_ids = tags.map { |t| quote_value(t.id, nil) }.join(', ')
155
- tagging_cond << " AND #{taggings_alias}.tag_id in (#{tag_ids})"
156
- select_clause << " #{table_name}.*" unless context and tag_types.one?
157
-
158
- if owned_by
159
- tagging_cond << ' AND ' +
160
- sanitize_sql([
161
- "#{taggings_alias}.tagger_id = ? AND #{taggings_alias}.tagger_type = ?",
162
- owned_by.id,
163
- owned_by.class.base_class.to_s
164
- ])
165
- end
166
-
167
- conditions << "EXISTS (SELECT 1 FROM #{tagging_cond})"
168
- if options.delete(:order_by_matching_tag_count)
169
- order_by << "(SELECT count(*) FROM #{tagging_cond}) desc"
170
- end
171
- else
172
- tags = ActsAsTaggableOn::Tag.named_any(tag_list)
173
-
174
- return empty_result unless tags.length == tag_list.length
175
-
176
- tags.each do |tag|
177
- taggings_alias = adjust_taggings_alias("#{alias_base_name[0..11]}_taggings_#{ActsAsTaggableOn::Utils.sha_prefix(tag.name)}")
178
- tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" \
179
- " ON #{taggings_alias}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" +
180
- " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name, nil)}" +
181
- " AND #{taggings_alias}.tag_id = #{quote_value(tag.id, nil)}"
182
113
 
183
- tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
184
- tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
114
+ return none if tag_list.empty?
185
115
 
186
- tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
187
-
188
- if owned_by
189
- tagging_join << ' AND ' +
190
- sanitize_sql([
191
- "#{taggings_alias}.tagger_id = ? AND #{taggings_alias}.tagger_type = ?",
192
- owned_by.id,
193
- owned_by.class.base_class.to_s
194
- ])
195
- end
196
-
197
- joins << tagging_join
198
- end
199
- end
200
-
201
- group ||= [] # Rails interprets this as a no-op in the group() call below
202
- if options.delete(:order_by_matching_tag_count)
203
- select_clause << "#{table_name}.*, COUNT(#{taggings_alias}.tag_id) AS #{taggings_alias}_count"
204
- group_columns = ActsAsTaggableOn::Utils.using_postgresql? ? grouped_column_names_for(self) : "#{table_name}.#{primary_key}"
205
- group = group_columns
206
- order_by << "#{taggings_alias}_count DESC"
207
-
208
- elsif options.delete(:match_all)
209
- taggings_alias, _ = adjust_taggings_alias("#{alias_base_name}_taggings_group"), "#{alias_base_name}_tags_group"
210
- joins << "LEFT OUTER JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" \
211
- " ON #{taggings_alias}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" \
212
- " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name, nil)}"
213
-
214
- joins << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
215
- joins << " AND " + sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
216
- joins << " AND " + sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
217
-
218
- group_columns = ActsAsTaggableOn::Utils.using_postgresql? ? grouped_column_names_for(self) : "#{table_name}.#{primary_key}"
219
- group = group_columns
220
- having = "COUNT(#{taggings_alias}.taggable_id) = #{tags.size}"
221
- end
222
-
223
- order_by << options[:order] if options[:order].present?
224
-
225
- query = self
226
- query = self.select(select_clause.join(',')) unless select_clause.empty?
227
- query.joins(joins.join(' '))
228
- .where(conditions.join(' AND '))
229
- .group(group)
230
- .having(having)
231
- .order(order_by.join(', '))
232
- .readonly(false)
116
+ ::ActsAsTaggableOn::Taggable::TaggedWithQuery.build(self, ActsAsTaggableOn::Tag, ActsAsTaggableOn::Tagging, tag_list, options)
233
117
  end
234
118
 
235
119
  def is_taggable?
236
120
  true
237
121
  end
238
122
 
239
- def adjust_taggings_alias(taggings_alias)
240
- if taggings_alias.size > 75
241
- taggings_alias = 'taggings_alias_' + Digest::SHA1.hexdigest(taggings_alias)
242
- end
243
- taggings_alias
244
- end
245
-
246
123
  def taggable_mixin
247
124
  @taggable_mixin ||= Module.new
248
125
  end
@@ -254,7 +131,7 @@ module ActsAsTaggableOn::Taggable
254
131
  end
255
132
 
256
133
  def custom_contexts
257
- @custom_contexts ||= []
134
+ @custom_contexts ||= taggings.map(&:context).uniq
258
135
  end
259
136
 
260
137
  def is_taggable?
@@ -278,7 +155,7 @@ module ActsAsTaggableOn::Taggable
278
155
  variable_name = "@#{context.to_s.singularize}_list"
279
156
  if instance_variable_get(variable_name)
280
157
  instance_variable_get(variable_name)
281
- 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)
282
159
  instance_variable_set(variable_name, ActsAsTaggableOn.default_parser.new(cached_tag_list_on(context)).parse)
283
160
  else
284
161
  instance_variable_set(variable_name, ActsAsTaggableOn::TagList.new(tags_on(context).map(&:name)))
@@ -307,7 +184,7 @@ module ActsAsTaggableOn::Taggable
307
184
 
308
185
  if ActsAsTaggableOn::Utils.using_postgresql?
309
186
  group_columns = grouped_column_names_for(ActsAsTaggableOn::Tag)
310
- 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)
311
188
  else
312
189
  scope.group("#{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key}")
313
190
  end.to_a
@@ -327,30 +204,19 @@ module ActsAsTaggableOn::Taggable
327
204
  add_custom_context(context)
328
205
 
329
206
  variable_name = "@#{context.to_s.singularize}_list"
330
- process_dirty_object(context, new_list) unless custom_contexts.include?(context.to_s)
331
207
 
332
- 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)
333
211
  end
334
212
 
335
213
  def tagging_contexts
336
- custom_contexts + self.class.tag_types.map(&:to_s)
214
+ self.class.tag_types.map(&:to_s) + custom_contexts
337
215
  end
338
216
 
339
- def process_dirty_object(context, new_list)
340
- value = new_list.is_a?(Array) ? ActsAsTaggableOn::TagList.new(new_list) : new_list
341
- attrib = "#{context.to_s.singularize}_list"
342
-
343
- if changed_attributes.include?(attrib)
344
- # The attribute already has an unsaved change.
345
- old = changed_attributes[attrib]
346
- @changed_attributes.delete(attrib) if old.to_s == value.to_s
347
- else
348
- old = tag_list_on(context)
349
- if self.class.preserve_tag_order
350
- @changed_attributes[attrib] = old if old.to_s != value.to_s
351
- else
352
- @changed_attributes[attrib] = old.to_s if old.sort != ActsAsTaggableOn.default_parser.new(value).parse.sort
353
- end
217
+ def taggable_tenant
218
+ if self.class.tenant_column
219
+ public_send(self.class.tenant_column)
354
220
  end
355
221
  end
356
222
 
@@ -407,12 +273,16 @@ module ActsAsTaggableOn::Taggable
407
273
 
408
274
  # Destroy old taggings:
409
275
  if old_tags.present?
410
- taggings.not_owned.by_context(context).destroy_all(tag_id: old_tags)
276
+ taggings.not_owned.by_context(context).where(tag_id: old_tags).destroy_all
411
277
  end
412
278
 
413
279
  # Create new taggings:
414
280
  new_tags.each do |tag|
415
- taggings.create!(tag_id: tag.id, context: context.to_s, taggable: self)
281
+ if taggable_tenant
282
+ taggings.create!(tag_id: tag.id, context: context.to_s, taggable: self, tenant: taggable_tenant)
283
+ else
284
+ taggings.create!(tag_id: tag.id, context: context.to_s, taggable: self)
285
+ end
416
286
  end
417
287
  end
418
288
 
@@ -421,6 +291,10 @@ module ActsAsTaggableOn::Taggable
421
291
 
422
292
  private
423
293
 
294
+ def ensure_included_cache_methods!
295
+ self.class.columns
296
+ end
297
+
424
298
  # Filters the tag lists from the attribute names.
425
299
  def attributes_for_update(attribute_names)
426
300
  tag_lists = tag_types.map {|tags_type| "#{tags_type.to_s.singularize}_list"}
@@ -432,7 +306,7 @@ module ActsAsTaggableOn::Taggable
432
306
  tag_lists = tag_types.map {|tags_type| "#{tags_type.to_s.singularize}_list"}
433
307
  super.delete_if {|attr| tag_lists.include? attr }
434
308
  end
435
-
309
+
436
310
  ##
437
311
  # Override this hook if you wish to subclass {ActsAsTaggableOn::Tag} --
438
312
  # context is provided so that you may conditionally use a Tag subclass
@@ -457,3 +331,4 @@ module ActsAsTaggableOn::Taggable
457
331
  end
458
332
  end
459
333
  end
334
+