acts-as-taggable-on 2.3.3 → 2.4.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 (44) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +3 -3
  3. data/Appraisals +7 -0
  4. data/Gemfile +3 -1
  5. data/{MIT-LICENSE → LICENSE.md} +1 -1
  6. data/README.md +303 -0
  7. data/Rakefile +2 -2
  8. data/acts-as-taggable-on.gemspec +25 -18
  9. data/gemfiles/rails_3.gemfile +8 -0
  10. data/gemfiles/rails_4.gemfile +8 -0
  11. data/lib/acts-as-taggable-on.rb +5 -0
  12. data/lib/acts_as_taggable_on/acts_as_taggable_on/cache.rb +2 -2
  13. data/lib/acts_as_taggable_on/acts_as_taggable_on/collection.rb +64 -7
  14. data/lib/acts_as_taggable_on/acts_as_taggable_on/compatibility.rb +34 -0
  15. data/lib/acts_as_taggable_on/acts_as_taggable_on/core.rb +86 -55
  16. data/lib/acts_as_taggable_on/acts_as_taggable_on/dirty.rb +3 -3
  17. data/lib/acts_as_taggable_on/acts_as_taggable_on/ownership.rb +24 -15
  18. data/lib/acts_as_taggable_on/acts_as_taggable_on/related.rb +32 -21
  19. data/lib/acts_as_taggable_on/tag.rb +37 -15
  20. data/lib/acts_as_taggable_on/tag_list.rb +2 -2
  21. data/lib/acts_as_taggable_on/taggable.rb +10 -7
  22. data/lib/acts_as_taggable_on/tagger.rb +12 -3
  23. data/lib/acts_as_taggable_on/tagging.rb +2 -2
  24. data/lib/acts_as_taggable_on/tags_helper.rb +0 -2
  25. data/lib/{acts-as-taggable-on → acts_as_taggable_on}/version.rb +1 -1
  26. data/spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb +1 -183
  27. data/spec/acts_as_taggable_on/acts_as_tagger_spec.rb +8 -8
  28. data/spec/acts_as_taggable_on/related_spec.rb +143 -0
  29. data/spec/acts_as_taggable_on/single_table_inheritance_spec.rb +187 -0
  30. data/spec/acts_as_taggable_on/tag_list_spec.rb +4 -4
  31. data/spec/acts_as_taggable_on/tag_spec.rb +61 -3
  32. data/spec/acts_as_taggable_on/taggable_spec.rb +178 -98
  33. data/spec/acts_as_taggable_on/tagger_spec.rb +32 -33
  34. data/spec/acts_as_taggable_on/tagging_spec.rb +1 -1
  35. data/spec/acts_as_taggable_on/tags_helper_spec.rb +2 -2
  36. data/spec/acts_as_taggable_on/utils_spec.rb +2 -2
  37. data/spec/models.rb +8 -2
  38. data/spec/schema.rb +1 -1
  39. data/spec/spec_helper.rb +7 -4
  40. metadata +101 -56
  41. data/CHANGELOG +0 -35
  42. data/README.rdoc +0 -244
  43. data/rails/init.rb +0 -1
  44. data/uninstall.rb +0 -1
@@ -13,30 +13,33 @@ module ActsAsTaggableOn::Taggable
13
13
  end
14
14
 
15
15
  module ClassMethods
16
+
16
17
  def initialize_acts_as_taggable_on_core
18
+ include taggable_mixin
17
19
  tag_types.map(&:to_s).each do |tags_type|
18
20
  tag_type = tags_type.to_s.singularize
19
21
  context_taggings = "#{tag_type}_taggings".to_sym
20
22
  context_tags = tags_type.to_sym
21
- taggings_order = (preserve_tag_order? ? "#{ActsAsTaggableOn::Tagging.table_name}.id" : nil)
22
-
23
+ taggings_order = (preserve_tag_order? ? "#{ActsAsTaggableOn::Tagging.table_name}.id" : [])
24
+
23
25
  class_eval do
24
26
  # when preserving tag order, include order option so that for a 'tags' context
25
27
  # the associations tag_taggings & tags are always returned in created order
