acts-as-taggable-on 2.3.3 → 2.4.0.beta

Sign up to get free protection for your applications and to get access to all the features.
@@ -14,6 +14,7 @@ module ActsAsTaggableOn::Taggable
14
14
 
15
15
  module ClassMethods
16
16
  def initialize_acts_as_taggable_on_core
17
+ include taggable_mixin
17
18
  tag_types.map(&:to_s).each do |tags_type|
18
19
  tag_type = tags_type.to_s.singularize
19
20
  context_taggings = "#{tag_type}_taggings".to_sym
@@ -36,7 +37,7 @@ module ActsAsTaggableOn::Taggable
36
37
  :order => taggings_order
37
38
  end
38
39
 
39
- class_eval %(
40
+ taggable_mixin.class_eval <<-RUBY, __FILE__, __LINE__ + 1
40
41
  def #{tag_type}_list
41
42
  tag_list_on('#{tags_type}')
42
43
  end
@@ -48,7 +49,7 @@ module ActsAsTaggableOn::Taggable
48
49
  def all_#{tags_type}_list
49
50
  all_tags_list_on('#{tags_type}')
50
51
  end
51
- )
52
+ RUBY
52
53
  end
53
54
  end
54
55
 
@@ -189,6 +190,10 @@ module ActsAsTaggableOn::Taggable
189
190
  end
190
191
  taggings_alias
191
192
  end
193
+
194
+ def taggable_mixin
195
+ @taggable_mixin ||= Module.new
196
+ end
192
197
  end
193
198
 
194
199
  module InstanceMethods
@@ -215,12 +220,13 @@ module ActsAsTaggableOn::Taggable
215
220
 
216
221
  def tag_list_cache_set_on(context)
217
222
  variable_name = "@#{context.to_s.singularize}_list"
218
- !instance_variable_get(variable_name).nil?
223
+ instance_variable_defined?(variable_name) && !instance_variable_get(variable_name).nil?
219
224
  end
220
225
 
221
226
  def tag_list_cache_on(context)
222
227
  variable_name = "@#{context.to_s.singularize}_list"
223
- instance_variable_get(variable_name) || instance_variable_set(variable_name, ActsAsTaggableOn::TagList.new(tags_on(context).map(&:name)))
228
+ return instance_variable_get(variable_name) if instance_variable_defined?(variable_name) && instance_variable_get(variable_name)
229
+ instance_variable_set(variable_name, ActsAsTaggableOn::TagList.new(tags_on(context).map(&:name)))
224
230
  end
225
231
 
226
232
  def tag_list_on(context)
@@ -230,7 +236,7 @@ module ActsAsTaggableOn::Taggable
230
236
 
231
237
  def all_tags_list_on(context)
232
238
  variable_name = "@all_#{context.to_s.singularize}_list"
233
- return instance_variable_get(variable_name) if instance_variable_get(variable_name)
239
+ return instance_variable_get(variable_name) if instance_variable_defined?(variable_name) && instance_variable_get(variable_name)
234
240
 
235
241
  instance_variable_set(variable_name, ActsAsTaggableOn::TagList.new(all_tags_on(context).map(&:name)).freeze)
236
242
  end
@@ -12,7 +12,7 @@ module ActsAsTaggableOn::Taggable
12
12
  tag_type = tags_type.to_s.singularize
13
13
  context_tags = tags_type.to_sym
14
14
 
15
- class_eval %(
15
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
16
16
  def #{tag_type}_list_changed?
17
17
  changed_attributes.include?("#{tag_type}_list")
18
18
  end
@@ -28,7 +28,7 @@ module ActsAsTaggableOn::Taggable
28
28
  def #{tag_type}_list_changes
29
29
  [changed_attributes['#{tag_type}_list'], __send__('#{tag_type}_list')] if changed_attributes.include?("#{tag_type}_list")
30
30
  end
31
- )
31
+ RUBY
32
32
 
33
33
  end
34
34
  end
@@ -19,11 +19,11 @@ module ActsAsTaggableOn::Taggable
19
19
 
20
20
  def initialize_acts_as_taggable_on_ownership
21
21
  tag_types.map(&:to_s).each do |tag_type|
22
- class_eval %(
22
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
23
23
  def #{tag_type}_from(owner)
24
24
  owner_tag_list_on(owner, '#{tag_type}')
25
25
  end
26
- )
26
+ RUBY
27
27
  end
28
28
  end
29
29
  end
@@ -45,7 +45,7 @@ module ActsAsTaggableOn::Taggable
45
45
 
