ransack 4.4.1 → 5.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 +2 -1
- data/lib/ransack/adapters/active_record/context.rb +12 -10
- data/lib/ransack/configuration.rb +38 -5
- data/lib/ransack/constants.rb +36 -11
- data/lib/ransack/context.rb +36 -4
- data/lib/ransack/helpers/form_builder.rb +1 -17
- data/lib/ransack/helpers/form_helper.rb +7 -7
- data/lib/ransack/nodes/attribute.rb +11 -2
- data/lib/ransack/nodes/condition.rb +106 -30
- data/lib/ransack/nodes/grouping.rb +23 -5
- data/lib/ransack/nodes/node.rb +13 -0
- data/lib/ransack/nodes/sort.rb +1 -2
- data/lib/ransack/nodes/value.rb +3 -1
- data/lib/ransack/predicate.rb +24 -3
- data/lib/ransack/search.rb +65 -22
- data/lib/ransack/version.rb +1 -1
- metadata +4 -60
- data/spec/console.rb +0 -13
- data/spec/factories/articles.rb +0 -7
- data/spec/factories/comments.rb +0 -7
- data/spec/factories/notes.rb +0 -13
- data/spec/factories/people.rb +0 -10
- data/spec/factories/tags.rb +0 -5
- data/spec/helpers/polyamorous_helper.rb +0 -13
- data/spec/helpers/ransack_helper.rb +0 -9
- data/spec/polyamorous/activerecord_compatibility_spec.rb +0 -15
- data/spec/polyamorous/join_association_spec.rb +0 -29
- data/spec/polyamorous/join_dependency_spec.rb +0 -80
- data/spec/polyamorous/join_spec.rb +0 -19
- data/spec/ransack/adapters/active_record/base_spec.rb +0 -943
- data/spec/ransack/adapters/active_record/context_spec.rb +0 -219
- data/spec/ransack/configuration_spec.rb +0 -201
- data/spec/ransack/helpers/form_builder_spec.rb +0 -169
- data/spec/ransack/helpers/form_helper_spec.rb +0 -1075
- data/spec/ransack/invalid_search_error_spec.rb +0 -27
- data/spec/ransack/nodes/condition_spec.rb +0 -333
- data/spec/ransack/nodes/grouping_spec.rb +0 -111
- data/spec/ransack/nodes/value_spec.rb +0 -126
- data/spec/ransack/predicate_spec.rb +0 -504
- data/spec/ransack/ransacker_spec.rb +0 -69
- data/spec/ransack/search_spec.rb +0 -821
- data/spec/ransack/translate_spec.rb +0 -16
- data/spec/spec_helper.rb +0 -52
- data/spec/support/en.yml +0 -18
- data/spec/support/schema.rb +0 -419
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0251527dc22cca8cb55ff672b598d74fef0ec4c2b1c6336cb0b8d52d69dabafc
|
|
4
|
+
data.tar.gz: fd218c05d4697cca5b9a8b4111af7a5fa4ddc559071e9a3194aca2f2b2da6c9d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bc524a7f9a4fac90d6f2e3b9818ccf76bab0f44c872c33db043f049f759f2641c03b338c2cfa28c70a20f6d1d2c4569390e43ec9e5da9daf2537002c519ad3fc
|
|
7
|
+
data.tar.gz: 3a599c3c9472f0033976ed0301cc0545b62711cccb36817db373c0f19ba0b828640a89bc3784b533346633112fb017353c63c490fa792b3eb3e7aea82f41e527
|
data/README.md
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/activerecord-hackery/ransack/actions)
|
|
4
4
|
[](http://badge.fury.io/rb/ransack)
|
|
5
|
+
[](https://deepwiki.com/activerecord-hackery/ransack)
|
|
5
6
|
[](https://codeclimate.com/github/activerecord-hackery/ransack)
|
|
6
7
|
[](#backers) [](#sponsors)
|
|
7
8
|
|
|
@@ -13,7 +14,7 @@ There are advanced searching solutions around, like ElasticSearch or Algolia. **
|
|
|
13
14
|
|
|
14
15
|
Ready to move beyond the basics? Use **advanced features** like i18n and extensive configuration options.
|
|
15
16
|
|
|
16
|
-
Ransack is supported for Rails 8.0, 7.2 on Ruby 3.1 and later.
|
|
17
|
+
Ransack is supported for Rails 8.1, 8.0, 7.2 on Ruby 3.1 and later.
|
|
17
18
|
|
|
18
19
|
## Installation
|
|
19
20
|
|
|
@@ -15,7 +15,7 @@ module Ransack
|
|
|
15
15
|
relation = attr.arel_attribute.relation
|
|
16
16
|
name = attr.arel_attribute.name.to_s
|
|
17
17
|
table = relation.respond_to?(:table_name) ? relation.table_name : relation.name
|
|
18
|
-
schema_cache = self.klass.
|
|
18
|
+
schema_cache = self.klass.connection_pool.schema_cache
|
|
19
19
|
unless schema_cache.send(:data_source_exists?, table)
|
|
20
20
|
raise "No table named #{table} exists."
|
|
21
21
|
end
|
|
@@ -44,15 +44,15 @@ module Ransack
|
|
|
44
44
|
if scope_or_sort.is_a?(Symbol)
|
|
45
45
|
relation = relation.send(scope_or_sort)
|
|
46
46
|
else
|
|
47
|
-
case Ransack.options[:
|
|
47
|
+
case Ransack.options[:fields_sort_option]
|
|
48
48
|
when :nulls_first
|
|
49
|
-
scope_or_sort = scope_or_sort.direction == :asc ?
|
|
49
|
+
scope_or_sort = scope_or_sort.direction == :asc ? scope_or_sort.nulls_first : scope_or_sort.nulls_last
|
|
50
50
|
when :nulls_last
|
|
51
|
-
scope_or_sort = scope_or_sort.direction == :asc ?
|
|
51
|
+
scope_or_sort = scope_or_sort.direction == :asc ? scope_or_sort.nulls_last : scope_or_sort.nulls_first
|
|
52
52
|
when :nulls_always_first
|
|
53
|
-
scope_or_sort =
|
|
53
|
+
scope_or_sort = scope_or_sort.nulls_first
|
|
54
54
|
when :nulls_always_last
|
|
55
|
-
scope_or_sort =
|
|
55
|
+
scope_or_sort = scope_or_sort.nulls_last
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
relation = relation.order(scope_or_sort)
|
|
@@ -65,7 +65,8 @@ module Ransack
|
|
|
65
65
|
|
|
66
66
|
def attribute_method?(str, klass = @klass)
|
|
67
67
|
exists = false
|
|
68
|
-
if ransackable_attribute?(str, klass)
|
|
68
|
+
if ransackable_attribute?(str, klass) ||
|
|
69
|
+
ransortable_attribute?(str, klass)
|
|
69
70
|
exists = true
|
|
70
71
|
elsif (segments = str.split(Constants::UNDERSCORE)).size > 1
|
|
71
72
|
remainder = []
|
|
@@ -212,9 +213,9 @@ module Ransack
|
|
|
212
213
|
end
|
|
213
214
|
when Arel::Nodes::Equality
|
|
214
215
|
pk = primary_key
|
|
215
|
-
if join_root.left
|
|
216
|
+
if join_root.left.eql?(pk)
|
|
216
217
|
join_root.right
|
|
217
|
-
elsif join_root.right
|
|
218
|
+
elsif join_root.right.eql?(pk)
|
|
218
219
|
join_root.left
|
|
219
220
|
else
|
|
220
221
|
nil
|
|
@@ -239,7 +240,8 @@ module Ransack
|
|
|
239
240
|
def get_parent_and_attribute_name(str, parent = @base)
|
|
240
241
|
attr_name = nil
|
|
241
242
|
|
|
242
|
-
if ransackable_attribute?(str, klassify(parent))
|
|
243
|
+
if ransackable_attribute?(str, klassify(parent)) ||
|
|
244
|
+
ransortable_attribute?(str, klassify(parent))
|
|
243
245
|
attr_name = str
|
|
244
246
|
elsif (segments = str.split(Constants::UNDERSCORE)).size > 1
|
|
245
247
|
remainder = []
|
|
@@ -34,8 +34,9 @@ module Ransack
|
|
|
34
34
|
down_arrow: '▲'.freeze,
|
|
35
35
|
default_arrow: nil,
|
|
36
36
|
sanitize_scope_args: true,
|
|
37
|
-
|
|
38
|
-
strip_whitespace: true
|
|
37
|
+
fields_sort_option: nil,
|
|
38
|
+
strip_whitespace: true,
|
|
39
|
+
ignore_blank_values: true
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
def configure
|
|
@@ -162,13 +163,24 @@ module Ransack
|
|
|
162
163
|
# User may want to configure it like this:
|
|
163
164
|
#
|
|
164
165
|
# Ransack.configure do |c|
|
|
165
|
-
# c.
|
|
166
|
+
# c.fields_sort_option = :nulls_first # or e.g. :nulls_always_last
|
|
166
167
|
# end
|
|
167
168
|
#
|
|
168
|
-
#
|
|
169
|
+
# Emitted through Arel's `nulls_first` / `nulls_last`, so it works on any
|
|
170
|
+
# backend Arel supports it for. MySQL has no NULLS FIRST / LAST syntax and
|
|
171
|
+
# Arel does not emulate it, so this option does not apply there.
|
|
169
172
|
#
|
|
173
|
+
# See https://www.postgresql.org/docs/current/queries-order.html
|
|
174
|
+
#
|
|
175
|
+
def fields_sort_option=(setting)
|
|
176
|
+
self.options[:fields_sort_option] = setting
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Renamed to `fields_sort_option` now that NULLS FIRST / NULLS LAST is
|
|
180
|
+
# emitted through Arel and is no longer PostgreSQL-specific. The old name
|
|
181
|
+
# still works so existing initializers keep running.
|
|
170
182
|
def postgres_fields_sort_option=(setting)
|
|
171
|
-
self.
|
|
183
|
+
self.fields_sort_option = setting
|
|
172
184
|
end
|
|
173
185
|
|
|
174
186
|
# By default, Ransack displays sort order indicator arrows in sort links.
|
|
@@ -197,6 +209,27 @@ module Ransack
|
|
|
197
209
|
self.options[:strip_whitespace] = boolean
|
|
198
210
|
end
|
|
199
211
|
|
|
212
|
+
# By default, Ransack ignores search conditions whose value is blank — an
|
|
213
|
+
# empty string, or an array containing only blank values. This is what makes
|
|
214
|
+
# a search form submitted with empty fields return every record rather than
|
|
215
|
+
# none, and it is almost always what an HTML form wants.
|
|
216
|
+
#
|
|
217
|
+
# Set this to false to treat a blank value as a value to search *for*:
|
|
218
|
+
# `name_eq: ''` then generates `WHERE name = ''`, and `id_in: []` generates a
|
|
219
|
+
# condition matching nothing rather than being dropped. This is usually what
|
|
220
|
+
# a JSON API wants, where an empty value is an explicit filter rather than an
|
|
221
|
+
# untouched form field.
|
|
222
|
+
#
|
|
223
|
+
# A `nil` value is ignored either way.
|
|
224
|
+
#
|
|
225
|
+
# Ransack.configure do |config|
|
|
226
|
+
# config.ignore_blank_values = false
|
|
227
|
+
# end
|
|
228
|
+
#
|
|
229
|
+
def ignore_blank_values=(boolean)
|
|
230
|
+
self.options[:ignore_blank_values] = boolean
|
|
231
|
+
end
|
|
232
|
+
|
|
200
233
|
def arel_predicate_with_suffix(arel_predicate, suffix)
|
|
201
234
|
if arel_predicate === Proc
|
|
202
235
|
proc { |v| "#{arel_predicate.call(v)}#{suffix}" }
|
data/lib/ransack/constants.rb
CHANGED
|
@@ -155,22 +155,47 @@ module Ransack
|
|
|
155
155
|
type: :boolean,
|
|
156
156
|
validator: proc { |v| BOOLEAN_VALUES.include?(v) },
|
|
157
157
|
formatter: proc { |v| nil } }
|
|
158
|
+
],
|
|
159
|
+
['length_eq'.freeze, {
|
|
160
|
+
arel_predicate: 'eq'.freeze,
|
|
161
|
+
type: :integer
|
|
162
|
+
}
|
|
163
|
+
],
|
|
164
|
+
['length_lt'.freeze, {
|
|
165
|
+
arel_predicate: 'lt'.freeze,
|
|
166
|
+
type: :integer
|
|
167
|
+
}
|
|
168
|
+
],
|
|
169
|
+
['length_lteq'.freeze, {
|
|
170
|
+
arel_predicate: 'lteq'.freeze,
|
|
171
|
+
type: :integer
|
|
172
|
+
}
|
|
173
|
+
],
|
|
174
|
+
['length_gt'.freeze, {
|
|
175
|
+
arel_predicate: 'gt'.freeze,
|
|
176
|
+
type: :integer
|
|
177
|
+
}
|
|
178
|
+
],
|
|
179
|
+
['length_gteq'.freeze, {
|
|
180
|
+
arel_predicate: 'gteq'.freeze,
|
|
181
|
+
type: :integer
|
|
182
|
+
}
|
|
158
183
|
]
|
|
159
184
|
].freeze
|
|
160
185
|
|
|
186
|
+
# The character used to escape LIKE wildcards. Emitted as an explicit
|
|
187
|
+
# `ESCAPE` clause alongside every LIKE / NOT LIKE predicate, so the
|
|
188
|
+
# behaviour does not depend on a backend's default escape character —
|
|
189
|
+
# MySQL and PostgreSQL default to backslash, SQLite has no default at all.
|
|
190
|
+
LIKE_ESCAPE_CHARACTER = '\\'.freeze
|
|
191
|
+
|
|
161
192
|
module_function
|
|
162
|
-
#
|
|
193
|
+
# Escapes the LIKE wildcards `%` and `_`, and the escape character itself,
|
|
194
|
+
# so they are matched literally. Paired with the `ESCAPE` clause added in
|
|
195
|
+
# `Condition#format_predicate`; escaping without it silently does nothing
|
|
196
|
+
# on SQLite. See https://github.com/activerecord-hackery/ransack/issues/1581
|
|
163
197
|
def escape_wildcards(unescaped)
|
|
164
|
-
|
|
165
|
-
when "Mysql2".freeze
|
|
166
|
-
# Necessary for MySQL
|
|
167
|
-
unescaped.to_s.gsub(/([\\%_])/, '\\\\\\1')
|
|
168
|
-
when "PostGIS".freeze, "PostgreSQL".freeze
|
|
169
|
-
# Necessary for PostgreSQL
|
|
170
|
-
unescaped.to_s.gsub(/([\\%_.])/, '\\\\\\1')
|
|
171
|
-
else
|
|
172
|
-
unescaped
|
|
173
|
-
end
|
|
198
|
+
unescaped.to_s.gsub(/([\\%_])/) { "#{LIKE_ESCAPE_CHARACTER}#{$1}" }
|
|
174
199
|
end
|
|
175
200
|
end
|
|
176
201
|
end
|
data/lib/ransack/context.rb
CHANGED
|
@@ -3,8 +3,7 @@ require 'ransack/visitor'
|
|
|
3
3
|
module Ransack
|
|
4
4
|
class Context
|
|
5
5
|
attr_reader :search, :object, :klass, :base, :engine, :arel_visitor
|
|
6
|
-
attr_accessor :auth_object, :search_key
|
|
7
|
-
attr_reader :arel_visitor
|
|
6
|
+
attr_accessor :auth_object, :search_key, :ignore_unknown_conditions
|
|
8
7
|
|
|
9
8
|
class << self
|
|
10
9
|
|
|
@@ -34,6 +33,12 @@ module Ransack
|
|
|
34
33
|
|
|
35
34
|
end # << self
|
|
36
35
|
|
|
36
|
+
# Unknown attributes, predicates and combinators raise rather than being
|
|
37
|
+
# ignored when either the global option or this search's own option says so.
|
|
38
|
+
def strict_conditions?
|
|
39
|
+
!Ransack.options[:ignore_unknown_conditions] || ignore_unknown_conditions == false
|
|
40
|
+
end
|
|
41
|
+
|
|
37
42
|
def initialize(object, options = {})
|
|
38
43
|
@object = relation_for(object)
|
|
39
44
|
@klass = @object.klass
|
|
@@ -89,6 +94,14 @@ module Ransack
|
|
|
89
94
|
@klass.method(scope).arity
|
|
90
95
|
end
|
|
91
96
|
|
|
97
|
+
def sanitize_scope_args(key, args)
|
|
98
|
+
if Ransack.options[:sanitize_scope_args] && !ransackable_scope_skip_sanitize_args?(key, object)
|
|
99
|
+
cast_scope_args(args)
|
|
100
|
+
else
|
|
101
|
+
args
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
92
105
|
def bind(object, str)
|
|
93
106
|
return nil unless str
|
|
94
107
|
object.parent, object.attr_name = bind_pair_for(str)
|
|
@@ -159,8 +172,11 @@ module Ransack
|
|
|
159
172
|
end
|
|
160
173
|
|
|
161
174
|
def ransackable_attribute?(str, klass)
|
|
162
|
-
klass.ransackable_attributes(auth_object).any? { |s| s.to_sym == str.to_sym }
|
|
163
|
-
|
|
175
|
+
klass.ransackable_attributes(auth_object).any? { |s| s.to_sym == str.to_sym }
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def ransortable_attribute?(str, klass)
|
|
179
|
+
klass.ransortable_attributes(auth_object).any? { |s| s.to_sym == str.to_sym }
|
|
164
180
|
end
|
|
165
181
|
|
|
166
182
|
def ransackable_association?(str, klass)
|
|
@@ -186,5 +202,21 @@ module Ransack
|
|
|
186
202
|
def searchable_associations(str = ''.freeze)
|
|
187
203
|
traverse(str).ransackable_associations(auth_object)
|
|
188
204
|
end
|
|
205
|
+
|
|
206
|
+
private
|
|
207
|
+
|
|
208
|
+
def cast_scope_args(args)
|
|
209
|
+
if args.is_a?(Array)
|
|
210
|
+
args = args.map(&method(:cast_scope_args))
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
if Constants::TRUE_VALUES.include? args
|
|
214
|
+
true
|
|
215
|
+
elsif Constants::FALSE_VALUES.include? args
|
|
216
|
+
false
|
|
217
|
+
else
|
|
218
|
+
args
|
|
219
|
+
end
|
|
220
|
+
end
|
|
189
221
|
end
|
|
190
222
|
end
|
|
@@ -1,22 +1,5 @@
|
|
|
1
1
|
require 'action_view'
|
|
2
2
|
|
|
3
|
-
module ActionView::Helpers::Tags
|
|
4
|
-
# TODO: Find a better way to solve this issue!
|
|
5
|
-
# This patch is needed since this Rails commit:
|
|
6
|
-
# https://github.com/rails/rails/commit/c1a118a
|
|
7
|
-
class Base
|
|
8
|
-
private
|
|
9
|
-
|
|
10
|
-
def value
|
|
11
|
-
if @allow_method_names_outside_object
|
|
12
|
-
object.send @method_name if object && object.respond_to?(@method_name, true)
|
|
13
|
-
else
|
|
14
|
-
object.send @method_name if object
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
|
|
20
3
|
RANSACK_FORM_BUILDER = 'RANSACK_FORM_BUILDER'.freeze
|
|
21
4
|
|
|
22
5
|
require 'simple_form' if
|
|
@@ -129,6 +112,7 @@ module Ransack
|
|
|
129
112
|
|
|
130
113
|
def predicate_select(options = {}, html_options = {})
|
|
131
114
|
options[:compounds] = true if options[:compounds].nil?
|
|
115
|
+
options[:selected] ||= nil
|
|
132
116
|
default = options.delete(:default) || Constants::CONT
|
|
133
117
|
|
|
134
118
|
keys =
|
|
@@ -10,7 +10,7 @@ module Ransack
|
|
|
10
10
|
search = extract_search_and_set_url(record, options, 'search_form_for')
|
|
11
11
|
options[:html] ||= {}
|
|
12
12
|
html_options = build_html_options(search, options, :get)
|
|
13
|
-
finalize_form_options(options, html_options)
|
|
13
|
+
finalize_form_options(search, options, html_options)
|
|
14
14
|
form_for(record, options, &proc)
|
|
15
15
|
end
|
|
16
16
|
|
|
@@ -30,7 +30,7 @@ module Ransack
|
|
|
30
30
|
search = extract_search_and_set_url(record, options, 'search_form_with')
|
|
31
31
|
options[:html] ||= {}
|
|
32
32
|
html_options = build_html_options(search, options, :get)
|
|
33
|
-
finalize_form_with_options(options, html_options)
|
|
33
|
+
finalize_form_with_options(search, options, html_options)
|
|
34
34
|
form_with(model: search, **options, &proc)
|
|
35
35
|
end
|
|
36
36
|
|
|
@@ -48,7 +48,7 @@ module Ransack
|
|
|
48
48
|
turbo_options = build_turbo_options(options)
|
|
49
49
|
method = options.delete(:method) || :post
|
|
50
50
|
html_options = build_html_options(search, options, method).merge(turbo_options)
|
|
51
|
-
finalize_form_options(options, html_options)
|
|
51
|
+
finalize_form_options(search, options, html_options)
|
|
52
52
|
form_for(record, options, &proc)
|
|
53
53
|
end
|
|
54
54
|
|
|
@@ -122,14 +122,14 @@ module Ransack
|
|
|
122
122
|
}
|
|
123
123
|
end
|
|
124
124
|
|
|
125
|
-
def finalize_form_options(options, html_options)
|
|
126
|
-
options[:as] ||=
|
|
125
|
+
def finalize_form_options(search, options, html_options)
|
|
126
|
+
options[:as] ||= search.context.search_key
|
|
127
127
|
options[:html].reverse_merge!(html_options)
|
|
128
128
|
options[:builder] ||= FormBuilder
|
|
129
129
|
end
|
|
130
130
|
|
|
131
|
-
def finalize_form_with_options(options, html_options)
|
|
132
|
-
options[:scope] ||=
|
|
131
|
+
def finalize_form_with_options(search, options, html_options)
|
|
132
|
+
options[:scope] ||= search.context.search_key
|
|
133
133
|
options[:html].reverse_merge!(html_options)
|
|
134
134
|
options[:builder] ||= FormBuilder
|
|
135
135
|
end
|
|
@@ -20,8 +20,7 @@ module Ransack
|
|
|
20
20
|
|
|
21
21
|
def valid?
|
|
22
22
|
bound? && attr &&
|
|
23
|
-
context.
|
|
24
|
-
.include?(attr_name.split('.').last)
|
|
23
|
+
context.ransackable_attribute?(attr_name.split('.').last, context.klassify(parent))
|
|
25
24
|
end
|
|
26
25
|
|
|
27
26
|
def associated_collection?
|
|
@@ -31,6 +30,8 @@ module Ransack
|
|
|
31
30
|
def type
|
|
32
31
|
if ransacker
|
|
33
32
|
ransacker.type
|
|
33
|
+
elsif enum?
|
|
34
|
+
:enum
|
|
34
35
|
else
|
|
35
36
|
context.type_for(self)
|
|
36
37
|
end
|
|
@@ -54,6 +55,14 @@ module Ransack
|
|
|
54
55
|
"Attribute <#{name}>"
|
|
55
56
|
end
|
|
56
57
|
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def enum?
|
|
61
|
+
bound? &&
|
|
62
|
+
klass.respond_to?(:defined_enums) &&
|
|
63
|
+
klass.defined_enums.key?(attr_name.to_s)
|
|
64
|
+
end
|
|
65
|
+
|
|
57
66
|
end
|
|
58
67
|
end
|
|
59
68
|
end
|
|
@@ -81,8 +81,13 @@ module Ransack
|
|
|
81
81
|
def attributes=(args)
|
|
82
82
|
case args
|
|
83
83
|
when Array
|
|
84
|
-
args.each do |
|
|
85
|
-
|
|
84
|
+
args.each do |attr|
|
|
85
|
+
if attr.is_a?(Hash) && (attr.key?(:name) || attr.key?("name"))
|
|
86
|
+
attr = attr.with_indifferent_access
|
|
87
|
+
build_attribute(attr[:name], attr[:ransacker_args])
|
|
88
|
+
else
|
|
89
|
+
build_attribute(attr)
|
|
90
|
+
end
|
|
86
91
|
end
|
|
87
92
|
when Hash
|
|
88
93
|
args.each do |index, attrs|
|
|
@@ -104,8 +109,10 @@ module Ransack
|
|
|
104
109
|
case args
|
|
105
110
|
when Array
|
|
106
111
|
args.each do |val|
|
|
107
|
-
val
|
|
108
|
-
|
|
112
|
+
if val.is_a?(Hash) && (val.key?(:value) || val.key?("value"))
|
|
113
|
+
val = val.with_indifferent_access[:value]
|
|
114
|
+
end
|
|
115
|
+
self.values << Value.new(@context, val)
|
|
109
116
|
end
|
|
110
117
|
when Hash
|
|
111
118
|
args.each do |index, attrs|
|
|
@@ -124,8 +131,9 @@ module Ransack
|
|
|
124
131
|
end
|
|
125
132
|
|
|
126
133
|
def combinator=(val)
|
|
127
|
-
|
|
134
|
+
super
|
|
128
135
|
end
|
|
136
|
+
|
|
129
137
|
alias :m= :combinator=
|
|
130
138
|
alias :m :combinator
|
|
131
139
|
|
|
@@ -171,10 +179,17 @@ module Ransack
|
|
|
171
179
|
end
|
|
172
180
|
end
|
|
173
181
|
|
|
182
|
+
# The long spellings route through the short setters: `p=` resolves a
|
|
183
|
+
# Predicate object, whereas the `predicate=` attribute writer would store
|
|
184
|
+
# the bare name.
|
|
185
|
+
LONG_KEYS = {
|
|
186
|
+
'attributes' => 'a', 'values' => 'v', 'predicate' => 'p', 'combinator' => 'm'
|
|
187
|
+
}.freeze
|
|
188
|
+
|
|
174
189
|
def build(params)
|
|
175
190
|
params.with_indifferent_access.each do |key, value|
|
|
176
|
-
if key.match(/^(a|v|p|m)$/)
|
|
177
|
-
self.send("#{key}=", value)
|
|
191
|
+
if key.match(/^(a|v|p|m|attributes|values|predicate|combinator)$/)
|
|
192
|
+
self.send("#{LONG_KEYS.fetch(key, key)}=", value)
|
|
178
193
|
end
|
|
179
194
|
end
|
|
180
195
|
|
|
@@ -217,10 +232,6 @@ module Ransack
|
|
|
217
232
|
end
|
|
218
233
|
alias :p :predicate_name
|
|
219
234
|
|
|
220
|
-
def arel_predicate
|
|
221
|
-
raise "not implemented"
|
|
222
|
-
end
|
|
223
|
-
|
|
224
235
|
def validated_values
|
|
225
236
|
values.select { |v| predicate.validator.call(v.value) }
|
|
226
237
|
end
|
|
@@ -260,7 +271,7 @@ module Ransack
|
|
|
260
271
|
end
|
|
261
272
|
|
|
262
273
|
def attr_value_for_attribute(attr)
|
|
263
|
-
return attr.attr if ActiveRecord::Base.
|
|
274
|
+
return attr.attr if ActiveRecord::Base.adapter_class::ADAPTER_NAME == "PostgreSQL"
|
|
264
275
|
|
|
265
276
|
predicate.case_insensitive ? attr.attr.lower : attr.attr
|
|
266
277
|
rescue
|
|
@@ -289,11 +300,11 @@ module Ransack
|
|
|
289
300
|
end
|
|
290
301
|
|
|
291
302
|
def arel_predicate
|
|
292
|
-
|
|
303
|
+
attributes.map { |attribute|
|
|
293
304
|
association = attribute.parent
|
|
294
305
|
parent_table = association.table
|
|
295
306
|
|
|
296
|
-
if negative? && attribute.associated_collection? && not_nested_condition(attribute, parent_table)
|
|
307
|
+
predicate = if negative? && attribute.associated_collection? && not_nested_condition(attribute, parent_table)
|
|
297
308
|
query = context.build_correlated_subquery(association)
|
|
298
309
|
context.remove_association(association)
|
|
299
310
|
|
|
@@ -316,18 +327,22 @@ module Ransack
|
|
|
316
327
|
else
|
|
317
328
|
format_predicate(attribute)
|
|
318
329
|
end
|
|
319
|
-
}.reduce(combinator_method)
|
|
320
330
|
|
|
321
|
-
|
|
322
|
-
#
|
|
323
|
-
#
|
|
324
|
-
#
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
331
|
+
# Applied per attribute rather than to the reduced node: once several
|
|
332
|
+
# attributes are combined, the result is an And/Or whose `right` is
|
|
333
|
+
# another predicate node rather than a Casted value, so
|
|
334
|
+
# replace_right_node? returns false and nothing is unwrapped at all.
|
|
335
|
+
if replace_right_node?(predicate)
|
|
336
|
+
# Replace right node object to plain integer value in order to avoid
|
|
337
|
+
# ActiveModel::RangeError from Arel::Node::Casted.
|
|
338
|
+
# The error can be ignored here because RDBMSs accept large numbers
|
|
339
|
+
# in condition clauses.
|
|
340
|
+
plain_value = predicate.right.value
|
|
341
|
+
predicate.right = plain_value
|
|
342
|
+
end
|
|
329
343
|
|
|
330
|
-
|
|
344
|
+
predicate
|
|
345
|
+
}.reduce(combinator_method)
|
|
331
346
|
end
|
|
332
347
|
|
|
333
348
|
def not_nested_condition(attribute, parent_table)
|
|
@@ -343,14 +358,39 @@ module Ransack
|
|
|
343
358
|
def format_predicate(attribute)
|
|
344
359
|
arel_pred = arel_predicate_for_attribute(attribute)
|
|
345
360
|
arel_values = formatted_values_for_attribute(attribute)
|
|
346
|
-
|
|
361
|
+
|
|
362
|
+
# The `present` / `blank` predicate formatters produce `[nil, '']` regardless
|
|
363
|
+
# of column type. The empty-string half has no meaning on non-string columns:
|
|
364
|
+
# ActiveRecord casts `''` to NULL for those types, producing the
|
|
365
|
+
# always-UNKNOWN `column != NULL` (or `column = NULL`) clause. Strip the
|
|
366
|
+
# empty string when the attribute is not a string-like column.
|
|
367
|
+
if arel_values.is_a?(Array) && arel_values.include?(''.freeze) && !string_like_attribute?(attribute)
|
|
368
|
+
arel_values = arel_values.reject { |v| v == ''.freeze }
|
|
369
|
+
end
|
|
370
|
+
|
|
347
371
|
# For LIKE predicates, wrap the value in Arel::Nodes.build_quoted to prevent
|
|
348
|
-
# ActiveRecord normalization from affecting wildcard patterns
|
|
372
|
+
# ActiveRecord normalization from affecting wildcard patterns, and pass
|
|
373
|
+
# the escape character so the escaping done by `escape_wildcards` is
|
|
374
|
+
# actually honoured. Without an explicit ESCAPE clause, SQLite treats a
|
|
375
|
+
# backslash as a literal character rather than an escape.
|
|
376
|
+
# See https://github.com/activerecord-hackery/ransack/issues/1581
|
|
377
|
+
# A length predicate compares LENGTH(column) rather than the column.
|
|
378
|
+
attr_value = length_predicate? ? length_function_for_attribute(attribute) : attr_value_for_attribute(attribute)
|
|
379
|
+
|
|
349
380
|
if like_predicate?(arel_pred)
|
|
350
|
-
|
|
381
|
+
# The compound forms (matches_any / matches_all and their negations)
|
|
382
|
+
# iterate over an Array of patterns, so each element is quoted on its
|
|
383
|
+
# own; wrapping the whole Array in one node would hand them a single
|
|
384
|
+
# Quoted to iterate over.
|
|
385
|
+
arel_values = if arel_values.is_a?(Array)
|
|
386
|
+
arel_values.map { |v| Arel::Nodes.build_quoted(v) }
|
|
387
|
+
else
|
|
388
|
+
Arel::Nodes.build_quoted(arel_values)
|
|
389
|
+
end
|
|
390
|
+
predicate = attr_value.public_send(arel_pred, arel_values, Constants::LIKE_ESCAPE_CHARACTER)
|
|
391
|
+
else
|
|
392
|
+
predicate = attr_value.public_send(arel_pred, arel_values)
|
|
351
393
|
end
|
|
352
|
-
|
|
353
|
-
predicate = attr_value_for_attribute(attribute).public_send(arel_pred, arel_values)
|
|
354
394
|
|
|
355
395
|
if in_predicate?(predicate)
|
|
356
396
|
predicate.right = predicate.right.map do |pr|
|
|
@@ -366,8 +406,22 @@ module Ransack
|
|
|
366
406
|
predicate.class == Arel::Nodes::In || predicate.class == Arel::Nodes::NotIn
|
|
367
407
|
end
|
|
368
408
|
|
|
409
|
+
LIKE_PREDICATES = %w[
|
|
410
|
+
matches matches_any matches_all
|
|
411
|
+
does_not_match does_not_match_any does_not_match_all
|
|
412
|
+
].freeze
|
|
413
|
+
|
|
369
414
|
def like_predicate?(arel_predicate)
|
|
370
|
-
arel_predicate
|
|
415
|
+
LIKE_PREDICATES.include?(arel_predicate)
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
STRING_LIKE_TYPES = %i[string text citext].freeze
|
|
419
|
+
|
|
420
|
+
def string_like_attribute?(attribute)
|
|
421
|
+
type = attribute.type
|
|
422
|
+
# Treat unknown types as string-like (conservative: keep the existing
|
|
423
|
+
# empty-string comparison rather than silently dropping it).
|
|
424
|
+
type.nil? || STRING_LIKE_TYPES.include?(type.to_sym)
|
|
371
425
|
end
|
|
372
426
|
|
|
373
427
|
def casted_array?(predicate)
|
|
@@ -391,6 +445,28 @@ module Ransack
|
|
|
391
445
|
attribute_type == :integer && arel_node.value.is_a?(Integer)
|
|
392
446
|
end
|
|
393
447
|
|
|
448
|
+
def length_predicate?
|
|
449
|
+
predicate_name.to_s.start_with?('length_')
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
# CHAR_LENGTH counts characters and is the SQL standard spelling; SQLite
|
|
453
|
+
# has no CHAR_LENGTH and its LENGTH already counts characters for text.
|
|
454
|
+
CHAR_LENGTH_ADAPTERS = %w[PostgreSQL PostGIS Mysql2 Trilogy].freeze
|
|
455
|
+
|
|
456
|
+
def length_function_for_attribute(attribute)
|
|
457
|
+
function_name =
|
|
458
|
+
if CHAR_LENGTH_ADAPTERS.include?(ActiveRecord::Base.adapter_class::ADAPTER_NAME)
|
|
459
|
+
'CHAR_LENGTH'.freeze
|
|
460
|
+
else
|
|
461
|
+
'LENGTH'.freeze
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
Arel::Nodes::NamedFunction.new(
|
|
465
|
+
function_name,
|
|
466
|
+
[attr_value_for_attribute(attribute)]
|
|
467
|
+
)
|
|
468
|
+
end
|
|
469
|
+
|
|
394
470
|
def valid_combinator?
|
|
395
471
|
attributes.size < 2 || Constants::AND_OR.include?(combinator)
|
|
396
472
|
end
|