acts-as-taggable-on 7.0.0 → 9.0.1

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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/spec.yml +76 -0
  3. data/Appraisals +13 -13
  4. data/CHANGELOG.md +27 -2
  5. data/Gemfile +1 -0
  6. data/README.md +32 -7
  7. data/acts-as-taggable-on.gemspec +2 -2
  8. data/db/migrate/1_acts_as_taggable_on_migration.rb +5 -8
  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_6.1.gemfile +3 -8
  18. data/gemfiles/{activerecord_5.2.gemfile → activerecord_7.0.gemfile} +6 -9
  19. data/lib/acts_as_taggable_on/default_parser.rb +8 -10
  20. data/lib/acts_as_taggable_on/engine.rb +2 -0
  21. data/lib/acts_as_taggable_on/generic_parser.rb +2 -0
  22. data/lib/acts_as_taggable_on/tag.rb +33 -27
  23. data/lib/acts_as_taggable_on/tag_list.rb +8 -11
  24. data/lib/acts_as_taggable_on/taggable/cache.rb +64 -62
  25. data/lib/acts_as_taggable_on/taggable/collection.rb +178 -142
  26. data/lib/acts_as_taggable_on/taggable/core.rb +250 -236
  27. data/lib/acts_as_taggable_on/taggable/ownership.rb +110 -98
  28. data/lib/acts_as_taggable_on/taggable/related.rb +60 -47
  29. data/lib/acts_as_taggable_on/taggable/tag_list_type.rb +6 -2
  30. data/lib/acts_as_taggable_on/taggable/tagged_with_query/all_tags_query.rb +110 -106
  31. data/lib/acts_as_taggable_on/taggable/tagged_with_query/any_tags_query.rb +57 -53
  32. data/lib/acts_as_taggable_on/taggable/tagged_with_query/exclude_tags_query.rb +63 -60
  33. data/lib/acts_as_taggable_on/taggable/tagged_with_query/query_base.rb +54 -46
  34. data/lib/acts_as_taggable_on/taggable/tagged_with_query.rb +14 -8
  35. data/lib/acts_as_taggable_on/taggable.rb +30 -12
  36. data/lib/acts_as_taggable_on/tagger.rb +9 -5
  37. data/lib/acts_as_taggable_on/tagging.rb +8 -4
  38. data/lib/acts_as_taggable_on/tags_helper.rb +3 -1
  39. data/lib/acts_as_taggable_on/utils.rb +4 -2
  40. data/lib/acts_as_taggable_on/version.rb +3 -1
  41. data/spec/acts_as_taggable_on/tag_spec.rb +16 -1
  42. data/spec/acts_as_taggable_on/taggable_spec.rb +6 -2
  43. data/spec/acts_as_taggable_on/tagging_spec.rb +26 -0
  44. data/spec/internal/app/models/taggable_model.rb +2 -0
  45. data/spec/internal/config/database.yml.sample +4 -8
  46. data/spec/internal/db/schema.rb +3 -0
  47. data/spec/support/database.rb +36 -26
  48. metadata +13 -22
  49. data/.travis.yml +0 -49
  50. data/UPGRADING.md +0 -8
  51. data/gemfiles/activerecord_5.0.gemfile +0 -21
  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