ransack 2.3.2 → 4.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (145) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +3 -0
  3. data/.github/SECURITY.md +12 -0
  4. data/.github/workflows/codeql.yml +72 -0
  5. data/.github/workflows/cronjob.yml +99 -0
  6. data/.github/workflows/deploy.yml +35 -0
  7. data/.github/workflows/rubocop.yml +20 -0
  8. data/.github/workflows/test-deploy.yml +29 -0
  9. data/.github/workflows/test.yml +131 -0
  10. data/.nojekyll +0 -0
  11. data/.rubocop.yml +50 -0
  12. data/CHANGELOG.md +251 -1
  13. data/CONTRIBUTING.md +51 -29
  14. data/Gemfile +12 -10
  15. data/README.md +45 -907
  16. data/bug_report_templates/test-ransack-scope-and-column-same-name.rb +78 -0
  17. data/bug_report_templates/test-ransacker-arel-present-predicate.rb +75 -0
  18. data/docs/.gitignore +19 -0
  19. data/docs/.nojekyll +0 -0
  20. data/docs/babel.config.js +3 -0
  21. data/docs/blog/2022-03-27-ransack-3.0.0.md +20 -0
  22. data/docs/docs/getting-started/_category_.json +4 -0
  23. data/docs/docs/getting-started/advanced-mode.md +46 -0
  24. data/docs/docs/getting-started/configuration.md +47 -0
  25. data/docs/docs/getting-started/search-matches.md +67 -0
  26. data/docs/docs/getting-started/simple-mode.md +288 -0
  27. data/docs/docs/getting-started/sorting.md +71 -0
  28. data/docs/docs/getting-started/using-predicates.md +282 -0
  29. data/docs/docs/going-further/_category_.json +4 -0
  30. data/docs/docs/going-further/acts-as-taggable-on.md +114 -0
  31. data/docs/docs/going-further/associations.md +70 -0
  32. data/docs/docs/going-further/custom-predicates.md +52 -0
  33. data/docs/docs/going-further/documentation.md +43 -0
  34. data/docs/docs/going-further/exporting-to-csv.md +49 -0
  35. data/docs/docs/going-further/external-guides.md +57 -0
  36. data/docs/docs/going-further/form-customisation.md +63 -0
  37. data/docs/docs/going-further/i18n.md +53 -0
  38. data/docs/docs/going-further/img/create_release.png +0 -0
  39. data/docs/docs/going-further/merging-searches.md +41 -0
  40. data/docs/docs/going-further/other-notes.md +428 -0
  41. data/docs/docs/going-further/polymorphic-search.md +46 -0
  42. data/docs/docs/going-further/ransackers.md +331 -0
  43. data/docs/docs/going-further/release_process.md +36 -0
  44. data/docs/docs/going-further/saving-queries.md +82 -0
  45. data/docs/docs/going-further/searching-postgres.md +57 -0
  46. data/docs/docs/going-further/wiki-contributors.md +82 -0
  47. data/docs/docs/intro.md +99 -0
  48. data/docs/docusaurus.config.js +120 -0
  49. data/docs/package.json +42 -0
  50. data/docs/sidebars.js +31 -0
  51. data/docs/src/components/HomepageFeatures/index.js +64 -0
  52. data/docs/src/components/HomepageFeatures/styles.module.css +11 -0
  53. data/docs/src/css/custom.css +39 -0
  54. data/docs/src/pages/index.module.css +23 -0
  55. data/docs/src/pages/markdown-page.md +7 -0
  56. data/docs/static/.nojekyll +0 -0
  57. data/docs/static/img/docusaurus.png +0 -0
  58. data/docs/static/img/favicon.ico +0 -0
  59. data/docs/static/img/logo.svg +1 -0
  60. data/docs/static/img/tutorial/docsVersionDropdown.png +0 -0
  61. data/docs/static/img/tutorial/localeDropdown.png +0 -0
  62. data/docs/static/img/undraw_docusaurus_mountain.svg +171 -0
  63. data/docs/static/img/undraw_docusaurus_react.svg +170 -0
  64. data/docs/static/img/undraw_docusaurus_tree.svg +40 -0
  65. data/docs/yarn.lock +8879 -0
  66. data/lib/polyamorous/activerecord/join_association.rb +70 -0
  67. data/{polyamorous/lib/polyamorous/activerecord_6.0_ruby_2 → lib/polyamorous/activerecord}/join_dependency.rb +33 -12
  68. data/lib/polyamorous/activerecord/reflection.rb +11 -0
  69. data/{polyamorous/lib → lib/polyamorous}/polyamorous.rb +3 -4
  70. data/lib/ransack/adapters/active_record/base.rb +83 -10
  71. data/lib/ransack/adapters/active_record/context.rb +56 -44
  72. data/lib/ransack/configuration.rb +53 -10
  73. data/lib/ransack/constants.rb +126 -4
  74. data/lib/ransack/context.rb +34 -5
  75. data/lib/ransack/helpers/form_builder.rb +6 -6
  76. data/lib/ransack/helpers/form_helper.rb +14 -5
  77. data/lib/ransack/helpers.rb +1 -1
  78. data/lib/ransack/locale/sv.yml +70 -0
  79. data/lib/ransack/nodes/attribute.rb +3 -3
  80. data/lib/ransack/nodes/condition.rb +80 -9
  81. data/lib/ransack/nodes/grouping.rb +4 -4
  82. data/lib/ransack/nodes/node.rb +1 -1
  83. data/lib/ransack/nodes/sort.rb +3 -3
  84. data/lib/ransack/nodes/value.rb +3 -3
  85. data/lib/ransack/predicate.rb +1 -1
  86. data/lib/ransack/ransacker.rb +1 -1
  87. data/lib/ransack/search.rb +15 -7
  88. data/lib/ransack/translate.rb +6 -6
  89. data/lib/ransack/version.rb +1 -1
  90. data/lib/ransack/visitor.rb +38 -2
  91. data/lib/ransack.rb +5 -8
  92. data/ransack.gemspec +9 -15
  93. data/spec/blueprints/articles.rb +1 -1
  94. data/spec/blueprints/comments.rb +1 -1
  95. data/spec/blueprints/notes.rb +1 -1
  96. data/spec/blueprints/tags.rb +1 -1
  97. data/spec/console.rb +5 -5
  98. data/spec/helpers/polyamorous_helper.rb +2 -8
  99. data/spec/helpers/ransack_helper.rb +1 -1
  100. data/spec/polyamorous/activerecord_compatibility_spec.rb +15 -0
  101. data/spec/{ransack → polyamorous}/join_association_spec.rb +3 -1
  102. data/spec/{ransack → polyamorous}/join_dependency_spec.rb +0 -16
  103. data/spec/ransack/adapters/active_record/base_spec.rb +125 -16
  104. data/spec/ransack/adapters/active_record/context_spec.rb +19 -18
  105. data/spec/ransack/configuration_spec.rb +33 -9
  106. data/spec/ransack/helpers/form_builder_spec.rb +8 -8
  107. data/spec/ransack/helpers/form_helper_spec.rb +109 -20
  108. data/spec/ransack/nodes/condition_spec.rb +37 -0
  109. data/spec/ransack/nodes/grouping_spec.rb +2 -2
  110. data/spec/ransack/nodes/value_spec.rb +115 -0
  111. data/spec/ransack/predicate_spec.rb +37 -2
  112. data/spec/ransack/search_spec.rb +238 -30
  113. data/spec/ransack/translate_spec.rb +1 -1
  114. data/spec/spec_helper.rb +7 -5
  115. data/spec/support/schema.rb +108 -11
  116. metadata +98 -62
  117. data/.travis.yml +0 -47
  118. data/lib/ransack/adapters/active_record/ransack/constants.rb +0 -128
  119. data/lib/ransack/adapters/active_record/ransack/context.rb +0 -55
  120. data/lib/ransack/adapters/active_record/ransack/nodes/condition.rb +0 -61
  121. data/lib/ransack/adapters/active_record/ransack/translate.rb +0 -8
  122. data/lib/ransack/adapters/active_record/ransack/visitor.rb +0 -47
  123. data/lib/ransack/adapters.rb +0 -64
  124. data/lib/ransack/nodes.rb +0 -8
  125. data/polyamorous/lib/polyamorous/activerecord_5.2_ruby_2/join_association.rb +0 -20
  126. data/polyamorous/lib/polyamorous/activerecord_5.2_ruby_2/join_dependency.rb +0 -79
  127. data/polyamorous/lib/polyamorous/activerecord_5.2_ruby_2/reflection.rb +0 -12
  128. data/polyamorous/lib/polyamorous/activerecord_6.0_ruby_2/join_association.rb +0 -2
  129. data/polyamorous/lib/polyamorous/activerecord_6.0_ruby_2/reflection.rb +0 -2
  130. data/polyamorous/lib/polyamorous/activerecord_6.1_ruby_2/join_association.rb +0 -2
  131. data/polyamorous/lib/polyamorous/activerecord_6.1_ruby_2/join_dependency.rb +0 -2
  132. data/polyamorous/lib/polyamorous/activerecord_6.1_ruby_2/reflection.rb +0 -2
  133. data/polyamorous/lib/polyamorous/version.rb +0 -3
  134. data/polyamorous/polyamorous.gemspec +0 -27
  135. /data/{logo → docs/static/logo}/ransack-h.png +0 -0
  136. /data/{logo → docs/static/logo}/ransack-h.svg +0 -0
  137. /data/{logo → docs/static/logo}/ransack-v.png +0 -0
  138. /data/{logo → docs/static/logo}/ransack-v.svg +0 -0
  139. /data/{logo → docs/static/logo}/ransack.png +0 -0
  140. /data/{logo → docs/static/logo}/ransack.svg +0 -0
  141. /data/{polyamorous/lib → lib}/polyamorous/join.rb +0 -0
  142. /data/{polyamorous/lib → lib}/polyamorous/swapping_reflection_class.rb +0 -0
  143. /data/{polyamorous/lib → lib}/polyamorous/tree_node.rb +0 -0
  144. /data/lib/ransack/{adapters/active_record.rb → active_record.rb} +0 -0
  145. /data/spec/{ransack → polyamorous}/join_spec.rb +0 -0
