acts-as-taggable-on 1.0.14 → 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/VERSION CHANGED
@@ -1 +1 @@
1
- 1.0.14
1
+ 1.1.0
@@ -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
@@ -82,11 +82,10 @@ module ActiveRecord
82
82
  end
83
83
  RUBY
84
84
  end
85
-
86
85
  if respond_to?(:tag_types)
87
86
  write_inheritable_attribute( :tag_types, (tag_types + args).uniq )
88
87
  else
89
- self.class_eval do
88
+ class_eval do
90
89
  write_inheritable_attribute(:tag_types, args.uniq)
91
90
  class_inheritable_reader :tag_types
92
91
 
@@ -117,6 +116,7 @@ module ActiveRecord
117
116
  # Pass either a tag string, or an array of strings or tags
118
117
  #
119
118
  # Options:
119
+ # :any - find models that match any of the given tags
120
120
  # :exclude - Find models that are not tagged with the given tags
121
121
  # :match_all - Find models that match all of the given tags, not just one
122
122
  # :conditions - A piece of SQL conditions to add to the query
@@ -139,9 +139,9 @@ module ActiveRecord
139
139
  end
140
140
 
141
141
  def find_options_for_find_tagged_with(tags, options = {})
142
- tags = TagList.from(tags)
142
+ tag_list = TagList.from(tags)
143
143
 
144
- return {} if tags.empty?
144
+ return {} if tag_list.empty?
145
145
 
146
146
  joins = []
147
147
  conditions = []
@@ -150,11 +150,16 @@ module ActiveRecord
150
150
 
151
151
 
152
152
  if options.delete(:exclude)
153
- 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 ")
154
154
  conditions << "#{table_name}.#{primary_key} NOT IN (SELECT #{Tagging.table_name}.taggable_id FROM #{Tagging.table_name} JOIN #{Tag.table_name} ON #{Tagging.table_name}.tag_id = #{Tag.table_name}.id AND (#{tags_conditions}) WHERE #{Tagging.table_name}.taggable_type = #{quote_value(base_class.name)})"
155
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
+
156
160
  else
157
- tags = Tag.named_like_any(tags)
161
+ tags = Tag.named_like_any(tag_list)
162
+ return { :conditions => "1 = 0" } unless tags.length == tag_list.length
158
163
 
159
164
  tags.each do |tag|
160
165
  safe_tag = tag.name.gsub(/[^a-zA-Z0-9]/, '')
@@ -184,7 +189,8 @@ module ActiveRecord
184
189
 
185
190
  { :joins => joins.join(" "),
186
191
  :group => group,
187
- :conditions => conditions.join(" AND ") }.update(options)
192
+ :conditions => conditions.join(" AND "),
193
+ :readonly => false }.update(options)
188
194
  end
189
195
 
190
196
  # Calculate the tag counts for all tags.
@@ -222,14 +228,32 @@ module ActiveRecord
222
228
 
223
229
  joins = ["LEFT OUTER JOIN #{Tagging.table_name} ON #{Tag.table_name}.id = #{Tagging.table_name}.tag_id"]
224
230
  joins << sanitize_sql(["AND #{Tagging.table_name}.context = ?",options.delete(:on).to_s]) unless options[:on].nil?
225
-
226
231
  joins << " INNER JOIN #{table_name} ON #{table_name}.#{primary_key} = #{Tagging.table_name}.taggable_id"
227
- unless self.descends_from_active_record?
232
+
233
+ unless descends_from_active_record?
228
234
  # Current model is STI descendant, so add type checking to the join condition
229
- joins << " AND #{table_name}.#{self.inheritance_column} = '#{self.name}'"
235
+ joins << " AND #{table_name}.#{inheritance_column} = '#{name}'"
230
236
  end
231
237
 
232
- 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
233
257
 
234
258
  at_least = sanitize_sql(['COUNT(*) >= ?', options.delete(:at_least)]) if options[:at_least]
235
259
  at_most = sanitize_sql(['COUNT(*) <= ?', options.delete(:at_most)]) if options[:at_most]
