acts-as-taggable-on 2.1.0 → 2.3.0

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 (42) hide show
  1. data/.gitignore +3 -0
  2. data/.travis.yml +1 -2
  3. data/CHANGELOG +10 -0
  4. data/README.rdoc +87 -67
  5. data/acts-as-taggable-on.gemspec +5 -4
  6. data/lib/acts-as-taggable-on/version.rb +1 -1
  7. data/lib/acts-as-taggable-on.rb +29 -6
  8. data/lib/acts_as_taggable_on/acts_as_taggable_on/cache.rb +1 -1
  9. data/lib/acts_as_taggable_on/acts_as_taggable_on/collection.rb +9 -21
  10. data/lib/acts_as_taggable_on/acts_as_taggable_on/core.rb +110 -33
  11. data/lib/acts_as_taggable_on/acts_as_taggable_on/dirty.rb +37 -0
  12. data/lib/acts_as_taggable_on/acts_as_taggable_on/ownership.rb +33 -12
  13. data/lib/acts_as_taggable_on/acts_as_taggable_on/related.rb +16 -16
  14. data/lib/acts_as_taggable_on/tag.rb +14 -18
  15. data/lib/acts_as_taggable_on/tag_list.rb +13 -12
  16. data/lib/acts_as_taggable_on/taggable.rb +102 -0
  17. data/lib/acts_as_taggable_on/{acts_as_tagger.rb → tagger.rb} +3 -3
  18. data/lib/acts_as_taggable_on/tagging.rb +12 -2
  19. data/lib/acts_as_taggable_on/tags_helper.rb +2 -2
  20. data/lib/acts_as_taggable_on/utils.rb +11 -8
  21. data/lib/generators/acts_as_taggable_on/migration/migration_generator.rb +9 -2
  22. data/lib/generators/acts_as_taggable_on/migration/templates/active_record/migration.rb +3 -1
  23. data/spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb +286 -61
  24. data/spec/acts_as_taggable_on/tag_list_spec.rb +84 -61
  25. data/spec/acts_as_taggable_on/tag_spec.rb +42 -24
  26. data/spec/acts_as_taggable_on/taggable_spec.rb +234 -39
  27. data/spec/acts_as_taggable_on/tagger_spec.rb +36 -15
  28. data/spec/acts_as_taggable_on/tagging_spec.rb +2 -5
  29. data/spec/acts_as_taggable_on/tags_helper_spec.rb +16 -0
  30. data/spec/acts_as_taggable_on/utils_spec.rb +2 -3
  31. data/spec/database.yml.sample +1 -1
  32. data/spec/generators/acts_as_taggable_on/migration/migration_generator_spec.rb +22 -0
  33. data/spec/models.rb +16 -2
  34. data/spec/schema.rb +12 -0
  35. data/spec/spec_helper.rb +4 -1
  36. metadata +98 -125
  37. data/VERSION +0 -1
  38. data/generators/acts_as_taggable_on_migration/acts_as_taggable_on_migration_generator.rb +0 -7
  39. data/generators/acts_as_taggable_on_migration/templates/migration.rb +0 -29
  40. data/lib/acts_as_taggable_on/acts_as_taggable_on.rb +0 -63
  41. data/lib/acts_as_taggable_on/compatibility/Gemfile +0 -8
  42. data/lib/acts_as_taggable_on/compatibility/active_record_backports.rb +0 -21
@@ -1,5 +1,5 @@
1
1
  module ActsAsTaggableOn::Taggable
2
- module Core
2
+ module Core
3
3
  def self.included(base)
4
4
  base.send :include, ActsAsTaggableOn::Taggable::Core::InstanceMethods
5
5
  base.extend ActsAsTaggableOn::Taggable::Core::ClassMethods
@@ -8,21 +8,32 @@ module ActsAsTaggableOn::Taggable
8
8
  attr_writer :custom_contexts
9
9
  after_save :save_tags
10
10
  end
11
-
11
+
12
12
  base.initialize_acts_as_taggable_on_core
