ransack 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +12 -4
  3. data/CONTRIBUTING.md +10 -4
  4. data/Gemfile +12 -9
  5. data/README.md +46 -11
  6. data/lib/ransack.rb +4 -2
  7. data/lib/ransack/adapters/active_record.rb +1 -1
  8. data/lib/ransack/adapters/active_record/3.0/compat.rb +16 -6
  9. data/lib/ransack/adapters/active_record/3.0/context.rb +32 -16
  10. data/lib/ransack/adapters/active_record/3.1/context.rb +32 -15
  11. data/lib/ransack/adapters/active_record/3.2/context.rb +1 -1
  12. data/lib/ransack/adapters/active_record/base.rb +9 -6
  13. data/lib/ransack/adapters/active_record/context.rb +193 -2
  14. data/lib/ransack/configuration.rb +4 -4
  15. data/lib/ransack/constants.rb +81 -18
  16. data/lib/ransack/context.rb +27 -12
  17. data/lib/ransack/helpers/form_builder.rb +126 -91
  18. data/lib/ransack/helpers/form_helper.rb +34 -12
  19. data/lib/ransack/naming.rb +2 -1
  20. data/lib/ransack/nodes/attribute.rb +6 -4
  21. data/lib/ransack/nodes/bindable.rb +3 -1
  22. data/lib/ransack/nodes/condition.rb +40 -27
  23. data/lib/ransack/nodes/grouping.rb +19 -13
  24. data/lib/ransack/nodes/node.rb +3 -3
  25. data/lib/ransack/nodes/sort.rb +5 -3
  26. data/lib/ransack/nodes/value.rb +2 -2
  27. data/lib/ransack/predicate.rb +18 -9
  28. data/lib/ransack/ransacker.rb +4 -4
  29. data/lib/ransack/search.rb +9 -12
  30. data/lib/ransack/translate.rb +42 -21
  31. data/lib/ransack/version.rb +1 -1
  32. data/lib/ransack/visitor.rb +4 -4
  33. data/ransack.gemspec +17 -7
  34. data/spec/blueprints/notes.rb +2 -0
  35. data/spec/blueprints/people.rb +4 -1
  36. data/spec/console.rb +3 -3
  37. data/spec/ransack/adapters/active_record/base_spec.rb +149 -22
  38. data/spec/ransack/adapters/active_record/context_spec.rb +5 -5
  39. data/spec/ransack/configuration_spec.rb +17 -8
  40. data/spec/ransack/dependencies_spec.rb +8 -0
  41. data/spec/ransack/helpers/form_builder_spec.rb +37 -14
  42. data/spec/ransack/helpers/form_helper_spec.rb +5 -5
  43. data/spec/ransack/predicate_spec.rb +6 -3
  44. data/spec/ransack/search_spec.rb +95 -73
  45. data/spec/ransack/translate_spec.rb +14 -0
  46. data/spec/spec_helper.rb +14 -8
  47. data/spec/support/en.yml +6 -0
  48. data/spec/support/schema.rb +76 -31
  49. metadata +48 -29
@@ -41,4 +41,4 @@ module Ransack
41
41
  end
42
42
  end
43
43
  end
44
- end
44
+ end
@@ -4,7 +4,7 @@ module Ransack
4
4
  module Base
5
5
 
6
6
  def self.extended(base)
7
- alias :search :ransack unless base.method_defined? :search
7
+ alias :search :ransack unless base.respond_to? :search
8
8
  base.class_eval do
9
9
  class_attribute :_ransackers
10
10
  self._ransackers ||= {}
@@ -12,11 +12,13 @@ module Ransack
12
12
  end
13
13
 
14
14
  def ransack(params = {}, options = {})
15
- Search.new(self, params, options)
15
+ Search.new(self, params ? params.delete_if {
16
+ |k, v| v.blank? && v != false } : params, options)
16
17
  end
17
18
 
18
19
  def ransacker(name, opts = {}, &block)
19
- self._ransackers = _ransackers.merge name.to_s => Ransacker.new(self, name, opts, &block)
20
+ self._ransackers = _ransackers.merge name.to_s => Ransacker
21
+ .new(self, name, opts, &block)
20
22
  end
21
23
 
22
24
  def ransackable_attributes(auth_object = nil)
@@ -24,15 +26,16 @@ module Ransack
24
26
  end
25
27
 
26
28
  def ransortable_attributes(auth_object = nil)
27
- # Here so users can overwrite the attributes that show up in the sort_select
29
+ # Here so users can overwrite the attributes
30
+ # that show up in the sort_select
28
31
  ransackable_attributes(auth_object)
29
32
  end
