ransack 5.0.2 → 6.0.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.
- checksums.yaml +4 -4
- data/README.md +13 -27
- data/lib/ransack/active_record/base.rb +154 -0
- data/lib/ransack/active_record/context.rb +504 -0
- data/lib/ransack/active_record/dialect.rb +83 -0
- data/lib/ransack/active_record/join.rb +75 -0
- data/lib/ransack/active_record/join_association.rb +104 -0
- data/lib/ransack/active_record/join_dependency.rb +123 -0
- data/lib/ransack/active_record/reflection.rb +18 -0
- data/lib/ransack/active_record/tree_node.rb +13 -0
- data/lib/ransack/active_record.rb +44 -9
- data/lib/ransack/configuration.rb +49 -1
- data/lib/ransack/constants.rb +52 -0
- data/lib/ransack/context.rb +159 -35
- data/lib/ransack/helpers/form_builder.rb +30 -25
- data/lib/ransack/nodes/bindable.rb +0 -1
- data/lib/ransack/nodes/condition.rb +215 -73
- data/lib/ransack/nodes/grouping.rb +33 -11
- data/lib/ransack/nodes/sort.rb +1 -1
- data/lib/ransack/nodes/value.rb +8 -1
- data/lib/ransack/predicate.rb +8 -1
- data/lib/ransack/search.rb +52 -9
- data/lib/ransack/version.rb +1 -1
- data/lib/ransack.rb +4 -1
- metadata +11 -13
- data/lib/polyamorous/activerecord/join_association.rb +0 -70
- data/lib/polyamorous/activerecord/join_association_7_2.rb +0 -55
- data/lib/polyamorous/activerecord/join_dependency.rb +0 -102
- data/lib/polyamorous/activerecord/reflection.rb +0 -11
- data/lib/polyamorous/join.rb +0 -70
- data/lib/polyamorous/polyamorous.rb +0 -27
- data/lib/polyamorous/swapping_reflection_class.rb +0 -11
- data/lib/polyamorous/tree_node.rb +0 -7
- data/lib/ransack/adapters/active_record/base.rb +0 -152
- data/lib/ransack/adapters/active_record/context.rb +0 -379
data/lib/ransack/context.rb
CHANGED
|
@@ -4,31 +4,33 @@ module Ransack
|
|
|
4
4
|
class Context
|
|
5
5
|
attr_reader :search, :object, :klass, :base, :engine, :arel_visitor
|
|
6
6
|
attr_accessor :auth_object, :search_key, :ignore_unknown_conditions
|
|
7
|
+
attr_writer :null_sentinel
|
|
8
|
+
|
|
9
|
+
# True when the search was built from `ActionController::Parameters` the
|
|
10
|
+
# controller permitted. A model with no allowlist of its own then lets
|
|
11
|
+
# that permit list be the boundary (see `allowlist` below).
|
|
12
|
+
attr_accessor :permitted
|
|
7
13
|
|
|
8
14
|
class << self
|
|
9
15
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
16
|
+
# An ORM integration registers a block that returns a Context for the
|
|
17
|
+
# objects it understands and nil for anything else. Ransack ships the
|
|
18
|
+
# Active Record integration (see Ransack::ActiveRecord::Context); an
|
|
19
|
+
# integration for another ORM registers its own resolver here.
|
|
20
|
+
def register(&resolver)
|
|
21
|
+
Ransack::Context.resolvers << resolver
|
|
14
22
|
end
|
|
15
23
|
|
|
16
|
-
def
|
|
17
|
-
|
|
18
|
-
when ActiveRecord::Relation
|
|
19
|
-
Adapters::ActiveRecord::Context.new(object.klass, options)
|
|
20
|
-
end
|
|
24
|
+
def resolvers
|
|
25
|
+
@resolvers ||= []
|
|
21
26
|
end
|
|
22
27
|
|
|
23
28
|
def for(object, options = {})
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
end
|
|
30
|
-
context or raise ArgumentError,
|
|
31
|
-
"Don't know what context to use for #{object}"
|
|
29
|
+
Ransack::Context.resolvers.each do |resolver|
|
|
30
|
+
context = resolver.call(object, options)
|
|
31
|
+
return context if context
|
|
32
|
+
end
|
|
33
|
+
raise ArgumentError, "Don't know what context to use for #{object}"
|
|
32
34
|
end
|
|
33
35
|
|
|
34
36
|
end # << self
|
|
@@ -39,15 +41,23 @@ module Ransack
|
|
|
39
41
|
!Ransack.options[:ignore_unknown_conditions] || ignore_unknown_conditions == false
|
|
40
42
|
end
|
|
41
43
|
|
|
44
|
+
# Falls back to the global option unless this search's own option was
|
|
45
|
+
# set, including to `false` to turn the feature off for one search. A
|
|
46
|
+
# Condition captures this when it is built, so a later search reusing
|
|
47
|
+
# this context does not change the conditions of an earlier one.
|
|
48
|
+
def null_sentinel
|
|
49
|
+
@null_sentinel.nil? ? Ransack.options[:null_sentinel] : @null_sentinel
|
|
50
|
+
end
|
|
51
|
+
|
|
42
52
|
def initialize(object, options = {})
|
|
43
53
|
@object = relation_for(object)
|
|
44
54
|
@klass = @object.klass
|
|
45
|
-
@
|
|
46
|
-
@join_type = options[:join_type] || Polyamorous::OuterJoin
|
|
55
|
+
@join_type = options[:join_type] || Arel::Nodes::OuterJoin
|
|
47
56
|
@search_key = options[:search_key] || Ransack.options[:search_key]
|
|
48
57
|
@associations_pot = {}
|
|
49
58
|
@tables_pot = {}
|
|
50
59
|
@lock_associations = []
|
|
60
|
+
@join_dependency = join_dependency(@object)
|
|
51
61
|
|
|
52
62
|
@base = @join_dependency.instance_variable_get(:@join_root)
|
|
53
63
|
end
|
|
@@ -70,14 +80,24 @@ module Ransack
|
|
|
70
80
|
end
|
|
71
81
|
end
|
|
72
82
|
|
|
83
|
+
# The SQL dialect the search will be rendered in, for the few places
|
|
84
|
+
# where generated SQL differs by database. See
|
|
85
|
+
# Ransack::ActiveRecord::Dialect for the questions it answers.
|
|
86
|
+
def dialect
|
|
87
|
+
raise NotImplementedError, "#{self.class} must implement #dialect"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# The model class behind a search object, an association node or a class.
|
|
91
|
+
# Each ORM integration decides what counts as a model, so this is defined
|
|
92
|
+
# on the integration's Context subclass.
|
|
73
93
|
def klassify(obj)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
94
|
+
raise NotImplementedError, "#{self.class} must implement #klassify"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Whether a class can be the base of a search. An ORM integration refines
|
|
98
|
+
# this; by default any class Ransack has been mixed into qualifies.
|
|
99
|
+
def searchable_class?(klass)
|
|
100
|
+
klass.respond_to?(:ransackable_attributes)
|
|
81
101
|
end
|
|
82
102
|
|
|
83
103
|
# Convert a string representing a chain of associations and an attribute
|
|
@@ -87,8 +107,15 @@ module Ransack
|
|
|
87
107
|
table_for(parent)[attr_name]
|
|
88
108
|
end
|
|
89
109
|
|
|
110
|
+
# A bare `false` normally means "this checkbox was not ticked" and the
|
|
111
|
+
# scope is not applied at all. A scope listed in
|
|
112
|
+
# `ransackable_scopes_skip_sanitize_args` has asked to see its values as
|
|
113
|
+
# given, so `false` reaches it like any other value and it can be driven
|
|
114
|
+
# by a yes / no / any select (#1375).
|
|
90
115
|
def chain_scope(scope, args)
|
|
91
|
-
return unless @klass.method(scope)
|
|
116
|
+
return unless @klass.method(scope)
|
|
117
|
+
return if args == false && !ransackable_scope_skip_sanitize_args?(scope, @klass)
|
|
118
|
+
|
|
92
119
|
@object = if scope_arity(scope) < 1 && args == true
|
|
93
120
|
@object.public_send(scope)
|
|
94
121
|
elsif scope_arity(scope) == 1 && args.is_a?(Array)
|
|
@@ -161,6 +188,10 @@ module Ransack
|
|
|
161
188
|
association_parts.join(Constants::UNDERSCORE)
|
|
162
189
|
)
|
|
163
190
|
next unless found_assoc = get_association(assoc, base)
|
|
191
|
+
# A polymorphic association is only complete once its
|
|
192
|
+
# `_of_Model_type` suffix has been consumed; matching on the bare
|
|
193
|
+
# name would ask the reflection for a class it cannot know (#1557).
|
|
194
|
+
next if found_assoc.polymorphic? && klass.nil?
|
|
164
195
|
path += association_parts
|
|
165
196
|
association_parts = []
|
|
166
197
|
base = klassify(klass || found_assoc)
|
|
@@ -171,27 +202,71 @@ module Ransack
|
|
|
171
202
|
end
|
|
172
203
|
|
|
173
204
|
def unpolymorphize_association(str)
|
|
174
|
-
if (match = str.match(/_of_([^_]+?)_type$/))
|
|
175
|
-
|
|
205
|
+
if (match = str.match(/_of_([^_]+?)_type$/)) &&
|
|
206
|
+
(klass = polymorphic_class(match.captures.first))
|
|
207
|
+
[match.pre_match, klass]
|
|
176
208
|
else
|
|
177
209
|
[str, nil]
|
|
178
210
|
end
|
|
179
211
|
end
|
|
180
212
|
|
|
213
|
+
# The class named in an `_of_Model_type` suffix comes from the query
|
|
214
|
+
# string, so it can name anything: a constant that does not exist, a
|
|
215
|
+
# lowercase word, a module, a class with no table, a path through
|
|
216
|
+
# something that is no module at all (`ENV::Person`). None of those is a
|
|
217
|
+
# model to search, so the suffix is then no polymorphic reference at all
|
|
218
|
+
# and the whole key fails as any other unknown attribute does, ignored or
|
|
219
|
+
# raised as `InvalidSearchError` under `ignore_unknown_conditions`.
|
|
220
|
+
# `Kernel.const_get` used to raise `NameError` straight from the URL
|
|
221
|
+
# (#1738).
|
|
222
|
+
def polymorphic_class(name)
|
|
223
|
+
klass = name.safe_constantize
|
|
224
|
+
klass if klass.is_a?(Class) && searchable_class?(klass)
|
|
225
|
+
rescue TypeError
|
|
226
|
+
# `safe_constantize` swallows a missing constant but not a namespace
|
|
227
|
+
# that is not a module, which Ruby reports as a TypeError.
|
|
228
|
+
nil
|
|
229
|
+
end
|
|
230
|
+
|
|
181
231
|
def ransackable_alias(str)
|
|
182
232
|
klass._ransack_aliases.fetch(str, klass._ransack_aliases.fetch(str.to_sym, str))
|
|
183
233
|
end
|
|
184
234
|
|
|
235
|
+
# Expands every `ransack_alias` in a condition name: the whole name, each
|
|
236
|
+
# `_or_` / `_and_` segment of a compound, and an alias defined on an
|
|
237
|
+
# associated model and reached through an association path
|
|
238
|
+
# (`person_term` when Person aliases `term`). An alias that expands to a
|
|
239
|
+
# compound is joined with that compound's own combinator, so
|
|
240
|
+
# `term_or_daddy` with `term => name_or_email` becomes
|
|
241
|
+
# `name_or_email_or_parent_name`. Returns the name unchanged when it
|
|
242
|
+
# holds no alias.
|
|
243
|
+
def resolve_aliases(str)
|
|
244
|
+
return str.to_s unless key_within_depth_limit?(str)
|
|
245
|
+
|
|
246
|
+
whole = resolve_alias_segment(str)
|
|
247
|
+
return whole if whole != str
|
|
248
|
+
# A name that is an attribute in its own right is never split, even if
|
|
249
|
+
# it contains a combinator (a column called `true_or_false`).
|
|
250
|
+
return str if attribute_method?(str)
|
|
251
|
+
|
|
252
|
+
segments = str.split(/_and_|_or_/)
|
|
253
|
+
resolved = segments.map { |segment| resolve_alias_segment(segment) }
|
|
254
|
+
return str if resolved == segments
|
|
255
|
+
|
|
256
|
+
combinator = str[/_(or|and)_/, 1] || Constants::OR
|
|
257
|
+
resolved.join("_#{combinator}_")
|
|
258
|
+
end
|
|
259
|
+
|
|
185
260
|
def ransackable_attribute?(str, klass)
|
|
186
|
-
klass
|
|
261
|
+
attributes_for(klass).any? { |s| s.to_sym == str.to_sym }
|
|
187
262
|
end
|
|
188
263
|
|
|
189
264
|
def ransortable_attribute?(str, klass)
|
|
190
|
-
klass
|
|
265
|
+
sortable_attributes_for(klass).any? { |s| s.to_sym == str.to_sym }
|
|
191
266
|
end
|
|
192
267
|
|
|
193
268
|
def ransackable_association?(str, klass)
|
|
194
|
-
klass
|
|
269
|
+
associations_for(klass).any? { |s| s.to_sym == str.to_sym }
|
|
195
270
|
end
|
|
196
271
|
|
|
197
272
|
def ransackable_scope?(str, klass)
|
|
@@ -203,19 +278,68 @@ module Ransack
|
|
|
203
278
|
end
|
|
204
279
|
|
|
205
280
|
def searchable_attributes(str = ''.freeze)
|
|
206
|
-
traverse(str)
|
|
281
|
+
attributes_for(traverse(str))
|
|
207
282
|
end
|
|
208
283
|
|
|
209
284
|
def sortable_attributes(str = ''.freeze)
|
|
210
|
-
traverse(str)
|
|
285
|
+
sortable_attributes_for(traverse(str))
|
|
211
286
|
end
|
|
212
287
|
|
|
213
288
|
def searchable_associations(str = ''.freeze)
|
|
214
|
-
traverse(str)
|
|
289
|
+
associations_for(traverse(str))
|
|
215
290
|
end
|
|
216
291
|
|
|
217
292
|
private
|
|
218
293
|
|
|
294
|
+
def attributes_for(klass)
|
|
295
|
+
allowlist(klass, :ransackable_attributes) { klass.authorizable_ransackable_attributes }
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# `ransortable_attributes` defaults to `ransackable_attributes`, so a
|
|
299
|
+
# model that defines either one has a sort list of its own.
|
|
300
|
+
def sortable_attributes_for(klass)
|
|
301
|
+
allowlist(klass, :ransortable_attributes, :ransackable_attributes) do
|
|
302
|
+
klass.authorizable_ransackable_attributes
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def associations_for(klass)
|
|
307
|
+
allowlist(klass, :ransackable_associations) { klass.authorizable_ransackable_associations }
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# The allowlist a search consults on `klass`. A model that defines its
|
|
311
|
+
# own `method` is always asked, so its list applies on top of whatever
|
|
312
|
+
# the controller permitted. A model that defines none has left the
|
|
313
|
+
# decision to the controller: when the parameters were permitted, the
|
|
314
|
+
# block's list of everything that exists is used, and otherwise the
|
|
315
|
+
# model's default runs, which raises asking for a list (#1403).
|
|
316
|
+
def allowlist(klass, method, *fallbacks)
|
|
317
|
+
if permitted && [method, *fallbacks].none? { |m| klass.ransackable_list_defined?(m) }
|
|
318
|
+
yield
|
|
319
|
+
else
|
|
320
|
+
klass.public_send(method, auth_object)
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def resolve_alias_segment(segment)
|
|
325
|
+
target = ransackable_alias(segment)
|
|
326
|
+
return target if target != segment
|
|
327
|
+
|
|
328
|
+
path = association_path(segment)
|
|
329
|
+
return segment if path.blank?
|
|
330
|
+
|
|
331
|
+
remainder = segment.delete_prefix("#{path}_")
|
|
332
|
+
associated = traverse(path)
|
|
333
|
+
target = associated._ransack_aliases.fetch(remainder, associated._ransack_aliases.fetch(remainder.to_sym, remainder))
|
|
334
|
+
return segment if target == remainder
|
|
335
|
+
# The target may itself be one attribute whose name contains a
|
|
336
|
+
# combinator (`terms_and_conditions`); it is prefixed whole.
|
|
337
|
+
return "#{path}_#{target}" if ransackable_attribute?(target, associated)
|
|
338
|
+
|
|
339
|
+
# Prefix every attribute of the alias's target with the path.
|
|
340
|
+
target.gsub(/(\A|_and_|_or_)/) { "#{$1}#{path}_" }
|
|
341
|
+
end
|
|
342
|
+
|
|
219
343
|
def cast_scope_args(args)
|
|
220
344
|
if args.is_a?(Array)
|
|
221
345
|
args = args.map(&method(:cast_scope_args))
|
|
@@ -112,7 +112,6 @@ module Ransack
|
|
|
112
112
|
|
|
113
113
|
def predicate_select(options = {}, html_options = {})
|
|
114
114
|
options[:compounds] = true if options[:compounds].nil?
|
|
115
|
-
options[:selected] ||= nil
|
|
116
115
|
default = options.delete(:default) || Constants::CONT
|
|
117
116
|
|
|
118
117
|
keys =
|
|
@@ -134,6 +133,7 @@ module Ransack
|
|
|
134
133
|
collection = keys.map { |k| [k, Translate.predicate(k)] }
|
|
135
134
|
object.predicate ||= Predicate.named(default) if
|
|
136
135
|
can_use_default?(default, :predicate, keys)
|
|
136
|
+
options[:selected] = selected_predicate unless options.key?(:selected)
|
|
137
137
|
template_collection_select(:p, collection, options, html_options)
|
|
138
138
|
end
|
|
139
139
|
|
|
@@ -144,17 +144,25 @@ module Ransack
|
|
|
144
144
|
|
|
145
145
|
private
|
|
146
146
|
|
|
147
|
+
# Rails reads a select's current value back through the field name, which
|
|
148
|
+
# for the predicate select is +p+. Only a Condition has a public +p+, and
|
|
149
|
+
# +Kernel#p+ is private, so a builder for a Search or Grouping would
|
|
150
|
+
# raise instead of finding nothing. Resolve the value here instead.
|
|
151
|
+
def selected_predicate
|
|
152
|
+
object.predicate_name if object.respond_to?(:predicate_name)
|
|
153
|
+
end
|
|
154
|
+
|
|
147
155
|
def template_grouped_collection_select(collection, options, html_options)
|
|
148
156
|
@template.grouped_collection_select(
|
|
149
157
|
@object_name, :name, collection, :last, :first, :first, :last,
|
|
150
|
-
objectify_options(options), @
|
|
158
|
+
objectify_options(options), @default_html_options.merge(html_options)
|
|
151
159
|
)
|
|
152
160
|
end
|
|
153
161
|
|
|
154
162
|
def template_collection_select(name, collection, options, html_options)
|
|
155
163
|
@template.collection_select(
|
|
156
164
|
@object_name, name, collection, :first, :last,
|
|
157
|
-
objectify_options(options), @
|
|
165
|
+
objectify_options(options), @default_html_options.merge(html_options)
|
|
158
166
|
)
|
|
159
167
|
end
|
|
160
168
|
|
|
@@ -188,31 +196,28 @@ module Ransack
|
|
|
188
196
|
end
|
|
189
197
|
end
|
|
190
198
|
|
|
199
|
+
# Expands the +associations:+ option of +attribute_select+ into the flat
|
|
200
|
+
# list of association paths ransack understands. A String or Symbol
|
|
201
|
+
# names one association; an Array lists several; a Hash reaches through
|
|
202
|
+
# an association to its own associations, so +{ articles: :comments }+
|
|
203
|
+
# yields +articles+ and +articles_comments+. Shapes nest freely.
|
|
191
204
|
def association_array(obj, prefix = nil)
|
|
192
|
-
(
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
case obj
|
|
199
|
-
when Array
|
|
200
|
-
obj
|
|
201
|
-
when Hash
|
|
202
|
-
association_hash(obj)
|
|
203
|
-
else
|
|
204
|
-
[obj]
|
|
205
|
-
end
|
|
206
|
-
end
|
|
207
|
-
|
|
208
|
-
def association_hash(obj)
|
|
209
|
-
obj.map do |key, value|
|
|
210
|
-
case value
|
|
211
|
-
when Array, Hash
|
|
212
|
-
association_array(value, key.to_s)
|
|
205
|
+
Array.wrap(obj).flat_map do |item|
|
|
206
|
+
case item
|
|
207
|
+
when Hash
|
|
208
|
+
association_hash(item, prefix)
|
|
209
|
+
when nil
|
|
210
|
+
[]
|
|
213
211
|
else
|
|
214
|
-
[
|
|
212
|
+
[[prefix, item].compact.join(Constants::UNDERSCORE)]
|
|
215
213
|
end
|
|
214
|
+
end.uniq
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def association_hash(obj, prefix = nil)
|
|
218
|
+
obj.flat_map do |key, value|
|
|
219
|
+
path = [prefix, key].compact.join(Constants::UNDERSCORE)
|
|
220
|
+
[path] + association_array(value, path)
|
|
216
221
|
end
|
|
217
222
|
end
|
|
218
223
|
|