13
13
  end
14
-
14
+
15
15
  module ClassMethods
16
16
  def initialize_acts_as_taggable_on_core
17
17
  tag_types.map(&:to_s).each do |tags_type|
18
18
  tag_type = tags_type.to_s.singularize
19
19
  context_taggings = "#{tag_type}_taggings".to_sym
20
20
  context_tags = tags_type.to_sym
21
-
21
+ taggings_order = (preserve_tag_order? ? "#{ActsAsTaggableOn::Tagging.table_name}.id" : nil)
22
+
22
23
  class_eval do
23
- has_many context_taggings, :as => :taggable, :dependent => :destroy, :include => :tag, :class_name => "ActsAsTaggableOn::Tagging",
24
- :conditions => ["#{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.id AND #{ActsAsTaggableOn::Tagging.table_name}.context = ?", tags_type]
25
- has_many context_tags, :through => context_taggings, :source => :tag, :class_name => "ActsAsTaggableOn::Tag"
24
+ # when preserving tag order, include order option so that for a 'tags' context
25
+ # 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
26
37
  end
27
38
 
28
39
  class_eval %(
@@ -38,11 +49,11 @@ module ActsAsTaggableOn::Taggable
38
49
  all_tags_list_on('#{tags_type}')
39
50
  end
40
51
  )
41
- end
52
+ end
42
53
  end
43
-
44
- def acts_as_taggable_on(*args)
45
- super(*args)
54
+
55
+ def taggable_on(preserve_tag_order, *tag_types)
56
+ super(preserve_tag_order, *tag_types)
46
57
  initialize_acts_as_taggable_on_core
47
58
  end
48
59
 
@@ -59,12 +70,14 @@ module ActsAsTaggableOn::Taggable
59
70
  # * <tt>:exclude</tt> - if set to true, return objects that are *NOT* tagged with the specified tags
60
71
  # * <tt>:any</tt> - if set to true, return objects that are tagged with *ANY* of the specified tags
61
72
  # * <tt>:match_all</tt> - if set to true, return objects that are *ONLY* tagged with the specified tags
73
+ # * <tt>:owned_by</tt> - return objects that are *ONLY* owned by the owner
62
74
  #
63
75
  # Example:
64
76
  # User.tagged_with("awesome", "cool") # Users that are tagged with awesome and cool
65
77
  # User.tagged_with("awesome", "cool", :exclude => true) # Users that are not tagged with awesome or cool
66
78
  # User.tagged_with("awesome", "cool", :any => true) # Users that are tagged with awesome or cool
67
79
  # User.tagged_with("awesome", "cool", :match_all => true) # Users that are tagged with just awesome and cool
80
+ # User.tagged_with("awesome", "cool", :owned_by => foo ) # Users that are tagged with just awesome and cool by 'foo'
68
81
  def tagged_with(tags, options = {})
69
82
  tag_list = ActsAsTaggableOn::TagList.from(tags)
70
83
  empty_result = scoped(:conditions => "1 = 0")
@@ -75,23 +88,35 @@ module ActsAsTaggableOn::Taggable
75
88
  conditions = []
76
89
 
77
90
  context = options.delete(:on)
91
+ owned_by = options.delete(:owned_by)
78
92
  alias_base_name = undecorated_table_name.gsub('.','_')
79
93
 
80
94
  if options.delete(:exclude)
81
- tags_conditions = tag_list.map { |t| sanitize_sql(["#{ActsAsTaggableOn::Tag.table_name}.name #{like_operator} ?", t]) }.join(" OR ")
82
- 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}.id AND (#{tags_conditions}) WHERE #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = #{quote_value(base_class.name)})"
95
+ if options.delete(:wild)
96
+ tags_conditions = tag_list.map { |t| sanitize_sql(["#{ActsAsTaggableOn::Tag.table_name}.name #{like_operator} ? ESCAPE '!'", "%#{escape_like(t)}%"]) }.join(" OR ")
97
+ else
98
+ tags_conditions = tag_list.map { |t| sanitize_sql(["#{ActsAsTaggableOn::Tag.table_name}.name #{like_operator} ?", t]) }.join(" OR ")
99
+ end
100
+
101
+ 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)})"
83
102
 