26
- has_many context_taggings, :as => :taggable,
27
- :dependent => :destroy,
28
- :include => :tag,
29
- :class_name => "ActsAsTaggableOn::Tagging",
30
- :conditions => ["#{ActsAsTaggableOn::Tagging.table_name}.context = ?", tags_type],
31
- :order => taggings_order
32
-
33
- has_many context_tags, :through => context_taggings,
34
- :source => :tag,
35
- :class_name => "ActsAsTaggableOn::Tag",
36
- :order => taggings_order
28
+ has_many_with_compatibility context_taggings, :as => :taggable,
29
+ :dependent => :destroy,
30
+ :class_name => "ActsAsTaggableOn::Tagging",
31
+ :order => taggings_order,
32
+ :conditions => ["#{ActsAsTaggableOn::Tagging.table_name}.context = (?)", tags_type],
33
+ :include => :tag
34
+
35
+ has_many_with_compatibility context_tags, :through => context_taggings,
36
+ :source => :tag,
37
+ :class_name => "ActsAsTaggableOn::Tag",
38
+ :order => taggings_order
39
+
37
40
  end
38
41
 
39
- class_eval %(
42
+ taggable_mixin.class_eval <<-RUBY, __FILE__, __LINE__ + 1
40
43
  def #{tag_type}_list
41
44
  tag_list_on('#{tags_type}')
42
45
  end
@@ -48,7 +51,7 @@ module ActsAsTaggableOn::Taggable
48
51
  def all_#{tags_type}_list
49
52
  all_tags_list_on('#{tags_type}')
50
53
  end
51
- )
54
+ RUBY
52
55
  end
53
56
  end
54
57
 
@@ -56,7 +59,7 @@ module ActsAsTaggableOn::Taggable
56
59
  super(preserve_tag_order, *tag_types)
57
60
  initialize_acts_as_taggable_on_core
58
61
  end
59
-
62
+
60
63
  # all column names are necessary for PostgreSQL group clause
61
64
  def grouped_column_names_for(object)
62
65
  object.column_names.map { |column| "#{object.table_name}.#{column}" }.join(", ")
@@ -80,12 +83,14 @@ module ActsAsTaggableOn::Taggable
80
83
  # User.tagged_with("awesome", "cool", :owned_by => foo ) # Users that are tagged with just awesome and cool by 'foo'
81
84
  def tagged_with(tags, options = {})
82
85
  tag_list = ActsAsTaggableOn::TagList.from(tags)
83
- empty_result = scoped(:conditions => "1 = 0")
86
+ empty_result = where("1 = 0")
84
87
 
85
88
  return empty_result if tag_list.empty?
86
89
 
87
90
  joins = []
88
91
  conditions = []
92
+ having = []
93
+ select_clause = []
89
94
 
90
95
  context = options.delete(:on)
91
96
  owned_by = options.delete(:owned_by)
@@ -101,15 +106,23 @@ module ActsAsTaggableOn::Taggable
101
106
 
102
107
  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)})"
103
108
 
109
+ if owned_by
110
+ joins << "JOIN #{ActsAsTaggableOn::Tagging.table_name}" +
111
+ " ON #{ActsAsTaggableOn::Tagging.table_name}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" +
112
+ " AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = #{quote_value(base_class.name)}" +
113
+ " AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_id = #{owned_by.id}" +
114
+ " AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_type = #{quote_value(owned_by.class.base_class.to_s)}"
115
+ end
116
+
104
117
  elsif options.delete(:any)
105
118
  # get tags, drop out if nothing returned (we need at least one)
106
- if options.delete(:wild)
107
- tags = ActsAsTaggableOn::Tag.named_like_any(tag_list)
119
+ tags = if options.delete(:wild)
120
+ ActsAsTaggableOn::Tag.named_like_any(tag_list)
108
121
  else
109
- tags = ActsAsTaggableOn::Tag.named_any(tag_list)
122
+ ActsAsTaggableOn::Tag.named_any(tag_list)
110
123
  end
111
124
 
112
- return scoped(:conditions => "1 = 0") unless tags.length > 0
125
+ return empty_result unless tags.length > 0
113
126
 
114
127
  # setup taggings alias so we can chain, ex: items_locations_taggings_awesome_cool_123
115
128
  # avoid ambiguous column name
@@ -128,29 +141,37 @@ module ActsAsTaggableOn::Taggable
128
141
  conditions << tags.map { |t| "#{taggings_alias}.tag_id = #{t.id}" }.join(" OR ")
129
142
  select_clause = "DISTINCT #{table_name}.*" unless context and tag_types.one?
130
143
 
131
- joins << tagging_join
144
+ if owned_by
145
+ tagging_join << " AND " +
146
+ sanitize_sql([
147
+ "#{taggings_alias}.tagger_id = ? AND #{taggings_alias}.tagger_type = ?",
148
+ owned_by.id,
149
+ owned_by.class.base_class.to_s
150
+ ])
151
+ end
132
152
 
