acts-as-taggable-on 1.0.12 → 1.1.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.
data/README.rdoc CHANGED
@@ -137,6 +137,46 @@ Tags can have owners:
137
137
  @some_user.owned_taggings
138
138
  @some_user.owned_tags
139
139
  @some_photo.locations_from(@some_user)
140
+
141
+ === Tag cloud calculations
142
+
143
+ To construct tag clouds, the frequency of each tag needs to be calculated.
144
+ Because we specified +acts_as_taggable_on+ on the <tt>User</tt> class, we can
145
+ get a calculation of all the tag counts by using <tt>User.tag_counts_on(:customs)</tt>. But what if we wanted a tag count for
146
+ an single user's posts? To achieve this we call tag_counts on the association:
147
+
148
+ User.find(:first).posts.tag_counts_on(:tags)
149
+
150
+ A helper is included to assist with generating tag clouds.
151
+
152
+ Here is an example that generates a tag cloud.
153
+
154
+ Helper:
155
+
156
+ module PostsHelper
157
+ include TagsHelper
158
+ end
159
+
160
+ Controller:
161
+
162
+ class PostController < ApplicationController
163
+ def tag_cloud
164
+ @tags = Post.tag_counts_on(:tags)
165
+ end
166
+ end
167
+
168
+ View:
169
+
170
+ <% tag_cloud(@tags, %w(css1 css2 css3 css4)) do |tag, css_class| %>
171
+ <%= link_to tag.name, { :action => :tag, :id => tag.name }, :class => css_class %>
172
+ <% end %>
173
+
174
+ CSS:
175
+
176
+ .css1 { font-size: 1.0em; }
177
+ .css2 { font-size: 1.2em; }
178
+ .css3 { font-size: 1.4em; }
179
+ .css4 { font-size: 1.6em; }
140
180
 
141
181
  == Contributors
142
182
 
@@ -154,5 +194,6 @@ Tags can have owners:
154
194
  * slainer68 - STI fix
155
195
  * harrylove - migration instructions and fix-ups
156
196
  * lawrencepit - cached tag work
197
+ * sobrinho - fixed tag_cloud helper
157
198
 
158
199
  Copyright (c) 2007-2009 Michael Bleigh (http://mbleigh.com/) and Intridea Inc. (http://intridea.com/), released under the MIT license
data/Rakefile CHANGED
@@ -9,7 +9,7 @@ begin
9
9
  gemspec.email = "michael@intridea.com"
10
10
  gemspec.homepage = "http://github.com/mbleigh/acts-as-taggable-on"
11
11
  gemspec.authors = ["Michael Bleigh"]
12
- gemspec.files = FileList["[A-Z]*", "{lib,spec,rails}/**/*"] - FileList["**/*.log"]
12
+ gemspec.files = FileList["[A-Z]*", "{generators,lib,spec,rails}/**/*"] - FileList["**/*.log"]
13
13
  end
14
14
  Jeweler::GemcutterTasks.new
15
15
  rescue LoadError
@@ -21,3 +21,9 @@ task :default => :spec
21
21
  Spec::Rake::SpecTask.new do |t|
22
22
  t.spec_files = FileList["spec/**/*_spec.rb"]
23
23
  end
24
+
25
+ Spec::Rake::SpecTask.new('rcov') do |t|
26
+ t.spec_files = FileList["spec/**/*_spec.rb"]
27
+ t.rcov = true
28
+ t.rcov_opts = ['--exclude', 'spec']
29
+ end
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.0.12
1
+ 1.1.1
@@ -0,0 +1,7 @@
1
+ class ActsAsTaggableOnMigrationGenerator < Rails::Generator::Base
2
+ def manifest
3
+ record do |m|
4
+ m.migration_template 'migration.rb', 'db/migrate', :migration_file_name => "acts_as_taggable_on_migration"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,29 @@
1
+ class ActsAsTaggableOnMigration < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :tags do |t|
4
+ t.column :name, :string
5
+ end
6
+
7
+ create_table :taggings do |t|
8
+ t.column :tag_id, :integer
9
+ t.column :taggable_id, :integer
10
+ t.column :tagger_id, :integer
11
+ t.column :tagger_type, :string
12
+
13
+ # You should make sure that the column created is
14
+ # long enough to store the required class names.
15
+ t.column :taggable_type, :string
16
+ t.column :context, :string
17
+
18
+ t.column :created_at, :datetime
19
+ end
20
+
21
+ add_index :taggings, :tag_id
22
+ add_index :taggings, [:taggable_id, :taggable_type, :context]
23
+ end
24
+
25
+ def self.down
26
+ drop_table :taggings
27
+ drop_table :tags
28
+ end
29
+ end
@@ -19,15 +19,15 @@ module ActiveRecord
19
19
  args.compact! if args
20
20
  for tag_type in args
21
21
  tag_type = tag_type.to_s
22
- # use aliased_join_table_name for context condition so that sphix can join multiple
22
+ # use aliased_join_table_name for context condition so that sphinx can join multiple
23
23
  # tag references from same model without getting an ambiguous column error
24
- self.class_eval do
24
+ class_eval do
25
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"
26
+ :include => :tag, :conditions => ['#{aliased_join_table_name || Tagging.table_name rescue Tagging.table_name}.context = ?',tag_type], :class_name => "Tagging"
27
27
  has_many "#{tag_type}".to_sym, :through => "#{tag_type.singularize}_taggings".to_sym, :source => :tag
28
28
  end
29
29
 
30
- self.class_eval <<-RUBY
30
+ class_eval <<-RUBY
31
31
  def self.taggable?
32
32
  true
33
33
  end
@@ -65,6 +65,14 @@ module ActiveRecord
65
65
  related_tags_for('#{tag_type}', klass, options)
66
66
  end
67
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
+
68
76
  def top_#{tag_type}(limit = 10)
69
77
  tag_counts_on('#{tag_type}', :order => 'count desc', :limit => limit.to_i)
70
78
  end
@@ -74,11 +82,10 @@ module ActiveRecord
74
82
  end
75
83
  RUBY
76
84
  end
77
-
78
85
  if respond_to?(:tag_types)
79
86
  write_inheritable_attribute( :tag_types, (tag_types + args).uniq )
80
87
  else
81
- self.class_eval do
88
+ class_eval do
82
89
  write_inheritable_attribute(:tag_types, args.uniq)
83
90
  class_inheritable_reader :tag_types
84
91
 
@@ -102,10 +109,6 @@ module ActiveRecord
102
109
  alias_method_chain :reload, :tag_list
103
110
  end
104
111
  end
105
-
106
- def is_taggable?
107
- false
108
- end
109
112
  end
110
113
 
111
114
  module SingletonMethods
@@ -113,6 +116,7 @@ module ActiveRecord
113
116
  # Pass either a tag string, or an array of strings or tags
114
117
  #
115
118
  # Options:
119
+ # :any - find models that match any of the given tags
116
120
  # :exclude - Find models that are not tagged with the given tags
117
121
  # :match_all - Find models that match all of the given tags, not just one
118
122
  # :conditions - A piece of SQL conditions to add to the query
@@ -135,9 +139,9 @@ module ActiveRecord
135
139
  end
136
140
 
137
141
  def find_options_for_find_tagged_with(tags, options = {})
138
- tags = TagList.from(tags)
142
+ tag_list = TagList.from(tags)
139
143
 
140
- return {} if tags.empty?
144
+ return {} if tag_list.empty?
141
145
 
142
146
  joins = []
143
147
  conditions = []
@@ -146,28 +150,30 @@ module ActiveRecord
146
150
 
147
151
 
148
152
  if options.delete(:exclude)
149
- tags_conditions = tags.map { |t| sanitize_sql(["#{Tag.table_name}.name LIKE ?", t]) }.join(" OR ")
153
+ tags_conditions = tag_list.map { |t| sanitize_sql(["#{Tag.table_name}.name LIKE ?", t]) }.join(" OR ")
150
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)})"
151
155
 