30
33
 
31
34
  def ransackable_associations(auth_object = nil)
32
- reflect_on_all_associations.map {|a| a.name.to_s}
35
+ reflect_on_all_associations.map { |a| a.name.to_s }
33
36
  end
34
37
 
35
38
  end
36
39
  end
37
40
  end
38
- end
41
+ end
@@ -1,5 +1,4 @@
1
1
  require 'ransack/context'
2
- require 'ransack/adapters/active_record/3.2/context'
3
2
  require 'ransack/adapters/active_record/compat'
4
3
  require 'polyamorous'
5
4
 
@@ -8,6 +7,10 @@ module Ransack
8
7
  module ActiveRecord
9
8
  class Context < ::Ransack::Context
10
9
 
10
+ # Because the AR::Associations namespace is insane
11
+ JoinDependency = ::ActiveRecord::Associations::JoinDependency
12
+ JoinPart = JoinDependency::JoinPart
13
+
11
14
  def initialize(object, options = {})
12
15
  super
13
16
  @arel_visitor = @engine.connection.visitor
@@ -31,11 +34,199 @@ module Ransack
31
34
  viz = Visitor.new
32
35
  relation = @object.where(viz.accept(search.base))
33
36
  if search.sorts.any?
34
- relation = relation.except(:order).reorder(viz.accept(search.sorts))
37
+ relation = relation.except(:order)
38
+ .reorder(viz.accept(search.sorts))
35
39
  end
36
40
  opts[:distinct] ? relation.distinct : relation
37
41
  end
38
42
 