153
+ joins << tagging_join
133
154
  else
134
155
  tags = ActsAsTaggableOn::Tag.named_any(tag_list)
156
+
135
157
  return empty_result unless tags.length == tag_list.length
136
158
 
137
159
  tags.each do |tag|
138
-
139
160
  taggings_alias = adjust_taggings_alias("#{alias_base_name[0..11]}_taggings_#{sha_prefix(tag.name)}")
140
-
141
161
  tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
142
162
  " ON #{taggings_alias}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" +
143
163
  " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}" +
144
164
  " AND #{taggings_alias}.tag_id = #{tag.id}"
165
+
145
166
  tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
146
167
 
147
168
  if owned_by
148
169
  tagging_join << " AND " +
149
- sanitize_sql([
150
- "#{taggings_alias}.tagger_id = ? AND #{taggings_alias}.tagger_type = ?",
151
- owned_by.id,
152
- owned_by.class.base_class.to_s
153
- ])
170
+ sanitize_sql([
171
+ "#{taggings_alias}.tagger_id = ? AND #{taggings_alias}.tagger_type = ?",
172
+ owned_by.id,
173
+ owned_by.class.base_class.to_s
174
+ ])
154
175
  end
155
176
 
156
177
  joins << tagging_join
@@ -170,13 +191,13 @@ module ActsAsTaggableOn::Taggable
170
191
  having = "COUNT(#{taggings_alias}.taggable_id) = #{tags.size}"
171
192
  end
172
193
 
173
- scoped(:select => select_clause,
174
- :joins => joins.join(" "),
175
- :group => group,
176
- :having => having,
177
- :conditions => conditions.join(" AND "),
178
- :order => options[:order],
179
- :readonly => false)
194
+ select(select_clause) \
195
+ .joins(joins.join(" ")) \
196
+ .where(conditions.join(" AND ")) \
197
+ .group(group) \
198
+ .having(having) \
199
+ .order(options[:order]) \
200
+ .readonly(false)
180
201
  end
181
202
 
182
203
  def is_taggable?
@@ -189,6 +210,10 @@ module ActsAsTaggableOn::Taggable
189
210
  end
190
211
  taggings_alias
191
212
  end
213
+
214
+ def taggable_mixin
215
+ @taggable_mixin ||= Module.new
216
+ end
192
217
  end
193
218
 
194
219
  module InstanceMethods
@@ -215,12 +240,13 @@ module ActsAsTaggableOn::Taggable
215
240
 
216
241
  def tag_list_cache_set_on(context)
217
242
  variable_name = "@#{context.to_s.singularize}_list"
218
- !instance_variable_get(variable_name).nil?
243
+ instance_variable_defined?(variable_name) && !instance_variable_get(variable_name).nil?
219
244
  end
220
245
 
221
246
  def tag_list_cache_on(context)
222
247
  variable_name = "@#{context.to_s.singularize}_list"
223
- instance_variable_get(variable_name) || instance_variable_set(variable_name, ActsAsTaggableOn::TagList.new(tags_on(context).map(&:name)))
248
+ return instance_variable_get(variable_name) if instance_variable_defined?(variable_name) && instance_variable_get(variable_name)
249
+ instance_variable_set(variable_name, ActsAsTaggableOn::TagList.new(tags_on(context).map(&:name)))
224
250
  end
225
251
 
226
252
  def tag_list_on(context)
@@ -230,7 +256,7 @@ module ActsAsTaggableOn::Taggable
230
256
 
231
257
  def all_tags_list_on(context)
232
258
  variable_name = "@all_#{context.to_s.singularize}_list"
233
- return instance_variable_get(variable_name) if instance_variable_get(variable_name)
259
+ return instance_variable_get(variable_name) if instance_variable_defined?(variable_name) && instance_variable_get(variable_name)
234
260
 
235
261
  instance_variable_set(variable_name, ActsAsTaggableOn::TagList.new(all_tags_on(context).map(&:name)).freeze)
236
262
  end
@@ -246,12 +272,10 @@ module ActsAsTaggableOn::Taggable
246
272
 
247
273
  if ActsAsTaggableOn::Tag.using_postgresql?
248
274
  group_columns = grouped_column_names_for(ActsAsTaggableOn::Tag)
249
- scope = scope.order("max(#{tagging_table_name}.created_at)").group(group_columns)
275
+ scope.order("max(#{tagging_table_name}.created_at)").group(group_columns)
250
276
  else
