acts_as_taggable_on 3.0.0.rc1

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.
Files changed (51) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +11 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +9 -0
  5. data/Appraisals +7 -0
  6. data/Gemfile +5 -0
  7. data/Guardfile +5 -0
  8. data/LICENSE.md +20 -0
  9. data/README.md +309 -0
  10. data/Rakefile +13 -0
  11. data/UPGRADING +7 -0
  12. data/acts_as_taggable_on.gemspec +35 -0
  13. data/db/migrate/1_acts_as_taggable_on_migration.rb +30 -0
  14. data/db/migrate/2_add_missing_unique_indices.rb +21 -0
  15. data/gemfiles/rails_3.gemfile +8 -0
  16. data/gemfiles/rails_4.gemfile +8 -0
  17. data/lib/acts_as_taggable_on.rb +61 -0
  18. data/lib/acts_as_taggable_on/acts_as_taggable_on/cache.rb +82 -0
  19. data/lib/acts_as_taggable_on/acts_as_taggable_on/collection.rb +187 -0
  20. data/lib/acts_as_taggable_on/acts_as_taggable_on/compatibility.rb +34 -0
  21. data/lib/acts_as_taggable_on/acts_as_taggable_on/core.rb +394 -0
  22. data/lib/acts_as_taggable_on/acts_as_taggable_on/dirty.rb +37 -0
  23. data/lib/acts_as_taggable_on/acts_as_taggable_on/ownership.rb +135 -0
  24. data/lib/acts_as_taggable_on/acts_as_taggable_on/related.rb +84 -0
  25. data/lib/acts_as_taggable_on/engine.rb +6 -0
  26. data/lib/acts_as_taggable_on/tag.rb +119 -0
  27. data/lib/acts_as_taggable_on/tag_list.rb +101 -0
  28. data/lib/acts_as_taggable_on/taggable.rb +105 -0
  29. data/lib/acts_as_taggable_on/tagger.rb +76 -0
  30. data/lib/acts_as_taggable_on/tagging.rb +34 -0
  31. data/lib/acts_as_taggable_on/tags_helper.rb +15 -0
  32. data/lib/acts_as_taggable_on/utils.rb +34 -0
  33. data/lib/acts_as_taggable_on/version.rb +4 -0
  34. data/spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb +265 -0
  35. data/spec/acts_as_taggable_on/acts_as_tagger_spec.rb +114 -0
  36. data/spec/acts_as_taggable_on/caching_spec.rb +77 -0
  37. data/spec/acts_as_taggable_on/related_spec.rb +143 -0
  38. data/spec/acts_as_taggable_on/single_table_inheritance_spec.rb +187 -0
  39. data/spec/acts_as_taggable_on/tag_list_spec.rb +126 -0
  40. data/spec/acts_as_taggable_on/tag_spec.rb +211 -0
  41. data/spec/acts_as_taggable_on/taggable_spec.rb +623 -0
  42. data/spec/acts_as_taggable_on/tagger_spec.rb +137 -0
  43. data/spec/acts_as_taggable_on/tagging_spec.rb +28 -0
  44. data/spec/acts_as_taggable_on/tags_helper_spec.rb +44 -0
  45. data/spec/acts_as_taggable_on/utils_spec.rb +21 -0
  46. data/spec/bm.rb +52 -0
  47. data/spec/database.yml.sample +19 -0
  48. data/spec/models.rb +58 -0
  49. data/spec/schema.rb +65 -0
  50. data/spec/spec_helper.rb +87 -0
  51. metadata +248 -0
