ransack 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. data/.gitignore +4 -0
  2. data/Gemfile +11 -0
  3. data/LICENSE +20 -0
  4. data/README.rdoc +5 -0
  5. data/Rakefile +19 -0
  6. data/lib/ransack.rb +24 -0
  7. data/lib/ransack/adapters/active_record.rb +2 -0
  8. data/lib/ransack/adapters/active_record/base.rb +17 -0
  9. data/lib/ransack/adapters/active_record/context.rb +153 -0
  10. data/lib/ransack/configuration.rb +39 -0
  11. data/lib/ransack/constants.rb +23 -0
  12. data/lib/ransack/context.rb +152 -0
  13. data/lib/ransack/helpers.rb +2 -0
  14. data/lib/ransack/helpers/form_builder.rb +172 -0
  15. data/lib/ransack/helpers/form_helper.rb +27 -0
  16. data/lib/ransack/locale/en.yml +67 -0
  17. data/lib/ransack/naming.rb +53 -0
  18. data/lib/ransack/nodes.rb +7 -0
  19. data/lib/ransack/nodes/and.rb +8 -0
  20. data/lib/ransack/nodes/attribute.rb +36 -0
  21. data/lib/ransack/nodes/condition.rb +209 -0
  22. data/lib/ransack/nodes/grouping.rb +207 -0
  23. data/lib/ransack/nodes/node.rb +34 -0
  24. data/lib/ransack/nodes/or.rb +8 -0
  25. data/lib/ransack/nodes/sort.rb +39 -0
  26. data/lib/ransack/nodes/value.rb +120 -0
  27. data/lib/ransack/predicate.rb +57 -0
  28. data/lib/ransack/search.rb +114 -0
  29. data/lib/ransack/translate.rb +92 -0
  30. data/lib/ransack/version.rb +3 -0
  31. data/ransack.gemspec +29 -0
  32. data/spec/blueprints/articles.rb +5 -0
  33. data/spec/blueprints/comments.rb +5 -0
  34. data/spec/blueprints/notes.rb +3 -0
  35. data/spec/blueprints/people.rb +4 -0
  36. data/spec/blueprints/tags.rb +3 -0
  37. data/spec/console.rb +22 -0
  38. data/spec/helpers/ransack_helper.rb +2 -0
  39. data/spec/playground.rb +37 -0
  40. data/spec/ransack/adapters/active_record/base_spec.rb +30 -0
  41. data/spec/ransack/adapters/active_record/context_spec.rb +29 -0
  42. data/spec/ransack/configuration_spec.rb +11 -0
  43. data/spec/ransack/helpers/form_builder_spec.rb +39 -0
  44. data/spec/ransack/nodes/compound_condition_spec.rb +0 -0
  45. data/spec/ransack/nodes/condition_spec.rb +0 -0
  46. data/spec/ransack/nodes/grouping_spec.rb +13 -0
  47. data/spec/ransack/predicate_spec.rb +25 -0
  48. data/spec/ransack/search_spec.rb +182 -0
  49. data/spec/spec_helper.rb +28 -0
  50. data/spec/support/schema.rb +102 -0
  51. 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,8 @@
1
+ require 'ransack/nodes/grouping'
2
+
3
+ module Ransack
4
+ module Nodes
5
+ class Or < Grouping
6
+ end
7
+ end
8
+ 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