46
46
  def cached_owned_tag_list_on(context)
47
47
  variable_name = "@owned_#{context}_list"
48
- cache = instance_variable_get(variable_name) || instance_variable_set(variable_name, {})
48
+ cache = (instance_variable_defined?(variable_name) && instance_variable_get(variable_name)) || instance_variable_set(variable_name, {})
49
49
  end
50
50
 
51
51
  def owner_tag_list_on(owner, context)
@@ -9,7 +9,7 @@ module ActsAsTaggableOn::Taggable
9
9
  module ClassMethods
10
10
  def initialize_acts_as_taggable_on_related
11
11
  tag_types.map(&:to_s).each do |tag_type|
12
- class_eval %(
12
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
13
13
  def find_related_#{tag_type}(options = {})
14
14
  related_tags_for('#{tag_type}', self.class, options)
15
15
  end
@@ -18,11 +18,11 @@ module ActsAsTaggableOn::Taggable
18
18
  def find_related_#{tag_type}_for(klass, options = {})
19
19
  related_tags_for('#{tag_type}', klass, options)
20
20
  end
21
- )
21
+ RUBY
22
22
  end
23
23
 
24
24
  unless tag_types.empty?
25
- class_eval %(
25
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
26
26
  def find_matching_contexts(search_context, result_context, options = {})
27
27
  matching_contexts_for(search_context.to_s, result_context.to_s, self.class, options)
28
28
  end
@@ -30,7 +30,7 @@ module ActsAsTaggableOn::Taggable
30
30
  def find_matching_contexts_for(klass, search_context, result_context, options = {})
31
31
  matching_contexts_for(search_context.to_s, result_context.to_s, klass, options)
32
32
  end
33
- )
33
+ RUBY
34
34
  end
35
35
  end
36
36
 
@@ -56,7 +56,9 @@ module ActsAsTaggableOn::Taggable
56
56
  end
57
57
 
58
58
  def related_tags_for(context, klass, options = {})
59
- tags_to_find = tags_on(context).collect { |t| t.name }
59
+ tags_to_ignore = Array.wrap(options.delete(:ignore)) || []
60
+ tags_to_ignore.map! { |t| t.to_s }
61
+ tags_to_find = tags_on(context).collect { |t| t.name }.reject { |t| tags_to_ignore.include? t }
60
62
 
61
63
  exclude_self = "#{klass.table_name}.#{klass.primary_key} != #{id} AND" if [self.class.base_class, self.class].include? klass
62
64
 
@@ -11,17 +11,30 @@ module ActsAsTaggableOn
11
11
  ### VALIDATIONS:
12
12
 
13
13
  validates_presence_of :name
14
- validates_uniqueness_of :name
14
+ validates_uniqueness_of :name, :if => :validates_name_uniqueness?
15
15
  validates_length_of :name, :maximum => 255
16
16
 
17
+ # monkey patch this method if don't need name uniqueness validation
18
+ def validates_name_uniqueness?
19
+ true
20
+ end
21
+
17
22
  ### SCOPES:
18
23
 
19
24
  def self.named(name)
20
- where(["lower(name) = ?", name.downcase])
25
+ if ActsAsTaggableOn.strict_case_match
26
+ where(["name = #{binary}?", name])
27
+ else
28
+ where(["lower(name) = ?", name.downcase])
29
+ end
21
30
  end
22
31
 
23
32
  def self.named_any(list)
24
- where(list.map { |tag| sanitize_sql(["lower(name) = ?", tag.to_s.mb_chars.downcase]) }.join(" OR "))
33
+ if ActsAsTaggableOn.strict_case_match
34
+ where(list.map { |tag| sanitize_sql(["name = #{binary}?", tag.to_s.mb_chars]) }.join(" OR "))
35
+ else
36
+ where(list.map { |tag| sanitize_sql(["lower(name) = ?", tag.to_s.mb_chars.downcase]) }.join(" OR "))
37
+ end
25
38
  end
26
39
 
27
40
  def self.named_like(name)
@@ -35,7 +48,11 @@ module ActsAsTaggableOn
35
48
  ### CLASS METHODS:
36
49
 
37
50
  def self.find_or_create_with_like_by_name(name)
38
- named_like(name).first || create(:name => name)
51
+ if (ActsAsTaggableOn.strict_case_match)
52
+ self.find_or_create_all_with_like_by_name([name]).first
53
+ else
54
+ named_like(name).first || create(:name => name)
55
+ end
39
56
  end
40
57
 
41
58
  def self.find_or_create_all_with_like_by_name(*list)