251
- scope = scope.group("#{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key}")
252
- end
253
-
254
- scope.all
277
+ scope.group("#{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key}")
278
+ end.to_a
255
279
  end
256
280
 
257
281
  ##
@@ -261,7 +285,7 @@ module ActsAsTaggableOn::Taggable
261
285
  # when preserving tag order, return tags in created order
262
286
  # if we added the order to the association this would always apply
263
287
  scope = scope.order("#{ActsAsTaggableOn::Tagging.table_name}.id") if self.class.preserve_tag_order?
264
- scope.all
288
+ scope
265
289
  end
266
290
 
267
291
  def set_tag_list_on(context, new_list)
@@ -303,7 +327,6 @@ module ActsAsTaggableOn::Taggable
303
327
  def save_tags
304
328
  tagging_contexts.each do |context|
305
329
  next unless tag_list_cache_set_on(context)
306
-
307
330
  # List of currently assigned tag names
308
331
  tag_list = tag_list_cache_on(context).uniq
309
332
 
@@ -315,13 +338,22 @@ module ActsAsTaggableOn::Taggable
315
338
 
316
339
  # Tag maintenance based on whether preserving the created order of tags
317
340
  if self.class.preserve_tag_order?
318
- # First off order the array of tag objects to match the tag list
319
- # rather than existing tags followed by new tags
320
- tags = tag_list.map{|l| tags.detect{|t| t.name.downcase == l.downcase}}
321
- # To preserve tags in the order in which they were added
322
- # delete all current tags and create new tags if the content or order has changed
323
- old_tags = (tags == current_tags ? [] : current_tags)
324
- new_tags = (tags == current_tags ? [] : tags)
341
+ old_tags, new_tags = current_tags - tags, tags - current_tags
342
+
343
+ shared_tags = current_tags & tags
344
+
345
+ if shared_tags.any? && tags[0...shared_tags.size] != shared_tags
346
+ index = shared_tags.each_with_index { |_, i| break i unless shared_tags[i] == tags[i] }
347
+
348
+ # Update arrays of tag objects
349
+ old_tags |= current_tags[index...current_tags.size]
350
+ new_tags |= current_tags[index...current_tags.size] & shared_tags
351
+
352
+ # Order the array of tag objects to match the tag list
353
+ new_tags = tags.map do |t|
354
+ new_tags.find { |n| n.name.downcase == t.name.downcase }
355
+ end.compact
356
+ end
325
357
  else
326
358
  # Delete discarded tags and create new tags
327
359
  old_tags = current_tags - tags
@@ -330,8 +362,7 @@ module ActsAsTaggableOn::Taggable
330
362
 
331
363
  # Find taggings to remove:
332
364
  if old_tags.present?
333
- old_taggings = taggings.where(:tagger_type => nil, :tagger_id => nil,
334
- :context => context.to_s, :tag_id => old_tags).all
365
+ old_taggings = taggings.where(:tagger_type => nil, :tagger_id => nil, :context => context.to_s, :tag_id => old_tags)
335
366
  end
336
367
 
337
368
  # Destroy old taggings:
@@ -12,7 +12,7 @@ module ActsAsTaggableOn::Taggable
12
12
  tag_type = tags_type.to_s.singularize
13
13
  context_tags = tags_type.to_sym
14
14
 
15
- class_eval %(
15
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
16
16
  def #{tag_type}_list_changed?
17
17
  changed_attributes.include?("#{tag_type}_list")
18
18
  end
@@ -28,10 +28,10 @@ module ActsAsTaggableOn::Taggable
28
28
  def #{tag_type}_list_changes
29
29
  [changed_attributes['#{tag_type}_list'], __send__('#{tag_type}_list')] if changed_attributes.include?("#{tag_type}_list")
30
30
  end
31
- )
31
+ RUBY
32
32
 
33
33
  end
34
34
  end
35
35
  end
36
36
  end
37
- end
37
+ end
@@ -19,11 +19,11 @@ module ActsAsTaggableOn::Taggable
19
19
 
20
20
  def initialize_acts_as_taggable_on_ownership
21
21
  tag_types.map(&:to_s).each do |tag_type|
22
- class_eval %(
22
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
23
23
  def #{tag_type}_from(owner)
24
24
  owner_tag_list_on(owner, '#{tag_type}')
25
25
  end
26
- )
26
+ RUBY
27
27
  end
28
28
  end
29
29
  end
