acts-as-taggable-on 1.0.11 → 1.1.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.
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.11
1
+ 1.1.0
@@ -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
@@ -1,3 +1,4 @@
1
+ require 'acts_as_taggable_on/group_helper'
1
2
  require 'acts_as_taggable_on/acts_as_taggable_on'
2
3
  require 'acts_as_taggable_on/acts_as_tagger'
3
4
  require 'acts_as_taggable_on/tag'
@@ -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,16 +109,14 @@ 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
115
+ include ActiveRecord::Acts::TaggableOn::GroupHelper
112
116
  # Pass either a tag string, or an array of strings or tags
113
117
  #
114
118
  # Options:
119
+ # :any - find models that match any of the given tags
115
120
  # :exclude - Find models that are not tagged with the given tags
116
121
  # :match_all - Find models that match all of the given tags, not just one
117
122
  # :conditions - A piece of SQL conditions to add to the query
@@ -134,9 +139,9 @@ module ActiveRecord
134
139
  end
135
140
 
136
141
  def find_options_for_find_tagged_with(tags, options = {})
137
- tags = TagList.from(tags)
142
+ tag_list = TagList.from(tags)
138
143
 
139
- return {} if tags.empty?
144
+ return {} if tag_list.empty?
140
145
 
141
146
  joins = []
142
147
  conditions = []
@@ -145,28 +150,30 @@ module ActiveRecord
145
150
 
146
151
 
147
152
  if options.delete(:exclude)
148
- 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 ")
149
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)})"
150
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
+
151
160
  else
161
+ tags = Tag.named_like_any(tag_list)
162
+ return { :conditions => "1 = 0" } unless tags.length == tag_list.length
163
+
152
164
  tags.each do |tag|
153
- safe_tag = tag.gsub(/[^a-zA-Z0-9]/, '')
165
+ safe_tag = tag.name.gsub(/[^a-zA-Z0-9]/, '')
154
166
  prefix = "#{safe_tag}_#{rand(1024)}"
155
167
 
156
168
  taggings_alias = "#{table_name}_taggings_#{prefix}"
157
- tags_alias = "#{table_name}_tags_#{prefix}"
158
169
 
159
170
  tagging_join = "JOIN #{Tagging.table_name} #{taggings_alias}" +
160
171
  " ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
161
- " 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}"
162
174
  tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
163
175
 
164
- tag_join = "JOIN #{Tag.table_name} #{tags_alias}" +
165
- " ON #{tags_alias}.id = #{taggings_alias}.tag_id" +
166
- " AND " + sanitize_sql(["#{tags_alias}.name like ?", tag])
167
-
168
176
  joins << tagging_join
169
- joins << tag_join
170
177
  end
171
178
  end
172
179
 
@@ -177,12 +184,13 @@ module ActiveRecord
177
184
  " ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
178
185
  " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}"
179
186
 
180
- group = "#{column_names_for_tagging_group} HAVING COUNT(#{taggings_alias}.taggable_id) = #{tags.size}"
187
+ group = "#{grouped_column_names_for(self)} HAVING COUNT(#{taggings_alias}.taggable_id) = #{tags.size}"
181
188
  end
182
189
 
183
190
  { :joins => joins.join(" "),
184
191
  :group => group,
185
- :conditions => conditions.join(" AND ") }.update(options)
192
+ :conditions => conditions.join(" AND "),
193
+ :readonly => false }.update(options)
186
194
  end
187
195
 
188
196
  # Calculate the tag counts for all tags.
@@ -220,19 +228,37 @@ module ActiveRecord
220
228
 
221
229
  joins = ["LEFT OUTER JOIN #{Tagging.table_name} ON #{Tag.table_name}.id = #{Tagging.table_name}.tag_id"]
222
230
  joins << sanitize_sql(["AND #{Tagging.table_name}.context = ?",options.delete(:on).to_s]) unless options[:on].nil?
223
-
224
231
  joins << " INNER JOIN #{table_name} ON #{table_name}.#{primary_key} = #{Tagging.table_name}.taggable_id"