@@ -45,9 +62,9 @@ module ActsAsTaggableOn
45
62
 
46
63
  existing_tags = Tag.named_any(list).all
47
64
  new_tag_names = list.reject do |name|
48
- name = comparable_name(name)
49
- existing_tags.any? { |tag| comparable_name(tag.name) == name }
50
- end
65
+ name = comparable_name(name)
66
+ existing_tags.any? { |tag| comparable_name(tag.name) == name }
67
+ end
51
68
  created_tags = new_tag_names.map { |name| Tag.create(:name => name) }
52
69
 
53
70
  existing_tags + created_tags
@@ -69,9 +86,14 @@ module ActsAsTaggableOn
69
86
 
70
87
  class << self
71
88
  private
72
- def comparable_name(str)
73
- str.mb_chars.downcase.to_s
74
- end
89
+
90
+ def comparable_name(str)
91
+ str.mb_chars.downcase.to_s
92
+ end
93
+
94
+ def binary
95
+ /mysql/ === ActiveRecord::Base.connection_config[:adapter] ? "BINARY " : nil
96
+ end
75
97
  end
76
98
  end
77
99
  end
@@ -58,7 +58,7 @@ module ActsAsTaggableOn
58
58
  end
59
59
 
60
60
  ##
61
- # Transform the tag_list into a tag string suitable for edting in a form.
61
+ # Transform the tag_list into a tag string suitable for editing in a form.
62
62
  # The tags are joined with <tt>TagList.delimiter</tt> and quoted if necessary.
63
63
  #
64
64
  # Example:
@@ -81,7 +81,7 @@ module ActsAsTaggableOn
81
81
  def clean!
82
82
  reject!(&:blank?)
83
83
  map!(&:strip)
84
- map!(&:downcase) if ActsAsTaggableOn.force_lowercase
84
+ map!{ |tag| tag.mb_chars.downcase.to_s } if ActsAsTaggableOn.force_lowercase
85
85
  map!(&:parameterize) if ActsAsTaggableOn.force_parameterize
86
86
 
87
87
  uniq!
@@ -185,6 +185,39 @@ describe "Acts As Taggable On" do
185
185
  taggable1.find_related_tags.should_not include(taggable1)
186
186
  end
187
187
 
188
+ context "Ignored Tags" do
189
+ let(:taggable1) { TaggableModel.create!(:name => "Taggable 1") }
190
+ let(:taggable2) { TaggableModel.create!(:name => "Taggable 2") }
191
+ let(:taggable3) { TaggableModel.create!(:name => "Taggable 3") }
192
+ before(:each) do
193
+ taggable1.tag_list = "one, two, four"
194
+ taggable1.save
195
+
196
+ taggable2.tag_list = "two, three"
197
+ taggable2.save
198
+
199
+ taggable3.tag_list = "one, three"
200
+ taggable3.save
201
+ end
202
+ it "should not include ignored tags in related search" do
203
+ taggable1.find_related_tags(:ignore => 'two').should_not include(taggable2)
204
+ taggable1.find_related_tags(:ignore => 'two').should include(taggable3)
205
+ end
206
+
207
+ it "should accept array of ignored tags" do
208
+ taggable4 = TaggableModel.create!(:name => "Taggable 4")
209
+ taggable4.tag_list = "four"
210
+ taggable4.save
211
+
212
+ taggable1.find_related_tags(:ignore => ['two', 'four']).should_not include(taggable2)
213
+ taggable1.find_related_tags(:ignore => ['two', 'four']).should_not include(taggable4)
214
+ end
215
+
216
+ it "should accept symbols as ignored tags" do
217
+ taggable1.find_related_tags(:ignore => :two).should_not include(taggable2)
218
+ end
219
+ end
220
+
188
221
  context "Inherited Models" do
189
222
  before do
190
223
  @taggable1 = InheritingTaggableModel.create!(:name => "InheritingTaggable 1")
@@ -84,8 +84,8 @@ describe ActsAsTaggableOn::TagList do
84
84
  it "should lowercase if force_lowercase is set to true" do
85
85
  ActsAsTaggableOn.force_lowercase = true
86
86
 
87
- tag_list = ActsAsTaggableOn::TagList.new("aweSomE","RaDicaL")
88
- tag_list.to_s.should == "awesome, radical"
87
+ tag_list = ActsAsTaggableOn::TagList.new("aweSomE","RaDicaL","Entrée")
88
+ tag_list.to_s.should == "awesome, radical, entrée"
89
89
 
90
90
  ActsAsTaggableOn.force_lowercase = false
