make_taggable 0.6.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (156) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +47 -0
  3. data/.gitignore +13 -0
  4. data/.rspec +3 -0
  5. data/.standard.yml +18 -0
  6. data/.standard_todo.yml +5 -0
  7. data/.travis.yml +36 -0
  8. data/Appraisals +11 -0
  9. data/CHANGELOG.md +0 -0
  10. data/CODE_OF_CONDUCT.md +74 -0
  11. data/CONTRIBUTING.md +57 -0
  12. data/Gemfile +16 -0
  13. data/LICENSE.md +20 -0
  14. data/LICENSE.txt +21 -0
  15. data/README.md +478 -0
  16. data/Rakefile +7 -0
  17. data/bin/console +14 -0
  18. data/bin/setup +8 -0
  19. data/db/migrate/1_create_make_taggable_tags.rb +10 -0
  20. data/db/migrate/2_create_make_taggable_taggings.rb +12 -0
  21. data/db/migrate/3_add_index_to_tags.rb +5 -0
  22. data/db/migrate/4_add_index_to_taggings.rb +12 -0
  23. data/gemfiles/rails_5.gemfile +9 -0
  24. data/gemfiles/rails_6.gemfile +9 -0
  25. data/gemfiles/rails_master.gemfile +9 -0
  26. data/lib/make_taggable.rb +134 -0
  27. data/lib/make_taggable/default_parser.rb +75 -0
  28. data/lib/make_taggable/engine.rb +4 -0
  29. data/lib/make_taggable/generic_parser.rb +19 -0
  30. data/lib/make_taggable/tag.rb +131 -0
  31. data/lib/make_taggable/tag_list.rb +102 -0
  32. data/lib/make_taggable/taggable.rb +100 -0
  33. data/lib/make_taggable/taggable/cache.rb +90 -0
  34. data/lib/make_taggable/taggable/collection.rb +183 -0
  35. data/lib/make_taggable/taggable/core.rb +323 -0
  36. data/lib/make_taggable/taggable/ownership.rb +137 -0
  37. data/lib/make_taggable/taggable/related.rb +71 -0
  38. data/lib/make_taggable/taggable/tag_list_type.rb +4 -0
  39. data/lib/make_taggable/taggable/tagged_with_query.rb +16 -0
  40. data/lib/make_taggable/taggable/tagged_with_query/all_tags_query.rb +111 -0
  41. data/lib/make_taggable/taggable/tagged_with_query/any_tags_query.rb +68 -0
  42. data/lib/make_taggable/taggable/tagged_with_query/exclude_tags_query.rb +81 -0
  43. data/lib/make_taggable/taggable/tagged_with_query/query_base.rb +61 -0
  44. data/lib/make_taggable/tagger.rb +89 -0
  45. data/lib/make_taggable/tagging.rb +32 -0
  46. data/lib/make_taggable/tags_helper.rb +15 -0
  47. data/lib/make_taggable/utils.rb +34 -0
  48. data/lib/make_taggable/version.rb +4 -0
  49. data/lib/tasks/tags_collate_utf8.rake +17 -0
  50. data/make_taggable.gemspec +26 -0
  51. data/spec/dummy/README.md +24 -0
  52. data/spec/dummy/Rakefile +6 -0
  53. data/spec/dummy/app/assets/config/manifest.js +2 -0
  54. data/spec/dummy/app/channels/application_cable/channel.rb +4 -0
  55. data/spec/dummy/app/channels/application_cable/connection.rb +4 -0
  56. data/spec/dummy/app/controllers/application_controller.rb +2 -0
  57. data/spec/dummy/app/controllers/concerns/.keep +0 -0
  58. data/spec/dummy/app/jobs/application_job.rb +7 -0
  59. data/spec/dummy/app/mailers/application_mailer.rb +4 -0
  60. data/spec/dummy/app/models/altered_inheriting_taggable_model.rb +5 -0
  61. data/spec/dummy/app/models/application_record.rb +3 -0
  62. data/spec/dummy/app/models/cached_model.rb +3 -0
  63. data/spec/dummy/app/models/cached_model_with_array.rb +11 -0
  64. data/spec/dummy/app/models/columns_override_model.rb +5 -0
  65. data/spec/dummy/app/models/company.rb +15 -0
  66. data/spec/dummy/app/models/concerns/.keep +0 -0
  67. data/spec/dummy/app/models/inheriting_taggable_model.rb +4 -0
  68. data/spec/dummy/app/models/market.rb +2 -0
  69. data/spec/dummy/app/models/non_standard_id_taggable_model.rb +8 -0
  70. data/spec/dummy/app/models/ordered_taggable_model.rb +4 -0
  71. data/spec/dummy/app/models/other_cached_model.rb +3 -0
  72. data/spec/dummy/app/models/other_taggable_model.rb +4 -0
  73. data/spec/dummy/app/models/student.rb +4 -0
  74. data/spec/dummy/app/models/taggable_model.rb +14 -0
  75. data/spec/dummy/app/models/untaggable_model.rb +3 -0
  76. data/spec/dummy/app/models/user.rb +3 -0
  77. data/spec/dummy/app/views/layouts/mailer.html.erb +13 -0
  78. data/spec/dummy/app/views/layouts/mailer.text.erb +1 -0
  79. data/spec/dummy/bin/rails +4 -0
  80. data/spec/dummy/bin/rake +4 -0
  81. data/spec/dummy/bin/setup +33 -0
  82. data/spec/dummy/config.ru +5 -0
  83. data/spec/dummy/config/application.rb +19 -0
  84. data/spec/dummy/config/boot.rb +5 -0
  85. data/spec/dummy/config/cable.yml +10 -0
  86. data/spec/dummy/config/credentials.yml.enc +1 -0
  87. data/spec/dummy/config/database.yml +25 -0
  88. data/spec/dummy/config/environment.rb +5 -0
  89. data/spec/dummy/config/environments/development.rb +52 -0
  90. data/spec/dummy/config/environments/production.rb +105 -0
  91. data/spec/dummy/config/environments/test.rb +49 -0
  92. data/spec/dummy/config/initializers/application_controller_renderer.rb +8 -0
  93. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  94. data/spec/dummy/config/initializers/cors.rb +16 -0
  95. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  96. data/spec/dummy/config/initializers/inflections.rb +16 -0
  97. data/spec/dummy/config/initializers/mime_types.rb +4 -0
  98. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  99. data/spec/dummy/config/locales/en.yml +33 -0
  100. data/spec/dummy/config/master.key +1 -0
  101. data/spec/dummy/config/puma.rb +38 -0
  102. data/spec/dummy/config/routes.rb +3 -0
  103. data/spec/dummy/config/spring.rb +6 -0
  104. data/spec/dummy/config/storage.yml +34 -0
  105. data/spec/dummy/db/migrate/20201119220853_create_taggable_models.rb +8 -0
  106. data/spec/dummy/db/migrate/20201119221037_create_columns_override_models.rb +9 -0
  107. data/spec/dummy/db/migrate/20201119221121_create_non_standard_id_taggable_models.rb +8 -0
  108. data/spec/dummy/db/migrate/20201119221228_create_untaggable_models.rb +8 -0
  109. data/spec/dummy/db/migrate/20201119221247_create_cached_models.rb +9 -0
  110. data/spec/dummy/db/migrate/20201119221314_create_other_cached_models.rb +11 -0
  111. data/spec/dummy/db/migrate/20201119221343_create_companies.rb +7 -0
  112. data/spec/dummy/db/migrate/20201119221416_create_users.rb +7 -0
  113. data/spec/dummy/db/migrate/20201119221434_create_other_taggable_models.rb +8 -0
  114. data/spec/dummy/db/migrate/20201119221507_create_ordered_taggable_models.rb +8 -0
  115. data/spec/dummy/db/migrate/20201119221530_create_cache_methods_injected_models.rb +7 -0
  116. data/spec/dummy/db/migrate/20201119221629_create_other_cached_with_array_models.rb +11 -0
  117. data/spec/dummy/db/migrate/20201119221746_create_taggable_model_with_jsons.rb +9 -0
  118. data/spec/dummy/db/migrate/20201119222429_create_make_taggable_tags.make_taggable_engine.rb +11 -0
  119. data/spec/dummy/db/migrate/20201119222430_create_make_taggable_taggings.make_taggable_engine.rb +13 -0
  120. data/spec/dummy/db/migrate/20201119222431_add_index_to_tags.make_taggable_engine.rb +6 -0
  121. data/spec/dummy/db/migrate/20201119222432_add_index_to_taggings.make_taggable_engine.rb +13 -0
  122. data/spec/dummy/db/schema.rb +117 -0
  123. data/spec/dummy/db/seeds.rb +7 -0
  124. data/spec/dummy/lib/tasks/.keep +0 -0
  125. data/spec/dummy/log/.keep +0 -0
  126. data/spec/dummy/public/robots.txt +1 -0
  127. data/spec/dummy/storage/.keep +0 -0
  128. data/spec/dummy/test/channels/application_cable/connection_test.rb +11 -0
  129. data/spec/dummy/test/controllers/.keep +0 -0
  130. data/spec/dummy/test/fixtures/.keep +0 -0
  131. data/spec/dummy/test/fixtures/files/.keep +0 -0
  132. data/spec/dummy/test/integration/.keep +0 -0
  133. data/spec/dummy/test/mailers/.keep +0 -0
  134. data/spec/dummy/test/models/.keep +0 -0
  135. data/spec/dummy/test/test_helper.rb +13 -0
  136. data/spec/dummy/vendor/.keep +0 -0
  137. data/spec/make_taggable/acts_as_tagger_spec.rb +112 -0
  138. data/spec/make_taggable/caching_spec.rb +123 -0
  139. data/spec/make_taggable/default_parser_spec.rb +45 -0
  140. data/spec/make_taggable/dirty_spec.rb +140 -0
  141. data/spec/make_taggable/generic_parser_spec.rb +13 -0
  142. data/spec/make_taggable/make_taggable_spec.rb +260 -0
  143. data/spec/make_taggable/related_spec.rb +93 -0
  144. data/spec/make_taggable/single_table_inheritance_spec.rb +220 -0
  145. data/spec/make_taggable/tag_list_spec.rb +169 -0
  146. data/spec/make_taggable/tag_spec.rb +297 -0
  147. data/spec/make_taggable/taggable_spec.rb +804 -0
  148. data/spec/make_taggable/tagger_spec.rb +149 -0
  149. data/spec/make_taggable/tagging_spec.rb +115 -0
  150. data/spec/make_taggable/tags_helper_spec.rb +43 -0
  151. data/spec/make_taggable/utils_spec.rb +22 -0
  152. data/spec/make_taggable_spec.rb +5 -0
  153. data/spec/spec_helper.rb +18 -0
  154. data/spec/support/array.rb +9 -0
  155. data/spec/support/helpers.rb +31 -0
  156. metadata +391 -0
