ransack 0.1.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/.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
|