91
91
  end
@@ -150,4 +150,63 @@ describe ActsAsTaggableOn::Tag do
150
150
 
151
151
  end
152
152
 
153
- end
153
+ describe "when using strict_case_match" do
154
+ before do
155
+ ActsAsTaggableOn.strict_case_match = true
156
+ @tag.name = "awesome"
157
+ @tag.save!
158
+ end
159
+
160
+ after do
161
+ ActsAsTaggableOn.strict_case_match = false
162
+ end
163
+
164
+ it "should find by name" do
165
+ ActsAsTaggableOn::Tag.find_or_create_with_like_by_name("awesome").should == @tag
166
+ end
167
+
168
+ it "should find by name case sensitively" do
169
+ expect {
170
+ ActsAsTaggableOn::Tag.find_or_create_with_like_by_name("AWESOME")
171
+ }.to change(ActsAsTaggableOn::Tag, :count)
172
+
173
+ ActsAsTaggableOn::Tag.last.name.should == "AWESOME"
174
+ end
175
+
176
+ it "should have a named_scope named(something) that matches exactly" do
177
+ uppercase_tag = ActsAsTaggableOn::Tag.create(:name => "Cool")
178
+ @tag.name = "cool"
179
+ @tag.save!
180
+
181
+ ActsAsTaggableOn::Tag.named('cool').should include(@tag)
182
+ ActsAsTaggableOn::Tag.named('cool').should_not include(uppercase_tag)
183
+ end
184
+ end
185
+
186
+ describe "name uniqeness validation" do
187
+ let(:duplicate_tag) { ActsAsTaggableOn::Tag.new(:name => 'ror') }
188
+
189
+ before { ActsAsTaggableOn::Tag.create(:name => 'ror') }
190
+
191
+ context "when don't need unique names" do
192
+ it "should not run uniqueness validation" do
193
+ duplicate_tag.stub(:validates_name_uniqueness?).and_return(false)
194
+ duplicate_tag.save
195
+ duplicate_tag.should be_persisted
196
+ end
197
+ end
198
+
199
+ context "when do need unique names" do
200
+ it "should run uniqueness validation" do
201
+ duplicate_tag.should_not be_valid
202
+ end
203
+
204
+ it "add error to name" do
205
+ duplicate_tag.save
206
+
207
+ duplicate_tag.should have(1).errors
208
+ duplicate_tag.errors.messages[:name].should include('has already been taken')
209
+ end
210
+ end
211
+ end
212
+ end
@@ -134,6 +134,16 @@ describe "Taggable" do
134
134
  @taggable.tag_counts_on(:tags).length.should == 2
135
135
  end
136
136
 
137
+ it "should have tags_on" do
138
+ TaggableModel.tags_on(:tags).all.should be_empty
139
+
140
+ @taggable.tag_list = ["awesome", "epic"]
141
+ @taggable.save
142
+
143
+ TaggableModel.tags_on(:tags).length.should == 2
144
+ @taggable.tags_on(:tags).length.should == 2
145
+ end
146
+
137
147
  it "should return [] right after create" do
138
148
  blank_taggable = TaggableModel.new(:name => "Bob Jones")
139
149
  blank_taggable.tag_list.should == []
@@ -242,6 +252,15 @@ describe "Taggable" do
242
252
  TaggableModel.all_tag_counts(:order => 'tags.id').first.count.should == 3 # ruby
243
253
  end
244
254
 
255
+ it "should be able to get all tags on model as whole" do
256
+ bob = TaggableModel.create(:name => "Bob", :tag_list => "ruby, rails, css")
257
+ frank = TaggableModel.create(:name => "Frank", :tag_list => "ruby, rails")
258
+ charlie = TaggableModel.create(:name => "Charlie", :skill_list => "ruby")
259
+
260
+ TaggableModel.all_tags.all.should_not be_empty
261
+ TaggableModel.all_tags(:order => 'tags.id').first.name.should == "ruby"
262
+ end
263
+
245
264
  it "should be able to use named scopes to chain tag finds by any tags by context" do
246
265
  bob = TaggableModel.create(:name => "Bob", :need_list => "rails", :offering_list => "c++")
247
266
  frank = TaggableModel.create(:name => "Frank", :need_list => "css", :offering_list => "css")
@@ -273,6 +292,14 @@ describe "Taggable" do
273
292
  TaggableModel.tagged_with("ruby").all_tag_counts(:order => 'tags.id').first.count.should == 3 # ruby
274
293
  end
275
294
 