225
- unless self.descends_from_active_record?
232
+
233
+ unless descends_from_active_record?
226
234
  # Current model is STI descendant, so add type checking to the join condition
227
- joins << " AND #{table_name}.#{self.inheritance_column} = '#{self.name}'"
235
+ joins << " AND #{table_name}.#{inheritance_column} = '#{name}'"
228
236
  end
229
237
 
230
- 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
231
257
 
232
258
  at_least = sanitize_sql(['COUNT(*) >= ?', options.delete(:at_least)]) if options[:at_least]
233
259
  at_most = sanitize_sql(['COUNT(*) <= ?', options.delete(:at_most)]) if options[:at_most]
234
260
  having = [at_least, at_most].compact.join(' AND ')
235
- group_by = "#{column_names_for_tag_group} HAVING COUNT(*) > 0"
261
+ group_by = "#{grouped_column_names_for(Tag)} HAVING COUNT(*) > 0"
236
262
  group_by << " AND #{having}" unless having.blank?
237
263
 
238
264
  { :select => "#{Tag.table_name}.*, COUNT(*) AS count",
@@ -247,21 +273,10 @@ module ActiveRecord
247
273
  def is_taggable?
248
274
  true
249
275
  end
250
-
251
- def column_names_for_tag_group
252
- Tag.column_names.map { |column| "#{Tag.table_name}.#{column}" }.join(", ")
253
- end
254
-
255
- def column_names_for_tagging_group
256
- column_names.map { |column| "#{table_name}.#{column}" }.join(", ")
257
- end
258
276
  end
259
277
 
260
278
  module InstanceMethods
261
-
262
- def tag_types
263
- self.class.tag_types
264
- end
279
+ include ActiveRecord::Acts::TaggableOn::GroupHelper
265
280
 
266
281
  def custom_contexts
267
282
  @custom_contexts ||= []
@@ -270,29 +285,40 @@ module ActiveRecord
270
285
  def is_taggable?
271
286
  self.class.is_taggable?
272
287
  end
273
-
288
+
274
289
  def add_custom_context(value)
275
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)
276
291
  end
277
292
 
278
- def tag_list_on(context, owner=nil)
279
- var_name = context.to_s.singularize + "_list"
293
+ def tag_list_on(context, owner = nil)
280
294
  add_custom_context(context)
281
- return instance_variable_get("@#{var_name}") unless instance_variable_get("@#{var_name}").nil?
282
-
295
+ cache = tag_list_cache_on(context)
296
+ return owner ? cache[owner] : cache[owner] if cache[owner]
297
+
283
298
  if !owner && self.class.caching_tag_list_on?(context) and !(cached_value = cached_tag_list_on(context)).nil?
284
- instance_variable_set("@#{var_name}", TagList.from(self["cached_#{var_name}"]))
299
+ cache[owner] = TagList.from(cached_tag_list_on(context))
285
300
  else
286
- instance_variable_set("@#{var_name}", TagList.new(*tags_on(context, owner).map(&:name)))
301
+ cache[owner] = TagList.new(*tags_on(context, owner).map(&:name))
287
302
  end
288
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
289
315
 
290
- def tags_on(context, owner=nil)
316
+ def tags_on(context, owner = nil)
291
317
  if owner
292
318
  opts = {:conditions => ["#{Tagging.table_name}.context = ? AND #{Tagging.table_name}.tagger_id = ? AND #{Tagging.table_name}.tagger_type = ?",
293
319
  context.to_s, owner.id, owner.class.to_s]}
294
320
  else
295
- 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]}
296
322
  end
297
323
  base_tags.find(:all, opts)
298
324
  end
@@ -300,14 +326,21 @@ module ActiveRecord
300
326
  def cached_tag_list_on(context)
301
327
  self["cached_#{context.to_s.singularize}_list"]
302
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
303
336
 
304
- def set_tag_list_on(context,new_list, tagger=nil)
305
- 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)
306
339
  add_custom_context(context)
307
340
  end
