ransack 0.1.0 → 0.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.
- 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
|