ransack 4.2.1 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -3
  3. data/lib/polyamorous/polyamorous.rb +2 -2
  4. data/lib/ransack/adapters/active_record/context.rb +30 -3
  5. data/lib/ransack/constants.rb +1 -1
  6. data/lib/ransack/context.rb +3 -0
  7. data/lib/ransack/helpers/form_builder.rb +6 -7
  8. data/lib/ransack/helpers/form_helper.rb +86 -20
  9. data/lib/ransack/invalid_search_error.rb +3 -0
  10. data/lib/ransack/locale/ja.yml +51 -51
  11. data/lib/ransack/locale/ko.yml +70 -0
  12. data/lib/ransack/locale/uk.yml +72 -0
  13. data/lib/ransack/nodes/condition.rb +39 -7
  14. data/lib/ransack/nodes/grouping.rb +1 -1
  15. data/lib/ransack/nodes/sort.rb +1 -1
  16. data/lib/ransack/nodes/value.rb +9 -1
  17. data/lib/ransack/search.rb +4 -3
  18. data/lib/ransack/version.rb +1 -1
  19. data/lib/ransack.rb +8 -0
  20. data/spec/polyamorous/join_association_spec.rb +0 -1
  21. data/spec/polyamorous/join_dependency_spec.rb +0 -1
  22. data/spec/ransack/adapters/active_record/base_spec.rb +106 -3
  23. data/spec/ransack/adapters/active_record/context_spec.rb +72 -0
  24. data/spec/ransack/helpers/form_builder_spec.rb +0 -2
  25. data/spec/ransack/helpers/form_helper_spec.rb +219 -5
  26. data/spec/ransack/nodes/condition_spec.rb +230 -0
  27. data/spec/ransack/nodes/grouping_spec.rb +2 -2
  28. data/spec/ransack/nodes/value_spec.rb +12 -1
  29. data/spec/ransack/predicate_spec.rb +16 -9
  30. data/spec/ransack/search_spec.rb +121 -1
  31. data/spec/ransack/translate_spec.rb +0 -1
  32. data/spec/spec_helper.rb +2 -3
  33. data/spec/support/schema.rb +42 -0
  34. metadata +43 -87
  35. data/.github/FUNDING.yml +0 -3
  36. data/.github/SECURITY.md +0 -12
  37. data/.github/workflows/codeql.yml +0 -72
  38. data/.github/workflows/cronjob.yml +0 -99
  39. data/.github/workflows/deploy.yml +0 -35
  40. data/.github/workflows/rubocop.yml +0 -20
  41. data/.github/workflows/test-deploy.yml +0 -29
  42. data/.github/workflows/test.yml +0 -131
  43. data/.gitignore +0 -7
  44. data/.nojekyll +0 -0
  45. data/.rubocop.yml +0 -50
  46. data/CHANGELOG.md +0 -1186
  47. data/CONTRIBUTING.md +0 -171
  48. data/Gemfile +0 -53
  49. data/Rakefile +0 -24
  50. data/bug_report_templates/test-ransack-scope-and-column-same-name.rb +0 -78
  51. data/bug_report_templates/test-ransacker-arel-present-predicate.rb +0 -75
  52. data/docs/.gitignore +0 -19
  53. data/docs/.nojekyll +0 -0
  54. data/docs/babel.config.js +0 -3
  55. data/docs/blog/2022-03-27-ransack-3.0.0.md +0 -20
  56. data/docs/docs/getting-started/_category_.json +0 -4
  57. data/docs/docs/getting-started/advanced-mode.md +0 -46
  58. data/docs/docs/getting-started/configuration.md +0 -47
  59. data/docs/docs/getting-started/search-matches.md +0 -67
  60. data/docs/docs/getting-started/simple-mode.md +0 -288
  61. data/docs/docs/getting-started/sorting.md +0 -71
  62. data/docs/docs/getting-started/using-predicates.md +0 -282
  63. data/docs/docs/going-further/_category_.json +0 -4
  64. data/docs/docs/going-further/acts-as-taggable-on.md +0 -114
  65. data/docs/docs/going-further/associations.md +0 -70
  66. data/docs/docs/going-further/custom-predicates.md +0 -52
  67. data/docs/docs/going-further/documentation.md +0 -43
  68. data/docs/docs/going-further/exporting-to-csv.md +0 -49
  69. data/docs/docs/going-further/external-guides.md +0 -57
  70. data/docs/docs/going-further/form-customisation.md +0 -63
  71. data/docs/docs/going-further/i18n.md +0 -53
  72. data/docs/docs/going-further/img/create_release.png +0 -0
  73. data/docs/docs/going-further/merging-searches.md +0 -41
  74. data/docs/docs/going-further/other-notes.md +0 -428
  75. data/docs/docs/going-further/polymorphic-search.md +0 -46
  76. data/docs/docs/going-further/ransackers.md +0 -331
  77. data/docs/docs/going-further/release_process.md +0 -36
  78. data/docs/docs/going-further/saving-queries.md +0 -82
  79. data/docs/docs/going-further/searching-postgres.md +0 -57
  80. data/docs/docs/going-further/wiki-contributors.md +0 -82
  81. data/docs/docs/intro.md +0 -99
  82. data/docs/docusaurus.config.js +0 -120
  83. data/docs/package.json +0 -42
  84. data/docs/sidebars.js +0 -31
  85. data/docs/src/components/HomepageFeatures/index.js +0 -64
  86. data/docs/src/components/HomepageFeatures/styles.module.css +0 -11
  87. data/docs/src/css/custom.css +0 -39
  88. data/docs/src/pages/index.module.css +0 -23
  89. data/docs/src/pages/markdown-page.md +0 -7
  90. data/docs/static/.nojekyll +0 -0
  91. data/docs/static/img/docusaurus.png +0 -0
  92. data/docs/static/img/favicon.ico +0 -0
  93. data/docs/static/img/logo.svg +0 -1
  94. data/docs/static/img/tutorial/docsVersionDropdown.png +0 -0
  95. data/docs/static/img/tutorial/localeDropdown.png +0 -0
  96. data/docs/static/img/undraw_docusaurus_mountain.svg +0 -171
  97. data/docs/static/img/undraw_docusaurus_react.svg +0 -170
  98. data/docs/static/img/undraw_docusaurus_tree.svg +0 -40
  99. data/docs/static/logo/ransack-h.png +0 -0
  100. data/docs/static/logo/ransack-h.svg +0 -34
  101. data/docs/static/logo/ransack-v.png +0 -0
  102. data/docs/static/logo/ransack-v.svg +0 -34
  103. data/docs/static/logo/ransack.png +0 -0
  104. data/docs/static/logo/ransack.svg +0 -21
  105. data/docs/yarn.lock +0 -8884
  106. data/ransack.gemspec +0 -26
