ransack 3.0.0 → 4.0.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 (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