@@ -261,29 +285,40 @@ module ActiveRecord
261
285
  def is_taggable?
262
286
  self.class.is_taggable?
263
287
  end
264
-
288
+
265
289
  def add_custom_context(value)
266
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)
267
291
  end
268
292
 
269
- def tag_list_on(context, owner=nil)
270
- var_name = context.to_s.singularize + "_list"
293
+ def tag_list_on(context, owner = nil)
271
294
  add_custom_context(context)
272
- return instance_variable_get("@#{var_name}") unless instance_variable_get("@#{var_name}").nil?
273
-
295
+ cache = tag_list_cache_on(context)
296
+ return owner ? cache[owner] : cache[owner] if cache[owner]
297
+
274
298
  if !owner && self.class.caching_tag_list_on?(context) and !(cached_value = cached_tag_list_on(context)).nil?
275
- instance_variable_set("@#{var_name}", TagList.from(self["cached_#{var_name}"]))
299
+ cache[owner] = TagList.from(cached_tag_list_on(context))
276
300
  else
277
- instance_variable_set("@#{var_name}", TagList.new(*tags_on(context, owner).map(&:name)))
301
+ cache[owner] = TagList.new(*tags_on(context, owner).map(&:name))
278
302
  end
279
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
280
315
 
281
- def tags_on(context, owner=nil)
316
+ def tags_on(context, owner = nil)
282
317
  if owner
283
318
  opts = {:conditions => ["#{Tagging.table_name}.context = ? AND #{Tagging.table_name}.tagger_id = ? AND #{Tagging.table_name}.tagger_type = ?",
284
319
  context.to_s, owner.id, owner.class.to_s]}
285
320
  else
286
- 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]}
287
322
  end
288
323
  base_tags.find(:all, opts)
289
324
  end
@@ -291,14 +326,21 @@ module ActiveRecord
291
326
  def cached_tag_list_on(context)
292
327
  self["cached_#{context.to_s.singularize}_list"]
293
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
294
336
 
295
- def set_tag_list_on(context,new_list, tagger=nil)
296
- 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)
297
339
  add_custom_context(context)
298
340
  end
299
341
 
300
342
  def tag_counts_on(context, options={})
301
- self.class.tag_counts_on(context, options.merge(:id => self.id))
343
+ self.class.tag_counts_on(context, options.merge(:id => id))
302
344
  end
303
345
 
304
346
  def related_tags_for(context, klass, options = {})
@@ -308,9 +350,9 @@ module ActiveRecord
308
350
  end
309
351
 
310
352
  def related_search_options(context, klass, options = {})
311
- tags_to_find = self.tags_on(context).collect { |t| t.name }
353
+ tags_to_find = tags_on(context).collect { |t| t.name }
312
354
 
313
- 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
314
356
 
