ransack 2.6.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/.nojekyll +0 -0
  3. data/CHANGELOG.md +75 -13
  4. data/README.md +45 -1039
  5. data/docs/.gitignore +20 -0
  6. data/docs/.nojekyll +0 -0
  7. data/docs/babel.config.js +3 -0
  8. data/docs/blog/2022-03-27-ransack-3.0.0.md +20 -0
  9. data/docs/docs/getting-started/_category_.json +4 -0
  10. data/docs/docs/getting-started/advanced-mode.md +46 -0
  11. data/docs/docs/getting-started/configuration.md +47 -0
  12. data/docs/docs/getting-started/search-matches.md +67 -0
  13. data/docs/docs/getting-started/simple-mode.md +284 -0
  14. data/docs/docs/getting-started/sorting.md +79 -0
  15. data/docs/docs/getting-started/using-predicates.md +282 -0
  16. data/docs/docs/going-further/_category_.json +4 -0
  17. data/docs/docs/going-further/associations.md +70 -0
  18. data/docs/docs/going-further/custom-predicates.md +52 -0
  19. data/docs/docs/going-further/documentation.md +31 -0
  20. data/docs/docs/going-further/exporting-to-csv.md +49 -0
  21. data/docs/docs/going-further/external-guides.md +57 -0
  22. data/docs/docs/going-further/form-customisation.md +63 -0
  23. data/docs/docs/going-further/i18n.md +53 -0
  24. data/docs/{img → docs/going-further/img}/create_release.png +0 -0
  25. data/docs/docs/going-further/merging-searches.md +41 -0
  26. data/docs/docs/going-further/other-notes.md +428 -0
  27. data/docs/docs/going-further/ransackers.md +331 -0
  28. data/docs/docs/going-further/release_process.md +36 -0
  29. data/docs/docs/going-further/saving-queries.md +82 -0
  30. data/docs/docs/going-further/searching-postgres.md +57 -0
  31. data/docs/docs/intro.md +99 -0
  32. data/docs/docusaurus.config.js +108 -0
  33. data/docs/package-lock.json +9207 -0
  34. data/docs/package.json +37 -0
  35. data/docs/sidebars.js +31 -0
  36. data/docs/src/components/HomepageFeatures/index.js +64 -0
  37. data/docs/src/components/HomepageFeatures/styles.module.css +11 -0
  38. data/docs/src/css/custom.css +39 -0
  39. data/docs/src/pages/index.module.css +23 -0
  40. data/docs/src/pages/markdown-page.md +7 -0
  41. data/docs/static/.nojekyll +0 -0
  42. data/docs/static/img/docusaurus.png +0 -0
  43. data/docs/static/img/favicon.ico +0 -0
  44. data/docs/static/img/logo.svg +1 -0
  45. data/docs/static/img/tutorial/docsVersionDropdown.png +0 -0
  46. data/docs/static/img/tutorial/localeDropdown.png +0 -0
  47. data/docs/static/img/undraw_docusaurus_mountain.svg +171 -0
  48. data/docs/static/img/undraw_docusaurus_react.svg +170 -0
  49. data/docs/static/img/undraw_docusaurus_tree.svg +40 -0
  50. data/{logo → docs/static/logo}/ransack-h.png +0 -0
  51. data/{logo → docs/static/logo}/ransack-h.svg +0 -0
  52. data/{logo → docs/static/logo}/ransack-v.png +0 -0
  53. data/{logo → docs/static/logo}/ransack-v.svg +0 -0
  54. data/{logo → docs/static/logo}/ransack.png +0 -0
  55. data/{logo → docs/static/logo}/ransack.svg +0 -0
  56. data/docs/yarn.lock +7671 -0
  57. data/lib/ransack/adapters/active_record/base.rb +0 -2
  58. data/lib/ransack/adapters/active_record/context.rb +1 -0
  59. data/lib/ransack/helpers/form_helper.rb +10 -2
  60. data/lib/ransack/search.rb +2 -2
  61. data/lib/ransack/version.rb +1 -1
  62. data/ransack.gemspec +2 -2
  63. data/spec/ransack/adapters/active_record/base_spec.rb +10 -1
  64. data/spec/ransack/helpers/form_helper_spec.rb +32 -0
  65. data/spec/ransack/search_spec.rb +23 -0
  66. data/spec/support/schema.rb +16 -0
  67. metadata +58 -12
  68. data/docs/release_process.md +0 -17
