acts-as-taggable-on 6.5.0 → 9.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/spec.yml +76 -0
  3. data/Appraisals +13 -9
  4. data/CHANGELOG.md +35 -5
  5. data/Gemfile +1 -0
  6. data/README.md +34 -9
  7. data/acts-as-taggable-on.gemspec +2 -2
  8. data/db/migrate/1_acts_as_taggable_on_migration.rb +5 -7
  9. data/db/migrate/2_add_missing_unique_indices.rb +6 -8
  10. data/db/migrate/3_add_taggings_counter_cache_to_tags.rb +3 -6
  11. data/db/migrate/4_add_missing_taggable_index.rb +5 -7
  12. data/db/migrate/5_change_collation_for_tag_names.rb +4 -6
  13. data/db/migrate/6_add_missing_indexes_on_taggings.rb +15 -13
  14. data/db/migrate/7_add_tenant_to_taggings.rb +13 -0
  15. data/docker-compose.yml +15 -0
  16. data/gemfiles/activerecord_6.0.gemfile +5 -8
  17. data/gemfiles/{activerecord_5.2.gemfile → activerecord_6.1.gemfile} +6 -9
  18. data/gemfiles/{activerecord_5.0.gemfile → activerecord_7.0.gemfile} +6 -9
  19. data/lib/acts-as-taggable-on.rb +1 -1
  20. data/lib/acts_as_taggable_on/default_parser.rb +8 -10
  21. data/lib/acts_as_taggable_on/engine.rb +2 -0
  22. data/lib/acts_as_taggable_on/generic_parser.rb +2 -0
  23. data/lib/acts_as_taggable_on/tag.rb +34 -28
  24. data/lib/acts_as_taggable_on/tag_list.rb +8 -11
  25. data/lib/acts_as_taggable_on/taggable/cache.rb +64 -62
  26. data/lib/acts_as_taggable_on/taggable/collection.rb +178 -142
  27. data/lib/acts_as_taggable_on/taggable/core.rb +250 -236
  28. data/lib/acts_as_taggable_on/taggable/ownership.rb +110 -98
  29. data/lib/acts_as_taggable_on/taggable/related.rb +60 -47
  30. data/lib/acts_as_taggable_on/taggable/tag_list_type.rb +6 -2
  31. data/lib/acts_as_taggable_on/taggable/tagged_with_query/all_tags_query.rb +110 -106
  32. data/lib/acts_as_taggable_on/taggable/tagged_with_query/any_tags_query.rb +57 -53
  33. data/lib/acts_as_taggable_on/taggable/tagged_with_query/exclude_tags_query.rb +63 -60
  34. data/lib/acts_as_taggable_on/taggable/tagged_with_query/query_base.rb +54 -46
  35. data/lib/acts_as_taggable_on/taggable/tagged_with_query.rb +14 -8
  36. data/lib/acts_as_taggable_on/taggable.rb +30 -12
  37. data/lib/acts_as_taggable_on/tagger.rb +10 -6
  38. data/lib/acts_as_taggable_on/tagging.rb +9 -5
  39. data/lib/acts_as_taggable_on/tags_helper.rb +3 -1
  40. data/lib/acts_as_taggable_on/utils.rb +4 -2
  41. data/lib/acts_as_taggable_on/version.rb +3 -1
  42. data/spec/acts_as_taggable_on/tag_spec.rb +16 -1
  43. data/spec/acts_as_taggable_on/taggable_spec.rb +7 -3
  44. data/spec/acts_as_taggable_on/tagging_spec.rb +26 -0
  45. data/spec/internal/app/models/taggable_model.rb +2 -0
  46. data/spec/internal/config/database.yml.sample +4 -8
  47. data/spec/internal/db/schema.rb +3 -0
  48. data/spec/support/database.rb +36 -26
  49. metadata +16 -24
  50. data/.travis.yml +0 -43
  51. data/UPGRADING.md +0 -8
  52. data/gemfiles/activerecord_5.1.gemfile +0 -21
@@ -1,14 +1,17 @@
1
- module ActsAsTaggableOn::Taggable
2
- module Collection
3
- def self.included(base)
4
- base.extend ActsAsTaggableOn::Taggable::Collection::ClassMethods
5
- base.initialize_acts_as_taggable_on_collection
6
- end
1
+ # frozen_string_literal: true
2
+
3
+ module ActsAsTaggableOn
4
+ module Taggable
5
+ module Collection
6
+ def self.included(base)
7
+ base.extend ActsAsTaggableOn::Taggable::Collection::ClassMethods
8
+ base.initialize_acts_as_taggable_on_collection
9
+ end
7
10
 