@@ -0,0 +1,297 @@
1
+ require "spec_helper"
2
+
3
+ describe MakeTaggable::Tag do
4
+ before(:each) do
5
+ @tag = MakeTaggable::Tag.new
6
+ @user = TaggableModel.create(name: "Pablo")
7
+ end
8
+
9
+ describe "named like any" do
10
+ context "case insensitive collation and unique index on tag name", if: using_case_insensitive_collation? do
11
+ before(:each) do
12
+ MakeTaggable::Tag.create(name: "Awesome")
13
+ MakeTaggable::Tag.create(name: "epic")
14
+ end
15
+
16
+ it "should find both tags" do
17
+ expect(MakeTaggable::Tag.named_like_any(%w[awesome epic]).count).to eq(2)
18
+ end
19
+ end
20
+
21
+ context "case insensitive collation without indexes or case sensitive collation with indexes" do
22
+ before(:each) do
23
+ MakeTaggable::Tag.create(name: "Awesome")
24
+ MakeTaggable::Tag.create(name: "awesome")
25
+ MakeTaggable::Tag.create(name: "epic")
26
+ end
27
+
28
+ it "should find both tags" do
29
+ expect(MakeTaggable::Tag.named_like_any(%w[awesome epic]).count).to eq(3)
30
+ end
31
+ end
32
+ end
33
+
34
+ describe "named any" do
35
+ context "with some special characters combinations", if: using_mysql? do
36
+ it "should not raise an invalid encoding exception" do
37
+ expect { MakeTaggable::Tag.named_any(["holä", "hol'ä"]) }.not_to raise_error
38
+ end
39
+ end
40
+ end
41
+
42
+ describe "for context" do
43
+ before(:each) do
44
+ @user.skill_list.add("ruby")
45
+ @user.save
46
+ end
47
+
48
+ it "should return tags that have been used in the given context" do
49
+ expect(MakeTaggable::Tag.for_context("skills").pluck(:name)).to include("ruby")
50
+ end
51
+
52
+ it "should not return tags that have been used in other contexts" do
53
+ expect(MakeTaggable::Tag.for_context("needs").pluck(:name)).to_not include("ruby")
54
+ end
55
+ end
56
+
57
+ describe "find or create by name" do
58
+ before(:each) do
59
+ @tag.name = "awesome"
60
+ @tag.save
61
+ end
62
+
63
+ it "should find by name" do
64
+ expect(MakeTaggable::Tag.find_or_create_with_like_by_name("awesome")).to eq(@tag)
65
+ end
66
+
67
+ it "should find by name case insensitive" do
68
+ expect(MakeTaggable::Tag.find_or_create_with_like_by_name("AWESOME")).to eq(@tag)
69
+ end
70
+
71
+ it "should create by name" do
72
+ expect(-> {
73
+ MakeTaggable::Tag.find_or_create_with_like_by_name("epic")
74
+ }).to change(MakeTaggable::Tag, :count).by(1)
75
+ end
76
+ end
77
+
78
+ describe "find or create by unicode name", unless: using_sqlite? do
79
+ before(:each) do
80
+ @tag.name = "привет"
81
+ @tag.save
82
+ end
83
+
84
+ it "should find by name" do
85
+ expect(MakeTaggable::Tag.find_or_create_with_like_by_name("привет")).to eq(@tag)
86
+ end
87
+
88
+ it "should find by name case insensitive" do
89
+ expect(MakeTaggable::Tag.find_or_create_with_like_by_name("ПРИВЕТ")).to eq(@tag)
90
+ end
91
+
92
+ it "should find by name accent insensitive", if: using_case_insensitive_collation? do
93
+ @tag.name = "inupiat"
94
+ @tag.save
95
+ expect(MakeTaggable::Tag.find_or_create_with_like_by_name("Iñupiat")).to eq(@tag)
96
+ end
97
+ end
98
+
99
+ describe "find or create all by any name" do
100
+ before(:each) do
101
+ @tag.name = "awesome"
102
+ @tag.save
103
+ end
104
+
105
+ it "should find by name" do
106
+ expect(MakeTaggable::Tag.find_or_create_all_with_like_by_name("awesome")).to eq([@tag])
107
+ end
108
+
109
+ it "should find by name case insensitive" do
110
+ expect(MakeTaggable::Tag.find_or_create_all_with_like_by_name("AWESOME")).to eq([@tag])
111
+ end
112
+
113
+ context "case sensitive" do
114
+ it "should find by name case sensitive" do
115
+ MakeTaggable.strict_case_match = true
116
+ expect {
117
+ MakeTaggable::Tag.find_or_create_all_with_like_by_name("AWESOME")
118
+ }.to change(MakeTaggable::Tag, :count).by(1)
119
+ end
120
+ end
121
+
122
+ it "should create by name" do
123
+ expect {
124
+ MakeTaggable::Tag.find_or_create_all_with_like_by_name("epic")
125
+ }.to change(MakeTaggable::Tag, :count).by(1)
126
+ end
127
+
128
+ context "case sensitive" do
129
+ it "should find or create by name case sensitive" do
130
+ MakeTaggable.strict_case_match = true
131
+ expect {
132
+ expect(MakeTaggable::Tag.find_or_create_all_with_like_by_name("AWESOME", "awesome").map(&:name)).to eq(%w[AWESOME awesome])
133
+ }.to change(MakeTaggable::Tag, :count).by(1)
134
+ end
135
+ end
136
+
137
+ it "should find or create by name" do
138
+ expect {
139
+ expect(MakeTaggable::Tag.find_or_create_all_with_like_by_name("awesome", "epic").map(&:name)).to eq(%w[awesome epic])
140
+ }.to change(MakeTaggable::Tag, :count).by(1)
141
+ end
142
+
143
+ it "should return an empty array if no tags are specified" do
144
+ expect(MakeTaggable::Tag.find_or_create_all_with_like_by_name([])).to be_empty
145
+ end
146
+ end
147
+
148
+ it "should require a name" do
149
+ @tag.valid?
150
+ # TODO, we should find another way to check this
151
+ expect(@tag.errors[:name]).to eq(["can't be blank"])
152
+
153
+ @tag.name = "something"
154
+ @tag.valid?
155
+
156
+ expect(@tag.errors[:name]).to be_empty
157
+ end
158
+
159
+ it "should limit the name length to 255 or less characters" do
160
+ @tag.name = "fgkgnkkgjymkypbuozmwwghblmzpqfsgjasflblywhgkwndnkzeifalfcpeaeqychjuuowlacmuidnnrkprgpcpybarbkrmziqihcrxirlokhnzfvmtzixgvhlxzncyywficpraxfnjptxxhkqmvicbcdcynkjvziefqzyndxkjmsjlvyvbwraklbalykyxoliqdlreeykuphdtmzfdwpphmrqvwvqffojkqhlzvinqajsxbszyvrqqyzusxranr"
161
+ @tag.valid?
162
+ # TODO, we should find another way to check this
163
+ expect(@tag.errors[:name]).to eq(["is too long (maximum is 255 characters)"])
164
+
165
+ @tag.name = "fgkgnkkgjymkypbuozmwwghblmzpqfsgjasflblywhgkwndnkzeifalfcpeaeqychjuuowlacmuidnnrkprgpcpybarbkrmziqihcrxirlokhnzfvmtzixgvhlxzncyywficpraxfnjptxxhkqmvicbcdcynkjvziefqzyndxkjmsjlvyvbwraklbalykyxoliqdlreeykuphdtmzfdwpphmrqvwvqffojkqhlzvinqajsxbszyvrqqyzusxran"
166
+ @tag.valid?
167
+ expect(@tag.errors[:name]).to be_empty
168
+ end
169
+
170
+ it "should equal a tag with the same name" do
171
+ @tag.name = "awesome"
172
+ new_tag = MakeTaggable::Tag.new(name: "awesome")
173
+ expect(new_tag).to eq(@tag)
174
+ end
175
+
176
+ it "should return its name when to_s is called" do
177
+ @tag.name = "cool"
178
+ expect(@tag.to_s).to eq("cool")
179
+ end
180
+
181
+ it "have named_scope named(something)" do
182
+ @tag.name = "cool"
183
+ @tag.save!
184
+ expect(MakeTaggable::Tag.named("cool")).to include(@tag)
185
+ end
186
+
187
+ it "have named_scope named_like(something)" do
188
+ @tag.name = "cool"
189
+ @tag.save!
190
+ @another_tag = MakeTaggable::Tag.create!(name: "coolip")
191
+ expect(MakeTaggable::Tag.named_like("cool")).to include(@tag, @another_tag)
192
+ end
193
+
194
+ describe "escape wildcard symbols in like requests" do
195
+ before(:each) do
196
+ @tag.name = "cool"
197
+ @tag.save
198
+ @another_tag = MakeTaggable::Tag.create!(name: "coo%")
199
+ @another_tag2 = MakeTaggable::Tag.create!(name: "coolish")
200
+ end
201
+
202
+ it "return escaped result when '%' char present in tag" do
203
+ expect(MakeTaggable::Tag.named_like("coo%")).to_not include(@tag)
204
+ expect(MakeTaggable::Tag.named_like("coo%")).to include(@another_tag)
205
+ end
206
+ end
207
+
208
+ describe "when using strict_case_match" do
209
+ before do
210
+ MakeTaggable.strict_case_match = true
211
+ @tag.name = "awesome"
212
+ @tag.save!
213
+ end
214
+
215
+ after do
216
+ MakeTaggable.strict_case_match = false
217
+ end
218
+
219
+ it "should find by name" do
220
+ expect(MakeTaggable::Tag.find_or_create_with_like_by_name("awesome")).to eq(@tag)
221
+ end
222
+
223
+ context "case sensitive" do
224
+ it "should find by name case sensitively" do
225
+ expect {
226
+ MakeTaggable::Tag.find_or_create_with_like_by_name("AWESOME")
227
+ }.to change(MakeTaggable::Tag, :count)
228
+
229
+ expect(MakeTaggable::Tag.last.name).to eq("AWESOME")
230
+ end
231
+ end
232
+
233
+ context "case sensitive" do
234
+ it "should have a named_scope named(something) that matches exactly" do
235
+ uppercase_tag = MakeTaggable::Tag.create(name: "Cool")
236
+ @tag.name = "cool"
237
+ @tag.save!
238
+
239
+ expect(MakeTaggable::Tag.named("cool")).to include(@tag)
240
+ expect(MakeTaggable::Tag.named("cool")).to_not include(uppercase_tag)
241
+ end
242
+ end
243
+
244
+ it "should not change encoding" do
245
+ name = "\u3042"
246
+ original_encoding = name.encoding
247
+ record = MakeTaggable::Tag.find_or_create_with_like_by_name(name)
248
+ record.reload
249
+ expect(record.name.encoding).to eq(original_encoding)
250
+ end
251
+
252
+ context "named any with some special characters combinations", if: using_mysql? do
253
+ it "should not raise an invalid encoding exception" do
254
+ expect { MakeTaggable::Tag.named_any(["holä", "hol'ä"]) }.not_to raise_error
255
+ end
256
+ end
257
+ end
258
+
259
+ describe "name uniqeness validation" do
260
+ let(:duplicate_tag) { MakeTaggable::Tag.new(name: "ror") }
261
+
262
+ before { MakeTaggable::Tag.create(name: "ror") }
263
+
264
+ context "when do need unique names" do
265
+ it "should run uniqueness validation" do
266
+ expect(duplicate_tag).to_not be_valid
267
+ end
268
+
269
+ it "add error to name" do
270
+ duplicate_tag.save
271
+
272
+ expect(duplicate_tag.errors.size).to eq(1)
273
+ expect(duplicate_tag.errors.messages[:name]).to include("has already been taken")
274
+ end
275
+ end
276
+ end
277
+
278
+ describe "popular tags" do
279
+ before do
280
+ %w[sports rails linux tennis golden_syrup].each_with_index do |t, i|
281
+ tag = MakeTaggable::Tag.new(name: t)
282
+ tag.taggings_count = i
283
+ tag.save!
284
+ end
285
+ end
286
+
287
+ it "should find the most popular tags" do
288
+ expect(MakeTaggable::Tag.most_used(3).first.name).to eq("golden_syrup")
289
+ expect(MakeTaggable::Tag.most_used(3).length).to eq(3)
290
+ end
291
+
292
+ it "should find the least popular tags" do
293
+ expect(MakeTaggable::Tag.least_used(3).first.name).to eq("sports")
294
+ expect(MakeTaggable::Tag.least_used(3).length).to eq(3)
295
+ end
296
+ end
297
+ end
@@ -0,0 +1,804 @@
1
+ require "spec_helper"
2
+
3
+ describe "Taggable To Preserve Order" do
4
+ before(:each) do
5
+ @taggable = OrderedTaggableModel.new(name: "Bob Jones")
6
+ end
7
+
8
+ it "should have tag associations" do
9
+ [:tags, :colours].each do |type|
10
+ expect(@taggable.respond_to?(type)).to be_truthy
11
+ expect(@taggable.respond_to?("#{type.to_s.singularize}_taggings")).to be_truthy
12
+ end
13
+ end
14
+
15
+ it "should have tag methods" do
16
+ [:tags, :colours].each do |type|
17
+ expect(@taggable.respond_to?("#{type.to_s.singularize}_list")).to be_truthy
18
+ expect(@taggable.respond_to?("#{type.to_s.singularize}_list=")).to be_truthy
19
+ expect(@taggable.respond_to?("all_#{type}_list")).to be_truthy
20
+ end
21
+ end
22
+
23
+ it "should return tag list in the order the tags were created" do
24
+ # create
25
+ @taggable.tag_list = "rails, ruby, css"
26
+ expect(@taggable.instance_variable_get("@tag_list").instance_of?(MakeTaggable::TagList)).to be_truthy
27
+
28
+ expect(-> {
29
+ @taggable.save
30
+ }).to change(MakeTaggable::Tag, :count).by(3)
31
+
32
+ @taggable.reload
33
+ expect(@taggable.tag_list).to eq(%w[rails ruby css])
34
+
35
+ # update
36
+ @taggable.tag_list = "pow, ruby, rails"
37
+ @taggable.save
38
+
39
+ @taggable.reload
40
+ expect(@taggable.tag_list).to eq(%w[pow ruby rails])
41
+
42
+ # update with no change
43
+ @taggable.tag_list = "pow, ruby, rails"
44
+ @taggable.save
45
+
46
+ @taggable.reload
47
+ expect(@taggable.tag_list).to eq(%w[pow ruby rails])
48
+
49
+ # update to clear tags
50
+ @taggable.tag_list = ""
51
+ @taggable.save
52
+
53
+ @taggable.reload
54
+ expect(@taggable.tag_list).to be_empty
55
+ end
56
+
57
+ it "should return tag objects in the order the tags were created" do
58
+ # create
59
+ @taggable.tag_list = "pow, ruby, rails"
60
+ expect(@taggable.instance_variable_get("@tag_list").instance_of?(MakeTaggable::TagList)).to be_truthy
61
+
62
+ expect(-> {
63
+ @taggable.save
64
+ }).to change(MakeTaggable::Tag, :count).by(3)
65
+
66
+ @taggable.reload
67
+ expect(@taggable.tags.map { |t| t.name }).to eq(%w[pow ruby rails])
68
+
69
+ # update
70
+ @taggable.tag_list = "rails, ruby, css, pow"
71
+ @taggable.save
72
+
73
+ @taggable.reload
74
+ expect(@taggable.tags.map { |t| t.name }).to eq(%w[rails ruby css pow])
75
+ end
76
+
77
+ it "should return tag objects in tagging id order" do
78
+ # create
79
+ @taggable.tag_list = "pow, ruby, rails"
80
+ @taggable.save
81
+
82
+ @taggable.reload
83
+ ids = @taggable.tags.map { |t| t.taggings.first.id }
84
+ expect(ids).to eq(ids.sort)
85
+
86
+ # update
87
+ @taggable.tag_list = "rails, ruby, css, pow"
88
+ @taggable.save
89
+
90
+ @taggable.reload
91
+ ids = @taggable.tags.map { |t| t.taggings.first.id }
92
+ expect(ids).to eq(ids.sort)
93
+ end
94
+ end
95
+
96
+ describe "Taggable" do
97
+ before(:each) do
98
+ @taggable = TaggableModel.new(name: "Bob Jones")
99
+ @taggables = [@taggable, TaggableModel.new(name: "John Doe")]
100
+ end
101
+
102
+ it "should have tag types" do
103
+ [:tags, :languages, :skills, :needs, :offerings].each do |type|
104
+ expect(TaggableModel.tag_types).to include type
105
+ end
106
+
107
+ expect(@taggable.tag_types).to eq(TaggableModel.tag_types)
108
+ end
109
+
110
+ it "should have tag_counts_on" do
111
+ expect(TaggableModel.tag_counts_on(:tags)).to be_empty
112
+
113
+ @taggable.tag_list = %w[awesome epic]
114
+ @taggable.save
115
+
116
+ expect(TaggableModel.tag_counts_on(:tags).length).to eq(2)
117
+ expect(@taggable.tag_counts_on(:tags).length).to eq(2)
118
+ end
119
+
120
+ context "tag_counts on a collection" do
121
+ context "a select clause is specified on the collection" do
122
+ it "should return tag counts without raising an error" do
123
+ expect(TaggableModel.tag_counts_on(:tags)).to be_empty
124
+
125
+ @taggable.tag_list = %w[awesome epic]
126
+ @taggable.save
127
+
128
+ expect {
129
+ expect(TaggableModel.select(:name).tag_counts_on(:tags).length).to eq(2)
130
+ }.not_to raise_error
131
+ end
132
+ end
133
+ end
134
+
135
+ it "should have tags_on" do
136
+ expect(TaggableModel.tags_on(:tags)).to be_empty
137
+
138
+ @taggable.tag_list = %w[awesome epic]
139
+ @taggable.save
140
+
141
+ expect(TaggableModel.tags_on(:tags).length).to eq(2)
142
+ expect(@taggable.tags_on(:tags).length).to eq(2)
143
+ end
144
+
145
+ it "should return [] right after create" do
146
+ blank_taggable = TaggableModel.new(name: "Bob Jones")
147
+ expect(blank_taggable.tag_list).to be_empty
148
+ end
149
+
150
+ it "should be able to create tags" do
151
+ @taggable.skill_list = "ruby, rails, css"
152
+ expect(@taggable.instance_variable_get("@skill_list").instance_of?(MakeTaggable::TagList)).to be_truthy
153
+
154
+ expect(-> {
155
+ @taggable.save
156
+ }).to change(MakeTaggable::Tag, :count).by(3)
157
+
158
+ @taggable.reload
159
+ expect(@taggable.skill_list.sort).to eq(%w[ruby rails css].sort)
160
+ end
161
+
162
+ it "should be able to create tags through the tag list directly" do
163
+ @taggable.tag_list_on(:test).add("hello")
164
+ expect(@taggable.tag_list_cache_on(:test)).to_not be_empty
165
+ expect(@taggable.tag_list_on(:test)).to eq(["hello"])
166
+
167
+ @taggable.save
168
+ @taggable.save_tags
169
+
170
+ @taggable.reload
171
+ expect(@taggable.tag_list_on(:test)).to eq(["hello"])
172
+ end
173
+
174
+ it "should differentiate between contexts" do
175
+ @taggable.skill_list = "ruby, rails, css"
176
+ @taggable.tag_list = "ruby, bob, charlie"
177
+ @taggable.save
178
+ @taggable.reload
179
+ expect(@taggable.skill_list).to include("ruby")
180
+ expect(@taggable.skill_list).to_not include("bob")
181
+ end
182
+
183
+ it "should be able to remove tags through list alone" do
184
+ @taggable.skill_list = "ruby, rails, css"
185
+ @taggable.save
186
+ @taggable.reload
187
+ expect(@taggable.skills.count).to eq(3)
188
+ @taggable.skill_list = "ruby, rails"
189
+ @taggable.save
190
+ @taggable.reload
191
+ expect(@taggable.skills.count).to eq(2)
192
+ end
193
+
194
+ it "should be able to select taggables by subset of tags using ActiveRelation methods" do
195
+ @taggables[0].tag_list = "bob"
196
+ @taggables[1].tag_list = "charlie"
197
+ @taggables[0].skill_list = "ruby"
198
+ @taggables[1].skill_list = "css"
199
+ @taggables.each { |taggable| taggable.save }
200
+
201
+ @found_taggables_by_tag = TaggableModel.joins(:tags).where(MakeTaggable.tags_table => {name: ["bob"]})
202
+ @found_taggables_by_skill = TaggableModel.joins(:skills).where(MakeTaggable.tags_table => {name: ["ruby"]})
203
+
204
+ expect(@found_taggables_by_tag).to include @taggables[0]
205
+ expect(@found_taggables_by_tag).to_not include @taggables[1]
206
+ expect(@found_taggables_by_skill).to include @taggables[0]
207
+ expect(@found_taggables_by_skill).to_not include @taggables[1]
208
+ end
209
+
210
+ it "should be able to find by tag" do
211
+ @taggable.skill_list = "ruby, rails, css"
212
+ @taggable.save
213
+
214
+ expect(TaggableModel.tagged_with("ruby").first).to eq(@taggable)
215
+ end
216
+
217
+ it "should be able to get a count with find by tag when using a group by" do
218
+ @taggable.skill_list = "ruby"
219
+ @taggable.save
220
+
221
+ expect(TaggableModel.tagged_with("ruby").group(:created_at).count.count).to eq(1)
222
+ end
223
+
224
+ it "can be used as scope" do
225
+ @taggable.skill_list = "ruby"
226
+ @taggable.save
227
+ untaggable_model = @taggable.untaggable_models.create!(name: "foobar")
228
+ scope_tag = TaggableModel.tagged_with("ruby", any: "distinct", order: "taggable_models.name asc")
229
+ expect(UntaggableModel.joins(:taggable_model).merge(scope_tag).except(:select)).to eq([untaggable_model])
230
+ end
231
+
232
+ it "shouldn't generate a query with DISTINCT by default" do
233
+ @taggable.skill_list = "ruby, rails, css"
234
+ @taggable.save
235
+
236
+ expect(TaggableModel.tagged_with("ruby").to_sql).to_not match(/DISTINCT/)
237
+ end
238
+
239
+ it "should be able to find a tag using dates" do
240
+ @taggable.skill_list = "ruby"
241
+ @taggable.save
242
+ today = Date.today.to_time.utc
243
+ tomorrow = Date.tomorrow.to_time.utc
244
+
245
+ expect(TaggableModel.tagged_with("ruby", start_at: today, end_at: tomorrow).count).to eq(1)
246
+ end
247
+
248
+ it "shouldn't be able to find a tag outside date range" do
249
+ @taggable.skill_list = "ruby"
250
+ @taggable.save
251
+
252
+ expect(TaggableModel.tagged_with("ruby", start_at: Date.today - 2.days, end_at: Date.today - 1.day).count).to eq(0)
253
+ end
254
+
255
+ it "should be able to find by tag with context" do
256
+ @taggable.skill_list = "ruby, rails, css, julia"
257
+ @taggable.tag_list = "bob, charlie, julia"
258
+ @taggable.save
259
+
260
+ expect(TaggableModel.tagged_with("ruby").first).to eq(@taggable)
261
+ expect(TaggableModel.tagged_with("ruby, css").first).to eq(@taggable)
262
+ expect(TaggableModel.tagged_with("bob", on: :skills).first).to_not eq(@taggable)
263
+ expect(TaggableModel.tagged_with("bob", on: :tags).first).to eq(@taggable)
264
+ expect(TaggableModel.tagged_with("julia", on: :skills).size).to eq(1)
265
+ expect(TaggableModel.tagged_with("julia", on: :tags).size).to eq(1)
266
+ expect(TaggableModel.tagged_with("julia", on: nil).size).to eq(2)
267
+ end
268
+
269
+ it "should not care about case" do
270
+ TaggableModel.create(name: "Bob", tag_list: "ruby")
271
+ TaggableModel.create(name: "Frank", tag_list: "Ruby")
272
+
273
+ expect(MakeTaggable::Tag.all.size).to eq(1)
274
+ expect(TaggableModel.tagged_with("ruby").to_a).to eq(TaggableModel.tagged_with("Ruby").to_a)
275
+ end
276
+
277
+ it "should be able to find by tags with other joins in the query" do
278
+ @taggable.skill_list = "ruby, rails, css"
279
+ @taggable.tag_list = "bob, charlie"
280
+ @taggable.save
281
+
282
+ expect(TaggableModel.tagged_with(["bob", "css"], any: true).to_a).to eq([@taggable])
283
+
284
+ bob = TaggableModel.create(name: "Bob", tag_list: "ruby, rails, css")
285
+ frank = TaggableModel.create(name: "Frank", tag_list: "ruby, rails")
286
+ charlie = TaggableModel.create(name: "Charlie", skill_list: "ruby, java")
287
+
288
+ # Test for explicit distinct in select
289
+ bob.untaggable_models.create!
290
+ frank.untaggable_models.create!
291
+ charlie.untaggable_models.create!
292
+
293
+ expect(TaggableModel.select("distinct(taggable_models.id), taggable_models.*").joins(:untaggable_models).tagged_with(["css", "java"], any: true).to_a.sort).to eq([bob, charlie].sort)
294
+
295
+ expect(TaggableModel.select("distinct(taggable_models.id), taggable_models.*").joins(:untaggable_models).tagged_with(["rails", "ruby"], any: false).to_a.sort).to eq([bob, frank].sort)
296
+ end
297
+
298
+ it "should not care about case for unicode names", unless: using_sqlite? do
299
+ MakeTaggable.strict_case_match = false
300
+ TaggableModel.create(name: "Anya", tag_list: "ПРИВЕТ")
301
+ TaggableModel.create(name: "Igor", tag_list: "привет")
302
+ TaggableModel.create(name: "Katia", tag_list: "ПРИВЕТ")
303
+
304
+ expect(MakeTaggable::Tag.all.size).to eq(1)
305
+ expect(TaggableModel.tagged_with("привет").to_a).to eq(TaggableModel.tagged_with("ПРИВЕТ").to_a)
306
+ end
307
+
308
+ context "should be able to create and find tags in languages without capitalization :" do
309
+ MakeTaggable.strict_case_match = false
310
+ {
311
+ japanese: {name: "Chihiro", tag_list: "日本の"},
312
+ hebrew: {name: "Salim", tag_list: "עברית"},
313
+ chinese: {name: "Ieie", tag_list: "中国的"},
314
+ arabic: {name: "Yasser", tag_list: "العربية"},
315
+ emo: {name: "Emo", tag_list: "✏"}
316
+ }.each do |language, values|
317
+ it language do
318
+ TaggableModel.create(values)
319
+ expect(TaggableModel.tagged_with(values[:tag_list]).count).to eq(1)
320
+ end
321
+ end
322
+ end
323
+
324
+ it "should be able to get tag counts on model as a whole" do
325
+ TaggableModel.create(name: "Bob", tag_list: "ruby, rails, css")
326
+ TaggableModel.create(name: "Frank", tag_list: "ruby, rails")
327
+ TaggableModel.create(name: "Charlie", skill_list: "ruby")
328
+ expect(TaggableModel.tag_counts).to_not be_empty
329
+ expect(TaggableModel.skill_counts).to_not be_empty
330
+ end
331
+
332
+ it "should be able to get all tag counts on model as whole" do
333
+ TaggableModel.create(name: "Bob", tag_list: "ruby, rails, css")
334
+ TaggableModel.create(name: "Frank", tag_list: "ruby, rails")
335
+ TaggableModel.create(name: "Charlie", skill_list: "ruby")
336
+
337
+ expect(TaggableModel.all_tag_counts).to_not be_empty
338
+ expect(TaggableModel.all_tag_counts(order: "#{MakeTaggable.tags_table}.id").first.count).to eq(3) # ruby
339
+ end
340
+
341
+ it "should be able to get all tags on model as whole" do
342
+ TaggableModel.create(name: "Bob", tag_list: "ruby, rails, css")
343
+ TaggableModel.create(name: "Frank", tag_list: "ruby, rails")
344
+ TaggableModel.create(name: "Charlie", skill_list: "ruby")
345
+
346
+ expect(TaggableModel.all_tags).to_not be_empty
347
+ expect(TaggableModel.all_tags(order: "#{MakeTaggable.tags_table}.id").first.name).to eq("ruby")
348
+ end
349
+
350
+ it "should be able to use named scopes to chain tag finds by any tags by context" do
351
+ bob = TaggableModel.create(name: "Bob", need_list: "rails", offering_list: "c++")
352
+ TaggableModel.create(name: "Frank", need_list: "css", offering_list: "css")
353
+ TaggableModel.create(name: "Steve", need_list: "c++", offering_list: "java")
354
+
355
+ # Let's only find those who need rails or css and are offering c++ or java
356
+ expect(TaggableModel.tagged_with(["rails, css"], on: :needs, any: true).tagged_with(["c++", "java"], on: :offerings, any: true).to_a).to eq([bob])
357
+ end
358
+
359
+ it "should not return read-only records" do
360
+ TaggableModel.create(name: "Bob", tag_list: "ruby, rails, css")
361
+ expect(TaggableModel.tagged_with("ruby").first).to_not be_readonly
362
+ end
363
+
364
+ it "should be able to get scoped tag counts" do
365
+ TaggableModel.create(name: "Bob", tag_list: "ruby, rails, css")
366
+ TaggableModel.create(name: "Frank", tag_list: "ruby, rails")
367
+ TaggableModel.create(name: "Charlie", skill_list: "ruby")
368
+
369
+ expect(TaggableModel.tagged_with("ruby").tag_counts(order: "#{MakeTaggable.tags_table}.id").first.count).to eq(2) # ruby
370
+ expect(TaggableModel.tagged_with("ruby").skill_counts.first.count).to eq(1) # ruby
371
+ end
372
+
373
+ it "should be able to get all scoped tag counts" do
374
+ TaggableModel.create(name: "Bob", tag_list: "ruby, rails, css")
375
+ TaggableModel.create(name: "Frank", tag_list: "ruby, rails")
376
+ TaggableModel.create(name: "Charlie", skill_list: "ruby")
377
+
378
+ expect(TaggableModel.tagged_with("ruby").all_tag_counts(order: "#{MakeTaggable.tags_table}.id").first.count).to eq(3) # ruby
379
+ end
380
+
381
+ it "should be able to get all scoped tags" do
382
+ TaggableModel.create(name: "Bob", tag_list: "ruby, rails, css")
383
+ TaggableModel.create(name: "Frank", tag_list: "ruby, rails")
384
+ TaggableModel.create(name: "Charlie", skill_list: "ruby")
385
+
386
+ expect(TaggableModel.tagged_with("ruby").all_tags(order: "#{MakeTaggable.tags_table}.id").first.name).to eq("ruby")
387
+ end
388
+
389
+ it "should only return tag counts for the available scope" do
390
+ frank = TaggableModel.create(name: "Frank", tag_list: "ruby, rails")
391
+ TaggableModel.create(name: "Bob", tag_list: "ruby, rails, css")
392
+ TaggableModel.create(name: "Charlie", skill_list: "ruby, java")
393
+
394
+ expect(TaggableModel.tagged_with("rails").all_tag_counts.size).to eq(3)
395
+ expect(TaggableModel.tagged_with("rails").all_tag_counts.any? { |tag| tag.name == "java" }).to be_falsy
396
+
397
+ # Test specific join syntaxes:
398
+ frank.untaggable_models.create!
399
+ expect(TaggableModel.tagged_with("rails").joins(:untaggable_models).all_tag_counts.size).to eq(2)
400
+ expect(TaggableModel.tagged_with("rails").joins([:untaggable_models]).all_tag_counts.size).to eq(2)
401
+ end
402
+
403
+ it "should only return tags for the available scope" do
404
+ frank = TaggableModel.create(name: "Frank", tag_list: "ruby, rails")
405
+ TaggableModel.create(name: "Bob", tag_list: "ruby, rails, css")
406
+ TaggableModel.create(name: "Charlie", skill_list: "ruby, java")
407
+
408
+ expect(TaggableModel.tagged_with("rails").all_tags.count).to eq(3)
409
+ expect(TaggableModel.tagged_with("rails").all_tags.any? { |tag| tag.name == "java" }).to be_falsy
410
+
411
+ # Test specific join syntaxes:
412
+ frank.untaggable_models.create!
413
+ expect(TaggableModel.tagged_with("rails").joins(:untaggable_models).all_tags.size).to eq(2)
414
+ expect(TaggableModel.tagged_with("rails").joins([:untaggable_models]).all_tags.size).to eq(2)
415
+ end
416
+
417
+ it "should be able to set a custom tag context list" do
418
+ bob = TaggableModel.create(name: "Bob")
419
+ bob.set_tag_list_on(:rotors, "spinning, jumping")
420
+ expect(bob.tag_list_on(:rotors)).to eq(%w[spinning jumping])
421
+ bob.save
422
+ bob.reload
423
+ expect(bob.tags_on(:rotors)).to_not be_empty
424
+ end
425
+
426
+ it "should be able to find tagged" do
427
+ bob = TaggableModel.create(name: "Bob", tag_list: "fitter, happier, more productive", skill_list: "ruby, rails, css")
428
+ frank = TaggableModel.create(name: "Frank", tag_list: "weaker, depressed, inefficient", skill_list: "ruby, rails, css")
429
+ steve = TaggableModel.create(name: "Steve", tag_list: "fitter, happier, more productive", skill_list: "c++, java, ruby")
430
+
431
+ expect(TaggableModel.tagged_with("ruby", order: "taggable_models.name").to_a).to eq([bob, frank, steve])
432
+ expect(TaggableModel.tagged_with("ruby, rails", order: "taggable_models.name").to_a).to eq([bob, frank])
433
+ expect(TaggableModel.tagged_with(%w[ruby rails], order: "taggable_models.name").to_a).to eq([bob, frank])
434
+ end
435
+
436
+ it "should be able to find tagged with quotation marks" do
437
+ bob = TaggableModel.create(name: "Bob", tag_list: "fitter, happier, more productive, 'I love the ,comma,'")
438
+ expect(TaggableModel.tagged_with("'I love the ,comma,'")).to include(bob)
439
+ end
440
+
441
+ it "should be able to find tagged with invalid tags" do
442
+ bob = TaggableModel.create(name: "Bob", tag_list: "fitter, happier, more productive")
443
+ expect(TaggableModel.tagged_with("sad, happier")).to_not include(bob)
444
+ end
445
+
446
+ it "should be able to find tagged with any tag" do
447
+ bob = TaggableModel.create(name: "Bob", tag_list: "fitter, happier, more productive", skill_list: "ruby, rails, css")
448
+ frank = TaggableModel.create(name: "Frank", tag_list: "weaker, depressed, inefficient", skill_list: "ruby, rails, css")
449
+ steve = TaggableModel.create(name: "Steve", tag_list: "fitter, happier, more productive", skill_list: "c++, java, ruby")
450
+
451
+ expect(TaggableModel.tagged_with(%w[ruby java], order: "taggable_models.name", any: true).to_a).to eq([bob, frank, steve])
452
+ expect(TaggableModel.tagged_with(%w[c++ fitter], order: "taggable_models.name", any: true).to_a).to eq([bob, steve])
453
+ expect(TaggableModel.tagged_with(%w[depressed css], order: "taggable_models.name", any: true).to_a).to eq([bob, frank])
454
+ end
455
+
456
+ it "should be able to order by number of matching tags when matching any" do
457
+ bob = TaggableModel.create(name: "Bob", tag_list: "fitter, happier, more productive", skill_list: "ruby, rails, css")
458
+ frank = TaggableModel.create(name: "Frank", tag_list: "weaker, depressed, inefficient", skill_list: "ruby, rails, css")
459
+ steve = TaggableModel.create(name: "Steve", tag_list: "fitter, happier, more productive", skill_list: "c++, java, ruby")
460
+
461
+ expect(TaggableModel.tagged_with(%w[ruby java], any: true, order_by_matching_tag_count: true, order: "taggable_models.name").to_a).to eq([steve, bob, frank])
462
+ expect(TaggableModel.tagged_with(%w[c++ fitter], any: true, order_by_matching_tag_count: true, order: "taggable_models.name").to_a).to eq([steve, bob])
463
+ expect(TaggableModel.tagged_with(%w[depressed css], any: true, order_by_matching_tag_count: true, order: "taggable_models.name").to_a).to eq([frank, bob])
464
+ expect(TaggableModel.tagged_with(["fitter", "happier", "more productive", "c++", "java", "ruby"], any: true, order_by_matching_tag_count: true, order: "taggable_models.name").to_a).to eq([steve, bob, frank])
465
+ expect(TaggableModel.tagged_with(%w[c++ java ruby fitter], any: true, order_by_matching_tag_count: true, order: "taggable_models.name").to_a).to eq([steve, bob, frank])
466
+ end
467
+
468
+ context "wild: true" do
469
+ it "should use params as wildcards" do
470
+ bob = TaggableModel.create(name: "Bob", tag_list: "bob, tricia")
471
+ frank = TaggableModel.create(name: "Frank", tag_list: "bobby, jim")
472
+ steve = TaggableModel.create(name: "Steve", tag_list: "john, patricia")
473
+ jim = TaggableModel.create(name: "Jim", tag_list: "jim, steve")
474
+
475
+ expect(TaggableModel.tagged_with(%w[bob tricia], wild: true, any: true).to_a.sort_by { |o| o.id }).to eq([bob, frank, steve])
476
+ expect(TaggableModel.tagged_with(%w[bob tricia], wild: true, exclude: true).to_a).to eq([jim])
477
+ expect(TaggableModel.tagged_with("ji", wild: true, any: true).to_a =~ [frank, jim])
478
+ end
479
+ end
480
+
481
+ it "should be able to find tagged on a custom tag context" do
482
+ bob = TaggableModel.create(name: "Bob")
483
+ bob.set_tag_list_on(:rotors, "spinning, jumping")
484
+ expect(bob.tag_list_on(:rotors)).to eq(%w[spinning jumping])
485
+ bob.save
486
+
487
+ expect(TaggableModel.tagged_with("spinning", on: :rotors).to_a).to eq([bob])
488
+ end
489
+
490
+ it "should be able to use named scopes to chain tag finds" do
491
+ bob = TaggableModel.create(name: "Bob", tag_list: "fitter, happier, more productive", skill_list: "ruby, rails, css")
492
+ frank = TaggableModel.create(name: "Frank", tag_list: "weaker, depressed, inefficient", skill_list: "ruby, rails, css")
493
+ steve = TaggableModel.create(name: "Steve", tag_list: "fitter, happier, more productive", skill_list: "c++, java, python")
494
+
495
+ # Let's only find those productive Rails developers
496
+ expect(TaggableModel.tagged_with("rails", on: :skills, order: "taggable_models.name").to_a).to eq([bob, frank])
497
+ expect(TaggableModel.tagged_with("happier", on: :tags, order: "taggable_models.name").to_a).to eq([bob, steve])
498
+ expect(TaggableModel.tagged_with("rails", on: :skills).tagged_with("happier", on: :tags).to_a).to eq([bob])
499
+ expect(TaggableModel.tagged_with("rails").tagged_with("happier", on: :tags).to_a).to eq([bob])
500
+ end
501
+
502
+ it "should be able to find tagged with only the matching tags" do
503
+ TaggableModel.create(name: "Bob", tag_list: "lazy, happier")
504
+ TaggableModel.create(name: "Frank", tag_list: "fitter, happier, inefficient")
505
+ steve = TaggableModel.create(name: "Steve", tag_list: "fitter, happier")
506
+
507
+ expect(TaggableModel.tagged_with("fitter, happier", match_all: true).to_a).to eq([steve])
508
+ end
509
+
510
+ it "should be able to find tagged with only the matching tags for a context" do
511
+ TaggableModel.create(name: "Bob", tag_list: "lazy, happier", skill_list: "ruby, rails, css")
512
+ frank = TaggableModel.create(name: "Frank", tag_list: "fitter, happier, inefficient", skill_list: "css")
513
+ TaggableModel.create(name: "Steve", tag_list: "fitter, happier", skill_list: "ruby, rails, css")
514
+
515
+ expect(TaggableModel.tagged_with("css", on: :skills, match_all: true).to_a).to eq([frank])
516
+ end
517
+
518
+ it "should be able to find tagged with some excluded tags" do
519
+ TaggableModel.create(name: "Bob", tag_list: "happier, lazy")
520
+ frank = TaggableModel.create(name: "Frank", tag_list: "happier")
521
+ steve = TaggableModel.create(name: "Steve", tag_list: "happier")
522
+
523
+ expect(TaggableModel.tagged_with("lazy", exclude: true)).to include(frank, steve)
524
+ expect(TaggableModel.tagged_with("lazy", exclude: true).size).to eq(2)
525
+ end
526
+
527
+ it "should return an empty scope for empty tags" do
528
+ ["", " ", nil, []].each do |tag|
529
+ expect(TaggableModel.tagged_with(tag)).to be_empty
530
+ end
531
+ end
532
+
533
+ it "should options key not be deleted" do
534
+ options = {exclude: true}
535
+ TaggableModel.tagged_with("foo", options)
536
+ expect(options).to eq({exclude: true})
537
+ end
538
+
539
+ it "should not delete tags if not updated" do
540
+ model = TaggableModel.create(name: "foo", tag_list: "ruby, rails, programming")
541
+ model.update(name: "bar")
542
+ model.reload
543
+ expect(model.tag_list.sort).to eq(%w[ruby rails programming].sort)
544
+ end
545
+
546
+ context "Duplicates" do
547
+ context "should not create duplicate taggings" do
548
+ let(:bob) { TaggableModel.create(name: "Bob") }
549
+ context "case sensitive" do
550
+ it "#add" do
551
+ expect(lambda {
552
+ bob.tag_list.add "happier"
553
+ bob.tag_list.add "happier"
554
+ bob.tag_list.add "happier", "rich", "funny"
555
+ bob.save
556
+ }).to change(MakeTaggable::Tagging, :count).by(3)
557
+ end
558
+ it "#<<" do
559
+ expect(lambda {
560
+ bob.tag_list << "social"
561
+ bob.tag_list << "social"
562
+ bob.tag_list << "social" << "wow"
563
+ bob.save
564
+ }).to change(MakeTaggable::Tagging, :count).by(2)
565
+ end
566
+
567
+ it "unicode" do
568
+ expect(lambda {
569
+ bob.tag_list.add "ПРИВЕТ"
570
+ bob.tag_list.add "ПРИВЕТ"
571
+ bob.tag_list.add "ПРИВЕТ", "ПРИВЕТ"
572
+ bob.save
573
+ }).to change(MakeTaggable::Tagging, :count).by(1)
574
+ end
575
+
576
+ it "#=" do
577
+ expect(lambda {
578
+ bob.tag_list = ["Happy", "Happy"]
579
+ bob.save
580
+ }).to change(MakeTaggable::Tagging, :count).by(1)
581
+ end
582
+ end
583
+ context "case insensitive" do
584
+ before(:all) { MakeTaggable.force_lowercase = true }
585
+ after(:all) { MakeTaggable.force_lowercase = false }
586
+
587
+ it "#<<" do
588
+ expect(lambda {
589
+ bob.tag_list << "Alone"
590
+ bob.tag_list << "AloNe"
591
+ bob.tag_list << "ALONE" << "In The dark"
592
+ bob.save
593
+ }).to change(MakeTaggable::Tagging, :count).by(2)
594
+ end
595
+
596
+ it "#add" do
597
+ expect(lambda {
598
+ bob.tag_list.add "forever"
599
+ bob.tag_list.add "ForEver"
600
+ bob.tag_list.add "FOREVER", "ALONE"
601
+ bob.save
602
+ }).to change(MakeTaggable::Tagging, :count).by(2)
603
+ end
604
+
605
+ it "unicode" do
606
+ expect(lambda {
607
+ bob.tag_list.add "ПРИВЕТ"
608
+ bob.tag_list.add "привет", "Привет"
609
+ bob.save
610
+ }).to change(MakeTaggable::Tagging, :count).by(1)
611
+ end
612
+
613
+ it "#=" do
614
+ expect(lambda {
615
+ bob.tag_list = ["Happy", "HAPPY"]
616
+ bob.save
617
+ }).to change(MakeTaggable::Tagging, :count).by(1)
618
+ end
619
+ end
620
+ end
621
+
622
+ xit "should not duplicate tags added on different threads", if: supports_concurrency?, skip: "FIXME, Deadlocks in travis" do
623
+ # TODO, try with more threads and fix deadlock
624
+ thread_count = 4
625
+ barrier = Barrier.new thread_count
626
+
627
+ expect {
628
+ thread_count.times.map { |idx|
629
+ Thread.start do
630
+ connor = TaggableModel.first_or_create(name: "Connor")
631
+ connor.tag_list = "There, can, be, only, one"
632
+ barrier.wait
633
+ begin
634
+ connor.save
635
+ rescue MakeTaggable::DuplicateTagError
636
+ # second save should succeed
637
+ connor.save
638
+ end
639
+ end
640
+ }.map(&:join)
641
+ }.to change(MakeTaggable::Tag, :count).by(5)
642
+ end
643
+ end
644
+
645
+ describe "Associations" do
646
+ before(:each) do
647
+ @taggable = TaggableModel.create(tag_list: "awesome, epic")
648
+ end
649
+
650
+ it "should not remove tags when creating associated objects" do
651
+ @taggable.untaggable_models.create!
652
+ @taggable.reload
653
+ expect(@taggable.tag_list.size).to eq(2)
654
+ end
655
+ end
656
+
657
+ describe "grouped_column_names_for method" do
658
+ it "should return all column names joined for Tag GROUP clause" do
659
+ # NOTE: type column supports an STI Tag subclass in the test suite, though
660
+ # isn't included by default in the migration generator
661
+ expect(@taggable.grouped_column_names_for(MakeTaggable::Tag))
662
+ .to eq("#{MakeTaggable.tags_table}.id, #{MakeTaggable.tags_table}.name, #{MakeTaggable.tags_table}.taggings_count, #{MakeTaggable.tags_table}.created_at, #{MakeTaggable.tags_table}.updated_at")
663
+ end
664
+
665
+ it "should return all column names joined for TaggableModel GROUP clause" do
666
+ expect(@taggable.grouped_column_names_for(TaggableModel)).to eq("taggable_models.id, taggable_models.name, taggable_models.type")
667
+ end
668
+
669
+ it "should return all column names joined for NonStandardIdTaggableModel GROUP clause" do
670
+ expect(@taggable.grouped_column_names_for(TaggableModel)).to eq("taggable_models.#{TaggableModel.primary_key}, taggable_models.name, taggable_models.type")
671
+ end
672
+ end
673
+
674
+ describe "NonStandardIdTaggable" do
675
+ before(:each) do
676
+ @taggable = NonStandardIdTaggableModel.new(name: "Bob Jones")
677
+ @taggables = [@taggable, NonStandardIdTaggableModel.new(name: "John Doe")]
678
+ end
679
+
680
+ it "should have tag types" do
681
+ [:tags, :languages, :skills, :needs, :offerings].each do |type|
682
+ expect(NonStandardIdTaggableModel.tag_types).to include type
683
+ end
684
+
685
+ expect(@taggable.tag_types).to eq(NonStandardIdTaggableModel.tag_types)
686
+ end
687
+
688
+ it "should have tag_counts_on" do
689
+ expect(NonStandardIdTaggableModel.tag_counts_on(:tags)).to be_empty
690
+
691
+ @taggable.tag_list = %w[awesome epic]
692
+ @taggable.save
693
+
694
+ expect(NonStandardIdTaggableModel.tag_counts_on(:tags).length).to eq(2)
695
+ expect(@taggable.tag_counts_on(:tags).length).to eq(2)
696
+ end
697
+
698
+ it "should have tags_on" do
699
+ expect(NonStandardIdTaggableModel.tags_on(:tags)).to be_empty
700
+
701
+ @taggable.tag_list = %w[awesome epic]
702
+ @taggable.save
703
+
704
+ expect(NonStandardIdTaggableModel.tags_on(:tags).length).to eq(2)
705
+ expect(@taggable.tags_on(:tags).length).to eq(2)
706
+ end
707
+
708
+ it "should be able to create tags" do
709
+ @taggable.skill_list = "ruby, rails, css"
710
+ expect(@taggable.instance_variable_get("@skill_list").instance_of?(MakeTaggable::TagList)).to be_truthy
711
+
712
+ expect(-> {
713
+ @taggable.save
714
+ }).to change(MakeTaggable::Tag, :count).by(3)
715
+
716
+ @taggable.reload
717
+ expect(@taggable.skill_list.sort).to eq(%w[ruby rails css].sort)
718
+ end
719
+
720
+ it "should be able to create tags through the tag list directly" do
721
+ @taggable.tag_list_on(:test).add("hello")
722
+ expect(@taggable.tag_list_cache_on(:test)).to_not be_empty
723
+ expect(@taggable.tag_list_on(:test)).to eq(["hello"])
724
+
725
+ @taggable.save
726
+ @taggable.save_tags
727
+
728
+ @taggable.reload
729
+ expect(@taggable.tag_list_on(:test)).to eq(["hello"])
730
+ end
731
+ end
732
+
733
+ describe "Autogenerated methods" do
734
+ it "should be overridable" do
735
+ expect(TaggableModel.create(tag_list: "woo").tag_list_submethod_called).to be_truthy
736
+ end
737
+ end
738
+
739
+ # See https://github.com/mbleigh/acts-as-taggable-on/pull/457 for details
740
+ context "tag_counts and aggreating scopes, compatability with MySQL " do
741
+ before(:each) do
742
+ TaggableModel.new(name: "Barb Jones").tap { |t| t.tag_list = %w[awesome fun] }.save
743
+ TaggableModel.new(name: "John Doe").tap { |t| t.tag_list = %w[cool fun hella] }.save
744
+ TaggableModel.new(name: "Jo Doe").tap { |t| t.tag_list = %w[curious young naive sharp] }.save
745
+
746
+ TaggableModel.all.each { |t| t.save }
747
+ end
748
+
749
+ context "Model.limit(x).tag_counts.sum(:tags_count)" do
750
+ it "should not break on Mysql" do
751
+ expect(TaggableModel.limit(2).tag_counts.sum("tags_count").to_i).to eq(5)
752
+ end
753
+ end
754
+
755
+ context "regression prevention, just making sure these esoteric queries still work" do
756
+ context "Model.tag_counts.limit(x)" do
757
+ it "should limit the tag objects (not very useful, of course)" do
758
+ array_of_tag_counts = TaggableModel.tag_counts.limit(2)
759
+ expect(array_of_tag_counts.count).to eq(2)
760
+ end
761
+ end
762
+
763
+ context "Model.tag_counts.sum(:tags_count)" do
764
+ it "should limit the total tags used" do
765
+ expect(TaggableModel.tag_counts.sum(:tags_count).to_i).to eq(9)
766
+ end
767
+ end
768
+
769
+ context "Model.tag_counts.limit(2).sum(:tags_count)" do
770
+ it "limit should have no effect; this is just a sanity check" do
771
+ expect(TaggableModel.tag_counts.limit(2).sum(:tags_count).to_i).to eq(9)
772
+ end
773
+ end
774
+ end
775
+ end
776
+ end
777
+
778
+ describe "Taggable model with json columns", if: postgresql_support_json? do
779
+ before(:each) do
780
+ @taggable = TaggableModelWithJson.new(name: "Bob Jones")
781
+ @taggables = [@taggable, TaggableModelWithJson.new(name: "John Doe")]
782
+ end
783
+
784
+ it "should be able to find by tag with context" do
785
+ @taggable.skill_list = "ruby, rails, css"
786
+ @taggable.tag_list = "bob, charlie"
787
+ @taggable.save
788
+
789
+ expect(TaggableModelWithJson.tagged_with("ruby").first).to eq(@taggable)
790
+ expect(TaggableModelWithJson.tagged_with("ruby, css").first).to eq(@taggable)
791
+ expect(TaggableModelWithJson.tagged_with("bob", on: :skills).first).to_not eq(@taggable)
792
+ expect(TaggableModelWithJson.tagged_with("bob", on: :tags).first).to eq(@taggable)
793
+ end
794
+
795
+ it "should be able to find tagged with any tag" do
796
+ bob = TaggableModelWithJson.create(name: "Bob", tag_list: "fitter, happier, more productive", skill_list: "ruby, rails, css")
797
+ frank = TaggableModelWithJson.create(name: "Frank", tag_list: "weaker, depressed, inefficient", skill_list: "ruby, rails, css")
798
+ steve = TaggableModelWithJson.create(name: "Steve", tag_list: "fitter, happier, more productive", skill_list: "c++, java, ruby")
799
+
800
+ expect(TaggableModelWithJson.tagged_with(%w[ruby java], order: "taggable_model_with_jsons.name", any: true).to_a).to eq([bob, frank, steve])
801
+ expect(TaggableModelWithJson.tagged_with(%w[c++ fitter], order: "taggable_model_with_jsons.name", any: true).to_a).to eq([bob, steve])
802
+ expect(TaggableModelWithJson.tagged_with(%w[depressed css], order: "taggable_model_with_jsons.name", any: true).to_a).to eq([bob, frank])
803
+ end
804
+ end