@@ -37,22 +37,25 @@ module ActsAsTaggableOn::Taggable
37
37
  #{ActsAsTaggableOn::Tagging.table_name}.tagger_id = ? AND
38
38
  #{ActsAsTaggableOn::Tagging.table_name}.tagger_type = ?), context.to_s, owner.id, owner.class.base_class.to_s])
39
39
  end
40
+
40
41
  # when preserving tag order, return tags in created order
41
42
  # if we added the order to the association this would always apply
42
- scope = scope.order("#{ActsAsTaggableOn::Tagging.table_name}.id") if self.class.preserve_tag_order?
43
- scope.all
43
+ if self.class.preserve_tag_order?
44
+ scope.order("#{ActsAsTaggableOn::Tagging.table_name}.id")
45
+ else
46
+ scope
47
+ end
44
48
  end
45
49
 
46
50
  def cached_owned_tag_list_on(context)
47
51
  variable_name = "@owned_#{context}_list"
48
- cache = instance_variable_get(variable_name) || instance_variable_set(variable_name, {})
52
+ cache = (instance_variable_defined?(variable_name) && instance_variable_get(variable_name)) || instance_variable_set(variable_name, {})
49
53
  end
50
54
 
51
55
  def owner_tag_list_on(owner, context)
52
56
  add_custom_context(context)
53
57
 
54
58
  cache = cached_owned_tag_list_on(context)
55
- cache.delete_if { |key, value| key.id == owner.id && key.class == owner.class }
56
59
 
57
60
  cache[owner] ||= ActsAsTaggableOn::TagList.new(*owner_tags_on(owner, context).map(&:name))
58
61
  end
@@ -61,7 +64,6 @@ module ActsAsTaggableOn::Taggable
61
64
  add_custom_context(context)
62
65
 
63
66
  cache = cached_owned_tag_list_on(context)
64
- cache.delete_if { |key, value| key.id == owner.id && key.class == owner.class }
65
67
 
66
68
  cache[owner] = ActsAsTaggableOn::TagList.from(new_list)
67
69
  end
@@ -86,13 +88,20 @@ module ActsAsTaggableOn::Taggable
86
88
 
87
89
  # Tag maintenance based on whether preserving the created order of tags
88
90
  if self.class.preserve_tag_order?
89
- # First off order the array of tag objects to match the tag list
90
- # rather than existing tags followed by new tags
91
- tags = tag_list.uniq.map{|s| tags.detect{|t| t.name.downcase == s.downcase}}
92
- # To preserve tags in the order in which they were added
93
- # delete all owned tags and create new tags if the content or order has changed
94
- old_tags = (tags == owned_tags ? [] : owned_tags)
95
- new_tags = (tags == owned_tags ? [] : tags)
91
+ old_tags, new_tags = owned_tags - tags, tags - owned_tags
92
+
93
+ shared_tags = owned_tags & tags
94
+
95
+ if shared_tags.any? && tags[0...shared_tags.size] != shared_tags
96
+ index = shared_tags.each_with_index { |_, i| break i unless shared_tags[i] == tags[i] }
97
+
98
+ # Update arrays of tag objects
99
+ old_tags |= owned_tags.from(index)
100
+ new_tags |= owned_tags.from(index) & shared_tags
101
+
102
+ # Order the array of tag objects to match the tag list
103
+ new_tags = tags.map { |t| new_tags.find { |n| n.name.downcase == t.name.downcase } }.compact
104
+ end
96
105
  else
97
106
  # Delete discarded tags and create new tags
98
107
  old_tags = owned_tags - tags
@@ -104,7 +113,7 @@ module ActsAsTaggableOn::Taggable
104
113
  if old_tags.present?
105
114
  old_taggings = ActsAsTaggableOn::Tagging.where(:taggable_id => id, :taggable_type => self.class.base_class.to_s,
106
115
  :tagger_type => owner.class.base_class.to_s, :tagger_id => owner.id,
107
- :tag_id => old_tags, :context => context).all
116
+ :tag_id => old_tags, :context => context)
108
117
  end
109
118
 
110
119
  # Destroy old taggings:
@@ -9,7 +9,7 @@ module ActsAsTaggableOn::Taggable
9
9
  module ClassMethods
10
10
  def initialize_acts_as_taggable_on_related
11
11
  tag_types.map(&:to_s).each do |tag_type|
