ransack 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/ransack.rb +1 -1
- data/lib/ransack/adapters/active_record.rb +18 -2
- data/lib/ransack/adapters/active_record/3.0/base.rb +34 -0
- data/lib/ransack/adapters/active_record/3.0/compat.rb +23 -0
- data/lib/ransack/adapters/active_record/3.0/context.rb +168 -0
- data/lib/ransack/adapters/active_record/3.0/join_association.rb +44 -0
- data/lib/ransack/adapters/active_record/3.0/join_dependency.rb +63 -0
- data/lib/ransack/adapters/active_record/base.rb +19 -2
- data/lib/ransack/adapters/active_record/context.rb +45 -33
- data/lib/ransack/adapters/active_record/join_association.rb +44 -0
- data/lib/ransack/adapters/active_record/join_dependency.rb +63 -0
- data/lib/ransack/context.rb +25 -65
- data/lib/ransack/helpers/form_builder.rb +10 -4
- data/lib/ransack/helpers/form_helper.rb +1 -0
- data/lib/ransack/locale/en.yml +1 -0
- data/lib/ransack/nodes.rb +1 -0
- data/lib/ransack/nodes/attribute.rb +21 -4
- data/lib/ransack/nodes/bindable.rb +29 -0
- data/lib/ransack/nodes/condition.rb +30 -28
- data/lib/ransack/nodes/sort.rb +5 -3
- data/lib/ransack/nodes/value.rb +84 -100
- data/lib/ransack/predicate.rb +1 -11
- data/lib/ransack/ransacker.rb +26 -0
- data/lib/ransack/search.rb +3 -2
- data/lib/ransack/version.rb +1 -1
- data/lib/ransack/visitor.rb +64 -0
- data/ransack.gemspec +3 -3
- data/spec/console.rb +1 -2
- data/spec/ransack/adapters/active_record/base_spec.rb +18 -0
- data/spec/ransack/adapters/active_record/context_spec.rb +2 -2
- data/spec/ransack/helpers/form_builder_spec.rb +4 -0
- data/spec/ransack/search_spec.rb +25 -2
- data/spec/spec_helper.rb +2 -3
- data/spec/support/schema.rb +8 -0
- metadata +16 -8
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
|
3
|
+
module Ransack
|
4
|
+
module Adapters
|
5
|
+
module ActiveRecord
|
6
|
+
class JoinAssociation < ::ActiveRecord::Associations::JoinDependency::JoinAssociation
|
7
|
+
|
8
|
+
def initialize(reflection, join_dependency, parent = nil, polymorphic_class = nil)
|
9
|
+
if polymorphic_class && ::ActiveRecord::Base > polymorphic_class
|
10
|
+
swapping_reflection_klass(reflection, polymorphic_class) do |reflection|
|
11
|
+
super(reflection, join_dependency, parent)
|
12
|
+
end
|
13
|
+
else
|
14
|
+
super(reflection, join_dependency, parent)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def swapping_reflection_klass(reflection, klass)
|
19
|
+
reflection = reflection.clone
|
20
|
+
original_polymorphic = reflection.options.delete(:polymorphic)
|
21
|
+
reflection.instance_variable_set(:@klass, klass)
|
22
|
+
yield reflection
|
23
|
+
ensure
|
24
|
+
reflection.options[:polymorphic] = original_polymorphic
|
25
|
+
end
|
26
|
+
|
27
|
+
def ==(other)
|
28
|
+
super && active_record == other.active_record
|
29
|
+
end
|
30
|
+
|
31
|
+
def build_constraint(reflection, table, key, foreign_table, foreign_key)
|
32
|
+
if reflection.options[:polymorphic]
|
33
|
+
super.and(
|
34
|
+
foreign_table[reflection.foreign_type].eq(reflection.klass.name)
|
35
|
+
)
|
36
|
+
else
|
37
|
+
super
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
|
3
|
+
module Ransack
|
4
|
+
module Adapters
|
5
|
+
module ActiveRecord
|
6
|
+
module JoinDependency
|
7
|
+
|
8
|
+
# Yes, I'm using alias_method_chain here. No, I don't feel too
|
9
|
+
# bad about it. JoinDependency, or, to call it by its full proper
|
10
|
+
# name, ::ActiveRecord::Associations::JoinDependency, is one of the
|
11
|
+
# most "for internal use only" chunks of ActiveRecord.
|
12
|
+
def self.included(base)
|
13
|
+
base.class_eval do
|
14
|
+
alias_method_chain :graft, :ransack
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def graft_with_ransack(*associations)
|
19
|
+
associations.each do |association|
|
20
|
+
join_associations.detect {|a| association == a} ||
|
21
|
+
build_polymorphic(association.reflection.name, association.find_parent_in(self) || join_base, association.join_type, association.reflection.klass)
|
22
|
+
end
|
23
|
+
self
|
24
|
+
end
|
25
|
+
|
26
|
+
# Should only be called by Ransack, and only with a single association name
|
27
|
+
def build_polymorphic(association, parent = nil, join_type = Arel::OuterJoin, klass = nil)
|
28
|
+
parent ||= join_parts.last
|
29
|
+
reflection = parent.reflections[association] or
|
30
|
+
raise ::ActiveRecord::ConfigurationError, "Association named '#{ association }' was not found; perhaps you misspelled it?"
|
31
|
+
unless join_association = find_join_association_respecting_polymorphism(reflection, parent, klass)
|
32
|
+
@reflections << reflection
|
33
|
+
join_association = build_join_association_respecting_polymorphism(reflection, parent, klass)
|
34
|
+
join_association.join_type = join_type
|
35
|
+
@join_parts << join_association
|
36
|
+
cache_joined_association(join_association)
|
37
|
+
end
|
38
|
+
|
39
|
+
join_association
|
40
|
+
end
|
41
|
+
|
42
|
+
def find_join_association_respecting_polymorphism(reflection, parent, klass)
|
43
|
+
if association = find_join_association(reflection, parent)
|
44
|
+
unless reflection.options[:polymorphic]
|
45
|
+
association
|
46
|
+
else
|
47
|
+
association if association.active_record == klass
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def build_join_association_respecting_polymorphism(reflection, parent, klass = nil)
|
53
|
+
if reflection.options[:polymorphic] && klass
|
54
|
+
JoinAssociation.new(reflection, self, parent, klass)
|
55
|
+
else
|
56
|
+
JoinAssociation.new(reflection, self, parent)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
data/lib/ransack/context.rb
CHANGED
@@ -1,6 +1,9 @@
|
|
1
|
+
require 'ransack/visitor'
|
2
|
+
|
1
3
|
module Ransack
|
2
4
|
class Context
|
3
5
|
attr_reader :search, :object, :klass, :base, :engine, :arel_visitor
|
6
|
+
attr_accessor :auth_object
|
4
7
|
|
5
8
|
class << self
|
6
9
|
|
@@ -36,9 +39,10 @@ module Ransack
|
|
36
39
|
@engine = @base.arel_engine
|
37
40
|
@arel_visitor = Arel::Visitors.visitor_for @engine
|
38
41
|
@default_table = Arel::Table.new(@base.table_name, :as => @base.aliased_table_name, :engine => @engine)
|
39
|
-
@
|
40
|
-
|
41
|
-
|
42
|
+
@bind_pairs = Hash.new do |hash, key|
|
43
|
+
parent, attr_name = get_parent_and_attribute_name(key.to_s)
|
44
|
+
if parent && attr_name
|
45
|
+
hash[key] = [parent, attr_name]
|
42
46
|
end
|
43
47
|
end
|
44
48
|
end
|
@@ -46,7 +50,12 @@ module Ransack
|
|
46
50
|
# Convert a string representing a chain of associations and an attribute
|
47
51
|
# into the attribute itself
|
48
52
|
def contextualize(str)
|
49
|
-
@
|
53
|
+
parent, attr_name = @bind_pairs[str]
|
54
|
+
table_for(parent)[attr_name]
|
55
|
+
end
|
56
|
+
|
57
|
+
def bind(object, str)
|
58
|
+
object.parent, object.attr_name = @bind_pairs[str]
|
50
59
|
end
|
51
60
|
|
52
61
|
def traverse(str, base = @base)
|
@@ -56,8 +65,9 @@ module Ransack
|
|
56
65
|
association_parts = []
|
57
66
|
found_assoc = nil
|
58
67
|
while !found_assoc && segments.size > 0 && association_parts << segments.shift do
|
59
|
-
|
60
|
-
|
68
|
+
assoc, klass = unpolymorphize_association(association_parts.join('_'))
|
69
|
+
if found_assoc = get_association(assoc, base)
|
70
|
+
base = traverse(segments.join('_'), klass || found_assoc.klass)
|
61
71
|
end
|
62
72
|
end
|
63
73
|
raise ArgumentError, "No association matches #{str}" unless found_assoc
|
@@ -74,10 +84,11 @@ module Ransack
|
|
74
84
|
association_parts = []
|
75
85
|
if (segments = str.split(/_/)).size > 0
|
76
86
|
while segments.size > 0 && !base.columns_hash[segments.join('_')] && association_parts << segments.shift do
|
77
|
-
|
87
|
+
assoc, klass = unpolymorphize_association(association_parts.join('_'))
|
88
|
+
if found_assoc = get_association(assoc, base)
|
78
89
|
path += association_parts
|
79
90
|
association_parts = []
|
80
|
-
base = klassify(found_assoc)
|
91
|
+
base = klassify(klass || found_assoc)
|
81
92
|
end
|
82
93
|
end
|
83
94
|
end
|
@@ -85,67 +96,16 @@ module Ransack
|
|
85
96
|
path.join('_')
|
86
97
|
end
|
87
98
|
|
88
|
-
def
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
def accept(object)
|
93
|
-
visit(object)
|
94
|
-
end
|
95
|
-
|
96
|
-
def can_accept?(object)
|
97
|
-
respond_to? DISPATCH[object.class]
|
98
|
-
end
|
99
|
-
|
100
|
-
def visit_Array(object)
|
101
|
-
object.map {|o| accept(o)}.compact
|
102
|
-
end
|
103
|
-
|
104
|
-
def visit_Ransack_Nodes_Condition(object)
|
105
|
-
object.apply_predicate if object.valid?
|
106
|
-
end
|
107
|
-
|
108
|
-
def visit_Ransack_Nodes_And(object)
|
109
|
-
nodes = object.values.map {|o| accept(o)}.compact
|
110
|
-
return nil unless nodes.size > 0
|
111
|
-
|
112
|
-
if nodes.size > 1
|
113
|
-
Arel::Nodes::Grouping.new(Arel::Nodes::And.new(nodes))
|
99
|
+
def unpolymorphize_association(str)
|
100
|
+
if (match = str.match(/_of_(.+?)_type$/)) && Kernel.const_defined?(match.captures.first)
|
101
|
+
[match.pre_match, Kernel.const_get(match.captures.first)]
|
114
102
|
else
|
115
|
-
|
103
|
+
[str, nil]
|
116
104
|
end
|
117
105
|
end
|
118
106
|
|
119
|
-
def
|
120
|
-
|
121
|
-
end
|
122
|
-
|
123
|
-
def visit_Ransack_Nodes_Or(object)
|
124
|
-
nodes = object.values.map {|o| accept(o)}.compact
|
125
|
-
return nil unless nodes.size > 0
|
126
|
-
|
127
|
-
if nodes.size > 1
|
128
|
-
nodes.inject(&:or)
|
129
|
-
else
|
130
|
-
nodes.first
|
131
|
-
end
|
132
|
-
end
|
133
|
-
|
134
|
-
def quoted?(object)
|
135
|
-
case object
|
136
|
-
when Arel::Nodes::SqlLiteral, Bignum, Fixnum
|
137
|
-
false
|
138
|
-
else
|
139
|
-
true
|
140
|
-
end
|
141
|
-
end
|
142
|
-
|
143
|
-
def visit(object)
|
144
|
-
send(DISPATCH[object.class], object)
|
145
|
-
end
|
146
|
-
|
147
|
-
DISPATCH = Hash.new do |hash, klass|
|
148
|
-
hash[klass] = "visit_#{klass.name.gsub('::', '_')}"
|
107
|
+
def searchable_attributes(str = '')
|
108
|
+
traverse(str).ransackable_attributes(auth_object)
|
149
109
|
end
|
150
110
|
|
151
111
|
end
|
@@ -11,6 +11,12 @@ module Ransack
|
|
11
11
|
super(method, text, options, &block)
|
12
12
|
end
|
13
13
|
|
14
|
+
def submit(value=nil, options={})
|
15
|
+
value, options = nil, value if value.is_a?(Hash)
|
16
|
+
value ||= Translate.word(:search).titleize
|
17
|
+
super(value, options)
|
18
|
+
end
|
19
|
+
|
14
20
|
def attribute_select(options = {}, html_options = {})
|
15
21
|
raise ArgumentError, "attribute_select must be called inside a search FormBuilder!" unless object.respond_to?(:context)
|
16
22
|
options[:include_blank] = true unless options.has_key?(:include_blank)
|
@@ -19,7 +25,7 @@ module Ransack
|
|
19
25
|
collection = bases.map do |base|
|
20
26
|
[
|
21
27
|
Translate.association(base, :context => object.context),
|
22
|
-
object.context.
|
28
|
+
object.context.searchable_attributes(base).map do |c|
|
23
29
|
[
|
24
30
|
attr_from_base_and_column(base, c),
|
25
31
|
Translate.attribute(attr_from_base_and_column(base, c), :context => object.context)
|
@@ -32,7 +38,7 @@ module Ransack
|
|
32
38
|
objectify_options(options), @default_options.merge(html_options)
|
33
39
|
)
|
34
40
|
else
|
35
|
-
collection = object.context.
|
41
|
+
collection = object.context.searchable_attributes(bases.first).map do |c|
|
36
42
|
[
|
37
43
|
attr_from_base_and_column(bases.first, c),
|
38
44
|
Translate.attribute(attr_from_base_and_column(bases.first, c), :context => object.context)
|
@@ -53,7 +59,7 @@ module Ransack
|
|
53
59
|
collection = bases.map do |base|
|
54
60
|
[
|
55
61
|
Translate.association(base, :context => object.context),
|
56
|
-
object.context.
|
62
|
+
object.context.searchable_attributes(base).map do |c|
|
57
63
|
[
|
58
64
|
attr_from_base_and_column(base, c),
|
59
65
|
Translate.attribute(attr_from_base_and_column(base, c), :context => object.context)
|
@@ -69,7 +75,7 @@ module Ransack
|
|
69
75
|
objectify_options(options), @default_options.merge(html_options)
|
70
76
|
)
|
71
77
|
else
|
72
|
-
collection = object.context.
|
78
|
+
collection = object.context.searchable_attributes(bases.first).map do |c|
|
73
79
|
[
|
74
80
|
attr_from_base_and_column(bases.first, c),
|
75
81
|
Translate.attribute(attr_from_base_and_column(bases.first, c), :context => object.context)
|
data/lib/ransack/locale/en.yml
CHANGED
data/lib/ransack/nodes.rb
CHANGED
@@ -1,8 +1,12 @@
|
|
1
1
|
module Ransack
|
2
2
|
module Nodes
|
3
3
|
class Attribute < Node
|
4
|
-
|
5
|
-
|
4
|
+
include Bindable
|
5
|
+
|
6
|
+
attr_reader :name
|
7
|
+
|
8
|
+
delegate :blank?, :present?, :==, :to => :name
|
9
|
+
delegate :engine, :to => :context
|
6
10
|
|
7
11
|
def initialize(context, name = nil)
|
8
12
|
super(context)
|
@@ -11,11 +15,23 @@ module Ransack
|
|
11
15
|
|
12
16
|
def name=(name)
|
13
17
|
@name = name
|
14
|
-
|
18
|
+
context.bind(self, name) unless name.blank?
|
15
19
|
end
|
16
20
|
|
17
21
|
def valid?
|
18
|
-
|
22
|
+
bound? && attr
|
23
|
+
end
|
24
|
+
|
25
|
+
def type
|
26
|
+
if ransacker
|
27
|
+
return ransacker.type
|
28
|
+
else
|
29
|
+
context.type_for(attr)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def cast_value(value)
|
34
|
+
value.cast_to_type(type)
|
19
35
|
end
|
20
36
|
|
21
37
|
def eql?(other)
|
@@ -31,6 +47,7 @@ module Ransack
|
|
31
47
|
def persisted?
|
32
48
|
false
|
33
49
|
end
|
50
|
+
|
34
51
|
end
|
35
52
|
end
|
36
53
|
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Ransack
|
2
|
+
module Nodes
|
3
|
+
module Bindable
|
4
|
+
|
5
|
+
attr_accessor :parent, :attr_name
|
6
|
+
|
7
|
+
def attr
|
8
|
+
@attr ||= ransacker ? ransacker.attr_from(self) : context.table_for(parent)[attr_name]
|
9
|
+
end
|
10
|
+
|
11
|
+
def ransacker
|
12
|
+
klass._ransackers[attr_name]
|
13
|
+
end
|
14
|
+
|
15
|
+
def klass
|
16
|
+
@klass ||= context.klassify(parent)
|
17
|
+
end
|
18
|
+
|
19
|
+
def bound?
|
20
|
+
attr_name.present? && parent.present?
|
21
|
+
end
|
22
|
+
|
23
|
+
def reset_binding!
|
24
|
+
@parent = @attr_name = @attr = @klass = nil
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -4,6 +4,8 @@ module Ransack
|
|
4
4
|
i18n_word :attribute, :predicate, :combinator, :value
|
5
5
|
i18n_alias :a => :attribute, :p => :predicate, :m => :combinator, :v => :value
|
6
6
|
|
7
|
+
delegate :cast_value, :to => :first_attribute
|
8
|
+
|
7
9
|
attr_reader :predicate
|
8
10
|
|
9
11
|
class << self
|
@@ -42,6 +44,10 @@ module Ransack
|
|
42
44
|
values.size <= 1 || predicate.compound || %w(in not_in).include?(predicate.name)
|
43
45
|
end
|
44
46
|
|
47
|
+
def first_attribute
|
48
|
+
attributes.first
|
49
|
+
end
|
50
|
+
|
45
51
|
def attributes
|
46
52
|
@attributes ||= []
|
47
53
|
end
|
@@ -74,12 +80,12 @@ module Ransack
|
|
74
80
|
case args
|
75
81
|
when Array
|
76
82
|
args.each do |val|
|
77
|
-
val = Value.new(@context, val
|
83
|
+
val = Value.new(@context, val)
|
78
84
|
self.values << val
|
79
85
|
end
|
80
86
|
when Hash
|
81
87
|
args.each do |index, attrs|
|
82
|
-
val = Value.new(@context, attrs[:value]
|
88
|
+
val = Value.new(@context, attrs[:value])
|
83
89
|
self.values << val
|
84
90
|
end
|
85
91
|
else
|
@@ -105,13 +111,13 @@ module Ransack
|
|
105
111
|
end
|
106
112
|
|
107
113
|
def build_value(val = nil)
|
108
|
-
Value.new(@context, val
|
114
|
+
Value.new(@context, val).tap do |value|
|
109
115
|
self.values << value
|
110
116
|
end
|
111
117
|
end
|
112
118
|
|
113
119
|
def value
|
114
|
-
predicate.compound ? values.map(
|
120
|
+
predicate.compound ? values.map {|v| cast_value(v)} : cast_value(values.first)
|
115
121
|
end
|
116
122
|
|
117
123
|
def build(params)
|
@@ -121,8 +127,6 @@ module Ransack
|
|
121
127
|
end
|
122
128
|
end
|
123
129
|
|
124
|
-
set_value_types!
|
125
|
-
|
126
130
|
self
|
127
131
|
end
|
128
132
|
|
@@ -162,48 +166,46 @@ module Ransack
|
|
162
166
|
end
|
163
167
|
alias :p :predicate_name
|
164
168
|
|
165
|
-
def
|
166
|
-
|
169
|
+
def arel_predicate
|
170
|
+
predicates = attributes.map do |attr|
|
171
|
+
attr.attr.send(predicate.arel_predicate, formatted_values_for_attribute(attr))
|
172
|
+
end
|
167
173
|
|
168
|
-
if
|
174
|
+
if predicates.size > 1
|
169
175
|
case combinator
|
170
176
|
when 'and'
|
171
|
-
Arel::Nodes::Grouping.new(Arel::Nodes::And.new(
|
172
|
-
attributes.map {|a| a.send(predicate.arel_predicate, predicate.format(values))}
|
173
|
-
))
|
177
|
+
Arel::Nodes::Grouping.new(Arel::Nodes::And.new(predicates))
|
174
178
|
when 'or'
|
175
|
-
|
176
|
-
memo.or(a.send(predicate.arel_predicate, predicate.format(values)))
|
177
|
-
end
|
179
|
+
predicates.inject(&:or)
|
178
180
|
end
|
179
181
|
else
|
180
|
-
|
182
|
+
predicates.first
|
181
183
|
end
|
182
184
|
end
|
183
185
|
|
184
|
-
|
186
|
+
def validated_values
|
187
|
+
values.select {|v| predicate.validator ? predicate.validator.call(v.value) : v.present?}
|
188
|
+
end
|
185
189
|
|
186
|
-
def
|
187
|
-
|
190
|
+
def casted_values_for_attribute(attr)
|
191
|
+
validated_values.map {|v| v.cast_to_type(predicate.type || attr.type)}
|
188
192
|
end
|
189
193
|
|
190
|
-
def
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
194
|
+
def formatted_values_for_attribute(attr)
|
195
|
+
casted_values_for_attribute(attr).map do |val|
|
196
|
+
val = attr.ransacker.formatter.call(val) if attr.ransacker && attr.ransacker.formatter
|
197
|
+
val = predicate.formatter.call(val) if predicate.formatter
|
198
|
+
val
|
195
199
|
end
|
196
200
|
end
|
197
201
|
|
202
|
+
private
|
203
|
+
|
198
204
|
def valid_combinator?
|
199
205
|
attributes.size < 2 ||
|
200
206
|
['and', 'or'].include?(combinator)
|
201
207
|
end
|
202
208
|
|
203
|
-
def arel_attributes
|
204
|
-
attributes.map(&:attr)
|
205
|
-
end
|
206
|
-
|
207
209
|
end
|
208
210
|
end
|
209
211
|
end
|