@@ -0,0 +1,41 @@
1
+ ---
2
+ sidebar_position: 5
3
+ title: Merging searches
4
+ ---
5
+
6
+ To find records that match multiple searches, it's possible to merge all the ransack search conditions into an ActiveRecord relation to perform a single query. In order to avoid conflicts between joined table names it's necessary to set up a shared context to track table aliases used across all the conditions before initializing the searches:
7
+
8
+ ```ruby
9
+ shared_context = Ransack::Context.for(Person)
10
+
11
+ search_parents = Person.ransack(
12
+ { parent_name_eq: "A" }, context: shared_context
13
+ )
14
+
15
+ search_children = Person.ransack(
16
+ { children_name_eq: "B" }, context: shared_context
17
+ )
18
+
19
+ shared_conditions = [search_parents, search_children].map { |search|
20
+ Ransack::Visitor.new.accept(search.base)
21
+ }
22
+
23
+ Person.joins(shared_context.join_sources)
24
+ .where(shared_conditions.reduce(&:or))
25
+ .to_sql
26
+ ```
27
+ Produces:
28
+ ```sql
29
+ SELECT "people".*
30
+ FROM "people"
31
+ LEFT OUTER JOIN "people" "parents_people"
32
+ ON "parents_people"."id" = "people"."parent_id"
33
+ LEFT OUTER JOIN "people" "children_people"
34
+ ON "children_people"."parent_id" = "people"."id"
35
+ WHERE (
36
+ ("parents_people"."name" = 'A' OR "children_people"."name" = 'B')
37
+ )
38
+ ORDER BY "people"."id" DESC
39
+ ```
40
+
41
+ Admittedly this is not as simple as it should be, but it's workable for now. (Implementing #417 could make this more straightforward.)
@@ -0,0 +1,428 @@
1
+ ---
2
+ sidebar_position: 8
3
+ title: Other notes
4
+ ---
5
+
6
+ ### Ransack Aliases
7
+
8
+ You can customize the attribute names for your Ransack searches by using a
9
+ `ransack_alias`. This is particularly useful for long attribute names that are
10
+ necessary when querying associations or multiple columns.
11
+
12
+ ```ruby
13
+ class Post < ActiveRecord::Base
14
+ belongs_to :author
15
+
16
+ # Abbreviate :author_first_name_or_author_last_name to :author
17
+ ransack_alias :author, :author_first_name_or_author_last_name
18
+ end
19
+ ```
20
+
21
+ Now, rather than using `:author_first_name_or_author_last_name_cont` in your
22
+ form, you can simply use `:author_cont`. This serves to produce more expressive
23
+ query parameters in your URLs.
24
+
25
+ ```erb
26
+ <%= search_form_for @q do |f| %>
27
+ <%= f.label :author_cont %>
28
+ <%= f.search_field :author_cont %>
29
+ <% end %>
30
+ ```
31
+
32
+ You can also use `ransack_alias` for sorting.
33
+
34
+ ```ruby
35
+ class Post < ActiveRecord::Base
36
+ belongs_to :author
37
+
38
+ # Abbreviate :author_first_name to :author
39
+ ransack_alias :author, :author_first_name
40
+ end
41
+ ```
42
+
43
+ Now, you can use `:author` instead of `:author_first_name` in a `sort_link`.
44
+
45
+ ```erb
46
+ <%= sort_link(@q, :author) %>
47
+ ```
48
+
49
+ Note that using `:author_first_name_or_author_last_name_cont` would produce an invalid sql query. In those cases, Ransack ignores the sorting clause.
50
+
51
+
52
+
53
+ ### Problem with DISTINCT selects
54
+
55
+ If passed `distinct: true`, `result` will generate a `SELECT DISTINCT` to
56
+ avoid returning duplicate rows, even if conditions on a join would otherwise
57
+ result in some. It generates the same SQL as calling `uniq` on the relation.
58
+
59
+ Please note that for many databases, a sort on an associated table's columns
60
+ may result in invalid SQL with `distinct: true` -- in those cases, you
61
+ will need to modify the result as needed to allow these queries to work.
62
+
63
+ For example, you could call joins and includes on the result which has the
64
+ effect of adding those tables columns to the select statement, overcoming
65
+ the issue, like so:
66
+
67
+ ```ruby
68
+ def index
69
+ @q = Person.ransack(params[:q])
70
+ @people = @q.result(distinct: true)
71
+ .includes(:articles)
72
+ .joins(:articles)
73
+ .page(params[:page])
74
+ end
75
+ ```
76
+
77
+ If the above doesn't help, you can also use ActiveRecord's `select` query
78
+ to explicitly add the columns you need, which brute force's adding the
79
+ columns you need that your SQL engine is complaining about, you need to
80
+ make sure you give all of the columns you care about, for example:
81
+
82
+ ```ruby
83
+ def index
84
+ @q = Person.ransack(params[:q])
85
+ @people = @q.result(distinct: true)
86
+ .select('people.*, articles.name, articles.description')
87
+ .page(params[:page])
88
+ end
89
+ ```
90
+
91
+ Another method to approach this when using Postgresql is to use ActiveRecords's `.includes` in combination with `.group` instead of `distinct: true`.
92
+
93
+ For example:
94
+ ```ruby
95
+ def index
96
+ @q = Person.ransack(params[:q])
97
+ @people = @q.result
98
+ .group('persons.id')
99
+ .includes(:articles)
100
+ .page(params[:page])
101
+ end
102
+
103
+ ```
104
+
105
+ A final way of last resort is to call `to_a.uniq` on the collection at the end
106
+ with the caveat that the de-duping is taking place in Ruby instead of in SQL,
107
+ which is potentially slower and uses more memory, and that it may display
108
+ awkwardly with pagination if the number of results is greater than the page size.
109
+
110
+ For example:
111
+
112
+ ```ruby
113
+ def index
114
+ @q = Person.ransack(params[:q])
115
+ @people = @q.result.includes(:articles).page(params[:page]).to_a.uniq
116
+ end
117
+ ```
118
+
119
+ #### `PG::UndefinedFunction: ERROR: could not identify an equality operator for type json`
120
+
121
+ If you get the above error while using `distinct: true` that means that
122
+ one of the columns that Ransack is selecting is a `json` column.
123
+ PostgreSQL does not provide comparison operators for the `json` type. While
124
+ it is possible to work around this, in practice it's much better to convert those
125
+ to `jsonb`, as [recommended by the PostgreSQL documentation](https://www.postgresql.org/docs/9.6/static/datatype-json.html).
126
+
127
+ ### Authorization (allowlisting/denylisting)
128
+
129
+ By default, searching and sorting are authorized on any column of your model
130
+ and no class methods/scopes are whitelisted.
131
+
132
+ Ransack adds four methods to `ActiveRecord::Base` that you can redefine as
133
+ class methods in your models to apply selective authorization:
134
+
135
+ - `ransackable_attributes`
136
+ - `ransackable_associations`
137
+ - `ransackable_scopes`
138
+ - `ransortable_attributes`
139
+
140
+ Here is how these four methods are implemented in Ransack:
141
+
142
+ ```ruby
143
+ # `ransackable_attributes` by default returns all column names
144
+ # and any defined ransackers as an array of strings.
145
+ # For overriding with a whitelist array of strings.
146
+ #
147
+ def ransackable_attributes(auth_object = nil)
148
+ column_names + _ransackers.keys
149
+ end
150
+
151
+ # `ransackable_associations` by default returns the names
152
+ # of all associations as an array of strings.
153
+ # For overriding with a whitelist array of strings.
154
+ #
155
+ def ransackable_associations(auth_object = nil)
156
+ reflect_on_all_associations.map { |a| a.name.to_s }
157
+ end
158
+
159
+ # `ransortable_attributes` by default returns the names
160
+ # of all attributes available for sorting as an array of strings.
161
+ # For overriding with a whitelist array of strings.
162
+ #
163
+ def ransortable_attributes(auth_object = nil)
164
+ ransackable_attributes(auth_object)
165
+ end
166
+
167
+ # `ransackable_scopes` by default returns an empty array
168
+ # i.e. no class methods/scopes are authorized.
169
+ # For overriding with a whitelist array of *symbols*.
170
+ #
171
+ def ransackable_scopes(auth_object = nil)
172
+ []
173
+ end
174
+ ```
175
+
176
+ Any values not returned from these methods will be ignored by Ransack, i.e.
177
+ they are not authorized.
178
+
179
+ All four methods can receive a single optional parameter, `auth_object`. When
180
+ you call the search or ransack method on your model, you can provide a value
181
+ for an `auth_object` key in the options hash which can be used by your own
182
+ overridden methods.
183
+
184
+ Here is an example that puts all this together, adapted from
185
+ [this blog post by Ernie Miller](http://erniemiller.org/2012/05/11/why-your-ruby-class-macros-might-suck-mine-did/).
186
+ In an `Article` model, add the following `ransackable_attributes` class method
187
+ (preferably private):
188
+
189
+ ```ruby
190
+ class Article < ActiveRecord::Base
191
+ def self.ransackable_attributes(auth_object = nil)
192
+ if auth_object == :admin
193
+ # whitelist all attributes for admin
194
+ super
195
+ else
196
+ # whitelist only the title and body attributes for other users
197
+ super & %w(title body)
198
+ end
199
+ end
200
+
201
+ private_class_method :ransackable_attributes
202
+ end
203
+ ```
204
+
205
+ Here is example code for the `articles_controller`:
206
+
207
+ ```ruby
208
+ class ArticlesController < ApplicationController
209
+ def index
210
+ @q = Article.ransack(params[:q], auth_object: set_ransack_auth_object)
211
+ @articles = @q.result
212
+ end
213
+
214
+ private
215
+
216
+ def set_ransack_auth_object
217
+ current_user.admin? ? :admin : nil
218
+ end
219
+ end
220
+ ```
221
+
222
+ Trying it out in `rails console`:
223
+
224
+ ```ruby
225
+ > Article
226
+ => Article(id: integer, person_id: integer, title: string, body: text)
227
+
228
+ > Article.ransackable_attributes
229
+ => ["title", "body"]
230
+
231
+ > Article.ransackable_attributes(:admin)
232
+ => ["id", "person_id", "title", "body"]
233
+
234
+ > Article.ransack(id_eq: 1).result.to_sql
235
+ => SELECT "articles".* FROM "articles" # Note that search param was ignored!
236
+
237
+ > Article.ransack({ id_eq: 1 }, { auth_object: nil }).result.to_sql
238
+ => SELECT "articles".* FROM "articles" # Search param still ignored!
239
+
240
+ > Article.ransack({ id_eq: 1 }, { auth_object: :admin }).result.to_sql
241
+ => SELECT "articles".* FROM "articles" WHERE "articles"."id" = 1
242
+ ```
243
+
244
+ That's it! Now you know how to whitelist/blacklist various elements in Ransack.
245
+
246
+ ### Handling unknown predicates or attributes
247
+
248
+ By default, Ransack will ignore any unknown predicates or attributes:
249
+
250
+ ```ruby
251
+ Article.ransack(unknown_attr_eq: 'Ernie').result.to_sql
252
+ => SELECT "articles".* FROM "articles"
253
+ ```
254
+
255
+ Ransack may be configured to raise an error if passed an unknown predicate or
256
+ attributes, by setting the `ignore_unknown_conditions` option to `false` in your
257
+ Ransack initializer file at `config/initializers/ransack.rb`:
258
+
259
+ ```ruby
260
+ Ransack.configure do |c|
261
+ # Raise errors if a query contains an unknown predicate or attribute.
262
+ # Default is true (do not raise error on unknown conditions).
263
+ c.ignore_unknown_conditions = false
264
+ end
265
+ ```
266
+
267
+ ```ruby
268
+ Article.ransack(unknown_attr_eq: 'Ernie')
269
+ # ArgumentError (Invalid search term unknown_attr_eq)
270
+ ```
271
+
272
+ As an alternative to setting a global configuration option, the `.ransack!`
273
+ class method also raises an error if passed an unknown condition:
274
+
275
+ ```ruby
276
+ Article.ransack!(unknown_attr_eq: 'Ernie')
277
+ # ArgumentError: Invalid search term unknown_attr_eq
278
+ ```
279
+
280
+ This is equivalent to the `ignore_unknown_conditions` configuration option,
281
+ except it may be applied on a case-by-case basis.
282
+
283
+ ### Using Scopes/Class Methods
284
+
285
+ Continuing on from the preceding section, searching by scopes requires defining
286
+ a whitelist of `ransackable_scopes` on the model class. The whitelist should be
287
+ an array of *symbols*. By default, all class methods (e.g. scopes) are ignored.
288
+ Scopes will be applied for matching `true` values, or for given values if the
289
+ scope accepts a value:
290
+
291
+ ```ruby
292
+ class Employee < ActiveRecord::Base
293
+ scope :activated, ->(boolean = true) { where(active: boolean) }
294
+ scope :salary_gt, ->(amount) { where('salary > ?', amount) }
295
+
296
+ # Scopes are just syntactical sugar for class methods, which may also be used:
297
+
298
+ def self.hired_since(date)
299
+ where('start_date >= ?', date)
300
+ end
301
+
302
+ def self.ransackable_scopes(auth_object = nil)
303
+ if auth_object.try(:admin?)
304
+ # allow admin users access to all three methods
305
+ %i(activated hired_since salary_gt)
306
+ else
307
+ # allow other users to search on `activated` and `hired_since` only
308
+ %i(activated hired_since)
309
+ end
310
+ end
311
+ end
312
+
313
+ Employee.ransack({ activated: true, hired_since: '2013-01-01' })
314
+
315
+ Employee.ransack({ salary_gt: 100_000 }, { auth_object: current_user })
316
+ ```
317
+
318
+ In Rails 3 and 4, if the `true` value is being passed via url params or some
319
+ other mechanism that will convert it to a string, the true value may not be
320
+ passed to the ransackable scope unless you wrap it in an array
321
+ (i.e. `activated: ['true']`). Ransack will take care of changing 'true' into a
322
+ boolean. This is currently resolved in Rails 5 :smiley:
323
+
324
+ However, perhaps you have `user_id: [1]` and you do not want Ransack to convert
325
+ 1 into a boolean. (Values sanitized to booleans can be found in the
326
+ [constants.rb](https://github.com/activerecord-hackery/ransack/blob/master/lib/ransack/constants.rb#L28)).
327
+ To turn this off globally, and handle type conversions yourself, set
328
+ `sanitize_custom_scope_booleans` to false in an initializer file like
329
+ config/initializers/ransack.rb:
330
+
331
+ ```ruby
332
+ Ransack.configure do |c|
333
+ c.sanitize_custom_scope_booleans = false
334
+ end
335
+ ```
336
+
337
+ To turn this off on a per-scope basis Ransack adds the following method to
338
+ `ActiveRecord::Base` that you can redefine to selectively override sanitization:
339
+
340
+ `ransackable_scopes_skip_sanitize_args`
341
+
342
+ Add the scope you wish to bypass this behavior to ransackable_scopes_skip_sanitize_args:
343
+
344
+ ```ruby
345
+ def self.ransackable_scopes_skip_sanitize_args
346
+ [:scope_to_skip_sanitize_args]
347
+ end
348
+ ```
349
+
350
+ Scopes are a recent addition to Ransack and currently have a few caveats:
351
+ First, a scope involving child associations needs to be defined in the parent
352
+ table model, not in the child model. Second, scopes with an array as an
353
+ argument are not easily usable yet, because the array currently needs to be
354
+ wrapped in an array to function (see
355
+ [this issue](https://github.com/activerecord-hackery/ransack/issues/404)),
356
+ which is not compatible with Ransack form helpers. For this use case, it may be
357
+ better for now to use [ransackers](https://github.com/activerecord-hackery/ransack/wiki/Using-Ransackers) instead,
358
+ where feasible. Pull requests with solutions and tests are welcome!
359
+
360
+ ### Grouping queries by OR instead of AND
361
+
362
+ The default `AND` grouping can be changed to `OR` by adding `m: 'or'` to the
363
+ query hash.
364
+
365
+ You can easily try it in your controller code by changing `params[:q]` in the
366
+ `index` action to `params[:q].try(:merge, m: 'or')` as follows:
367
+
368
+ ```ruby
369
+ def index
370
+ @q = Artist.ransack(params[:q].try(:merge, m: 'or'))
371
+ @artists = @q.result
372
+ end
373
+ ```
374
+ Normally, if you wanted users to be able to toggle between `AND` and `OR`
375
+ query grouping, you would probably set up your search form so that `m` was in
376
+ the URL params hash, but here we assigned `m` manually just to try it out
377
+ quickly.
378
+
379
+ Alternatively, trying it in the Rails console:
380
+
381
+ ```ruby
382
+ artists = Artist.ransack(name_cont: 'foo', style_cont: 'bar', m: 'or')
383
+ => Ransack::Search<class: Artist, base: Grouping <conditions: [
384
+ Condition <attributes: ["name"], predicate: cont, values: ["foo"]>,
385
+ Condition <attributes: ["style"], predicate: cont, values: ["bar"]>
386
+ ], combinator: or>>
387
+
388
+ artists.result.to_sql
389
+ => "SELECT \"artists\".* FROM \"artists\"
390
+ WHERE ((\"artists\".\"name\" ILIKE '%foo%'
391
+ OR \"artists\".\"style\" ILIKE '%bar%'))"
392
+ ```
393
+
394
+ The combinator becomes `or` instead of the default `and`, and the SQL query
395
+ becomes `WHERE...OR` instead of `WHERE...AND`.
396
+
397
+ This works with associations as well. Imagine an Artist model that has many
398
+ Memberships, and many Musicians through Memberships:
399
+
400
+ ```ruby
401
+ artists = Artist.ransack(name_cont: 'foo', musicians_email_cont: 'bar', m: 'or')
402
+ => Ransack::Search<class: Artist, base: Grouping <conditions: [
403
+ Condition <attributes: ["name"], predicate: cont, values: ["foo"]>,
404
+ Condition <attributes: ["musicians_email"], predicate: cont, values: ["bar"]>
405
+ ], combinator: or>>
406
+
407
+ artists.result.to_sql
408
+ => "SELECT \"artists\".* FROM \"artists\"
409
+ LEFT OUTER JOIN \"memberships\"
410
+ ON \"memberships\".\"artist_id\" = \"artists\".\"id\"
411
+ LEFT OUTER JOIN \"musicians\"
412
+ ON \"musicians\".\"id\" = \"memberships\".\"musician_id\"
413
+ WHERE ((\"artists\".\"name\" ILIKE '%foo%'
414
+ OR \"musicians\".\"email\" ILIKE '%bar%'))"
415
+ ```
416
+
417
+ ### Using SimpleForm
418
+
419
+ If you would like to combine the Ransack and SimpleForm form builders, set the
420
+ `RANSACK_FORM_BUILDER` environment variable before Rails boots up, e.g. in
421
+ `config/application.rb` before `require 'rails/all'` as shown below (and add
422
+ `gem 'simple_form'` in your Gemfile).
423
+
424
+ ```ruby
425
+ require File.expand_path('../boot', __FILE__)
426
+ ENV['RANSACK_FORM_BUILDER'] = '::SimpleForm::FormBuilder'
427
+ require 'rails/all'
428
+ ```