ransack 5.0.1 → 6.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 (35) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +13 -27
  3. data/lib/ransack/active_record/base.rb +154 -0
  4. data/lib/ransack/active_record/context.rb +504 -0
  5. data/lib/ransack/active_record/dialect.rb +83 -0
  6. data/lib/ransack/active_record/join.rb +75 -0
  7. data/lib/ransack/active_record/join_association.rb +104 -0
  8. data/lib/ransack/active_record/join_dependency.rb +123 -0
  9. data/lib/ransack/active_record/reflection.rb +18 -0
  10. data/lib/ransack/active_record/tree_node.rb +13 -0
  11. data/lib/ransack/active_record.rb +44 -9
  12. data/lib/ransack/configuration.rb +49 -1
  13. data/lib/ransack/constants.rb +61 -0
  14. data/lib/ransack/context.rb +170 -35
  15. data/lib/ransack/helpers/form_builder.rb +30 -25
  16. data/lib/ransack/nodes/bindable.rb +0 -1
  17. data/lib/ransack/nodes/condition.rb +215 -73
  18. data/lib/ransack/nodes/grouping.rb +35 -11
  19. data/lib/ransack/nodes/sort.rb +1 -1
  20. data/lib/ransack/nodes/value.rb +8 -1
  21. data/lib/ransack/predicate.rb +8 -1
  22. data/lib/ransack/search.rb +52 -9
  23. data/lib/ransack/version.rb +1 -1
  24. data/lib/ransack.rb +4 -1
  25. metadata +11 -13
  26. data/lib/polyamorous/activerecord/join_association.rb +0 -70
  27. data/lib/polyamorous/activerecord/join_association_7_2.rb +0 -55
  28. data/lib/polyamorous/activerecord/join_dependency.rb +0 -102
  29. data/lib/polyamorous/activerecord/reflection.rb +0 -11
  30. data/lib/polyamorous/join.rb +0 -70
  31. data/lib/polyamorous/polyamorous.rb +0 -27
  32. data/lib/polyamorous/swapping_reflection_class.rb +0 -11
  33. data/lib/polyamorous/tree_node.rb +0 -7
  34. data/lib/ransack/adapters/active_record/base.rb +0 -152
  35. data/lib/ransack/adapters/active_record/context.rb +0 -375
@@ -22,6 +22,11 @@ module Ransack
22
22
 
23
23
  def initialize(object, params = {}, options = {})
24
24
  strip_whitespace = options.fetch(:strip_whitespace, Ransack.options[:strip_whitespace])
25
+ # Strong parameters carry whether the controller permitted them. Read
26
+ # that before the unwrap below discards it: a model with no allowlist
27
+ # of its own lets a permitted controller be the boundary, and a plain
28
+ # Hash never counts (#1403).
29
+ permitted = params.respond_to?(:permitted?) && params.permitted?
25
30
  params = params.to_unsafe_h if params.respond_to?(:to_unsafe_h)
26
31
  if params.is_a? Hash
27
32
  # deep_transform_values rebuilds every nested hash and array, which
@@ -36,7 +41,9 @@ module Ransack
36
41
  end
37
42
  @context = options[:context] || Context.for(object, options)
38
43
  @context.auth_object = options[:auth_object]
44
+ @context.permitted = permitted
39
45
  @context.ignore_unknown_conditions = options[:ignore_unknown_conditions]
46
+ @context.null_sentinel = options[:null_sentinel]
40
47
  @base = Nodes::Grouping.new(
41
48
  @context, options[:grouping] || Constants::AND
42
49
  )
@@ -65,7 +72,10 @@ module Ransack
65
72
  self
66
73
  end
67
74
 
75
+ # Replaces the sorts, so `search.sorts = []` clears them. Before 6.0 each
76
+ # assignment appended (#994); use `build_sort` to add one.
68
77
  def sorts=(args)
78
+ @sorts = [] unless args.is_a?(String)
69
79
  case args
70
80
  when Array
71
81
  args.each do |sort|
@@ -74,12 +84,11 @@ module Ransack
74
84
  else
75
85
  sort = Nodes::Sort.extract(@context, sort)
76
86
  end
77
- self.sorts << sort if sort
87
+ add_sort(sort) if sort
78
88
  end
79
89
  when Hash
80
90
  args.each do |index, attrs|
81
- sort = Nodes::Sort.new(@context).build(attrs)
82
- self.sorts << sort
91
+ add_sort(Nodes::Sort.new(@context).build(attrs))
83
92
  end
84
93
  when String
85
94
  self.sorts = [args]