8
- module ClassMethods
9
- def initialize_acts_as_taggable_on_collection
10
- tag_types.map(&:to_s).each do |tag_type|
11
- class_eval <<-RUBY, __FILE__, __LINE__ + 1
11
+ module ClassMethods
12
+ def initialize_acts_as_taggable_on_collection
13
+ tag_types.map(&:to_s).each do |tag_type|
14
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
12
15
  def self.#{tag_type.singularize}_counts(options={})
13
16
  tag_counts_on('#{tag_type}', options)
14
17
  end
@@ -24,159 +27,192 @@ module ActsAsTaggableOn::Taggable
24
27
  def self.top_#{tag_type}(limit = 10)
25
28
  tag_counts_on('#{tag_type}', order: 'count desc', limit: limit.to_i)
26
29
  end
27
- RUBY
30
+ RUBY
31
+ end
28
32
  end
29
- end
30
-
31
- def acts_as_taggable_on(*args)
32
- super(*args)
33
- initialize_acts_as_taggable_on_collection
34
- end
35
-
36
- def tag_counts_on(context, options = {})
37
- all_tag_counts(options.merge({on: context.to_s}))
38
- end
39
33
 
40
- def tags_on(context, options = {})
41
- all_tags(options.merge({on: context.to_s}))
42
- end
43
-
44
- ##
45
- # Calculate the tag names.
46
- # To be used when you don't need tag counts and want to avoid the taggable joins.
47
- #
48
- # @param [Hash] options Options:
49
- # * :start_at - Restrict the tags to those created after a certain time
50
- # * :end_at - Restrict the tags to those created before a certain time
51
- # * :conditions - A piece of SQL conditions to add to the query. Note we don't join the taggable objects for performance reasons.
52
- # * :limit - The maximum number of tags to return
53
- # * :order - A piece of SQL to order by. Eg 'tags.count desc' or 'taggings.created_at desc'
54
- # * :on - Scope the find to only include a certain context
55
- def all_tags(options = {})
56
- options = options.dup
57
- options.assert_valid_keys :start_at, :end_at, :conditions, :order, :limit, :on
58
-
59
- ## Generate conditions:
60
- options[:conditions] = sanitize_sql(options[:conditions]) if options[:conditions]
61
-
62
- ## Generate scope:
63
- tagging_scope = ActsAsTaggableOn::Tagging.select("#{ActsAsTaggableOn::Tagging.table_name}.tag_id")
64
- tag_scope = ActsAsTaggableOn::Tag.select("#{ActsAsTaggableOn::Tag.table_name}.*").order(options[:order]).limit(options[:limit])
65
-
66
- # Joins and conditions
67
- tagging_conditions(options).each { |condition| tagging_scope = tagging_scope.where(condition) }
68
- tag_scope = tag_scope.where(options[:conditions])
69
-
70
- group_columns = "#{ActsAsTaggableOn::Tagging.table_name}.tag_id"
71
-
72
- # Append the current scope to the scope, because we can't use scope(:find) in RoR 3.0 anymore:
73
- tagging_scope = generate_tagging_scope_in_clause(tagging_scope, table_name, primary_key).group(group_columns)
74
-
75
- tag_scope_joins(tag_scope, tagging_scope)
76
- end
77
-
78
- ##
79
- # Calculate the tag counts for all tags.
80
- #
81
- # @param [Hash] options Options:
82
- # * :start_at - Restrict the tags to those created after a certain time
83
- # * :end_at - Restrict the tags to those created before a certain time
84
- # * :conditions - A piece of SQL conditions to add to the query
85
- # * :limit - The maximum number of tags to return
86
- # * :order - A piece of SQL to order by. Eg 'tags.count desc' or 'taggings.created_at desc'
87
- # * :at_least - Exclude tags with a frequency less than the given value
88
- # * :at_most - Exclude tags with a frequency greater than the given value
89
- # * :on - Scope the find to only include a certain context
90
- def all_tag_counts(options = {})
91
- options = options.dup
92
- options.assert_valid_keys :start_at, :end_at, :conditions, :at_least, :at_most, :order, :limit, :on, :id
93
-
94
- ## Generate conditions:
95
- options[:conditions] = sanitize_sql(options[:conditions]) if options[:conditions]
96
-
97
- ## Generate scope:
98
- tagging_scope = ActsAsTaggableOn::Tagging.select("#{ActsAsTaggableOn::Tagging.table_name}.tag_id, COUNT(#{ActsAsTaggableOn::Tagging.table_name}.tag_id) AS tags_count")
99
- tag_scope = ActsAsTaggableOn::Tag.select("#{ActsAsTaggableOn::Tag.table_name}.*, #{ActsAsTaggableOn::Tagging.table_name}.tags_count AS count").order(options[:order]).limit(options[:limit])
100
-
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)
34
+ def acts_as_taggable_on(*args)
35
+ super(*args)
36
+ initialize_acts_as_taggable_on_collection
106
37
  end
