make_taggable 0.6.3

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 (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