ransack 3.0.0 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/codeql.yml +72 -0
  3. data/.github/workflows/deploy.yml +35 -0
  4. data/.github/workflows/test-deploy.yml +29 -0
  5. data/.github/workflows/test.yml +22 -39
  6. data/.rubocop.yml +3 -0
  7. data/CHANGELOG.md +118 -0
  8. data/CONTRIBUTING.md +38 -16
  9. data/Gemfile +10 -10
  10. data/README.md +9 -14
  11. data/bug_report_templates/test-ransacker-arel-present-predicate.rb +4 -0
  12. data/docs/.gitignore +0 -1
  13. data/docs/docs/getting-started/advanced-mode.md +1 -1
  14. data/docs/docs/getting-started/search-matches.md +1 -1
  15. data/docs/docs/getting-started/simple-mode.md +30 -26
  16. data/docs/docs/getting-started/sorting.md +1 -1
  17. data/docs/docs/getting-started/using-predicates.md +1 -1
  18. data/docs/docs/going-further/acts-as-taggable-on.md +114 -0
  19. data/docs/docs/going-further/documentation.md +14 -2
  20. data/docs/docs/going-further/exporting-to-csv.md +2 -2
  21. data/docs/docs/going-further/form-customisation.md +1 -1
  22. data/docs/docs/going-further/i18n.md +3 -3
  23. data/docs/docs/going-further/merging-searches.md +1 -1
  24. data/docs/docs/going-further/other-notes.md +1 -1
  25. data/docs/docs/going-further/polymorphic-search.md +40 -0
  26. data/docs/docs/going-further/saving-queries.md +1 -1
  27. data/docs/docs/going-further/searching-postgres.md +1 -1
  28. data/docs/docs/going-further/wiki-contributors.md +82 -0
  29. data/docs/docs/intro.md +2 -2
  30. data/docs/docusaurus.config.js +16 -4
  31. data/docs/package.json +7 -2
  32. data/docs/yarn.lock +3036 -1917
  33. data/lib/polyamorous/activerecord_6.1_ruby_2/reflection.rb +11 -1
  34. data/lib/polyamorous/activerecord_7.1_ruby_2/join_association.rb +1 -0
  35. data/lib/polyamorous/activerecord_7.1_ruby_2/join_dependency.rb +1 -0
  36. data/lib/polyamorous/activerecord_7.1_ruby_2/reflection.rb +1 -0
  37. data/lib/ransack/adapters/active_record/base.rb +78 -7
  38. data/lib/ransack/adapters/active_record/context.rb +17 -49
  39. data/lib/ransack/configuration.rb +25 -12
  40. data/lib/ransack/constants.rb +125 -3
  41. data/lib/ransack/context.rb +34 -5
  42. data/lib/ransack/helpers/form_builder.rb +3 -3
  43. data/lib/ransack/helpers/form_helper.rb +4 -3
  44. data/lib/ransack/nodes/attribute.rb +2 -2
  45. data/lib/ransack/nodes/condition.rb +80 -7
  46. data/lib/ransack/nodes/grouping.rb +3 -3
  47. data/lib/ransack/nodes/node.rb +1 -1
  48. data/lib/ransack/nodes/value.rb +2 -2
  49. data/lib/ransack/predicate.rb +1 -1
  50. data/lib/ransack/ransacker.rb +1 -1
  51. data/lib/ransack/search.rb +9 -4
  52. data/lib/ransack/translate.rb +2 -2
  53. data/lib/ransack/version.rb +1 -1
  54. data/lib/ransack/visitor.rb +38 -2
  55. data/lib/ransack.rb +3 -6
  56. data/ransack.gemspec +3 -3
  57. data/spec/helpers/polyamorous_helper.rb +2 -8
  58. data/spec/ransack/adapters/active_record/base_spec.rb +73 -0
  59. data/spec/ransack/configuration_spec.rb +9 -9
  60. data/spec/ransack/helpers/form_builder_spec.rb +8 -8
  61. data/spec/ransack/helpers/form_helper_spec.rb +60 -2
  62. data/spec/ransack/nodes/condition_spec.rb +24 -0
  63. data/spec/ransack/nodes/value_spec.rb +115 -0
  64. data/spec/ransack/predicate_spec.rb +36 -1
  65. data/spec/ransack/translate_spec.rb +1 -1
  66. data/spec/support/schema.rb +27 -10
  67. metadata +20 -21
  68. data/docs/package-lock.json +0 -9207
  69. data/lib/polyamorous/activerecord_6.0_ruby_2/join_association.rb +0 -20
  70. data/lib/polyamorous/activerecord_6.0_ruby_2/join_dependency.rb +0 -79
  71. data/lib/polyamorous/activerecord_6.0_ruby_2/reflection.rb +0 -11
  72. data/lib/polyamorous.rb +0 -1
  73. data/lib/ransack/adapters/active_record/ransack/constants.rb +0 -128
  74. data/lib/ransack/adapters/active_record/ransack/context.rb +0 -56
  75. data/lib/ransack/adapters/active_record/ransack/nodes/condition.rb +0 -69
  76. data/lib/ransack/adapters/active_record/ransack/translate.rb +0 -8
  77. data/lib/ransack/adapters/active_record/ransack/visitor.rb +0 -47
  78. data/lib/ransack/adapters.rb +0 -64
  79. data/lib/ransack/nodes.rb +0 -8
  80. /data/lib/ransack/{adapters/active_record.rb → active_record.rb} +0 -0