107
38
 
108
- # Conditions
109
- tagging_conditions(options).each { |condition| tagging_scope = tagging_scope.where(condition) }
110
- tag_scope = tag_scope.where(options[:conditions])
111
-
112
- # GROUP BY and HAVING clauses:
113
- having = ["COUNT(#{ActsAsTaggableOn::Tagging.table_name}.tag_id) > 0"]
114
- having.push sanitize_sql(["COUNT(#{ActsAsTaggableOn::Tagging.table_name}.tag_id) >= ?", options.delete(:at_least)]) if options[:at_least]
115
- having.push sanitize_sql(["COUNT(#{ActsAsTaggableOn::Tagging.table_name}.tag_id) <= ?", options.delete(:at_most)]) if options[:at_most]
116
- having = having.compact.join(' AND ')
117
-
118
- group_columns = "#{ActsAsTaggableOn::Tagging.table_name}.tag_id"
39
+ def tag_counts_on(context, options = {})
40
+ all_tag_counts(options.merge({ on: context.to_s }))
41
+ end
119
42
 
120
- unless options[:id]
121
- # Append the current scope to the scope, because we can't use scope(:find) in RoR 3.0 anymore:
122
- tagging_scope = generate_tagging_scope_in_clause(tagging_scope, table_name, primary_key)
43
+ def tags_on(context, options = {})
44
+ all_tags(options.merge({ on: context.to_s }))
123
45
  end
124
46
 
125
- tagging_scope = tagging_scope.group(group_columns).having(having)
47
+ ##
48
+ # Calculate the tag names.
49
+ # To be used when you don't need tag counts and want to avoid the taggable joins.
50
+ #
51
+ # @param [Hash] options Options:
52
+ # * :start_at - Restrict the tags to those created after a certain time
53
+ # * :end_at - Restrict the tags to those created before a certain time
54
+ # * :conditions - A piece of SQL conditions to add to the query. Note we don't join the taggable objects for performance reasons.
55
+ # * :limit - The maximum number of tags to return
56
+ # * :order - A piece of SQL to order by. Eg 'tags.count desc' or 'taggings.created_at desc'
57
+ # * :on - Scope the find to only include a certain context
58
+ def all_tags(options = {})
59
+ options = options.dup
60
+ options.assert_valid_keys :start_at, :end_at, :conditions, :order, :limit, :on
61
+
62
+ ## Generate conditions:
63
+ options[:conditions] = sanitize_sql(options[:conditions]) if options[:conditions]
64
+
65
+ ## Generate scope:
66
+ tagging_scope = ActsAsTaggableOn::Tagging.select("#{ActsAsTaggableOn::Tagging.table_name}.tag_id")
67
+ tag_scope = ActsAsTaggableOn::Tag.select("#{ActsAsTaggableOn::Tag.table_name}.*").order(options[:order]).limit(options[:limit])
68
+
69
+ # Joins and conditions
70
+ tagging_conditions(options).each { |condition| tagging_scope = tagging_scope.where(condition) }
71
+ tag_scope = tag_scope.where(options[:conditions])
72
+
73
+ group_columns = "#{ActsAsTaggableOn::Tagging.table_name}.tag_id"
126
74
 
127
- tag_scope_joins(tag_scope, tagging_scope)
128
- end
75
+ # Append the current scope to the scope, because we can't use scope(:find) in RoR 3.0 anymore:
76
+ tagging_scope = generate_tagging_scope_in_clause(tagging_scope, table_name, primary_key).group(group_columns)
129
77
 