12
- class_eval %(
12
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
13
13
  def find_related_#{tag_type}(options = {})
14
14
  related_tags_for('#{tag_type}', self.class, options)
15
15
  end
@@ -18,11 +18,11 @@ module ActsAsTaggableOn::Taggable
18
18
  def find_related_#{tag_type}_for(klass, options = {})
19
19
  related_tags_for('#{tag_type}', klass, options)
20
20
  end
21
- )
21
+ RUBY
22
22
  end
23
23
 
24
24
  unless tag_types.empty?
25
- class_eval %(
25
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
26
26
  def find_matching_contexts(search_context, result_context, options = {})
27
27
  matching_contexts_for(search_context.to_s, result_context.to_s, self.class, options)
28
28
  end
@@ -30,7 +30,7 @@ module ActsAsTaggableOn::Taggable
30
30
  def find_matching_contexts_for(klass, search_context, result_context, options = {})
31
31
  matching_contexts_for(search_context.to_s, result_context.to_s, klass, options)
32
32
  end
33
- )
33
+ RUBY
34
34
  end
35
35
  end
36
36
 
@@ -44,29 +44,40 @@ module ActsAsTaggableOn::Taggable
44
44
  def matching_contexts_for(search_context, result_context, klass, options = {})
45
45
  tags_to_find = tags_on(search_context).collect { |t| t.name }
46
46
 
47
- exclude_self = "#{klass.table_name}.#{klass.primary_key} != #{id} AND" if [self.class.base_class, self.class].include? klass
48
-
49
- group_columns = ActsAsTaggableOn::Tag.using_postgresql? ? grouped_column_names_for(klass) : "#{klass.table_name}.#{klass.primary_key}"
50
-
51
- klass.scoped({ :select => "#{klass.table_name}.*, COUNT(#{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key}) AS count",
52
- :from => "#{klass.table_name}, #{ActsAsTaggableOn::Tag.table_name}, #{ActsAsTaggableOn::Tagging.table_name}",
53
- :conditions => ["#{exclude_self} #{klass.table_name}.#{klass.primary_key} = #{ActsAsTaggableOn::Tagging.table_name}.taggable_id AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = '#{klass.base_class.to_s}' AND #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key} AND #{ActsAsTaggableOn::Tag.table_name}.name IN (?) AND #{ActsAsTaggableOn::Tagging.table_name}.context = ?", tags_to_find, result_context],
54
- :group => group_columns,
55
- :order => "count DESC" }.update(options))
47
+ klass.select("#{klass.table_name}.*, COUNT(#{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key}) AS count") \
48
+ .from("#{klass.table_name}, #{ActsAsTaggableOn::Tag.table_name}, #{ActsAsTaggableOn::Tagging.table_name}") \
49
+ .where(["#{exclude_self(klass, id)} #{klass.table_name}.#{klass.primary_key} = #{ActsAsTaggableOn::Tagging.table_name}.taggable_id AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = '#{klass.base_class.to_s}' AND #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key} AND #{ActsAsTaggableOn::Tag.table_name}.name IN (?) AND #{ActsAsTaggableOn::Tagging.table_name}.context = ?", tags_to_find, result_context]) \
50
+ .group(group_columns(klass)) \
51
+ .order("count DESC")
56
52
  end
57
53
 
58
54
  def related_tags_for(context, klass, options = {})
59
- tags_to_find = tags_on(context).collect { |t| t.name }
55
+ tags_to_ignore = Array.wrap(options.delete(:ignore)).map(&:to_s) || []
56
+ tags_to_find = tags_on(context).collect { |t| t.name }.reject { |t| tags_to_ignore.include? t }
60
57
 
61
- exclude_self = "#{klass.table_name}.#{klass.primary_key} != #{id} AND" if [self.class.base_class, self.class].include? klass
58
+ klass.select("#{klass.table_name}.*, COUNT(#{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key}) AS count") \
59
+ .from("#{klass.table_name}, #{ActsAsTaggableOn::Tag.table_name}, #{ActsAsTaggableOn::Tagging.table_name}") \
60
+ .where(["#{exclude_self(klass, id)} #{klass.table_name}.#{klass.primary_key} = #{ActsAsTaggableOn::Tagging.table_name}.taggable_id AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = '#{klass.base_class.to_s}' AND #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key} AND #{ActsAsTaggableOn::Tag.table_name}.name IN (?)", tags_to_find]) \
61
+ .group(group_columns(klass)) \
62
+ .order("count DESC")
63
+ end
62
64
 
