acts-as-taggable-on 1.0.13 → 2.0.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 (40) hide show
  1. data/CHANGELOG +5 -2
  2. data/Gemfile +6 -0
  3. data/README.rdoc +61 -31
  4. data/Rakefile +46 -16
  5. data/VERSION +1 -1
  6. data/generators/acts_as_taggable_on_migration/acts_as_taggable_on_migration_generator.rb +7 -0
  7. data/generators/acts_as_taggable_on_migration/templates/migration.rb +29 -0
  8. data/lib/acts-as-taggable-on.rb +30 -7
  9. data/lib/acts_as_taggable_on/acts_as_taggable_on/cache.rb +53 -0
  10. data/lib/acts_as_taggable_on/acts_as_taggable_on/collection.rb +98 -0
  11. data/lib/acts_as_taggable_on/acts_as_taggable_on/core.rb +237 -0
  12. data/lib/acts_as_taggable_on/acts_as_taggable_on/ownership.rb +101 -0
  13. data/lib/acts_as_taggable_on/acts_as_taggable_on/related.rb +64 -0
  14. data/lib/acts_as_taggable_on/acts_as_taggable_on.rb +43 -373
  15. data/lib/acts_as_taggable_on/acts_as_tagger.rb +58 -43
  16. data/lib/acts_as_taggable_on/compatibility/Gemfile +6 -0
  17. data/lib/acts_as_taggable_on/compatibility/active_record_backports.rb +17 -0
  18. data/lib/acts_as_taggable_on/tag.rb +47 -8
  19. data/lib/acts_as_taggable_on/tag_list.rb +45 -45
  20. data/lib/acts_as_taggable_on/tagging.rb +17 -2
  21. data/lib/acts_as_taggable_on/tags_helper.rb +8 -2
  22. data/lib/generators/acts_as_taggable_on/migration/migration_generator.rb +31 -0
  23. data/lib/generators/acts_as_taggable_on/migration/templates/active_record/migration.rb +28 -0
  24. data/rails/init.rb +1 -7
  25. data/spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb +98 -53
  26. data/spec/acts_as_taggable_on/acts_as_tagger_spec.rb +46 -4
  27. data/spec/acts_as_taggable_on/tag_list_spec.rb +18 -0
  28. data/spec/acts_as_taggable_on/tag_spec.rb +66 -13
  29. data/spec/acts_as_taggable_on/taggable_spec.rb +142 -70
  30. data/spec/acts_as_taggable_on/tagger_spec.rb +73 -5
  31. data/spec/acts_as_taggable_on/tagging_spec.rb +18 -3
  32. data/spec/acts_as_taggable_on/tags_helper_spec.rb +1 -3
  33. data/spec/bm.rb +52 -0
  34. data/spec/models.rb +30 -0
  35. data/spec/schema.rb +13 -2
  36. data/spec/spec.opts +1 -2
  37. data/spec/spec_helper.rb +39 -34
  38. metadata +28 -8
  39. data/lib/acts_as_taggable_on/group_helper.rb +0 -12
  40. data/spec/acts_as_taggable_on/group_helper_spec.rb +0 -18