295
+ it "should be able to get all scoped tags" do
296
+ bob = TaggableModel.create(:name => "Bob", :tag_list => "ruby, rails, css")
297
+ frank = TaggableModel.create(:name => "Frank", :tag_list => "ruby, rails")
298
+ charlie = TaggableModel.create(:name => "Charlie", :skill_list => "ruby")
299
+
300
+ TaggableModel.tagged_with("ruby").all_tags(:order => 'tags.id').first.name.should == "ruby"
301
+ end
302
+
276
303
  it 'should only return tag counts for the available scope' do
277
304
  bob = TaggableModel.create(:name => "Bob", :tag_list => "ruby, rails, css")
278
305
  frank = TaggableModel.create(:name => "Frank", :tag_list => "ruby, rails")
@@ -288,6 +315,21 @@ describe "Taggable" do
288
315
  TaggableModel.tagged_with('rails').scoped(:joins => [:untaggable_models]).all_tag_counts.should have(2).items
289
316
  end
290
317
 
318
+ it 'should only return tags for the available scope' do
319
+ bob = TaggableModel.create(:name => "Bob", :tag_list => "ruby, rails, css")
320
+ frank = TaggableModel.create(:name => "Frank", :tag_list => "ruby, rails")
321
+ charlie = TaggableModel.create(:name => "Charlie", :skill_list => "ruby, java")
322
+
323
+ TaggableModel.tagged_with('rails').all_tags.should have(3).items
324
+ TaggableModel.tagged_with('rails').all_tags.any? { |tag| tag.name == 'java' }.should be_false
325
+
326
+ # Test specific join syntaxes:
327
+ frank.untaggable_models.create!
328
+ TaggableModel.tagged_with('rails').scoped(:joins => :untaggable_models).all_tags.should have(2).items
329
+ TaggableModel.tagged_with('rails').scoped(:joins => { :untaggable_models => :taggable_model }).all_tags.should have(2).items
330
+ TaggableModel.tagged_with('rails').scoped(:joins => [:untaggable_models]).all_tags.should have(2).items
331
+ end
332
+
291
333
  it "should be able to set a custom tag context list" do
292
334
  bob = TaggableModel.create(:name => "Bob")
293
335
  bob.set_tag_list_on(:rotors, "spinning, jumping")
@@ -456,6 +498,17 @@ describe "Taggable" do
456
498
  TaggableModel.tag_counts_on(:tags, :order => 'tags.id').map(&:name).should == %w(bob kelso fork spoon)
457
499
  end
458
500
 
501
+ it "should have different tags_on for inherited models" do
502
+ @inherited_same.tag_list = "bob, kelso"
503
+ @inherited_same.save!
504
+ @inherited_different.tag_list = "fork, spoon"
505
+ @inherited_different.save!
506
+
507
+ InheritingTaggableModel.tags_on(:tags, :order => 'tags.id').map(&:name).should == %w(bob kelso)
508
+ AlteredInheritingTaggableModel.tags_on(:tags, :order => 'tags.id').map(&:name).should == %w(fork spoon)
509
+ TaggableModel.tags_on(:tags, :order => 'tags.id').map(&:name).should == %w(bob kelso fork spoon)
510
+ end
511
+
459
512
  it 'should store same tag without validation conflict' do
460
513
  @taggable.tag_list = 'one'
461
514
  @taggable.save!
@@ -492,6 +545,16 @@ describe "Taggable" do
492
545
  @taggable.tag_counts_on(:tags).length.should == 2
493
546
  end
494
547
 
548
+ it "should have tags_on" do
549
+ NonStandardIdTaggableModel.tags_on(:tags).all.should be_empty
550
+
551
+ @taggable.tag_list = ["awesome", "epic"]
552
+ @taggable.save
553
+
554
+ NonStandardIdTaggableModel.tags_on(:tags).length.should == 2
555
+ @taggable.tags_on(:tags).length.should == 2
556
+ end
557
+
495
558
  it "should be able to create tags" do
496
559
  @taggable.skill_list = "ruby, rails, css"
497
560
  @taggable.instance_variable_get("@skill_list").instance_of?(ActsAsTaggableOn::TagList).should be_true
@@ -538,6 +601,12 @@ describe "Taggable" do
538
601
  @taggable.changes.should == {}
539
602
  end
540
603
  end
604
+
605
+ describe "Autogenerated methods" do
606
+ it "should be overridable" do
607
+ TaggableModel.create(:tag_list=>'woo').tag_list_submethod_called.should be_true
608
+ end
609
+ end
541
610
  end
542
611
 
543
612