43
+ def attribute_method?(str, klass = @klass)
44
+ exists = false
45
+ if ransackable_attribute?(str, klass)
46
+ exists = true
47
+ elsif (segments = str.split(/_/)).size > 1
48
+ remainder = []
49
+ found_assoc = nil
50
+ while !found_assoc && remainder.unshift(
51
+ segments.pop) && segments.size > 0 do
52
+ assoc, poly_class = unpolymorphize_association(
53
+ segments.join('_')
54
+ )
55
+ if found_assoc = get_association(assoc, klass)
56
+ exists = attribute_method?(remainder.join('_'),
57
+ poly_class || found_assoc.klass
58
+ )
59
+ end
60
+ end
61
+ end
62
+ exists
63
+ end
64
+
65
+ def table_for(parent)
66
+ parent.table
67
+ end
68
+
69
+ def klassify(obj)
70
+ if Class === obj && ::ActiveRecord::Base > obj
71
+ obj
72
+ elsif obj.respond_to? :klass
73
+ obj.klass
74
+ elsif obj.respond_to? :base_klass
75
+ obj.base_klass
76
+ else
77
+ raise ArgumentError, "Don't know how to klassify #{obj}"
78
+ end
79
+ end
80
+
81
+ private
82
+
83
+ def get_parent_and_attribute_name(str, parent = @base)
84
+ attr_name = nil
85
+
86
+ if ransackable_attribute?(str, klassify(parent))
87
+ attr_name = str
88
+ elsif (segments = str.split(/_/)).size > 1
89
+ remainder = []
90
+ found_assoc = nil
91
+ while remainder.unshift(
92
+ segments.pop) && segments.size > 0 && !found_assoc do
93
+ assoc, klass = unpolymorphize_association(segments.join('_'))
94
+ if found_assoc = get_association(assoc, parent)
95
+ join = build_or_find_association(found_assoc.name, parent, klass)
96
+ parent, attr_name = get_parent_and_attribute_name(
97
+ remainder.join('_'), join
98
+ )
99
+ end
100
+ end
101
+ end
102
+
103
+ [parent, attr_name]
104
+ end
105
+
106
+ def get_association(str, parent = @base)
107
+ klass = klassify parent
108
+ ransackable_association?(str, klass) &&
109
+ klass.reflect_on_all_associations.detect { |a| a.name.to_s == str }
110
+ end
111
+
112
+ def join_dependency(relation)
113
+ if relation.respond_to?(:join_dependency) # Squeel will enable this
114
+ relation.join_dependency
115
+ else
116
+ build_join_dependency(relation)
117
+ end
118
+ end
119
+
120
+ # Checkout active_record/relation/query_methods.rb +build_joins+ for
121
+ # reference. Lots of duplicated code maybe we can avoid it
122
+ def build_join_dependency(relation)
123
+ buckets = relation.joins_values.group_by do |join|
124
+ case join
125
+ when String
126
+ 'string_join'
127
+ when Hash, Symbol, Array
128
+ 'association_join'
129
+ when JoinDependency, JoinDependency::JoinAssociation
130
+ 'stashed_join'
131
+ when Arel::Nodes::Join
132
+ 'join_node'
133
+ else
134
+ raise 'unknown class: %s' % join.class.name
135
+ end
136
+ end
137
+
138
+ association_joins = buckets['association_join'] || []
139
+
140
+ stashed_association_joins = buckets['stashed_join'] || []
141
+
142
+ join_nodes = buckets['join_node'] || []
143
+
144
+ string_joins = (buckets['string_join'] || [])
145
+ .map { |x| x.strip }
146
+ .uniq
147
+
148
+ join_list = relation.send :custom_join_ast,
149
+ relation.table.from(relation.table), string_joins
150
+
151
+ join_dependency = JoinDependency.new(
152
+ relation.klass, association_joins, join_list
153
+ )
154
+
155
+ join_nodes.each do |join|
156
+ join_dependency.alias_tracker.aliases[join.left.name.downcase] = 1
157
+ end
158
+
159
+ if ::ActiveRecord::VERSION::STRING >= "4.1"
160
+ join_dependency
161
+ else
162
+ join_dependency.graft(*stashed_association_joins)
163
+ end
164
+ end
165
+
166
+ if ::ActiveRecord::VERSION::STRING >= "4.1"
167
+
168
+ def build_or_find_association(name, parent = @base, klass = nil)
169
+ list = if ::ActiveRecord::VERSION::STRING >= "4.1"
170
+ @join_dependency.join_root.children.detect
171
+ else
172
+ @join_dependency.join_associations
173
+ end
174
+
175
+ found_association = list.detect do |assoc|
176
+ assoc.reflection.name == name &&
177
+ @associations_pot[assoc] == parent &&
178
+ (!klass || assoc.reflection.klass == klass)
179
+ end
180
+
181
+ unless found_association
182
+ jd = JoinDependency.new(
183
+ parent.base_klass,
184
+ Polyamorous::Join.new(name, @join_type, klass),
185
+ []
186
+ )
187
+ found_association = jd.join_root.children.last
188
+ associations found_association, parent
189
+
190
+ # TODO maybe we dont need to push associations here, we could loop
191
+ # through the @associations_pot instead
192
+ @join_dependency.join_root.children.push found_association
193
+
194
+ # Builds the arel nodes properly for this association
195
+ @join_dependency.send(
196
+ :construct_tables!, jd.join_root, found_association
197
+ )
198
+
199
+ # Leverage the stashed association functionality in AR
200
+ @object = @object.joins(jd)
201
+ end
202
+
203
+ found_association
204
+ end
205
+
206
+ def associations(assoc, parent)
207
+ @associations_pot ||= {}
208
+ @associations_pot[assoc] = parent
209
+ end
210
+ else
211
+
212
+ def build_or_find_association(name, parent = @base, klass = nil)
213
+ found_association = @join_dependency.join_associations
214
+ .detect do |assoc|
215
+ assoc.reflection.name == name &&
216
+ assoc.parent == parent &&
217
+ (!klass || assoc.reflection.klass == klass)
218
+ end
219
+ unless found_association
220
+ @join_dependency.send(:build, Polyamorous::Join.new(
221
+ name, @join_type, klass), parent)
222
+ found_association = @join_dependency.join_associations.last
223
+ # Leverage the stashed association functionality in AR
224
+ @object = @object.joins(found_association)
225
+ end
226
+
227
+ found_association
228
+ end
229
+ end
39
230
  end
40
231
  end
41
232
  end
@@ -7,7 +7,7 @@ module Ransack
7
7
  mattr_accessor :predicates, :options
8
8
  self.predicates = {}
9
9
  self.options = {
10
- :search_key => :q
10
+ search_key: :q
11
11
  }
12
12
 
13
13
  def configure
@@ -27,9 +27,9 @@ module Ransack
27
27
  ['_any', '_all'].each do |suffix|
28
28
  self.predicates[name + suffix] = Predicate.new(
29
29
  opts.merge(
30
- :name => name + suffix,
31
- :arel_predicate => opts[:arel_predicate] + suffix,
32
- :compound => true
30
+ name: name + suffix,
31
+ arel_predicate: opts[:arel_predicate] + suffix,
32
+ compound: true
33
33
  )
34
34
  )
35
35
  end if compounds
@@ -6,31 +6,94 @@ module Ransack
6
6
  AREL_PREDICATES = %w(eq not_eq matches does_not_match lt lteq gt gteq in not_in)
7
7
 
