ransack 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/Gemfile +11 -0
- data/LICENSE +20 -0
- data/README.rdoc +5 -0
- data/Rakefile +19 -0
- data/lib/ransack.rb +24 -0
- data/lib/ransack/adapters/active_record.rb +2 -0
- data/lib/ransack/adapters/active_record/base.rb +17 -0
- data/lib/ransack/adapters/active_record/context.rb +153 -0
- data/lib/ransack/configuration.rb +39 -0
- data/lib/ransack/constants.rb +23 -0
- data/lib/ransack/context.rb +152 -0
- data/lib/ransack/helpers.rb +2 -0
- data/lib/ransack/helpers/form_builder.rb +172 -0
- data/lib/ransack/helpers/form_helper.rb +27 -0
- data/lib/ransack/locale/en.yml +67 -0
- data/lib/ransack/naming.rb +53 -0
- data/lib/ransack/nodes.rb +7 -0
- data/lib/ransack/nodes/and.rb +8 -0
- data/lib/ransack/nodes/attribute.rb +36 -0
- data/lib/ransack/nodes/condition.rb +209 -0
- data/lib/ransack/nodes/grouping.rb +207 -0
- data/lib/ransack/nodes/node.rb +34 -0
- data/lib/ransack/nodes/or.rb +8 -0
- data/lib/ransack/nodes/sort.rb +39 -0
- data/lib/ransack/nodes/value.rb +120 -0
- data/lib/ransack/predicate.rb +57 -0
- data/lib/ransack/search.rb +114 -0
- data/lib/ransack/translate.rb +92 -0
- data/lib/ransack/version.rb +3 -0
- data/ransack.gemspec +29 -0
- data/spec/blueprints/articles.rb +5 -0
- data/spec/blueprints/comments.rb +5 -0
- data/spec/blueprints/notes.rb +3 -0
- data/spec/blueprints/people.rb +4 -0
- data/spec/blueprints/tags.rb +3 -0
- data/spec/console.rb +22 -0
- data/spec/helpers/ransack_helper.rb +2 -0
- data/spec/playground.rb +37 -0
- data/spec/ransack/adapters/active_record/base_spec.rb +30 -0
- data/spec/ransack/adapters/active_record/context_spec.rb +29 -0
- data/spec/ransack/configuration_spec.rb +11 -0
- data/spec/ransack/helpers/form_builder_spec.rb +39 -0
- data/spec/ransack/nodes/compound_condition_spec.rb +0 -0
- data/spec/ransack/nodes/condition_spec.rb +0 -0
- data/spec/ransack/nodes/grouping_spec.rb +13 -0
- data/spec/ransack/predicate_spec.rb +25 -0
- data/spec/ransack/search_spec.rb +182 -0
- data/spec/spec_helper.rb +28 -0
- data/spec/support/schema.rb +102 -0
- metadata +200 -0
@@ -0,0 +1,207 @@
|
|
1
|
+
module Ransack
|
2
|
+
module Nodes
|
3
|
+
class Grouping < Node
|
4
|
+
attr_reader :conditions
|
5
|
+
i18n_word :condition, :and, :or
|
6
|
+
i18n_alias :c => :condition, :n => :and, :o => :or
|
7
|
+
|
8
|
+
delegate :each, :to => :values
|
9
|
+
|
10
|
+
def persisted?
|
11
|
+
false
|
12
|
+
end
|
13
|
+
|
14
|
+
def translate(key, options = {})
|
15
|
+
super or Translate.attribute(key.to_s, options.merge(:context => context))
|
16
|
+
end
|
17
|
+
|
18
|
+
def conditions
|
19
|
+
@conditions ||= []
|
20
|
+
end
|
21
|
+
alias :c :conditions
|
22
|
+
|
23
|
+
def conditions=(conditions)
|
24
|
+
case conditions
|
25
|
+
when Array
|
26
|
+
conditions.each do |attrs|
|
27
|
+
condition = Condition.new(@context).build(attrs)
|
28
|
+
self.conditions << condition if condition.valid?
|
29
|
+
end
|
30
|
+
when Hash
|
31
|
+
conditions.each do |index, attrs|
|
32
|
+
condition = Condition.new(@context).build(attrs)
|
33
|
+
self.conditions << condition if condition.valid?
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
self.conditions.uniq!
|
38
|
+
end
|
39
|
+
alias :c= :conditions=
|
40
|
+
|
41
|
+
def [](key)
|
42
|
+
if condition = conditions.detect {|c| c.key == key.to_s}
|
43
|
+
condition
|
44
|
+
else
|
45
|
+
nil
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def []=(key, value)
|
50
|
+
conditions.reject! {|c| c.key == key.to_s}
|
51
|
+
self.conditions << value
|
52
|
+
end
|
53
|
+
|
54
|
+
def values
|
55
|
+
conditions + ors + ands
|
56
|
+
end
|
57
|
+
|
58
|
+
def respond_to?(method_id)
|
59
|
+
super or begin
|
60
|
+
method_name = method_id.to_s
|
61
|
+
writer = method_name.sub!(/\=$/, '')
|
62
|
+
attribute_method?(method_name) ? true : false
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def build_condition(opts = {})
|
67
|
+
new_condition(opts).tap do |condition|
|
68
|
+
self.conditions << condition
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def new_condition(opts = {})
|
73
|
+
attrs = opts[:attributes] || 1
|
74
|
+
vals = opts[:values] || 1
|
75
|
+
condition = Condition.new(@context)
|
76
|
+
condition.predicate = Predicate.named('eq')
|
77
|
+
attrs.times { condition.build_attribute }
|
78
|
+
vals.times { condition.build_value }
|
79
|
+
condition
|
80
|
+
end
|
81
|
+
|
82
|
+
def ands
|
83
|
+
@ands ||= []
|
84
|
+
end
|
85
|
+
alias :n :ands
|
86
|
+
|
87
|
+
def ands=(ands)
|
88
|
+
case ands
|
89
|
+
when Array
|
90
|
+
ands.each do |attrs|
|
91
|
+
and_object = And.new(@context).build(attrs)
|
92
|
+
self.ands << and_object if and_object.values.any?
|
93
|
+
end
|
94
|
+
when Hash
|
95
|
+
ands.each do |index, attrs|
|
96
|
+
and_object = And.new(@context).build(attrs)
|
97
|
+
self.ands << and_object if and_object.values.any?
|
98
|
+
end
|
99
|
+
else
|
100
|
+
raise ArgumentError, "Invalid argument (#{ands.class}) supplied to ands="
|
101
|
+
end
|
102
|
+
end
|
103
|
+
alias :n= :ands=
|
104
|
+
|
105
|
+
def ors
|
106
|
+
@ors ||= []
|
107
|
+
end
|
108
|
+
alias :o :ors
|
109
|
+
|
110
|
+
def ors=(ors)
|
111
|
+
case ors
|
112
|
+
when Array
|
113
|
+
ors.each do |attrs|
|
114
|
+
or_object = Or.new(@context).build(attrs)
|
115
|
+
self.ors << or_object if or_object.values.any?
|
116
|
+
end
|
117
|
+
when Hash
|
118
|
+
ors.each do |index, attrs|
|
119
|
+
or_object = Or.new(@context).build(attrs)
|
120
|
+
self.ors << or_object if or_object.values.any?
|
121
|
+
end
|
122
|
+
else
|
123
|
+
raise ArgumentError, "Invalid argument (#{ors.class}) supplied to ors="
|
124
|
+
end
|
125
|
+
end
|
126
|
+
alias :o= :ors=
|
127
|
+
|
128
|
+
def method_missing(method_id, *args)
|
129
|
+
method_name = method_id.to_s
|
130
|
+
writer = method_name.sub!(/\=$/, '')
|
131
|
+
if attribute_method?(method_name)
|
132
|
+
writer ? write_attribute(method_name, *args) : read_attribute(method_name)
|
133
|
+
else
|
134
|
+
super
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def attribute_method?(name)
|
139
|
+
name = strip_predicate_and_index(name)
|
140
|
+
case name
|
141
|
+
when /^(n|o|c|ands|ors|conditions)=?$/
|
142
|
+
true
|
143
|
+
else
|
144
|
+
name.split(/_and_|_or_/).select {|n| !@context.attribute_method?(n)}.empty?
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def build_and(params = {})
|
149
|
+
params ||= {}
|
150
|
+
new_and(params).tap do |new_and|
|
151
|
+
self.ands << new_and
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def new_and(params = {})
|
156
|
+
And.new(@context).build(params)
|
157
|
+
end
|
158
|
+
|
159
|
+
def build_or(params = {})
|
160
|
+
params ||= {}
|
161
|
+
new_or(params).tap do |new_or|
|
162
|
+
self.ors << new_or
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def new_or(params = {})
|
167
|
+
Or.new(@context).build(params)
|
168
|
+
end
|
169
|
+
|
170
|
+
def build(params)
|
171
|
+
params.with_indifferent_access.each do |key, value|
|
172
|
+
case key
|
173
|
+
when /^(n|o|c)$/
|
174
|
+
self.send("#{key}=", value)
|
175
|
+
else
|
176
|
+
write_attribute(key.to_s, value)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
self
|
180
|
+
end
|
181
|
+
|
182
|
+
private
|
183
|
+
|
184
|
+
def write_attribute(name, val)
|
185
|
+
# TODO: Methods
|
186
|
+
if condition = Condition.extract(@context, name, val)
|
187
|
+
self[name] = condition
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def read_attribute(name)
|
192
|
+
if self[name].respond_to?(:value)
|
193
|
+
self[name].value
|
194
|
+
else
|
195
|
+
self[name]
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
def strip_predicate_and_index(str)
|
200
|
+
string = str.split(/\(/).first
|
201
|
+
Ransack::Configuration.predicate_keys.detect {|p| string.sub!(/_#{p}$/, '')}
|
202
|
+
string
|
203
|
+
end
|
204
|
+
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Ransack
|
2
|
+
module Nodes
|
3
|
+
class Node
|
4
|
+
attr_reader :context
|
5
|
+
delegate :contextualize, :to => :context
|
6
|
+
class_attribute :i18n_words
|
7
|
+
class_attribute :i18n_aliases
|
8
|
+
self.i18n_words = []
|
9
|
+
self.i18n_aliases = {}
|
10
|
+
|
11
|
+
class << self
|
12
|
+
def i18n_word(*args)
|
13
|
+
self.i18n_words += args.map(&:to_s)
|
14
|
+
end
|
15
|
+
|
16
|
+
def i18n_alias(opts = {})
|
17
|
+
self.i18n_aliases.merge! Hash[opts.map {|k, v| [k.to_s, v.to_s]}]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize(context)
|
22
|
+
@context = context
|
23
|
+
end
|
24
|
+
|
25
|
+
def translate(key, options = {})
|
26
|
+
key = i18n_aliases[key.to_s] if i18n_aliases.has_key?(key.to_s)
|
27
|
+
if i18n_words.include?(key.to_s)
|
28
|
+
Translate.word(key)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Ransack
|
2
|
+
module Nodes
|
3
|
+
class Sort < Node
|
4
|
+
attr_reader :name, :attr, :dir
|
5
|
+
i18n_word :asc, :desc
|
6
|
+
|
7
|
+
class << self
|
8
|
+
def extract(context, str)
|
9
|
+
attr, direction = str.split(/\s+/,2)
|
10
|
+
self.new(context).build(:name => attr, :dir => direction)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def build(params)
|
15
|
+
params.with_indifferent_access.each do |key, value|
|
16
|
+
if key.match(/^(name|dir)$/)
|
17
|
+
self.send("#{key}=", value)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
self
|
22
|
+
end
|
23
|
+
|
24
|
+
def valid?
|
25
|
+
@attr
|
26
|
+
end
|
27
|
+
|
28
|
+
def name=(name)
|
29
|
+
@name = name
|
30
|
+
@attr = contextualize(name) unless name.blank?
|
31
|
+
end
|
32
|
+
|
33
|
+
def dir=(dir)
|
34
|
+
@dir = %w(asc desc).include?(dir) ? dir : 'asc'
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
module Ransack
|
2
|
+
module Nodes
|
3
|
+
class Value < Node
|
4
|
+
attr_reader :value_before_cast, :type
|
5
|
+
delegate :blank?, :to => :value_before_cast
|
6
|
+
|
7
|
+
def initialize(context, value = nil, type = nil)
|
8
|
+
super(context)
|
9
|
+
@value_before_cast = value
|
10
|
+
self.type = type if type
|
11
|
+
end
|
12
|
+
|
13
|
+
def value=(val)
|
14
|
+
@value_before_cast = value
|
15
|
+
@value = nil
|
16
|
+
end
|
17
|
+
|
18
|
+
def value
|
19
|
+
@value ||= cast_to_type(@value_before_cast, @type)
|
20
|
+
end
|
21
|
+
|
22
|
+
def persisted?
|
23
|
+
false
|
24
|
+
end
|
25
|
+
|
26
|
+
def eql?(other)
|
27
|
+
self.class == other.class &&
|
28
|
+
self.value_before_cast == other.value_before_cast
|
29
|
+
end
|
30
|
+
alias :== :eql?
|
31
|
+
|
32
|
+
def hash
|
33
|
+
value_before_cast.hash
|
34
|
+
end
|
35
|
+
|
36
|
+
def type=(type)
|
37
|
+
@value = nil
|
38
|
+
@type = type
|
39
|
+
end
|
40
|
+
|
41
|
+
def cast_to_type(val, type)
|
42
|
+
case type
|
43
|
+
when :date
|
44
|
+
cast_to_date(val)
|
45
|
+
when :datetime, :timestamp, :time
|
46
|
+
cast_to_time(val)
|
47
|
+
when :boolean
|
48
|
+
cast_to_boolean(val)
|
49
|
+
when :integer
|
50
|
+
cast_to_integer(val)
|
51
|
+
when :float
|
52
|
+
cast_to_float(val)
|
53
|
+
when :decimal
|
54
|
+
cast_to_decimal(val)
|
55
|
+
else
|
56
|
+
cast_to_string(val)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def cast_to_date(val)
|
61
|
+
if val.respond_to?(:to_date)
|
62
|
+
val.to_date rescue nil
|
63
|
+
else
|
64
|
+
y, m, d = *[val].flatten
|
65
|
+
m ||= 1
|
66
|
+
d ||= 1
|
67
|
+
Date.new(y,m,d) rescue nil
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# FIXME: doesn't seem to be casting, even with Time.zone.local
|
72
|
+
def cast_to_time(val)
|
73
|
+
if val.is_a?(Array)
|
74
|
+
Time.zone.local(*val) rescue nil
|
75
|
+
else
|
76
|
+
unless val.acts_like?(:time)
|
77
|
+
val = val.is_a?(String) ? Time.zone.parse(val) : val.to_time rescue val
|
78
|
+
end
|
79
|
+
val.in_time_zone
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def cast_to_boolean(val)
|
84
|
+
if val.is_a?(String) && val.blank?
|
85
|
+
nil
|
86
|
+
else
|
87
|
+
Constants::TRUE_VALUES.include?(val)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def cast_to_string(val)
|
92
|
+
val.respond_to?(:to_s) ? val.to_s : String.new(val)
|
93
|
+
end
|
94
|
+
|
95
|
+
def cast_to_integer(val)
|
96
|
+
val.blank? ? nil : val.to_i
|
97
|
+
end
|
98
|
+
|
99
|
+
def cast_to_float(val)
|
100
|
+
val.blank? ? nil : val.to_f
|
101
|
+
end
|
102
|
+
|
103
|
+
def cast_to_decimal(val)
|
104
|
+
if val.blank?
|
105
|
+
nil
|
106
|
+
elsif val.class == BigDecimal
|
107
|
+
val
|
108
|
+
elsif val.respond_to?(:to_d)
|
109
|
+
val.to_d
|
110
|
+
else
|
111
|
+
val.to_s.to_d
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def array_of_arrays?(val)
|
116
|
+
Array === val && Array === val.first
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Ransack
|
2
|
+
class Predicate
|
3
|
+
attr_reader :name, :arel_predicate, :type, :formatter, :validator, :compound
|
4
|
+
|
5
|
+
class << self
|
6
|
+
def named(name)
|
7
|
+
Configuration.predicates[name.to_s]
|
8
|
+
end
|
9
|
+
|
10
|
+
def for_attribute_name(attribute_name)
|
11
|
+
self.named(Configuration.predicate_keys.detect {|p| attribute_name.to_s.match(/_#{p}$/)})
|
12
|
+
end
|
13
|
+
|
14
|
+
def collection
|
15
|
+
Configuration.predicates.map {|k, v| [k, Translate.predicate(k)]}
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(opts = {})
|
20
|
+
@name = opts[:name]
|
21
|
+
@arel_predicate = opts[:arel_predicate]
|
22
|
+
@type = opts[:type]
|
23
|
+
@formatter = opts[:formatter]
|
24
|
+
@validator = opts[:validator]
|
25
|
+
@compound = opts[:compound]
|
26
|
+
end
|
27
|
+
|
28
|
+
def format(vals)
|
29
|
+
if formatter
|
30
|
+
vals.select {|v| validator ? validator.call(v.value_before_cast) : !v.blank?}.
|
31
|
+
map {|v| formatter.call(v.value)}
|
32
|
+
else
|
33
|
+
vals.select {|v| validator ? validator.call(v.value_before_cast) : !v.blank?}.
|
34
|
+
map {|v| v.value}
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def eql?(other)
|
39
|
+
self.class == other.class &&
|
40
|
+
self.name == other.name
|
41
|
+
end
|
42
|
+
alias :== :eql?
|
43
|
+
|
44
|
+
def hash
|
45
|
+
name.hash
|
46
|
+
end
|
47
|
+
|
48
|
+
def validate(vals)
|
49
|
+
if validator
|
50
|
+
vals.select {|v| validator.call(v.value_before_cast)}.any?
|
51
|
+
else
|
52
|
+
vals.select {|v| !v.blank?}.any?
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|