84
103
  elsif options.delete(:any)
85
104
  # get tags, drop out if nothing returned (we need at least one)
86
- tags = ActsAsTaggableOn::Tag.named_any(tag_list)
105
+ if options.delete(:wild)
106
+ tags = ActsAsTaggableOn::Tag.named_like_any(tag_list)
107
+ else
108
+ tags = ActsAsTaggableOn::Tag.named_any(tag_list)
109
+ end
110
+
87
111
  return scoped(:conditions => "1 = 0") unless tags.length > 0
88
112
 
89
113
  # setup taggings alias so we can chain, ex: items_locations_taggings_awesome_cool_123
90
114
  # avoid ambiguous column name
91
115
  taggings_context = context ? "_#{context}" : ''
92
-
93
- #TODO: fix alias to be smaller
94
- taggings_alias = "#{alias_base_name}#{taggings_context}_taggings_#{tags.map(&:safe_name).join('_')}_#{rand(1024)}"
116
+
117
+ taggings_alias = adjust_taggings_alias(
118
+ "#{alias_base_name[0..4]}#{taggings_context[0..6]}_taggings_#{sha_prefix(tags.map(&:name).join('_'))}"
119
+ )
95
120
 
96
121
  tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
97
122
  " ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
@@ -109,9 +134,8 @@ module ActsAsTaggableOn::Taggable
109
134
  return empty_result unless tags.length == tag_list.length
110
135
 
111
136
  tags.each do |tag|
112
- prefix = "#{tag.safe_name}_#{rand(1024)}"
113
137
 
114
- taggings_alias = "#{alias_base_name}_taggings_#{prefix}"
138
+ taggings_alias = adjust_taggings_alias("#{alias_base_name[0..11]}_taggings_#{sha_prefix(tag.name)}")
115
139
 
116
140
  tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
117
141
  " ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
@@ -119,11 +143,20 @@ module ActsAsTaggableOn::Taggable
119
143
  " AND #{taggings_alias}.tag_id = #{tag.id}"
120
144
  tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
121
145
 
146
+ if owned_by
147
+ tagging_join << " AND " +
148
+ sanitize_sql([
149
+ "#{taggings_alias}.tagger_id = ? AND #{taggings_alias}.tagger_type = ?",
150
+ owned_by.id,
151
+ owned_by.class.to_s
152
+ ])
153
+ end
154
+
122
155
  joins << tagging_join
123
156
  end
124
157
  end
125
158
 
126
- taggings_alias, tags_alias = "#{alias_base_name}_taggings_group", "#{alias_base_name}_tags_group"
159
+ taggings_alias, tags_alias = adjust_taggings_alias("#{alias_base_name}_taggings_group"), "#{alias_base_name}_tags_group"
127
160
 
128
161
  if options.delete(:match_all)
129
162
  joins << "LEFT OUTER JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
@@ -146,8 +179,15 @@ module ActsAsTaggableOn::Taggable
146
179
  def is_taggable?
147
180
  true
148
181
  end
149
- end
150
-
182
+
183
+ def adjust_taggings_alias(taggings_alias)
184
+ if taggings_alias.size > 75
185
+ taggings_alias = 'taggings_alias_' + Digest::SHA1.hexdigest(taggings_alias)
186
+ end
187
+ taggings_alias
188
+ end
189
+ end
190
+
151
191
  module InstanceMethods
152
192
  # all column names are necessary for PostgreSQL group clause
153
193
  def grouped_column_names_for(object)
@@ -200,7 +240,7 @@ module ActsAsTaggableOn::Taggable
200
240
 
201
241
  opts = ["#{tagging_table_name}.context = ?", context.to_s]
202
242
  scope = base_tags.where(opts)