156
+ elsif options.delete(:any)
157
+ tags_conditions = tag_list.map { |t| sanitize_sql(["#{Tag.table_name}.name LIKE ?", t]) }.join(" OR ")
158
+ conditions << "#{table_name}.#{primary_key} 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)})"
159
+
152
160
  else
161
+ tags = Tag.named_like_any(tag_list)
162
+ return { :conditions => "1 = 0" } unless tags.length == tag_list.length
163
+
153
164
  tags.each do |tag|
154
- safe_tag = tag.gsub(/[^a-zA-Z0-9]/, '')
165
+ safe_tag = tag.name.gsub(/[^a-zA-Z0-9]/, '')
155
166
  prefix = "#{safe_tag}_#{rand(1024)}"
156
167
 
157
168
  taggings_alias = "#{table_name}_taggings_#{prefix}"
158
- tags_alias = "#{table_name}_tags_#{prefix}"
159
169
 
160
170
  tagging_join = "JOIN #{Tagging.table_name} #{taggings_alias}" +
161
171
  " ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
162
- " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}"
172
+ " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}" +
173
+ " AND #{taggings_alias}.tag_id = #{tag.id}"
163
174
  tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
164
175
 
165
- tag_join = "JOIN #{Tag.table_name} #{tags_alias}" +
166
- " ON #{tags_alias}.id = #{taggings_alias}.tag_id" +
167
- " AND " + sanitize_sql(["#{tags_alias}.name like ?", tag])
168
-
169
176
  joins << tagging_join
170
- joins << tag_join
171
177
  end
172
178
  end
173
179
 
@@ -183,7 +189,8 @@ module ActiveRecord
183
189
 
184
190
  { :joins => joins.join(" "),
185
191
  :group => group,
186
- :conditions => conditions.join(" AND ") }.update(options)
192
+ :conditions => conditions.join(" AND "),
193
+ :readonly => false }.update(options)
187
194
  end
188
195
 
189
196
  # Calculate the tag counts for all tags.
@@ -221,14 +228,32 @@ module ActiveRecord
221
228
 
222
229
  joins = ["LEFT OUTER JOIN #{Tagging.table_name} ON #{Tag.table_name}.id = #{Tagging.table_name}.tag_id"]
223
230
  joins << sanitize_sql(["AND #{Tagging.table_name}.context = ?",options.delete(:on).to_s]) unless options[:on].nil?
224
-
225
231
  joins << " INNER JOIN #{table_name} ON #{table_name}.#{primary_key} = #{Tagging.table_name}.taggable_id"
226
- unless self.descends_from_active_record?
232
+
233
+ unless descends_from_active_record?
227
234
  # Current model is STI descendant, so add type checking to the join condition
228
- joins << " AND #{table_name}.#{self.inheritance_column} = '#{self.name}'"
235
+ joins << " AND #{table_name}.#{inheritance_column} = '#{name}'"
229
236
  end
230
237
 
231
- joins << scope[:joins] if scope && scope[:joins]
238
+ # Based on a proposed patch by donV to ActiveRecord Base
239
+ # This is needed because merge_joins and construct_join are private in ActiveRecord Base
240
+ if scope && scope[:joins]
241
+ case scope[:joins]
242
+ when Array
243
+ scope_joins = scope[:joins].flatten
244
+ strings = scope_joins.select{|j| j.is_a? String}
245
+ joins << strings.join(' ') + " "
246
+ symbols = scope_joins - strings
247
+ join_dependency = ActiveRecord::Associations::ClassMethods::InnerJoinDependency.new(self, symbols, nil)
248
+ joins << " #{join_dependency.join_associations.collect { |assoc| assoc.association_join }.join} "
249
+ joins.flatten!
250
+ when Symbol, Hash
251
+ join_dependency = ActiveRecord::Associations::ClassMethods::InnerJoinDependency.new(self, scope[:joins], nil)
252
+ joins << " #{join_dependency.join_associations.collect { |assoc| assoc.association_join }.join} "
253
+ when String
254
+ joins << scope[:joins]
255
+ end
256
+ end
232
257
 
233
258
  at_least = sanitize_sql(['COUNT(*) >= ?', options.delete(:at_least)]) if options[:at_least]
234
259
  at_most = sanitize_sql(['COUNT(*) <= ?', options.delete(:at_most)]) if options[:at_most]
@@ -253,10 +278,6 @@ module ActiveRecord
253
278
  module InstanceMethods
254
279
  include ActiveRecord::Acts::TaggableOn::GroupHelper
255
280
 
256
- def tag_types
257
- self.class.tag_types
258
- end
259
-
260
281
  def custom_contexts
261
282
  @custom_contexts ||= []
262
283
  end
@@ -264,29 +285,40 @@ module ActiveRecord
264
285
  def is_taggable?
265
286
  self.class.is_taggable?
266
287
  end
267
-
288
+
268
289
  def add_custom_context(value)
269
290
  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
291
  end
271
292
 
272
- def tag_list_on(context, owner=nil)
273
- var_name = context.to_s.singularize + "_list"
293
+ def tag_list_on(context, owner = nil)
274
294
  add_custom_context(context)
275
- return instance_variable_get("@#{var_name}") unless instance_variable_get("@#{var_name}").nil?
276
-
295
+ cache = tag_list_cache_on(context)
296
+ return owner ? cache[owner] : cache[owner] if cache[owner]
297
+
277
298
  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}"]))
299
+ cache[owner] = TagList.from(cached_tag_list_on(context))
279
300
  else
280
- instance_variable_set("@#{var_name}", TagList.new(*tags_on(context, owner).map(&:name)))
301
+ cache[owner] = TagList.new(*tags_on(context, owner).map(&:name))
281
302
  end
282
303
  end
304
+
305
+ def all_tags_list_on(context)
306
+ variable_name = "@all_#{context.to_s.singularize}_list"
307
+ return instance_variable_get(variable_name) if instance_variable_get(variable_name)
308
+ instance_variable_set(variable_name, TagList.new(all_tags_on(context).map(&:name)).freeze)
309
+ end
310
+
311
+ def all_tags_on(context)
312
+ opts = {:conditions => ["#{Tagging.table_name}.context = ?", context.to_s]}
313
+ base_tags.find(:all, opts.merge(:order => "#{Tagging.table_name}.created_at"))
314
+ end
283
315
 
284
- def tags_on(context, owner=nil)
316
+ def tags_on(context, owner = nil)
285
317
  if owner
286
318
  opts = {:conditions => ["#{Tagging.table_name}.context = ? AND #{Tagging.table_name}.tagger_id = ? AND #{Tagging.table_name}.tagger_type = ?",
287
319
  context.to_s, owner.id, owner.class.to_s]}
288
320
  else
289
- opts = {:conditions => ["#{Tagging.table_name}.context = ?", context.to_s]}
321
+ opts = {:conditions => ["#{Tagging.table_name}.context = ? AND #{Tagging.table_name}.tagger_id IS NULL", context.to_s]}
290
322
  end