@@ -1 +1,11 @@
1
- require 'polyamorous/activerecord_6.0_ruby_2/reflection'
1
+ module Polyamorous
2
+ module ReflectionExtensions
3
+ def join_scope(table, foreign_table, foreign_klass)
4
+ if respond_to?(:polymorphic?) && polymorphic?
5
+ super.where!(foreign_table[foreign_type].eq(klass.name))
6
+ else
7
+ super
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1 @@
1
+ require 'polyamorous/activerecord_6.1_ruby_2/join_association'
@@ -0,0 +1 @@
1
+ require 'polyamorous/activerecord_6.1_ruby_2/join_dependency'
@@ -0,0 +1 @@
1
+ require 'polyamorous/activerecord_6.1_ruby_2/reflection'
@@ -35,12 +35,7 @@ module Ransack
35
35
  # For overriding with a whitelist array of strings.
36
36
  #
37
37
  def ransackable_attributes(auth_object = nil)
38
- @ransackable_attributes ||= if Ransack::SUPPORTS_ATTRIBUTE_ALIAS
39
- column_names + _ransackers.keys + _ransack_aliases.keys +
40
- attribute_aliases.keys
41
- else
42
- column_names + _ransackers.keys + _ransack_aliases.keys
43
- end
38
+ @ransackable_attributes ||= deprecated_ransackable_list(:ransackable_attributes)
44
39
  end
45
40
 
46
41
  # Ransackable_associations, by default, returns the names
@@ -48,7 +43,7 @@ module Ransack
48
43
  # For overriding with a whitelist array of strings.
49
44
  #
50
45
  def ransackable_associations(auth_object = nil)
51
- @ransackable_associations ||= reflect_on_all_associations.map { |a| a.name.to_s }
46
+ @ransackable_associations ||= deprecated_ransackable_list(:ransackable_associations)
52
47
  end
53
48
 
54
49
  # Ransortable_attributes, by default, returns the names
@@ -75,6 +70,82 @@ module Ransack
75
70
  []
76
71
  end
77
72
 