203
-
243
+
204
244
  if ActsAsTaggableOn::Tag.using_postgresql?
205
245
  group_columns = grouped_column_names_for(ActsAsTaggableOn::Tag)
206
246
  scope = scope.order("max(#{tagging_table_name}.created_at)").group(group_columns)
@@ -214,13 +254,19 @@ module ActsAsTaggableOn::Taggable
214
254
  ##
215
255
  # Returns all tags that are not owned of a given context
216
256
  def tags_on(context)
217
- base_tags.where(["#{ActsAsTaggableOn::Tagging.table_name}.context = ? AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_id IS NULL", context.to_s]).all
257
+ scope = base_tags.where(["#{ActsAsTaggableOn::Tagging.table_name}.context = ? AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_id IS NULL", context.to_s])
258
+ # when preserving tag order, return tags in created order
259
+ # if we added the order to the association this would always apply
260
+ scope = scope.order("#{ActsAsTaggableOn::Tagging.table_name}.id") if self.class.preserve_tag_order?
261
+ scope.all
218
262
  end
219
263
 
220
264
  def set_tag_list_on(context, new_list)
221
265
  add_custom_context(context)
222
266
 
223
267
  variable_name = "@#{context.to_s.singularize}_list"
268
+ process_dirty_object(context, new_list) unless custom_contexts.include?(context.to_s)
269
+
224
270
  instance_variable_set(variable_name, ActsAsTaggableOn::TagList.from(new_list))
225
271
  end
226
272
 
@@ -228,12 +274,26 @@ module ActsAsTaggableOn::Taggable
228
274
  custom_contexts + self.class.tag_types.map(&:to_s)
229
275
  end
230
276
 
277
+ def process_dirty_object(context,new_list)
278
+ value = new_list.is_a?(Array) ? new_list.join(', ') : new_list
279
+ attrib = "#{context.to_s.singularize}_list"
280
+
281
+ if changed_attributes.include?(attrib)
282
+ # The attribute already has an unsaved change.
283
+ old = changed_attributes[attrib]
284
+ changed_attributes.delete(attrib) if (old.to_s == value.to_s)
285
+ else
286
+ old = tag_list_on(context).to_s
287
+ changed_attributes[attrib] = old if (old.to_s != value.to_s)
288
+ end
289
+ end
290
+
231
291
  def reload(*args)
232
292
  self.class.tag_types.each do |context|
233
293
  instance_variable_set("@#{context.to_s.singularize}_list", nil)
234
294
  instance_variable_set("@all_#{context.to_s.singularize}_list", nil)
235
295
  end
236
-
296
+
237
297
  super(*args)
238
298
  end
239
299
 
@@ -241,22 +301,39 @@ module ActsAsTaggableOn::Taggable
241
301
  tagging_contexts.each do |context|
242
302
  next unless tag_list_cache_set_on(context)
243
303
 
304
+ # List of currently assigned tag names
244
305
  tag_list = tag_list_cache_on(context).uniq
245
306
 
246
307
  # Find existing tags or create non-existing tags:
247
- tag_list = ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name(tag_list)
308
+ tags = ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name(tag_list)
248
309
 
310
+ # Tag objects for currently assigned tags
249
311
  current_tags = tags_on(context)
250
- old_tags = current_tags - tag_list
251
- new_tags = tag_list - current_tags
252
-
312
+
313
+ # Tag maintenance based on whether preserving the created order of tags
314
+ if self.class.preserve_tag_order?
315
+ # First off order the array of tag objects to match the tag list
316
+ # rather than existing tags followed by new tags
317
+ tags = tag_list.map{|l| tags.detect{|t| t.name.downcase == l.downcase}}
318
+ # To preserve tags in the order in which they were added
319
+ # delete all current tags and create new tags if the content or order has changed
320
+ old_tags = (tags == current_tags ? [] : current_tags)
321
+ new_tags = (tags == current_tags ? [] : tags)
322
+ else
323
+ # Delete discarded tags and create new tags
324
+ old_tags = current_tags - tags
325
+ new_tags = tags - current_tags
326
+ end
327
+
253
328
  # Find taggings to remove:
254
- old_taggings = taggings.where(:tagger_type => nil, :tagger_id => nil,
255
- :context => context.to_s, :tag_id => old_tags).all
329
+ if old_tags.present?
330
+ old_taggings = taggings.where(:tagger_type => nil, :tagger_id => nil,
331
+ :context => context.to_s, :tag_id => old_tags).all
332
+ end
256
333
 
334
+ # Destroy old taggings:
257
335
  if old_taggings.present?
258
- # Destroy old taggings:
259
- ActsAsTaggableOn::Tagging.destroy_all :id => old_taggings.map(&:id)
336
+ ActsAsTaggableOn::Tagging.destroy_all "#{ActsAsTaggableOn::Tagging.primary_key}".to_sym => old_taggings.map(&:id)
260
337
  end
261
338
 
262
339
  # Create new taggings:
@@ -0,0 +1,37 @@
1
+ module ActsAsTaggableOn::Taggable
2
+ module Dirty
3
+ def self.included(base)
4
+ base.extend ActsAsTaggableOn::Taggable::Dirty::ClassMethods
5
+
6
+ base.initialize_acts_as_taggable_on_dirty
7
+ end
8
+
9
+ module ClassMethods
10
+ def initialize_acts_as_taggable_on_dirty
11
+ tag_types.map(&:to_s).each do |tags_type|
12
+ tag_type = tags_type.to_s.singularize
13
+ context_tags = tags_type.to_sym
14
+
15
+ class_eval %(
16
+ def #{tag_type}_list_changed?
17
+ changed_attributes.include?("#{tag_type}_list")
18
+ end
19
+
20
+ def #{tag_type}_list_was
21
+ changed_attributes.include?("#{tag_type}_list") ? changed_attributes["#{tag_type}_list"] : __send__("#{tag_type}_list")
22
+ end
23
+
24
+ def #{tag_type}_list_change
25
+ [changed_attributes['#{tag_type}_list'], __send__('#{tag_type}_list')] if changed_attributes.include?("#{tag_type}_list")
26
+ end
27
+
28
+ def #{tag_type}_list_changes
29
+ [changed_attributes['#{tag_type}_list'], __send__('#{tag_type}_list')] if changed_attributes.include?("#{tag_type}_list")
30
+ end
31
+ )
32
+
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -31,12 +31,16 @@ module ActsAsTaggableOn::Taggable
31
31
  module InstanceMethods
32
32
  def owner_tags_on(owner, context)
33
33
  if owner.nil?