63
- group_columns = ActsAsTaggableOn::Tag.using_postgresql? ? grouped_column_names_for(klass) : "#{klass.table_name}.#{klass.primary_key}"
65
+ private
66
+
67
+ def exclude_self(klass, id)
68
+ if [self.class.base_class, self.class].include? klass
69
+ "#{klass.table_name}.#{klass.primary_key} != #{id} AND"
70
+ else
71
+ nil
72
+ end
73
+ end
64
74
 
65
- klass.scoped({ :select => "#{klass.table_name}.*, COUNT(#{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key}) AS count",
66
- :from => "#{klass.table_name}, #{ActsAsTaggableOn::Tag.table_name}, #{ActsAsTaggableOn::Tagging.table_name}",
67
- :conditions => ["#{exclude_self} #{klass.table_name}.#{klass.primary_key} = #{ActsAsTaggableOn::Tagging.table_name}.taggable_id AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = '#{klass.base_class.to_s}' AND #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key} AND #{ActsAsTaggableOn::Tag.table_name}.name IN (?)", tags_to_find],
68
- :group => group_columns,
69
- :order => "count DESC" }.update(options))
75
+ def group_columns(klass)
76
+ if ActsAsTaggableOn::Tag.using_postgresql?
77
+ grouped_column_names_for(klass)
78
+ else
79
+ "#{klass.table_name}.#{klass.primary_key}"
80
+ end
70
81
  end
71
82
  end
72
83
  end
@@ -2,7 +2,7 @@ module ActsAsTaggableOn
2
2
  class Tag < ::ActiveRecord::Base
3
3
  include ActsAsTaggableOn::Utils
4
4
 
5
- attr_accessible :name
5
+ attr_accessible :name if defined?(ActiveModel::MassAssignmentSecurity)
6
6
 
7
7
  ### ASSOCIATIONS:
8
8
 
@@ -11,17 +11,30 @@ module ActsAsTaggableOn
11
11
  ### VALIDATIONS:
12
12
 
13
13
  validates_presence_of :name
14
- validates_uniqueness_of :name
14
+ validates_uniqueness_of :name, :if => :validates_name_uniqueness?
15
15
  validates_length_of :name, :maximum => 255
16
16
 
17
+ # monkey patch this method if don't need name uniqueness validation
18
+ def validates_name_uniqueness?
19
+ true
20
+ end
21
+
17
22
  ### SCOPES:
18
23
 
19
24
  def self.named(name)
20
- where(["lower(name) = ?", name.downcase])
25
+ if ActsAsTaggableOn.strict_case_match
26
+ where(["name = #{binary}?", name])
27
+ else
28
+ where(["lower(name) = ?", name.downcase])
29
+ end
21
30
  end
22
31
 
23
32
  def self.named_any(list)
24
- where(list.map { |tag| sanitize_sql(["lower(name) = ?", tag.to_s.mb_chars.downcase]) }.join(" OR "))
33
+ if ActsAsTaggableOn.strict_case_match
34
+ where(list.map { |tag| sanitize_sql(["name = #{binary}?", tag.to_s.mb_chars]) }.join(" OR "))
35
+ else
36
+ where(list.map { |tag| sanitize_sql(["lower(name) = ?", tag.to_s.mb_chars.downcase]) }.join(" OR "))
37
+ end
25
38
  end
26
39
 
27
40
  def self.named_like(name)
@@ -35,7 +48,11 @@ module ActsAsTaggableOn
35
48
  ### CLASS METHODS:
36
49
 
37
50
  def self.find_or_create_with_like_by_name(name)
38
- named_like(name).first || create(:name => name)
51
+ if (ActsAsTaggableOn.strict_case_match)
52
+ self.find_or_create_all_with_like_by_name([name]).first
53
+ else
54
+ named_like(name).first || create(:name => name)
55
+ end
39
56
  end
40
57
 
41
58
  def self.find_or_create_all_with_like_by_name(*list)
@@ -43,14 +60,14 @@ module ActsAsTaggableOn
43
60
 
44
61
  return [] if list.empty?
45
62
 
46
- existing_tags = Tag.named_any(list).all
47
- new_tag_names = list.reject do |name|
48
- name = comparable_name(name)
49
- existing_tags.any? { |tag| comparable_name(tag.name) == name }
50
- end
51
- created_tags = new_tag_names.map { |name| Tag.create(:name => name) }
63
+ existing_tags = Tag.named_any(list)
64
+
65
+ list.map do |tag_name|
66
+ comparable_tag_name = comparable_name(tag_name)
67
+ existing_tag = existing_tags.find { |tag| comparable_name(tag.name) == comparable_tag_name }
52
68
 
