ransack 1.1.0 → 1.2.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 (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