@@ -64,6 +64,7 @@ module Ransack
64
64
  end
65
65
 
66
66
  specify { expect { subject }.to raise_error ArgumentError }
67
+ specify { expect { subject }.to raise_error InvalidSearchError }
67
68
  end
68
69
 
69
70
  context "when ignore_unknown_conditions is true" do
@@ -98,6 +99,235 @@ module Ransack
98
99
  specify { expect(subject).to eq Condition.extract(Context.for(Person), 'full_name_eq', Person.first.name) }
99
100
  end
100
101
  end
102
+
103
+ context 'with wildcard string values' do
104
+ it 'properly quotes values with wildcards for LIKE predicates' do
105
+ ransack_hash = { name_cont: 'test%' }
106
+ sql = Person.ransack(ransack_hash).result.to_sql
107
+
108
+ # The % should be properly quoted in the SQL
109
+ case ActiveRecord::Base.connection.adapter_name
110
+ when "Mysql2"
111
+ expect(sql).to include("LIKE '%test\\\\%%'")
112
+ expect(sql).not_to include("NOT LIKE '%test\\\\%%'")
113
+ when "PostGIS", "PostgreSQL"
114
+ expect(sql).to include("ILIKE '%test\\%%'")
115
+ expect(sql).not_to include("NOT ILIKE '%test\\%%'")
116
+ else
117
+ expect(sql).to include("LIKE '%test%%'")
118
+ expect(sql).not_to include("NOT LIKE '%test%%'")
119
+ end
120
+ end
121
+
122
+ it 'properly quotes values with wildcards for NOT LIKE predicates' do
123
+ ransack_hash = { name_not_cont: 'test%' }
124
+ sql = Person.ransack(ransack_hash).result.to_sql
125
+
126
+ # The % should be properly quoted in the SQL
127
+ case ActiveRecord::Base.connection.adapter_name
128
+ when "Mysql2"
129
+ expect(sql).to include("NOT LIKE '%test\\\\%%'")
130
+ when "PostGIS", "PostgreSQL"
131
+ expect(sql).to include("NOT ILIKE '%test\\%%'")
132
+ else
133
+ expect(sql).to include("NOT LIKE '%test%%'")
134
+ end
135
+ end
136
+ end
137
+
138
+ context 'with negative conditions on associations' do
139
+ it 'handles not_null predicate with true value correctly' do
140
+ ransack_hash = { comments_id_not_null: true }
141
+ sql = Person.ransack(ransack_hash).result.to_sql
142
+
143
+ # Should generate an IN query with IS NOT NULL condition
144
+ expect(sql).to include('IN (')
145
+ expect(sql).to include('IS NOT NULL')
146
+ expect(sql).not_to include('IS NULL')
147
+ end
148
+
149
+ it 'handles not_null predicate with false value correctly' do
150
+ ransack_hash = { comments_id_not_null: false }
151
+ sql = Person.ransack(ransack_hash).result.to_sql
152
+
153
+ # Should generate a NOT IN query with IS NULL condition
154
+ expect(sql).to include('NOT IN (')
155
+ expect(sql).to include('IS NULL')
156
+ expect(sql).not_to include('IS NOT NULL')
157
+ end
158
+
159
+ it 'handles not_cont predicate correctly' do
160
+ ransack_hash = { comments_body_not_cont: 'test' }
161
+ sql = Person.ransack(ransack_hash).result.to_sql
162
+
163
+ # Should generate a NOT IN query with LIKE condition (not NOT LIKE)
164
+ expect(sql).to include('NOT IN (')
165
+ expect(sql).to include("LIKE '%test%'")
166
+ expect(sql).not_to include("NOT LIKE '%test%'")
167
+ end
168
+ end
169
+
170
+ context 'with nested conditions' do
171
+ it 'correctly identifies non-nested conditions' do
172
+ condition = Condition.extract(
173
+ Context.for(Person), 'name_eq', 'Test'
174
+ )
175
+
176
+ # Create a mock parent table
177
+ parent_table = Person.arel_table
178
+
179
+ # Get the attribute name and make sure it starts with the table name
180
+ attribute = condition.attributes.first
181
+ expect(attribute.name).to eq('name')
182
+ expect(parent_table.name).to eq('people')
183
+
184
+ # The method should return false because 'name' doesn't start with 'people'
185
+ result = condition.send(:not_nested_condition, attribute, parent_table)
186
+ expect(result).to be false
187
+ end
188
+
189
+ it 'correctly identifies truly non-nested conditions when attribute name starts with table name' do
190
+ # Create a condition with an attribute that starts with the table name
191
+ condition = Condition.extract(
192
+ Context.for(Person), 'name_eq', 'Test'
193
+ )
194
+
195
+ # Modify the attribute name to start with the table name for testing purposes
196
+ attribute = condition.attributes.first
197
+ allow(attribute).to receive(:name).and_return('people_name')
198
+
199
+ # Create a parent table
200
+ parent_table = Person.arel_table
201
+
202
+ # Now the method should return true because 'people_name' starts with 'people'
203
+ result = condition.send(:not_nested_condition, attribute, parent_table)
204
+ expect(result).to be true
205
+ end
206
+
207
+ it 'correctly identifies nested conditions' do
208
+ condition = Condition.extract(
209
+ Context.for(Person), 'articles_title_eq', 'Test'
210
+ )
211
+
212
+ # Create a mock table alias
213
+ parent_table = Arel::Nodes::TableAlias.new(
214
+ Article.arel_table,
215
+ Article.arel_table
216
+ )
217
+
218
+ # Access the private method using send
219
+ result = condition.send(:not_nested_condition, condition.attributes.first, parent_table)
220
+
221
+ # Should return false for nested condition
222
+ expect(result).to be false
223
+ end
224
+ end
225
+
226
+ context 'with polymorphic associations and not_in predicate' do
227
+ before do
228
+ # Define test models for polymorphic associations
229
+ class ::TestTask < ActiveRecord::Base
230
+ self.table_name = 'tasks'
231
+ has_many :follows, primary_key: :uid, inverse_of: :followed, foreign_key: :followed_uid, class_name: 'TestFollow'
232
+ has_many :users, through: :follows, source: :follower, source_type: 'TestUser'
233
+
234
+ # Add ransackable_attributes method
235
+ def self.ransackable_attributes(auth_object = nil)
236
+ ["created_at", "id", "name", "uid", "updated_at"]
237
+ end
238
+
239
+ # Add ransackable_associations method
240
+ def self.ransackable_associations(auth_object = nil)
241
+ ["follows", "users"]
242
+ end
243
+ end
244
+
245
+ class ::TestFollow < ActiveRecord::Base
246
+ self.table_name = 'follows'
247
+ belongs_to :follower, polymorphic: true, foreign_key: :follower_uid, primary_key: :uid
248
+ belongs_to :followed, polymorphic: true, foreign_key: :followed_uid, primary_key: :uid
249
+
250
+ # Add ransackable_attributes method
251
+ def self.ransackable_attributes(auth_object = nil)
252
+ ["created_at", "followed_type", "followed_uid", "follower_type", "follower_uid", "id", "updated_at"]
253
+ end
254
+
255
+ # Add ransackable_associations method
256
+ def self.ransackable_associations(auth_object = nil)
257
+ ["followed", "follower"]
258
+ end
259
+ end
260
+
261
+ class ::TestUser < ActiveRecord::Base
262
+ self.table_name = 'users'
263
+ has_many :follows, primary_key: :uid, inverse_of: :follower, foreign_key: :follower_uid, class_name: 'TestFollow'
264
+ has_many :tasks, through: :follows, source: :followed, source_type: 'TestTask'
265
+
266
+ # Add ransackable_attributes method
267
+ def self.ransackable_attributes(auth_object = nil)
268
+ ["created_at", "id", "name", "uid", "updated_at"]
269
+ end
270
+
271
+ # Add ransackable_associations method
272
+ def self.ransackable_associations(auth_object = nil)
273
+ ["follows", "tasks"]
274
+ end
275
+ end
276
+
277
+ # Create tables if they don't exist
278
+ ActiveRecord::Base.connection.create_table(:tasks, force: true) do |t|
279
+ t.string :uid
280
+ t.string :name
281
+ t.timestamps null: false
282
+ end
283
+
284
+ ActiveRecord::Base.connection.create_table(:follows, force: true) do |t|
285
+ t.string :followed_uid, null: false
286
+ t.string :followed_type, null: false
287
+ t.string :follower_uid, null: false
288
+ t.string :follower_type, null: false
289
+ t.timestamps null: false
290
+ t.index [:followed_uid, :followed_type]
291
+ t.index [:follower_uid, :follower_type]
292
+ end
293
+
294
+ ActiveRecord::Base.connection.create_table(:users, force: true) do |t|
295
+ t.string :uid
296
+ t.string :name
297
+ t.timestamps null: false
298
+ end
299
+ end
300
+
301
+ after do
302
+ # Clean up test models and tables
303
+ Object.send(:remove_const, :TestTask)
304
+ Object.send(:remove_const, :TestFollow)
305
+ Object.send(:remove_const, :TestUser)
306
+
307
+ ActiveRecord::Base.connection.drop_table(:tasks, if_exists: true)
308
+ ActiveRecord::Base.connection.drop_table(:follows, if_exists: true)
309
+ ActiveRecord::Base.connection.drop_table(:users, if_exists: true)
310
+ end
311
+
312
+ it 'correctly handles not_in predicate with polymorphic associations' do
313
+ # Create the search
314
+ search = TestTask.ransack(users_uid_not_in: ['uid_example'])
315
+ sql = search.result.to_sql
316
+
317
+ # Verify the SQL contains the expected NOT IN clause
318
+ expect(sql).to include('NOT IN')
319
+ expect(sql).to include("follower_uid")
320
+ expect(sql).to include("followed_uid")
321
+ expect(sql).to include("'uid_example'")
322
+
323
+ # The SQL should include a reference to tasks.uid
324
+ expect(sql).to include("tasks")
325
+ expect(sql).to include("uid")
326
+
327
+ # The SQL should include a reference to follows table
328
+ expect(sql).to include("follows")
329
+ end
330
+ end
101
331
  end