53
- existing_tags + created_tags
69
+ existing_tag || Tag.create(:name => tag_name)
70
+ end
54
71
  end
55
72
 
56
73
  ### INSTANCE METHODS:
@@ -69,9 +86,14 @@ module ActsAsTaggableOn
69
86
 
70
87
  class << self
71
88
  private
72
- def comparable_name(str)
73
- str.mb_chars.downcase.to_s
74
- end
89
+
90
+ def comparable_name(str)
91
+ str.mb_chars.downcase.to_s
92
+ end
93
+
94
+ def binary
95
+ /mysql/ === ActiveRecord::Base.connection_config[:adapter] ? "BINARY " : nil
96
+ end
75
97
  end
76
98
  end
77
99
  end
@@ -58,7 +58,7 @@ module ActsAsTaggableOn
58
58
  end
59
59
 
60
60
  ##
61
- # Transform the tag_list into a tag string suitable for edting in a form.
61
+ # Transform the tag_list into a tag string suitable for editing in a form.
62
62
  # The tags are joined with <tt>TagList.delimiter</tt> and quoted if necessary.
63
63
  #
64
64
  # Example:
@@ -81,7 +81,7 @@ module ActsAsTaggableOn
81
81
  def clean!
82
82
  reject!(&:blank?)
83
83
  map!(&:strip)
84
- map!(&:downcase) if ActsAsTaggableOn.force_lowercase
84
+ map!{ |tag| tag.mb_chars.downcase.to_s } if ActsAsTaggableOn.force_lowercase
85
85
  map!(&:parameterize) if ActsAsTaggableOn.force_parameterize
86
86
 
87
87
  uniq!
@@ -80,7 +80,7 @@ module ActsAsTaggableOn
80
80
  self.preserve_tag_order = preserve_tag_order
81
81
 
82
82
  class_eval do
83
- has_many :taggings, :as => :taggable, :dependent => :destroy, :include => :tag, :class_name => "ActsAsTaggableOn::Tagging"
83
+ has_many :taggings, :as => :taggable, :dependent => :destroy, :class_name => "ActsAsTaggableOn::Tagging"
84
84
  has_many :base_tags, :through => :taggings, :source => :tag, :class_name => "ActsAsTaggableOn::Tag"
85
85
 
86
86
  def self.taggable?
@@ -88,14 +88,17 @@ module ActsAsTaggableOn
88
88
  end
89
89
 
90
90
  include ActsAsTaggableOn::Utils
91
- include ActsAsTaggableOn::Taggable::Core
92
- include ActsAsTaggableOn::Taggable::Collection
93
- include ActsAsTaggableOn::Taggable::Cache
94
- include ActsAsTaggableOn::Taggable::Ownership
95
- include ActsAsTaggableOn::Taggable::Related
96
- include ActsAsTaggableOn::Taggable::Dirty
97
91
  end
98
92
  end
93
+
94
+ # each of these add context-specific methods and must be
95
+ # called on each call of taggable_on
96
+ include ActsAsTaggableOn::Taggable::Core
97
+ include ActsAsTaggableOn::Taggable::Collection
98
+ include ActsAsTaggableOn::Taggable::Cache
99
+ include ActsAsTaggableOn::Taggable::Ownership
100
+ include ActsAsTaggableOn::Taggable::Related
101
+ include ActsAsTaggableOn::Taggable::Dirty
99
102
  end
100
103
 
101
104
  end
@@ -15,9 +15,18 @@ module ActsAsTaggableOn
15
15
  # end
16
16
  def acts_as_tagger(opts={})
17
17
  class_eval do
18
- has_many :owned_taggings, opts.merge(:as => :tagger, :dependent => :destroy,
19
- :include => :tag, :class_name => "ActsAsTaggableOn::Tagging")
20
- has_many :owned_tags, :through => :owned_taggings, :source => :tag, :uniq => true, :class_name => "ActsAsTaggableOn::Tag"
18
+ has_many_with_compatibility :owned_taggings,
19
+ opts.merge(
20
+ :as => :tagger,
21
+ :dependent => :destroy,
22
+ :class_name => "ActsAsTaggableOn::Tagging"
23
+ )
24
+
25
+ has_many_with_compatibility :owned_tags,
26
+ :through => :owned_taggings,
27
+ :source => :tag,
28
+ :class_name => "ActsAsTaggableOn::Tag",
29
+ :uniq => true
21
30
  end
22
31
 
23
32
  include ActsAsTaggableOn::Tagger::InstanceMethods