8
8
  DERIVED_PREDICATES = [
9
- ['cont', {:arel_predicate => 'matches', :formatter => proc {|v| "%#{escape_wildcards(v)}%"}}],
10
- ['not_cont', {:arel_predicate => 'does_not_match', :formatter => proc {|v| "%#{escape_wildcards(v)}%"}}],
11
- ['start', {:arel_predicate => 'matches', :formatter => proc {|v| "#{escape_wildcards(v)}%"}}],
12
- ['not_start', {:arel_predicate => 'does_not_match', :formatter => proc {|v| "#{escape_wildcards(v)}%"}}],
13
- ['end', {:arel_predicate => 'matches', :formatter => proc {|v| "%#{escape_wildcards(v)}"}}],
14
- ['not_end', {:arel_predicate => 'does_not_match', :formatter => proc {|v| "%#{escape_wildcards(v)}"}}],
15
- ['true', {:arel_predicate => 'eq', :compounds => false, :type => :boolean, :validator => proc {|v| TRUE_VALUES.include?(v)}}],
16
- ['false', {:arel_predicate => 'eq', :compounds => false, :type => :boolean, :validator => proc {|v| TRUE_VALUES.include?(v)}, :formatter => proc {|v| !v}}],
17
- ['present', {:arel_predicate => 'not_eq_all', :compounds => false, :type => :boolean, :validator => proc {|v| TRUE_VALUES.include?(v)}, :formatter => proc {|v| [nil, '']}}],
18
- ['blank', {:arel_predicate => 'eq_any', :compounds => false, :type => :boolean, :validator => proc {|v| TRUE_VALUES.include?(v)}, :formatter => proc {|v| [nil, '']}}],
19
- ['null', {:arel_predicate => 'eq', :compounds => false, :type => :boolean, :validator => proc {|v| TRUE_VALUES.include?(v)}, :formatter => proc {|v| nil}}],
20
- ['not_null', {:arel_predicate => 'not_eq', :compounds => false, :type => :boolean, :validator => proc {|v| TRUE_VALUES.include?(v)}, :formatter => proc {|v| nil}}]
9
+ ['cont', {
10
+ arel_predicate: 'matches',
11
+ formatter: proc { |v| "%#{escape_wildcards(v)}%" }
12
+ }
13
+ ],
14
+ ['not_cont', {
15
+ arel_predicate: 'does_not_match',
16
+ formatter: proc { |v| "%#{escape_wildcards(v)}%" }
17
+ }
18
+ ],
19
+ ['start', {
20
+ arel_predicate: 'matches',
21
+ formatter: proc { |v| "#{escape_wildcards(v)}%" }
22
+ }
23
+ ],
24
+ ['not_start', {
25
+ arel_predicate: 'does_not_match',
26
+ formatter: proc { |v| "#{escape_wildcards(v)}%" }
27
+ }
28
+ ],
29
+ ['end', {
30
+ arel_predicate: 'matches',
31
+ formatter: proc { |v| "%#{escape_wildcards(v)}" }
32
+ }
33
+ ],
34
+ ['not_end', {
35
+ arel_predicate: 'does_not_match',
36
+ formatter: proc { |v| "%#{escape_wildcards(v)}" }
37
+ }
38
+ ],
39
+ ['true', {
40
+ arel_predicate: 'eq',
41
+ compounds: false,
42
+ type: :boolean,
43
+ validator: proc { |v| TRUE_VALUES.include?(v) }
44
+ }
45
+ ],
46
+ ['false', {
47
+ arel_predicate: 'eq',
48
+ compounds: false,
49
+ type: :boolean,
50
+ validator: proc { |v| TRUE_VALUES.include?(v) },
51
+ formatter: proc { |v| !v }
52
+ }
53
+ ],
54
+ ['present', {
55
+ arel_predicate: 'not_eq_all',
56
+ compounds: false,
57
+ type: :boolean,
58
+ validator: proc { |v| TRUE_VALUES.include?(v) },
59
+ formatter: proc { |v| [nil, ''] }
60
+ }
61
+ ],
62
+ ['blank', {
63
+ arel_predicate: 'eq_any',
64
+ compounds: false,
65
+ type: :boolean,
66
+ validator: proc { |v| TRUE_VALUES.include?(v) },
67
+ formatter: proc { |v| [nil, ''] }
68
+ }
69
+ ],
70
+ ['null', {
71
+ arel_predicate: 'eq',
72
+ compounds: false,
73
+ type: :boolean,
74
+ validator: proc { |v| TRUE_VALUES.include?(v)},
75
+ formatter: proc { |v| nil }
76
+ }
77
+ ],
78
+ ['not_null', {
79
+ arel_predicate: 'not_eq',
80
+ compounds: false,
81
+ type: :boolean,
82
+ validator: proc { |v| TRUE_VALUES.include?(v) },
83
+ formatter: proc { |v| nil } }
84
+ ]
21
85
  ]