@@ -20,6 +20,44 @@ module Ransack
20
20
  Search.new(Person, name_eq: 'foobar')
21
21
  end
22
22
 
23
+ context 'whitespace stripping' do
24
+ context 'when whitespace_strip option is true' do
25
+ before do
26
+ Ransack.configure { |c| c.strip_whitespace = true }
27
+ end
28
+
29
+ it 'strips leading & trailing whitespace before building' do
30
+ expect_any_instance_of(Search).to receive(:build)
31
+ .with({ 'name_eq' => 'foobar' })
32
+ Search.new(Person, name_eq: ' foobar ')
33
+ end
34
+ end
35
+
36
+ context 'when whitespace_strip option is false' do
37
+ before do
38
+ Ransack.configure { |c| c.strip_whitespace = false }
39
+ end
40
+
41
+ it 'doesn\'t strip leading & trailing whitespace before building' do
42
+ expect_any_instance_of(Search).to receive(:build)
43
+ .with({ 'name_eq' => ' foobar ' })
44
+ Search.new(Person, name_eq: ' foobar ')
45
+ end
46
+ end
47
+
48
+ it 'strips leading & trailing whitespace when strip_whitespace search parameter is true' do
49
+ expect_any_instance_of(Search).to receive(:build)
50
+ .with({ 'name_eq' => 'foobar' })
51
+ Search.new(Person, { name_eq: ' foobar ' }, { strip_whitespace: true })
52
+ end
53
+
54
+ it 'doesn\'t strip leading & trailing whitespace when strip_whitespace search parameter is false' do
55
+ expect_any_instance_of(Search).to receive(:build)
56
+ .with({ 'name_eq' => ' foobar ' })
57
+ Search.new(Person, { name_eq: ' foobar ' }, { strip_whitespace: false })
58
+ end
59
+ end
60
+
23
61
  it 'removes empty suffixed conditions before building' do