308
341
 
309
342
  def tag_counts_on(context, options={})
310
- self.class.tag_counts_on(context, options.merge(:id => self.id))
343
+ self.class.tag_counts_on(context, options.merge(:id => id))
311
344
  end
312
345
 
313
346
  def related_tags_for(context, klass, options = {})
@@ -317,39 +350,58 @@ module ActiveRecord
317
350
  end
318
351
 
319
352
  def related_search_options(context, klass, options = {})
320
- tags_to_find = self.tags_on(context).collect { |t| t.name }
353
+ tags_to_find = tags_on(context).collect { |t| t.name }
321
354
 
322
- 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
323
356
 
324
357
  { :select => "#{klass.table_name}.*, COUNT(#{Tag.table_name}.id) AS count",
325
358
  :from => "#{klass.table_name}, #{Tag.table_name}, #{Tagging.table_name}",
326
359
  :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],
327
- :group => "#{klass.table_name}.id",
360
+ :group => grouped_column_names_for(klass),
328
361
  :order => "count DESC"
329
362
  }.update(options)
330
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 }
331
373
 
374
+ exclude_self = "#{klass.table_name}.id != #{id} AND" if self.class == klass
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
+
332
384
  def save_cached_tag_list
333
385
  self.class.tag_types.map(&:to_s).each do |tag_type|
334
386
  if self.class.send("caching_#{tag_type.singularize}_list?")
335
- 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(', ')
336
388
  end
337
389
  end
338
390
  end
339
391
 
340
392
  def save_tags
341
393
  (custom_contexts + self.class.tag_types.map(&:to_s)).each do |tag_type|
342
- next unless instance_variable_get("@#{tag_type.singularize}_list")
343
- owner = instance_variable_get("@#{tag_type.singularize}_list").owner
344
- new_tag_names = instance_variable_get("@#{tag_type.singularize}_list") - tags_on(tag_type).map(&:name)
345
- old_tags = tags_on(tag_type, owner).reject { |tag| instance_variable_get("@#{tag_type.singularize}_list").include?(tag.name) }
346
-
347
- self.class.transaction do
348
- base_tags.delete(*old_tags) if old_tags.any?
349
- new_tag_names.each do |new_tag_name|
350
- new_tag = Tag.find_or_create_with_like_by_name(new_tag_name)
351
- Tagging.create(:tag_id => new_tag.id, :context => tag_type,
352
- :taggable => self, :tagger => owner)
394
+ tag_list_cache = tag_list_cache_on(tag_type)
395
+ for owner, tag_list in tag_list_cache
396
+ new_tag_names = tag_list - tags_on(tag_type, owner).map(&:name)
397
+ old_tags = tags_on(tag_type, owner).reject { |tag| tag_list.include?(tag.name) }
398
+ transaction do
399
+ base_tags.delete(*old_tags) if old_tags.any?
400
+ new_tag_names.each do |new_tag_name|
401
+ new_tag = Tag.find_or_create_with_like_by_name(new_tag_name)
402
+ Tagging.create(:tag_id => new_tag.id, :context => tag_type,
403
+ :taggable => self, :tagger => owner)
404
+ end
353
405
  end
354
406
  end
355
407
  end
@@ -359,7 +411,7 @@ module ActiveRecord
359
411
 
360
412
  def reload_with_tag_list(*args)
361
413
  self.class.tag_types.each do |tag_type|
362
- self.instance_variable_set("@#{tag_type.to_s.singularize}_list", nil)
414
+ instance_variable_set("@#{tag_type.to_s.singularize}_list", nil)
363
415
  end
364
416
 
365
417
  reload_without_tag_list(*args)
@@ -0,0 +1,12 @@
1
+ module ActiveRecord
2
+ module Acts
3
+ module TaggableOn
4
+ module GroupHelper
5
+ # all column names are necessary for PostgreSQL group clause
6
+ def grouped_column_names_for(object)
7
+ object.column_names.map { |column| "#{object.table_name}.#{column}" }.join(", ")
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -1,9 +1,24 @@
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
 
