ransack 1.1.0 → 1.2.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/.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
|