24
62
  expect_any_instance_of(Search).to receive(:build).with({})
25
63
  Search.new(Person, name_eq_any: [''])
@@ -109,6 +147,43 @@ module Ransack
109
147
  expect(s.result.to_sql).to include 'published'
110
148
  end
111
149
 
150
+ # The failure/oversight in Ransack::Nodes::Condition#arel_predicate or deeper is beyond my understanding of the structures
151
+ it 'preserves (inverts) default scope and conditions for negative subqueries' do
152
+ # the positive case (published_articles_title_eq) is
153
+ # SELECT "people".* FROM "people"
154
+ # LEFT OUTER JOIN "articles" ON "articles"."person_id" = "people"."id"
155
+ # AND "articles"."published" = 't'
156
+ # AND ('default_scope' = 'default_scope')
157
+ # WHERE "articles"."title" = 'Test' ORDER BY "people"."id" DESC
158
+ #
159
+ # negative case was
160
+ # SELECT "people".* FROM "people" WHERE "people"."id" NOT IN (
161
+ # SELECT "articles"."person_id" FROM "articles"
162
+ # WHERE "articles"."person_id" = "people"."id"
163
+ # AND NOT ("articles"."title" != 'Test')
164
+ # ) ORDER BY "people"."id" DESC
165
+ #
166
+ # Should have been like
167
+ # SELECT "people".* FROM "people" WHERE "people"."id" NOT IN (
168
+ # SELECT "articles"."person_id" FROM "articles"
169
+ # WHERE "articles"."person_id" = "people"."id"
170
+ # AND "articles"."title" = 'Test' AND "articles"."published" = 't' AND ('default_scope' = 'default_scope')
171
+ # ) ORDER BY "people"."id" DESC
172
+ #
173
+ # With tenanting (eg default_scope with column reference), NOT IN should be like
174
+ # SELECT "people".* FROM "people" WHERE "people"."tenant_id" = 'tenant_id' AND "people"."id" NOT IN (
175
+ # SELECT "articles"."person_id" FROM "articles"
176
+ # WHERE "articles"."person_id" = "people"."id"
177
+ # AND "articles"."tenant_id" = 'tenant_id'
178
+ # AND "articles"."title" = 'Test' AND "articles"."published" = 't' AND ('default_scope' = 'default_scope')
179
+ # ) ORDER BY "people"."id" DESC
180
+
181
+ pending("spec should pass, but I do not know how/where to fix lib code")
182
+ s = Search.new(Person, published_articles_title_not_eq: 'Test')
183
+ expect(s.result.to_sql).to include 'default_scope'
184
+ expect(s.result.to_sql).to include 'published'
185
+ end
186
+
112
187
  it 'discards empty conditions' do
113
188
  s = Search.new(Person, children_name_eq: '')
114
189
  condition = s.base[:children_name_eq]
@@ -189,7 +264,7 @@ module Ransack
189
264
  context 'with an invalid condition' do
190
265
  subject { Search.new(Person, unknown_attr_eq: 'Ernie') }
191
266
 
192
- context 'when ignore_unknown_conditions is false' do
267
+ context 'when ignore_unknown_conditions configuration option is false' do
193
268
  before do
194
269
  Ransack.configure { |c| c.ignore_unknown_conditions = false }
195
270
  end
@@ -197,13 +272,39 @@ module Ransack
197
272
  specify { expect { subject }.to raise_error ArgumentError }
198
273
  end
199
274
 
200
- context 'when ignore_unknown_conditions is true' do
275
+ context 'when ignore_unknown_conditions configuration option is true' do
201
276
  before do