14
+ ### NAMED SCOPES:
15
+
16
+ named_scope :named, lambda { |name| { :conditions => ["name = ?", name] } }
17
+ named_scope :named_like, lambda { |name| { :conditions => ["name LIKE ?", "%#{name}%"] } }
18
+ named_scope :named_like_any, lambda { |list| { :conditions => list.map { |tag| sanitize_sql(["name LIKE ?", tag.to_s]) }.join(" OR ") } }
19
+
20
+ ### METHODS:
21
+
7
22
  # LIKE is used for cross-database case-insensitivity
8
23
  def self.find_or_create_with_like_by_name(name)
9
24
  find(:first, :conditions => ["name LIKE ?", name]) || create(:name => name)
@@ -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)
@@ -40,14 +44,6 @@ describe "Acts As Taggable On" do
40
44
  @taggable.should respond_to(:tag_list, :skill_list, :language_list)
41
45
  @taggable.should respond_to(:tag_list=, :skill_list=, :language_list=)
42
46
  end
43
-
44
- it "should return all column names joined for Tag GROUP clause" do
45
- TaggableModel.column_names_for_tag_group.should == "tags.id, tags.name"
46
- end
47
-
48
- it "should return all column names joined for TaggableModel GROUP clause" do
49
- TaggableModel.column_names_for_tagging_group.should == "taggable_models.id, taggable_models.name, taggable_models.type"
50
- end
51
47
  end
52
48
 
53
49
  describe "Single Table Inheritance" do
@@ -127,17 +123,59 @@ describe "Acts As Taggable On" do
127
123
  end
128
124
  end
129
125
 
130
- describe 'Tagging Contexts' do
131
- before(:all) do
132
- class Array
133
- def freq
134
- k=Hash.new(0)
135
- self.each {|e| k[e]+=1}
136
- k
137
- end
138
- 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)
139
143
  end
140
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
141
179
  it 'should eliminate duplicate tagging contexts ' do
142
180
  TaggableModel.acts_as_taggable_on(:skills, :skills)
143
181
  TaggableModel.tag_types.freq[:skills].should_not == 3
@@ -164,10 +202,6 @@ describe "Acts As Taggable On" do
164
202
  TaggableModel.acts_as_taggable_on([nil])
165
203
  }.should_not raise_error
166
204
  end
167
-
168
- after(:all) do
169
- class Array; remove_method :freq; end
170
- end
171
205
  end
172
206
 
173
207
  end
@@ -48,8 +48,22 @@ describe "acts_as_tagger" do
48
48
 
49
49
  it 'should by default create the tag context on-the-fly' do
50
50
  @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')
51
+ @tagger.tag(@taggable, :with=>'that', :on => :here_ond_now)
52
+ @taggable.tag_list_on(:here_ond_now).should_not include('that')
53
+ @taggable.all_tags_list_on(:here_ond_now).should include('that')
54
+ end
55
+
56
+ it "should show all the tag list when both public and owned tags exist" do
57
+ @taggable.tag_list = 'ruby, python'
58
+ @tagger.tag(@taggable, :with => 'java, lisp', :on => :tags)
59
+ @taggable.all_tags_on(:tags).map(&:name).sort.should == %w(ruby python java lisp).sort
60
+ end
61
+
62
+ it "should not add owned tags to the common list" do
63
+ @taggable.tag_list = 'ruby, python'
64
+ @tagger.tag(@taggable, :with => 'java, lisp', :on => :foo)
65
+ @tagger.tag(@taggable, :with => '', :on => :foo)
66
+ @taggable.tag_list.should == %w(ruby python)
53
67
  end
54
68
 
55
69
  it "should throw an exception when the default is over-ridden" do