34
- base_tags.where([%(#{ActsAsTaggableOn::Tagging.table_name}.context = ?), context.to_s]).all
34
+ scope = base_tags.where([%(#{ActsAsTaggableOn::Tagging.table_name}.context = ?), context.to_s])
35
35
  else
36
- base_tags.where([%(#{ActsAsTaggableOn::Tagging.table_name}.context = ? AND
37
- #{ActsAsTaggableOn::Tagging.table_name}.tagger_id = ? AND
38
- #{ActsAsTaggableOn::Tagging.table_name}.tagger_type = ?), context.to_s, owner.id, owner.class.to_s]).all
36
+ scope = base_tags.where([%(#{ActsAsTaggableOn::Tagging.table_name}.context = ? AND
37
+ #{ActsAsTaggableOn::Tagging.table_name}.tagger_id = ? AND
38
+ #{ActsAsTaggableOn::Tagging.table_name}.tagger_type = ?), context.to_s, owner.id, owner.class.to_s])
39
39
  end
40
+ # when preserving tag order, return tags in created order
41
+ # 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
40
44
  end
41
45
 
42
46
  def cached_owned_tag_list_on(context)
@@ -73,21 +77,38 @@ module ActsAsTaggableOn::Taggable
73
77
  def save_owned_tags
74
78
  tagging_contexts.each do |context|
75
79
  cached_owned_tag_list_on(context).each do |owner, tag_list|
80
+
76
81
  # Find existing tags or create non-existing tags:
77
- tag_list = ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name(tag_list.uniq)
82
+ tags = ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name(tag_list.uniq)
78
83
 
79
- owned_tags = owner_tags_on(owner, context)
80
- old_tags = owned_tags - tag_list
81
- new_tags = tag_list - owned_tags
84
+ # Tag objects for owned tags
85
+ owned_tags = owner_tags_on(owner, context)
86
+
87
+ # Tag maintenance based on whether preserving the created order of tags
88
+ 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)
96
+ else
97
+ # Delete discarded tags and create new tags
98
+ old_tags = owned_tags - tags
99
+ new_tags = tags - owned_tags
100
+ end
82
101
 
83
102
  # Find all taggings that belong to the taggable (self), are owned by the owner,
84
103
  # have the correct context, and are removed from the list.
85
- old_taggings = ActsAsTaggableOn::Tagging.where(:taggable_id => id, :taggable_type => self.class.base_class.to_s,
86
- :tagger_type => owner.class.to_s, :tagger_id => owner.id,
87
- :tag_id => old_tags, :context => context).all
104
+ if old_tags.present?
105
+ old_taggings = ActsAsTaggableOn::Tagging.where(:taggable_id => id, :taggable_type => self.class.base_class.to_s,
106
+ :tagger_type => owner.class.to_s, :tagger_id => owner.id,
107
+ :tag_id => old_tags, :context => context).all
108
+ end
88
109
 
110
+ # Destroy old taggings:
89
111
  if old_taggings.present?
90
- # Destroy old taggings:
91
112
  ActsAsTaggableOn::Tagging.destroy_all(:id => old_taggings.map(&:id))
92
113
  end
93
114
 
@@ -5,7 +5,7 @@ module ActsAsTaggableOn::Taggable
5
5
  base.extend ActsAsTaggableOn::Taggable::Related::ClassMethods
6
6
  base.initialize_acts_as_taggable_on_related
7
7
  end
8
-
8
+
9
9
  module ClassMethods
10
10
  def initialize_acts_as_taggable_on_related
11
11
  tag_types.map(&:to_s).each do |tag_type|
@@ -20,7 +20,7 @@ module ActsAsTaggableOn::Taggable
20
20
  end
21
21
  )
22
22
  end
23
-
23
+
24
24
  unless tag_types.empty?
25
25
  class_eval %(
26
26
  def find_matching_contexts(search_context, result_context, options = {})
@@ -31,43 +31,43 @@ module ActsAsTaggableOn::Taggable
31
31
  matching_contexts_for(search_context.to_s, result_context.to_s, klass, options)
32
32
  end
33
33
  )
34
- end
34
+ end
35
35
  end
36
-
36
+
37
37
  def acts_as_taggable_on(*args)
38
38
  super(*args)
39
39
  initialize_acts_as_taggable_on_related
40
40
  end
41
41
  end
42
-
42
+
43
43
  module InstanceMethods
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}.id != #{id} AND" if self.class == klass
48
-
47
+ exclude_self = "#{klass.table_name}.#{klass.primary_key} != #{id} AND" if [self.class.base_class, self.class].include? klass
48
+
49
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}.id) AS count",
50
+
51
+ klass.scoped({ :select => "#{klass.table_name}.*, COUNT(#{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key}) AS count",
52
52
  :from => "#{klass.table_name}, #{ActsAsTaggableOn::Tag.table_name}, #{ActsAsTaggableOn::Tagging.table_name}",
53
- :conditions => ["#{exclude_self} #{klass.table_name}.id = #{ActsAsTaggableOn::Tagging.table_name}.taggable_id AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = '#{klass.to_s}' AND #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.id AND #{ActsAsTaggableOn::Tag.table_name}.name IN (?) AND #{ActsAsTaggableOn::Tagging.table_name}.context = ?", tags_to_find, result_context],
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
54
  :group => group_columns,
55
55
  :order => "count DESC" }.update(options))
56
56
  end
57
-
57
+
58
58
  def related_tags_for(context, klass, options = {})
59
59
  tags_to_find = tags_on(context).collect { |t| t.name }
60
60
 
61
- exclude_self = "#{klass.table_name}.id != #{id} AND" if self.class == klass
61
+ exclude_self = "#{klass.table_name}.#{klass.primary_key} != #{id} AND" if [self.class.base_class, self.class].include? klass
62
62
 
63
- group_columns = ActsAsTaggableOn::Tag.using_postgresql? ? grouped_column_names_for(klass) : "#{klass.table_name}.#{klass.primary_key}"
63
+ group_columns = ActsAsTaggableOn::Tag.using_postgresql? ? grouped_column_names_for(klass) : "#{klass.table_name}.#{klass.primary_key}"
64
64
 
65
- klass.scoped({ :select => "#{klass.table_name}.*, COUNT(#{ActsAsTaggableOn::Tag.table_name}.id) AS count",
65
+ klass.scoped({ :select => "#{klass.table_name}.*, COUNT(#{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key}) AS count",
66
66
  :from => "#{klass.table_name}, #{ActsAsTaggableOn::Tag.table_name}, #{ActsAsTaggableOn::Tagging.table_name}",
67
- :conditions => ["#{exclude_self} #{klass.table_name}.id = #{ActsAsTaggableOn::Tagging.table_name}.taggable_id AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = '#{klass.to_s}' AND #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.id AND #{ActsAsTaggableOn::Tag.table_name}.name IN (?)", tags_to_find],
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
68
  :group => group_columns,
69
69
  :order => "count DESC" }.update(options))
70
70
  end
71
71
  end
72
72
  end
73
- end
73
+ end
@@ -1,8 +1,7 @@
1
1
  module ActsAsTaggableOn
2
2
  class Tag < ::ActiveRecord::Base
3
- include ActsAsTaggableOn::ActiveRecord::Backports if ::ActiveRecord::VERSION::MAJOR < 3
4
3
  include ActsAsTaggableOn::Utils
5
-
4
+
6
5
  attr_accessible :name
7
6
 
8
7
  ### ASSOCIATIONS:
@@ -13,23 +12,24 @@ module ActsAsTaggableOn
13
12
 
14
13
  validates_presence_of :name
15
14
  validates_uniqueness_of :name
15
+ validates_length_of :name, :maximum => 255
16
16
 
17
17
  ### SCOPES:
18
-
18
+
19
19
  def self.named(name)
20
- where(["name #{like_operator} ?", escape_like(name)])
20
+ where(["lower(name) = ?", name.downcase])
21
21
  end
22
-
22
+
23
23
  def self.named_any(list)
24
- where(list.map { |tag| sanitize_sql(["name #{like_operator} ?", escape_like(tag.to_s)]) }.join(" OR "))
24
+ where(list.map { |tag| sanitize_sql(["lower(name) = ?", tag.to_s.downcase]) }.join(" OR "))
25
25
  end
26
-
26
+
27
27
  def self.named_like(name)
28
- where(["name #{like_operator} ?", "%#{escape_like(name)}%"])
28
+ where(["name #{like_operator} ? ESCAPE '!'", "%#{escape_like(name)}%"])
29
29
  end
30
30
 
31
31
  def self.named_like_any(list)
32
- where(list.map { |tag| sanitize_sql(["name #{like_operator} ?", "%#{escape_like(tag.to_s)}%"]) }.join(" OR "))
32
+ where(list.map { |tag| sanitize_sql(["name #{like_operator} ? ESCAPE '!'", "%#{escape_like(tag.to_s)}%"]) }.join(" OR "))
33
33
  end
34
34
 
35
35
  ### CLASS METHODS:
@@ -44,7 +44,7 @@ module ActsAsTaggableOn
44
44
  return [] if list.empty?
45
45
 
46
46
  existing_tags = Tag.named_any(list).all
47
- new_tag_names = list.reject do |name|
47
+ new_tag_names = list.reject do |name|
48
48
  name = comparable_name(name)
49
49
  existing_tags.any? { |tag| comparable_name(tag.name) == name }
50
50
  end
@@ -66,16 +66,12 @@ module ActsAsTaggableOn
66
66
  def count
67
67
  read_attribute(:count).to_i
68
68
  end
69
-
70
- def safe_name
71
- name.gsub(/[^a-zA-Z0-9]/, '')
72
- end
73
-
69
+
74
70
  class << self
75
- private
71
+ private
76
72
  def comparable_name(str)
77
- RUBY_VERSION >= "1.9" ? str.downcase : str.mb_chars.downcase
73
+ str.mb_chars.downcase.to_s
78
74
  end
79
75
  end
80
76
  end
81
- end
77
+ end
@@ -1,14 +1,13 @@
1
+ require 'active_support/core_ext/module/delegation'
2
+
1
3
  module ActsAsTaggableOn
2
4
  class TagList < Array
3
- cattr_accessor :delimiter
4
- self.delimiter = ','
5
-
6
5
  attr_accessor :owner
7
6
 
8
7
  def initialize(*args)
9
8
  add(*args)
10
9
  end
11
-
10
+
12
11
  ##
13
12
  # Returns a new TagList using the given tag string.
14
13
  #
@@ -16,17 +15,16 @@ module ActsAsTaggableOn
16
15
  # tag_list = TagList.from("One , Two, Three")
17
16
  # tag_list # ["One", "Two", "Three"]
18
17
  def self.from(string)
19
- glue = delimiter.ends_with?(" ") ? delimiter : "#{delimiter} "
20
- string = string.join(glue) if string.respond_to?(:join)
18
+ string = string.join(ActsAsTaggableOn.glue) if string.respond_to?(:join)
21
19
 
22
20
  new.tap do |tag_list|
23
21
  string = string.to_s.dup
24
22
 
25
23
  # Parse the quoted tags
26
- string.gsub!(/(\A|#{delimiter})\s*"(.*?)"\s*(#{delimiter}\s*|\z)/) { tag_list << $2; $3 }
27
- string.gsub!(/(\A|#{delimiter})\s*'(.*?)'\s*(#{delimiter}\s*|\z)/) { tag_list << $2; $3 }
24
+ string.gsub!(/(\A|#{ActsAsTaggableOn.delimiter})\s*"(.*?)"\s*(#{ActsAsTaggableOn.delimiter}\s*|\z)/) { tag_list << $2; $3 }
25
+ string.gsub!(/(\A|#{ActsAsTaggableOn.delimiter})\s*'(.*?)'\s*(#{ActsAsTaggableOn.delimiter}\s*|\z)/) { tag_list << $2; $3 }
28
26
 
29
- tag_list.add(string.split(delimiter))
27
+ tag_list.add(string.split(ActsAsTaggableOn.delimiter))
30
28
  end
31
29
  end
32
30
 
@@ -69,16 +67,19 @@ module ActsAsTaggableOn
69
67
  tags.send(:clean!)
70
68
 
71
69
  tags.map do |name|
72
- name.include?(delimiter) ? "\"#{name}\"" : name
73
- end.join(delimiter.ends_with?(" ") ? delimiter : "#{delimiter} ")
70
+ name.include?(ActsAsTaggableOn.delimiter) ? "\"#{name}\"" : name
71
+ end.join(ActsAsTaggableOn.glue)
74
72
  end
75
73
 
76
74
  private
77
-
75
+
78
76
  # Remove whitespace, duplicates, and blanks.
79
77
  def clean!
80
78
  reject!(&:blank?)
81
79
  map!(&:strip)
80
+ map!(&:downcase) if ActsAsTaggableOn.force_lowercase
81
+ map!(&:parameterize) if ActsAsTaggableOn.force_parameterize
82
+
82
83
  uniq!
83
84
  end
84
85