202
277
  Ransack.configure { |c| c.ignore_unknown_conditions = true }
203
278
  end
204
279
 
205
280
  specify { expect { subject }.not_to raise_error }
206
281
  end
282
+
283
+ subject(:with_ignore_unknown_conditions_false) {
284
+ Search.new(Person,
285
+ { unknown_attr_eq: 'Ernie' },
286
+ { ignore_unknown_conditions: false }
287
+ )
288
+ }
289
+
290
+ subject(:with_ignore_unknown_conditions_true) {
291
+ Search.new(Person,
292
+ { unknown_attr_eq: 'Ernie' },
293
+ { ignore_unknown_conditions: true }
294
+ )
295
+ }
296
+
297
+ context 'when ignore_unknown_conditions search parameter is absent' do
298
+ specify { expect { subject }.not_to raise_error }
299
+ end
300
+
301
+ context 'when ignore_unknown_conditions search parameter is false' do
302
+ specify { expect { with_ignore_unknown_conditions_false }.to raise_error ArgumentError }
303
+ end
304
+
305
+ context 'when ignore_unknown_conditions search parameter is true' do
306
+ specify { expect { with_ignore_unknown_conditions_true }.not_to raise_error }
307
+ end
207
308
  end
208
309
 
209
310
  it 'does not modify the parameters' do
@@ -211,6 +312,29 @@ module Ransack
211
312
  expect { Search.new(Person, params) }.not_to change { params }
212
313
  end
213
314
 
315
+ context "ransackable_scope" do
316
+ around(:each) do |example|
317
+ Person.define_singleton_method(:name_eq) do |name|
318
+ self.where(name: name)
319
+ end
320
+
321
+ begin
322
+ example.run
323
+ ensure
324
+ Person.singleton_class.undef_method :name_eq
325
+ end
326
+ end
327
+
328
+ it "is prioritized over base predicates" do
329
+ allow(Person).to receive(:ransackable_scopes)
330
+ .and_return(Person.ransackable_scopes + [:name_eq])
331
+
332
+ s = Search.new(Person, name_eq: "Johny")
333
+ expect(s.instance_variable_get(:@scope_args)["name_eq"]).to eq("Johny")
334
+ expect(s.base[:name_eq]).to be_nil
335
+ end
336
+ end
337
+
214
338
  end
215
339
 
216
340
  describe '#result' do
@@ -220,6 +344,9 @@ module Ransack
220
344
  let(:children_people_name_field) {
221
345
  "#{quote_table_name("children_people")}.#{quote_column_name("name")}"
222
346
  }
347
+ let(:notable_type_field) {
348
+ "#{quote_table_name("notes")}.#{quote_column_name("notable_type")}"
349
+ }
223
350
  it 'evaluates conditions contextually' do
224
351
  s = Search.new(Person, children_name_eq: 'Ernie')
225
352
  expect(s.result).to be_an ActiveRecord::Relation
@@ -234,12 +361,14 @@ module Ransack
234
361
  parent_name_eq: "parent_name_query",
235
362
  parent_articles_title_eq: 'parents_article_title_query'
236
363
  }).result
364
+
237
365
  real_query = remove_quotes_and_backticks(s.to_sql)
238
366
 
239
367
  expect(real_query)
240
- .to match(%r{LEFT OUTER JOIN articles ON (\('default_scope' = 'default_scope'\) AND )?articles.person_id = people.id})
368
+ .to match(%r{LEFT OUTER JOIN articles ON (\('default_scope' = 'default_scope'\) AND )?articles.person_id = people.id})
241
369
  expect(real_query)
242
- .to match(%r{LEFT OUTER JOIN articles articles_people ON (\('default_scope' = 'default_scope'\) AND )?articles_people.person_id = parents_people.id})
370
+ .to match(%r{LEFT OUTER JOIN articles articles_people ON (\('default_scope' = 'default_scope'\) AND )?articles_people.person_id = parents_people.id})
371
+
243
372
  expect(real_query)
244
373
  .to include "people.name = 'person_name_query'"
245
374
  expect(real_query)
@@ -282,6 +411,7 @@ module Ransack
282
411
  s = Search.new(Note, notable_of_Person_type_name_eq: 'Ernie').result
283
412
  expect(s).to be_an ActiveRecord::Relation
284
413
  expect(s.to_sql).to match /#{people_name_field} = 'Ernie'/
414
+ expect(s.to_sql).to match /#{notable_type_field} = 'Person'/
285
415
  end
286
416
 
287
417
  it 'evaluates nested conditions' do
@@ -320,11 +450,8 @@ module Ransack
320
450
  { m: 'or', comments_body_cont: 'e', articles_comments_body_cont: 'e' }
321
451
  ]
322
452
  )
323
- if ActiveRecord::VERSION::MAJOR == 3
324
- all_or_load, uniq_or_distinct = :all, :uniq
325
- else
326
- all_or_load, uniq_or_distinct = :load, :distinct
327
- end
453
+
454
+ all_or_load, uniq_or_distinct = :load, :distinct
328
455
  expect(s.result.send(all_or_load).size)
329
456
  .to eq(9000)
330
457
  expect(s.result(distinct: true).size)
@@ -377,88 +504,169 @@ module Ransack
377
504
  expect(sort.dir).to eq 'asc'
378
505
  end
379
506
 
