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.
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