291
323
  base_tags.find(:all, opts)
292
324
  end
@@ -294,14 +326,21 @@ module ActiveRecord
294
326
  def cached_tag_list_on(context)
295
327
  self["cached_#{context.to_s.singularize}_list"]
296
328
  end
329
+
330
+ def tag_list_cache_on(context)
331
+ variable_name = "@#{context.to_s.singularize}_list"
332
+ cache = instance_variable_get(variable_name)
333
+ instance_variable_set(variable_name, cache = {}) unless cache
334
+ cache
335
+ end
297
336
 
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))
337
+ def set_tag_list_on(context, new_list, tagger = nil)
338
+ tag_list_cache_on(context)[tagger] = TagList.from(new_list)
300
339
  add_custom_context(context)
301
340
  end
302
341
 
303
342
  def tag_counts_on(context, options={})
304
- self.class.tag_counts_on(context, options.merge(:id => self.id))
343
+ self.class.tag_counts_on(context, options.merge(:id => id))
305
344
  end
306
345
 
307
346
  def related_tags_for(context, klass, options = {})
@@ -311,9 +350,9 @@ module ActiveRecord
311
350
  end
312
351
 
313
352
  def related_search_options(context, klass, options = {})
314
- tags_to_find = self.tags_on(context).collect { |t| t.name }
353
+ tags_to_find = tags_on(context).collect { |t| t.name }
315
354
 
316
- exclude_self = "#{klass.table_name}.id != #{self.id} AND" if self.class == klass
355
+ exclude_self = "#{klass.table_name}.id != #{id} AND" if self.class == klass
317
356
 
318
357
  { :select => "#{klass.table_name}.*, COUNT(#{Tag.table_name}.id) AS count",
319
358
  :from => "#{klass.table_name}, #{Tag.table_name}, #{Tagging.table_name}",
@@ -322,38 +361,72 @@ module ActiveRecord
322
361
  :order => "count DESC"
323
362
  }.update(options)
324
363
  end
364
+
365
+ def matching_contexts_for(search_context, result_context, klass, options = {})
366
+ search_conditions = matching_context_search_options(search_context, result_context, klass, options)
367
+
368
+ klass.find(:all, search_conditions)
369
+ end
370
+
371
+ def matching_context_search_options(search_context, result_context, klass, options = {})
372
+ tags_to_find = tags_on(search_context).collect { |t| t.name }
373
+
374
+ exclude_self = "#{klass.table_name}.id != #{id} AND" if self.class == klass
325
375
 
376
+ { :select => "#{klass.table_name}.*, COUNT(#{Tag.table_name}.id) AS count",
377
+ :from => "#{klass.table_name}, #{Tag.table_name}, #{Tagging.table_name}",
378
+ :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],
379
+ :group => grouped_column_names_for(klass),
380
+ :order => "count DESC"
381
+ }.update(options)
382
+ end
383
+
326
384
  def save_cached_tag_list
327
385
  self.class.tag_types.map(&:to_s).each do |tag_type|
328
386
  if self.class.send("caching_#{tag_type.singularize}_list?")
329
- self["cached_#{tag_type.singularize}_list"] = send("#{tag_type.singularize}_list").to_s
387
+ self["cached_#{tag_type.singularize}_list"] = tag_list_cache_on(tag_type.singularize).tags.join(', ')
330
388
  end
331
389
  end
332
390
  end
333
391
 
334
392
  def save_tags
335
- (custom_contexts + self.class.tag_types.map(&:to_s)).each do |tag_type|
336
- next unless instance_variable_get("@#{tag_type.singularize}_list")
337
- owner = instance_variable_get("@#{tag_type.singularize}_list").owner
338
- new_tag_names = instance_variable_get("@#{tag_type.singularize}_list") - tags_on(tag_type).map(&:name)
339
- old_tags = tags_on(tag_type, owner).reject { |tag| instance_variable_get("@#{tag_type.singularize}_list").include?(tag.name) }
340
-
341
- self.class.transaction do
342
- base_tags.delete(*old_tags) if old_tags.any?
343
- new_tag_names.each do |new_tag_name|
344
- new_tag = Tag.find_or_create_with_like_by_name(new_tag_name)
345
- Tagging.create(:tag_id => new_tag.id, :context => tag_type,
346
- :taggable => self, :tagger => owner)
393
+ contexts = custom_contexts + self.class.tag_types.map(&:to_s)
394
+
395
+ transaction do
396
+ contexts.each do |context|
397
+ cache = tag_list_cache_on(context)
398
+
399
+ cache.each do |owner, list|
400
+ new_tags = Tag.find_or_create_all_with_like_by_name(list.uniq)
401
+
402
+ # Destroy old taggings:
403
+ if owner
404
+ old_tags = tags_on(context, owner) - new_tags
405
+ old_taggings = taggings.find(:all, :conditions => { :tag_id => old_tags, :tagger_id => owner, :tagger_type => owner.class.to_s, :context => context })
406
+ old_taggings.each(&:destroy)
407
+ else
408
+ old_tags = tags_on(context) - new_tags
409
+ base_tags.delete(*old_tags)
410
+ end
411
+
412
+ new_tags.reject! { |tag| taggings.any? { |tagging| tagging.tag == tag &&
413
+ tagging.tagger == owner &&
414
+ tagging.context == context } }
415
+
416
+ # create new taggings:
417
+ new_tags.each do |tag|
418
+ taggings.create!(:tag_id => tag.id, :context => context, :tagger => owner)
419
+ end
347
420
  end
348
421
  end
349
- end
422
+ end
350
423
 
351
424
  true
352
425
  end
353
426
 
354
427
  def reload_with_tag_list(*args)
355
428
  self.class.tag_types.each do |tag_type|
356
- self.instance_variable_set("@#{tag_type.to_s.singularize}_list", nil)
429
+ instance_variable_set("@#{tag_type.to_s.singularize}_list", nil)
357
430
  end
358
431
 
359
432
  reload_without_tag_list(*args)
@@ -1,14 +1,41 @@
1
1
  class Tag < ActiveRecord::Base
2
- has_many :taggings
2
+
3
+ attr_accessible :name
4
+
5
+ ### ASSOCIATIONS:
6
+
7
+ has_many :taggings, :dependent => :destroy
8
+
9
+ ### VALIDATIONS:
3
10
 
4
11
  validates_presence_of :name
5
12
  validates_uniqueness_of :name
6
13
 
7
- # LIKE is used for cross-database case-insensitivity
14
+ ### NAMED SCOPES:
15
+
16
+ named_scope :named, lambda { |name| { :conditions => ["name LIKE ?", name] } }
17
+ named_scope :named_any, lambda { |list| { :conditions => list.map { |tag| sanitize_sql(["name LIKE ?", tag.to_s]) }.join(" OR ") } }
18
+ named_scope :named_like, lambda { |name| { :conditions => ["name LIKE ?", "%#{name}%"] } }
19
+ named_scope :named_like_any, lambda { |list| { :conditions => list.map { |tag| sanitize_sql(["name LIKE ?", "%#{tag.to_s}%"]) }.join(" OR ") } }
20
+
21
+ ### CLASS METHODS:
22
+
8
23
  def self.find_or_create_with_like_by_name(name)
9
- find(:first, :conditions => ["name LIKE ?", name]) || create(:name => name)
24
+ named_like(name).first || create(:name => name)
10
25
  end
11
26
 