22
86
 
23
87
  module_function
24
88
  # replace % \ to \% \\
25
89
  def escape_wildcards(unescaped)
26
90
  case ActiveRecord::Base.connection.adapter_name
27
- when "SQLite"
28
- unescaped
29
- else
30
- # Necessary for PostgreSQL and MySQL
31
- unescaped.to_s.gsub(/([\\|\%|.])/, '\\\\\\1')
91
+ when "SQLite"
92
+ unescaped
93
+ else
94
+ # Necessary for PostgreSQL and MySQL
95
+ unescaped.to_s.gsub(/([\\|\%|.])/, '\\\\\\1')
32
96
  end
33
97
  end
34
-
35
98
  end
36
99
  end
@@ -8,8 +8,11 @@ module Ransack
8
8
  class << self
9
9
 
10
10
  def for(object, options = {})
11
- context = Class === object ? for_class(object, options) : for_object(object, options)
12
- context or raise ArgumentError, "Don't know what context to use for #{object}"
11
+ context = Class === object ?
12
+ for_class(object, options) :
13
+ for_object(object, options)
14
+ context or raise ArgumentError,
15
+ "Don't know what context to use for #{object}"
13
16
  end
14
17
 
15
18
  def for_class(klass, options = {})
@@ -33,9 +36,18 @@ module Ransack
33
36
  @join_dependency = join_dependency(@object)
34
37
  @join_type = options[:join_type] || Arel::OuterJoin
35
38
  @search_key = options[:search_key] || Ransack.options[:search_key]
36
- @base = @join_dependency.join_base
37
- @engine = @base.arel_engine
38
- @default_table = Arel::Table.new(@base.table_name, :as => @base.aliased_table_name, :engine => @engine)
39
+
40
+ if ::ActiveRecord::VERSION::STRING >= "4.1"
41
+ @base = @join_dependency.join_root
42
+ @engine = @base.base_klass.arel_engine
43
+ else
44
+ @base = @join_dependency.join_base
45
+ @engine = @base.arel_engine
46
+ end
47
+
48
+ @default_table = Arel::Table.new(
49
+ @base.table_name, as: @base.aliased_table_name, engine: @engine
50
+ )
39
51
  @bind_pairs = Hash.new do |hash, key|
40
52
  parent, attr_name = get_parent_and_attribute_name(key.to_s)
41
53
  if parent && attr_name
@@ -54,7 +66,7 @@ module Ransack
54
66
  elsif obj.respond_to? :base_klass # Rails 4
55
67
  obj.base_klass
56
68
  else
57
- raise ArgumentError, "Don't know how to klassify #{obj}"
69
+ raise ArgumentError, "Don't know how to klassify #{obj.inspect}"
58
70
  end
59
71
  end
60
72
 
@@ -85,7 +97,8 @@ module Ransack
85
97
 
86
98
  remainder.unshift segments.pop
87
99
  end
88
- raise UntraversableAssociationError, "No association matches #{str}" unless found_assoc
100
+ raise UntraversableAssociationError,
101
+ "No association matches #{str}" unless found_assoc
89
102
  end
90
103
 
91
104
  klassify(base)
@@ -98,8 +111,10 @@ module Ransack
98
111
  segments = str.split(/_/)
99
112
  association_parts = []
100
113
  if (segments = str.split(/_/)).size > 0
101
- while segments.size > 0 && !base.columns_hash[segments.join('_')] && association_parts << segments.shift do
102
- assoc, klass = unpolymorphize_association(association_parts.join('_'))
114
+ while segments.size > 0 && !base.columns_hash[segments.join('_')] &&
115
+ association_parts << segments.shift do
116
+ assoc, klass = unpolymorphize_association(association_parts
117
+ .join('_'))
103
118
  if found_assoc = get_association(assoc, base)
104
119
  path += association_parts
105
120
  association_parts = []
@@ -120,7 +135,8 @@ module Ransack
120
135
  end
121
136
 
122
137
  def ransackable_attribute?(str, klass)
123
- klass.ransackable_attributes(auth_object).include? str
138
+ klass.ransackable_attributes(auth_object).include?(str) ||
139
+ klass.ransortable_attributes(auth_object).include?(str)
124
140
  end
125
141
 
126
142
  def ransackable_association?(str, klass)
@@ -138,6 +154,5 @@ module Ransack
138
154
  def searchable_associations(str = '')
139
155
  traverse(str).ransackable_associations(auth_object)
140
156
  end
141
-
142
157
  end
143
- end
158
+ end