380
- it 'creates sorts based on multiple attributes/directions in array format' do
381
- @s.sorts = ['id desc', { name: 'name', dir: 'asc' }]
507
+ it 'creates sorts based on a single alias/direction' do
508
+ @s.sorts = 'daddy desc'
509
+ expect(@s.sorts.size).to eq(1)
510
+ sort = @s.sorts.first
511
+ expect(sort).to be_a Nodes::Sort
512
+ expect(sort.name).to eq 'parent_name'
513
+ expect(sort.dir).to eq 'desc'
514
+ end
515
+
516
+ it 'creates sorts based on a single alias and uppercase direction' do
517
+ @s.sorts = 'daddy DESC'
518
+ expect(@s.sorts.size).to eq(1)
519
+ sort = @s.sorts.first
520
+ expect(sort).to be_a Nodes::Sort
521
+ expect(sort.name).to eq 'parent_name'
522
+ expect(sort.dir).to eq 'desc'
523
+ end
524
+
525
+ it 'creates sorts based on a single alias and without direction' do
526
+ @s.sorts = 'daddy'
527
+ expect(@s.sorts.size).to eq(1)
528
+ sort = @s.sorts.first
529
+ expect(sort).to be_a Nodes::Sort
530
+ expect(sort.name).to eq 'parent_name'
531
+ expect(sort.dir).to eq 'asc'
532
+ end
533
+
534
+ it 'creates sorts based on attributes, alias and directions in array format' do
535
+ @s.sorts = ['id desc', { name: 'daddy', dir: 'asc' }]
382
536
  expect(@s.sorts.size).to eq(2)
383
537
  sort1, sort2 = @s.sorts
384
538
  expect(sort1).to be_a Nodes::Sort
385
539
  expect(sort1.name).to eq 'id'
386
540
  expect(sort1.dir).to eq 'desc'
387
541
  expect(sort2).to be_a Nodes::Sort
388
- expect(sort2.name).to eq 'name'
542
+ expect(sort2.name).to eq 'parent_name'
389
543
  expect(sort2.dir).to eq 'asc'
390
544
  end
391
545
 
392
- it 'creates sorts based on multiple attributes and uppercase directions in array format' do
393
- @s.sorts = ['id DESC', { name: 'name', dir: 'ASC' }]
546
+ it 'creates sorts based on attributes, alias and uppercase directions in array format' do
547
+ @s.sorts = ['id DESC', { name: 'daddy', dir: 'ASC' }]
394
548
  expect(@s.sorts.size).to eq(2)
395
549
  sort1, sort2 = @s.sorts
396
550
  expect(sort1).to be_a Nodes::Sort
397
551
  expect(sort1.name).to eq 'id'
398
552
  expect(sort1.dir).to eq 'desc'
399
553
  expect(sort2).to be_a Nodes::Sort
400
- expect(sort2.name).to eq 'name'
554
+ expect(sort2.name).to eq 'parent_name'
401
555
  expect(sort2.dir).to eq 'asc'
402
556
  end
403
557
 
404
- it 'creates sorts based on multiple attributes and different directions
558
+ it 'creates sorts based on attributes, alias and different directions
405
559
  in array format' do
406
- @s.sorts = ['id DESC', { name: 'name', dir: nil }]
560
+ @s.sorts = ['id DESC', { name: 'daddy', dir: nil }]
407
561
  expect(@s.sorts.size).to eq(2)
408
562
  sort1, sort2 = @s.sorts
409
563
  expect(sort1).to be_a Nodes::Sort
410
564
  expect(sort1.name).to eq 'id'
411
565
  expect(sort1.dir).to eq 'desc'
412
566
  expect(sort2).to be_a Nodes::Sort
413
- expect(sort2.name).to eq 'name'
567
+ expect(sort2.name).to eq 'parent_name'
414
568
  expect(sort2.dir).to eq 'asc'
415
569
  end
416
570
 
417
- it 'creates sorts based on multiple attributes/directions in hash format' do
571
+ it 'creates sorts based on attributes, alias and directions in hash format' do
418
572
  @s.sorts = {
419
573
  '0' => { name: 'id', dir: 'desc' },
420
- '1' => { name: 'name', dir: 'asc' }
574
+ '1' => { name: 'daddy', dir: 'asc' }
421
575
  }
422
576
  expect(@s.sorts.size).to eq(2)
423
577
  expect(@s.sorts).to be_all { |s| Nodes::Sort === s }
424
578
  id_sort = @s.sorts.detect { |s| s.name == 'id' }
425
- name_sort = @s.sorts.detect { |s| s.name == 'name' }
579
+ daddy_sort = @s.sorts.detect { |s| s.name == 'parent_name' }
426
580
  expect(id_sort.dir).to eq 'desc'
427
- expect(name_sort.dir).to eq 'asc'
581
+ expect(daddy_sort.dir).to eq 'asc'
428
582
  end
429
583
 
430
- it 'creates sorts based on multiple attributes and uppercase directions
584
+ it 'creates sorts based on attributes, alias and uppercase directions
431
585
  in hash format' do
432
586
  @s.sorts = {
433
587
  '0' => { name: 'id', dir: 'DESC' },
434
- '1' => { name: 'name', dir: 'ASC' }
588
+ '1' => { name: 'daddy', dir: 'ASC' }
435
589
  }