73
+ # Bare list of all potentially searchable attributes. Searchable attributes
74
+ # need to be explicitly allowlisted through the `ransackable_attributes`
75
+ # method in each model, but if you're allowing almost everything to be
76
+ # searched, this list can be used as a base for exclusions.
77
+ #
78
+ def authorizable_ransackable_attributes
79
+ if Ransack::SUPPORTS_ATTRIBUTE_ALIAS
80
+ column_names + _ransackers.keys + _ransack_aliases.keys +
81
+ attribute_aliases.keys
82
+ else
83
+ column_names + _ransackers.keys + _ransack_aliases.keys
84
+ end.uniq
85
+ end
86
+
87
+ # Bare list of all potentially searchable associations. Searchable
88
+ # associations need to be explicitly allowlisted through the
89
+ # `ransackable_associations` method in each model, but if you're
90
+ # allowing almost everything to be searched, this list can be used as a
91
+ # base for exclusions.
92
+ #
93
+ def authorizable_ransackable_associations
94
+ reflect_on_all_associations.map { |a| a.name.to_s }
95
+ end
96
+
97
+ private
98
+
99
+ def deprecated_ransackable_list(method)
100
+ list_type = method.to_s.delete_prefix("ransackable_")
101
+
102
+ if explicitly_defined?(method)
103
+ warn_deprecated <<~ERROR
104
+ Ransack's builtin `#{method}` method is deprecated and will result
105
+ in an error in the future. If you want to authorize the full list
106
+ of searchable #{list_type} for this model, use
107
+ `authorizable_#{method}` instead of delegating to `super`.
108
+ ERROR
109
+
110
+ public_send("authorizable_#{method}")
111
+ else
112
+ raise <<~MESSAGE
113
+ Ransack needs #{name} #{list_type} explicitly allowlisted as
114
+ searchable. Define a `#{method}` class method in your `#{name}`
115
+ model, watching out for items you DON'T want searchable (for
116
+ example, `encrypted_password`, `password_reset_token`, `owner` or
117
+ other sensitive information). You can use the following as a base:
118
+
119
+ ```ruby
120
+ class #{name} < ApplicationRecord
121
+
122
+ # ...
123
+
124
+ def self.#{method}(auth_object = nil)
125
+ #{public_send("authorizable_#{method}").sort.inspect}
126
+ end
127
+
128
+ # ...
129
+
130
+ end
131
+ ```
132
+ MESSAGE
133
+ end
134
+ end
135
+
136
+ def explicitly_defined?(method)
137
+ definer_ancestor = singleton_class.ancestors.find do |ancestor|
138
+ ancestor.instance_methods(false).include?(method)
139
+ end
140
+
141
+ definer_ancestor != Ransack::Adapters::ActiveRecord::Base
142
+ end
143
+
144
+ def warn_deprecated(message)
145
+ caller_location = caller_locations.find { |location| !location.path.start_with?(File.expand_path("../..", __dir__)) }
146
+
147
+ warn "DEPRECATION WARNING: #{message.squish} (called at #{caller_location.path}:#{caller_location.lineno})"
148
+ end
78
149
  end
79
150
  end
80
151
  end
@@ -110,13 +110,7 @@ module Ransack
110
110
  def join_sources
111
111
  base, joins = begin
112
112
  alias_tracker = ::ActiveRecord::Associations::AliasTracker.create(self.klass.connection, @object.table.name, [])
113
- constraints = if ::Gem::Version.new(::ActiveRecord::VERSION::STRING) >= ::Gem::Version.new(Constants::RAILS_6_1)
114
- @join_dependency.join_constraints(@object.joins_values, alias_tracker, @object.references_values)
115
- elsif ::Gem::Version.new(::ActiveRecord::VERSION::STRING) >= ::Gem::Version.new(Constants::RAILS_6_0)
116
- @join_dependency.join_constraints(@object.joins_values, alias_tracker)
117
- else
118
- @join_dependency.join_constraints(@object.joins_values, @join_type, alias_tracker)
119
- end
113
+ constraints = @join_dependency.join_constraints(@object.joins_values, alias_tracker, @object.references_values)
120
114
 
