acts-as-taggable-on 2.3.3 → 2.4.1

Sign up to get free protection for your applications and to get access to all the features.
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