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,2 @@
1
+ require 'ransack/helpers/form_builder'
2
+ require 'ransack/helpers/form_helper'
@@ -0,0 +1,172 @@
1
+ require 'action_view'
2
+
3
+ module Ransack
4
+ module Helpers
5
+ class FormBuilder < ::ActionView::Helpers::FormBuilder
6
+ def label(method, *args, &block)
7
+ options = args.extract_options!
8
+ text = args.first
9
+ i18n = options[:i18n] || {}
10
+ text ||= object.translate(method, i18n.reverse_merge(:include_associations => true)) if object.respond_to? :translate
11
+ super(method, text, options, &block)
12
+ end
13
+
14
+ def attribute_select(options = {}, html_options = {})
15
+ raise ArgumentError, "attribute_select must be called inside a search FormBuilder!" unless object.respond_to?(:context)
16
+ options[:include_blank] = true unless options.has_key?(:include_blank)
17
+ bases = [''] + association_array(options[:associations])
18
+ if bases.size > 1
19
+ collection = bases.map do |base|
20
+ [
21
+ Translate.association(base, :context => object.context),
22
+ object.context.searchable_columns(base).map do |c|
23
+ [
24
+ attr_from_base_and_column(base, c),
25
+ Translate.attribute(attr_from_base_and_column(base, c), :context => object.context)
26
+ ]
27
+ end
28
+ ]
29
+ end
30
+ @template.grouped_collection_select(
31
+ @object_name, :name, collection, :last, :first, :first, :last,
32
+ objectify_options(options), @default_options.merge(html_options)
33
+ )
34
+ else
35
+ collection = object.context.searchable_columns(bases.first).map do |c|
36
+ [
37
+ attr_from_base_and_column(bases.first, c),
38
+ Translate.attribute(attr_from_base_and_column(bases.first, c), :context => object.context)
39
+ ]
40
+ end
41
+ @template.collection_select(
42
+ @object_name, :name, collection, :first, :last,
43
+ objectify_options(options), @default_options.merge(html_options)
44
+ )
45
+ end
46
+ end
47
+
48
+ def sort_select(options = {}, html_options = {})
49
+ raise ArgumentError, "sort_select must be called inside a search FormBuilder!" unless object.respond_to?(:context)
50
+ options[:include_blank] = true unless options.has_key?(:include_blank)
51
+ bases = [''] + association_array(options[:associations])
52
+ if bases.any?
53
+ collection = bases.map do |base|
54
+ [
55
+ Translate.association(base, :context => object.context),
56
+ object.context.searchable_columns(base).map do |c|
57
+ [
58
+ attr_from_base_and_column(base, c),
59
+ Translate.attribute(attr_from_base_and_column(base, c), :context => object.context)
60
+ ]
61
+ end
62
+ ]
63
+ end
64
+ @template.grouped_collection_select(
65
+ @object_name, :name, collection, :last, :first, :first, :last,
66
+ objectify_options(options), @default_options.merge(html_options)
67
+ ) + @template.collection_select(
68
+ @object_name, :dir, [['asc', object.translate('asc')], ['desc', object.translate('desc')]], :first, :last,
69
+ objectify_options(options), @default_options.merge(html_options)
70
+ )
71
+ else
72
+ collection = object.context.searchable_columns(bases.first).map do |c|
73
+ [
74
+ attr_from_base_and_column(bases.first, c),
75
+ Translate.attribute(attr_from_base_and_column(bases.first, c), :context => object.context)
76
+ ]
77
+ end
78
+ @template.collection_select(
79
+ @object_name, :name, collection, :first, :last,
80
+ objectify_options(options), @default_options.merge(html_options)
81
+ ) + @template.collection_select(
82
+ @object_name, :dir, [['asc', object.translate('asc')], ['desc', object.translate('desc')]], :first, :last,
83
+ objectify_options(options), @default_options.merge(html_options)
84
+ )
85
+ end
86
+ end
87
+
88
+ def sort_fields(*args, &block)
89
+ search_fields(:s, args, block)
90
+ end
91
+
92
+ def condition_fields(*args, &block)
93
+ search_fields(:c, args, block)
94
+ end
95
+
96
+ def and_fields(*args, &block)
97
+ search_fields(:n, args, block)
98
+ end
99
+
100
+ def or_fields(*args, &block)
101
+ search_fields(:o, args, block)
102
+ end
103
+
104
+ def attribute_fields(*args, &block)
105
+ search_fields(:a, args, block)
106
+ end
107
+
108
+ def predicate_fields(*args, &block)
109
+ search_fields(:p, args, block)
110
+ end
111
+
112
+ def value_fields(*args, &block)
113
+ search_fields(:v, args, block)
114
+ end
115
+
116
+ def search_fields(name, args, block)
117
+ args << {} unless args.last.is_a?(Hash)
118
+ args.last[:builder] ||= options[:builder]
119
+ args.last[:parent_builder] = self
120
+ options = args.extract_options!
121
+ objects = args.shift
122
+ objects ||= @object.send(name)
123
+ objects = [objects] unless Array === objects
124
+ name = "#{options[:object_name] || object_name}[#{name}]"
125
+ output = ActiveSupport::SafeBuffer.new
126
+ objects.each do |child|
127
+ output << @template.fields_for("#{name}[#{options[:child_index] || nested_child_index(name)}]", child, options, &block)
128
+ end
129
+ output
130
+ end
131
+
132
+ def predicate_select(options = {}, html_options = {})
133
+ @template.collection_select(
134
+ @object_name, :p, Predicate.collection, :first, :last,
135
+ objectify_options(options), @default_options.merge(html_options)
136
+ )
137
+ end
138
+
139
+ def combinator_select(options = {}, html_options = {})
140
+ @template.collection_select(
141
+ @object_name, :m, [['or', Translate.word(:or)], ['and', Translate.word(:and)]], :first, :last,
142
+ objectify_options(options), @default_options.merge(html_options)
143
+ )
144
+ end
145
+
146
+ private
147
+
148
+ def association_array(obj, prefix = nil)
149
+ ([prefix] + case obj
150
+ when Array
151
+ obj
152
+ when Hash
153
+ obj.map do |key, value|
154
+ case value
155
+ when Array, Hash
156
+ bases_array(value, key.to_s)
157
+ else
158
+ [key.to_s, [key, value].join('_')]
159
+ end
160
+ end
161
+ else
162
+ [obj]
163
+ end).compact.flatten.map {|v| [prefix, v].compact.join('_')}
164
+ end
165
+
166
+ def attr_from_base_and_column(base, column)
167
+ [base, column].reject {|v| v.blank?}.join('_')
168
+ end
169
+
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,27 @@
1
+ module Ransack
2
+ module Helpers
3
+ module FormHelper
4
+ def search_form_for(record, options = {}, &proc)
5
+ if record.is_a?(Ransack::Search)
6
+ search = record
7
+ options[:url] ||= polymorphic_path(search.klass)
8
+ elsif record.is_a?(Array) && (search = record.detect {|o| o.is_a?(Ransack::Search)})
9
+ options[:url] ||= polymorphic_path(record.map {|o| o.is_a?(Ransack::Search) ? o.klass : o})
10
+ else
11
+ raise ArgumentError, "No Ransack::Search object was provided to search_form_for!"
12
+ end
13
+ options[:html] ||= {}
14
+ html_options = {
15
+ :class => options[:as] ? "#{options[:as]}_search" : "#{search.klass.to_s.underscore}_search",
16
+ :id => options[:as] ? "#{options[:as]}_search" : "#{search.klass.to_s.underscore}_search",
17
+ :method => :get
18
+ }
19
+ options[:html].reverse_merge!(html_options)
20
+ options[:builder] ||= FormBuilder
21
+
22
+ form_for(record, options, &proc)
23
+ end
24
+
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,67 @@
1
+ en:
2
+ ransack:
3
+ predicate: "predicate"
4
+ and: "and"
5
+ or: "or"
6
+ combinator: "combinator"
7
+ attribute: "attribute"
8
+ value: "value"
9
+ condition: "condition"
10
+ sort: "sort"
11
+ asc: "ascending"
12
+ desc: "descending"
13
+ predicates:
14
+ eq: "equals"
15
+ eq_any: "equals any"
16
+ eq_all: "equals all"
17
+ not_eq: "not equal to"
18
+ not_eq_any: "not equal to any"
19
+ not_eq_all: "not equal to all"
20
+ matches: "matches"
21
+ matches_any: "matches_any"
22
+ matches_all: "matches all"
23
+ does_not_match: "doesn't match"
24
+ does_not_match_any: "doesn't match any"
25
+ does_not_match_all: "doesn't match all"
26
+ lt: "less than"
27
+ lt_any: "less than any"
28
+ lt_all: "less than all"
29
+ lteq: "less than or equal to"
30
+ lteq_any: "less than or equal to any"
31
+ lteq_all: "less than or equal to all"
32
+ gt: "greater than"
33
+ gt_any: "greater than any"
34
+ gt_all: "greater than all"
35
+ gteq: "greater than or equal to"
36
+ gteq_any: "greater than or equal to any"
37
+ gteq_all: "greater than or equal to all"
38
+ in: "in"
39
+ in_any: "in any"
40
+ in_all: "in all"
41
+ not_in: "not in"
42
+ not_in_any: "not in any"
43
+ not_in_all: "not in all"
44
+ cont: "contains"
45
+ cont_any: "contains any"
46
+ cont_all: "contains all"
47
+ not_cont: "doesn't contain"
48
+ not_cont_any: "doesn't contain any"
49
+ not_cont_all: "doesn't contain all"
50
+ start: "starts with"
51
+ start_any: "starts with any"
52
+ start_all: "starts with all"
53
+ not_start: "doesn't start with"
54
+ not_start_any: "doesn't start with any"
55
+ not_start_all: "doesn't start with all"
56
+ end: "ends with"
57
+ end_any: "ends with any"
58
+ end_all: "ends with all"
59
+ not_end: "doesn't end with"
60
+ not_end_any: "doesn't end with any"
61
+ not_end_all: "doesn't end with all"
62
+ true: "is true"
63
+ false: "is false"
64
+ present: "is present"
65
+ blank: "is blank"
66
+ null: "is null"
67
+ not_null: "is not null"
@@ -0,0 +1,53 @@
1
+ module Ransack
2
+ module Naming
3
+
4
+ def self.included(base)
5
+ base.extend ClassMethods
6
+ end
7
+
8
+ def persisted?
9
+ false
10
+ end
11
+
12
+ def to_key
13
+ nil
14
+ end
15
+
16
+ def to_param
17
+ nil
18
+ end
19
+
20
+ def to_model
21
+ self
22
+ end
23
+ end
24
+
25
+ class Name < String
26
+ attr_reader :singular, :plural, :element, :collection, :partial_path, :human, :param_key, :route_key, :i18n_key
27
+ alias_method :cache_key, :collection
28
+
29
+ def initialize
30
+ super("Search")
31
+ @singular = "search".freeze
32
+ @plural = "searches".freeze
33
+ @element = "search".freeze
34
+ @human = "Search".freeze
35
+ @collection = "ransack/searches".freeze
36
+ @partial_path = "#{@collection}/#{@element}".freeze
37
+ @param_key = "q".freeze
38
+ @route_key = "searches".freeze
39
+ @i18n_key = :ransack
40
+ end
41
+ end
42
+
43
+ module ClassMethods
44
+ def model_name
45
+ @_model_name ||= Name.new
46
+ end
47
+
48
+ def i18n_scope
49
+ :ransack
50
+ end
51
+ end
52
+
53
+ end
@@ -0,0 +1,7 @@
1
+ require 'ransack/nodes/node'
2
+ require 'ransack/nodes/attribute'
3
+ require 'ransack/nodes/value'
4
+ require 'ransack/nodes/condition'
5
+ require 'ransack/nodes/sort'
6
+ require 'ransack/nodes/and'
7
+ require 'ransack/nodes/or'
@@ -0,0 +1,8 @@
1
+ require 'ransack/nodes/grouping'
2
+
3
+ module Ransack
4
+ module Nodes
5
+ class And < Grouping
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,36 @@
1
+ module Ransack
2
+ module Nodes
3
+ class Attribute < Node
4
+ attr_reader :name, :attr
5
+ delegate :blank?, :==, :to => :name
6
+
7
+ def initialize(context, name = nil)
8
+ super(context)
9
+ self.name = name unless name.blank?
10
+ end
11
+
12
+ def name=(name)
13
+ @name = name
14
+ @attr = contextualize(name) unless name.blank?
15
+ end
16
+
17
+ def valid?
18
+ @attr
19
+ end
20
+
21
+ def eql?(other)
22
+ self.class == other.class &&
23
+ self.name == other.name
24
+ end
25
+ alias :== :eql?
26
+
27
+ def hash
28
+ self.name.hash
29
+ end
30
+
31
+ def persisted?
32
+ false
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,209 @@
1
+ module Ransack
2
+ module Nodes
3
+ class Condition < Node
4
+ i18n_word :attribute, :predicate, :combinator, :value
5
+ i18n_alias :a => :attribute, :p => :predicate, :m => :combinator, :v => :value
6
+
7
+ attr_reader :predicate
8
+
9
+ class << self
10
+ def extract(context, key, values)
11
+ attributes, predicate = extract_attributes_and_predicate(key)
12
+ if attributes.size > 0
13
+ combinator = key.match(/_(or|and)_/) ? $1 : nil
14
+ condition = self.new(context)
15
+ condition.build(
16
+ :a => attributes,
17
+ :p => predicate.name,
18
+ :m => combinator,
19
+ :v => [values]
20
+ )
21
+ predicate.validate(condition.values) ? condition : nil
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def extract_attributes_and_predicate(key)
28
+ str = key.dup
29
+ name = Ransack::Configuration.predicate_keys.detect {|p| str.sub!(/_#{p}$/, '')}
30
+ predicate = Predicate.named(name)
31
+ raise ArgumentError, "No valid predicate for #{key}" unless predicate
32
+ attributes = str.split(/_and_|_or_/)
33
+ [attributes, predicate]
34
+ end
35
+ end
36
+
37
+ def valid?
38
+ attributes.detect(&:valid?) && predicate && valid_arity? && predicate.validate(values) && valid_combinator?
39
+ end
40
+
41
+ def valid_arity?
42
+ values.size <= 1 || predicate.compound || %w(in not_in).include?(predicate.name)
43
+ end
44
+
45
+ def attributes
46
+ @attributes ||= []
47
+ end
48
+ alias :a :attributes
49
+
50
+ def attributes=(args)
51
+ case args
52
+ when Array
53
+ args.each do |attr|
54
+ attr = Attribute.new(@context, attr)
55
+ self.attributes << attr if attr.valid?
56
+ end
57
+ when Hash
58
+ args.each do |index, attrs|
59
+ attr = Attribute.new(@context, attrs[:name])
60
+ self.attributes << attr if attr.valid?
61
+ end
62
+ else
63
+ raise ArgumentError, "Invalid argument (#{args.class}) supplied to attributes="
64
+ end
65
+ end
66
+ alias :a= :attributes=
67
+
68
+ def values
69
+ @values ||= []
70
+ end
71
+ alias :v :values
72
+
73
+ def values=(args)
74
+ case args
75
+ when Array
76
+ args.each do |val|
77
+ val = Value.new(@context, val, current_type)
78
+ self.values << val
79
+ end
80
+ when Hash
81
+ args.each do |index, attrs|
82
+ val = Value.new(@context, attrs[:value], current_type)
83
+ self.values << val
84
+ end
85
+ else
86
+ raise ArgumentError, "Invalid argument (#{args.class}) supplied to values="
87
+ end
88
+ end
89
+ alias :v= :values=
90
+
91
+ def combinator
92
+ @attributes.size > 1 ? @combinator : nil
93
+ end
94
+
95
+ def combinator=(val)
96
+ @combinator = ['and', 'or'].detect {|v| v == val.to_s} || nil
97
+ end
98
+ alias :m= :combinator=
99
+ alias :m :combinator
100
+
101
+ def build_attribute(name = nil)
102
+ Attribute.new(@context, name).tap do |attribute|
103
+ self.attributes << attribute
104
+ end
105
+ end
106
+
107
+ def build_value(val = nil)
108
+ Value.new(@context, val, current_type).tap do |value|
109
+ self.values << value
110
+ end
111
+ end
112
+
113
+ def value
114
+ predicate.compound ? values.map(&:value) : values.first.value
115
+ end
116
+
117
+ def build(params)
118
+ params.with_indifferent_access.each do |key, value|
119
+ if key.match(/^(a|v|p|m)$/)
120
+ self.send("#{key}=", value)
121
+ end
122
+ end
123
+
124
+ set_value_types!
125
+
126
+ self
127
+ end
128
+
129
+ def persisted?
130
+ false
131
+ end
132
+
133
+ def key
134
+ @key ||= attributes.map(&:name).join("_#{combinator}_") + "_#{predicate.name}"
135
+ end
136
+
137
+ def eql?(other)
138
+ self.class == other.class &&
139
+ self.attributes == other.attributes &&
140
+ self.predicate == other.predicate &&
141
+ self.values == other.values &&
142
+ self.combinator == other.combinator
143
+ end
144
+ alias :== :eql?
145
+
146
+ def hash
147
+ [attributes, predicate, values, combinator].hash
148
+ end
149
+
150
+ def predicate_name=(name)
151
+ self.predicate = Predicate.named(name)
152
+ end
153
+ alias :p= :predicate_name=
154
+
155
+ def predicate=(predicate)
156
+ @predicate = predicate
157
+ predicate
158
+ end
159
+
160
+ def predicate_name
161
+ predicate.name if predicate
162
+ end
163
+ alias :p :predicate_name
164
+
165
+ def apply_predicate
166
+ attributes = arel_attributes.compact
167
+
168
+ if attributes.size > 1
169
+ case combinator
170
+ when 'and'
171
+ Arel::Nodes::Grouping.new(Arel::Nodes::And.new(
172
+ attributes.map {|a| a.send(predicate.arel_predicate, predicate.format(values))}
173
+ ))
174
+ when 'or'
175
+ attributes.inject(attributes.shift.send(predicate.arel_predicate, predicate.format(values))) do |memo, a|
176
+ memo.or(a.send(predicate.arel_predicate, predicate.format(values)))
177
+ end
178
+ end
179
+ else
180
+ attributes.first.send(predicate.arel_predicate, predicate.format(values))
181
+ end
182
+ end
183
+
184
+ private
185
+
186
+ def set_value_types!
187
+ self.values.each {|v| v.type = current_type}
188
+ end
189
+
190
+ def current_type
191
+ if predicate && predicate.type
192
+ predicate.type
193
+ elsif attributes.size > 0
194
+ @context.type_for(attributes.first.attr)
195
+ end
196
+ end
197
+
198
+ def valid_combinator?
199
+ attributes.size < 2 ||
200
+ ['and', 'or'].include?(combinator)
201
+ end
202
+
203
+ def arel_attributes
204
+ attributes.map(&:attr)
205
+ end
206
+
207
+ end
208
+ end
209
+ end