sb-acts-as-taggable-on 6.5.0

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