130
- def safe_to_sql(relation)
131
- connection.respond_to?(:unprepared_statement) ? connection.unprepared_statement { relation.to_sql } : relation.to_sql
132
- end
78
+ tag_scope_joins(tag_scope, tagging_scope)
79
+ end
133
80
 
134
- private
81
+ ##
82
+ # Calculate the tag counts for all tags.
83
+ #
84
+ # @param [Hash] options Options:
85
+ # * :start_at - Restrict the tags to those created after a certain time
86
+ # * :end_at - Restrict the tags to those created before a certain time
87
+ # * :conditions - A piece of SQL conditions to add to the query
88
+ # * :limit - The maximum number of tags to return
89
+ # * :order - A piece of SQL to order by. Eg 'tags.count desc' or 'taggings.created_at desc'
90
+ # * :at_least - Exclude tags with a frequency less than the given value
91
+ # * :at_most - Exclude tags with a frequency greater than the given value
92
+ # * :on - Scope the find to only include a certain context
93
+ def all_tag_counts(options = {})
94
+ options = options.dup
95
+ options.assert_valid_keys :start_at, :end_at, :conditions, :at_least, :at_most, :order, :limit, :on, :id
96
+
97
+ ## Generate conditions:
98
+ options[:conditions] = sanitize_sql(options[:conditions]) if options[:conditions]
99
+
100
+ ## Generate scope:
101
+ tagging_scope = ActsAsTaggableOn::Tagging.select("#{ActsAsTaggableOn::Tagging.table_name}.tag_id, COUNT(#{ActsAsTaggableOn::Tagging.table_name}.tag_id) AS tags_count")
102
+ tag_scope = ActsAsTaggableOn::Tag.select("#{ActsAsTaggableOn::Tag.table_name}.*, #{ActsAsTaggableOn::Tagging.table_name}.tags_count AS count").order(options[:order]).limit(options[:limit])
103
+
104
+ # Current model is STI descendant, so add type checking to the join condition
105
+ unless descends_from_active_record?
106
+ taggable_join = "INNER JOIN #{table_name} ON #{table_name}.#{primary_key} = #{ActsAsTaggableOn::Tagging.table_name}.taggable_id"
107
+ taggable_join = taggable_join + " AND #{table_name}.#{inheritance_column} = '#{name}'"
108
+ tagging_scope = tagging_scope.joins(taggable_join)
109
+ end
110
+
111
+ # Conditions
112
+ tagging_conditions(options).each { |condition| tagging_scope = tagging_scope.where(condition) }
113
+ tag_scope = tag_scope.where(options[:conditions])
114
+
115
+ # GROUP BY and HAVING clauses:
116
+ having = ["COUNT(#{ActsAsTaggableOn::Tagging.table_name}.tag_id) > 0"]
117
+ if options[:at_least]
118
+ having.push sanitize_sql(["COUNT(#{ActsAsTaggableOn::Tagging.table_name}.tag_id) >= ?",
119
+ options.delete(:at_least)])
120
+ end
121
+ if options[:at_most]
122
+ having.push sanitize_sql(["COUNT(#{ActsAsTaggableOn::Tagging.table_name}.tag_id) <= ?",
123
+ options.delete(:at_most)])
124
+ end
125
+ having = having.compact.join(' AND ')
126
+
127
+ group_columns = "#{ActsAsTaggableOn::Tagging.table_name}.tag_id"
128
+
129
+ unless options[:id]
130
+ # Append the current scope to the scope, because we can't use scope(:find) in RoR 3.0 anymore:
131
+ tagging_scope = generate_tagging_scope_in_clause(tagging_scope, table_name, primary_key)
132
+ end
133
+
134
+ tagging_scope = tagging_scope.group(group_columns).having(having)
135
+
136
+ tag_scope_joins(tag_scope, tagging_scope)
137
+ end
135
138
 
136
- def generate_tagging_scope_in_clause(tagging_scope, table_name, primary_key)
137
- table_name_pkey = "#{table_name}.#{primary_key}"
138
- if ActsAsTaggableOn::Utils.using_mysql?
139
- # See https://github.com/mbleigh/acts-as-taggable-on/pull/457 for details
140
- scoped_ids = pluck(table_name_pkey)
141
- tagging_scope = tagging_scope.where("#{ActsAsTaggableOn::Tagging.table_name}.taggable_id IN (?)", scoped_ids)
142
- else
143
- tagging_scope = tagging_scope.where("#{ActsAsTaggableOn::Tagging.table_name}.taggable_id IN(#{safe_to_sql(except(:select).select(table_name_pkey))})")
139
+ def safe_to_sql(relation)
140
+ if connection.respond_to?(:unprepared_statement)
141
+ connection.unprepared_statement do
142
+ relation.to_sql
143
+ end
144
+ else
145
+ relation.to_sql
146
+ end
144
147
  end
