ransack 1.6.6 → 2.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (125) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +3 -0
  3. data/.travis.yml +35 -53
  4. data/CHANGELOG.md +530 -9
  5. data/CONTRIBUTING.md +63 -26
  6. data/Gemfile +24 -24
  7. data/README.md +371 -96
  8. data/Rakefile +6 -29
  9. data/lib/polyamorous/activerecord_5.2_ruby_2/join_association.rb +24 -0
  10. data/lib/polyamorous/activerecord_5.2_ruby_2/join_dependency.rb +79 -0
  11. data/lib/polyamorous/activerecord_5.2_ruby_2/reflection.rb +11 -0
  12. data/lib/polyamorous/activerecord_6.0_ruby_2/join_association.rb +1 -0
  13. data/lib/polyamorous/activerecord_6.0_ruby_2/join_dependency.rb +80 -0
  14. data/lib/polyamorous/activerecord_6.0_ruby_2/reflection.rb +1 -0
  15. data/lib/polyamorous/activerecord_6.1_ruby_2/join_association.rb +74 -0
  16. data/lib/polyamorous/activerecord_6.1_ruby_2/join_dependency.rb +93 -0
  17. data/lib/polyamorous/activerecord_6.1_ruby_2/reflection.rb +1 -0
  18. data/lib/polyamorous/join.rb +70 -0
  19. data/lib/polyamorous/polyamorous.rb +24 -0
  20. data/lib/polyamorous/swapping_reflection_class.rb +11 -0
  21. data/lib/polyamorous/tree_node.rb +7 -0
  22. data/lib/ransack/adapters/active_record/base.rb +23 -2
  23. data/lib/ransack/adapters/active_record/context.rb +206 -139
  24. data/lib/ransack/adapters/active_record/ransack/constants.rb +70 -55
  25. data/lib/ransack/adapters/active_record/ransack/context.rb +10 -18
  26. data/lib/ransack/adapters/active_record/ransack/nodes/condition.rb +43 -36
  27. data/lib/ransack/adapters/active_record/ransack/translate.rb +1 -5
  28. data/lib/ransack/adapters/active_record/ransack/visitor.rb +23 -0
  29. data/lib/ransack/adapters/active_record.rb +11 -10
  30. data/lib/ransack/adapters.rb +64 -0
  31. data/lib/ransack/configuration.rb +91 -4
  32. data/lib/ransack/constants.rb +13 -26
  33. data/lib/ransack/context.rb +45 -40
  34. data/lib/ransack/helpers/form_builder.rb +21 -12
  35. data/lib/ransack/helpers/form_helper.rb +99 -69
  36. data/lib/ransack/locale/ar.yml +70 -0
  37. data/lib/ransack/locale/az.yml +70 -0
  38. data/lib/ransack/locale/bg.yml +70 -0
  39. data/lib/ransack/locale/ca.yml +70 -0
  40. data/lib/ransack/locale/da.yml +70 -0
  41. data/lib/ransack/locale/de.yml +70 -0
  42. data/lib/ransack/locale/el.yml +70 -0
  43. data/lib/ransack/locale/es.yml +22 -22
  44. data/lib/ransack/locale/fa.yml +70 -0
  45. data/lib/ransack/locale/fi.yml +71 -0
  46. data/lib/ransack/locale/id.yml +70 -0
  47. data/lib/ransack/locale/it.yml +70 -0
  48. data/lib/ransack/locale/ja.yml +70 -0
  49. data/lib/ransack/locale/nl.yml +4 -4
  50. data/lib/ransack/locale/pt-BR.yml +70 -0
  51. data/lib/ransack/locale/ru.yml +70 -0
  52. data/lib/ransack/locale/sk.yml +70 -0
  53. data/lib/ransack/locale/tr.yml +70 -0
  54. data/lib/ransack/locale/{zh.yml → zh-CN.yml} +13 -13
  55. data/lib/ransack/locale/zh-TW.yml +70 -0
  56. data/lib/ransack/nodes/attribute.rb +6 -3
  57. data/lib/ransack/nodes/bindable.rb +18 -6
  58. data/lib/ransack/nodes/condition.rb +85 -24
  59. data/lib/ransack/nodes/grouping.rb +17 -11
  60. data/lib/ransack/nodes/sort.rb +9 -5
  61. data/lib/ransack/nodes/value.rb +74 -68
  62. data/lib/ransack/nodes.rb +2 -3
  63. data/lib/ransack/predicate.rb +17 -20
  64. data/lib/ransack/search.rb +15 -15
  65. data/lib/ransack/translate.rb +115 -115
  66. data/lib/ransack/version.rb +1 -1
  67. data/lib/ransack/visitor.rb +1 -12
  68. data/lib/ransack.rb +11 -17
  69. data/logo/ransack-h.png +0 -0
  70. data/logo/ransack-h.svg +34 -0
  71. data/logo/ransack-v.png +0 -0
  72. data/logo/ransack-v.svg +34 -0
  73. data/logo/ransack.png +0 -0
  74. data/logo/ransack.svg +21 -0
  75. data/ransack.gemspec +9 -27
  76. data/spec/console.rb +4 -0
  77. data/spec/helpers/polyamorous_helper.rb +19 -0
  78. data/spec/polyamorous/join_association_spec.rb +35 -0
  79. data/spec/polyamorous/join_dependency_spec.rb +97 -0
  80. data/spec/polyamorous/join_spec.rb +19 -0
  81. data/spec/ransack/adapters/active_record/base_spec.rb +380 -91
  82. data/spec/ransack/adapters/active_record/context_spec.rb +72 -33
  83. data/spec/ransack/configuration_spec.rb +87 -14
  84. data/spec/ransack/helpers/form_builder_spec.rb +2 -11
  85. data/spec/ransack/helpers/form_helper_spec.rb +481 -113
  86. data/spec/ransack/nodes/condition_spec.rb +25 -1
  87. data/spec/ransack/nodes/grouping_spec.rb +62 -6
  88. data/spec/ransack/predicate_spec.rb +79 -5
  89. data/spec/ransack/search_spec.rb +159 -79
  90. data/spec/spec_helper.rb +8 -0
  91. data/spec/support/schema.rb +99 -37
  92. metadata +57 -184
  93. data/lib/ransack/adapters/active_record/3.0/compat.rb +0 -179
  94. data/lib/ransack/adapters/active_record/3.0/context.rb +0 -205
  95. data/lib/ransack/adapters/active_record/3.1/context.rb +0 -219
  96. data/lib/ransack/adapters/active_record/3.2/context.rb +0 -44
  97. data/lib/ransack/adapters/active_record/compat.rb +0 -14
  98. data/lib/ransack/adapters/mongoid/3.2/.gitkeep +0 -0
  99. data/lib/ransack/adapters/mongoid/attributes/attribute.rb +0 -37
  100. data/lib/ransack/adapters/mongoid/attributes/order_predications.rb +0 -17
  101. data/lib/ransack/adapters/mongoid/attributes/predications.rb +0 -141
  102. data/lib/ransack/adapters/mongoid/base.rb +0 -126
  103. data/lib/ransack/adapters/mongoid/context.rb +0 -208
  104. data/lib/ransack/adapters/mongoid/inquiry_hash.rb +0 -23
  105. data/lib/ransack/adapters/mongoid/ransack/constants.rb +0 -88
  106. data/lib/ransack/adapters/mongoid/ransack/context.rb +0 -60
  107. data/lib/ransack/adapters/mongoid/ransack/nodes/condition.rb +0 -27
  108. data/lib/ransack/adapters/mongoid/ransack/translate.rb +0 -13
  109. data/lib/ransack/adapters/mongoid/ransack/visitor.rb +0 -24
  110. data/lib/ransack/adapters/mongoid/table.rb +0 -35
  111. data/lib/ransack/adapters/mongoid.rb +0 -13
  112. data/spec/mongoid/adapters/mongoid/base_spec.rb +0 -276
  113. data/spec/mongoid/adapters/mongoid/context_spec.rb +0 -56
  114. data/spec/mongoid/configuration_spec.rb +0 -102
  115. data/spec/mongoid/dependencies_spec.rb +0 -8
  116. data/spec/mongoid/helpers/ransack_helper.rb +0 -11
  117. data/spec/mongoid/nodes/condition_spec.rb +0 -34
  118. data/spec/mongoid/nodes/grouping_spec.rb +0 -13
  119. data/spec/mongoid/predicate_spec.rb +0 -155
  120. data/spec/mongoid/search_spec.rb +0 -446
  121. data/spec/mongoid/support/mongoid.yml +0 -6
  122. data/spec/mongoid/support/schema.rb +0 -128
  123. data/spec/mongoid/translate_spec.rb +0 -14
  124. data/spec/mongoid_spec_helper.rb +0 -59
  125. data/spec/ransack/dependencies_spec.rb +0 -10