436
590
  expect(@s.sorts.size).to eq(2)
437
591
  expect(@s.sorts).to be_all { |s| Nodes::Sort === s }
438
592
  id_sort = @s.sorts.detect { |s| s.name == 'id' }
439
- name_sort = @s.sorts.detect { |s| s.name == 'name' }
593
+ daddy_sort = @s.sorts.detect { |s| s.name == 'parent_name' }
440
594
  expect(id_sort.dir).to eq 'desc'
441
- expect(name_sort.dir).to eq 'asc'
595
+ expect(daddy_sort.dir).to eq 'asc'
442
596
  end
443
597
 
444
- it 'creates sorts based on multiple attributes and different directions
598
+ it 'creates sorts based on attributes, alias and different directions
445
599
  in hash format' do
446
600
  @s.sorts = {
447
601
  '0' => { name: 'id', dir: 'DESC' },
448
- '1' => { name: 'name', dir: nil }
602
+ '1' => { name: 'daddy', dir: nil }
449
603
  }
450
604
  expect(@s.sorts.size).to eq(2)
451
605
  expect(@s.sorts).to be_all { |s| Nodes::Sort === s }
452
606
  id_sort = @s.sorts.detect { |s| s.name == 'id' }
453
- name_sort = @s.sorts.detect { |s| s.name == 'name' }
607
+ daddy_sort = @s.sorts.detect { |s| s.name == 'parent_name' }
454
608
  expect(id_sort.dir).to eq 'desc'
455
- expect(name_sort.dir).to eq 'asc'
609
+ expect(daddy_sort.dir).to eq 'asc'
456
610
  end
457
611
 
458
612
  it 'overrides existing sort' do
459
613
  @s.sorts = 'id asc'
460
614
  expect(@s.result.first.id).to eq 1
461
615
  end
616
+
617
+ it "PG's sort option", if: ::ActiveRecord::Base.connection.adapter_name == "PostgreSQL" do
618
+ default = Ransack.options.clone
619
+
620
+ s = Search.new(Person, s: 'name asc')
621
+ expect(s.result.to_sql).to eq "SELECT \"people\".* FROM \"people\" ORDER BY \"people\".\"name\" ASC"
622
+
623
+ Ransack.configure { |c| c.postgres_fields_sort_option = :nulls_first }
624
+ s = Search.new(Person, s: 'name asc')
625
+ expect(s.result.to_sql).to eq "SELECT \"people\".* FROM \"people\" ORDER BY \"people\".\"name\" ASC NULLS FIRST"
626
+ s = Search.new(Person, s: 'name desc')
627
+ expect(s.result.to_sql).to eq "SELECT \"people\".* FROM \"people\" ORDER BY \"people\".\"name\" DESC NULLS LAST"
628
+
629
+ Ransack.configure { |c| c.postgres_fields_sort_option = :nulls_last }
630
+ s = Search.new(Person, s: 'name asc')
631
+ expect(s.result.to_sql).to eq "SELECT \"people\".* FROM \"people\" ORDER BY \"people\".\"name\" ASC NULLS LAST"
632
+ s = Search.new(Person, s: 'name desc')
633
+ expect(s.result.to_sql).to eq "SELECT \"people\".* FROM \"people\" ORDER BY \"people\".\"name\" DESC NULLS FIRST"
634
+
635
+ Ransack.options = default
636
+ end
637
+
638
+ it "PG's sort option with double name", if: ::ActiveRecord::Base.connection.adapter_name == "PostgreSQL" do
639
+ default = Ransack.options.clone
640
+
641
+ s = Search.new(Person, s: 'doubled_name asc')
642
+ expect(s.result.to_sql).to eq "SELECT \"people\".* FROM \"people\" ORDER BY \"people\".\"name\" || \"people\".\"name\" ASC"
643
+
644
+ Ransack.configure { |c| c.postgres_fields_sort_option = :nulls_first }
645
+ s = Search.new(Person, s: 'doubled_name asc')
646
+ expect(s.result.to_sql).to eq "SELECT \"people\".* FROM \"people\" ORDER BY \"people\".\"name\" || \"people\".\"name\" ASC NULLS FIRST"
647
+ s = Search.new(Person, s: 'doubled_name desc')
648
+ expect(s.result.to_sql).to eq "SELECT \"people\".* FROM \"people\" ORDER BY \"people\".\"name\" || \"people\".\"name\" DESC NULLS LAST"
649
+
650
+ Ransack.configure { |c| c.postgres_fields_sort_option = :nulls_last }
651
+ s = Search.new(Person, s: 'doubled_name asc')
652
+ expect(s.result.to_sql).to eq "SELECT \"people\".* FROM \"people\" ORDER BY \"people\".\"name\" || \"people\".\"name\" ASC NULLS LAST"
653
+ s = Search.new(Person, s: 'doubled_name desc')
654
+ expect(s.result.to_sql).to eq "SELECT \"people\".* FROM \"people\" ORDER BY \"people\".\"name\" || \"people\".\"name\" DESC NULLS FIRST"
655
+
656
+ Ransack.configure { |c| c.postgres_fields_sort_option = :nulls_always_first }
657
+ s = Search.new(Person, s: 'doubled_name asc')
658
+ expect(s.result.to_sql).to eq "SELECT \"people\".* FROM \"people\" ORDER BY \"people\".\"name\" || \"people\".\"name\" ASC NULLS FIRST"
659
+ s = Search.new(Person, s: 'doubled_name desc')
660
+ expect(s.result.to_sql).to eq "SELECT \"people\".* FROM \"people\" ORDER BY \"people\".\"name\" || \"people\".\"name\" DESC NULLS FIRST"
661
+
662
+ Ransack.configure { |c| c.postgres_fields_sort_option = :nulls_always_last }
663
+ s = Search.new(Person, s: 'doubled_name asc')
664
+ expect(s.result.to_sql).to eq "SELECT \"people\".* FROM \"people\" ORDER BY \"people\".\"name\" || \"people\".\"name\" ASC NULLS LAST"
665
+ s = Search.new(Person, s: 'doubled_name desc')
666
+ expect(s.result.to_sql).to eq "SELECT \"people\".* FROM \"people\" ORDER BY \"people\".\"name\" || \"people\".\"name\" DESC NULLS LAST"
667
+
668
+ Ransack.options = default
669
+ end
462
670
  end