@@ -1,381 +1,51 @@
1
- module ActiveRecord
2
- module Acts
3
- module TaggableOn
4
- def self.included(base)
5
- base.extend(ClassMethods)
6
- end
7
-
8
- module ClassMethods
9
- def taggable?
10
- false
11
- end
12
-
13
- def acts_as_taggable
14
- acts_as_taggable_on :tags
15
- end
16
-
17
- def acts_as_taggable_on(*args)
18
- args.flatten! if args
19
- args.compact! if args
20
- for tag_type in args
21
- tag_type = tag_type.to_s
22
- # use aliased_join_table_name for context condition so that sphix can join multiple
23
- # tag references from same model without getting an ambiguous column error
24
- self.class_eval do
25
- has_many "#{tag_type.singularize}_taggings".to_sym, :as => :taggable, :dependent => :destroy,
26
- :include => :tag, :conditions => ['#{aliased_join_table_name rescue "taggings"}.context = ?',tag_type], :class_name => "Tagging"
27
- has_many "#{tag_type}".to_sym, :through => "#{tag_type.singularize}_taggings".to_sym, :source => :tag
28
- end
29
-
30
- self.class_eval <<-RUBY
31
- def self.taggable?
32
- true
33
- end
34
-
35
- def self.caching_#{tag_type.singularize}_list?
36
- caching_tag_list_on?("#{tag_type}")
37
- end
38
-
39
- def self.#{tag_type.singularize}_counts(options={})
40
- tag_counts_on('#{tag_type}',options)
41
- end
42
-
43
- def #{tag_type.singularize}_list
44
- tag_list_on('#{tag_type}')
45
- end
46
-
47
- def #{tag_type.singularize}_list=(new_tags)
48
- set_tag_list_on('#{tag_type}',new_tags)
49
- end
50
-
51
- def #{tag_type.singularize}_counts(options = {})
52
- tag_counts_on('#{tag_type}',options)
53
- end
54
-
55
- def #{tag_type}_from(owner)
56
- tag_list_on('#{tag_type}', owner)
57
- end
58
-
59
- def find_related_#{tag_type}(options = {})
60
- related_tags_for('#{tag_type}', self.class, options)
61
- end
62
- alias_method :find_related_on_#{tag_type}, :find_related_#{tag_type}
63
-
64
- def find_related_#{tag_type}_for(klass, options = {})
65
- related_tags_for('#{tag_type}', klass, options)
66
- end
67
-
68
- def find_matching_contexts(search_context, result_context, options = {})
69
- matching_contexts_for(search_context.to_s, result_context.to_s, self.class, options)
70
- end
71
-
72
- def find_matching_contexts_for(klass, search_context, result_context, options = {})
73
- matching_contexts_for(search_context.to_s, result_context.to_s, klass, options)
74
- end
75
-
76
- def top_#{tag_type}(limit = 10)
77
- tag_counts_on('#{tag_type}', :order => 'count desc', :limit => limit.to_i)
78
- end
79
-
80
- def self.top_#{tag_type}(limit = 10)
81
- tag_counts_on('#{tag_type}', :order => 'count desc', :limit => limit.to_i)
82
- end
83
- RUBY
84
- end
85
-
86
- if respond_to?(:tag_types)
87
- write_inheritable_attribute( :tag_types, (tag_types + args).uniq )
88
- else
89
- self.class_eval do
90
- write_inheritable_attribute(:tag_types, args.uniq)
91
- class_inheritable_reader :tag_types
92
-
93
- has_many :taggings, :as => :taggable, :dependent => :destroy, :include => :tag
94
- has_many :base_tags, :class_name => "Tag", :through => :taggings, :source => :tag
95
-
96
- attr_writer :custom_contexts
97
-
98
- before_save :save_cached_tag_list
99
- after_save :save_tags
100
-
101
- if respond_to?(:named_scope)
102
- named_scope :tagged_with, lambda{ |*args|
103
- find_options_for_find_tagged_with(*args)
104
- }
105
- end
106
- end
107
-
108
- include ActiveRecord::Acts::TaggableOn::InstanceMethods
109
- extend ActiveRecord::Acts::TaggableOn::SingletonMethods
110
- alias_method_chain :reload, :tag_list
111
- end
112
- end
113
- end
114
-
115
- module SingletonMethods
116
- include ActiveRecord::Acts::TaggableOn::GroupHelper
117
- # Pass either a tag string, or an array of strings or tags
118
- #
119
- # Options:
120
- # :exclude - Find models that are not tagged with the given tags
121
- # :match_all - Find models that match all of the given tags, not just one
122
- # :conditions - A piece of SQL conditions to add to the query
123
- # :on - scopes the find to a context
124
- def find_tagged_with(*args)
125
- options = find_options_for_find_tagged_with(*args)
126
- options.blank? ? [] : find(:all,options)
127
- end
128
-
129
- def caching_tag_list_on?(context)
130
- column_names.include?("cached_#{context.to_s.singularize}_list")
131
- end
132
-
133
- def tag_counts_on(context, options = {})
134
- Tag.find(:all, find_options_for_tag_counts(options.merge({:on => context.to_s})))
135
- end
136
-
137
- def all_tag_counts(options = {})
138
- Tag.find(:all, find_options_for_tag_counts(options))
139
- end
140
-
141
- def find_options_for_find_tagged_with(tags, options = {})
142
- tags = TagList.from(tags)
143
-
144
- return {} if tags.empty?
145
-
146
- joins = []
147
- conditions = []
148
-
149
- context = options.delete(:on)
150
-
151
-
152
- if options.delete(:exclude)
153
- tags_conditions = tags.map { |t| sanitize_sql(["#{Tag.table_name}.name LIKE ?", t]) }.join(" OR ")
154
- conditions << "#{table_name}.#{primary_key} NOT IN (SELECT #{Tagging.table_name}.taggable_id FROM #{Tagging.table_name} JOIN #{Tag.table_name} ON #{Tagging.table_name}.tag_id = #{Tag.table_name}.id AND (#{tags_conditions}) WHERE #{Tagging.table_name}.taggable_type = #{quote_value(base_class.name)})"
155
-
156
- else
157
- tags.each do |tag|
158
- safe_tag = tag.gsub(/[^a-zA-Z0-9]/, '')
159
- prefix = "#{safe_tag}_#{rand(1024)}"
160
-
161
- taggings_alias = "#{table_name}_taggings_#{prefix}"
162
- tags_alias = "#{table_name}_tags_#{prefix}"
163
-
164
- tagging_join = "JOIN #{Tagging.table_name} #{taggings_alias}" +
165
- " ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
166
- " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}"
167
- tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
168
-
169
- tag_join = "JOIN #{Tag.table_name} #{tags_alias}" +
170
- " ON #{tags_alias}.id = #{taggings_alias}.tag_id" +
171
- " AND " + sanitize_sql(["#{tags_alias}.name like ?", tag])
172
-
173
- joins << tagging_join
174
- joins << tag_join
175
- end
176
- end
177
-
178
- taggings_alias, tags_alias = "#{table_name}_taggings_group", "#{table_name}_tags_group"
179
-
180
- if options.delete(:match_all)
181
- joins << "LEFT OUTER JOIN #{Tagging.table_name} #{taggings_alias}" +
182
- " ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
183
- " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}"
184
-
185
- group = "#{grouped_column_names_for(self)} HAVING COUNT(#{taggings_alias}.taggable_id) = #{tags.size}"
186
- end
187
-
188
- { :joins => joins.join(" "),
189
- :group => group,
190
- :conditions => conditions.join(" AND ") }.update(options)
191
- end
192
-
193
- # Calculate the tag counts for all tags.
194
- #
195
- # Options:
196
- # :start_at - Restrict the tags to those created after a certain time
197
- # :end_at - Restrict the tags to those created before a certain time
198
- # :conditions - A piece of SQL conditions to add to the query
199
- # :limit - The maximum number of tags to return
200
- # :order - A piece of SQL to order by. Eg 'tags.count desc' or 'taggings.created_at desc'
201
- # :at_least - Exclude tags with a frequency less than the given value
202
- # :at_most - Exclude tags with a frequency greater than the given value
203
- # :on - Scope the find to only include a certain context
204
- def find_options_for_tag_counts(options = {})
205
- options.assert_valid_keys :start_at, :end_at, :conditions, :at_least, :at_most, :order, :limit, :on, :id
206
-
207
- scope = scope(:find)
208
- start_at = sanitize_sql(["#{Tagging.table_name}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
209
- end_at = sanitize_sql(["#{Tagging.table_name}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
210
-
211
- taggable_type = sanitize_sql(["#{Tagging.table_name}.taggable_type = ?", base_class.name])
212
- taggable_id = sanitize_sql(["#{Tagging.table_name}.taggable_id = ?", options.delete(:id)]) if options[:id]
213
- options[:conditions] = sanitize_sql(options[:conditions]) if options[:conditions]
214
-
215
- conditions = [
216
- taggable_type,
217
- taggable_id,
218
- options[:conditions],
219
- start_at,
220
- end_at
221
- ]
222
-
223
- conditions = conditions.compact.join(' AND ')
224
- conditions = merge_conditions(conditions, scope[:conditions]) if scope
225
-
226
- joins = ["LEFT OUTER JOIN #{Tagging.table_name} ON #{Tag.table_name}.id = #{Tagging.table_name}.tag_id"]
227
- joins << sanitize_sql(["AND #{Tagging.table_name}.context = ?",options.delete(:on).to_s]) unless options[:on].nil?
228
-
229
- joins << " INNER JOIN #{table_name} ON #{table_name}.#{primary_key} = #{Tagging.table_name}.taggable_id"
230
- unless self.descends_from_active_record?
231
- # Current model is STI descendant, so add type checking to the join condition
232
- joins << " AND #{table_name}.#{self.inheritance_column} = '#{self.name}'"
233
- end
234
-
235
- joins << scope[:joins] if scope && scope[:joins]
236
-
237
- at_least = sanitize_sql(['COUNT(*) >= ?', options.delete(:at_least)]) if options[:at_least]
238
- at_most = sanitize_sql(['COUNT(*) <= ?', options.delete(:at_most)]) if options[:at_most]
239
- having = [at_least, at_most].compact.join(' AND ')
240
- group_by = "#{grouped_column_names_for(Tag)} HAVING COUNT(*) > 0"
241
- group_by << " AND #{having}" unless having.blank?
242
-
243
- { :select => "#{Tag.table_name}.*, COUNT(*) AS count",
244
- :joins => joins.join(" "),
245
- :conditions => conditions,
246
- :group => group_by,
247
- :limit => options[:limit],
248
- :order => options[:order]
249
- }
250
- end
251
-
252
- def is_taggable?
253
- true
254
- end
255
- end
256
-
257
- module InstanceMethods
258
- include ActiveRecord::Acts::TaggableOn::GroupHelper
259
-
260
- def custom_contexts
261
- @custom_contexts ||= []
262
- end
263
-
264
- def is_taggable?
265
- self.class.is_taggable?
266
- end
267
-
268
- def add_custom_context(value)
269
- custom_contexts << value.to_s unless custom_contexts.include?(value.to_s) or self.class.tag_types.map(&:to_s).include?(value.to_s)
270
- end
271
-
272
- def tag_list_on(context, owner=nil)
273
- var_name = context.to_s.singularize + "_list"
274
- add_custom_context(context)
275
- return instance_variable_get("@#{var_name}") unless instance_variable_get("@#{var_name}").nil?
276
-
277
- if !owner && self.class.caching_tag_list_on?(context) and !(cached_value = cached_tag_list_on(context)).nil?
278
- instance_variable_set("@#{var_name}", TagList.from(self["cached_#{var_name}"]))
279
- else
280
- instance_variable_set("@#{var_name}", TagList.new(*tags_on(context, owner).map(&:name)))
281
- end
282
- end
283
-
284
- def tags_on(context, owner=nil)
285
- if owner
286
- opts = {:conditions => ["#{Tagging.table_name}.context = ? AND #{Tagging.table_name}.tagger_id = ? AND #{Tagging.table_name}.tagger_type = ?",
287
- context.to_s, owner.id, owner.class.to_s]}
288
- else
289
- opts = {:conditions => ["#{Tagging.table_name}.context = ?", context.to_s]}
290
- end
291
- base_tags.find(:all, opts)
292
- end
293
-
294
- def cached_tag_list_on(context)
295
- self["cached_#{context.to_s.singularize}_list"]
296
- end
297
-
298
- def set_tag_list_on(context,new_list, tagger=nil)
299
- instance_variable_set("@#{context.to_s.singularize}_list", TagList.from_owner(tagger, new_list))
300
- add_custom_context(context)
301
- end
302
-
303
- def tag_counts_on(context, options={})
304
- self.class.tag_counts_on(context, options.merge(:id => self.id))
305
- end
306
-
307
- def related_tags_for(context, klass, options = {})
308
- search_conditions = related_search_options(context, klass, options)
309
-
310
- klass.find(:all, search_conditions)
311
- end
312
-
313
- def related_search_options(context, klass, options = {})
314
- tags_to_find = self.tags_on(context).collect { |t| t.name }
315
-
316
- exclude_self = "#{klass.table_name}.id != #{self.id} AND" if self.class == klass
317
-
318
- { :select => "#{klass.table_name}.*, COUNT(#{Tag.table_name}.id) AS count",
319
- :from => "#{klass.table_name}, #{Tag.table_name}, #{Tagging.table_name}",
320
- :conditions => ["#{exclude_self} #{klass.table_name}.id = #{Tagging.table_name}.taggable_id AND #{Tagging.table_name}.taggable_type = '#{klass.to_s}' AND #{Tagging.table_name}.tag_id = #{Tag.table_name}.id AND #{Tag.table_name}.name IN (?)", tags_to_find],
321
- :group => grouped_column_names_for(klass),
322
- :order => "count DESC"
323
- }.update(options)
324
- end
325
-
326
- def matching_contexts_for(search_context, result_context, klass, options = {})
327
- search_conditions = matching_context_search_options(search_context, result_context, klass, options)
328
-
329
- klass.find(:all, search_conditions)
330
- end
331
-
332
- def matching_context_search_options(search_context, result_context, klass, options = {})
333
- tags_to_find = self.tags_on(search_context).collect { |t| t.name }
1
+ module ActsAsTaggableOn
2
+ module Taggable
3
+ def taggable?
4
+ false
5
+ end
334
6
 
335
- exclude_self = "#{klass.table_name}.id != #{self.id} AND" if self.class == klass
7
+ ##
8
+ # This is an alias for calling <tt>acts_as_taggable_on :tags</tt>.
9
+ #
10
+ # Example:
11
+ # class Book < ActiveRecord::Base
12
+ # acts_as_taggable
13
+ # end
14
+ def acts_as_taggable
15
+ acts_as_taggable_on :tags
16
+ end
336
17
 
337
- { :select => "#{klass.table_name}.*, COUNT(#{Tag.table_name}.id) AS count",
338
- :from => "#{klass.table_name}, #{Tag.table_name}, #{Tagging.table_name}",
339
- :conditions => ["#{exclude_self} #{klass.table_name}.id = #{Tagging.table_name}.taggable_id AND #{Tagging.table_name}.taggable_type = '#{klass.to_s}' AND #{Tagging.table_name}.tag_id = #{Tag.table_name}.id AND #{Tag.table_name}.name IN (?) AND #{Tagging.table_name}.context = ?", tags_to_find, result_context],
340
- :group => grouped_column_names_for(klass),
341
- :order => "count DESC"
342
- }.update(options)
343
- end
18
+ ##
19
+ # Make a model taggable on specified contexts.
20
+ #
21
+ # @param [Array] tag_types An array of taggable contexts
22
+ #
23
+ # Example:
24
+ # class User < ActiveRecord::Base
25
+ # acts_as_taggable_on :languages, :skills
26
+ # end
27
+ def acts_as_taggable_on(*tag_types)
28
+ tag_types = tag_types.to_a.flatten.compact.map(&:to_sym)
29
+
30
+ if taggable?
31
+ write_inheritable_attribute(:tag_types, (self.tag_types + tag_types).uniq)
32
+ else
33
+ write_inheritable_attribute(:tag_types, tag_types)
34
+ class_inheritable_reader(:tag_types)
344
35
 
345
- def save_cached_tag_list
346
- self.class.tag_types.map(&:to_s).each do |tag_type|
347
- if self.class.send("caching_#{tag_type.singularize}_list?")
348
- self["cached_#{tag_type.singularize}_list"] = send("#{tag_type.singularize}_list").to_s
349
- end
350
- end
351
- end
36
+ class_eval do
37
+ has_many :taggings, :as => :taggable, :dependent => :destroy, :include => :tag
38
+ has_many :base_tags, :class_name => "Tag", :through => :taggings, :source => :tag
352
39
 
353
- def save_tags
354
- (custom_contexts + self.class.tag_types.map(&:to_s)).each do |tag_type|
355
- next unless instance_variable_get("@#{tag_type.singularize}_list")
356
- owner = instance_variable_get("@#{tag_type.singularize}_list").owner
357
- new_tag_names = instance_variable_get("@#{tag_type.singularize}_list") - tags_on(tag_type).map(&:name)
358
- old_tags = tags_on(tag_type, owner).reject { |tag| instance_variable_get("@#{tag_type.singularize}_list").include?(tag.name) }
359
-
360
- self.class.transaction do
361
- base_tags.delete(*old_tags) if old_tags.any?
362
- new_tag_names.each do |new_tag_name|
363
- new_tag = Tag.find_or_create_with_like_by_name(new_tag_name)
364
- Tagging.create(:tag_id => new_tag.id, :context => tag_type,
365
- :taggable => self, :tagger => owner)
366
- end
367
- end
40
+ def self.taggable?
41
+ true
368
42
  end
369
-
370
- true
371
- end
372
-
373
- def reload_with_tag_list(*args)
374
- self.class.tag_types.each do |tag_type|
375
- self.instance_variable_set("@#{tag_type.to_s.singularize}_list", nil)
376
- end
377
-
378
- reload_without_tag_list(*args)
43
+
44
+ include ActsAsTaggableOn::Taggable::Core
45
+ include ActsAsTaggableOn::Taggable::Collection
46
+ include ActsAsTaggableOn::Taggable::Cache
47
+ include ActsAsTaggableOn::Taggable::Ownership
48
+ include ActsAsTaggableOn::Taggable::Related
379
49
  end
380
50
  end
381
51
  end
@@ -1,52 +1,67 @@
1
- module ActiveRecord
2
- module Acts
3
- module Tagger
4
- def self.included(base)
5
- base.extend ClassMethods
6
- end
7
-
8
- module ClassMethods
9
- def acts_as_tagger(opts={})
10
- has_many :owned_taggings, opts.merge(:as => :tagger, :dependent => :destroy,
1
+ module ActsAsTaggableOn
2
+ module Tagger
3
+ def self.included(base)
4
+ base.extend ClassMethods
5
+ end
6
+
7
+ module ClassMethods
8
+ ##
9
+ # Make a model a tagger. This allows an instance of a model to claim ownership
10
+ # of tags.
11
+ #
12
+ # Example:
13
+ # class User < ActiveRecord::Base
14
+ # acts_as_tagger
15
+ # end
16
+ def acts_as_tagger(opts={})
17
+ class_eval do
18
+ has_many :owned_taggings, opts.merge(:as => :tagger, :dependent => :destroy,
11
19
  :include => :tag, :class_name => "Tagging")
12
20
  has_many :owned_tags, :through => :owned_taggings, :source => :tag, :uniq => true
13
- include ActiveRecord::Acts::Tagger::InstanceMethods
14
- extend ActiveRecord::Acts::Tagger::SingletonMethods
15
- end
16
-
17
- def is_tagger?
18
- false
19
21
  end
22
+
23
+ include ActsAsTaggableOn::Tagger::InstanceMethods
24
+ extend ActsAsTaggableOn::Tagger::SingletonMethods
20
25
  end
21
-
22
- module InstanceMethods
23
- def self.included(base)
24
- end
25
-
26
- def tag(taggable, opts={})
27
- opts.reverse_merge!(:force => true)
28
-
29
- return false unless taggable.respond_to?(:is_taggable?) && taggable.is_taggable?
30
- raise "You need to specify a tag context using :on" unless opts.has_key?(:on)
31
- raise "You need to specify some tags using :with" unless opts.has_key?(:with)
32
- raise "No context :#{opts[:on]} defined in #{taggable.class.to_s}" unless
33
- ( opts[:force] || taggable.tag_types.include?(opts[:on]) )
34
-
35
- taggable.set_tag_list_on(opts[:on].to_s, opts[:with], self)
36
- taggable.save
37
- end
38
-
39
- def is_tagger?
40
- self.class.is_tagger?
41
- end
26
+
27
+ def is_tagger?
28
+ false
42
29
  end
43
-
44
- module SingletonMethods
45
- def is_tagger?
46
- true
47
- end
30
+ end
31
+
32
+ module InstanceMethods
33
+ ##
34
+ # Tag a taggable model with tags that are owned by the tagger.
35
+ #
36
+ # @param taggable The object that will be tagged
37
+ # @param [Hash] options An hash with options. Available options are:
38
+ # * <tt>:with</tt> - The tags that you want to
39
+ # * <tt>:on</tt> - The context on which you want to tag
40
+ #
41
+ # Example:
42
+ # @user.tag(@photo, :with => "paris, normandy", :on => :locations)
43
+ def tag(taggable, opts={})
44
+ opts.reverse_merge!(:force => true)
45
+
46
+ return false unless taggable.respond_to?(:is_taggable?) && taggable.is_taggable?
47
+
48
+ raise "You need to specify a tag context using :on" unless opts.has_key?(:on)
49
+ raise "You need to specify some tags using :with" unless opts.has_key?(:with)
50
+ raise "No context :#{opts[:on]} defined in #{taggable.class.to_s}" unless (opts[:force] || taggable.tag_types.include?(opts[:on]))
51
+
52
+ taggable.set_owner_tag_list_on(self, opts[:on].to_s, opts[:with])
53
+ taggable.save
54
+ end
55
+
56
+ def is_tagger?
57
+ self.class.is_tagger?
58
+ end
59
+ end
60
+
61
+ module SingletonMethods
62
+ def is_tagger?
63
+ true
48
64
  end
49
-
50
65
  end
51
66
  end
52
67
  end
@@ -0,0 +1,6 @@
1
+ source :gemcutter
2
+
3
+ # Rails 2.3
4
+ gem 'rails', '2.3.5'
5
+ gem 'rspec', '1.3.0', :require => 'spec'
6
+ gem 'sqlite3-ruby', '1.2.5', :require => 'sqlite3'
@@ -0,0 +1,17 @@
1
+ module ActsAsTaggableOn
2
+ module ActiveRecord
3
+ module Backports
4
+ def self.included(base)
5
+ base.class_eval do
6
+ named_scope :where, lambda { |conditions| { :conditions => conditions } }
7
+ named_scope :joins, lambda { |joins| { :joins => joins } }
8
+ named_scope :group, lambda { |group| { :group => group } }
9
+ named_scope :order, lambda { |order| { :order => order } }
10
+ named_scope :select, lambda { |select| { :select => select } }
11
+ named_scope :limit, lambda { |limit| { :limit => limit } }
12
+ named_scope :readonly, lambda { |readonly| { :readonly => readonly } }
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -1,26 +1,65 @@
1
1
  class Tag < ActiveRecord::Base
2
- has_many :taggings, :dependent => :destroy
2
+ include ActsAsTaggableOn::ActiveRecord::Backports if ActiveRecord::VERSION::MAJOR < 3
3
3
 
4
+ attr_accessible :name
5
+
6
+ ### ASSOCIATIONS:
7
+
8
+ has_many :taggings, :dependent => :destroy
9
+
10
+ ### VALIDATIONS:
11
+
4
12
  validates_presence_of :name
5
13
  validates_uniqueness_of :name
14
+
15
+ ### SCOPES:
16
+
17
+ def self.named(name)
18
+ where(["name LIKE ?", name])
19
+ end
6
20
 
7
- named_scope :named, lambda { |name| { :conditions => ["name = ?", name] } }
8
- named_scope :named_like, lambda { |name| { :conditions => ["name LIKE ?", "%#{name}%"] } }
21
+ def self.named_any(list)
22
+ where(list.map { |tag| sanitize_sql(["name LIKE ?", tag.to_s]) }.join(" OR "))
23
+ end
9
24
 
10
- # LIKE is used for cross-database case-insensitivity
25
+ def self.named_like(name)
26
+ where(["name LIKE ?", "%#{name}%"])
27
+ end
28
+
29
+ def self.named_like_any(list)
30
+ where(list.map { |tag| sanitize_sql(["name LIKE ?", "%#{tag.to_s}%"]) }.join(" OR "))
31
+ end
32
+
33
+ ### CLASS METHODS:
34
+
11
35
  def self.find_or_create_with_like_by_name(name)
12
- find(:first, :conditions => ["name LIKE ?", name]) || create(:name => name)
36
+ named_like(name).first || create(:name => name)
13
37
  end
14
-
38
+
39
+ def self.find_or_create_all_with_like_by_name(*list)
40
+ list = [list].flatten
41
+
42
+ return [] if list.empty?
43
+
44
+ existing_tags = Tag.named_any(list).all
45
+ new_tag_names = list.reject { |name| existing_tags.any? { |tag| tag.name.mb_chars.downcase == name.mb_chars.downcase } }
46
+ created_tags = new_tag_names.map { |name| Tag.create(:name => name) }
47
+
48
+ existing_tags + created_tags
49
+ end
50
+
51
+ ### INSTANCE METHODS:
52
+
15
53
  def ==(object)
16
54
  super || (object.is_a?(Tag) && name == object.name)
17
55
  end
18
-
56
+
19
57
  def to_s
20
58
  name
21
59
  end
22
-
60
+
23
61
  def count
24
62
  read_attribute(:count).to_i
25
63
  end
64
+
26
65
  end