@@ -105,22 +114,35 @@ module Ransack
105
114
  Nodes::Sort.new(@context).build(opts)
106
115
  end
107
116
 
117
+ # A scope wins over an attribute of the same name, as it does when the
118
+ # params hash is built (#1472).
108
119
  def method_missing(method_id, *args)
109
120
  method_name = method_id.to_s
110
121
  getter_name = method_name.sub(/=$/, ''.freeze)
111
- if base.attribute_method?(getter_name)
112
- base.send(method_id, *args)
113
- elsif @context.ransackable_scope?(getter_name, @context.object)
122
+ if @context.ransackable_scope?(getter_name, @context.object)
114
123
  if method_name =~ /=$/
115
- add_scope getter_name, args
124
+ add_scope getter_name, args.size == 1 ? args.first : args
116
125
  else
117
126
  @scope_args[method_name]
118
127
  end
128
+ elsif base.attribute_method?(getter_name)
129
+ base.send(method_id, *args)
119
130
  else
120
131
  super
121
132
  end
122
133
  end
123
134
 
135
+ # Rails' form helpers only read a field's value back when the object
136
+ # says it responds to the reader; `form_with` in particular checks
137
+ # before calling. Mirror method_missing so a search_form_with field is
138
+ # pre-filled the same way a search_form_for one is.
139
+ def respond_to_missing?(method_id, include_private = false)
140
+ getter_name = method_id.to_s.sub(/=$/, ''.freeze)
141
+ base.attribute_method?(getter_name) ||
142
+ @context.ransackable_scope?(getter_name, @context.object) ||
143
+ super
144
+ end
145
+
124
146
  def inspect