121
115
  [
122
116
  Arel::SelectManager.new(@object.table),
@@ -284,11 +278,7 @@ module Ransack
284
278
  join_list = join_nodes + convert_join_strings_to_ast(relation.table, string_joins)
285
279
 
286
280
  alias_tracker = ::ActiveRecord::Associations::AliasTracker.create(self.klass.connection, relation.table.name, join_list)
287
- join_dependency = if ::Gem::Version.new(::ActiveRecord::VERSION::STRING) >= ::Gem::Version.new(Constants::RAILS_6_0)
288
- Polyamorous::JoinDependency.new(relation.klass, relation.table, association_joins, Arel::Nodes::OuterJoin)
289
- else
290
- Polyamorous::JoinDependency.new(relation.klass, relation.table, association_joins)
291
- end
281
+ join_dependency = Polyamorous::JoinDependency.new(relation.klass, relation.table, association_joins, Arel::Nodes::OuterJoin)
292
282
  join_dependency.instance_variable_set(:@alias_tracker, alias_tracker)
293
283
  join_nodes.each do |join|
294
284
  join_dependency.send(:alias_tracker).aliases[join.left.name.downcase] = 1
@@ -315,22 +305,13 @@ module Ransack
315
305
  end
316
306
 
317
307
  def build_association(name, parent = @base, klass = nil)
318
- if ::Gem::Version.new(::ActiveRecord::VERSION::STRING) >= ::Gem::Version.new(Constants::RAILS_6_0)
319
- jd = Polyamorous::JoinDependency.new(
320
- parent.base_klass,
321
- parent.table,
322
- Polyamorous::Join.new(name, @join_type, klass),
323
- @join_type
324
- )
325
- found_association = jd.instance_variable_get(:@join_root).children.last
326
- else
327
- jd = Polyamorous::JoinDependency.new(
328
- parent.base_klass,
329
- parent.table,
330
- Polyamorous::Join.new(name, @join_type, klass)
331
- )
332
- found_association = jd.instance_variable_get(:@join_root).children.last
333
- end
308
+ jd = Polyamorous::JoinDependency.new(
309
+ parent.base_klass,
310
+ parent.table,
311
+ Polyamorous::Join.new(name, @join_type, klass),
312
+ @join_type
313
+ )
314
+ found_association = jd.instance_variable_get(:@join_root).children.last
334
315
 
335
316
  @associations_pot[found_association] = parent
336
317
 
@@ -339,11 +320,7 @@ module Ransack
339
320
  @join_dependency.instance_variable_get(:@join_root).children.push found_association
340
321
 
341
322
  # Builds the arel nodes properly for this association
342
- if ::Gem::Version.new(::ActiveRecord::VERSION::STRING) >= ::Gem::Version.new(Constants::RAILS_6_1)
343
- @tables_pot[found_association] = @join_dependency.construct_tables_for_association!(jd.instance_variable_get(:@join_root), found_association)
344
- else
345
- @join_dependency.send(:construct_tables!, jd.instance_variable_get(:@join_root))
346
- end
323
+ @tables_pot[found_association] = @join_dependency.construct_tables_for_association!(jd.instance_variable_get(:@join_root), found_association)
347
324
 
348
325
  # Leverage the stashed association functionality in AR
349
326
  @object = @object.joins(jd)
@@ -353,22 +330,13 @@ module Ransack
353
330
  def extract_joins(association)
354
331
  parent = @join_dependency.instance_variable_get(:@join_root)
355
332
  reflection = association.reflection
356
- join_constraints = if ::Gem::Version.new(::ActiveRecord::VERSION::STRING) >= ::Gem::Version.new(Constants::RAILS_6_1)
357
- association.join_constraints_with_tables(
358
- parent.table,
359
- parent.base_klass,
360
- Arel::Nodes::OuterJoin,
361
- @join_dependency.instance_variable_get(:@alias_tracker),
362
- @tables_pot[association]
363
- )
364
- else
365
- association.join_constraints(
366
- parent.table,
367
- parent.base_klass,
368
- Arel::Nodes::OuterJoin,
369
- @join_dependency.instance_variable_get(:@alias_tracker)
370
- )
371
- end
333
+ join_constraints = association.join_constraints_with_tables(
334
+ parent.table,
335
+ parent.base_klass,
336
+ Arel::Nodes::OuterJoin,
337
+ @join_dependency.instance_variable_get(:@alias_tracker),
338
+ @tables_pot[association]
339
+ )
372
340
  join_constraints.to_a.flatten
373
341
  end
374
342
  end
@@ -27,15 +27,15 @@ module Ransack
27
27
  self.predicates = PredicateCollection.new
28
28
 
29
29
  self.options = {
30
- :search_key => :q,
31
- :ignore_unknown_conditions => true,
32
- :hide_sort_order_indicators => false,
33
- :up_arrow => '&#9660;'.freeze,
34
- :down_arrow => '&#9650;'.freeze,
35
- :default_arrow => nil,
36
- :sanitize_scope_args => true,
37
- :postgres_fields_sort_option => nil,
38
- :strip_whitespace => true
30
+ search_key: :q,
31
+ ignore_unknown_conditions: true,
32
+ hide_sort_order_indicators: false,
33
+ up_arrow: '&#9660;'.freeze,
34
+ down_arrow: '&#9650;'.freeze,
35
+ default_arrow: nil,
36
+ sanitize_scope_args: true,
37
+ postgres_fields_sort_option: nil,
38
+ strip_whitespace: true
39
39
  }
40
40
 
41
41
  def configure
@@ -55,11 +55,11 @@ module Ransack
55
55
  compound_name = name + suffix
56
56
  self.predicates[compound_name] = Predicate.new(
57
57
  opts.merge(
58
- :name => compound_name,
59
- :arel_predicate => arel_predicate_with_suffix(
58
+ name: compound_name,
59
+ arel_predicate: arel_predicate_with_suffix(
60
60
  opts[:arel_predicate], suffix
61
61
  ),
62
- :compound => true
62
+ compound: true
63
63
  )
64
64
  )
65
65
  end if compounds
@@ -101,6 +101,19 @@ module Ransack
101
101
  self.options[:ignore_unknown_conditions] = boolean
102
102
  end
103
103
 
104
+ # By default Ransack ignores empty predicates. Ransack can also fallback to
105
+ # a default predicate by setting it in an initializer file
106
+ # like `config/initializers/ransack.rb` as follows:
107
+ #
108
+ # Ransack.configure do |config|
109
+ # # Use the 'eq' predicate if an unknown predicate is passed
110
+ # config.default_predicate = 'eq'
111
+ # end
112
+ #
113
+ def default_predicate=(name)
114
+ self.options[:default_predicate] = name
115
+ end
116
+
104
117
  # By default, Ransack displays sort order indicator arrows with HTML codes:
105
118
  #
106
119
  # up_arrow: '&#9660;'
@@ -45,10 +45,132 @@ module Ransack
45
45
  NOT_EQ_ALL = 'not_eq_all'.freeze
46
46
  CONT = 'cont'.freeze
47
47
 
48
- RAILS_6_0 = '6.0.0'.freeze
49
- RAILS_6_1 = '6.1.0'.freeze
50
-
51
48
  RANSACK_SLASH_SEARCHES = 'ransack/searches'.freeze
52
49
  RANSACK_SLASH_SEARCHES_SLASH_SEARCH = 'ransack/searches/search'.freeze
50
+
51
+ DISTINCT = 'DISTINCT '.freeze
52
+
53
+ DERIVED_PREDICATES = [
54
+ [CONT, {
55
+ arel_predicate: 'matches'.freeze,
56
+ formatter: proc { |v| "%#{escape_wildcards(v)}%" }
57
+ }
58
+ ],
59
+ ['not_cont'.freeze, {
60
+ arel_predicate: 'does_not_match'.freeze,
61
+ formatter: proc { |v| "%#{escape_wildcards(v)}%" }
62
+ }
63
+ ],
64
+ ['i_cont'.freeze, {
65
+ arel_predicate: 'matches'.freeze,
66
+ formatter: proc { |v| "%#{escape_wildcards(v.downcase)}%" },
67
+ case_insensitive: true
68
+ }
69
+ ],
70
+ ['not_i_cont'.freeze, {
71
+ arel_predicate: 'does_not_match'.freeze,
72
+ formatter: proc { |v| "%#{escape_wildcards(v.downcase)}%" },
73
+ case_insensitive: true
74
+ }
75
+ ],
76
+ ['start'.freeze, {
77
+ arel_predicate: 'matches'.freeze,
78
+ formatter: proc { |v| "#{escape_wildcards(v)}%" }
79
+ }
80
+ ],
81
+ ['not_start'.freeze, {
82
+ arel_predicate: 'does_not_match'.freeze,
83
+ formatter: proc { |v| "#{escape_wildcards(v)}%" }
84
+ }
85
+ ],
86
+ ['end'.freeze, {
87
+ arel_predicate: 'matches'.freeze,
88
+ formatter: proc { |v| "%#{escape_wildcards(v)}" }
89
+ }
90
+ ],
91
+ ['not_end'.freeze, {
92
+ arel_predicate: 'does_not_match'.freeze,
93
+ formatter: proc { |v| "%#{escape_wildcards(v)}" }
94
+ }
95
+ ],
96
+ ['true'.freeze, {
97
+ arel_predicate: proc { |v| v ? EQ : NOT_EQ },
98
+ compounds: false,
99
+ type: :boolean,
100
+ validator: proc { |v| BOOLEAN_VALUES.include?(v) },
101
+ formatter: proc { |v| true }
102
+ }
103
+ ],
104
+ ['not_true'.freeze, {
105
+ arel_predicate: proc { |v| v ? NOT_EQ : EQ },
106
+ compounds: false,
107
+ type: :boolean,
108
+ validator: proc { |v| BOOLEAN_VALUES.include?(v) },
109
+ formatter: proc { |v| true }
110
+ }
111
+ ],
112
+ ['false'.freeze, {
113
+ arel_predicate: proc { |v| v ? EQ : NOT_EQ },
114
+ compounds: false,
115
+ type: :boolean,
116
+ validator: proc { |v| BOOLEAN_VALUES.include?(v) },
117
+ formatter: proc { |v| false }
118
+ }
119
+ ],
120
+ ['not_false'.freeze, {
121
+ arel_predicate: proc { |v| v ? NOT_EQ : EQ },
122
+ compounds: false,
123
+ type: :boolean,
124
+ validator: proc { |v| BOOLEAN_VALUES.include?(v) },
125
+ formatter: proc { |v| false }
126
+ }
127
+ ],
128
+ ['present'.freeze, {
129
+ arel_predicate: proc { |v| v ? NOT_EQ_ALL : EQ_ANY },
130
+ compounds: false,
131
+ type: :boolean,
132
+ validator: proc { |v| BOOLEAN_VALUES.include?(v) },
133
+ formatter: proc { |v| [nil, ''.freeze].freeze }
134
+ }
135
+ ],
136
+ ['blank'.freeze, {
137
+ arel_predicate: proc { |v| v ? EQ_ANY : NOT_EQ_ALL },
138
+ compounds: false,
139
+ type: :boolean,
140
+ validator: proc { |v| BOOLEAN_VALUES.include?(v) },
141
+ formatter: proc { |v| [nil, ''.freeze].freeze }
142
+ }
143
+ ],
144
+ ['null'.freeze, {
145
+ arel_predicate: proc { |v| v ? EQ : NOT_EQ },
146
+ compounds: false,
147
+ type: :boolean,
148
+ validator: proc { |v| BOOLEAN_VALUES.include?(v) },
149
+ formatter: proc { |v| nil }
150
+ }
151
+ ],
152
+ ['not_null'.freeze, {
153
+ arel_predicate: proc { |v| v ? NOT_EQ : EQ },
154
+ compounds: false,
155
+ type: :boolean,
156
+ validator: proc { |v| BOOLEAN_VALUES.include?(v) },
157
+ formatter: proc { |v| nil } }
158
+ ]
159
+ ].freeze
160
+
161
+ module_function
162
+ # replace % \ to \% \\
163
+ def escape_wildcards(unescaped)
164
+ case ActiveRecord::Base.connection.adapter_name
165
+ when "Mysql2".freeze
166
+ # Necessary for MySQL
167
+ unescaped.to_s.gsub(/([\\%_])/, '\\\\\\1')
168
+ when "PostgreSQL".freeze
169
+ # Necessary for PostgreSQL
170
+ unescaped.to_s.gsub(/([\\%_.])/, '\\\\\\1')
171
+ else
172
+ unescaped
173
+ end
174
+ end
53
175
  end
54
176
  end
@@ -1,19 +1,24 @@
1
1
  require 'ransack/visitor'
2
- Ransack::Adapters.object_mapper.require_context
3
2
 
4
3
  module Ransack
5
4
  class Context
6
5
  attr_reader :search, :object, :klass, :base, :engine, :arel_visitor
7
6
  attr_accessor :auth_object, :search_key
7
+ attr_reader :arel_visitor
8
8
 
9
9
  class << self
10
10
 
11
11
  def for_class(klass, options = {})
12
- raise "not implemented"
12
+ if klass < ActiveRecord::Base
13
+ Adapters::ActiveRecord::Context.new(klass, options)
14
+ end
13
15
  end
14
16
 
15
17
  def for_object(object, options = {})
16
- raise "not implemented"
18
+ case object
19
+ when ActiveRecord::Relation
20
+ Adapters::ActiveRecord::Context.new(object.klass, options)
21
+ end
17
22
  end
18
23
 
19
24
  def for(object, options = {})
@@ -30,11 +35,35 @@ module Ransack
30
35
  end # << self
31
36
 
32
37
  def initialize(object, options = {})
33
- raise "not implemented"
38
+ @object = relation_for(object)
39
+ @klass = @object.klass
40
+ @join_dependency = join_dependency(@object)
41
+ @join_type = options[:join_type] || Polyamorous::OuterJoin
42
+ @search_key = options[:search_key] || Ransack.options[:search_key]
43
+ @associations_pot = {}
44
+ @tables_pot = {}
45
+ @lock_associations = []
46
+
47
+ @base = @join_dependency.instance_variable_get(:@join_root)
48
+ end
49
+
50
+ def bind_pair_for(key)
51
+ @bind_pairs ||= {}
52
+
53
+ @bind_pairs[key] ||= begin
54
+ parent, attr_name = get_parent_and_attribute_name(key.to_s)
55
+ [parent, attr_name] if parent && attr_name
56
+ end
34
57
  end
35
58
 
36
59
  def klassify(obj)
37
- raise "not implemented"
60
+ if Class === obj && ::ActiveRecord::Base > obj
61
+ obj
62
+ elsif obj.respond_to? :klass
63
+ obj.klass
64
+ else
65
+ raise ArgumentError, "Don't know how to klassify #{obj.inspect}"
66
+ end
38
67
  end
39
68
 
40
69
  # Convert a string representing a chain of associations and an attribute
@@ -33,7 +33,7 @@ module Ransack
33
33
  text = args.first
34
34
  i18n = options[:i18n] || {}
35
35
  text ||= object.translate(
36
- method, i18n.reverse_merge(:include_associations => true)
36
+ method, i18n.reverse_merge(include_associations: true)
37
37
  ) if object.respond_to? :translate
38
38
  super(method, text, options, &block)
39
39
  end
@@ -240,7 +240,7 @@ module Ransack
240
240
  def get_attribute_element(action, base)
241
241
  begin
242
242
  [
243
- Translate.association(base, :context => object.context),
243
+ Translate.association(base, context: object.context),
244
244
  collection_for_base(action, base)
245
245
  ]
246
246
  rescue UntraversableAssociationError
@@ -253,7 +253,7 @@ module Ransack
253
253
  [
254
254
  attr_from_base_and_column(base, c),
255
255
  Translate.attribute(
256
- attr_from_base_and_column(base, c), :context => object.context
256
+ attr_from_base_and_column(base, c), context: object.context
257
257
  )
258
258
  ]
259
259
  end
@@ -129,8 +129,8 @@ module Ransack
129
129
  end
130
130
 
131
131
  def url_options
132
- @params.merge(
133
- @options.except(:class).merge(
132
+ @params.except(:host).merge(
133
+ @options.except(:class, :data, :host).merge(
134
134
  @search.context.search_key => search_and_sort_params))
135
135
  end
136
136
 
@@ -180,7 +180,8 @@ module Ransack
180
180
  end
181
181
 
182
182
  def search_params
183
- @params[@search.context.search_key].presence || {}
183
+ query_params = @params[@search.context.search_key]
184
+ query_params.is_a?(Hash) ? query_params : {}
184
185
  end
185
186
 
186
187
  def sort_params
@@ -5,8 +5,8 @@ module Ransack
5
5
 
6
6
  attr_reader :name, :ransacker_args
7
7
 
8
- delegate :blank?, :present?, :to => :name
9
- delegate :engine, :to => :context
8
+ delegate :blank?, :present?, to: :name
9
+ delegate :engine, to: :context
10
10
 
11
11
  def initialize(context, name = nil, ransacker_args = [])
12
12
  super(context)
@@ -2,8 +2,8 @@ module Ransack
2
2
  module Nodes
3
3
  class Condition < Node
4
4
  i18n_word :attribute, :predicate, :combinator, :value
5
- i18n_alias :a => :attribute, :p => :predicate,
6
- :m => :combinator, :v => :value
5
+ i18n_alias a: :attribute, p: :predicate,
6
+ m: :combinator, v: :value
7
7
 
8
8
  attr_accessor :predicate
9
9
 
@@ -15,10 +15,10 @@ module Ransack
15
15
  if attributes.size > 0 && predicate
16
16
  condition = self.new(context)
17
17
  condition.build(
18
- :a => attributes,
19
- :p => predicate.name,
20
- :m => combinator,
21
- :v => predicate.wants_array ? Array(values) : [values]
18
+ a: attributes,
19
+ p: predicate.name,
20
+ m: combinator,
21
+ v: predicate.wants_array ? Array(values) : [values]
22
22
  )
23
23
  # TODO: Figure out what to do with multiple types of attributes,
24
24
  # if anything. Tempted to go with "garbage in, garbage out" here.
@@ -283,12 +283,85 @@ module Ransack
283
283
  predicate.negative?
284
284
  end
285
285
 
286
+ def arel_predicate
287
+ predicate = attributes.map { |attribute|
288
+ association = attribute.parent
289
+ if negative? && attribute.associated_collection?
290
+ query = context.build_correlated_subquery(association)
291
+ context.remove_association(association)
292
+ if self.predicate_name == 'not_null' && self.value
293
+ query.where(format_predicate(attribute))
294
+ Arel::Nodes::In.new(context.primary_key, Arel.sql(query.to_sql))
295
+ else
296
+ query.where(format_predicate(attribute).not)
297
+ Arel::Nodes::NotIn.new(context.primary_key, Arel.sql(query.to_sql))
298
+ end
299
+ else
300
+ format_predicate(attribute)
301
+ end
302
+ }.reduce(combinator_method)
303
+
304
+ if replace_right_node?(predicate)
305
+ # Replace right node object to plain integer value in order to avoid
306
+ # ActiveModel::RangeError from Arel::Node::Casted.
307
+ # The error can be ignored here because RDBMSs accept large numbers
308
+ # in condition clauses.
309
+ plain_value = predicate.right.value
310
+ predicate.right = plain_value
311
+ end
312
+
313
+ predicate
314
+ end
315
+
286
316
  private
287
317
 
318
+ def combinator_method
319
+ combinator === Constants::OR ? :or : :and
320
+ end
321
+
322
+ def format_predicate(attribute)
323
+ arel_pred = arel_predicate_for_attribute(attribute)
324
+ arel_values = formatted_values_for_attribute(attribute)
325
+ predicate = attr_value_for_attribute(attribute).public_send(arel_pred, arel_values)
326
+
327
+ if in_predicate?(predicate)
328
+ predicate.right = predicate.right.map do |pr|
329
+ casted_array?(pr) ? format_values_for(pr) : pr
330
+ end
331
+ end
332
+
333
+ predicate
334
+ end
335
+
336
+ def in_predicate?(predicate)
337
+ return unless defined?(Arel::Nodes::Casted)
338
+ predicate.class == Arel::Nodes::In || predicate.class == Arel::Nodes::NotIn
339
+ end
340
+
341
+ def casted_array?(predicate)
342
+ predicate.value.is_a?(Array) && predicate.is_a?(Arel::Nodes::Casted)
343
+ end
344
+
345
+ def format_values_for(predicate)
346
+ predicate.value.map do |val|
347
+ val.is_a?(String) ? Arel::Nodes.build_quoted(val) : val
348
+ end
349
+ end
350
+
351
+ def replace_right_node?(predicate)
352
+ return false unless predicate.is_a?(Arel::Nodes::Binary)
353
+
354
+ arel_node = predicate.right
355
+ return false unless arel_node.is_a?(Arel::Nodes::Casted)
356
+
357
+ relation, name = arel_node.attribute.values
358
+ attribute_type = relation.type_for_attribute(name).type
359
+ attribute_type == :integer && arel_node.value.is_a?(Integer)
360
+ end
361
+
288
362
  def valid_combinator?
289
363
  attributes.size < 2 || Constants::AND_OR.include?(combinator)
290
364
  end
291
-
292
365
  end
293
366
  end
294
367
  end