@@ -0,0 +1,126 @@
1
+ # encoding: utf-8
2
+ require 'spec_helper'
3
+
4
+ describe ActsAsTaggableOn::TagList do
5
+ let(:tag_list) { ActsAsTaggableOn::TagList.new("awesome","radical") }
6
+
7
+ it { should be_kind_of Array }
8
+
9
+ it "#from should return empty array if empty array is passed" do
10
+ ActsAsTaggableOn::TagList.from([]).should be_empty
11
+ end
12
+
13
+ describe "#add" do
14
+ it "should be able to be add a new tag word" do
15
+ tag_list.add("cool")
16
+ tag_list.include?("cool").should be_true
17
+ end
18
+
19
+ it "should be able to add delimited lists of words" do
20
+ tag_list.add("cool, wicked", :parse => true)
21
+ tag_list.should include("cool", "wicked")
22
+ end
23
+
24
+ it "should be able to add delimited list of words with quoted delimiters" do
25
+ tag_list.add("'cool, wicked', \"really cool, really wicked\"", :parse => true)
26
+ tag_list.should include("cool, wicked", "really cool, really wicked")
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.should include("john's cool car", "mary's wicked toy")
32
+ end
33
+
34
+ it "should be able to add an array of words" do
35
+ tag_list.add(["cool", "wicked"], :parse => true)
36
+ tag_list.should include("cool", "wicked")
37
+ end
38
+
39
+ it "should quote escape tags with commas in them" do
40
+ tag_list.add("cool","rad,bodacious")
41
+ tag_list.to_s.should == "awesome, radical, cool, \"rad,bodacious\""
42
+ end
43
+
44
+ end
45
+
46
+ describe "#remove" do
47
+ it "should be able to remove words" do
48
+ tag_list.remove("awesome")
49
+ tag_list.should_not include("awesome")
50
+ end
51
+
52
+ it "should be able to remove delimited lists of words" do
53
+ tag_list.remove("awesome, radical", :parse => true)
54
+ tag_list.should be_empty
55
+ end
56
+
57
+ it "should be able to remove an array of words" do
58
+ tag_list.remove(["awesome", "radical"], :parse => true)
59
+ tag_list.should be_empty
60
+ end
61
+ end
62
+
63
+ describe "#to_s" do
64
+ it "should give a delimited list of words when converted to string" do
65
+ tag_list.to_s.should == "awesome, radical"
66
+ end
67
+
68
+ it "should be able to call to_s on a frozen tag list" do
69
+ tag_list.freeze
70
+ lambda { tag_list.add("cool","rad,bodacious") }.should raise_error
71
+ lambda { tag_list.to_s }.should_not raise_error
72
+ end
73
+ end
74
+
75
+ describe "cleaning" do
76
+ it "should parameterize if force_parameterize is set to true" do
77
+ ActsAsTaggableOn.force_parameterize = true
78
+ tag_list = ActsAsTaggableOn::TagList.new("awesome()","radical)(cc")
79
+
80
+ tag_list.to_s.should == "awesome, radical-cc"
81
+ ActsAsTaggableOn.force_parameterize = false
82
+ end
83
+
84
+ it "should lowercase if force_lowercase is set to true" do
85
+ ActsAsTaggableOn.force_lowercase = true
86
+
87
+ tag_list = ActsAsTaggableOn::TagList.new("aweSomE","RaDicaL","Entrée")
88
+ tag_list.to_s.should == "awesome, radical, entrée"
89
+
90
+ ActsAsTaggableOn.force_lowercase = false
91
+ end
92
+
93
+ end
94
+
95
+ describe "Multiple Delimiter" do
96
+ before do
97
+ @old_delimiter = ActsAsTaggableOn.delimiter
98
+ end
99
+
100
+ after do
101
+ ActsAsTaggableOn.delimiter = @old_delimiter
102
+ end
103
+
104
+ it "should separate tags by delimiters" do
105
+ ActsAsTaggableOn.delimiter = [',', ' ', '\|']
106
+ tag_list = ActsAsTaggableOn::TagList.from "cool, data|I have"
107
+ tag_list.to_s.should == 'cool, data, I, have'
108
+ end
109
+
110
+ it "should escape quote" do
111
+ ActsAsTaggableOn.delimiter = [',', ' ', '\|']
112
+ tag_list = ActsAsTaggableOn::TagList.from "'I have'|cool, data"
113
+ tag_list.to_s.should == '"I have", cool, data'
114
+
115
+ tag_list = ActsAsTaggableOn::TagList.from '"I, have"|cool, data'
116
+ tag_list.to_s.should == '"I, have", cool, data'
117
+ end
118
+
119
+ it "should work for utf8 delimiter and long delimiter" do
120
+ ActsAsTaggableOn.delimiter = [',', '的', '可能是']
121
+ tag_list = ActsAsTaggableOn::TagList.from "我的东西可能是不见了,还好有备份"
122
+ tag_list.to_s.should == "我, 东西, 不见了, 还好有备份"
123
+ end
124
+ end
125
+
126
+ end
@@ -0,0 +1,211 @@
1
+ # encoding: utf-8
2
+ require 'spec_helper'
3
+
4
+ describe ActsAsTaggableOn::Tag do
5
+ before(:each) do
6
+ clean_database!
7
+ @tag = ActsAsTaggableOn::Tag.new
8
+ @user = TaggableModel.create(:name => "Pablo")
9
+ end
10
+
11
+ describe "named like any" do
12
+ before(:each) do
13
+ ActsAsTaggableOn::Tag.create(:name => "Awesome")
14
+ ActsAsTaggableOn::Tag.create(:name => "awesome")
15
+ ActsAsTaggableOn::Tag.create(:name => "epic")
16
+ end
17
+
18
+ it "should find both tags" do
19
+ ActsAsTaggableOn::Tag.named_like_any(["awesome", "epic"]).should have(3).items
20
+ end
21
+ end
22
+
23
+ describe "find or create by name" do
24
+ before(:each) do
25
+ @tag.name = "awesome"
26
+ @tag.save
27
+ end
28
+
29
+ it "should find by name" do
30
+ ActsAsTaggableOn::Tag.find_or_create_with_like_by_name("awesome").should == @tag
31
+ end
32
+
33
+ it "should find by name case insensitive" do
34
+ ActsAsTaggableOn::Tag.find_or_create_with_like_by_name("AWESOME").should == @tag
35
+ end
36
+
37
+ it "should create by name" do
38
+ lambda {
39
+ ActsAsTaggableOn::Tag.find_or_create_with_like_by_name("epic")
40
+ }.should change(ActsAsTaggableOn::Tag, :count).by(1)
41
+ end
42
+ end
43
+
44
+ unless ActsAsTaggableOn::Tag.using_sqlite?
45
+ describe "find or create by unicode name" do
46
+ before(:each) do
47
+ @tag.name = "привет"
48
+ @tag.save
49
+ end
50
+
51
+ it "should find by name" do
52
+ ActsAsTaggableOn::Tag.find_or_create_with_like_by_name("привет").should == @tag
53
+ end
54
+
55
+ it "should find by name case insensitive" do
56
+ ActsAsTaggableOn::Tag.find_or_create_with_like_by_name("ПРИВЕТ").should == @tag
57
+ end
58
+ end
59
+ end
60
+
61
+ describe "find or create all by any name" do
62
+ before(:each) do
63
+ @tag.name = "awesome"
64
+ @tag.save
65
+ end
66
+
67
+ it "should find by name" do
68
+ ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name("awesome").should == [@tag]
69
+ end
70
+
71
+ it "should find by name case insensitive" do
72
+ ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name("AWESOME").should == [@tag]
73
+ end
74
+
75
+ it "should create by name" do
76
+ lambda {
77
+ ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name("epic")
78
+ }.should change(ActsAsTaggableOn::Tag, :count).by(1)
79
+ end
80
+
81
+ it "should find or create by name" do
82
+ lambda {
83
+ ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name("awesome", "epic").map(&:name).should == ["awesome", "epic"]
84
+ }.should change(ActsAsTaggableOn::Tag, :count).by(1)
85
+ end
86
+
87
+ it "should return an empty array if no tags are specified" do
88
+ ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name([]).should == []
89
+ end
90
+ end
91
+
92
+ it "should require a name" do
93
+ @tag.valid?
94
+
95
+ @tag.errors[:name].should == ["can't be blank"]
96
+
97
+ @tag.name = "something"
98
+ @tag.valid?
99
+
100
+ @tag.errors[:name].should == []
101
+ end
102
+
103
+ it "should limit the name length to 255 or less characters" do
104
+ @tag.name = "fgkgnkkgjymkypbuozmwwghblmzpqfsgjasflblywhgkwndnkzeifalfcpeaeqychjuuowlacmuidnnrkprgpcpybarbkrmziqihcrxirlokhnzfvmtzixgvhlxzncyywficpraxfnjptxxhkqmvicbcdcynkjvziefqzyndxkjmsjlvyvbwraklbalykyxoliqdlreeykuphdtmzfdwpphmrqvwvqffojkqhlzvinqajsxbszyvrqqyzusxranr"
105
+ @tag.valid?
106
+ @tag.errors[:name].should == ["is too long (maximum is 255 characters)"]
107
+
108
+ @tag.name = "fgkgnkkgjymkypbuozmwwghblmzpqfsgjasflblywhgkwndnkzeifalfcpeaeqychjuuowlacmuidnnrkprgpcpybarbkrmziqihcrxirlokhnzfvmtzixgvhlxzncyywficpraxfnjptxxhkqmvicbcdcynkjvziefqzyndxkjmsjlvyvbwraklbalykyxoliqdlreeykuphdtmzfdwpphmrqvwvqffojkqhlzvinqajsxbszyvrqqyzusxran"
109
+ @tag.valid?
110
+ @tag.errors[:name].should == []
111
+ end
112
+
113
+ it "should equal a tag with the same name" do
114
+ @tag.name = "awesome"
115
+ new_tag = ActsAsTaggableOn::Tag.new(:name => "awesome")
116
+ new_tag.should == @tag
117
+ end
118
+
119
+ it "should return its name when to_s is called" do
120
+ @tag.name = "cool"
121
+ @tag.to_s.should == "cool"
122
+ end
123
+
124
+ it "have named_scope named(something)" do
125
+ @tag.name = "cool"
126
+ @tag.save!
127
+ ActsAsTaggableOn::Tag.named('cool').should include(@tag)
128
+ end
129
+
130
+ it "have named_scope named_like(something)" do
131
+ @tag.name = "cool"
132
+ @tag.save!
133
+ @another_tag = ActsAsTaggableOn::Tag.create!(:name => "coolip")
134
+ ActsAsTaggableOn::Tag.named_like('cool').should include(@tag, @another_tag)
135
+ end
136
+
137
+ describe "escape wildcard symbols in like requests" do
138
+ before(:each) do
139
+ @tag.name = "cool"
140
+ @tag.save
141
+ @another_tag = ActsAsTaggableOn::Tag.create!(:name => "coo%")
142
+ @another_tag2 = ActsAsTaggableOn::Tag.create!(:name => "coolish")
143
+ end
144
+
145
+ it "return escaped result when '%' char present in tag" do
146
+ ActsAsTaggableOn::Tag.named_like('coo%').should_not include(@tag)
147
+ ActsAsTaggableOn::Tag.named_like('coo%').should include(@another_tag)
148
+ end
149
+
150
+ end
151
+
152
+ describe "when using strict_case_match" do
153
+ before do
154
+ ActsAsTaggableOn.strict_case_match = true
155
+ @tag.name = "awesome"
156
+ @tag.save!
157
+ end
158
+
159
+ after do
160
+ ActsAsTaggableOn.strict_case_match = false
161
+ end
162
+
163
+ it "should find by name" do
164
+ ActsAsTaggableOn::Tag.find_or_create_with_like_by_name("awesome").should == @tag
165
+ end
166
+
167
+ it "should find by name case sensitively" do
168
+ expect {
169
+ ActsAsTaggableOn::Tag.find_or_create_with_like_by_name("AWESOME")
170
+ }.to change(ActsAsTaggableOn::Tag, :count)
171
+
172
+ ActsAsTaggableOn::Tag.last.name.should == "AWESOME"
173
+ end
174
+
175
+ it "should have a named_scope named(something) that matches exactly" do
176
+ uppercase_tag = ActsAsTaggableOn::Tag.create(:name => "Cool")
177
+ @tag.name = "cool"
178
+ @tag.save!
179
+
180
+ ActsAsTaggableOn::Tag.named('cool').should include(@tag)
181
+ ActsAsTaggableOn::Tag.named('cool').should_not include(uppercase_tag)
182
+ end
183
+ end
184
+
185
+ describe "name uniqeness validation" do
186
+ let(:duplicate_tag) { ActsAsTaggableOn::Tag.new(:name => 'ror') }
187
+
188
+ before { ActsAsTaggableOn::Tag.create(:name => 'ror') }
189
+
190
+ context "when don't need unique names" do
191
+ it "should not run uniqueness validation" do
192
+ duplicate_tag.stub(:validates_name_uniqueness?).and_return(false)
193
+ duplicate_tag.save
194
+ duplicate_tag.should be_persisted
195
+ end
196
+ end
197
+
198
+ context "when do need unique names" do
199
+ it "should run uniqueness validation" do
200
+ duplicate_tag.should_not be_valid
201
+ end
202
+
203
+ it "add error to name" do
204
+ duplicate_tag.save
205
+
206
+ duplicate_tag.should have(1).errors
207
+ duplicate_tag.errors.messages[:name].should include('has already been taken')
208
+ end
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,623 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Taggable To Preserve Order" do
4
+ before(:each) do
5
+ clean_database!
6
+ @taggable = OrderedTaggableModel.new(:name => "Bob Jones")
7
+ end
8
+
9
+ it "should have tag types" do
10
+ [:tags, :colours].each do |type|
11
+ OrderedTaggableModel.tag_types.should include type
12
+ end
13
+
14
+ @taggable.tag_types.should == OrderedTaggableModel.tag_types
15
+ end
16
+
17
+ it "should have tag associations" do
18
+ [:tags, :colours].each do |type|
19
+ @taggable.respond_to?(type).should be_true
20
+ @taggable.respond_to?("#{type.to_s.singularize}_taggings").should be_true
21
+ end
22
+ end
23
+
24
+ # it "should have tag associations ordered by id" do
25
+ # [:tags, :colours].each do |type|
26
+ # OrderedTaggableModel.reflect_on_association(type).options[:order].should include('id')
27
+ # OrderedTaggableModel.reflect_on_association("#{type.to_s.singularize}_taggings".to_sym).options[:order].should include('id')
28
+ # end
29
+ # end
30
+
31
+ it "should have tag methods" do
32
+ [:tags, :colours].each do |type|
33
+ @taggable.respond_to?("#{type.to_s.singularize}_list").should be_true
34
+ @taggable.respond_to?("#{type.to_s.singularize}_list=").should be_true
35
+ @taggable.respond_to?("all_#{type.to_s}_list").should be_true
36
+ end
37
+ end
38
+
39
+ it "should return tag list in the order the tags were created" do
40
+ # create
41
+ @taggable.tag_list = "rails, ruby, css"
42
+ @taggable.instance_variable_get("@tag_list").instance_of?(ActsAsTaggableOn::TagList).should be_true
43
+
44
+ lambda {
45
+ @taggable.save
46
+ }.should change(ActsAsTaggableOn::Tag, :count).by(3)
47
+
48
+ @taggable.reload
49
+ @taggable.tag_list.should == %w(rails ruby css)
50
+
51
+ # update
52
+ @taggable.tag_list = "pow, ruby, rails"
53
+ @taggable.save
54
+
55
+ @taggable.reload
56
+ @taggable.tag_list.should == %w(pow ruby rails)
57
+
58
+ # update with no change
59
+ @taggable.tag_list = "pow, ruby, rails"
60
+ @taggable.save
61
+
62
+ @taggable.reload
63
+ @taggable.tag_list.should == %w(pow ruby rails)
64
+
65
+ # update to clear tags
66
+ @taggable.tag_list = ""
67
+ @taggable.save
68
+
69
+ @taggable.reload
70
+ @taggable.tag_list.should == []
71
+ end
72
+
73
+ it "should return tag objects in the order the tags were created" do
74
+ # create
75
+ @taggable.tag_list = "pow, ruby, rails"
76
+ @taggable.instance_variable_get("@tag_list").instance_of?(ActsAsTaggableOn::TagList).should be_true
77
+
78
+ lambda {
79
+ @taggable.save
80
+ }.should change(ActsAsTaggableOn::Tag, :count).by(3)
81
+
82
+ @taggable.reload
83
+ @taggable.tags.map{|t| t.name}.should == %w(pow ruby rails)
84
+
85
+ # update
86
+ @taggable.tag_list = "rails, ruby, css, pow"
87
+ @taggable.save
88
+
89
+ @taggable.reload
90
+ @taggable.tags.map{|t| t.name}.should == %w(rails ruby css pow)
91
+ end
92
+
93
+ it "should return tag objects in tagging id order" do
94
+ # create
95
+ @taggable.tag_list = "pow, ruby, rails"
96
+ @taggable.save
97
+
98
+ @taggable.reload
99
+ ids = @taggable.tags.map{|t| t.taggings.first.id}
100
+ ids.should == ids.sort
101
+
102
+ # update
103
+ @taggable.tag_list = "rails, ruby, css, pow"
104
+ @taggable.save
105
+
106
+ @taggable.reload
107
+ ids = @taggable.tags.map{|t| t.taggings.first.id}
108
+ ids.should == ids.sort
109
+ end
110
+ end
111
+
112
+ describe "Taggable" do
113
+ before(:each) do
114
+ clean_database!
115
+ @taggable = TaggableModel.new(:name => "Bob Jones")
116
+ @taggables = [@taggable, TaggableModel.new(:name => "John Doe")]
117
+ end
118
+
119
+ it "should have tag types" do
120
+ [:tags, :languages, :skills, :needs, :offerings].each do |type|
121
+ TaggableModel.tag_types.should include type
122
+ end
123
+
124
+ @taggable.tag_types.should == TaggableModel.tag_types
125
+ end
126
+
127
+ it "should have tag_counts_on" do
128
+ TaggableModel.tag_counts_on(:tags).should be_empty
129
+
130
+ @taggable.tag_list = ["awesome", "epic"]
131
+ @taggable.save
132
+
133
+ TaggableModel.tag_counts_on(:tags).length.should == 2
134
+ @taggable.tag_counts_on(:tags).length.should == 2
135
+ end
136
+
137
+ it "should have tags_on" do
138
+ TaggableModel.tags_on(:tags).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
+
147
+ it "should return [] right after create" do
148
+ blank_taggable = TaggableModel.new(:name => "Bob Jones")
149
+ blank_taggable.tag_list.should == []
150
+ end
151
+
152
+ it "should be able to create tags" do
153
+ @taggable.skill_list = "ruby, rails, css"
154
+ @taggable.instance_variable_get("@skill_list").instance_of?(ActsAsTaggableOn::TagList).should be_true
155
+
156
+ lambda {
157
+ @taggable.save
158
+ }.should change(ActsAsTaggableOn::Tag, :count).by(3)
159
+
160
+ @taggable.reload
161
+ @taggable.skill_list.sort.should == %w(ruby rails css).sort
162
+ end
163
+
164
+ it "should be able to create tags through the tag list directly" do
165
+ @taggable.tag_list_on(:test).add("hello")
166
+ @taggable.tag_list_cache_on(:test).should_not be_empty
167
+ @taggable.tag_list_on(:test).should == ["hello"]
168
+
169
+ @taggable.save
170
+ @taggable.save_tags
171
+
172
+ @taggable.reload
173
+ @taggable.tag_list_on(:test).should == ["hello"]
174
+ end
175
+
176
+ it "should differentiate between contexts" do
177
+ @taggable.skill_list = "ruby, rails, css"
178
+ @taggable.tag_list = "ruby, bob, charlie"
179
+ @taggable.save
180
+ @taggable.reload
181
+ @taggable.skill_list.should include("ruby")
182
+ @taggable.skill_list.should_not include("bob")
183
+ end
184
+
185
+ it "should be able to remove tags through list alone" do
186
+ @taggable.skill_list = "ruby, rails, css"
187
+ @taggable.save
188
+ @taggable.reload
189
+ @taggable.should have(3).skills
190
+ @taggable.skill_list = "ruby, rails"
191
+ @taggable.save
192
+ @taggable.reload
193
+ @taggable.should have(2).skills
194
+ end
195
+
196
+ it "should be able to select taggables by subset of tags using ActiveRelation methods" do
197
+ @taggables[0].tag_list = "bob"
198
+ @taggables[1].tag_list = "charlie"
199
+ @taggables[0].skill_list = "ruby"
200
+ @taggables[1].skill_list = "css"
201
+ @taggables.each{|taggable| taggable.save}
202
+
203
+ @found_taggables_by_tag = TaggableModel.joins(:tags).where(:tags => {:name => ["bob"]})
204
+ @found_taggables_by_skill = TaggableModel.joins(:skills).where(:tags => {:name => ["ruby"]})
205
+
206
+ @found_taggables_by_tag.should include @taggables[0]
207
+ @found_taggables_by_tag.should_not include @taggables[1]
208
+ @found_taggables_by_skill.should include @taggables[0]
209
+ @found_taggables_by_skill.should_not include @taggables[1]
210
+ end
211
+
212
+ it "should be able to find by tag" do
213
+ @taggable.skill_list = "ruby, rails, css"
214
+ @taggable.save
215
+
216
+ TaggableModel.tagged_with("ruby").first.should == @taggable
217
+ end
218
+
219
+ it "should be able to find by tag with context" do
220
+ @taggable.skill_list = "ruby, rails, css"
221
+ @taggable.tag_list = "bob, charlie"
222
+ @taggable.save
223
+
224
+ TaggableModel.tagged_with("ruby").first.should == @taggable
225
+ TaggableModel.tagged_with("ruby, css").first.should == @taggable
226
+ TaggableModel.tagged_with("bob", :on => :skills).first.should_not == @taggable
227
+ TaggableModel.tagged_with("bob", :on => :tags).first.should == @taggable
228
+ end
229
+
230
+ it "should not care about case" do
231
+ bob = TaggableModel.create(:name => "Bob", :tag_list => "ruby")
232
+ frank = TaggableModel.create(:name => "Frank", :tag_list => "Ruby")
233
+
234
+ ActsAsTaggableOn::Tag.all.size.should == 1
235
+ TaggableModel.tagged_with("ruby").to_a.should == TaggableModel.tagged_with("Ruby").to_a
236
+ end
237
+
238
+ it "should be able to get tag counts on model as a whole" do
239
+ bob = TaggableModel.create(:name => "Bob", :tag_list => "ruby, rails, css")
240
+ frank = TaggableModel.create(:name => "Frank", :tag_list => "ruby, rails")
241
+ charlie = TaggableModel.create(:name => "Charlie", :skill_list => "ruby")
242
+ TaggableModel.tag_counts.should_not be_empty
243
+ TaggableModel.skill_counts.should_not be_empty
244
+ end
245
+
246
+ it "should be able to get all tag counts on model as whole" do
247
+ bob = TaggableModel.create(:name => "Bob", :tag_list => "ruby, rails, css")
248
+ frank = TaggableModel.create(:name => "Frank", :tag_list => "ruby, rails")
249
+ charlie = TaggableModel.create(:name => "Charlie", :skill_list => "ruby")
250
+
251
+ TaggableModel.all_tag_counts.should_not be_empty
252
+ TaggableModel.all_tag_counts(:order => 'tags.id').first.count.should == 3 # ruby
253
+ end
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.should_not be_empty
261
+ TaggableModel.all_tags(:order => 'tags.id').first.name.should == "ruby"
262
+ end
263
+
264
+ it "should be able to use named scopes to chain tag finds by any tags by context" do
265
+ bob = TaggableModel.create(:name => "Bob", :need_list => "rails", :offering_list => "c++")
266
+ frank = TaggableModel.create(:name => "Frank", :need_list => "css", :offering_list => "css")
267
+ steve = TaggableModel.create(:name => 'Steve', :need_list => "c++", :offering_list => "java")
268
+
269
+ # Let's only find those who need rails or css and are offering c++ or java
270
+ TaggableModel.tagged_with(['rails, css'], :on => :needs, :any => true).tagged_with(['c++', 'java'], :on => :offerings, :any => true).to_a.should == [bob]
271
+ end
272
+
273
+ it "should not return read-only records" do
274
+ TaggableModel.create(:name => "Bob", :tag_list => "ruby, rails, css")
275
+ TaggableModel.tagged_with("ruby").first.should_not be_readonly
276
+ end
277
+
278
+ it "should be able to get scoped tag counts" do
279
+ bob = TaggableModel.create(:name => "Bob", :tag_list => "ruby, rails, css")
280
+ frank = TaggableModel.create(:name => "Frank", :tag_list => "ruby, rails")
281
+ charlie = TaggableModel.create(:name => "Charlie", :skill_list => "ruby")
282
+
283
+ TaggableModel.tagged_with("ruby").tag_counts(:order => 'tags.id').first.count.should == 2 # ruby
284
+ TaggableModel.tagged_with("ruby").skill_counts.first.count.should == 1 # ruby
285
+ end
286
+
287
+ it "should be able to get all scoped tag counts" do
288
+ bob = TaggableModel.create(:name => "Bob", :tag_list => "ruby, rails, css")
289
+ frank = TaggableModel.create(:name => "Frank", :tag_list => "ruby, rails")
290
+ charlie = TaggableModel.create(:name => "Charlie", :skill_list => "ruby")
291
+
292
+ TaggableModel.tagged_with("ruby").all_tag_counts(:order => 'tags.id').first.count.should == 3 # ruby
293
+ end
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
+
303
+ it 'should only return tag counts for the available scope' do
304
+ bob = TaggableModel.create(:name => "Bob", :tag_list => "ruby, rails, css")
305
+ frank = TaggableModel.create(:name => "Frank", :tag_list => "ruby, rails")
306
+ charlie = TaggableModel.create(:name => "Charlie", :skill_list => "ruby, java")
307
+
308
+ TaggableModel.tagged_with('rails').all_tag_counts.should have(3).items
309
+ TaggableModel.tagged_with('rails').all_tag_counts.any? { |tag| tag.name == 'java' }.should be_false
310
+
311
+ # Test specific join syntaxes:
312
+ frank.untaggable_models.create!
313
+ TaggableModel.tagged_with('rails').joins(:untaggable_models).all_tag_counts.should have(2).items
314
+ TaggableModel.tagged_with('rails').joins(:untaggable_models => :taggable_model).all_tag_counts.should have(2).items
315
+ TaggableModel.tagged_with('rails').joins([:untaggable_models]).all_tag_counts.should have(2).items
316
+ end
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').joins(:untaggable_models).all_tags.should have(2).items
329
+ TaggableModel.tagged_with('rails').joins({ :untaggable_models => :taggable_model }).all_tags.should have(2).items
330
+ TaggableModel.tagged_with('rails').joins([:untaggable_models]).all_tags.should have(2).items
331
+ end
332
+
333
+ it "should be able to set a custom tag context list" do
334
+ bob = TaggableModel.create(:name => "Bob")
335
+ bob.set_tag_list_on(:rotors, "spinning, jumping")
336
+ bob.tag_list_on(:rotors).should == ["spinning","jumping"]
337
+ bob.save
338
+ bob.reload
339
+ bob.tags_on(:rotors).should_not be_empty
340
+ end
341
+
342
+ it "should be able to find tagged" do
343
+ bob = TaggableModel.create(:name => "Bob", :tag_list => "fitter, happier, more productive", :skill_list => "ruby, rails, css")
344
+ frank = TaggableModel.create(:name => "Frank", :tag_list => "weaker, depressed, inefficient", :skill_list => "ruby, rails, css")
345
+ steve = TaggableModel.create(:name => 'Steve', :tag_list => 'fitter, happier, more productive', :skill_list => 'c++, java, ruby')
346
+
347
+ TaggableModel.tagged_with("ruby", :order => 'taggable_models.name').to_a.should == [bob, frank, steve]
348
+ TaggableModel.tagged_with("ruby, rails", :order => 'taggable_models.name').to_a.should == [bob, frank]
349
+ TaggableModel.tagged_with(["ruby", "rails"], :order => 'taggable_models.name').to_a.should == [bob, frank]
350
+ end
351
+
352
+ it "should be able to find tagged with quotation marks" do
353
+ bob = TaggableModel.create(:name => "Bob", :tag_list => "fitter, happier, more productive, 'I love the ,comma,'")
354
+ TaggableModel.tagged_with("'I love the ,comma,'").should include(bob)
355
+ end
356
+
357
+ it "should be able to find tagged with invalid tags" do
358
+ bob = TaggableModel.create(:name => "Bob", :tag_list => "fitter, happier, more productive")
359
+ TaggableModel.tagged_with("sad, happier").should_not include(bob)
360
+ end
361
+
362
+ it "should be able to find tagged with any tag" do
363
+ bob = TaggableModel.create(:name => "Bob", :tag_list => "fitter, happier, more productive", :skill_list => "ruby, rails, css")
364
+ frank = TaggableModel.create(:name => "Frank", :tag_list => "weaker, depressed, inefficient", :skill_list => "ruby, rails, css")
365
+ steve = TaggableModel.create(:name => 'Steve', :tag_list => 'fitter, happier, more productive', :skill_list => 'c++, java, ruby')
366
+
367
+ TaggableModel.tagged_with(["ruby", "java"], :order => 'taggable_models.name', :any => true).to_a.should == [bob, frank, steve]
368
+ TaggableModel.tagged_with(["c++", "fitter"], :order => 'taggable_models.name', :any => true).to_a.should == [bob, steve]
369
+ TaggableModel.tagged_with(["depressed", "css"], :order => 'taggable_models.name', :any => true).to_a.should == [bob, frank]
370
+ end
371
+
372
+ context "wild: true" do
373
+ it "should use params as wildcards" do
374
+ bob = TaggableModel.create(:name => "Bob", :tag_list => "bob, tricia")
375
+ frank = TaggableModel.create(:name => "Frank", :tag_list => "bobby, jim")
376
+ steve = TaggableModel.create(:name => "Steve", :tag_list => "john, patricia")
377
+ jim = TaggableModel.create(:name => "Jim", :tag_list => "jim, steve")
378
+
379
+
380
+ TaggableModel.tagged_with(["bob", "tricia"], :wild => true, :any => true).to_a.sort_by{|o| o.id}.should == [bob, frank, steve]
381
+ TaggableModel.tagged_with(["bob", "tricia"], :wild => true, :exclude => true).to_a.should == [jim]
382
+ end
383
+ end
384
+
385
+ it "should be able to find tagged on a custom tag context" do
386
+ bob = TaggableModel.create(:name => "Bob")
387
+ bob.set_tag_list_on(:rotors, "spinning, jumping")
388
+ bob.tag_list_on(:rotors).should == ["spinning","jumping"]
389
+ bob.save
390
+
391
+ TaggableModel.tagged_with("spinning", :on => :rotors).to_a.should == [bob]
392
+ end
393
+
394
+ it "should be able to use named scopes to chain tag finds" do
395
+ bob = TaggableModel.create(:name => "Bob", :tag_list => "fitter, happier, more productive", :skill_list => "ruby, rails, css")
396
+ frank = TaggableModel.create(:name => "Frank", :tag_list => "weaker, depressed, inefficient", :skill_list => "ruby, rails, css")
397
+ steve = TaggableModel.create(:name => 'Steve', :tag_list => 'fitter, happier, more productive', :skill_list => 'c++, java, python')
398
+
399
+ # Let's only find those productive Rails developers
400
+ TaggableModel.tagged_with('rails', :on => :skills, :order => 'taggable_models.name').to_a.should == [bob, frank]
401
+ TaggableModel.tagged_with('happier', :on => :tags, :order => 'taggable_models.name').to_a.should == [bob, steve]
402
+ TaggableModel.tagged_with('rails', :on => :skills).tagged_with('happier', :on => :tags).to_a.should == [bob]
403
+ TaggableModel.tagged_with('rails').tagged_with('happier', :on => :tags).to_a.should == [bob]
404
+ end
405
+
406
+ it "should be able to find tagged with only the matching tags" do
407
+ bob = TaggableModel.create(:name => "Bob", :tag_list => "lazy, happier")
408
+ frank = TaggableModel.create(:name => "Frank", :tag_list => "fitter, happier, inefficient")
409
+ steve = TaggableModel.create(:name => 'Steve', :tag_list => "fitter, happier")
410
+
411
+ TaggableModel.tagged_with("fitter, happier", :match_all => true).to_a.should == [steve]
412
+ end
413
+
414
+ it "should be able to find tagged with some excluded tags" do
415
+ bob = TaggableModel.create(:name => "Bob", :tag_list => "happier, lazy")
416
+ frank = TaggableModel.create(:name => "Frank", :tag_list => "happier")
417
+ steve = TaggableModel.create(:name => 'Steve', :tag_list => "happier")
418
+
419
+ TaggableModel.tagged_with("lazy", :exclude => true).to_a.should == [frank, steve]
420
+ end
421
+
422
+ it "should return an empty scope for empty tags" do
423
+ TaggableModel.tagged_with('').should == []
424
+ TaggableModel.tagged_with(' ').should == []
425
+ TaggableModel.tagged_with(nil).should == []
426
+ TaggableModel.tagged_with([]).should == []
427
+ end
428
+
429
+ it "should not create duplicate taggings" do
430
+ bob = TaggableModel.create(:name => "Bob")
431
+ lambda {
432
+ bob.tag_list << "happier"
433
+ bob.tag_list << "happier"
434
+ bob.save
435
+ }.should change(ActsAsTaggableOn::Tagging, :count).by(1)
436
+ end
437
+
438
+ describe "Associations" do
439
+ before(:each) do
440
+ @taggable = TaggableModel.create(:tag_list => "awesome, epic")
441
+ end
442
+
443
+ it "should not remove tags when creating associated objects" do
444
+ @taggable.untaggable_models.create!
445
+ @taggable.reload
446
+ @taggable.tag_list.should have(2).items
447
+ end
448
+ end
449
+
450
+ describe "grouped_column_names_for method" do
451
+ it "should return all column names joined for Tag GROUP clause" do
452
+ @taggable.grouped_column_names_for(ActsAsTaggableOn::Tag).should == "tags.id, tags.name"
453
+ end
454
+
455
+ it "should return all column names joined for TaggableModel GROUP clause" do
456
+ @taggable.grouped_column_names_for(TaggableModel).should == "taggable_models.id, taggable_models.name, taggable_models.type"
457
+ end
458
+
459
+ it "should return all column names joined for NonStandardIdTaggableModel GROUP clause" do
460
+ @taggable.grouped_column_names_for(TaggableModel).should == "taggable_models.#{TaggableModel.primary_key}, taggable_models.name, taggable_models.type"
461
+ end
462
+ end
463
+
464
+ describe "NonStandardIdTaggable" do
465
+ before(:each) do
466
+ clean_database!
467
+ @taggable = NonStandardIdTaggableModel.new(:name => "Bob Jones")
468
+ @taggables = [@taggable, NonStandardIdTaggableModel.new(:name => "John Doe")]
469
+ end
470
+
471
+ it "should have tag types" do
472
+ [:tags, :languages, :skills, :needs, :offerings].each do |type|
473
+ NonStandardIdTaggableModel.tag_types.should include type
474
+ end
475
+
476
+ @taggable.tag_types.should == NonStandardIdTaggableModel.tag_types
477
+ end
478
+
479
+ it "should have tag_counts_on" do
480
+ NonStandardIdTaggableModel.tag_counts_on(:tags).should be_empty
481
+
482
+ @taggable.tag_list = ["awesome", "epic"]
483
+ @taggable.save
484
+
485
+ NonStandardIdTaggableModel.tag_counts_on(:tags).length.should == 2
486
+ @taggable.tag_counts_on(:tags).length.should == 2
487
+ end
488
+
489
+ it "should have tags_on" do
490
+ NonStandardIdTaggableModel.tags_on(:tags).should be_empty
491
+
492
+ @taggable.tag_list = ["awesome", "epic"]
493
+ @taggable.save
494
+
495
+ NonStandardIdTaggableModel.tags_on(:tags).length.should == 2
496
+ @taggable.tags_on(:tags).length.should == 2
497
+ end
498
+
499
+ it "should be able to create tags" do
500
+ @taggable.skill_list = "ruby, rails, css"
501
+ @taggable.instance_variable_get("@skill_list").instance_of?(ActsAsTaggableOn::TagList).should be_true
502
+
503
+ lambda {
504
+ @taggable.save
505
+ }.should change(ActsAsTaggableOn::Tag, :count).by(3)
506
+
507
+ @taggable.reload
508
+ @taggable.skill_list.sort.should == %w(ruby rails css).sort
509
+ end
510
+
511
+ it "should be able to create tags through the tag list directly" do
512
+ @taggable.tag_list_on(:test).add("hello")
513
+ @taggable.tag_list_cache_on(:test).should_not be_empty
514
+ @taggable.tag_list_on(:test).should == ["hello"]
515
+
516
+ @taggable.save
517
+ @taggable.save_tags
518
+
519
+ @taggable.reload
520
+ @taggable.tag_list_on(:test).should == ["hello"]
521
+ end
522
+ end
523
+
524
+ describe "Dirty Objects" do
525
+ context "with un-contexted tags" do
526
+ before(:each) do
527
+ @taggable = TaggableModel.create(:tag_list => "awesome, epic")
528
+ end
529
+
530
+ context "when tag_list changed" do
531
+ before(:each) do
532
+ @taggable.changes.should == {}
533
+ @taggable.tag_list = 'one'
534
+ end
535
+
536
+ it 'should show changes of dirty object' do
537
+ @taggable.changes.should == {"tag_list"=>["awesome, epic", ["one"]]}
538
+ end
539
+
540
+ it 'flags tag_list as changed' do
541
+ @taggable.tag_list_changed?.should be_true
542
+ end
543
+
544
+ it 'preserves original value' do
545
+ @taggable.tag_list_was.should == "awesome, epic"
546
+ end
547
+
548
+ it 'shows what the change was' do
549
+ @taggable.tag_list_change.should == ["awesome, epic", ["one"]]
550
+ end
551
+ end
552
+
553
+ context 'when tag_list is the same' do
554
+ before(:each) do
555
+ @taggable.tag_list = "awesome, epic"
556
+ end
557
+
558
+ it 'is not flagged as changed' do
559
+ @taggable.tag_list_changed?.should be_false
560
+ end
561
+
562
+ it 'does not show any changes to the taggable item' do
563
+ @taggable.changes.should == {}
564
+ end
565
+ end
566
+ end
567
+
568
+ context "with context tags" do
569
+ before(:each) do
570
+ @taggable = TaggableModel.create(:language_list => "awesome, epic")
571
+ end
572
+
573
+ context "when language_list changed" do
574
+ before(:each) do
575
+ @taggable.changes.should == {}
576
+ @taggable.language_list = 'one'
577
+ end
578
+
579
+ it 'should show changes of dirty object' do
580
+ @taggable.changes.should == {"language_list"=>["awesome, epic", ["one"]]}
581
+ end
582
+
583
+ it 'flags language_list as changed' do
584
+ @taggable.language_list_changed?.should be_true
585
+ end
586
+
587
+ it 'preserves original value' do
588
+ @taggable.language_list_was.should == "awesome, epic"
589
+ end
590
+
591
+ it 'shows what the change was' do
592
+ @taggable.language_list_change.should == ["awesome, epic", ["one"]]
593
+ end
594
+
595
+ it 'shows what the changes were' do
596
+ @taggable.language_list_changes.should == ["awesome, epic", ["one"]]
597
+ end
598
+ end
599
+
600
+ context 'when language_list is the same' do
601
+ before(:each) do
602
+ @taggable.language_list = "awesome, epic"
603
+ end
604
+
605
+ it 'is not flagged as changed' do
606
+ @taggable.language_list_changed?.should be_false
607
+ end
608
+
609
+ it 'does not show any changes to the taggable item' do
610
+ @taggable.changes.should == {}
611
+ end
612
+ end
613
+ end
614
+ end
615
+
616
+ describe "Autogenerated methods" do
617
+ it "should be overridable" do
618
+ TaggableModel.create(:tag_list=>'woo').tag_list_submethod_called.should be_true
619
+ end
620
+ end
621
+ end
622
+
623
+