315
357
  { :select => "#{klass.table_name}.*, COUNT(#{Tag.table_name}.id) AS count",
316
358
  :from => "#{klass.table_name}, #{Tag.table_name}, #{Tagging.table_name}",
@@ -327,9 +369,9 @@ module ActiveRecord
327
369
  end
328
370
 
329
371
  def matching_context_search_options(search_context, result_context, klass, options = {})
330
- tags_to_find = self.tags_on(search_context).collect { |t| t.name }
372
+ tags_to_find = tags_on(search_context).collect { |t| t.name }
331
373
 
332
- exclude_self = "#{klass.table_name}.id != #{self.id} AND" if self.class == klass
374
+ exclude_self = "#{klass.table_name}.id != #{id} AND" if self.class == klass
333
375
 
334
376
  { :select => "#{klass.table_name}.*, COUNT(#{Tag.table_name}.id) AS count",
335
377
  :from => "#{klass.table_name}, #{Tag.table_name}, #{Tagging.table_name}",
@@ -342,24 +384,24 @@ module ActiveRecord
342
384
  def save_cached_tag_list
343
385
  self.class.tag_types.map(&:to_s).each do |tag_type|
344
386
  if self.class.send("caching_#{tag_type.singularize}_list?")
345
- 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(', ')
346
388
  end
347
389
  end
348
390
  end
349
391
 
350
392
  def save_tags
351
393
  (custom_contexts + self.class.tag_types.map(&:to_s)).each do |tag_type|
352
- next unless instance_variable_get("@#{tag_type.singularize}_list")
353
- owner = instance_variable_get("@#{tag_type.singularize}_list").owner
354
- new_tag_names = instance_variable_get("@#{tag_type.singularize}_list") - tags_on(tag_type).map(&:name)
355
- old_tags = tags_on(tag_type, owner).reject { |tag| instance_variable_get("@#{tag_type.singularize}_list").include?(tag.name) }
356
-
357
- self.class.transaction do
358
- base_tags.delete(*old_tags) if old_tags.any?
359
- new_tag_names.each do |new_tag_name|
360
- new_tag = Tag.find_or_create_with_like_by_name(new_tag_name)
361
- Tagging.create(:tag_id => new_tag.id, :context => tag_type,
362
- :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
363
405
  end
364
406
  end
365
407
  end
@@ -369,7 +411,7 @@ module ActiveRecord
369
411
 
370
412
  def reload_with_tag_list(*args)
371
413
  self.class.tag_types.each do |tag_type|
372
- self.instance_variable_set("@#{tag_type.to_s.singularize}_list", nil)
414
+ instance_variable_set("@#{tag_type.to_s.singularize}_list", nil)
373
415
  end
374
416
 
375
417
  reload_without_tag_list(*args)
@@ -1,13 +1,24 @@
1
1
  class Tag < ActiveRecord::Base
2
+
3
+ attr_accessible :name
4
+
5
+ ### ASSOCIATIONS:
6
+
2
7
  has_many :taggings, :dependent => :destroy
3
8
 
9
+ ### VALIDATIONS:
10
+
4
11
  validates_presence_of :name
5
12
  validates_uniqueness_of :name
6
13
 
14
+ ### NAMED SCOPES:
15
+
7
16
  named_scope :named, lambda { |name| { :conditions => ["name = ?", name] } }
8
17
  named_scope :named_like, lambda { |name| { :conditions => ["name LIKE ?", "%#{name}%"] } }
9
18
  named_scope :named_like_any, lambda { |list| { :conditions => list.map { |tag| sanitize_sql(["name LIKE ?", tag.to_s]) }.join(" OR ") } }
10
19
 
20
+ ### METHODS:
21
+
11
22
  # LIKE is used for cross-database case-insensitivity
12
23
  def self.find_or_create_with_like_by_name(name)
13
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,8 +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
5
9
 
6
10
  validates_presence_of :context
7
11
  validates_presence_of :tag_id
12
+
13
+ validates_uniqueness_of :tag_id, :scope => [:taggable_type, :taggable_id, :context, :tagger_id, :tagger_type]
8
14
  end
data/rails/init.rb CHANGED
@@ -2,6 +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
- ActionView::Base.send :include, TagsHelper if defined?(ActionView::Base)
6
-
7
- RAILS_DEFAULT_LOGGER.info "** acts_as_taggable_on: initialized properly."
5
+ ActionView::Base.send :include, TagsHelper if defined?(ActionView::Base)
@@ -176,16 +176,6 @@ describe "Acts As Taggable On" do
176
176
  end
177
177
 
178
178
  describe 'Tagging Contexts' do
179
- before(:all) do
180
- class Array
181
- def freq
182
- k=Hash.new(0)
183
- self.each {|e| k[e]+=1}
184
- k
185
- end
186
- end
187
- end
188
-
189
179
  it 'should eliminate duplicate tagging contexts ' do
190
180
  TaggableModel.acts_as_taggable_on(:skills, :skills)
191
181
  TaggableModel.tag_types.freq[:skills].should_not == 3
@@ -212,10 +202,6 @@ describe "Acts As Taggable On" do
212
202
  TaggableModel.acts_as_taggable_on([nil])
213
203
  }.should_not raise_error
214
204
  end
215
-
216
- after(:all) do
217
- class Array; remove_method :freq; end
218
- end
219
205
  end
220
206
 
221
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
@@ -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
@@ -7,35 +7,40 @@ describe "Taggable" do
7
7
  end
8
8
 
9
9
  it "should have tag types" do
10
- TaggableModel.tag_types.should == [:tags, :languages, :skills, :needs, :offerings]
11
- @taggable.tag_types.should == TaggableModel.tag_types
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
12
14
  end
13
15
 
14
16
  it "should have tag_counts_on" do
15
17
  TaggableModel.tag_counts_on(:tags).should be_empty
16
-
18
+
17
19
  @taggable.tag_list = ["awesome", "epic"]
18
20
  @taggable.save
19
21
 
20
22
  TaggableModel.tag_counts_on(:tags).count.should == 2
21
23
  @taggable.tag_counts_on(:tags).count.should == 2
22
24
  end
23
-
25
+
24
26
  it "should be able to create tags" do
25
27
  @taggable.skill_list = "ruby, rails, css"
26
- @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
27
30
  @taggable.save
28
-
31
+
29
32
  Tag.find(:all).size.should == 3
30
33
  end
31
-
34
+
32
35
  it "should be able to create tags through the tag list directly" do
33
36
  @taggable.tag_list_on(:test).add("hello")
34
- @taggable.save
37
+ @taggable.tag_list_cache_on(:test).should_not be_empty
38
+ @taggable.save
39
+ @taggable.save_tags
35
40
  @taggable.reload
36
41
  @taggable.tag_list_on(:test).should == ["hello"]
37
42
  end
38
-
43
+
39
44
  it "should differentiate between contexts" do
40
45
  @taggable.skill_list = "ruby, rails, css"
41
46
  @taggable.tag_list = "ruby, bob, charlie"
@@ -44,7 +49,7 @@ describe "Taggable" do
44
49
  @taggable.skill_list.include?("ruby").should be_true
45
50
  @taggable.skill_list.include?("bob").should be_false
46
51
  end
47
-
52
+
48
53
  it "should be able to remove tags through list alone" do
49
54
  @taggable.skill_list = "ruby, rails, css"
50
55
  @taggable.save
@@ -55,13 +60,13 @@ describe "Taggable" do
55
60
  @taggable.reload
56
61
  @taggable.should have(2).skills
57
62
  end
58
-
63
+
59
64
  it "should be able to find by tag" do
60
65
  @taggable.skill_list = "ruby, rails, css"
61
66
  @taggable.save
62
67
  TaggableModel.find_tagged_with("ruby").first.should == @taggable
63
68
  end
64
-
69
+
65
70
  it "should be able to find by tag with context" do
66
71
  @taggable.skill_list = "ruby, rails, css"
67
72
  @taggable.tag_list = "bob, charlie"
@@ -70,25 +75,27 @@ describe "Taggable" do
70
75
  TaggableModel.find_tagged_with("bob", :on => :skills).first.should_not == @taggable
71
76
  TaggableModel.find_tagged_with("bob", :on => :tags).first.should == @taggable
72
77
  end
73
-
78
+
74
79
  it "should be able to use the tagged_with named scope" do
75
80
  @taggable.skill_list = "ruby, rails, css"
76
81
  @taggable.tag_list = "bob, charlie"
77
82
  @taggable.save
78
-
83
+
79
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
80
87
  TaggableModel.tagged_with("bob", :on => :skills).first.should_not == @taggable
81
88
  TaggableModel.tagged_with("bob", :on => :tags).first.should == @taggable
82
89
  end
83
-
90
+
84
91
  it "should not care about case" do
85
92
  bob = TaggableModel.create(:name => "Bob", :tag_list => "ruby")
86
93
  frank = TaggableModel.create(:name => "Frank", :tag_list => "Ruby")
87
-
94
+
88
95
  Tag.find(:all).size.should == 1
89
96
  TaggableModel.find_tagged_with("ruby").should == TaggableModel.find_tagged_with("Ruby")
90
97
  end
91
-
98
+
92
99
  it "should be able to get tag counts on model as a whole" do
93
100
  bob = TaggableModel.create(:name => "Bob", :tag_list => "ruby, rails, css")
94
101
  frank = TaggableModel.create(:name => "Frank", :tag_list => "ruby, rails")
@@ -96,33 +103,39 @@ describe "Taggable" do
96
103
  TaggableModel.tag_counts.should_not be_empty
97
104
  TaggableModel.skill_counts.should_not be_empty
98
105
  end
99
-
106
+
100
107
  it "should be able to get all tag counts on model as whole" do
101
108
  bob = TaggableModel.create(:name => "Bob", :tag_list => "ruby, rails, css")
102
109
  frank = TaggableModel.create(:name => "Frank", :tag_list => "ruby, rails")
103
110
  charlie = TaggableModel.create(:name => "Charlie", :skill_list => "ruby")
104
-
111
+
105
112
  TaggableModel.all_tag_counts.should_not be_empty
106
113
  TaggableModel.all_tag_counts.first.count.should == 3 # ruby
107
114
  end
108
-
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
+
109
122
  it "should be able to get scoped tag counts" do
110
123
  bob = TaggableModel.create(:name => "Bob", :tag_list => "ruby, rails, css")
111
124
  frank = TaggableModel.create(:name => "Frank", :tag_list => "ruby, rails")
112
125
  charlie = TaggableModel.create(:name => "Charlie", :skill_list => "ruby")
113
-
126
+
114
127
  TaggableModel.tagged_with("ruby").tag_counts.first.count.should == 2 # ruby
115
128
  TaggableModel.tagged_with("ruby").skill_counts.first.count.should == 1 # ruby
116
129
  end
117
-
130
+
118
131
  it "should be able to get all scoped tag counts" do
119
132
  bob = TaggableModel.create(:name => "Bob", :tag_list => "ruby, rails, css")
120
133
  frank = TaggableModel.create(:name => "Frank", :tag_list => "ruby, rails")
121
134
  charlie = TaggableModel.create(:name => "Charlie", :skill_list => "ruby")
122
-
135
+
123
136
  TaggableModel.tagged_with("ruby").all_tag_counts.first.count.should == 3 # ruby
124
137
  end
125
-
138
+
126
139
  it "should be able to set a custom tag context list" do
127
140
  bob = TaggableModel.create(:name => "Bob")
128
141
  bob.set_tag_list_on(:rotors, "spinning, jumping")
@@ -131,17 +144,27 @@ describe "Taggable" do
131
144
  bob.reload
132
145
  bob.tags_on(:rotors).should_not be_empty
133
146
  end
134
-
147
+
135
148
  it "should be able to find tagged" do
136
149
  bob = TaggableModel.create(:name => "Bob", :tag_list => "fitter, happier, more productive", :skill_list => "ruby, rails, css")
137
150
  frank = TaggableModel.create(:name => "Frank", :tag_list => "weaker, depressed, inefficient", :skill_list => "ruby, rails, css")
138
151
  steve = TaggableModel.create(:name => 'Steve', :tag_list => 'fitter, happier, more productive', :skill_list => 'c++, java, ruby')
139
-
152
+
140
153
  TaggableModel.find_tagged_with("ruby", :order => 'taggable_models.name').should == [bob, frank, steve]
141
154
  TaggableModel.find_tagged_with("ruby, rails", :order => 'taggable_models.name').should == [bob, frank]
142
155
  TaggableModel.find_tagged_with(["ruby", "rails"], :order => 'taggable_models.name').should == [bob, frank]
143
156
  end
144
-
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
+
145
168
  it "should be able to find tagged on a custom tag context" do
146
169
  bob = TaggableModel.create(:name => "Bob")
147
170
  bob.set_tag_list_on(:rotors, "spinning, jumping")
@@ -154,29 +177,38 @@ describe "Taggable" do
154
177
  bob = TaggableModel.create(:name => "Bob", :tag_list => "fitter, happier, more productive", :skill_list => "ruby, rails, css")
155
178
  frank = TaggableModel.create(:name => "Frank", :tag_list => "weaker, depressed, inefficient", :skill_list => "ruby, rails, css")
156
179
  steve = TaggableModel.create(:name => 'Steve', :tag_list => 'fitter, happier, more productive', :skill_list => 'c++, java, python')
157
-
180
+
158
181
  # Let's only find those productive Rails developers
159
182
  TaggableModel.tagged_with('rails', :on => :skills, :order => 'taggable_models.name').should == [bob, frank]
160
183
  TaggableModel.tagged_with('happier', :on => :tags, :order => 'taggable_models.name').should == [bob, steve]
161
184
  TaggableModel.tagged_with('rails', :on => :skills).tagged_with('happier', :on => :tags).should == [bob]
162
185
  TaggableModel.tagged_with('rails').tagged_with('happier', :on => :tags).should == [bob]
163
186
  end
164
-
187
+
165
188
  it "should be able to find tagged with only the matching tags" do
166
189
  bob = TaggableModel.create(:name => "Bob", :tag_list => "lazy, happier")
167
190
  frank = TaggableModel.create(:name => "Frank", :tag_list => "fitter, happier, inefficient")
168
191
  steve = TaggableModel.create(:name => 'Steve', :tag_list => "fitter, happier")
169
-
192
+
170
193
  TaggableModel.find_tagged_with("fitter, happier", :match_all => true).should == [steve]
171
194
  end
172
-
195
+
173
196
  it "should be able to find tagged with some excluded tags" do
174
197
  bob = TaggableModel.create(:name => "Bob", :tag_list => "happier, lazy")
175
198
  frank = TaggableModel.create(:name => "Frank", :tag_list => "happier")
176
199
  steve = TaggableModel.create(:name => 'Steve', :tag_list => "happier")
177
-
200
+
178
201
  TaggableModel.find_tagged_with("lazy", :exclude => true).should == [frank, steve]
179
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
180
212
 
181
213
  describe "Single Table Inheritance" do
182
214
  before do
@@ -185,32 +217,32 @@ describe "Taggable" do
185
217
  @inherited_same = InheritingTaggableModel.new(:name => "inherited same")
186
218
  @inherited_different = AlteredInheritingTaggableModel.new(:name => "inherited different")
187
219
  end
188
-
220
+
189
221
  it "should be able to save tags for inherited models" do
190
222
  @inherited_same.tag_list = "bob, kelso"
191
223
  @inherited_same.save
192
224
  InheritingTaggableModel.find_tagged_with("bob").first.should == @inherited_same
193
225
  end
194
-
226
+
195
227
  it "should find STI tagged models on the superclass" do
196
228
  @inherited_same.tag_list = "bob, kelso"
197
229
  @inherited_same.save
198
230
  TaggableModel.find_tagged_with("bob").first.should == @inherited_same
199
231
  end
200
-
232
+
201
233
  it "should be able to add on contexts only to some subclasses" do
202
234
  @inherited_different.part_list = "fork, spoon"
203
235
  @inherited_different.save
204
236
  InheritingTaggableModel.find_tagged_with("fork", :on => :parts).should be_empty
205
237
  AlteredInheritingTaggableModel.find_tagged_with("fork", :on => :parts).first.should == @inherited_different
206
238
  end
207
-
239
+
208
240
  it "should have different tag_counts_on for inherited models" do
209
241
  @inherited_same.tag_list = "bob, kelso"
210
242
  @inherited_same.save!
211
243
  @inherited_different.tag_list = "fork, spoon"
212
244
  @inherited_different.save!
213
-
245
+
214
246
  InheritingTaggableModel.tag_counts_on(:tags).map(&:name).should == %w(bob kelso)
215
247
  AlteredInheritingTaggableModel.tag_counts_on(:tags).map(&:name).should == %w(fork spoon)
216
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
@@ -13,4 +13,13 @@ describe Tagging do
13
13
  @tagging.should_not be_valid
14
14
  @tagging.errors.on(:tag_id).should == "can't be blank"
15
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
16
25
  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,7 +24,10 @@ 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')
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.14
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-17 00:00:00 +01:00
12
+ date: 2010-02-03 00:00:00 +01:00
13
13
  default_executable:
14
14
  dependencies: []
15
15