27
+ def self.find_or_create_all_with_like_by_name(*list)
28
+ list = [list].flatten
29
+
30
+ existing_tags = Tag.named_any(list).all
31
+ new_tag_names = list.reject { |name| existing_tags.any? { |tag| tag.name.downcase == name.downcase } }
32
+ created_tags = new_tag_names.map { |name| Tag.create(:name => name) }
33
+
34
+ existing_tags + created_tags
35
+ end
36
+
37
+ ### INSTANCE METHODS:
38
+
12
39
  def ==(object)
13
40
  super || (object.is_a?(Tag) && name == object.name)
14
41
  end
@@ -41,9 +41,10 @@ class TagList < Array
41
41
  # tag_list = TagList.new("Round", "Square,Cube")
42
42
  # tag_list.to_s # 'Round, "Square,Cube"'
43
43
  def to_s
44
- clean!
44
+ tags = frozen? ? self.dup : self
45
+ tags.send(:clean!)
45
46
 
46
- map do |name|
47
+ tags.map do |name|
47
48
  name.include?(delimiter) ? "\"#{name}\"" : name
48
49
  end.join(delimiter.ends_with?(" ") ? delimiter : "#{delimiter} ")
49
50
  end
@@ -55,7 +56,7 @@ class TagList < Array
55
56
  map!(&:strip)
56
57
  uniq!
57
58
  end
58
-
59
+
59
60
  def extract_and_apply_options!(args)
60
61
  options = args.last.is_a?(Hash) ? args.pop : {}
61
62
  options.assert_valid_keys :parse
@@ -79,17 +80,11 @@ class TagList < Array
79
80
  string = string.to_s.dup
80
81
 
81
82
  # Parse the quoted tags
