ransack 1.1.0 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +12 -4
- data/CONTRIBUTING.md +10 -4
- data/Gemfile +12 -9
- data/README.md +46 -11
- data/lib/ransack.rb +4 -2
- data/lib/ransack/adapters/active_record.rb +1 -1
- data/lib/ransack/adapters/active_record/3.0/compat.rb +16 -6
- data/lib/ransack/adapters/active_record/3.0/context.rb +32 -16
- data/lib/ransack/adapters/active_record/3.1/context.rb +32 -15
- data/lib/ransack/adapters/active_record/3.2/context.rb +1 -1
- data/lib/ransack/adapters/active_record/base.rb +9 -6
- data/lib/ransack/adapters/active_record/context.rb +193 -2
- data/lib/ransack/configuration.rb +4 -4
- data/lib/ransack/constants.rb +81 -18
- data/lib/ransack/context.rb +27 -12
- data/lib/ransack/helpers/form_builder.rb +126 -91
- data/lib/ransack/helpers/form_helper.rb +34 -12
- data/lib/ransack/naming.rb +2 -1
- data/lib/ransack/nodes/attribute.rb +6 -4
- data/lib/ransack/nodes/bindable.rb +3 -1
- data/lib/ransack/nodes/condition.rb +40 -27
- data/lib/ransack/nodes/grouping.rb +19 -13
- data/lib/ransack/nodes/node.rb +3 -3
- data/lib/ransack/nodes/sort.rb +5 -3
- data/lib/ransack/nodes/value.rb +2 -2
- data/lib/ransack/predicate.rb +18 -9
- data/lib/ransack/ransacker.rb +4 -4
- data/lib/ransack/search.rb +9 -12
- data/lib/ransack/translate.rb +42 -21
- data/lib/ransack/version.rb +1 -1
- data/lib/ransack/visitor.rb +4 -4
- data/ransack.gemspec +17 -7
- data/spec/blueprints/notes.rb +2 -0
- data/spec/blueprints/people.rb +4 -1
- data/spec/console.rb +3 -3
- data/spec/ransack/adapters/active_record/base_spec.rb +149 -22
- data/spec/ransack/adapters/active_record/context_spec.rb +5 -5
- data/spec/ransack/configuration_spec.rb +17 -8
- data/spec/ransack/dependencies_spec.rb +8 -0
- data/spec/ransack/helpers/form_builder_spec.rb +37 -14
- data/spec/ransack/helpers/form_helper_spec.rb +5 -5
- data/spec/ransack/predicate_spec.rb +6 -3
- data/spec/ransack/search_spec.rb +95 -73
- data/spec/ransack/translate_spec.rb +14 -0
- data/spec/spec_helper.rb +14 -8
- data/spec/support/en.yml +6 -0
- data/spec/support/schema.rb +76 -31
- metadata +48 -29
@@ -4,7 +4,7 @@ module Ransack
|
|
4
4
|
module Base
|
5
5
|
|
6
6
|
def self.extended(base)
|
7
|
-
alias :search :ransack unless base.
|
7
|
+
alias :search :ransack unless base.respond_to? :search
|
8
8
|
base.class_eval do
|
9
9
|
class_attribute :_ransackers
|
10
10
|
self._ransackers ||= {}
|
@@ -12,11 +12,13 @@ module Ransack
|
|
12
12
|
end
|
13
13
|
|
14
14
|
def ransack(params = {}, options = {})
|
15
|
-
Search.new(self, params
|
15
|
+
Search.new(self, params ? params.delete_if {
|
16
|
+
|k, v| v.blank? && v != false } : params, options)
|
16
17
|
end
|
17
18
|
|
18
19
|
def ransacker(name, opts = {}, &block)
|
19
|
-
self._ransackers = _ransackers.merge name.to_s => Ransacker
|
20
|
+
self._ransackers = _ransackers.merge name.to_s => Ransacker
|
21
|
+
.new(self, name, opts, &block)
|
20
22
|
end
|
21
23
|
|
22
24
|
def ransackable_attributes(auth_object = nil)
|
@@ -24,15 +26,16 @@ module Ransack
|
|
24
26
|
end
|
25
27
|
|
26
28
|
def ransortable_attributes(auth_object = nil)
|
27
|
-
# Here so users can overwrite the attributes
|
29
|
+
# Here so users can overwrite the attributes
|
30
|
+
# that show up in the sort_select
|
28
31
|
ransackable_attributes(auth_object)
|
29
32
|
end
|
30
33
|
|
31
34
|
def ransackable_associations(auth_object = nil)
|
32
|
-
reflect_on_all_associations.map {|a| a.name.to_s}
|
35
|
+
reflect_on_all_associations.map { |a| a.name.to_s }
|
33
36
|
end
|
34
37
|
|
35
38
|
end
|
36
39
|
end
|
37
40
|
end
|
38
|
-
end
|
41
|
+
end
|
@@ -1,5 +1,4 @@
|
|
1
1
|
require 'ransack/context'
|
2
|
-
require 'ransack/adapters/active_record/3.2/context'
|
3
2
|
require 'ransack/adapters/active_record/compat'
|
4
3
|
require 'polyamorous'
|
5
4
|
|
@@ -8,6 +7,10 @@ module Ransack
|
|
8
7
|
module ActiveRecord
|
9
8
|
class Context < ::Ransack::Context
|
10
9
|
|
10
|
+
# Because the AR::Associations namespace is insane
|
11
|
+
JoinDependency = ::ActiveRecord::Associations::JoinDependency
|
12
|
+
JoinPart = JoinDependency::JoinPart
|
13
|
+
|
11
14
|
def initialize(object, options = {})
|
12
15
|
super
|
13
16
|
@arel_visitor = @engine.connection.visitor
|
@@ -31,11 +34,199 @@ module Ransack
|
|
31
34
|
viz = Visitor.new
|
32
35
|
relation = @object.where(viz.accept(search.base))
|
33
36
|
if search.sorts.any?
|
34
|
-
relation = relation.except(:order)
|
37
|
+
relation = relation.except(:order)
|
38
|
+
.reorder(viz.accept(search.sorts))
|
35
39
|
end
|
36
40
|
opts[:distinct] ? relation.distinct : relation
|
37
41
|
end
|
38
42
|
|
43
|
+
def attribute_method?(str, klass = @klass)
|
44
|
+
exists = false
|
45
|
+
if ransackable_attribute?(str, klass)
|
46
|
+
exists = true
|
47
|
+
elsif (segments = str.split(/_/)).size > 1
|
48
|
+
remainder = []
|
49
|
+
found_assoc = nil
|
50
|
+
while !found_assoc && remainder.unshift(
|
51
|
+
segments.pop) && segments.size > 0 do
|
52
|
+
assoc, poly_class = unpolymorphize_association(
|
53
|
+
segments.join('_')
|
54
|
+
)
|
55
|
+
if found_assoc = get_association(assoc, klass)
|
56
|
+
exists = attribute_method?(remainder.join('_'),
|
57
|
+
poly_class || found_assoc.klass
|
58
|
+
)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
exists
|
63
|
+
end
|
64
|
+
|
65
|
+
def table_for(parent)
|
66
|
+
parent.table
|
67
|
+
end
|
68
|
+
|
69
|
+
def klassify(obj)
|
70
|
+
if Class === obj && ::ActiveRecord::Base > obj
|
71
|
+
obj
|
72
|
+
elsif obj.respond_to? :klass
|
73
|
+
obj.klass
|
74
|
+
elsif obj.respond_to? :base_klass
|
75
|
+
obj.base_klass
|
76
|
+
else
|
77
|
+
raise ArgumentError, "Don't know how to klassify #{obj}"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def get_parent_and_attribute_name(str, parent = @base)
|
84
|
+
attr_name = nil
|
85
|
+
|
86
|
+
if ransackable_attribute?(str, klassify(parent))
|
87
|
+
attr_name = str
|
88
|
+
elsif (segments = str.split(/_/)).size > 1
|
89
|
+
remainder = []
|
90
|
+
found_assoc = nil
|
91
|
+
while remainder.unshift(
|
92
|
+
segments.pop) && segments.size > 0 && !found_assoc do
|
93
|
+
assoc, klass = unpolymorphize_association(segments.join('_'))
|
94
|
+
if found_assoc = get_association(assoc, parent)
|
95
|
+
join = build_or_find_association(found_assoc.name, parent, klass)
|
96
|
+
parent, attr_name = get_parent_and_attribute_name(
|
97
|
+
remainder.join('_'), join
|
98
|
+
)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
[parent, attr_name]
|
104
|
+
end
|
105
|
+
|
106
|
+
def get_association(str, parent = @base)
|
107
|
+
klass = klassify parent
|
108
|
+
ransackable_association?(str, klass) &&
|
109
|
+
klass.reflect_on_all_associations.detect { |a| a.name.to_s == str }
|
110
|
+
end
|
111
|
+
|
112
|
+
def join_dependency(relation)
|
113
|
+
if relation.respond_to?(:join_dependency) # Squeel will enable this
|
114
|
+
relation.join_dependency
|
115
|
+
else
|
116
|
+
build_join_dependency(relation)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# Checkout active_record/relation/query_methods.rb +build_joins+ for
|
121
|
+
# reference. Lots of duplicated code maybe we can avoid it
|
122
|
+
def build_join_dependency(relation)
|
123
|
+
buckets = relation.joins_values.group_by do |join|
|
124
|
+
case join
|
125
|
+
when String
|
126
|
+
'string_join'
|
127
|
+
when Hash, Symbol, Array
|
128
|
+
'association_join'
|
129
|
+
when JoinDependency, JoinDependency::JoinAssociation
|
130
|
+
'stashed_join'
|
131
|
+
when Arel::Nodes::Join
|
132
|
+
'join_node'
|
133
|
+
else
|
134
|
+
raise 'unknown class: %s' % join.class.name
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
association_joins = buckets['association_join'] || []
|
139
|
+
|
140
|
+
stashed_association_joins = buckets['stashed_join'] || []
|
141
|
+
|
142
|
+
join_nodes = buckets['join_node'] || []
|
143
|
+
|
144
|
+
string_joins = (buckets['string_join'] || [])
|
145
|
+
.map { |x| x.strip }
|
146
|
+
.uniq
|
147
|
+
|
148
|
+
join_list = relation.send :custom_join_ast,
|
149
|
+
relation.table.from(relation.table), string_joins
|
150
|
+
|
151
|
+
join_dependency = JoinDependency.new(
|
152
|
+
relation.klass, association_joins, join_list
|
153
|
+
)
|
154
|
+
|
155
|
+
join_nodes.each do |join|
|
156
|
+
join_dependency.alias_tracker.aliases[join.left.name.downcase] = 1
|
157
|
+
end
|
158
|
+
|
159
|
+
if ::ActiveRecord::VERSION::STRING >= "4.1"
|
160
|
+
join_dependency
|
161
|
+
else
|
162
|
+
join_dependency.graft(*stashed_association_joins)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
if ::ActiveRecord::VERSION::STRING >= "4.1"
|
167
|
+
|
168
|
+
def build_or_find_association(name, parent = @base, klass = nil)
|
169
|
+
list = if ::ActiveRecord::VERSION::STRING >= "4.1"
|
170
|
+
@join_dependency.join_root.children.detect
|
171
|
+
else
|
172
|
+
@join_dependency.join_associations
|
173
|
+
end
|
174
|
+
|
175
|
+
found_association = list.detect do |assoc|
|
176
|
+
assoc.reflection.name == name &&
|
177
|
+
@associations_pot[assoc] == parent &&
|
178
|
+
(!klass || assoc.reflection.klass == klass)
|
179
|
+
end
|
180
|
+
|
181
|
+
unless found_association
|
182
|
+
jd = JoinDependency.new(
|
183
|
+
parent.base_klass,
|
184
|
+
Polyamorous::Join.new(name, @join_type, klass),
|
185
|
+
[]
|
186
|
+
)
|
187
|
+
found_association = jd.join_root.children.last
|
188
|
+
associations found_association, parent
|
189
|
+
|
190
|
+
# TODO maybe we dont need to push associations here, we could loop
|
191
|
+
# through the @associations_pot instead
|
192
|
+
@join_dependency.join_root.children.push found_association
|
193
|
+
|
194
|
+
# Builds the arel nodes properly for this association
|
195
|
+
@join_dependency.send(
|
196
|
+
:construct_tables!, jd.join_root, found_association
|
197
|
+
)
|
198
|
+
|
199
|
+
# Leverage the stashed association functionality in AR
|
200
|
+
@object = @object.joins(jd)
|
201
|
+
end
|
202
|
+
|
203
|
+
found_association
|
204
|
+
end
|
205
|
+
|
206
|
+
def associations(assoc, parent)
|
207
|
+
@associations_pot ||= {}
|
208
|
+
@associations_pot[assoc] = parent
|
209
|
+
end
|
210
|
+
else
|
211
|
+
|
212
|
+
def build_or_find_association(name, parent = @base, klass = nil)
|
213
|
+
found_association = @join_dependency.join_associations
|
214
|
+
.detect do |assoc|
|
215
|
+
assoc.reflection.name == name &&
|
216
|
+
assoc.parent == parent &&
|
217
|
+
(!klass || assoc.reflection.klass == klass)
|
218
|
+
end
|
219
|
+
unless found_association
|
220
|
+
@join_dependency.send(:build, Polyamorous::Join.new(
|
221
|
+
name, @join_type, klass), parent)
|
222
|
+
found_association = @join_dependency.join_associations.last
|
223
|
+
# Leverage the stashed association functionality in AR
|
224
|
+
@object = @object.joins(found_association)
|
225
|
+
end
|
226
|
+
|
227
|
+
found_association
|
228
|
+
end
|
229
|
+
end
|
39
230
|
end
|
40
231
|
end
|
41
232
|
end
|
@@ -7,7 +7,7 @@ module Ransack
|
|
7
7
|
mattr_accessor :predicates, :options
|
8
8
|
self.predicates = {}
|
9
9
|
self.options = {
|
10
|
-
:
|
10
|
+
search_key: :q
|
11
11
|
}
|
12
12
|
|
13
13
|
def configure
|
@@ -27,9 +27,9 @@ module Ransack
|
|
27
27
|
['_any', '_all'].each do |suffix|
|
28
28
|
self.predicates[name + suffix] = Predicate.new(
|
29
29
|
opts.merge(
|
30
|
-
:
|
31
|
-
:
|
32
|
-
:
|
30
|
+
name: name + suffix,
|
31
|
+
arel_predicate: opts[:arel_predicate] + suffix,
|
32
|
+
compound: true
|
33
33
|
)
|
34
34
|
)
|
35
35
|
end if compounds
|
data/lib/ransack/constants.rb
CHANGED
@@ -6,31 +6,94 @@ module Ransack
|
|
6
6
|
AREL_PREDICATES = %w(eq not_eq matches does_not_match lt lteq gt gteq in not_in)
|
7
7
|
|
8
8
|
DERIVED_PREDICATES = [
|
9
|
-
['cont', {
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
['
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
['
|
20
|
-
|
9
|
+
['cont', {
|
10
|
+
arel_predicate: 'matches',
|
11
|
+
formatter: proc { |v| "%#{escape_wildcards(v)}%" }
|
12
|
+
}
|
13
|
+
],
|
14
|
+
['not_cont', {
|
15
|
+
arel_predicate: 'does_not_match',
|
16
|
+
formatter: proc { |v| "%#{escape_wildcards(v)}%" }
|
17
|
+
}
|
18
|
+
],
|
19
|
+
['start', {
|
20
|
+
arel_predicate: 'matches',
|
21
|
+
formatter: proc { |v| "#{escape_wildcards(v)}%" }
|
22
|
+
}
|
23
|
+
],
|
24
|
+
['not_start', {
|
25
|
+
arel_predicate: 'does_not_match',
|
26
|
+
formatter: proc { |v| "#{escape_wildcards(v)}%" }
|
27
|
+
}
|
28
|
+
],
|
29
|
+
['end', {
|
30
|
+
arel_predicate: 'matches',
|
31
|
+
formatter: proc { |v| "%#{escape_wildcards(v)}" }
|
32
|
+
}
|
33
|
+
],
|
34
|
+
['not_end', {
|
35
|
+
arel_predicate: 'does_not_match',
|
36
|
+
formatter: proc { |v| "%#{escape_wildcards(v)}" }
|
37
|
+
}
|
38
|
+
],
|
39
|
+
['true', {
|
40
|
+
arel_predicate: 'eq',
|
41
|
+
compounds: false,
|
42
|
+
type: :boolean,
|
43
|
+
validator: proc { |v| TRUE_VALUES.include?(v) }
|
44
|
+
}
|
45
|
+
],
|
46
|
+
['false', {
|
47
|
+
arel_predicate: 'eq',
|
48
|
+
compounds: false,
|
49
|
+
type: :boolean,
|
50
|
+
validator: proc { |v| TRUE_VALUES.include?(v) },
|
51
|
+
formatter: proc { |v| !v }
|
52
|
+
}
|
53
|
+
],
|
54
|
+
['present', {
|
55
|
+
arel_predicate: 'not_eq_all',
|
56
|
+
compounds: false,
|
57
|
+
type: :boolean,
|
58
|
+
validator: proc { |v| TRUE_VALUES.include?(v) },
|
59
|
+
formatter: proc { |v| [nil, ''] }
|
60
|
+
}
|
61
|
+
],
|
62
|
+
['blank', {
|
63
|
+
arel_predicate: 'eq_any',
|
64
|
+
compounds: false,
|
65
|
+
type: :boolean,
|
66
|
+
validator: proc { |v| TRUE_VALUES.include?(v) },
|
67
|
+
formatter: proc { |v| [nil, ''] }
|
68
|
+
}
|
69
|
+
],
|
70
|
+
['null', {
|
71
|
+
arel_predicate: 'eq',
|
72
|
+
compounds: false,
|
73
|
+
type: :boolean,
|
74
|
+
validator: proc { |v| TRUE_VALUES.include?(v)},
|
75
|
+
formatter: proc { |v| nil }
|
76
|
+
}
|
77
|
+
],
|
78
|
+
['not_null', {
|
79
|
+
arel_predicate: 'not_eq',
|
80
|
+
compounds: false,
|
81
|
+
type: :boolean,
|
82
|
+
validator: proc { |v| TRUE_VALUES.include?(v) },
|
83
|
+
formatter: proc { |v| nil } }
|
84
|
+
]
|
21
85
|
]
|
22
86
|
|
23
87
|
module_function
|
24
88
|
# replace % \ to \% \\
|
25
89
|
def escape_wildcards(unescaped)
|
26
90
|
case ActiveRecord::Base.connection.adapter_name
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
91
|
+
when "SQLite"
|
92
|
+
unescaped
|
93
|
+
else
|
94
|
+
# Necessary for PostgreSQL and MySQL
|
95
|
+
unescaped.to_s.gsub(/([\\|\%|.])/, '\\\\\\1')
|
32
96
|
end
|
33
97
|
end
|
34
|
-
|
35
98
|
end
|
36
99
|
end
|
data/lib/ransack/context.rb
CHANGED
@@ -8,8 +8,11 @@ module Ransack
|
|
8
8
|
class << self
|
9
9
|
|
10
10
|
def for(object, options = {})
|
11
|
-
context = Class === object ?
|
12
|
-
|
11
|
+
context = Class === object ?
|
12
|
+
for_class(object, options) :
|
13
|
+
for_object(object, options)
|
14
|
+
context or raise ArgumentError,
|
15
|
+
"Don't know what context to use for #{object}"
|
13
16
|
end
|
14
17
|
|
15
18
|
def for_class(klass, options = {})
|
@@ -33,9 +36,18 @@ module Ransack
|
|
33
36
|
@join_dependency = join_dependency(@object)
|
34
37
|
@join_type = options[:join_type] || Arel::OuterJoin
|
35
38
|
@search_key = options[:search_key] || Ransack.options[:search_key]
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
+
|
40
|
+
if ::ActiveRecord::VERSION::STRING >= "4.1"
|
41
|
+
@base = @join_dependency.join_root
|
42
|
+
@engine = @base.base_klass.arel_engine
|
43
|
+
else
|
44
|
+
@base = @join_dependency.join_base
|
45
|
+
@engine = @base.arel_engine
|
46
|
+
end
|
47
|
+
|
48
|
+
@default_table = Arel::Table.new(
|
49
|
+
@base.table_name, as: @base.aliased_table_name, engine: @engine
|
50
|
+
)
|
39
51
|
@bind_pairs = Hash.new do |hash, key|
|
40
52
|
parent, attr_name = get_parent_and_attribute_name(key.to_s)
|
41
53
|
if parent && attr_name
|
@@ -54,7 +66,7 @@ module Ransack
|
|
54
66
|
elsif obj.respond_to? :base_klass # Rails 4
|
55
67
|
obj.base_klass
|
56
68
|
else
|
57
|
-
raise ArgumentError, "Don't know how to klassify #{obj}"
|
69
|
+
raise ArgumentError, "Don't know how to klassify #{obj.inspect}"
|
58
70
|
end
|
59
71
|
end
|
60
72
|
|
@@ -85,7 +97,8 @@ module Ransack
|
|
85
97
|
|
86
98
|
remainder.unshift segments.pop
|
87
99
|
end
|
88
|
-
raise UntraversableAssociationError,
|
100
|
+
raise UntraversableAssociationError,
|
101
|
+
"No association matches #{str}" unless found_assoc
|
89
102
|
end
|
90
103
|
|
91
104
|
klassify(base)
|
@@ -98,8 +111,10 @@ module Ransack
|
|
98
111
|
segments = str.split(/_/)
|
99
112
|
association_parts = []
|
100
113
|
if (segments = str.split(/_/)).size > 0
|
101
|
-
while segments.size > 0 && !base.columns_hash[segments.join('_')] &&
|
102
|
-
|
114
|
+
while segments.size > 0 && !base.columns_hash[segments.join('_')] &&
|
115
|
+
association_parts << segments.shift do
|
116
|
+
assoc, klass = unpolymorphize_association(association_parts
|
117
|
+
.join('_'))
|
103
118
|
if found_assoc = get_association(assoc, base)
|
104
119
|
path += association_parts
|
105
120
|
association_parts = []
|
@@ -120,7 +135,8 @@ module Ransack
|
|
120
135
|
end
|
121
136
|
|
122
137
|
def ransackable_attribute?(str, klass)
|
123
|
-
klass.ransackable_attributes(auth_object).include?
|
138
|
+
klass.ransackable_attributes(auth_object).include?(str) ||
|
139
|
+
klass.ransortable_attributes(auth_object).include?(str)
|
124
140
|
end
|
125
141
|
|
126
142
|
def ransackable_association?(str, klass)
|
@@ -138,6 +154,5 @@ module Ransack
|
|
138
154
|
def searchable_associations(str = '')
|
139
155
|
traverse(str).ransackable_associations(auth_object)
|
140
156
|
end
|
141
|
-
|
142
157
|
end
|
143
|
-
end
|
158
|
+
end
|