145
148
 
146
- tagging_scope
147
- end
149
+ private
148
150
 
149
- def tagging_conditions(options)
150
- tagging_conditions = []
151
- tagging_conditions.push sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
152
- tagging_conditions.push sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
151
+ def generate_tagging_scope_in_clause(tagging_scope, table_name, primary_key)
152
+ table_name_pkey = "#{table_name}.#{primary_key}"
153
+ if ActsAsTaggableOn::Utils.using_mysql?
154
+ # See https://github.com/mbleigh/acts-as-taggable-on/pull/457 for details
155
+ scoped_ids = pluck(table_name_pkey)
156
+ tagging_scope = tagging_scope.where("#{ActsAsTaggableOn::Tagging.table_name}.taggable_id IN (?)",
157
+ scoped_ids)
158
+ else
159
+ tagging_scope = tagging_scope.where("#{ActsAsTaggableOn::Tagging.table_name}.taggable_id IN(#{safe_to_sql(except(:select).select(table_name_pkey))})")
160
+ end
153
161
 
154
- taggable_conditions = sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.taggable_type = ?", base_class.name])
155
- taggable_conditions << sanitize_sql([" AND #{ActsAsTaggableOn::Tagging.table_name}.context = ?", options.delete(:on).to_s]) if options[:on]
156
- taggable_conditions << sanitize_sql([" AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_id = ?", options[:id]]) if options[:id]
162
+ tagging_scope
163
+ end
157
164
 
158
- tagging_conditions.push taggable_conditions
165
+ def tagging_conditions(options)
166
+ tagging_conditions = []
167
+ if options[:end_at]
168
+ tagging_conditions.push sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at <= ?",
169
+ options.delete(:end_at)])
170
+ end
171
+ if options[:start_at]
172
+ tagging_conditions.push sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at >= ?",
173
+ options.delete(:start_at)])
174
+ end
175
+
176
+ taggable_conditions = sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.taggable_type = ?",
177
+ base_class.name])
178
+ if options[:on]
179
+ taggable_conditions << sanitize_sql([" AND #{ActsAsTaggableOn::Tagging.table_name}.context = ?",
180
+ options.delete(:on).to_s])
181
+ end
182
+
183
+ if options[:id]
184
+ taggable_conditions << if options[:id].is_a? Array
185
+ sanitize_sql([" AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_id IN (?)",
186
+ options[:id]])
187
+ else
188
+ sanitize_sql([" AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_id = ?",
189
+ options[:id]])
190
+ end
191
+ end
192
+
193
+ tagging_conditions.push taggable_conditions
194
+
195
+ tagging_conditions
196
+ end
159
197
 
160
- tagging_conditions
198
+ def tag_scope_joins(tag_scope, tagging_scope)
199
+ 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")
200
+ tag_scope.extending(CalculationMethods)
201
+ end
161
202
  end
162
203
 
163
- def tag_scope_joins(tag_scope, tagging_scope)
164
- 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")
165
- tag_scope.extending(CalculationMethods)
204
+ def tag_counts_on(context, options = {})
205
+ self.class.tag_counts_on(context, options.merge(id: id))
166
206
  end
167
- end
168
207
 
169
- def tag_counts_on(context, options={})
170
- self.class.tag_counts_on(context, options.merge(id: id))
171
- end
172
-
173
- module CalculationMethods
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 = {})
178
- # https://github.com/rails/rails/commit/da9b5d4a8435b744fcf278fffd6d7f1e36d4a4f2
179
- super(column_name)
208
+ module CalculationMethods
209
+ # Rails 5 TODO: Remove options argument as soon we remove support to
210
+ # activerecord-deprecated_finders.
211
+ # See https://github.com/rails/rails/blob/master/activerecord/lib/active_record/relation/calculations.rb#L38
212
+ def count(column_name = :all, _options = {})
213
+ # https://github.com/rails/rails/commit/da9b5d4a8435b744fcf278fffd6d7f1e36d4a4f2
214
+ super(column_name)
215
+ end
180
216
  end
181
217
  end
182
218
  end