82
- string.gsub!(/"(.*?)"\s*#{delimiter}?\s*/) { tag_list << $1; "" }
83
- string.gsub!(/'(.*?)'\s*#{delimiter}?\s*/) { tag_list << $1; "" }
83
+ string.gsub!(/(\A|#{delimiter})\s*"(.*?)"\s*(#{delimiter}\s*|\z)/) { tag_list << $2; $3 }
84
+ string.gsub!(/(\A|#{delimiter})\s*'(.*?)'\s*(#{delimiter}\s*|\z)/) { tag_list << $2; $3 }
84
85
 
85
86
  tag_list.add(string.split(delimiter))
86
87
  end
87
88
  end
88
-
89
- def from_owner(owner, *tags)
90
- returning from(*tags) do |taglist|
91
- taglist.owner = owner
92
- end
93
- end
94
89
  end
95
90
  end
@@ -1,6 +1,14 @@
1
1
  class Tagging < ActiveRecord::Base #:nodoc:
2
+ attr_accessible :tag, :tag_id, :context,
3
+ :taggable, :taggable_type, :taggable_id,
4
+ :tagger, :tagger_type, :tagger_id
5
+
2
6
  belongs_to :tag
3
7
  belongs_to :taggable, :polymorphic => true
4
8
  belongs_to :tagger, :polymorphic => true
9
+
5
10
  validates_presence_of :context
11
+ validates_presence_of :tag_id
12
+
13
+ validates_uniqueness_of :tag_id, :scope => [:taggable_type, :taggable_id, :context, :tagger_id, :tagger_type]
6
14
  end
@@ -1,6 +1,8 @@
1
1
  module TagsHelper
2
2
  # See the README for an example using tag_cloud.
3
3
  def tag_cloud(tags, classes)
4
+ return [] if tags.empty?
5
+
4
6
  max_count = tags.sort_by(&:count).last.count.to_f
5
7
 
6
8
  tags.each do |tag|
data/rails/init.rb CHANGED
@@ -2,5 +2,4 @@ require 'acts-as-taggable-on'
2
2
 
3
3
  ActiveRecord::Base.send :include, ActiveRecord::Acts::TaggableOn
4
4
  ActiveRecord::Base.send :include, ActiveRecord::Acts::Tagger
5
-
6
- RAILS_DEFAULT_LOGGER.info "** acts_as_taggable_on: initialized properly."
5
+ ActionView::Base.send :include, TagsHelper if defined?(ActionView::Base)
@@ -18,6 +18,10 @@ describe "Acts As Taggable On" do
18
18
  it "should create a class attribute for tag types" do
19
19
  @taggable.class.should respond_to(:tag_types)
20
20
  end
21
+
22
+ it "should create an instance attribute for tag types" do
23
+ @taggable.should respond_to(:tag_types)
24
+ end
21
25
 
22
26
  it "should generate an association for each tag type" do
23
27
  @taggable.should respond_to(:tags, :skills, :languages)
@@ -119,17 +123,59 @@ describe "Acts As Taggable On" do
119
123
  end
120
124
  end
121
125
 
122
- describe 'Tagging Contexts' do
123
- before(:all) do
124
- class Array
125
- def freq
126
- k=Hash.new(0)
127
- self.each {|e| k[e]+=1}
128
- k
129
- end
130
- end
126
+ describe "Matching Contexts" do
127
+ it "should find objects with tags of matching contexts" do
128
+ taggable1 = TaggableModel.create!(:name => "Taggable 1")
129
+ taggable2 = TaggableModel.create!(:name => "Taggable 2")
130
+ taggable3 = TaggableModel.create!(:name => "Taggable 3")
131
+
132
+ taggable1.offering_list = "one, two"
133
+ taggable1.save!
134
+
135
+ taggable2.need_list = "one, two"
136
+ taggable2.save!
137
+
138
+ taggable3.offering_list = "one, two"
139
+ taggable3.save!
140
+
141
+ taggable1.find_matching_contexts(:offerings, :needs).should include(taggable2)
142
+ taggable1.find_matching_contexts(:offerings, :needs).should_not include(taggable3)
131
143
  end
132
144
 
145
+ it "should find other related objects with tags of matching contexts" do
146
+ taggable1 = TaggableModel.create!(:name => "Taggable 1")
147
+ taggable2 = OtherTaggableModel.create!(:name => "Taggable 2")
148
+ taggable3 = OtherTaggableModel.create!(:name => "Taggable 3")
149
+
150
+ taggable1.offering_list = "one, two"
151
+ taggable1.save
152
+
153
+ taggable2.need_list = "one, two"
154
+ taggable2.save
155
+
156
+ taggable3.offering_list = "one, two"
157
+ taggable3.save
158
+
159
+ taggable1.find_matching_contexts_for(OtherTaggableModel, :offerings, :needs).should include(taggable2)
160
+ taggable1.find_matching_contexts_for(OtherTaggableModel, :offerings, :needs).should_not include(taggable3)
161
+ end
162
+
163
+ it "should not include the object itself in the list of related objects" do
164
+ taggable1 = TaggableModel.create!(:name => "Taggable 1")
165
+ taggable2 = TaggableModel.create!(:name => "Taggable 2")
166
+
167
+ taggable1.tag_list = "one"
168
+ taggable1.save
169
+
170
+ taggable2.tag_list = "one, two"
171
+ taggable2.save
172
+
173
+ taggable1.find_related_tags.should include(taggable2)
174
+ taggable1.find_related_tags.should_not include(taggable1)
175
+ end
176
+ end
177
+
178
+ describe 'Tagging Contexts' do
133
179
  it 'should eliminate duplicate tagging contexts ' do
134
180
  TaggableModel.acts_as_taggable_on(:skills, :skills)
135
181
  TaggableModel.tag_types.freq[:skills].should_not == 3
@@ -156,10 +202,6 @@ describe "Acts As Taggable On" do
156
202
  TaggableModel.acts_as_taggable_on([nil])
157
203
  }.should_not raise_error
158
204
  end
159
-
160
- after(:all) do
161
- class Array; remove_method :freq; end
162
- end
163
205
  end
164
206
 
165
207
  end
@@ -1,8 +1,11 @@
1
1
  require File.dirname(__FILE__) + '/../spec_helper'
2
2
 
3
3
  describe "acts_as_tagger" do
4
+ before(:each) do
5
+ [TaggableUser, TaggableModel, Tagging, Tag].each(&:destroy_all)
6
+ end
7
+
4
8
  context "Tagger Method Generation" do
5
-
6
9
  before(:each) do
7
10
  @tagger = TaggableUser.new()
8
11
  end
@@ -48,8 +51,22 @@ describe "acts_as_tagger" do
48
51
 
49
52
  it 'should by default create the tag context on-the-fly' do
50
53
  @taggable.tag_list_on(:here_ond_now).should be_empty
51
- @tagger.tag(@taggable, :with=>'that', :on=>:here_ond_now)
52
- @taggable.tag_list_on(:here_ond_now).should include('that')
54
+ @tagger.tag(@taggable, :with=>'that', :on => :here_ond_now)
55
+ @taggable.tag_list_on(:here_ond_now).should_not include('that')
56
+ @taggable.all_tags_list_on(:here_ond_now).should include('that')
57
+ end
58
+
59
+ it "should show all the tag list when both public and owned tags exist" do
60
+ @taggable.tag_list = 'ruby, python'
61
+ @tagger.tag(@taggable, :with => 'java, lisp', :on => :tags)
62
+ @taggable.all_tags_on(:tags).map(&:name).sort.should == %w(ruby python java lisp).sort
63
+ end
64
+
65
+ it "should not add owned tags to the common list" do
66
+ @taggable.tag_list = 'ruby, python'
67
+ @tagger.tag(@taggable, :with => 'java, lisp', :on => :foo)
68
+ @tagger.tag(@taggable, :with => '', :on => :foo)
69
+ @taggable.tag_list.should == %w(ruby python)
53
70
  end
54
71
 
55
72
  it "should throw an exception when the default is over-ridden" do
@@ -64,9 +81,28 @@ describe "acts_as_tagger" do
64
81
  @tagger.tag(@taggable, :with=>'this, and, that', :on=>:foo_boo, :force=>false) rescue
65
82
  @taggable.tag_list_on(:foo_boo).should be_empty
66
83
  end
67
-
68
84
  end
69
-
85
+
86
+ context "when called by multiple tagger's" do
87
+ before(:each) do
88
+ @user_x = TaggableUser.new
89
+ @user_y = TaggableUser.new
90
+ @taggable = TaggableModel.new(:name => 'acts_as_taggable_on', :tag_list => 'plugin')
91
+
92
+ @user_x.tag(@taggable, :with => 'ruby, rails', :on => :tags)
93
+ @user_y.tag(@taggable, :with => 'ruby, plugin', :on => :tags)
94
+ end
95
+
96
+ it "should not delete other taggers tags" do
97
+ @user_y.tag(@taggable, :with => '', :on => :tags)
98
+ @taggable.all_tags_list_on(:tags).should include('ruby')
99
+ end
100
+
101
+ it "should not delete original tags" do
102
+ @user_y.tag(@taggable, :with => '', :on => :tags)
103
+ @taggable.all_tags_list_on(:tags).should include('plugin')
104
+ end
105
+ end
70
106
  end
71
107
 
72
108
  end
@@ -20,6 +20,18 @@ describe TagList do
20
20
  @tag_list.include?("wicked").should be_true
21
21
  end
22
22
 
23
+ it "should be able to add delimited list of words with quoted delimiters" do
24
+ @tag_list.add("'cool, wicked', \"really cool, really wicked\"", :parse => true)
25
+ @tag_list.include?("cool, wicked").should be_true
26
+ @tag_list.include?("really cool, really wicked").should be_true
27
+ end
28
+
29
+ it "should be able to handle other uses of quotation marks correctly" do
30
+ @tag_list.add("john's cool car, mary's wicked toy", :parse => true)
31
+ @tag_list.include?("john's cool car").should be_true
32
+ @tag_list.include?("mary's wicked toy").should be_true
33
+ end
34
+
23
35
  it "should be able to add an array of words" do
24
36
  @tag_list.add(["cool", "wicked"], :parse => true)
25
37
  @tag_list.include?("cool").should be_true
@@ -49,4 +61,10 @@ describe TagList do
49
61
  @tag_list.add("cool","rad,bodacious")
50
62
  @tag_list.to_s.should == "awesome, radical, cool, \"rad,bodacious\""
51
63
  end
64
+
65
+ it "should be able to call to_s on a frozen tag list" do
66
+ @tag_list.freeze
67
+ lambda { @tag_list.add("cool","rad,bodacious") }.should raise_error
68
+ lambda { @tag_list.to_s }.should_not raise_error
69
+ end
52
70
  end
@@ -4,8 +4,68 @@ describe Tag do
4
4
  before(:each) do
5
5
  @tag = Tag.new
6
6
  @user = TaggableModel.create(:name => "Pablo")
7
+ Tag.delete_all
7
8
  end
8
9
 
10
+ describe "named like any" do
11
+ before(:each) do
12
+ Tag.create(:name => "awesome")
13
+ Tag.create(:name => "epic")
14
+ end
15
+
16
+ it "should find both tags" do
17
+ Tag.named_like_any(["awesome", "epic"]).should have(2).items
18
+ end
19
+ end
20
+
21
+ describe "find or create by name" do
22
+ before(:each) do
23
+ @tag.name = "awesome"
24
+ @tag.save
25
+ end
26
+
27
+ it "should find by name" do
28
+ Tag.find_or_create_with_like_by_name("awesome").should == @tag
29
+ end
30
+
31
+ it "should find by name case insensitive" do
32
+ Tag.find_or_create_with_like_by_name("AWESOME").should == @tag
33
+ end
34
+
35
+ it "should create by name" do
36
+ lambda {
37
+ Tag.find_or_create_with_like_by_name("epic")
38
+ }.should change(Tag, :count).by(1)
39
+ end
40
+ end
41
+
42
+ describe "find or create all by any name" do
43
+ before(:each) do
44
+ @tag.name = "awesome"
45
+ @tag.save
46
+ end
47
+
48
+ it "should find by name" do
49
+ Tag.find_or_create_all_with_like_by_name("awesome").should == [@tag]
50
+ end
51
+
52
+ it "should find by name case insensitive" do
53
+ Tag.find_or_create_all_with_like_by_name("AWESOME").should == [@tag]
54
+ end
55
+
56
+ it "should create by name" do
57
+ lambda {
58
+ Tag.find_or_create_all_with_like_by_name("epic")
59
+ }.should change(Tag, :count).by(1)
60
+ end
61
+
62
+ it "should find or create by name" do
63
+ lambda {
64
+ Tag.find_or_create_all_with_like_by_name("awesome", "epic").map(&:name).should == ["awesome", "epic"]
65
+ }.should change(Tag, :count).by(1)
66
+ end
67
+ end
68
+
9
69
  it "should require a name" do
10
70
  @tag.valid?
11
71
  @tag.errors.on(:name).should == "can't be blank"
@@ -24,4 +84,17 @@ describe Tag do
24
84
  @tag.name = "cool"
25
85
  @tag.to_s.should == "cool"
26
86
  end
87
+
88
+ it "have named_scope named(something)" do
89
+ @tag.name = "cool"
90
+ @tag.save!
91
+ Tag.named('cool').should include(@tag)
92
+ end
93
+
94
+ it "have named_scope named_like(something)" do
95
+ @tag.name = "cool"
96
+ @tag.save!
97
+ @another_tag = Tag.create!(:name => "coolip")
98
+ Tag.named_like('cool').should include(@tag, @another_tag)
99
+ end
27
100
  end
@@ -5,31 +5,51 @@ describe "Taggable" do
5
5
  [TaggableModel, Tag, Tagging, TaggableUser].each(&:delete_all)
6
6
  @taggable = TaggableModel.new(:name => "Bob Jones")
7
7
  end
8
-
8
+
9
+ it "should have tag types" do
10
+ for type in [:tags, :languages, :skills, :needs, :offerings]
11
+ TaggableModel.tag_types.should include type
12
+ end
13
+ @taggable.tag_types.should == TaggableModel.tag_types
14
+ end
15
+
16
+ it "should have tag_counts_on" do
17
+ TaggableModel.tag_counts_on(:tags).should be_empty
18
+
19
+ @taggable.tag_list = ["awesome", "epic"]
20
+ @taggable.save
21
+
22
+ TaggableModel.tag_counts_on(:tags).count.should == 2
23
+ @taggable.tag_counts_on(:tags).count.should == 2
24
+ end
25
+
9
26
  it "should be able to create tags" do
10
27
  @taggable.skill_list = "ruby, rails, css"
11
- @taggable.instance_variable_get("@skill_list").instance_of?(TagList).should be_true
28
+ @taggable.instance_variable_get("@skill_list").instance_of?(Hash).should be_true
29
+ @taggable.instance_variable_get("@skill_list")[nil].instance_of?(TagList).should be_true
12
30
  @taggable.save
13
-
31
+
14
32
  Tag.find(:all).size.should == 3
15
33
  end
16
-
34
+
17
35
  it "should be able to create tags through the tag list directly" do
18
36
  @taggable.tag_list_on(:test).add("hello")
19
- @taggable.save
37
+ @taggable.tag_list_cache_on(:test).should_not be_empty
38
+ @taggable.save
39
+ @taggable.save_tags
20
40
  @taggable.reload
21
41
  @taggable.tag_list_on(:test).should == ["hello"]
22
42
  end
23
-
43
+
24
44
  it "should differentiate between contexts" do
25
45
  @taggable.skill_list = "ruby, rails, css"
26
46
  @taggable.tag_list = "ruby, bob, charlie"
27
47
  @taggable.save
28
48
  @taggable.reload
29
- @taggable.skill_list.include?("ruby").should be_true
30
- @taggable.skill_list.include?("bob").should be_false
49
+ @taggable.skill_list.should include("ruby")
50
+ @taggable.skill_list.should_not include("bob")
31
51
  end
32
-
52
+
33
53
  it "should be able to remove tags through list alone" do
34
54
  @taggable.skill_list = "ruby, rails, css"
35
55
  @taggable.save
@@ -40,13 +60,13 @@ describe "Taggable" do
40
60
  @taggable.reload
41
61
  @taggable.should have(2).skills
42
62
  end
43
-
63
+
44
64
  it "should be able to find by tag" do
45
65
  @taggable.skill_list = "ruby, rails, css"
46
66
  @taggable.save
47
67
  TaggableModel.find_tagged_with("ruby").first.should == @taggable
48
68
  end
49
-
69
+
50
70
  it "should be able to find by tag with context" do
51
71
  @taggable.skill_list = "ruby, rails, css"
52
72
  @taggable.tag_list = "bob, charlie"
@@ -55,25 +75,27 @@ describe "Taggable" do
55
75
  TaggableModel.find_tagged_with("bob", :on => :skills).first.should_not == @taggable
56
76
  TaggableModel.find_tagged_with("bob", :on => :tags).first.should == @taggable
57
77
  end
58
-
78
+
59
79
  it "should be able to use the tagged_with named scope" do
60
80
  @taggable.skill_list = "ruby, rails, css"
61
81
  @taggable.tag_list = "bob, charlie"
62
82
  @taggable.save
63
-
83
+
64
84
  TaggableModel.tagged_with("ruby").first.should == @taggable
85
+ TaggableModel.tagged_with("ruby, css").first.should == @taggable
86
+ TaggableModel.tagged_with("ruby, nonexistingtag").should be_empty
65
87
  TaggableModel.tagged_with("bob", :on => :skills).first.should_not == @taggable
66
88
  TaggableModel.tagged_with("bob", :on => :tags).first.should == @taggable
67
89
  end
68
-
90
+
69
91
  it "should not care about case" do
70
92
  bob = TaggableModel.create(:name => "Bob", :tag_list => "ruby")
71
93
  frank = TaggableModel.create(:name => "Frank", :tag_list => "Ruby")
72
-
94
+
73
95
  Tag.find(:all).size.should == 1
74
96
  TaggableModel.find_tagged_with("ruby").should == TaggableModel.find_tagged_with("Ruby")
75
97
  end
76
-
98
+
77
99
  it "should be able to get tag counts on model as a whole" do
78
100
  bob = TaggableModel.create(:name => "Bob", :tag_list => "ruby, rails, css")
79
101
  frank = TaggableModel.create(:name => "Frank", :tag_list => "ruby, rails")
@@ -81,33 +103,39 @@ describe "Taggable" do
81
103
  TaggableModel.tag_counts.should_not be_empty
82
104
  TaggableModel.skill_counts.should_not be_empty
83
105
  end
84
-
106
+
85
107
  it "should be able to get all tag counts on model as whole" do
86
108
  bob = TaggableModel.create(:name => "Bob", :tag_list => "ruby, rails, css")
87
109
  frank = TaggableModel.create(:name => "Frank", :tag_list => "ruby, rails")
88
110
  charlie = TaggableModel.create(:name => "Charlie", :skill_list => "ruby")
89
-
111
+
90
112
  TaggableModel.all_tag_counts.should_not be_empty
91
113
  TaggableModel.all_tag_counts.first.count.should == 3 # ruby
92
114
  end
93
-
115
+
116
+ it "should not return read-only records" do
117
+ TaggableModel.create(:name => "Bob", :tag_list => "ruby, rails, css")
118
+
119
+ TaggableModel.tagged_with("ruby").first.should_not be_readonly
120
+ end
121
+
94
122
  it "should be able to get scoped tag counts" do
95
123
  bob = TaggableModel.create(:name => "Bob", :tag_list => "ruby, rails, css")
96
124
  frank = TaggableModel.create(:name => "Frank", :tag_list => "ruby, rails")
97
125
  charlie = TaggableModel.create(:name => "Charlie", :skill_list => "ruby")
98
-
126
+
99
127
  TaggableModel.tagged_with("ruby").tag_counts.first.count.should == 2 # ruby
100
128
  TaggableModel.tagged_with("ruby").skill_counts.first.count.should == 1 # ruby
101
129
  end
102
-
130
+
103
131
  it "should be able to get all scoped tag counts" do
104
132
  bob = TaggableModel.create(:name => "Bob", :tag_list => "ruby, rails, css")
105
133
  frank = TaggableModel.create(:name => "Frank", :tag_list => "ruby, rails")
106
134
  charlie = TaggableModel.create(:name => "Charlie", :skill_list => "ruby")
107
-
135
+
108
136
  TaggableModel.tagged_with("ruby").all_tag_counts.first.count.should == 3 # ruby
109
137
  end
110
-
138
+
111
139
  it "should be able to set a custom tag context list" do
112
140
  bob = TaggableModel.create(:name => "Bob")
113
141
  bob.set_tag_list_on(:rotors, "spinning, jumping")
@@ -116,17 +144,27 @@ describe "Taggable" do
116
144
  bob.reload
117
145
  bob.tags_on(:rotors).should_not be_empty
118
146
  end
119
-
147
+
120
148
  it "should be able to find tagged" do
121
149
  bob = TaggableModel.create(:name => "Bob", :tag_list => "fitter, happier, more productive", :skill_list => "ruby, rails, css")
122
150
  frank = TaggableModel.create(:name => "Frank", :tag_list => "weaker, depressed, inefficient", :skill_list => "ruby, rails, css")
123
151
  steve = TaggableModel.create(:name => 'Steve', :tag_list => 'fitter, happier, more productive', :skill_list => 'c++, java, ruby')
124
-
152
+
125
153
  TaggableModel.find_tagged_with("ruby", :order => 'taggable_models.name').should == [bob, frank, steve]
126
154
  TaggableModel.find_tagged_with("ruby, rails", :order => 'taggable_models.name').should == [bob, frank]
127
155
  TaggableModel.find_tagged_with(["ruby", "rails"], :order => 'taggable_models.name').should == [bob, frank]
128
156
  end
129
-
157
+
158
+ it "should be able to find tagged with any tag" do
159
+ bob = TaggableModel.create(:name => "Bob", :tag_list => "fitter, happier, more productive", :skill_list => "ruby, rails, css")
160
+ frank = TaggableModel.create(:name => "Frank", :tag_list => "weaker, depressed, inefficient", :skill_list => "ruby, rails, css")
161
+ steve = TaggableModel.create(:name => 'Steve', :tag_list => 'fitter, happier, more productive', :skill_list => 'c++, java, ruby')
162
+
163
+ TaggableModel.find_tagged_with(["ruby", "java"], :order => 'taggable_models.name', :any => true).should == [bob, frank, steve]
164
+ TaggableModel.find_tagged_with(["c++", "fitter"], :order => 'taggable_models.name', :any => true).should == [bob, steve]
165
+ TaggableModel.find_tagged_with(["depressed", "css"], :order => 'taggable_models.name', :any => true).should == [bob, frank]
166
+ end
167
+
130
168
  it "should be able to find tagged on a custom tag context" do
131
169
  bob = TaggableModel.create(:name => "Bob")
132
170
  bob.set_tag_list_on(:rotors, "spinning, jumping")
@@ -139,29 +177,38 @@ describe "Taggable" do
139
177
  bob = TaggableModel.create(:name => "Bob", :tag_list => "fitter, happier, more productive", :skill_list => "ruby, rails, css")
140
178
  frank = TaggableModel.create(:name => "Frank", :tag_list => "weaker, depressed, inefficient", :skill_list => "ruby, rails, css")
141
179
  steve = TaggableModel.create(:name => 'Steve', :tag_list => 'fitter, happier, more productive', :skill_list => 'c++, java, python')
142
-
180
+
143
181
  # Let's only find those productive Rails developers
144
182
  TaggableModel.tagged_with('rails', :on => :skills, :order => 'taggable_models.name').should == [bob, frank]
145
183
  TaggableModel.tagged_with('happier', :on => :tags, :order => 'taggable_models.name').should == [bob, steve]
146
184
  TaggableModel.tagged_with('rails', :on => :skills).tagged_with('happier', :on => :tags).should == [bob]
147
185
  TaggableModel.tagged_with('rails').tagged_with('happier', :on => :tags).should == [bob]
148
186
  end
149
-
187
+
150
188
  it "should be able to find tagged with only the matching tags" do
151
189
  bob = TaggableModel.create(:name => "Bob", :tag_list => "lazy, happier")
152
190
  frank = TaggableModel.create(:name => "Frank", :tag_list => "fitter, happier, inefficient")
153
191
  steve = TaggableModel.create(:name => 'Steve', :tag_list => "fitter, happier")
154
-
192
+
155
193
  TaggableModel.find_tagged_with("fitter, happier", :match_all => true).should == [steve]
156
194
  end
157
-
195
+
158
196
  it "should be able to find tagged with some excluded tags" do
159
197
  bob = TaggableModel.create(:name => "Bob", :tag_list => "happier, lazy")
160
198
  frank = TaggableModel.create(:name => "Frank", :tag_list => "happier")
161
199
  steve = TaggableModel.create(:name => 'Steve', :tag_list => "happier")
162
-
200
+
163
201
  TaggableModel.find_tagged_with("lazy", :exclude => true).should == [frank, steve]
164
202
  end
203
+
204
+ it "should not create duplicate taggings" do
205
+ bob = TaggableModel.create(:name => "Bob")
206
+ lambda {
207
+ bob.tag_list << "happier"
208
+ bob.tag_list << "happier"
209
+ bob.save
210
+ }.should change(Tagging, :count).by(1)
211
+ end
165
212
 
166
213
  describe "Single Table Inheritance" do
167
214
  before do
@@ -170,32 +217,32 @@ describe "Taggable" do
170
217
  @inherited_same = InheritingTaggableModel.new(:name => "inherited same")
171
218
  @inherited_different = AlteredInheritingTaggableModel.new(:name => "inherited different")
172
219
  end
173
-
220
+
174
221
  it "should be able to save tags for inherited models" do
175
222
  @inherited_same.tag_list = "bob, kelso"
176
223
  @inherited_same.save
177
224
  InheritingTaggableModel.find_tagged_with("bob").first.should == @inherited_same
178
225
  end
179
-
226
+
180
227
  it "should find STI tagged models on the superclass" do
181
228
  @inherited_same.tag_list = "bob, kelso"
182
229
  @inherited_same.save
183
230
  TaggableModel.find_tagged_with("bob").first.should == @inherited_same
184
231
  end
185
-
232
+
186
233
  it "should be able to add on contexts only to some subclasses" do
187
234
  @inherited_different.part_list = "fork, spoon"
188
235
  @inherited_different.save
189
236
  InheritingTaggableModel.find_tagged_with("fork", :on => :parts).should be_empty
190
237
  AlteredInheritingTaggableModel.find_tagged_with("fork", :on => :parts).first.should == @inherited_different
191
238
  end
192
-
239
+
193
240
  it "should have different tag_counts_on for inherited models" do
194
241
  @inherited_same.tag_list = "bob, kelso"
195
242
  @inherited_same.save!
196
243
  @inherited_different.tag_list = "fork, spoon"
197
244
  @inherited_different.save!
198
-
245
+
199
246
  InheritingTaggableModel.tag_counts_on(:tags).map(&:name).should == %w(bob kelso)
200
247
  AlteredInheritingTaggableModel.tag_counts_on(:tags).map(&:name).should == %w(fork spoon)
201
248
  TaggableModel.tag_counts_on(:tags).map(&:name).should == %w(bob kelso fork spoon)
@@ -6,17 +6,32 @@ describe "Tagger" do
6
6
  @user = TaggableUser.new
7
7
  @taggable = TaggableModel.new(:name => "Bob Jones")
8
8
  end
9
-
9
+
10
10
  it "should have taggings" do
11
11
  @user.tag(@taggable, :with=>'ruby,scheme', :on=>:tags)
12
12
  @user.owned_taggings.size == 2
13
13
  end
14
-
14
+
15
15
  it "should have tags" do
16
16
  @user.tag(@taggable, :with=>'ruby,scheme', :on=>:tags)
17
17
  @user.owned_tags.size == 2
18
18
  end
19
19
 
20
+ it "should not overlap or lose tags from different users" do
21
+ @user2 = TaggableUser.new
22
+ lambda{
23
+ @user.tag(@taggable, :with => 'ruby, scheme', :on => :tags)
24
+ @user2.tag(@taggable, :with => 'java, python, lisp, ruby', :on => :tags)
25
+ }.should change(Tagging, :count).by(6)
26
+
27
+ @user.owned_tags.map(&:name).sort.should == %w(ruby scheme)
28
+ @user2.owned_tags.map(&:name).sort.should == %w(java python lisp ruby).sort
29
+ @taggable.tags_from(@user).sort.should == %w(ruby scheme)
30
+ @taggable.tags_from(@user2).sort.should == %w(java lisp python ruby)
31
+ @taggable.all_tags_list_on(:tags).sort.should == %w(ruby scheme java python lisp).sort
32
+ @taggable.all_tags_on(:tags).size.should == 6
33
+ end
34
+
20
35
  it "is tagger" do
21
36
  @user.is_tagger?.should(be_true)
22
37
  end
@@ -4,4 +4,22 @@ describe Tagging do
4
4
  before(:each) do
5
5
  @tagging = Tagging.new
6
6
  end
7
+
8
+ it "should not be valid with a invalid tag" do
9
+ @tagging.taggable = TaggableModel.create(:name => "Bob Jones")
10
+ @tagging.tag = Tag.new(:name => "")
11
+ @tagging.context = "tags"
12
+
13
+ @tagging.should_not be_valid
14
+ @tagging.errors.on(:tag_id).should == "can't be blank"
15
+ end
16
+
17
+ it "should not create duplicate taggings" do
18
+ @taggable = TaggableModel.create(:name => "Bob Jones")
19
+ @tag = Tag.create(:name => "awesome")
20
+
21
+ lambda {
22
+ 2.times { Tagging.create(:taggable => @taggable, :tag => @tag, :context => 'tags') }
23
+ }.should change(Tagging, :count).by(1)
24
+ end
7
25
  end
@@ -0,0 +1,30 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ describe TagsHelper do
4
+ before(:each) do
5
+ [TaggableModel, Tag, Tagging].each(&:delete_all)
6
+
7
+ @bob = TaggableModel.create(:name => "Bob Jones", :language_list => "ruby, php")
8
+ @tom = TaggableModel.create(:name => "Tom Marley", :language_list => "ruby, java")
9
+ @eve = TaggableModel.create(:name => "Eve Nodd", :language_list => "ruby, c++")
10
+
11
+ @helper = class Helper
12
+ include TagsHelper
13
+ end.new
14
+
15
+
16
+ end
17
+
18
+ it "should yield the proper css classes" do
19
+ tags = { }
20
+
21
+ @helper.tag_cloud(TaggableModel.tag_counts_on(:languages), ["sucky", "awesome"]) do |tag, css_class|
22
+ tags[tag.name] = css_class
23
+ end
24
+
25
+ tags["ruby"].should == "awesome"
26
+ tags["java"].should == "sucky"
27
+ tags["c++"].should == "sucky"
28
+ tags["php"].should == "sucky"
29
+ end
30
+ end
data/spec/spec_helper.rb CHANGED
@@ -1,12 +1,20 @@
1
1
  # require File.dirname(__FILE__) + '/../../../../spec/spec_helper'
2
2
  require 'rubygems'
3
- require 'activerecord'
3
+ require 'active_record'
4
4
  require 'spec'
5
5
 
6
6
  module Spec::Example::ExampleGroupMethods
7
7
  alias :context :describe
8
8
  end
9
9
 
10
+ class Array
11
+ def freq
12
+ k=Hash.new(0)
13
+ each {|e| k[e]+=1}
14
+ k
15
+ end
16
+ end
17
+
10
18
  TEST_DATABASE_FILE = File.join(File.dirname(__FILE__), '..', 'test.sqlite3')
11
19
 
12
20
  File.unlink(TEST_DATABASE_FILE) if File.exist?(TEST_DATABASE_FILE)
@@ -16,18 +24,24 @@ ActiveRecord::Base.establish_connection(
16
24
 
17
25
  RAILS_DEFAULT_LOGGER = Logger.new(File.join(File.dirname(__FILE__), "debug.log"))
18
26
 
19
- load(File.dirname(__FILE__) + '/schema.rb')
27
+ ActiveRecord::Base.silence do
28
+ ActiveRecord::Migration.verbose = false
29
+ load(File.dirname(__FILE__) + '/schema.rb')
30
+ end
20
31
 
21
32
  $: << File.join(File.dirname(__FILE__), '..', 'lib')
22
33
  require File.join(File.dirname(__FILE__), '..', 'init')
23
34
 
24
35
  class TaggableModel < ActiveRecord::Base
25
- acts_as_taggable_on :tags, :languages
36
+ acts_as_taggable
37
+ acts_as_taggable_on :languages
26
38
  acts_as_taggable_on :skills
39
+ acts_as_taggable_on :needs, :offerings
27
40
  end
28
41
 
29
42
  class OtherTaggableModel < ActiveRecord::Base
30
43
  acts_as_taggable_on :tags, :languages
44
+ acts_as_taggable_on :needs, :offerings
31
45
  end
32
46
 
33
47
  class InheritingTaggableModel < TaggableModel
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: acts-as-taggable-on
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.12
4
+ version: 1.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Bleigh
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-12-07 00:00:00 -05:00
12
+ date: 2010-02-06 00:00:00 +01:00
13
13
  default_executable:
14
14
  dependencies: []
15
15
 
@@ -27,6 +27,8 @@ files:
27
27
  - README.rdoc
28
28
  - Rakefile
29
29
  - VERSION
30
+ - generators/acts_as_taggable_on_migration/acts_as_taggable_on_migration_generator.rb
31
+ - generators/acts_as_taggable_on_migration/templates/migration.rb
30
32
  - lib/acts-as-taggable-on.rb
31
33
  - lib/acts_as_taggable_on/acts_as_taggable_on.rb
32
34
  - lib/acts_as_taggable_on/acts_as_tagger.rb
@@ -44,6 +46,7 @@ files:
44
46
  - spec/acts_as_taggable_on/taggable_spec.rb
45
47
  - spec/acts_as_taggable_on/tagger_spec.rb
46
48
  - spec/acts_as_taggable_on/tagging_spec.rb
49
+ - spec/acts_as_taggable_on/tags_helper_spec.rb
47
50
  - spec/schema.rb
48
51
  - spec/spec.opts
49
52
  - spec/spec_helper.rb
@@ -84,5 +87,6 @@ test_files:
84
87
  - spec/acts_as_taggable_on/taggable_spec.rb
85
88
  - spec/acts_as_taggable_on/tagger_spec.rb
86
89
  - spec/acts_as_taggable_on/tagging_spec.rb
90
+ - spec/acts_as_taggable_on/tags_helper_spec.rb
87
91
  - spec/schema.rb
88
92
  - spec/spec_helper.rb