102
332
  end
103
333
  end
@@ -3,7 +3,6 @@ require 'spec_helper'
3
3
  module Ransack
4
4
  module Nodes
5
5
  describe Grouping do
6
-
7
6
  before do
8
7
  @g = 1
9
8
  end
@@ -66,6 +65,7 @@ module Ransack
66
65
  }
67
66
  }
68
67
  end
68
+
69
69
  before { subject.conditions = conditions }
70
70
 
71
71
  it 'expect duplicates to be removed' do
@@ -98,6 +98,7 @@ module Ransack
98
98
  }
99
99
  }
100
100
  end
101
+
101
102
  before { subject.conditions = conditions }
102
103
 
103
104
  it 'expect them to be parsed as different and not as duplicates' do
@@ -105,7 +106,6 @@ module Ransack
105
106
  end
106
107
  end
107
108
  end
108
-
109
109
  end
110
110
  end
111
111
  end
@@ -71,6 +71,18 @@ module Ransack
71
71
  end
72
72
  end
73
73
 
74
+ [[], ["12"], ["101.5"]].each do |value|
75
+ context "with an array value (#{value.inspect})" do
76
+ let(:raw_value) { value }
77
+
78
+ it "should cast to integer as nil" do
79
+ result = subject.cast(:integer)
80
+
81
+ expect(result).to be nil
82
+ end
83
+ end
84
+ end
85
+
74
86
  ["12", "101.5"].each do |value|