463
671
 
464
672
  describe '#method_missing' do
@@ -8,7 +8,7 @@ module Ransack
8
8
  ar_translation = ::Namespace::Article.human_attribute_name(:title)
9
9
  ransack_translation = Ransack::Translate.attribute(
10
10
  :title,
11
- :context => ::Namespace::Article.ransack.context
11
+ context: ::Namespace::Article.ransack.context
12
12
  )
13
13
  expect(ransack_translation).to eq ar_translation
14
14
  end
data/spec/spec_helper.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require 'machinist/active_record'
2
+ require 'polyamorous/polyamorous'
2
3
  require 'sham'
3
4
  require 'faker'
4
5
  require 'ransack'
@@ -16,16 +17,17 @@ I18n.load_path += Dir[File.join(File.dirname(__FILE__), 'support', '*.yml')]
16
17
  Dir[File.expand_path('../{helpers,support,blueprints}/*.rb', __FILE__)]
17
18
  .each { |f| require f }
18
19
 
20
+ Faker::Config.random = Random.new(0)
19
21
  Sham.define do
20
22
  name { Faker::Name.name }
21
23
  title { Faker::Lorem.sentence }
22
24
  body { Faker::Lorem.paragraph }
23
25
  salary { |index| 30000 + (index * 1000) }
24
- tag_name { Faker::Lorem.words(3).join(' ') }
25
- note { Faker::Lorem.words(7).join(' ') }
26
- only_admin { Faker::Lorem.words(3).join(' ') }
27
- only_search { Faker::Lorem.words(3).join(' ') }
28
- only_sort { Faker::Lorem.words(3).join(' ') }
26
+ tag_name { Faker::Lorem.words(number: 3).join(' ') }
27
+ note { Faker::Lorem.words(number: 7).join(' ') }
28
+ only_admin { Faker::Lorem.words(number: 3).join(' ') }
29
+ only_search { Faker::Lorem.words(number: 3).join(' ') }
30
+ only_sort { Faker::Lorem.words(number: 3).join(' ') }
29
31
  notable_id { |id| id }
30
32
  end
31
33
 
@@ -6,6 +6,8 @@ when 'mysql', 'mysql2'
6
6
  ActiveRecord::Base.establish_connection(
7
7
  adapter: 'mysql2',
8
8
  database: 'ransack',
9
+ username: ENV.fetch("MYSQL_USERNAME") { "root" },
10
+ password: ENV.fetch("MYSQL_PASSWORD") { "" },
9
11
  encoding: 'utf8'
10
12
  )
11
13
  when 'pg', 'postgres', 'postgresql'
@@ -26,7 +28,24 @@ else
26
28
  )
27
29
  end
28
30
 
29
- class Person < ActiveRecord::Base
31
+ # This is just a test app with no sensitive data, so we explicitly allowlist all
32
+ # attributes and associations for search. In general, end users should
33
+ # explicitly authorize each model, but this shows a way to configure the
34
+ # unrestricted default behavior of versions prior to Ransack 4.
35
+ #
36
+ class ApplicationRecord < ActiveRecord::Base
37
+ self.abstract_class = true
38
+
39
+ def self.ransackable_attributes(auth_object = nil)
40
+ authorizable_ransackable_attributes
41
+ end
42
+
43
+ def self.ransackable_associations(auth_object = nil)
44
+ authorizable_ransackable_associations
45
+ end
46
+ end
47
+
48
+ class Person < ApplicationRecord
30
49
  default_scope { order(id: :desc) }
31
50
  belongs_to :parent, class_name: 'Person', foreign_key: :parent_id
32
51
  has_many :children, class_name: 'Person', foreign_key: :parent_id
@@ -85,7 +104,6 @@ class Person < ActiveRecord::Base
85
104
  )
86
105
  end
87
106
 
88
-
89
107
  ransacker :sql_literal_id do
90
108
  Arel.sql('people.id')
91
109
  end
@@ -108,12 +126,11 @@ class Person < ActiveRecord::Base
108
126
  Arel.sql(query)
109
127
  end
110
128
 
111
-
112
129
  def self.ransackable_attributes(auth_object = nil)
113
130
  if auth_object == :admin