125
147
  details = [
126
148
  [:class, klass.name],
@@ -136,6 +158,22 @@ module Ransack
136
158
 
137
159
  private
138
160
 
161
+ # A sort that names neither a sortable attribute nor a `sort_by_<name>_<dir>`
162
+ # scope produces no ORDER BY. Under a strict search that is an error, the
163
+ # same as an unknown attribute in a condition (#1427); otherwise it is
164
+ # dropped as before.
165
+ def add_sort(sort)
166
+ if @context.strict_conditions? && !sort.valid? && !sort_scope?(sort)
167
+ raise InvalidSearchError, "Invalid sort term #{sort.name}"
168
+ end
169
+
170
+ self.sorts << sort
171
+ end
172
+
173
+ def sort_scope?(sort)
174
+ @context.object.respond_to?(:"sort_by_#{sort.name}_#{sort.dir}")
175
+ end
176
+
139
177
  def add_scope(key, args)
140
178
  sanitized_args = @context.sanitize_scope_args(key, args)
141
179
 
@@ -242,9 +280,14 @@ module Ransack
242
280
  end
243
281
 
244
282
  # Values in the `c:` API may be given bare or wrapped in a `{ value: ... }`
245
- # envelope, the same two forms `Condition#values=` accepts.
283
+ # envelope, the same two forms `Condition#values=` accepts. The envelope is
284
+ # opened by key rather than by truthiness: `false` is a value to search
285
+ # for, and `value[:value] || value['value']` read it as nil, which the
286
+ # pruning above then dropped (#1736).
246
287
  def unwrap_value(value)
247
- value.is_a?(Hash) ? (value[:value] || value['value']) : value
288
+ return value unless value.is_a?(Hash)
289
+
290
+ value.key?(:value) ? value[:value] : value['value']
248
291
  end
249
292
  end
250
293
  end
@@ -1,3 +1,3 @@
1
1
  module Ransack
2
- VERSION = '5.0.1'
2
+ VERSION = '6.0.0'
3
3
  end
data/lib/ransack.rb CHANGED
@@ -4,11 +4,14 @@ require 'active_support/deprecator'
4
4
 
5
5
  require 'active_support/core_ext'
6
6
  require 'ransack/configuration'
7
- require 'polyamorous/polyamorous'
8
7
 
9
8
  module Ransack
10
9
  extend Configuration
11
10
  class UntraversableAssociationError < StandardError; end
11
+
12
+ def self.deprecator
13
+ @deprecator ||= ActiveSupport::Deprecation.new('7.0', 'Ransack')
14
+ end
12
15
  end
13
16
 
14
17
  Ransack.configure do |config|
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ransack
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.0.1
4
+ version: 6.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ernie Miller
@@ -62,25 +62,23 @@ email:
62
62
  - ernie@erniemiller.org
63
63
  - radarlistener@gmail.com
64
64
  - jonnyatack@gmail.com
65
- - magma.craters2h@icloud.com
65
+ - sean@immersive-app.com
66
66
  executables: []
67
67
  extensions: []
68
68
  extra_rdoc_files: []
69
69
  files:
70
70
  - LICENSE
71
71
  - README.md
72
- - lib/polyamorous/activerecord/join_association.rb
73
- - lib/polyamorous/activerecord/join_association_7_2.rb
74
- - lib/polyamorous/activerecord/join_dependency.rb
75
- - lib/polyamorous/activerecord/reflection.rb
76
- - lib/polyamorous/join.rb
77
- - lib/polyamorous/polyamorous.rb
78
- - lib/polyamorous/swapping_reflection_class.rb
79
- - lib/polyamorous/tree_node.rb
80
72
  - lib/ransack.rb
81
73
  - lib/ransack/active_record.rb
82
- - lib/ransack/adapters/active_record/base.rb
83
- - lib/ransack/adapters/active_record/context.rb
74
+ - lib/ransack/active_record/base.rb
75
+ - lib/ransack/active_record/context.rb
76
+ - lib/ransack/active_record/dialect.rb
77
+ - lib/ransack/active_record/join.rb
78
+ - lib/ransack/active_record/join_association.rb
79
+ - lib/ransack/active_record/join_dependency.rb
80
+ - lib/ransack/active_record/reflection.rb
81
+ - lib/ransack/active_record/tree_node.rb
84
82
  - lib/ransack/configuration.rb
85
83
  - lib/ransack/constants.rb
86
84
  - lib/ransack/context.rb
@@ -142,7 +140,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
142
140
  requirements:
143
141
  - - ">="
144
142
  - !ruby/object:Gem::Version
145
- version: '3.1'
143
+ version: '3.2'
146
144
  required_rubygems_version: !ruby/object:Gem::Requirement
147
145
  requirements:
148
146
  - - ">="
@@ -1,70 +0,0 @@
1
- module Polyamorous
2
- module JoinAssociationExtensions
3
- include SwappingReflectionClass
4
- def self.prepended(base)
5
- base.class_eval { attr_reader :join_type }
6
- end
7
-
8
- def initialize(reflection, children, polymorphic_class = nil, join_type = Arel::Nodes::InnerJoin)
9
- @join_type = join_type
10
- if polymorphic_class && ::ActiveRecord::Base > polymorphic_class
11
- swapping_reflection_klass(reflection, polymorphic_class) do |reflection|
12
- super(reflection, children)
13
- self.reflection.options[:polymorphic] = true
14
- end
15
- else
16
- super(reflection, children)
17
- end
18
- end
19
-
20
- # Same as #join_constraints, but instead of constructing tables from the
21
- # given block, uses the ones passed
22
- def join_constraints_with_tables(foreign_table, foreign_klass, join_type, alias_tracker, tables)
23
- joins = []
24
- chain = []
25
-
26
- reflection.chain.each.with_index do |reflection, i|
27
- table = tables[i]
28
-
29
- @table ||= table
30
- chain << [reflection, table]
31
- end
32
-
33
- # The chain starts with the target table, but we want to end with it here (makes
34
- # more sense in this context), so we reverse
35
- chain.reverse_each do |reflection, table|
36
- klass = reflection.klass
37
-
38
- join_scope = reflection.join_scope(table, foreign_table, foreign_klass)
39
-
40
- unless join_scope.references_values.empty?
41
- join_dependency = join_scope.construct_join_dependency(
42
- join_scope.eager_load_values | join_scope.includes_values, Arel::Nodes::OuterJoin
43
- )
44
- join_scope.joins!(join_dependency)
45
- end
46
-
47
- arel = join_scope.arel(alias_tracker.aliases)
48
- nodes = arel.constraints.first
49
-
50
- if nodes.is_a?(Arel::Nodes::And)
51
- others = nodes.children.extract! do |node|
52
- !Arel.fetch_attribute(node) { |attr| attr.relation.name == table.name }
53
- end
54
- end
55
-
56
- joins << table.create_join(table, table.create_on(nodes), join_type)
57
-
58
- if others && !others.empty?
59
- joins.concat arel.join_sources
60
- append_constraints(joins.last, others)
61
- end
62
-
63
- # The current table in this iteration becomes the foreign table in the next
64
- foreign_table, foreign_klass = table, klass
65
- end
66
-
67
- joins
68
- end
69
- end
70
- end
@@ -1,55 +0,0 @@
1
- module Polyamorous
2
- module JoinAssociationExtensions
3
- # Same as #join_constraints, but instead of constructing tables from the
4
- # given block, uses the ones passed
5
- def join_constraints_with_tables(foreign_table, foreign_klass, join_type, alias_tracker, tables)
6
- joins = []
7
- chain = []
8
-
9
- reflection.chain.each.with_index do |reflection, i|
10
- table = tables[i]
11
-
12
- @table ||= table
13
- chain << [reflection, table]
14
- end
15
-
16
- base_klass.with_connection do |connection|
17
- # The chain starts with the target table, but we want to end with it here (makes
18
- # more sense in this context), so we reverse
19
- chain.reverse_each do |reflection, table|
20
- klass = reflection.klass
21
-
22
- join_scope = reflection.join_scope(table, foreign_table, foreign_klass)
23
-
24
- unless join_scope.references_values.empty?
25
- join_dependency = join_scope.construct_join_dependency(
26
- join_scope.eager_load_values | join_scope.includes_values, Arel::Nodes::OuterJoin
27
- )
28
- join_scope.joins!(join_dependency)
29
- end
30
-
31
- arel = join_scope.arel(alias_tracker.aliases)
32
- nodes = arel.constraints.first
33
-
34
- if nodes.is_a?(Arel::Nodes::And)
35
- others = nodes.children.extract! do |node|
36
- !Arel.fetch_attribute(node) { |attr| attr.relation.name == table.name }
37
- end
38
- end
39
-
40
- joins << table.create_join(table, table.create_on(nodes), join_type)
41
-
42
- if others && !others.empty?
43
- joins.concat arel.join_sources
44
- append_constraints(connection, joins.last, others)
45
- end
46
-
47
- # The current table in this iteration becomes the foreign table in the next
48
- foreign_table, foreign_klass = table, klass
49
- end
50
-
51
- joins
52
- end
53
- end
54
- end
55
- end
@@ -1,102 +0,0 @@
1
- module Polyamorous
2
- module JoinDependencyExtensions
3
- # Replaces ActiveRecord::Associations::JoinDependency#build
4
- def build(associations, base_klass)
5
- associations.map do |name, right|
6
- if name.is_a? Join
7
- reflection = find_reflection base_klass, name.name
8
- reflection.check_validity!
9
- reflection.check_eager_loadable!
10
-
11
- klass = if reflection.polymorphic?
12
- name.klass || base_klass
13
- else
14
- reflection.klass
15
- end
16
- JoinAssociation.new(reflection, build(right, klass), name.klass, name.type)
17
- else
18
- reflection = find_reflection base_klass, name
19
- reflection.check_validity!
20
- reflection.check_eager_loadable!
21
-
22
- if reflection.polymorphic?
23
- raise ActiveRecord::EagerLoadPolymorphicError.new(reflection)
24
- end
25
- JoinAssociation.new(reflection, build(right, reflection.klass))
26
- end
27
- end
28
- end
29
-
30
- def join_constraints(joins_to_add, alias_tracker, references)
31
- @alias_tracker = alias_tracker
32
- @joined_tables = {}
33
- @references = {}
34
-
35
- references.each do |table_name|
36
- @references[table_name.to_sym] = table_name if table_name.is_a?(String)
37
- end
38
-
39
- joins = make_join_constraints(join_root, join_type)
40
-
41
- joins.concat joins_to_add.flat_map { |oj|
42
- if join_root.match?(oj.join_root) && join_root.table.name == oj.join_root.table.name
43
- walk join_root, oj.join_root, oj.join_type
44
- else
45
- make_join_constraints(oj.join_root, oj.join_type)
46
- end
47
- }
48
- end
49
-
50
- def construct_tables_for_association!(join_root, association)
51
- tables = table_aliases_for(join_root, association)
52
- association.table = tables.first
53
- tables
54
- end
55
-
56
- private
57
-
58
- def table_aliases_for(parent, node)
59
- @joined_tables ||= {}
60
- node.reflection.chain.map { |reflection|
61
- table, terminated = @joined_tables[reflection]
62
- root = reflection == node.reflection
63
-
64
- if table && (!root || !terminated)
65
- @joined_tables[reflection] = [table, true] if root
66
- table
67
- else
68
- table = alias_tracker.aliased_table_for(reflection.klass.arel_table) do
69
- name = reflection.alias_candidate(parent.table_name)
70
- root ? name : "#{name}_join"
71
- end
72
- @joined_tables[reflection] ||= [table, root] if join_type == Arel::Nodes::OuterJoin
73
- table
74
- end
75
- }
76
- end
77
-
78
- module ClassMethods
79
- # Prepended before ActiveRecord::Associations::JoinDependency#walk_tree
80
- #
81
- def walk_tree(associations, hash)
82
- case associations
83
- when TreeNode
84
- associations.add_to_tree(hash)
85
- when Hash
86
- associations.each do |k, v|
87
- cache =
88
- if TreeNode === k
89
- k.add_to_tree(hash)
90
- else
91
- hash[k] ||= {}
92
- end
93
- walk_tree(v, cache)
94
- end
95
- else
96
- super(associations, hash)
97
- end
98
- end
99
- end
100
-
101
- end
102
- end
@@ -1,11 +0,0 @@
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
@@ -1,70 +0,0 @@
1
- module Polyamorous
2
- class Join
3
- include TreeNode
4
-
5
- attr_accessor :name
6
- attr_reader :type, :klass
7
-
8
- def initialize(name, type = InnerJoin, klass = nil)
9
- @name = name
10
- @type = convert_to_arel_join_type(type)
11
- @klass = convert_to_class(klass) if klass
12
- end
13
-
14
- def klass=(klass)
15
- @klass = convert_to_class(klass) if klass
16
- end
17
-
18
- def type=(type)
19
- @type = convert_to_arel_join_type(type) if type
20
- end
21
-
22
- def hash
23
- [@name, @type, @klass].hash
24
- end
25
-
26
- def eql?(other)
27
- self.class == other.class &&
28
- self.name == other.name &&
29
- self.type == other.type &&
30
- self.klass == other.klass
31
- end
32
-
33
- alias :== :eql?
34
-
35
- def add_to_tree(hash)
36
- hash[self] ||= {}
37
- end
38
-
39
- private
40
-
41
- def convert_to_arel_join_type(type)
42
- case type
43
- when 'inner', :inner
44
- InnerJoin
45
- when 'outer', :outer
46
- OuterJoin
47
- when Class
48
- if [InnerJoin, OuterJoin].include? type
49
- type
50
- else
51
- raise ArgumentError, "#{type} cannot be converted to an ARel join type"
52
- end
53
- else
54
- raise ArgumentError, "#{type} cannot be converted to an ARel join type"
55
- end
56
- end
57
-
58
- def convert_to_class(value)
59
- case value
60
- when String, Symbol
61
- Kernel.const_get(value)
62
- when Class
63
- value
64
- else
65
- raise ArgumentError, "#{value} cannot be converted to a Class"
66
- end
67
- end
68
-
69
- end
70
- end
@@ -1,27 +0,0 @@
1
- ActiveSupport.on_load(:active_record) do
2
- module Polyamorous
3
- InnerJoin = Arel::Nodes::InnerJoin
4
- OuterJoin = Arel::Nodes::OuterJoin
5
-
6
- JoinDependency = ::ActiveRecord::Associations::JoinDependency
7
- JoinAssociation = ::ActiveRecord::Associations::JoinDependency::JoinAssociation
8
- end
9
-
10
- require 'polyamorous/tree_node'
11
- require 'polyamorous/join'
12
- require 'polyamorous/swapping_reflection_class'
13
-
14
- require 'polyamorous/activerecord/join_association'
15
- require 'polyamorous/activerecord/join_dependency'
16
- require 'polyamorous/activerecord/reflection'
17
-
18
- if ::ActiveRecord.version >= ::Gem::Version.new("7.2") && ::ActiveRecord.version < ::Gem::Version.new("7.2.2.1")
19
- require "polyamorous/activerecord/join_association_7_2"
20
- end
21
-
22
- ActiveRecord::Reflection::AbstractReflection.send(:prepend, Polyamorous::ReflectionExtensions)
23
-
24
- Polyamorous::JoinDependency.send(:prepend, Polyamorous::JoinDependencyExtensions)
25
- Polyamorous::JoinDependency.singleton_class.send(:prepend, Polyamorous::JoinDependencyExtensions::ClassMethods)
26
- Polyamorous::JoinAssociation.send(:prepend, Polyamorous::JoinAssociationExtensions)
27
- end
@@ -1,11 +0,0 @@
1
- module Polyamorous
2
- module SwappingReflectionClass
3
- def swapping_reflection_klass(reflection, klass)
4
- new_reflection = reflection.clone
5
- new_reflection.instance_variable_set(:@options, reflection.options.clone)
6
- new_reflection.options.delete(:polymorphic)
7
- new_reflection.instance_variable_set(:@klass, klass)
8
- yield new_reflection
9
- end
10
- end
11
- end
@@ -1,7 +0,0 @@
1
- module Polyamorous
2
- module TreeNode
3
- def add_to_tree(hash)
4
- raise NotImplementedError
5
- end
6
- end
7
- end