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

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.
@@ -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