114
- super - ['only_sort']
131
+ authorizable_ransackable_attributes - ['only_sort']
115
132
  else
116
- super - ['only_sort', 'only_admin']
133
+ authorizable_ransackable_attributes - ['only_sort', 'only_admin']
117
134
  end
118
135
  end
119
136
 
@@ -129,7 +146,7 @@ end
129
146
  class Musician < Person
130
147
  end
131
148
 
132
- class Article < ActiveRecord::Base
149
+ class Article < ApplicationRecord
133
150
  belongs_to :person
134
151
  has_many :comments
135
152
  has_and_belongs_to_many :tags
@@ -138,12 +155,51 @@ class Article < ActiveRecord::Base
138
155
  alias_attribute :content, :body
139
156
 
140
157
  default_scope { where("'default_scope' = 'default_scope'") }
158
+ scope :latest_comment_cont, lambda { |msg|
159
+ join = <<-SQL
160
+ (LEFT OUTER JOIN (
161
+ SELECT
162
+ comments.*,
163
+ row_number() OVER (PARTITION BY comments.article_id ORDER BY comments.id DESC) AS rownum
164
+ FROM comments
165
+ ) AS latest_comment
166
+ ON latest_comment.article_id = article.id
167
+ AND latest_comment.rownum = 1
168
+ )
169
+ SQL
170
+ .squish
171
+
172
+ joins(join).where("latest_comment.body ILIKE ?", "%#{msg}%")
173
+ }
174
+
175
+ ransacker :title_type, formatter: lambda { |tuples|
176
+ title, type = JSON.parse(tuples)
177
+ Arel::Nodes::Grouping.new(
178
+ [
179
+ Arel::Nodes.build_quoted(title),
180
+ Arel::Nodes.build_quoted(type)
181
+ ]
182
+ )
183
+ } do |_parent|
184
+ articles = Article.arel_table
185
+ Arel::Nodes::Grouping.new(
186
+ %i[title type].map do |field|
187
+ Arel::Nodes::NamedFunction.new(
188
+ 'COALESCE',
189
+ [
190
+ Arel::Nodes::NamedFunction.new('TRIM', [articles[field]]),
191
+ Arel::Nodes.build_quoted('')
192
+ ]
193
+ )
194
+ end
195
+ )
196
+ end
141
197
  end
142
198
 
143
199
  class StoryArticle < Article
144
200
  end
145
201
 
146
- class Recommendation < ActiveRecord::Base
202
+ class Recommendation < ApplicationRecord
147
203
  belongs_to :person
148
204
  belongs_to :target_person, class_name: 'Person'
149
205
  belongs_to :article
@@ -161,19 +217,40 @@ module Namespace
161
217
  end
162
218
  end
163
219
 
164
- class Comment < ActiveRecord::Base
220
+ class Comment < ApplicationRecord
165
221
  belongs_to :article
166
222
  belongs_to :person
223
+
224
+ default_scope { where(disabled: false) }
167
225
  end
168
226
 
169
- class Tag < ActiveRecord::Base
227
+ class Tag < ApplicationRecord
170
228
  has_and_belongs_to_many :articles
171
229
  end
172
230
 
173
- class Note < ActiveRecord::Base
231
+ class Note < ApplicationRecord
174
232
  belongs_to :notable, polymorphic: true
175
233
  end
176
234
 
235
+ class Account < ApplicationRecord
236
+ belongs_to :agent_account, class_name: "Account"
237
+ belongs_to :trade_account, class_name: "Account"
238
+ end
239
+
240
+ class Address < ApplicationRecord
241
+ has_one :organization
242
+ end
243
+
244
+ class Organization < ApplicationRecord
245
+ belongs_to :address
246
+ has_many :employees
247
+ end
248
+
249
+ class Employee < ApplicationRecord
250
+ belongs_to :organization
251
+ has_one :address, through: :organization
252
+ end
253
+
177
254
  module Schema
178
255
  def self.create
179
256
  ActiveRecord::Migration.verbose = false
@@ -209,6 +286,7 @@ module Schema
209
286
  t.integer :article_id
210
287
  t.integer :person_id
211
288
  t.text :body
289
+ t.boolean :disabled, default: false
212
290
  end
213
291
 
214
292
  create_table :tags, force: true do |t|
@@ -231,6 +309,25 @@ module Schema
231
309
  t.integer :target_person_id
232
310
  t.integer :article_id
233
311
  end
312
+
313
+ create_table :accounts, force: true do |t|
314
+ t.belongs_to :agent_account
315
+ t.belongs_to :trade_account
316
+ end
317
+
318
+ create_table :addresses, force: true do |t|
319
+ t.string :city
320
+ end
321
+
322
+ create_table :organizations, force: true do |t|
323
+ t.string :name
324
+ t.integer :address_id
325
+ end
326
+
327
+ create_table :employees, force: true do |t|
328
+ t.string :name
329
+ t.integer :organization_id
330
+ end
234
331
  end
235
332
 
236
333
  10.times do
@@ -256,7 +353,7 @@ module Schema
256
353
  end
257
354
 
258
355
  module SubDB
259
- class Base < ActiveRecord::Base
356
+ class Base < ApplicationRecord
260
357
  self.abstract_class = true
261
358
  establish_connection(
262
359
  adapter: 'sqlite3',