ransack 2.3.2 → 4.1.1

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