75
87
  context "with a float value (#{value})" do
76
88
  let(:raw_value) { value }
@@ -109,7 +121,6 @@ module Ransack
109
121
  end
110
122
  end
111
123
  end
112
-
113
124
  end
114
125
  end
115
126
  end
@@ -5,7 +5,6 @@ module Ransack
5
5
  FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE'].to_set
6
6
 
7
7
  describe Predicate do
8
-
9
8
  before do
10
9
  @s = Search.new(Person)
11
10
  end
@@ -158,9 +157,10 @@ module Ransack
158
157
 
159
158
  describe 'cont' do
160
159
  it_has_behavior 'wildcard escaping', :name_cont,
161
- (if ActiveRecord::Base.connection.adapter_name == "PostgreSQL"
160
+ (case ActiveRecord::Base.connection.adapter_name
161
+ when "PostGIS", "PostgreSQL"
162
162
  /"people"."name" ILIKE '%\\%\\.\\_\\\\%'/
163
- elsif ActiveRecord::Base.connection.adapter_name == "Mysql2"
163
+ when "Mysql2"
164
164
  /`people`.`name` LIKE '%\\\\%.\\\\_\\\\\\\\%'/
165
165
  else
166
166
  /"people"."name" LIKE '%%._\\%'/
@@ -177,9 +177,10 @@ module Ransack
177
177
 
178
178
  describe 'not_cont' do
179
179
  it_has_behavior 'wildcard escaping', :name_not_cont,
180
- (if ActiveRecord::Base.connection.adapter_name == "PostgreSQL"
180
+ (case ActiveRecord::Base.connection.adapter_name
181
+ when "PostGIS", "PostgreSQL"
181
182
  /"people"."name" NOT ILIKE '%\\%\\.\\_\\\\%'/
182
- elsif ActiveRecord::Base.connection.adapter_name == "Mysql2"
183
+ when "Mysql2"
183
184
  /`people`.`name` NOT LIKE '%\\\\%.\\\\_\\\\\\\\%'/
184
185
  else
185
186
  /"people"."name" NOT LIKE '%%._\\%'/
@@ -196,9 +197,12 @@ module Ransack
196
197
 
197
198
  describe 'i_cont' do
198
199
  it_has_behavior 'wildcard escaping', :name_i_cont,
199
- (if ActiveRecord::Base.connection.adapter_name == "PostgreSQL"
200
+ (case ActiveRecord::Base.connection.adapter_name
201
+ when "PostGIS"
202
+ /LOWER\("people"."name"\) ILIKE '%\\%\\.\\_\\\\%'/
203
+ when "PostgreSQL"
200
204
  /"people"."name" ILIKE '%\\%\\.\\_\\\\%'/
201
- elsif ActiveRecord::Base.connection.adapter_name == "Mysql2"
205
+ when "Mysql2"
202
206
  /LOWER\(`people`.`name`\) LIKE '%\\\\%.\\\\_\\\\\\\\%'/
203
207
  else
204
208
  /LOWER\("people"."name"\) LIKE '%%._\\%'/
@@ -215,9 +219,12 @@ module Ransack
215
219
 
216
220
  describe 'not_i_cont' do
217
221
  it_has_behavior 'wildcard escaping', :name_not_i_cont,
218
- (if ActiveRecord::Base.connection.adapter_name == "PostgreSQL"
222
+ (case ActiveRecord::Base.connection.adapter_name
223
+ when "PostGIS"
224
+ /LOWER\("people"."name"\) NOT ILIKE '%\\%\\.\\_\\\\%'/
225
+ when "PostgreSQL"
219
226
  /"people"."name" NOT ILIKE '%\\%\\.\\_\\\\%'/
220
- elsif ActiveRecord::Base.connection.adapter_name == "Mysql2"
227
+ when "Mysql2"
221
228
  /LOWER\(`people`.`name`\) NOT LIKE '%\\\\%.\\\\_\\\\\\\\%'/
222
229
  else
223
230
  /LOWER\("people"."name"\) NOT LIKE '%%._\\%'/
@@ -178,7 +178,6 @@ module Ransack
178
178
  # AND "articles"."title" = 'Test' AND "articles"."published" = 't' AND ('default_scope' = 'default_scope')
179
179
  # ) ORDER BY "people"."id" DESC
180
180
 
181
- pending("spec should pass, but I do not know how/where to fix lib code")
182
181
  s = Search.new(Person, published_articles_title_not_eq: 'Test')
183
182
  expect(s.result.to_sql).to include 'default_scope'
184
183
  expect(s.result.to_sql).to include 'published'
@@ -270,6 +269,7 @@ module Ransack
270
269
  end
271
270
 
272
271
  specify { expect { subject }.to raise_error ArgumentError }
272
+ specify { expect { subject }.to raise_error InvalidSearchError }
273
273
  end
274
274
 
275
275
  context 'when ignore_unknown_conditions configuration option is true' do
@@ -300,6 +300,7 @@ module Ransack
300
300
 
301
301
  context 'when ignore_unknown_conditions search parameter is false' do
302
302
  specify { expect { with_ignore_unknown_conditions_false }.to raise_error ArgumentError }
303
+ specify { expect { with_ignore_unknown_conditions_false }.to raise_error InvalidSearchError }
303
304
  end
304
305
 
305
306
  context 'when ignore_unknown_conditions search parameter is true' do
@@ -335,18 +336,90 @@ module Ransack
335
336
  end
336
337
  end
337
338
 
339
+ context "ransackable_scope with array arguments" do
340
+ around(:each) do |example|
341
+ Person.define_singleton_method(:domestic) do |countries|
342
+ self.where(name: countries)
343
+ end
344
+
345
+ Person.define_singleton_method(:flexible_scope) do |*args|
346
+ self.where(id: args)
347
+ end
348
+
349
+ Person.define_singleton_method(:two_param_scope) do |param1, param2|
350
+ self.where(name: param1, id: param2)
351
+ end
352
+
353
+ begin
354
+ example.run
355
+ ensure
356
+ Person.singleton_class.undef_method :domestic
357
+ Person.singleton_class.undef_method :flexible_scope
358
+ Person.singleton_class.undef_method :two_param_scope
359
+ end
360
+ end
361
+
362
+ it "handles scopes that take arrays as single arguments (arity 1)" do
363
+ allow(Person).to receive(:ransackable_scopes)
364
+ .and_return(Person.ransackable_scopes + [:domestic])
365
+
366
+ # This should not raise ArgumentError
367
+ expect {
368
+ s = Search.new(Person, domestic: ['US', 'JP'])
369
+ s.result # This triggers the actual scope call
370
+ }.not_to raise_error
371
+
372
+ s = Search.new(Person, domestic: ['US', 'JP'])
373
+ expect(s.instance_variable_get(:@scope_args)["domestic"]).to eq("US")
374
+ end
375
+
376
+ it "handles scopes with flexible arity (negative arity)" do
377
+ allow(Person).to receive(:ransackable_scopes)
378
+ .and_return(Person.ransackable_scopes + [:flexible_scope])
379
+
380
+ expect {
381
+ s = Search.new(Person, flexible_scope: ['US', 'JP'])
382
+ s.result
383
+ }.not_to raise_error
384
+ end
385
+
386
+ it "handles scopes with arity > 1" do
387
+ allow(Person).to receive(:ransackable_scopes)
388
+ .and_return(Person.ransackable_scopes + [:two_param_scope])
389
+
390
+ expect {
391
+ s = Search.new(Person, two_param_scope: ['param1', 'param2'])
392
+ s.result
393
+ }.not_to raise_error
394
+ end
395
+
396
+ it "still supports the workaround with nested arrays" do
397
+ allow(Person).to receive(:ransackable_scopes)
398
+ .and_return(Person.ransackable_scopes + [:domestic])
399
+
400
+ # The workaround from the issue should still work
401
+ expect {
402
+ s = Search.new(Person, domestic: [['US', 'JP']])
403
+ s.result
404
+ }.not_to raise_error
405
+ end
406
+ end
338
407
  end
339
408
 
340
409
  describe '#result' do
341
410
  let(:people_name_field) {
342
411
  "#{quote_table_name("people")}.#{quote_column_name("name")}"
343
412
  }
413
+ let(:people_temperament_field) {
414
+ "#{quote_table_name("people")}.#{quote_column_name("temperament")}"
415
+ }
344
416
  let(:children_people_name_field) {
345
417
  "#{quote_table_name("children_people")}.#{quote_column_name("name")}"
346
418
  }
347
419
  let(:notable_type_field) {
348
420
  "#{quote_table_name("notes")}.#{quote_column_name("notable_type")}"
349
421
  }
422
+
350
423
  it 'evaluates conditions contextually' do
351
424
  s = Search.new(Person, children_name_eq: 'Ernie')
352
425
  expect(s.result).to be_an ActiveRecord::Relation
@@ -354,6 +427,36 @@ module Ransack
354
427
  children_people_name_field} = 'Ernie'/
355
428
  end
356
429
 
430
+ context 'when evaluating enums' do
431
+ before do
432
+ Person.take.update_attribute(:temperament, 'choleric')
433
+ end
434
+
435
+ it 'evaluates enum key correctly' do
436
+ s = Search.new(Person, temperament_eq: 'choleric')
437
+
438
+ expect(s.result.to_sql).not_to match /#{
439
+ people_temperament_field} = 0/
440
+
441
+ expect(s.result.to_sql).to match /#{
442
+ people_temperament_field} = #{Person.temperaments[:choleric]}/
443
+
444
+ expect(s.result).not_to be_empty
445
+ end
446
+
447
+ it 'evaluates enum value correctly' do
448
+ s = Search.new(Person, temperament_eq: Person.temperaments[:choleric])
449
+
450
+ expect(s.result.to_sql).not_to match /#{
451
+ people_temperament_field} = 0/
452
+
453
+ expect(s.result.to_sql).to match /#{
454
+ people_temperament_field} = #{Person.temperaments[:choleric]}/
455
+
456
+ expect(s.result).not_to be_empty
457
+ end
458
+ end
459
+
357
460
  it 'use appropriate table alias' do
358
461
  s = Search.new(Person, {
359
462
  name_eq: "person_name_query",
@@ -477,6 +580,11 @@ module Ransack
477
580
  @s = Search.new(Person)
478
581
  end
479
582
 
583
+ it 'doesn\'t creates sorts' do
584
+ @s.sorts = ''
585
+ expect(@s.sorts.size).to eq(0)
586
+ end
587
+
480
588
  it 'creates sorts based on a single attribute/direction' do
481
589
  @s.sorts = 'id desc'
482
590
  expect(@s.sorts.size).to eq(1)
@@ -614,6 +722,18 @@ module Ransack
614
722
  expect(@s.result.first.id).to eq 1
615
723
  end
616
724
 
725
+ it 'raises ArgumentError when an invalid argument is sent' do
726
+ expect do
727
+ @s.sorts = 1234
728
+ end.to raise_error(ArgumentError, "Invalid argument (Integer) supplied to sorts=")
729
+ end
730
+
731
+ it 'raises InvalidSearchError when an invalid argument is sent' do
732
+ expect do
733
+ @s.sorts = 1234
734
+ end.to raise_error(Ransack::InvalidSearchError, "Invalid argument (Integer) supplied to sorts=")
735
+ end
736
+
617
737
  it "PG's sort option", if: ::ActiveRecord::Base.connection.adapter_name == "PostgreSQL" do
618
738
  default = Ransack.options.clone
619
739
 
@@ -2,7 +2,6 @@ require 'spec_helper'
2
2
 
3
3
  module Ransack
4
4
  describe Translate do
5
-
6
5
  describe '.attribute' do
7
6
  it 'translate namespaced attribute like AR does' do
8
7
  ar_translation = ::Namespace::Article.human_attribute_name(:title)
data/spec/spec_helper.rb CHANGED
@@ -1,13 +1,12 @@
1
- require 'machinist/active_record'
2
- require 'polyamorous/polyamorous'
1
+ require 'ransack'
3
2
  require 'sham'
4
3
  require 'faker'
5
- require 'ransack'
6
4
  require 'action_controller'
7
5
  require 'ransack/helpers'
8
6
  require 'pry'
9
7
  require 'simplecov'
10
8
  require 'byebug'
9
+ require 'machinist/active_record'
11
10
 
12
11
  SimpleCov.start
13
12
  I18n.enforce_available_locales = false
@@ -1,4 +1,5 @@
1
1
  require 'active_record'
2
+ require 'activerecord-postgis-adapter'
2
3
 
3
4
  case ENV['DB'].try(:downcase)
4
5
  when 'mysql', 'mysql2'
@@ -20,6 +21,17 @@ when 'pg', 'postgres', 'postgresql'
20
21
  host: ENV.fetch("DATABASE_HOST") { "localhost" },
21
22
  min_messages: 'warning'
22
23
  )
24
+ when 'postgis'
25
+ # To test with PostGIS: `DB=postgis bundle exec rake spec`
26
+ ActiveRecord::Base.establish_connection(
27
+ adapter: 'postgis',
28
+ postgis_extension: 'postgis',
29
+ database: 'ransack',
30
+ username: ENV.fetch("DATABASE_USERNAME") { "postgres" },
31
+ password: ENV.fetch("DATABASE_PASSWORD") { "" },
32
+ host: ENV.fetch("DATABASE_HOST") { "localhost" },
33
+ min_messages: 'warning'
34
+ )
23
35
  else
24
36
  # Otherwise, assume SQLite3: `bundle exec rake spec`
25
37
  ActiveRecord::Base.establish_connection(
@@ -69,6 +81,8 @@ class Person < ApplicationRecord
69
81
  scope :sort_by_reverse_name_asc, lambda { order(Arel.sql("REVERSE(name) ASC")) }
70
82
  scope :sort_by_reverse_name_desc, lambda { order("REVERSE(name) DESC") }
71
83
 
84
+ enum :temperament, { sanguine: 1, choleric: 2, melancholic: 3, phlegmatic: 4 }
85
+
72
86
  alias_attribute :full_name, :name
73
87
 
74
88
  ransack_alias :term, :name_or_email
@@ -126,6 +140,17 @@ class Person < ApplicationRecord
126
140
  Arel.sql(query)
127
141
  end
128
142
 
143
+ ransacker :article_tags, formatter: proc { |id|
144
+ if Tag.exists?(id)
145
+ joins(articles: :tags)
146
+ .where(tags: { id: id })
147
+ .distinct
148
+ .select(:id).arel
149
+ end
150
+ } do |parent|
151
+ parent.table[:id]
152
+ end
153
+
129
154
  def self.ransackable_attributes(auth_object = nil)
130
155
  if auth_object == :admin
131
156
  authorizable_ransackable_attributes - ['only_sort']
@@ -151,6 +176,7 @@ class Article < ApplicationRecord
151
176
  has_many :comments
152
177
  has_and_belongs_to_many :tags
153
178
  has_many :notes, as: :notable
179
+ has_many :recent_notes, as: :notable
154
180
 
155
181
  alias_attribute :content, :body
156
182
 
@@ -220,18 +246,28 @@ end
220
246
  class Comment < ApplicationRecord
221
247
  belongs_to :article
222
248
  belongs_to :person
249
+ has_and_belongs_to_many :tags
223
250
 
224
251
  default_scope { where(disabled: false) }
225
252
  end
226
253
 
227
254
  class Tag < ApplicationRecord
228
255
  has_and_belongs_to_many :articles
256
+ has_and_belongs_to_many :comments
229
257
  end
230
258
 
231
259
  class Note < ApplicationRecord
232
260
  belongs_to :notable, polymorphic: true
233
261
  end
234
262
 
263
+ class RecentNote < ApplicationRecord
264
+ DEFAULT_NOTABLE_ID = 1
265
+ self.table_name = "notes"
266
+ default_scope { where(notable_id: DEFAULT_NOTABLE_ID) }
267
+
268
+ belongs_to :notable, polymorphic: true
269
+ end
270
+
235
271
  class Account < ApplicationRecord
236
272
  belongs_to :agent_account, class_name: "Account"
237
273
  belongs_to :trade_account, class_name: "Account"
@@ -266,6 +302,7 @@ module Schema
266
302
  t.string :new_start
267
303
  t.string :stop_end
268
304
  t.integer :salary
305
+ t.integer :temperament
269
306
  t.date :life_start
270
307
  t.boolean :awesome, default: false
271
308
  t.boolean :terms_and_conditions, default: false
@@ -298,6 +335,11 @@ module Schema
298
335
  t.integer :tag_id
299
336
  end
300
337
 
338
+ create_table :comments_tags, force: true, id: false do |t|
339
+ t.integer :comment_id
340
+ t.integer :tag_id
341
+ end
342
+
301
343
  create_table :notes, force: true do |t|
302
344
  t.integer :notable_id
303
345
  t.string :notable_type