@@ -0,0 +1,18 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ describe "Group Helper" do
4
+
5
+ describe "grouped_column_names_for method" do
6
+ before(:each) do
7
+ @taggable = TaggableModel.new(:name => "Bob Jones")
8
+ end
9
+
10
+ it "should return all column names joined for Tag GROUP clause" do
11
+ @taggable.grouped_column_names_for(Tag).should == "tags.id, tags.name"
12
+ end
13
+
14
+ it "should return all column names joined for TaggableModel GROUP clause" do
15
+ @taggable.grouped_column_names_for(TaggableModel).should == "taggable_models.id, taggable_models.name, taggable_models.type"
16
+ end
17
+ end
18
+ 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,41 @@ 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
+
9
42
  it "should require a name" do
10
43
  @tag.valid?
11
44
  @tag.errors.on(:name).should == "can't be blank"
@@ -24,4 +57,17 @@ describe Tag do
24
57
  @tag.name = "cool"
25
58
  @tag.to_s.should == "cool"
26
59
  end
60
+
61
+ it "have named_scope named(something)" do
62
+ @tag.name = "cool"
63
+ @tag.save!
64
+ Tag.named('cool').should include(@tag)
65
+ end
66
+
67
+ it "have named_scope named_like(something)" do
68
+ @tag.name = "cool"
69
+ @tag.save!
70
+ @another_tag = Tag.create!(:name => "coolip")
71
+ Tag.named_like('cool').should include(@tag, @another_tag)
72
+ end
27
73
  end
@@ -5,22 +5,42 @@ 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"
@@ -29,7 +49,7 @@ describe "Taggable" do
29
49
  @taggable.skill_list.include?("ruby").should be_true
30
50
  @taggable.skill_list.include?("bob").should be_false
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).should == %w(ruby scheme)
28
+ @user2.owned_tags.map(&:name).sort.should == %w(java python lisp ruby).sort
29
+ @taggable.tags_from(@user).should == %w(ruby scheme)
30
+ @taggable.tags_from(@user2).should == %w(java python lisp 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.11
4
+ version: 1.1.0
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-02 00:00:00 -05:00
12
+ date: 2010-02-03 00:00:00 +01:00
13
13
  default_executable:
14
14
  dependencies: []
15
15
 
@@ -27,9 +27,12 @@ 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
35
+ - lib/acts_as_taggable_on/group_helper.rb
33
36
  - lib/acts_as_taggable_on/tag.rb
34
37
  - lib/acts_as_taggable_on/tag_list.rb
35
38
  - lib/acts_as_taggable_on/tagging.rb
@@ -37,11 +40,13 @@ files:
37
40
  - rails/init.rb
38
41
  - spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb
39
42
  - spec/acts_as_taggable_on/acts_as_tagger_spec.rb
43
+ - spec/acts_as_taggable_on/group_helper_spec.rb
40
44
  - spec/acts_as_taggable_on/tag_list_spec.rb
41
45
  - spec/acts_as_taggable_on/tag_spec.rb
42
46
  - spec/acts_as_taggable_on/taggable_spec.rb
43
47
  - spec/acts_as_taggable_on/tagger_spec.rb
44
48
  - spec/acts_as_taggable_on/tagging_spec.rb
49
+ - spec/acts_as_taggable_on/tags_helper_spec.rb
45
50
  - spec/schema.rb
46
51
  - spec/spec.opts
47
52
  - spec/spec_helper.rb
@@ -76,10 +81,12 @@ summary: ActsAsTaggableOn is a tagging plugin for Rails that provides multiple t
76
81
  test_files:
77
82
  - spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb
78
83
  - spec/acts_as_taggable_on/acts_as_tagger_spec.rb
84
+ - spec/acts_as_taggable_on/group_helper_spec.rb
79
85
  - spec/acts_as_taggable_on/tag_list_spec.rb
80
86
  - spec/acts_as_taggable_on/tag_spec.rb
81
87
  - spec/acts_as_taggable_on/taggable_spec.rb
82
88
  - spec/acts_as_taggable_on/tagger_spec.rb
83
89
  - spec/acts_as_taggable_on/tagging_spec.rb
90
+ - spec/acts_as_taggable_on/tags_helper_spec.rb
84
91
  - spec/schema.rb
85
92
  - spec/spec_helper.rb