ransack 1.4.1 → 1.5.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/CHANGELOG.md +75 -0
- data/CONTRIBUTING.md +7 -7
- data/Gemfile +7 -0
- data/README.md +139 -34
- data/lib/ransack/adapters/active_record/3.0/compat.rb +5 -5
- data/lib/ransack/adapters/active_record/3.0/context.rb +32 -16
- data/lib/ransack/adapters/active_record/3.1/context.rb +31 -16
- data/lib/ransack/adapters/active_record/context.rb +34 -26
- data/lib/ransack/configuration.rb +1 -1
- data/lib/ransack/constants.rb +72 -37
- data/lib/ransack/context.rb +18 -12
- data/lib/ransack/helpers/form_builder.rb +32 -15
- data/lib/ransack/helpers/form_helper.rb +83 -55
- data/lib/ransack/naming.rb +1 -1
- data/lib/ransack/nodes/condition.rb +9 -9
- data/lib/ransack/nodes/grouping.rb +15 -9
- data/lib/ransack/nodes/sort.rb +9 -4
- data/lib/ransack/predicate.rb +10 -10
- data/lib/ransack/search.rb +15 -8
- data/lib/ransack/translate.rb +9 -6
- data/lib/ransack/version.rb +1 -1
- data/lib/ransack/visitor.rb +8 -2
- data/spec/ransack/adapters/active_record/base_spec.rb +2 -2
- data/spec/ransack/helpers/form_builder_spec.rb +63 -43
- data/spec/ransack/helpers/form_helper_spec.rb +163 -2
- data/spec/ransack/nodes/grouping_spec.rb +44 -1
- data/spec/ransack/predicate_spec.rb +165 -3
- data/spec/ransack/translate_spec.rb +4 -1
- data/spec/support/en.yml +5 -0
- data/spec/support/schema.rb +6 -0
- metadata +3 -3
data/lib/ransack/context.rb
CHANGED
@@ -37,7 +37,7 @@ module Ransack
|
|
37
37
|
@join_type = options[:join_type] || Polyamorous::OuterJoin
|
38
38
|
@search_key = options[:search_key] || Ransack.options[:search_key]
|
39
39
|
|
40
|
-
if ::ActiveRecord::VERSION::STRING >= "4.1"
|
40
|
+
if ::ActiveRecord::VERSION::STRING >= "4.1".freeze
|
41
41
|
@base = @join_dependency.join_root
|
42
42
|
@engine = @base.base_klass.arel_engine
|
43
43
|
else
|
@@ -95,7 +95,7 @@ module Ransack
|
|
95
95
|
end
|
96
96
|
|
97
97
|
def traverse(str, base = @base)
|
98
|
-
str ||=
|
98
|
+
str ||= Ransack::Constants::EMPTY
|
99
99
|
|
100
100
|
if (segments = str.split(/_/)).size > 0
|
101
101
|
remainder = []
|
@@ -103,9 +103,13 @@ module Ransack
|
|
103
103
|
while !found_assoc && segments.size > 0 do
|
104
104
|
# Strip the _of_Model_type text from the association name, but hold
|
105
105
|
# onto it in klass, for use as the next base
|
106
|
-
assoc, klass = unpolymorphize_association(segments
|
106
|
+
assoc, klass = unpolymorphize_association(segments
|
107
|
+
.join(Ransack::Constants::UNDERSCORE))
|
107
108
|
if found_assoc = get_association(assoc, base)
|
108
|
-
base = traverse(
|
109
|
+
base = traverse(
|
110
|
+
remainder.join(
|
111
|
+
Ransack::Constants::UNDERSCORE), klass || found_assoc.klass
|
112
|
+
)
|
109
113
|
end
|
110
114
|
|
111
115
|
remainder.unshift segments.pop
|
@@ -119,15 +123,17 @@ module Ransack
|
|
119
123
|
|
120
124
|
def association_path(str, base = @base)
|
121
125
|
base = klassify(base)
|
122
|
-
str ||=
|
126
|
+
str ||= Ransack::Constants::EMPTY
|
123
127
|
path = []
|
124
128
|
segments = str.split(/_/)
|
125
129
|
association_parts = []
|
126
130
|
if (segments = str.split(/_/)).size > 0
|
127
|
-
while segments.size > 0 &&
|
131
|
+
while segments.size > 0 &&
|
132
|
+
!base.columns_hash[segments.join(Ransack::Constants::UNDERSCORE)] &&
|
128
133
|
association_parts << segments.shift do
|
129
|
-
assoc, klass = unpolymorphize_association(
|
130
|
-
|
134
|
+
assoc, klass = unpolymorphize_association(
|
135
|
+
association_parts.join(Ransack::Constants::UNDERSCORE)
|
136
|
+
)
|
131
137
|
if found_assoc = get_association(assoc, base)
|
132
138
|
path += association_parts
|
133
139
|
association_parts = []
|
@@ -136,7 +142,7 @@ module Ransack
|
|
136
142
|
end
|
137
143
|
end
|
138
144
|
|
139
|
-
path.join(
|
145
|
+
path.join(Ransack::Constants::UNDERSCORE)
|
140
146
|
end
|
141
147
|
|
142
148
|
def unpolymorphize_association(str)
|
@@ -160,15 +166,15 @@ module Ransack
|
|
160
166
|
klass.ransackable_scopes(auth_object).any? { |s| s.to_s == str }
|
161
167
|
end
|
162
168
|
|
163
|
-
def searchable_attributes(str =
|
169
|
+
def searchable_attributes(str = Ransack::Constants::EMPTY)
|
164
170
|
traverse(str).ransackable_attributes(auth_object)
|
165
171
|
end
|
166
172
|
|
167
|
-
def sortable_attributes(str =
|
173
|
+
def sortable_attributes(str = Ransack::Constants::EMPTY)
|
168
174
|
traverse(str).ransortable_attributes(auth_object)
|
169
175
|
end
|
170
176
|
|
171
|
-
def searchable_associations(str =
|
177
|
+
def searchable_associations(str = Ransack::Constants::EMPTY)
|
172
178
|
traverse(str).ransackable_associations(auth_object)
|
173
179
|
end
|
174
180
|
end
|
@@ -1,11 +1,14 @@
|
|
1
1
|
require 'action_view'
|
2
2
|
|
3
|
+
RANSACK_FORM_BUILDER = 'RANSACK_FORM_BUILDER'.freeze
|
4
|
+
|
3
5
|
require 'simple_form' if
|
4
|
-
(ENV[
|
6
|
+
(ENV[RANSACK_FORM_BUILDER] || Ransack::Constants::EMPTY)
|
7
|
+
.match('SimpleForm'.freeze)
|
5
8
|
|
6
9
|
module Ransack
|
7
10
|
module Helpers
|
8
|
-
class FormBuilder < (ENV[
|
11
|
+
class FormBuilder < (ENV[RANSACK_FORM_BUILDER].try(:constantize) ||
|
9
12
|
ActionView::Helpers::FormBuilder)
|
10
13
|
|
11
14
|
def label(method, *args, &block)
|
@@ -27,12 +30,13 @@ module Ransack
|
|
27
30
|
def attribute_select(options = nil, html_options = nil, action = nil)
|
28
31
|
options = options || {}
|
29
32
|
html_options = html_options || {}
|
30
|
-
action = action ||
|
33
|
+
action = action || Ransack::Constants::SEARCH
|
31
34
|
default = options.delete(:default)
|
32
35
|
raise ArgumentError, formbuilder_error_message(
|
33
36
|
"#{action}_select") unless object.respond_to?(:context)
|
34
37
|
options[:include_blank] = true unless options.has_key?(:include_blank)
|
35
|
-
bases = [
|
38
|
+
bases = [Ransack::Constants::EMPTY] +
|
39
|
+
association_array(options[:associations])
|
36
40
|
if bases.size > 1
|
37
41
|
collection = attribute_collection_for_bases(action, bases)
|
38
42
|
object.name ||= default if can_use_default?(
|
@@ -50,12 +54,12 @@ module Ransack
|
|
50
54
|
|
51
55
|
def sort_direction_select(options = {}, html_options = {})
|
52
56
|
raise ArgumentError, formbuilder_error_message(
|
53
|
-
'sort_direction') unless object.respond_to?(:context)
|
57
|
+
'sort_direction'.freeze) unless object.respond_to?(:context)
|
54
58
|
template_collection_select(:dir, sort_array, options, html_options)
|
55
59
|
end
|
56
60
|
|
57
61
|
def sort_select(options = {}, html_options = {})
|
58
|
-
attribute_select(options, html_options,
|
62
|
+
attribute_select(options, html_options, Ransack::Constants::SORT) +
|
59
63
|
sort_direction_select(options, html_options)
|
60
64
|
end
|
61
65
|
|
@@ -107,7 +111,7 @@ module Ransack
|
|
107
111
|
|
108
112
|
def predicate_select(options = {}, html_options = {})
|
109
113
|
options[:compounds] = true if options[:compounds].nil?
|
110
|
-
default = options.delete(:default) || 'cont'
|
114
|
+
default = options.delete(:default) || 'cont'.freeze
|
111
115
|
|
112
116
|
keys = options[:compounds] ? Predicate.names :
|
113
117
|
Predicate.names.reject { |k| k.match(/_(any|all)$/) }
|
@@ -116,7 +120,9 @@ module Ransack
|
|
116
120
|
keys = keys.select { |k| only.call(k) }
|
117
121
|
else
|
118
122
|
only = Array.wrap(only).map(&:to_s)
|
119
|
-
keys = keys.select {
|
123
|
+
keys = keys.select {
|
124
|
+
|k| only.include? k.sub(/_(any|all)$/, Ransack::Constants::EMPTY)
|
125
|
+
}
|
120
126
|
end
|
121
127
|
end
|
122
128
|
collection = keys.map { |k| [k, Translate.predicate(k)] }
|
@@ -157,14 +163,23 @@ module Ransack
|
|
157
163
|
end
|
158
164
|
|
159
165
|
def sort_array
|
160
|
-
[
|
166
|
+
[
|
167
|
+
[Ransack::Constants::ASC, object.translate(Ransack::Constants::ASC)],
|
168
|
+
[Ransack::Constants::DESC, object.translate(Ransack::Constants::DESC)]
|
169
|
+
]
|
161
170
|
end
|
162
171
|
|
163
172
|
def combinator_choices
|
164
173
|
if Nodes::Condition === object
|
165
|
-
[
|
174
|
+
[
|
175
|
+
[Ransack::Constants::OR, Translate.word(:any)],
|
176
|
+
[Ransack::Constants::AND, Translate.word(:all)]
|
177
|
+
]
|
166
178
|
else
|
167
|
-
[
|
179
|
+
[
|
180
|
+
[Ransack::Constants::AND, Translate.word(:all)],
|
181
|
+
[Ransack::Constants::OR, Translate.word(:any)]
|
182
|
+
]
|
168
183
|
end
|
169
184
|
end
|
170
185
|
|
@@ -172,7 +187,7 @@ module Ransack
|
|
172
187
|
([prefix] + association_object(obj))
|
173
188
|
.compact
|
174
189
|
.flatten
|
175
|
-
.map { |v| [prefix, v].compact.join(
|
190
|
+
.map { |v| [prefix, v].compact.join(Ransack::Constants::UNDERSCORE) }
|
176
191
|
end
|
177
192
|
|
178
193
|
def association_object(obj)
|
@@ -192,7 +207,7 @@ module Ransack
|
|
192
207
|
when Array, Hash
|
193
208
|
association_array(value, key.to_s)
|
194
209
|
else
|
195
|
-
[key.to_s, [key, value].join(
|
210
|
+
[key.to_s, [key, value].join(Ransack::Constants::UNDERSCORE)]
|
196
211
|
end
|
197
212
|
end
|
198
213
|
end
|
@@ -227,11 +242,13 @@ module Ransack
|
|
227
242
|
end
|
228
243
|
|
229
244
|
def attr_from_base_and_column(base, column)
|
230
|
-
[base, column].reject { |v| v.blank? }
|
245
|
+
[base, column].reject { |v| v.blank? }
|
246
|
+
.join(Ransack::Constants::UNDERSCORE)
|
231
247
|
end
|
232
248
|
|
233
249
|
def formbuilder_error_message(action)
|
234
|
-
"#{
|
250
|
+
"#{
|
251
|
+
action.sub(Ransack::Constants::SEARCH, Ransack::Constants::ATTRIBUTE)
|
235
252
|
} must be called inside a search FormBuilder!"
|
236
253
|
end
|
237
254
|
|
@@ -2,26 +2,6 @@ module Ransack
|
|
2
2
|
module Helpers
|
3
3
|
module FormHelper
|
4
4
|
|
5
|
-
def asc
|
6
|
-
'asc'.freeze
|
7
|
-
end
|
8
|
-
|
9
|
-
def desc
|
10
|
-
'desc'.freeze
|
11
|
-
end
|
12
|
-
|
13
|
-
def asc_arrow
|
14
|
-
'▲'.freeze
|
15
|
-
end
|
16
|
-
|
17
|
-
def desc_arrow
|
18
|
-
'▼'.freeze
|
19
|
-
end
|
20
|
-
|
21
|
-
def non_breaking_space
|
22
|
-
' '.freeze
|
23
|
-
end
|
24
|
-
|
25
5
|
def search_form_for(record, options = {}, &proc)
|
26
6
|
if record.is_a?(Ransack::Search)
|
27
7
|
search = record
|
@@ -48,13 +28,14 @@ module Ransack
|
|
48
28
|
"#{search.klass.to_s.underscore}_search",
|
49
29
|
:method => :get
|
50
30
|
}
|
51
|
-
options[:as] ||=
|
31
|
+
options[:as] ||= Ransack::Constants::DEFAULT_SEARCH_KEY
|
52
32
|
options[:html].reverse_merge!(html_options)
|
53
33
|
options[:builder] ||= FormBuilder
|
54
34
|
|
55
35
|
form_for(record, options, &proc)
|
56
36
|
end
|
57
37
|
|
38
|
+
# sort_link @q, :name, [:name, 'kind ASC'], 'Player Name'
|
58
39
|
def sort_link(search, attribute, *args)
|
59
40
|
# Extract out a routing proxy for url_for scoping later
|
60
41
|
if search.is_a?(Array)
|
@@ -65,65 +46,112 @@ module Ransack
|
|
65
46
|
raise TypeError, "First argument must be a Ransack::Search!" unless
|
66
47
|
Search === search
|
67
48
|
|
68
|
-
|
69
|
-
|
49
|
+
# This is the field that this link represents. The direction of the sort icon (up/down arrow) will
|
50
|
+
# depend on the sort status of this field
|
51
|
+
field_name = attribute.to_s
|
70
52
|
|
71
|
-
|
53
|
+
# Determine the fields we want to sort on
|
54
|
+
sort_fields = if Array === args.first
|
55
|
+
args.shift
|
56
|
+
else
|
57
|
+
Array(field_name)
|
58
|
+
end
|
72
59
|
|
73
|
-
|
74
|
-
if
|
60
|
+
label_text =
|
61
|
+
if String === args.first
|
75
62
|
args.shift.to_s
|
76
63
|
else
|
77
|
-
Translate.attribute(
|
64
|
+
Translate.attribute(field_name, :context => search.context)
|
78
65
|
end
|
79
|
-
)
|
80
|
-
|
81
|
-
if existing_sort = search.sorts.detect { |s| s.name == attr_name }
|
82
|
-
prev_attr, prev_dir = existing_sort.name, existing_sort.dir
|
83
|
-
end
|
84
66
|
|
85
67
|
options = args.first.is_a?(Hash) ? args.shift.dup : {}
|
86
68
|
default_order = options.delete :default_order
|
87
|
-
|
69
|
+
default_order_is_a_hash = Hash === default_order
|
88
70
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
71
|
+
# If the default order is a hash of fields, duplicate it and let us access it with strings or symbols
|
72
|
+
default_order = default_order.dup.with_indifferent_access if
|
73
|
+
default_order_is_a_hash
|
74
|
+
|
75
|
+
search_params = params[search.context.search_key].presence ||
|
76
|
+
{}.with_indifferent_access
|
77
|
+
|
78
|
+
# Find the current direction (if there is one) of the primary sort field
|
79
|
+
if existing_sort = search.sorts.detect { |s| s.name == field_name }
|
80
|
+
field_current_dir = existing_sort.dir
|
93
81
|
end
|
94
82
|
|
83
|
+
sort_params = []
|
84
|
+
|
85
|
+
Array(sort_fields).each do |sort_field|
|
86
|
+
attr_name, new_dir = sort_field.to_s.downcase.split(/\s+/)
|
87
|
+
current_dir = nil
|
88
|
+
|
89
|
+
# if the user didn't specify the sort direction, detect the previous
|
90
|
+
# sort direction on this field and reverse it
|
91
|
+
if Ransack::Constants::ASC_DESC.none? { |d| d == new_dir }
|
92
|
+
if existing_sort = search.sorts.detect { |s| s.name == attr_name }
|
93
|
+
current_dir = existing_sort.dir
|
94
|
+
end
|
95
|
+
|
96
|
+
new_dir =
|
97
|
+
if current_dir
|
98
|
+
if current_dir == Ransack::Constants::DESC
|
99
|
+
Ransack::Constants::ASC
|
100
|
+
else
|
101
|
+
Ransack::Constants::DESC
|
102
|
+
end
|
103
|
+
elsif default_order_is_a_hash
|
104
|
+
default_order[attr_name] || Ransack::Constants::ASC
|
105
|
+
else
|
106
|
+
default_order || Ransack::Constants::ASC
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
sort_params << "#{attr_name} #{new_dir}"
|
111
|
+
end
|
112
|
+
|
113
|
+
# if there is only one sort parameter, remove it from the array and just
|
114
|
+
# use the string as the parameter
|
115
|
+
sort_params = sort_params.first if sort_params.size == 1
|
116
|
+
|
95
117
|
html_options = args.first.is_a?(Hash) ? args.shift.dup : {}
|
96
|
-
css = [
|
97
|
-
|
118
|
+
css = [Ransack::Constants::SORT_LINK, field_current_dir]
|
119
|
+
.compact.join(Ransack::Constants::SPACE)
|
120
|
+
html_options[:class] = [css, html_options[:class]]
|
121
|
+
.compact.join(Ransack::Constants::SPACE)
|
122
|
+
|
98
123
|
query_hash = {}
|
99
124
|
query_hash[search.context.search_key] = search_params
|
100
|
-
|
125
|
+
.merge(:s => sort_params)
|
101
126
|
options.merge!(query_hash)
|
102
|
-
options_for_url = params.merge
|
127
|
+
options_for_url = params.merge(options)
|
103
128
|
|
104
|
-
url =
|
129
|
+
url =
|
130
|
+
if routing_proxy && respond_to?(routing_proxy)
|
105
131
|
send(routing_proxy).url_for(options_for_url)
|
106
132
|
else
|
107
133
|
url_for(options_for_url)
|
108
134
|
end
|
109
135
|
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
.join(non_breaking_space)
|
114
|
-
.html_safe,
|
115
|
-
url,
|
116
|
-
html_options
|
117
|
-
)
|
136
|
+
name = link_name(label_text, field_current_dir)
|
137
|
+
|
138
|
+
link_to(name, url, html_options)
|
118
139
|
end
|
119
140
|
|
120
141
|
private
|
121
142
|
|
122
|
-
def
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
143
|
+
def link_name(label_text, dir)
|
144
|
+
[ERB::Util.h(label_text), order_indicator_for(dir)]
|
145
|
+
.compact
|
146
|
+
.join(Ransack::Constants::NON_BREAKING_SPACE)
|
147
|
+
.html_safe
|
148
|
+
end
|
149
|
+
|
150
|
+
def order_indicator_for(dir)
|
151
|
+
if dir == Ransack::Constants::ASC
|
152
|
+
Ransack::Constants::ASC_ARROW
|
153
|
+
elsif dir == Ransack::Constants::DESC
|
154
|
+
Ransack::Constants::DESC_ARROW
|
127
155
|
else
|
128
156
|
nil
|
129
157
|
end
|
data/lib/ransack/naming.rb
CHANGED
@@ -105,7 +105,7 @@ module Ransack
|
|
105
105
|
end
|
106
106
|
|
107
107
|
def combinator=(val)
|
108
|
-
@combinator =
|
108
|
+
@combinator = Ransack::Constants::AND_OR.detect { |v| v == val.to_s } || nil
|
109
109
|
end
|
110
110
|
alias :m= :combinator=
|
111
111
|
alias :m :combinator
|
@@ -180,9 +180,9 @@ module Ransack
|
|
180
180
|
|
181
181
|
if predicates.size > 1
|
182
182
|
case combinator
|
183
|
-
when
|
183
|
+
when Ransack::Constants::AND
|
184
184
|
Arel::Nodes::Grouping.new(Arel::Nodes::And.new(predicates))
|
185
|
-
when
|
185
|
+
when Ransack::Constants::OR
|
186
186
|
predicates.inject(&:or)
|
187
187
|
end
|
188
188
|
else
|
@@ -226,14 +226,14 @@ module Ransack
|
|
226
226
|
|
227
227
|
def inspect
|
228
228
|
data = [
|
229
|
-
['attributes', a.try(:map, &:name)],
|
230
|
-
['predicate', p],
|
231
|
-
[
|
232
|
-
['values', v.try(:map, &:value)]
|
229
|
+
['attributes'.freeze, a.try(:map, &:name)],
|
230
|
+
['predicate'.freeze, p],
|
231
|
+
[Ransack::Constants::COMBINATOR, m],
|
232
|
+
['values'.freeze, v.try(:map, &:value)]
|
233
233
|
]
|
234
234
|
.reject { |e| e[1].blank? }
|
235
235
|
.map { |v| "#{v[0]}: #{v[1]}" }
|
236
|
-
.join(
|
236
|
+
.join(Ransack::Constants::COMMA_SPACE)
|
237
237
|
"Condition <#{data}>"
|
238
238
|
end
|
239
239
|
|
@@ -241,7 +241,7 @@ module Ransack
|
|
241
241
|
|
242
242
|
def valid_combinator?
|
243
243
|
attributes.size < 2 ||
|
244
|
-
|
244
|
+
Ransack::Constants::AND_OR.include?(combinator)
|
245
245
|
end
|
246
246
|
|
247
247
|
end
|