@@ -4,6 +4,21 @@ module Ransack
4
4
  module Nodes
5
5
  describe Condition do
6
6
 
7
+ context 'with an alias' do
8
+ subject {
9
+ Condition.extract(
10
+ Context.for(Person), 'term_start', Person.first(2).map(&:name)
11
+ )
12
+ }
13
+
14
+ specify { expect(subject.combinator).to eq 'or' }
15
+ specify { expect(subject.predicate.name).to eq 'start' }
16
+
17
+ it 'converts the alias to the correct attributes' do
18
+ expect(subject.attributes.map(&:name)).to eq(['name', 'email'])
19
+ end
20
+ end
21
+
7
22
  context 'with multiple values and an _any predicate' do
8
23
  subject {
9
24
  Condition.extract(
@@ -14,6 +29,15 @@ module Ransack
14
29
  specify { expect(subject.values.size).to eq(2) }
15
30
  end
16
31
 
32
+ describe '#negative?' do
33
+ let(:context) { Context.for(Person) }
34
+ let(:eq) { Condition.extract(context, 'name_eq', 'A') }
35
+ let(:not_eq) { Condition.extract(context, 'name_not_eq', 'A') }
36
+
37
+ specify { expect(not_eq.negative?).to be true }
38
+ specify { expect(eq.negative?).to be false }
39
+ end
40
+
17
41
  context 'with an invalid predicate' do
18
42
  subject {
19
43
  Condition.extract(
@@ -34,7 +58,7 @@ module Ransack
34
58
  Ransack.configure { |c| c.ignore_unknown_conditions = true }
35
59
  end
36
60
 
37
- specify { subject.should be_nil }
61
+ specify { expect(subject).to be_nil }
38
62
  end
39
63
  end
40
64
  end
@@ -15,37 +15,93 @@ module Ransack
15
15
  describe '#attribute_method?' do
16
16
  context 'for attributes of the context' do
17
17
  it 'is true' do
18
- expect(subject.attribute_method?('name')).to be_true
18
+ expect(subject.attribute_method?('name')).to be true
19
19
  end
20
20
 
21
21
  context "when the attribute contains '_and_'" do
22
22
  it 'is true' do
23
- expect(subject.attribute_method?('terms_and_conditions')).to be_true
23
+ expect(subject.attribute_method?('terms_and_conditions')).to be true
24
24
  end
25
25
  end
26
26
 
27
27
  context "when the attribute contains '_or_'" do
28
28
  it 'is true' do
29
- expect(subject.attribute_method?('true_or_false')).to be_true
29
+ expect(subject.attribute_method?('true_or_false')).to be true
30
30
  end
31
31
  end
32
32
 
33
33
  context "when the attribute ends with '_start'" do
34
34
  it 'is true' do
35
- expect(subject.attribute_method?('life_start')).to be_true
35
+ expect(subject.attribute_method?('life_start')).to be true
36
36
  end
37
37
  end
38
38
 
39
39
  context "when the attribute ends with '_end'" do
40
40
  it 'is true' do
41
- expect(subject.attribute_method?('stop_end')).to be_true
41
+ expect(subject.attribute_method?('stop_end')).to be true
42
42
  end
43
43
  end
44
44
  end
45
45
 
46
46
  context 'for unknown attributes' do
47
47
  it 'is false' do
48
- expect(subject.attribute_method?('not_an_attribute')).to be_false
48
+ expect(subject.attribute_method?('not_an_attribute')).to be false
49
+ end
50
+ end
51
+ end
52
+
53
+ describe '#conditions=' do
54
+ context 'when conditions are identical' do
55
+ let(:conditions) do
56
+ {
57
+ '0' => {
58
+ 'a' => { '0'=> { 'name' => 'name', 'ransacker_args' => '' } },
59
+ 'p' => 'cont',
60
+ 'v' => { '0' => { 'value' => 'John' } }
61
+ },
62
+ '1' => {
63
+ 'a' => { '0' => { 'name' => 'name', 'ransacker_args' => '' } },
64
+ 'p' => 'cont',
65
+ 'v' => { '0' => { 'value' => 'John' } }
66
+ }
67
+ }
68
+ end
69
+ before { subject.conditions = conditions }
70
+
71
+ it 'expect duplicates to be removed' do
72
+ expect(subject.conditions.count).to eq 1
73
+ end
74
+ end
75
+
76
+ context 'when conditions differ only by ransacker_args' do
77
+ let(:conditions) do
78
+ {
79
+ '0' => {
80
+ 'a' => {
81
+ '0' => {
82
+ 'name' => 'with_arguments',
83
+ 'ransacker_args' => [1,2]
84
+ }
85
+ },
86
+ 'p' => 'eq',
87
+ 'v' => { '0' => { 'value' => '10' } }
88
+ },
89
+ '1' => {
90
+ 'a' => {
91
+ '0' => {
92
+ 'name' => 'with_arguments',
93
+ 'ransacker_args' => [3,4]
94
+ }
95
+ },
96
+ 'p' => 'eq',
97
+ 'v' => { '0' => { 'value' => '10' } }
98
+ }
99
+ }
100
+ end
101
+ before { subject.conditions = conditions }
102
+
103
+ it 'expect them to be parsed as different and not as duplicates' do
104
+ expect(subject.conditions.count).to eq 2
49
105
  end
50
106
  end
51
107
  end
@@ -16,7 +16,7 @@ module Ransack
16
16
  expect { subject.result }.to_not raise_error
17
17
  end
18
18
 
19
- it "escapes '%', '.' and '\\\\' in value" do
19
+ it "escapes '%', '.', '_' and '\\\\' in value" do
20
20
  subject.send(:"#{method}=", '%._\\')
21
21
  expect(subject.result.to_sql).to match(regexp)
22
22
  end
@@ -124,9 +124,9 @@ module Ransack
124
124
  describe 'cont' do
125
125
  it_has_behavior 'wildcard escaping', :name_cont,
126
126
  (if ActiveRecord::Base.connection.adapter_name == "PostgreSQL"
127
- /"people"."name" ILIKE '%\\%\\._\\\\%'/
127
+ /"people"."name" ILIKE '%\\%\\.\\_\\\\%'/
128
128
  elsif ActiveRecord::Base.connection.adapter_name == "Mysql2"
129
- /`people`.`name` LIKE '%\\\\%\\\\._\\\\\\\\%'/
129
+ /`people`.`name` LIKE '%\\\\%.\\\\_\\\\\\\\%'/
130
130
  else
131
131
  /"people"."name" LIKE '%%._\\%'/
132
132
  end) do
@@ -143,9 +143,9 @@ module Ransack
143
143
  describe 'not_cont' do
144
144
  it_has_behavior 'wildcard escaping', :name_not_cont,
145
145
  (if ActiveRecord::Base.connection.adapter_name == "PostgreSQL"
146
- /"people"."name" NOT ILIKE '%\\%\\._\\\\%'/
146
+ /"people"."name" NOT ILIKE '%\\%\\.\\_\\\\%'/
147
147
  elsif ActiveRecord::Base.connection.adapter_name == "Mysql2"
148
- /`people`.`name` NOT LIKE '%\\\\%\\\\._\\\\\\\\%'/
148
+ /`people`.`name` NOT LIKE '%\\\\%.\\\\_\\\\\\\\%'/
149
149
  else
150
150
  /"people"."name" NOT LIKE '%%._\\%'/
151
151
  end) do
@@ -159,6 +159,44 @@ module Ransack
159
159
  end
160
160
  end
161
161
 
162
+ describe 'i_cont' do
163
+ it_has_behavior 'wildcard escaping', :name_i_cont,
164
+ (if ActiveRecord::Base.connection.adapter_name == "PostgreSQL"
165
+ /"people"."name" ILIKE '%\\%\\.\\_\\\\%'/
166
+ elsif ActiveRecord::Base.connection.adapter_name == "Mysql2"
167
+ /LOWER\(`people`.`name`\) LIKE '%\\\\%.\\\\_\\\\\\\\%'/
168
+ else
169
+ /LOWER\("people"."name"\) LIKE '%%._\\%'/
170
+ end) do
171
+ subject { @s }
172
+ end
173
+
174
+ it 'generates a LIKE query with LOWER(column) and value surrounded by %' do
175
+ @s.name_i_cont = 'Ric'
176
+ field = "#{quote_table_name("people")}.#{quote_column_name("name")}"
177
+ expect(@s.result.to_sql).to match /[LOWER\(]?#{field}\)? I?LIKE '%ric%'/
178
+ end
179
+ end
180
+
181
+ describe 'not_i_cont' do
182
+ it_has_behavior 'wildcard escaping', :name_not_i_cont,
183
+ (if ActiveRecord::Base.connection.adapter_name == "PostgreSQL"
184
+ /"people"."name" NOT ILIKE '%\\%\\.\\_\\\\%'/
185
+ elsif ActiveRecord::Base.connection.adapter_name == "Mysql2"
186
+ /LOWER\(`people`.`name`\) NOT LIKE '%\\\\%.\\\\_\\\\\\\\%'/
187
+ else
188
+ /LOWER\("people"."name"\) NOT LIKE '%%._\\%'/
189
+ end) do
190
+ subject { @s }
191
+ end
192
+
193
+ it 'generates a NOT LIKE query with LOWER(column) and value surrounded by %' do
194
+ @s.name_not_i_cont = 'Ric'
195
+ field = "#{quote_table_name("people")}.#{quote_column_name("name")}"
196
+ expect(@s.result.to_sql).to match /[LOWER\(]?#{field}\)? NOT I?LIKE '%ric%'/
197
+ end
198
+ end
199
+
162
200
  describe 'start' do
163
201
  it 'generates a LIKE query with value followed by %' do
164
202
  @s.name_start = 'Er'
@@ -329,6 +367,28 @@ module Ransack
329
367
  field = "#{quote_table_name("people")}.#{quote_column_name("name")}"
330
368
  expect(@s.result.to_sql).to match /#{field} IS NULL/
331
369
  end
370
+
371
+ describe 'with association qeury' do
372
+ it 'generates a value IS NOT NULL query' do
373
+ @s.comments_id_not_null = true
374
+ sql = @s.result.to_sql
375
+ parent_field = "#{quote_table_name("people")}.#{quote_column_name("id")}"
376
+ expect(sql).to match /#{parent_field} IN/
377
+ field = "#{quote_table_name("comments")}.#{quote_column_name("id")}"
378
+ expect(sql).to match /#{field} IS NOT NULL/
379
+ expect(sql).not_to match /AND NOT/
380
+ end
381
+
382
+ it 'generates a value IS NULL query when assigned false' do
383
+ @s.comments_id_not_null = false
384
+ sql = @s.result.to_sql
385
+ parent_field = "#{quote_table_name("people")}.#{quote_column_name("id")}"
386
+ expect(sql).to match /#{parent_field} NOT IN/
387
+ field = "#{quote_table_name("comments")}.#{quote_column_name("id")}"
388
+ expect(sql).to match /#{field} IS NULL/
389
+ expect(sql).to match /AND NOT/
390
+ end
391
+ end
332
392
  end
333
393
 
334
394
  describe 'present' do
@@ -359,6 +419,20 @@ module Ransack
359
419
  end
360
420
  end
361
421
 
422
+ context "defining custom predicates" do
423
+ describe "with 'not_in' arel predicate" do
424
+ before do
425
+ Ransack.configure {|c| c.add_predicate "not_in_csv", arel_predicate: "not_in", formatter: proc { |v| v.split(",") } }
426
+ end
427
+
428
+ it 'generates a value IS NOT NULL query' do
429
+ @s.name_not_in_csv = ["a", "b"]
430
+ field = "#{quote_table_name("people")}.#{quote_column_name("name")}"
431
+ expect(@s.result.to_sql).to match /#{field} NOT IN \('a', 'b'\)/
432
+ end
433
+ end
434
+ end
435
+
362
436
  private
363
437
 
364
438
  def test_boolean_equality_for(boolean_value)
@@ -20,6 +20,12 @@ module Ransack
20
20
  Search.new(Person, name_eq: 'foobar')
21
21
  end
22
22
 
23
+ it 'strip leading & trailing whitespace before building' do
24
+ expect_any_instance_of(Search).to receive(:build)
25
+ .with({ 'name_eq' => 'foobar' })
26
+ Search.new(Person, name_eq: ' foobar ')
27
+ end
28
+
23
29
  it 'removes empty suffixed conditions before building' do
24
30
  expect_any_instance_of(Search).to receive(:build).with({})
25
31
  Search.new(Person, name_eq_any: [''])
@@ -43,16 +49,16 @@ module Ransack
43
49
 
44
50
  it 'accepts a context option' do
45
51
  shared_context = Context.for(Person)
46
- search1 = Search.new(Person, { name_eq: 'A' }, context: shared_context)
47
- search2 = Search.new(Person, { name_eq: 'B' }, context: shared_context)
48
- expect(search1.context).to be search2.context
52
+ s1 = Search.new(Person, { name_eq: 'A' }, context: shared_context)
53
+ s2 = Search.new(Person, { name_eq: 'B' }, context: shared_context)
54
+ expect(s1.context).to be s2.context
49
55
  end
50
56
  end
51
57
 
52
58
  describe '#build' do
53
59
  it 'creates conditions for top-level attributes' do
54
- search = Search.new(Person, name_eq: 'Ernie')
55
- condition = search.base[:name_eq]
60
+ s = Search.new(Person, name_eq: 'Ernie')
61
+ condition = s.base[:name_eq]
56
62
  expect(condition).to be_a Nodes::Condition
57
63
  expect(condition.predicate.name).to eq 'eq'
58
64
  expect(condition.attributes.first.name).to eq 'name'
@@ -60,8 +66,8 @@ module Ransack
60
66
  end
61
67
 
62
68
  it 'creates conditions for association attributes' do
63
- search = Search.new(Person, children_name_eq: 'Ernie')
64
- condition = search.base[:children_name_eq]
69
+ s = Search.new(Person, children_name_eq: 'Ernie')
70
+ condition = s.base[:children_name_eq]
65
71
  expect(condition).to be_a Nodes::Condition
66
72
  expect(condition.predicate.name).to eq 'eq'
67
73
  expect(condition.attributes.first.name).to eq 'children_name'
@@ -69,8 +75,8 @@ module Ransack
69
75
  end
70
76
 
71
77
  it 'creates conditions for polymorphic belongs_to association attributes' do
72
- search = Search.new(Note, notable_of_Person_type_name_eq: 'Ernie')
73
- condition = search.base[:notable_of_Person_type_name_eq]
78
+ s = Search.new(Note, notable_of_Person_type_name_eq: 'Ernie')
79
+ condition = s.base[:notable_of_Person_type_name_eq]
74
80
  expect(condition).to be_a Nodes::Condition
75
81
  expect(condition.predicate.name).to eq 'eq'
76
82
  expect(condition.attributes.first.name)
@@ -80,9 +86,9 @@ module Ransack
80
86
 
81
87
  it 'creates conditions for multiple polymorphic belongs_to association
82
88
  attributes' do
83
- search = Search.new(Note,
89
+ s = Search.new(Note,
84
90
  notable_of_Person_type_name_or_notable_of_Article_type_title_eq: 'Ernie')
85
- condition = search.
91
+ condition = s.
86
92
  base[:notable_of_Person_type_name_or_notable_of_Article_type_title_eq]
87
93
  expect(condition).to be_a Nodes::Condition
88
94
  expect(condition.predicate.name).to eq 'eq'
@@ -93,14 +99,62 @@ module Ransack
93
99
  expect(condition.value).to eq 'Ernie'
94
100
  end
95
101
 
96
- it 'preserves default scope conditions for associations' do
97
- search = Search.new(Person, articles_title_eq: 'Test')
98
- expect(search.result.to_sql).to include 'default_scope'
102
+ it 'creates conditions for aliased attributes',
103
+ if: Ransack::SUPPORTS_ATTRIBUTE_ALIAS do
104
+ s = Search.new(Person, full_name_eq: 'Ernie')
105
+ condition = s.base[:full_name_eq]
106
+ expect(condition).to be_a Nodes::Condition
107
+ expect(condition.predicate.name).to eq 'eq'
108
+ expect(condition.attributes.first.name).to eq 'full_name'
109
+ expect(condition.value).to eq 'Ernie'
110
+ end
111
+
112
+ it 'preserves default scope and conditions for associations' do
113
+ s = Search.new(Person, published_articles_title_eq: 'Test')
114
+ expect(s.result.to_sql).to include 'default_scope'
115
+ expect(s.result.to_sql).to include 'published'
116
+ end
117
+
118
+ # The failure/oversight in Ransack::Nodes::Condition#arel_predicate or deeper is beyond my understanding of the structures
119
+ it 'preserves (inverts) default scope and conditions for negative subqueries' do
120
+ # the positive case (published_articles_title_eq) is
121
+ # SELECT "people".* FROM "people"
122
+ # LEFT OUTER JOIN "articles" ON "articles"."person_id" = "people"."id"
123
+ # AND "articles"."published" = 't'
124
+ # AND ('default_scope' = 'default_scope')
125
+ # WHERE "articles"."title" = 'Test' ORDER BY "people"."id" DESC
126
+ #
127
+ # negative case was
128
+ # SELECT "people".* FROM "people" WHERE "people"."id" NOT IN (
129
+ # SELECT "articles"."person_id" FROM "articles"
130
+ # WHERE "articles"."person_id" = "people"."id"
131
+ # AND NOT ("articles"."title" != 'Test')
132
+ # ) ORDER BY "people"."id" DESC
133
+ #
134
+ # Should have been like
135
+ # SELECT "people".* FROM "people" WHERE "people"."id" NOT IN (
136
+ # SELECT "articles"."person_id" FROM "articles"
137
+ # WHERE "articles"."person_id" = "people"."id"
138
+ # AND "articles"."title" = 'Test' AND "articles"."published" = 't' AND ('default_scope' = 'default_scope')
139
+ # ) ORDER BY "people"."id" DESC
140
+ #
141
+ # With tenanting (eg default_scope with column reference), NOT IN should be like
142
+ # SELECT "people".* FROM "people" WHERE "people"."tenant_id" = 'tenant_id' AND "people"."id" NOT IN (
143
+ # SELECT "articles"."person_id" FROM "articles"
144
+ # WHERE "articles"."person_id" = "people"."id"
145
+ # AND "articles"."tenant_id" = 'tenant_id'
146
+ # AND "articles"."title" = 'Test' AND "articles"."published" = 't' AND ('default_scope' = 'default_scope')
147
+ # ) ORDER BY "people"."id" DESC
148
+
149
+ pending("spec should pass, but I do not know how/where to fix lib code")
150
+ s = Search.new(Person, published_articles_title_not_eq: 'Test')
151
+ expect(s.result.to_sql).to include 'default_scope'
152
+ expect(s.result.to_sql).to include 'published'
99
153
  end
100
154
 
101
155
  it 'discards empty conditions' do
102
- search = Search.new(Person, children_name_eq: '')
103
- condition = search.base[:children_name_eq]
156
+ s = Search.new(Person, children_name_eq: '')
157
+ condition = s.base[:children_name_eq]
104
158
  expect(condition).to be_nil
105
159
  end
106
160
 
@@ -110,13 +164,13 @@ module Ransack
110
164
  end
111
165
 
112
166
  it 'accepts arrays of groupings' do
113
- search = Search.new(Person,
167
+ s = Search.new(Person,
114
168
  g: [
115
169
  { m: 'or', name_eq: 'Ernie', children_name_eq: 'Ernie' },
116
170
  { m: 'or', name_eq: 'Bert', children_name_eq: 'Bert' },
117
171
  ]
118
172
  )
119
- ors = search.groupings
173
+ ors = s.groupings
120
174
  expect(ors.size).to eq(2)
121
175
  or1, or2 = ors
122
176
  expect(or1).to be_a Nodes::Grouping
@@ -125,14 +179,14 @@ module Ransack
125
179
  expect(or2.combinator).to eq 'or'
126
180
  end
127
181
 
128
- it 'accepts "attributes" hashes for groupings' do
129
- search = Search.new(Person,
182
+ it 'accepts attributes hashes for groupings' do
183
+ s = Search.new(Person,
130
184
  g: {
131
185
  '0' => { m: 'or', name_eq: 'Ernie', children_name_eq: 'Ernie' },
132
186
  '1' => { m: 'or', name_eq: 'Bert', children_name_eq: 'Bert' },
133
187
  }
134
188
  )
135
- ors = search.groupings
189
+ ors = s.groupings
136
190
  expect(ors.size).to eq(2)
137
191
  or1, or2 = ors
138
192
  expect(or1).to be_a Nodes::Grouping
@@ -141,8 +195,8 @@ module Ransack
141
195
  expect(or2.combinator).to eq 'or'
142
196
  end
143
197
 
144
- it 'accepts "attributes" hashes for conditions' do
145
- search = Search.new(Person,
198
+ it 'accepts attributes hashes for conditions' do
199
+ s = Search.new(Person,
146
200
  c: {
147
201
  '0' => { a: ['name'], p: 'eq', v: ['Ernie'] },
148
202
  '1' => {
@@ -151,7 +205,7 @@ module Ransack
151
205
  }
152
206
  }
153
207
  )
154
- conditions = search.base.conditions
208
+ conditions = s.base.conditions
155
209
  expect(conditions.size).to eq(2)
156
210
  expect(conditions.map { |c| c.class })
157
211
  .to eq [Nodes::Condition, Nodes::Condition]
@@ -162,8 +216,8 @@ module Ransack
162
216
  config.add_predicate 'ary_pred', wants_array: true
163
217
  end
164
218
 
165
- search = Search.new(Person, name_ary_pred: ['Ernie', 'Bert'])
166
- condition = search.base[:name_ary_pred]
219
+ s = Search.new(Person, name_ary_pred: ['Ernie', 'Bert'])
220
+ condition = s.base[:name_ary_pred]
167
221
  expect(condition).to be_a Nodes::Condition
168
222
  expect(condition.predicate.name).to eq 'ary_pred'
169
223
  expect(condition.attributes.first.name).to eq 'name'
@@ -171,8 +225,8 @@ module Ransack
171
225
  end
172
226
 
173
227
  it 'does not evaluate the query on #inspect' do
174
- search = Search.new(Person, children_id_in: [1, 2, 3])
175
- expect(search.inspect).not_to match /ActiveRecord/
228
+ s = Search.new(Person, children_id_in: [1, 2, 3])
229
+ expect(s.inspect).not_to match /ActiveRecord/
176
230
  end
177
231
 
178
232
  context 'with an invalid condition' do
@@ -209,62 +263,86 @@ module Ransack
209
263
  let(:children_people_name_field) {
210
264
  "#{quote_table_name("children_people")}.#{quote_column_name("name")}"
211
265
  }
266
+ let(:notable_type_field) {
267
+ "#{quote_table_name("notes")}.#{quote_column_name("notable_type")}"
268
+ }
212
269
  it 'evaluates conditions contextually' do
213
- search = Search.new(Person, children_name_eq: 'Ernie')
214
- expect(search.result).to be_an ActiveRecord::Relation
215
- expect(search.result.to_sql).to match /#{
270
+ s = Search.new(Person, children_name_eq: 'Ernie')
271
+ expect(s.result).to be_an ActiveRecord::Relation
272
+ expect(s.result.to_sql).to match /#{
216
273
  children_people_name_field} = 'Ernie'/
217
274
  end
218
275
 
219
- # FIXME: Make this spec pass for Rails 4.1 / 4.2 / 5.0 and not just 4.0 by
220
- # commenting out lines 221 and 242 to run the test. Addresses issue #374.
221
- # https://github.com/activerecord-hackery/ransack/issues/374
222
- #
223
- if ::ActiveRecord::VERSION::STRING.first(3) == '4.0'
224
- it 'evaluates conditions for multiple belongs_to associations to the
225
- same table contextually' do
226
- s = Search.new(Recommendation,
227
- person_name_eq: 'Ernie',
228
- target_person_parent_name_eq: 'Test'
229
- ).result
230
- expect(s).to be_an ActiveRecord::Relation
231
- real_query = remove_quotes_and_backticks(s.to_sql)
232
- expected_query = <<-SQL
233
- SELECT recommendations.* FROM recommendations
234
- LEFT OUTER JOIN people ON people.id = recommendations.person_id
235
- LEFT OUTER JOIN people target_people_recommendations
236
- ON target_people_recommendations.id = recommendations.target_person_id
237
- LEFT OUTER JOIN people parents_people
238
- ON parents_people.id = target_people_recommendations.parent_id
239
- WHERE ((people.name = 'Ernie' AND parents_people.name = 'Test'))
240
- SQL
241
- .squish
242
- expect(real_query).to eq expected_query
243
- end
276
+ it 'use appropriate table alias' do
277
+ skip "Rails 6 regressed here, but it's fixed in 6-0-stable since https://github.com/rails/rails/commit/f9ba52477ca288e7effa5f6794ae3df3f4e982bc" if ENV["RAILS"] == "v6.0.3"
278
+
279
+ s = Search.new(Person, {
280
+ name_eq: "person_name_query",
281
+ articles_title_eq: "person_article_title_query",
282
+ parent_name_eq: "parent_name_query",
283
+ parent_articles_title_eq: 'parents_article_title_query'
284
+ }).result
285
+
286
+ real_query = remove_quotes_and_backticks(s.to_sql)
287
+
288
+ expect(real_query)
289
+ .to match(%r{LEFT OUTER JOIN articles ON (\('default_scope' = 'default_scope'\) AND )?articles.person_id = people.id})
290
+ expect(real_query)
291
+ .to match(%r{LEFT OUTER JOIN articles articles_people ON (\('default_scope' = 'default_scope'\) AND )?articles_people.person_id = parents_people.id})
292
+
293
+ expect(real_query)
294
+ .to include "people.name = 'person_name_query'"
295
+ expect(real_query)
296
+ .to include "articles.title = 'person_article_title_query'"
297
+ expect(real_query)
298
+ .to include "parents_people.name = 'parent_name_query'"
299
+ expect(real_query)
300
+ .to include "articles_people.title = 'parents_article_title_query'"
301
+ end
302
+
303
+ it 'evaluates conditions for multiple `belongs_to` associations to the same table contextually' do
304
+ s = Search.new(
305
+ Recommendation,
306
+ person_name_eq: 'Ernie',
307
+ target_person_parent_name_eq: 'Test'
308
+ ).result
309
+ expect(s).to be_an ActiveRecord::Relation
310
+ real_query = remove_quotes_and_backticks(s.to_sql)
311
+ expected_query = <<-SQL
312
+ SELECT recommendations.* FROM recommendations
313
+ LEFT OUTER JOIN people ON people.id = recommendations.person_id
314
+ LEFT OUTER JOIN people target_people_recommendations
315
+ ON target_people_recommendations.id = recommendations.target_person_id
316
+ LEFT OUTER JOIN people parents_people
317
+ ON parents_people.id = target_people_recommendations.parent_id
318
+ WHERE (people.name = 'Ernie' AND parents_people.name = 'Test')
319
+ SQL
320
+ .squish
321
+ expect(real_query).to eq expected_query
244
322
  end
245
323
 
246
324
  it 'evaluates compound conditions contextually' do
247
- search = Search.new(Person, children_name_or_name_eq: 'Ernie').result
248
- expect(search).to be_an ActiveRecord::Relation
249
- expect(search.to_sql).to match /#{children_people_name_field
325
+ s = Search.new(Person, children_name_or_name_eq: 'Ernie').result
326
+ expect(s).to be_an ActiveRecord::Relation
327
+ expect(s.to_sql).to match /#{children_people_name_field
250
328
  } = 'Ernie' OR #{people_name_field} = 'Ernie'/
251
329
  end
252
330
 
253
331
  it 'evaluates polymorphic belongs_to association conditions contextually' do
254
- search = Search.new(Note, notable_of_Person_type_name_eq: 'Ernie')
255
- .result
256
- expect(search).to be_an ActiveRecord::Relation
257
- expect(search.to_sql).to match /#{people_name_field} = 'Ernie'/
332
+ s = Search.new(Note, notable_of_Person_type_name_eq: 'Ernie').result
333
+ expect(s).to be_an ActiveRecord::Relation
334
+ expect(s.to_sql).to match /#{people_name_field} = 'Ernie'/
335
+ expect(s.to_sql).to match /#{notable_type_field} = 'Person'/
258
336
  end
259
337
 
260
338
  it 'evaluates nested conditions' do
261
- search = Search.new(Person, children_name_eq: 'Ernie',
339
+ s = Search.new(Person, children_name_eq: 'Ernie',
262
340
  g: [
263
341
  { m: 'or', name_eq: 'Ernie', children_children_name_eq: 'Ernie' }
264
342
  ]
265
343
  ).result
266
- expect(search).to be_an ActiveRecord::Relation
267
- first, last = search.to_sql.split(/ AND /)
344
+ expect(s).to be_an ActiveRecord::Relation
345
+ first, last = s.to_sql.split(/ AND /)
268
346
  expect(first).to match /#{children_people_name_field} = 'Ernie'/
269
347
  expect(last).to match /#{
270
348
  people_name_field} = 'Ernie' OR #{
@@ -273,14 +351,14 @@ module Ransack
273
351
  end
274
352
 
275
353
  it 'evaluates arrays of groupings' do
276
- search = Search.new(Person,
354
+ s = Search.new(Person,
277
355
  g: [
278
356
  { m: 'or', name_eq: 'Ernie', children_name_eq: 'Ernie' },
279
357
  { m: 'or', name_eq: 'Bert', children_name_eq: 'Bert' }
280
358
  ]
281
359
  ).result
282
- expect(search).to be_an ActiveRecord::Relation
283
- first, last = search.to_sql.split(/ AND /)
360
+ expect(s).to be_an ActiveRecord::Relation
361
+ first, last = s.to_sql.split(/ AND /)
284
362
  expect(first).to match /#{people_name_field} = 'Ernie' OR #{
285
363
  children_people_name_field} = 'Ernie'/
286
364
  expect(last).to match /#{people_name_field} = 'Bert' OR #{
@@ -288,22 +366,24 @@ module Ransack
288
366
  end
289
367
 
290
368
  it 'returns distinct records when passed distinct: true' do
291
- search = Search.new(Person,
369
+ s = Search.new(Person,
292
370
  g: [
293
371
  { m: 'or', comments_body_cont: 'e', articles_comments_body_cont: 'e' }
294
372
  ]
295
373
  )
296
- if ActiveRecord::VERSION::MAJOR == 3
297
- all_or_load, uniq_or_distinct = :all, :uniq
298
- else
299
- all_or_load, uniq_or_distinct = :load, :distinct
300
- end
301
- expect(search.result.send(all_or_load).size)
374
+
375
+ all_or_load, uniq_or_distinct = :load, :distinct
376
+ expect(s.result.send(all_or_load).size)
302
377
  .to eq(9000)
303
- expect(search.result(distinct: true).size)
378
+ expect(s.result(distinct: true).size)
304
379
  .to eq(10)
305
- expect(search.result.send(all_or_load).send(uniq_or_distinct))
306
- .to eq search.result(distinct: true).send(all_or_load)
380
+ expect(s.result.send(all_or_load).send(uniq_or_distinct))
381
+ .to eq s.result(distinct: true).send(all_or_load)
382
+ end
383
+
384
+ it 'evaluates joins with belongs_to join' do
385
+ s = Person.joins(:parent).ransack(parent_name_eq: 'Ernie').result(distinct: true)
386
+ expect(s).to be_an ActiveRecord::Relation
307
387
  end
308
388
 
309
389
  private