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.
- checksums.yaml +4 -4
- data/.github/FUNDING.yml +3 -0
- data/.github/SECURITY.md +12 -0
- data/.github/workflows/codeql.yml +72 -0
- data/.github/workflows/cronjob.yml +99 -0
- data/.github/workflows/deploy.yml +35 -0
- data/.github/workflows/rubocop.yml +20 -0
- data/.github/workflows/test-deploy.yml +29 -0
- data/.github/workflows/test.yml +131 -0
- data/.nojekyll +0 -0
- data/.rubocop.yml +50 -0
- data/CHANGELOG.md +251 -1
- data/CONTRIBUTING.md +51 -29
- data/Gemfile +12 -10
- data/README.md +45 -907
- data/bug_report_templates/test-ransack-scope-and-column-same-name.rb +78 -0
- data/bug_report_templates/test-ransacker-arel-present-predicate.rb +75 -0
- data/docs/.gitignore +19 -0
- data/docs/.nojekyll +0 -0
- data/docs/babel.config.js +3 -0
- data/docs/blog/2022-03-27-ransack-3.0.0.md +20 -0
- data/docs/docs/getting-started/_category_.json +4 -0
- data/docs/docs/getting-started/advanced-mode.md +46 -0
- data/docs/docs/getting-started/configuration.md +47 -0
- data/docs/docs/getting-started/search-matches.md +67 -0
- data/docs/docs/getting-started/simple-mode.md +288 -0
- data/docs/docs/getting-started/sorting.md +71 -0
- data/docs/docs/getting-started/using-predicates.md +282 -0
- data/docs/docs/going-further/_category_.json +4 -0
- data/docs/docs/going-further/acts-as-taggable-on.md +114 -0
- data/docs/docs/going-further/associations.md +70 -0
- data/docs/docs/going-further/custom-predicates.md +52 -0
- data/docs/docs/going-further/documentation.md +43 -0
- data/docs/docs/going-further/exporting-to-csv.md +49 -0
- data/docs/docs/going-further/external-guides.md +57 -0
- data/docs/docs/going-further/form-customisation.md +63 -0
- data/docs/docs/going-further/i18n.md +53 -0
- data/docs/docs/going-further/img/create_release.png +0 -0
- data/docs/docs/going-further/merging-searches.md +41 -0
- data/docs/docs/going-further/other-notes.md +428 -0
- data/docs/docs/going-further/polymorphic-search.md +46 -0
- data/docs/docs/going-further/ransackers.md +331 -0
- data/docs/docs/going-further/release_process.md +36 -0
- data/docs/docs/going-further/saving-queries.md +82 -0
- data/docs/docs/going-further/searching-postgres.md +57 -0
- data/docs/docs/going-further/wiki-contributors.md +82 -0
- data/docs/docs/intro.md +99 -0
- data/docs/docusaurus.config.js +120 -0
- data/docs/package.json +42 -0
- data/docs/sidebars.js +31 -0
- data/docs/src/components/HomepageFeatures/index.js +64 -0
- data/docs/src/components/HomepageFeatures/styles.module.css +11 -0
- data/docs/src/css/custom.css +39 -0
- data/docs/src/pages/index.module.css +23 -0
- data/docs/src/pages/markdown-page.md +7 -0
- data/docs/static/.nojekyll +0 -0
- data/docs/static/img/docusaurus.png +0 -0
- data/docs/static/img/favicon.ico +0 -0
- data/docs/static/img/logo.svg +1 -0
- data/docs/static/img/tutorial/docsVersionDropdown.png +0 -0
- data/docs/static/img/tutorial/localeDropdown.png +0 -0
- data/docs/static/img/undraw_docusaurus_mountain.svg +171 -0
- data/docs/static/img/undraw_docusaurus_react.svg +170 -0
- data/docs/static/img/undraw_docusaurus_tree.svg +40 -0
- data/docs/yarn.lock +8879 -0
- data/lib/polyamorous/activerecord/join_association.rb +70 -0
- data/{polyamorous/lib/polyamorous/activerecord_6.0_ruby_2 → lib/polyamorous/activerecord}/join_dependency.rb +33 -12
- data/lib/polyamorous/activerecord/reflection.rb +11 -0
- data/{polyamorous/lib → lib/polyamorous}/polyamorous.rb +3 -4
- data/lib/ransack/adapters/active_record/base.rb +83 -10
- data/lib/ransack/adapters/active_record/context.rb +56 -44
- data/lib/ransack/configuration.rb +53 -10
- data/lib/ransack/constants.rb +126 -4
- data/lib/ransack/context.rb +34 -5
- data/lib/ransack/helpers/form_builder.rb +6 -6
- data/lib/ransack/helpers/form_helper.rb +14 -5
- data/lib/ransack/helpers.rb +1 -1
- data/lib/ransack/locale/sv.yml +70 -0
- data/lib/ransack/nodes/attribute.rb +3 -3
- data/lib/ransack/nodes/condition.rb +80 -9
- data/lib/ransack/nodes/grouping.rb +4 -4
- data/lib/ransack/nodes/node.rb +1 -1
- data/lib/ransack/nodes/sort.rb +3 -3
- data/lib/ransack/nodes/value.rb +3 -3
- data/lib/ransack/predicate.rb +1 -1
- data/lib/ransack/ransacker.rb +1 -1
- data/lib/ransack/search.rb +15 -7
- data/lib/ransack/translate.rb +6 -6
- data/lib/ransack/version.rb +1 -1
- data/lib/ransack/visitor.rb +38 -2
- data/lib/ransack.rb +5 -8
- data/ransack.gemspec +9 -15
- data/spec/blueprints/articles.rb +1 -1
- data/spec/blueprints/comments.rb +1 -1
- data/spec/blueprints/notes.rb +1 -1
- data/spec/blueprints/tags.rb +1 -1
- data/spec/console.rb +5 -5
- data/spec/helpers/polyamorous_helper.rb +2 -8
- data/spec/helpers/ransack_helper.rb +1 -1
- data/spec/polyamorous/activerecord_compatibility_spec.rb +15 -0
- data/spec/{ransack → polyamorous}/join_association_spec.rb +3 -1
- data/spec/{ransack → polyamorous}/join_dependency_spec.rb +0 -16
- data/spec/ransack/adapters/active_record/base_spec.rb +125 -16
- data/spec/ransack/adapters/active_record/context_spec.rb +19 -18
- data/spec/ransack/configuration_spec.rb +33 -9
- data/spec/ransack/helpers/form_builder_spec.rb +8 -8
- data/spec/ransack/helpers/form_helper_spec.rb +109 -20
- data/spec/ransack/nodes/condition_spec.rb +37 -0
- data/spec/ransack/nodes/grouping_spec.rb +2 -2
- data/spec/ransack/nodes/value_spec.rb +115 -0
- data/spec/ransack/predicate_spec.rb +37 -2
- data/spec/ransack/search_spec.rb +238 -30
- data/spec/ransack/translate_spec.rb +1 -1
- data/spec/spec_helper.rb +7 -5
- data/spec/support/schema.rb +108 -11
- metadata +98 -62
- data/.travis.yml +0 -47
- data/lib/ransack/adapters/active_record/ransack/constants.rb +0 -128
- data/lib/ransack/adapters/active_record/ransack/context.rb +0 -55
- data/lib/ransack/adapters/active_record/ransack/nodes/condition.rb +0 -61
- data/lib/ransack/adapters/active_record/ransack/translate.rb +0 -8
- data/lib/ransack/adapters/active_record/ransack/visitor.rb +0 -47
- data/lib/ransack/adapters.rb +0 -64
- data/lib/ransack/nodes.rb +0 -8
- data/polyamorous/lib/polyamorous/activerecord_5.2_ruby_2/join_association.rb +0 -20
- data/polyamorous/lib/polyamorous/activerecord_5.2_ruby_2/join_dependency.rb +0 -79
- data/polyamorous/lib/polyamorous/activerecord_5.2_ruby_2/reflection.rb +0 -12
- data/polyamorous/lib/polyamorous/activerecord_6.0_ruby_2/join_association.rb +0 -2
- data/polyamorous/lib/polyamorous/activerecord_6.0_ruby_2/reflection.rb +0 -2
- data/polyamorous/lib/polyamorous/activerecord_6.1_ruby_2/join_association.rb +0 -2
- data/polyamorous/lib/polyamorous/activerecord_6.1_ruby_2/join_dependency.rb +0 -2
- data/polyamorous/lib/polyamorous/activerecord_6.1_ruby_2/reflection.rb +0 -2
- data/polyamorous/lib/polyamorous/version.rb +0 -3
- data/polyamorous/polyamorous.gemspec +0 -27
- /data/{logo → docs/static/logo}/ransack-h.png +0 -0
- /data/{logo → docs/static/logo}/ransack-h.svg +0 -0
- /data/{logo → docs/static/logo}/ransack-v.png +0 -0
- /data/{logo → docs/static/logo}/ransack-v.svg +0 -0
- /data/{logo → docs/static/logo}/ransack.png +0 -0
- /data/{logo → docs/static/logo}/ransack.svg +0 -0
- /data/{polyamorous/lib → lib}/polyamorous/join.rb +0 -0
- /data/{polyamorous/lib → lib}/polyamorous/swapping_reflection_class.rb +0 -0
- /data/{polyamorous/lib → lib}/polyamorous/tree_node.rb +0 -0
- /data/lib/ransack/{adapters/active_record.rb → active_record.rb} +0 -0
- /data/spec/{ransack → polyamorous}/join_spec.rb +0 -0
data/spec/ransack/search_spec.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
324
|
-
|
|
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
|
|
381
|
-
@s.sorts =
|
|
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 '
|
|
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
|
|
393
|
-
@s.sorts = ['id DESC', { name: '
|
|
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 '
|
|
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
|
|
558
|
+
it 'creates sorts based on attributes, alias and different directions
|
|
405
559
|
in array format' do
|
|
406
|
-
@s.sorts = ['id DESC', { name: '
|
|
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 '
|
|
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
|
|
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: '
|
|
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
|
-
|
|
579
|
+
daddy_sort = @s.sorts.detect { |s| s.name == 'parent_name' }
|
|
426
580
|
expect(id_sort.dir).to eq 'desc'
|
|
427
|
-
expect(
|
|
581
|
+
expect(daddy_sort.dir).to eq 'asc'
|
|
428
582
|
end
|
|
429
583
|
|
|
430
|
-
it 'creates sorts based on
|
|
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: '
|
|
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
|
-
|
|
593
|
+
daddy_sort = @s.sorts.detect { |s| s.name == 'parent_name' }
|
|
440
594
|
expect(id_sort.dir).to eq 'desc'
|
|
441
|
-
expect(
|
|
595
|
+
expect(daddy_sort.dir).to eq 'asc'
|
|
442
596
|
end
|
|
443
597
|
|
|
444
|
-
it 'creates sorts based on
|
|
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: '
|
|
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
|
-
|
|
607
|
+
daddy_sort = @s.sorts.detect { |s| s.name == 'parent_name' }
|
|
454
608
|
expect(id_sort.dir).to eq 'desc'
|
|
455
|
-
expect(
|
|
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
|
-
:
|
|
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
|
|
data/spec/support/schema.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
131
|
+
authorizable_ransackable_attributes - ['only_sort']
|
|
115
132
|
else
|
|
116
|
-
|
|
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 <
|
|
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 <
|
|
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 <
|
|
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 <
|
|
227
|
+
class Tag < ApplicationRecord
|
|
170
228
|
has_and_belongs_to_many :articles
|
|
171
229
|
end
|
|
172
230
|
|
|
173
|
-
class Note <
|
|
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 <
|
|
356
|
+
class Base < ApplicationRecord
|
|
260
357
|
self.abstract_class = true
|
|
261
358
|
establish_connection(
|
|
262
359
|
adapter: 'sqlite3',
|