ransack 4.3.0 → 4.4.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 (104) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -2
  3. data/lib/polyamorous/polyamorous.rb +1 -1
  4. data/lib/ransack/adapters/active_record/context.rb +30 -3
  5. data/lib/ransack/context.rb +3 -0
  6. data/lib/ransack/helpers/form_builder.rb +6 -7
  7. data/lib/ransack/helpers/form_helper.rb +86 -20
  8. data/lib/ransack/locale/ja.yml +51 -51
  9. data/lib/ransack/locale/ko.yml +6 -6
  10. data/lib/ransack/locale/uk.yml +72 -0
  11. data/lib/ransack/nodes/condition.rb +36 -6
  12. data/lib/ransack/nodes/grouping.rb +1 -1
  13. data/lib/ransack/nodes/sort.rb +1 -1
  14. data/lib/ransack/nodes/value.rb +9 -1
  15. data/lib/ransack/search.rb +1 -1
  16. data/lib/ransack/version.rb +1 -1
  17. data/lib/ransack.rb +8 -0
  18. data/spec/polyamorous/join_association_spec.rb +0 -1
  19. data/spec/polyamorous/join_dependency_spec.rb +0 -1
  20. data/spec/ransack/adapters/active_record/base_spec.rb +101 -2
  21. data/spec/ransack/adapters/active_record/context_spec.rb +72 -0
  22. data/spec/ransack/helpers/form_builder_spec.rb +0 -2
  23. data/spec/ransack/helpers/form_helper_spec.rb +219 -5
  24. data/spec/ransack/nodes/condition_spec.rb +229 -0
  25. data/spec/ransack/nodes/grouping_spec.rb +2 -2
  26. data/spec/ransack/nodes/value_spec.rb +12 -1
  27. data/spec/ransack/predicate_spec.rb +0 -1
  28. data/spec/ransack/search_spec.rb +107 -1
  29. data/spec/ransack/translate_spec.rb +0 -1
  30. data/spec/spec_helper.rb +2 -3
  31. data/spec/support/schema.rb +30 -0
  32. metadata +41 -87
  33. data/.github/FUNDING.yml +0 -3
  34. data/.github/SECURITY.md +0 -12
  35. data/.github/workflows/codeql.yml +0 -72
  36. data/.github/workflows/cronjob.yml +0 -141
  37. data/.github/workflows/deploy.yml +0 -35
  38. data/.github/workflows/rubocop.yml +0 -20
  39. data/.github/workflows/test-deploy.yml +0 -29
  40. data/.github/workflows/test.yml +0 -183
  41. data/.gitignore +0 -7
  42. data/.nojekyll +0 -0
  43. data/.rubocop.yml +0 -50
  44. data/CHANGELOG.md +0 -1193
  45. data/CONTRIBUTING.md +0 -171
  46. data/Gemfile +0 -58
  47. data/Rakefile +0 -24
  48. data/bug_report_templates/test-ransack-scope-and-column-same-name.rb +0 -78
  49. data/bug_report_templates/test-ransacker-arel-present-predicate.rb +0 -75
  50. data/docs/.gitignore +0 -19
  51. data/docs/.nojekyll +0 -0
  52. data/docs/babel.config.js +0 -3
  53. data/docs/blog/2022-03-27-ransack-3.0.0.md +0 -20
  54. data/docs/docs/getting-started/_category_.json +0 -4
  55. data/docs/docs/getting-started/advanced-mode.md +0 -46
  56. data/docs/docs/getting-started/configuration.md +0 -47
  57. data/docs/docs/getting-started/search-matches.md +0 -67
  58. data/docs/docs/getting-started/simple-mode.md +0 -289
  59. data/docs/docs/getting-started/sorting.md +0 -71
  60. data/docs/docs/getting-started/using-predicates.md +0 -282
  61. data/docs/docs/going-further/_category_.json +0 -4
  62. data/docs/docs/going-further/acts-as-taggable-on.md +0 -114
  63. data/docs/docs/going-further/associations.md +0 -70
  64. data/docs/docs/going-further/custom-predicates.md +0 -52
  65. data/docs/docs/going-further/documentation.md +0 -43
  66. data/docs/docs/going-further/exporting-to-csv.md +0 -49
  67. data/docs/docs/going-further/external-guides.md +0 -57
  68. data/docs/docs/going-further/form-customisation.md +0 -63
  69. data/docs/docs/going-further/i18n.md +0 -53
  70. data/docs/docs/going-further/img/create_release.png +0 -0
  71. data/docs/docs/going-further/merging-searches.md +0 -41
  72. data/docs/docs/going-further/other-notes.md +0 -425
  73. data/docs/docs/going-further/polymorphic-search.md +0 -46
  74. data/docs/docs/going-further/ransackers.md +0 -331
  75. data/docs/docs/going-further/release_process.md +0 -36
  76. data/docs/docs/going-further/saving-queries.md +0 -82
  77. data/docs/docs/going-further/searching-postgres.md +0 -57
  78. data/docs/docs/going-further/wiki-contributors.md +0 -82
  79. data/docs/docs/intro.md +0 -99
  80. data/docs/docusaurus.config.js +0 -120
  81. data/docs/package.json +0 -42
  82. data/docs/sidebars.js +0 -31
  83. data/docs/src/components/HomepageFeatures/index.js +0 -64
  84. data/docs/src/components/HomepageFeatures/styles.module.css +0 -11
  85. data/docs/src/css/custom.css +0 -39
  86. data/docs/src/pages/index.module.css +0 -23
  87. data/docs/src/pages/markdown-page.md +0 -7
  88. data/docs/static/.nojekyll +0 -0
  89. data/docs/static/img/docusaurus.png +0 -0
  90. data/docs/static/img/favicon.ico +0 -0
  91. data/docs/static/img/logo.svg +0 -1
  92. data/docs/static/img/tutorial/docsVersionDropdown.png +0 -0
  93. data/docs/static/img/tutorial/localeDropdown.png +0 -0
  94. data/docs/static/img/undraw_docusaurus_mountain.svg +0 -171
  95. data/docs/static/img/undraw_docusaurus_react.svg +0 -170
  96. data/docs/static/img/undraw_docusaurus_tree.svg +0 -40
  97. data/docs/static/logo/ransack-h.png +0 -0
  98. data/docs/static/logo/ransack-h.svg +0 -34
  99. data/docs/static/logo/ransack-v.png +0 -0
  100. data/docs/static/logo/ransack-v.svg +0 -34
  101. data/docs/static/logo/ransack.png +0 -0
  102. data/docs/static/logo/ransack.svg +0 -21
  103. data/docs/yarn.lock +0 -8884
  104. data/ransack.gemspec +0 -26
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3ea2c1ef5d55f6a67d7d182146f2274d9b4343285920ae69bec3a5902fa4bf14
4
- data.tar.gz: db7e3f5f6ae62d9bebc8ac65bc9a50bc451585abd69e88aca20012c6777fbae6
3
+ metadata.gz: 4fe4a128cacdb920f2efe5fc5063cd21b8fa1a4a527fa07e84123f2ead7a6694
4
+ data.tar.gz: 82f66ffeaa4d8bed52614374fe71617bb1c2d85ac1745dfca7e4520e07782545
5
5
  SHA512:
6
- metadata.gz: 8c89ea0cb39b675a116b8c9698acbe106a24415e107435b4035c837e921dc359c77f7806415f9008e76dcaf658b888a02c8df137fd6295b71c1d445feb1846fc
7
- data.tar.gz: 197c2f53ab0e98d7a24203fafb81187a44235c2536734ee0976bb78d2e9a552f3cf309d5a06ef5704bd8f4ece49931ad7517defa603067d1a113b230f5554681
6
+ metadata.gz: a80ce4b0da981c6e3b2d18de8c742d3598a2a7479b44d04adaa1ce3c60a81fa4377f706c78048c1d72f719d488afd379c303b55811053d8613eb82cf0f0dfcad
7
+ data.tar.gz: cd3f836f63aa19596a3510961bbe64a1f9abcc858473bf9516623e5fe76908bcbb346d36cf1cd7cb83094f0a3c0f4bcdb942b0e137a9020cd8772e28d01d9f51
data/README.md CHANGED
@@ -33,9 +33,11 @@ gem 'ransack', :github => 'activerecord-hackery/ransack', :branch => 'main'
33
33
 
34
34
  ### Documentation
35
35
 
36
- There is [extensive documentation on Ransack](https://activerecord-hackery.github.io/ransack/), which is a [Docusaurus](https://docusaurus.io/) project and run as a GitHub Pages site.
36
+ There is [extensive documentation on Ransack](https://activerecord-hackery.github.io/ransack/), which is a [Docusaurus](https://docusaurus.io/) project and run as a GitHub Pages site. Alternatively there is [AI Generated documentation](https://deepwiki.com/activerecord-hackery/ransack/1-overview) produced by [devin.ai](https://devin.ai/).
37
37
 
38
- ## Issues tracker
38
+ This [gist](https://gist.github.com/raghubetina/d5fc3df67ddbadcac271) has a quick-start cheatsheet, created by [@raghubetina](https://gist.github.com/raghubetina)
39
+
40
+ ## Issue tracker
39
41
 
40
42
  * Before filing an issue, please read the [Contributing Guide](CONTRIBUTING.md).
41
43
  * File an issue if a bug is caused by Ransack, is new (has not already been reported), and _can be reproduced from the information you provide_.
@@ -1,4 +1,4 @@
1
- if defined?(::ActiveRecord)
1
+ ActiveSupport.on_load(:active_record) do
2
2
  module Polyamorous
3
3
  InnerJoin = Arel::Nodes::InnerJoin
4
4
  OuterJoin = Arel::Nodes::OuterJoin
@@ -19,7 +19,8 @@ module Ransack
19
19
  unless schema_cache.send(:data_source_exists?, table)
20
20
  raise "No table named #{table} exists."
21
21
  end
22
- attr.klass.columns.find { |column| column.name == name }.type
22
+ column = attr.klass.columns.find { |column| column.name == name }
23
+ column&.type
23
24
  end
24
25
 
25
26
  def evaluate(search, opts = {})
@@ -171,7 +172,25 @@ module Ransack
171
172
  join_constraints.each do |j|
172
173
  subquery.join_sources << Arel::Nodes::InnerJoin.new(j.left, j.right)
173
174
  end
174
- subquery.where(correlated_key.eq(primary_key))
175
+
176
+ # Handle polymorphic associations where correlated_key is an array
177
+ if correlated_key.is_a?(Array)
178
+ # For polymorphic associations, we need to add conditions for both the foreign key and type
179
+ correlated_key.each_with_index do |key, index|
180
+ if index == 0
181
+ # This is the foreign key
182
+ subquery = subquery.where(key.eq(primary_key))
183
+ else
184
+ # This is the type key, which should be equal to the model name
185
+ subquery = subquery.where(key.eq(@klass.name))
186
+ end
187
+ end
188
+ else
189
+ # Original behavior for non-polymorphic associations
190
+ subquery = subquery.where(correlated_key.eq(primary_key))
191
+ end
192
+
193
+ subquery
175
194
  end
176
195
 
177
196
  def primary_key
@@ -201,7 +220,15 @@ module Ransack
201
220
  nil
202
221
  end
203
222
  when Arel::Nodes::And
204
- extract_correlated_key(join_root.left) || extract_correlated_key(join_root.right)
223
+ # And may have multiple children, so we need to check all, not via left/right
224
+ if join_root.children.any?
225
+ join_root.children.each do |child|
226
+ key = extract_correlated_key(child)
227
+ return key if key
228
+ end
229
+ else
230
+ extract_correlated_key(join_root.left) || extract_correlated_key(join_root.right)
231
+ end
205
232
  else
206
233
  # eg parent was Arel::Nodes::And and the evaluated side was one of
207
234
  # Arel::Nodes::Grouping or MultiTenant::TenantEnforcementClause
@@ -77,6 +77,9 @@ module Ransack
77
77
  return unless @klass.method(scope) && args != false
78
78
  @object = if scope_arity(scope) < 1 && args == true
79
79
  @object.public_send(scope)
80
+ elsif scope_arity(scope) == 1 && args.is_a?(Array)
81
+ # For scopes with arity 1, pass the array as a single argument instead of splatting
82
+ @object.public_send(scope, args)
80
83
  else
81
84
  @object.public_send(scope, *args)
82
85
  end
@@ -6,13 +6,12 @@ module ActionView::Helpers::Tags
6
6
  # https://github.com/rails/rails/commit/c1a118a
7
7
  class Base
8
8
  private
9
- if defined? ::ActiveRecord
10
- def value
11
- if @allow_method_names_outside_object
12
- object.send @method_name if object && object.respond_to?(@method_name, true)
13
- else
14
- object.send @method_name if object
15
- end
9
+
10
+ def value
11
+ if @allow_method_names_outside_object
12
+ object.send @method_name if object && object.respond_to?(@method_name, true)
13
+ else
14
+ object.send @method_name if object
16
15
  end
17
16
  end
18
17
  end
@@ -7,30 +7,48 @@ module Ransack
7
7
  # <%= search_form_for(@q) do |f| %>
8
8
  #
9
9
  def search_form_for(record, options = {}, &proc)
10
- if record.is_a? Ransack::Search
11
- search = record
12
- options[:url] ||= polymorphic_path(
13
- search.klass, format: options.delete(:format)
14
- )
15
- elsif record.is_a?(Array) &&
16
- (search = record.detect { |o| o.is_a?(Ransack::Search) })
17
- options[:url] ||= polymorphic_path(
18
- options_for(record), format: options.delete(:format)
19
- )
10
+ search = extract_search_and_set_url(record, options, 'search_form_for')
11
+ options[:html] ||= {}
12
+ html_options = build_html_options(search, options, :get)
13
+ finalize_form_options(options, html_options)
14
+ form_for(record, options, &proc)
15
+ end
16
+
17
+ # +search_form_with+
18
+ #
19
+ # <%= search_form_with(model: @q) do |f| %>
20
+ #
21
+ def search_form_with(record_or_options = {}, options = {}, &proc)
22
+ if record_or_options.is_a?(Hash) && record_or_options.key?(:model)
23
+ # Called with keyword arguments: search_form_with(model: @q)
24
+ options = record_or_options
25
+ record = options.delete(:model)
20
26
  else
21
- raise ArgumentError,
22
- 'No Ransack::Search object was provided to search_form_for!'
27
+ # Called with positional arguments: search_form_with(@q)
28
+ record = record_or_options
23
29
  end
30
+ search = extract_search_and_set_url(record, options, 'search_form_with')
24
31
  options[:html] ||= {}
25
- html_options = {
26
- class: html_option_for(options[:class], search),
27
- id: html_option_for(options[:id], search),
28
- method: :get
29
- }
30
- options[:as] ||= Ransack.options[:search_key]
31
- options[:html].reverse_merge!(html_options)
32
- options[:builder] ||= FormBuilder
32
+ html_options = build_html_options(search, options, :get)
33
+ finalize_form_with_options(options, html_options)
34
+ form_with(model: search, **options, &proc)
35
+ end
33
36
 
37
+ # +turbo_search_form_for+
38
+ #
39
+ # <%= turbo_search_form_for(@q) do |f| %>
40
+ #
41
+ # This is a turbo-enabled version of search_form_for that submits via turbo streams
42
+ # instead of traditional HTML GET requests. Useful for seamless integration with
43
+ # paginated results and other turbo-enabled components.
44
+ #
45
+ def turbo_search_form_for(record, options = {}, &proc)
46
+ search = extract_search_and_set_url(record, options, 'turbo_search_form_for')
47
+ options[:html] ||= {}
48
+ turbo_options = build_turbo_options(options)
49
+ method = options.delete(:method) || :post
50
+ html_options = build_html_options(search, options, method).merge(turbo_options)
51
+ finalize_form_options(options, html_options)
34
52
  form_for(record, options, &proc)
35
53
  end
36
54
 
@@ -68,6 +86,54 @@ module Ransack
68
86
 
69
87
  private
70
88
 
89
+ def extract_search_and_set_url(record, options, method_name)
90
+ if record.is_a? Ransack::Search
91
+ search = record
92
+ options[:url] ||= polymorphic_path(
93
+ search.klass, format: options.delete(:format)
94
+ )
95
+ search
96
+ elsif record.is_a?(Array) &&
97
+ (search = record.detect { |o| o.is_a?(Ransack::Search) })
98
+ options[:url] ||= polymorphic_path(
99
+ options_for(record), format: options.delete(:format)
100
+ )
101
+ search
102
+ else
103
+ raise ArgumentError,
104
+ "No Ransack::Search object was provided to #{method_name}!"
105
+ end
106
+ end
107
+
108
+ def build_turbo_options(options)
109
+ data_options = {}
110
+ if options[:turbo_frame]
111
+ data_options[:turbo_frame] = options.delete(:turbo_frame)
112
+ end
113
+ data_options[:turbo_action] = options.delete(:turbo_action) || 'advance'
114
+ { data: data_options }
115
+ end
116
+
117
+ def build_html_options(search, options, method)
118
+ {
119
+ class: html_option_for(options[:class], search),
120
+ id: html_option_for(options[:id], search),
121
+ method: method
122
+ }
123
+ end
124
+
125
+ def finalize_form_options(options, html_options)
126
+ options[:as] ||= Ransack.options[:search_key]
127
+ options[:html].reverse_merge!(html_options)
128
+ options[:builder] ||= FormBuilder
129
+ end
130
+
131
+ def finalize_form_with_options(options, html_options)
132
+ options[:scope] ||= Ransack.options[:search_key]
133
+ options[:html].reverse_merge!(html_options)
134
+ options[:builder] ||= FormBuilder
135
+ end
136
+
71
137
  def options_for(record)
72
138
  record.map { |r| parse_record(r) }
73
139
  end
@@ -14,57 +14,57 @@ ja:
14
14
  asc: "昇順"
15
15
  desc: "降順"
16
16
  predicates:
17
- eq: "は以下と等しい"
18
- eq_any: "は以下のいずれかに等しい"
19
- eq_all: "は以下の全てに等しい"
20
- not_eq: "は以下と等しくない"
21
- not_eq_any: "は以下のいずれかに等しくない"
22
- not_eq_all: "は以下の全てと等しくない"
23
- matches: "は以下と合致している"
24
- matches_any: "は以下のいずれかと合致している"
25
- matches_all: "は以下の全てと合致している"
26
- does_not_match: "は以下と合致していない"
27
- does_not_match_any: "は以下のいずれかに合致していない"
28
- does_not_match_all: "は以下の全てに合致していない"
29
- lt: "は以下よりも小さい"
30
- lt_any: "は以下のいずれかより小さい"
31
- lt_all: "は以下の全てよりも小さい"
32
- lteq: "は以下より小さいか等しい"
33
- lteq_any: "は以下のいずれかより小さいか等しい"
34
- lteq_all: "は以下の全てより小さいか等しい"
35
- gt: "は以下より大きい"
36
- gt_any: "は以下のいずれかより大きい"
37
- gt_all: "は以下の全てより大きい"
38
- gteq: "は以下より大きいか等しい"
39
- gteq_any: "は以下のいずれかより大きいか等しい"
40
- gteq_all: "は以下の全てより大きいか等しい"
41
- in: "は以下の範囲内である"
42
- in_any: "は以下のいずれかの範囲内である"
43
- in_all: "は以下の全ての範囲内である"
44
- not_in: "は以下の範囲内でない"
45
- not_in_any: "は以下のいずれかの範囲内でない"
46
- not_in_all: "は以下の全ての範囲内"
47
- cont: "は以下を含む"
48
- cont_any: "はいずれかを含む"
49
- cont_all: "は以下の全てを含む"
50
- not_cont: "は含まない"
51
- not_cont_any: "は以下のいずれかを含まない"
52
- not_cont_all: "は以下の全てを含まない"
53
- start: "は以下で始まる"
54
- start_any: "は以下のどれかで始まる"
55
- start_all: "は以下の全てで始まる"
56
- not_start: "は以下で始まらない"
57
- not_start_any: "は以下のいずれかで始まらない"
58
- not_start_all: "は以下の全てで始まらない"
59
- end: "は以下で終わる"
60
- end_any: "は以下のいずれかで終わる"
61
- end_all: "は以下の全てで終わる"
62
- not_end: "は以下のどれでも終わらない"
63
- not_end_any: "は以下のいずれかで終わらない"
64
- not_end_all: "は以下の全てで終わらない"
17
+ eq: "等しい"
18
+ eq_any: "いずれかに等しい"
19
+ eq_all: "全てに等しい"
20
+ not_eq: "等しくない"
21
+ not_eq_any: "いずれかに等しくない"
22
+ not_eq_all: "全てと等しくない"
23
+ matches: "合致している"
24
+ matches_any: "いずれかと合致している"
25
+ matches_all: "全てと合致している"
26
+ does_not_match: "合致していない"
27
+ does_not_match_any: "いずれかに合致していない"
28
+ does_not_match_all: "全てに合致していない"
29
+ lt: "小さい"
30
+ lt_any: "いずれかより小さい"
31
+ lt_all: "全てよりも小さい"
32
+ lteq: "小さいか等しい"
33
+ lteq_any: "いずれかより小さいか等しい"
34
+ lteq_all: "全てより小さいか等しい"
35
+ gt: "大きい"
36
+ gt_any: "いずれかより大きい"
37
+ gt_all: "全てより大きい"
38
+ gteq: "大きいか等しい"
39
+ gteq_any: "いずれかより大きいか等しい"
40
+ gteq_all: "全てより大きいか等しい"
41
+ in: "範囲内である"
42
+ in_any: "いずれかの範囲内である"
43
+ in_all: "全ての範囲内である"
44
+ not_in: "範囲内でない"
45
+ not_in_any: "いずれかの範囲内でない"
46
+ not_in_all: "全ての範囲内"
47
+ cont: "含む"
48
+ cont_any: "いずれかを含む"
49
+ cont_all: "全てを含む"
50
+ not_cont: "含まない"
51
+ not_cont_any: "いずれかを含まない"
52
+ not_cont_all: "全てを含まない"
53
+ start: "始まる"
54
+ start_any: "どれかで始まる"
55
+ start_all: "全てで始まる"
56
+ not_start: "始まらない"
57
+ not_start_any: "いずれかで始まらない"
58
+ not_start_all: "全てで始まらない"
59
+ end: "終わる"
60
+ end_any: "いずれかで終わる"
61
+ end_all: "全てで終わる"
62
+ not_end: "どれでも終わらない"
63
+ not_end_any: "いずれかで終わらない"
64
+ not_end_all: "全てで終わらない"
65
65
  'true': "真"
66
66
  'false': "偽"
67
- present: "は存在する"
68
- blank: "は空である"
67
+ present: "存在する"
68
+ blank: "空である"
69
69
  'null': "無効"
70
- not_null: "は無効ではない"
70
+ not_null: "無効ではない"
@@ -62,9 +62,9 @@ ko:
62
62
  not_end: "끝나지 않음"
63
63
  not_end_any: "어떤 것이든 끝나지 않음"
64
64
  not_end_all: "모두 끝나지 않음"
65
- 'true': "is true"
66
- 'false': "is false"
67
- present: "is present"
68
- blank: "is blank"
69
- 'null': "is null"
70
- not_null: "is not null"
65
+ 'true': ""
66
+ 'false': "거짓"
67
+ present: "존재함"
68
+ blank: "비어있음"
69
+ 'null': ""
70
+ not_null: "널이 아님"
@@ -0,0 +1,72 @@
1
+ # Інші переклади на https://github.com/activerecord-hackery/ransack/blob/main/lib/ransack/locale
2
+ #
3
+ uk:
4
+ ransack:
5
+ search: пошук
6
+ predicate: предикат
7
+ and: і
8
+ or: або
9
+ any: будь-який
10
+ all: усі
11
+ combinator: комбінатор
12
+ attribute: атрибут
13
+ value: значення
14
+ condition: умова
15
+ sort: сортування
16
+ asc: за зростанням
17
+ desc: за спаданням
18
+ predicates:
19
+ eq: рівний
20
+ eq_any: рівний будь-якому
21
+ eq_all: рівний усім
22
+ not_eq: не рівний
23
+ not_eq_any: не рівний будь-якому
24
+ not_eq_all: не рівний усім
25
+ matches: збігається
26
+ matches_any: збігається з будь-яким
27
+ matches_all: збігається з усіма
28
+ does_not_match: не збігається
29
+ does_not_match_any: не збігається з будь-яким
30
+ does_not_match_all: не збігається з усіма
31
+ lt: менше ніж
32
+ lt_any: менше за будь-який
33
+ lt_all: менше за всі
34
+ lteq: менше або рівне
35
+ lteq_any: менше або рівне будь-якому
36
+ lteq_all: менше або рівне всім
37
+ gt: більше ніж
38
+ gt_any: більше ніж будь-який
39
+ gt_all: більше ніж усі
40
+ gteq: більше або рівне
41
+ gteq_any: більше або рівне будь-якому
42
+ gteq_all: більше або рівне всім
43
+ in: міститься у
44
+ in_any: міститься в будь-якому
45
+ in_all: міститься в усіх
46
+ not_in: не міститься у
47
+ not_in_any: не міститься в будь-якому
48
+ not_in_all: не міститься в усіх
49
+ cont: містить
50
+ cont_any: містить будь-який
51
+ cont_all: містить усі
52
+ not_cont: не містить
53
+ not_cont_any: не містить жодного
54
+ not_cont_all: не містить усіх
55
+ start: починається з
56
+ start_any: починається з будь-якого
57
+ start_all: починається з усіх
58
+ not_start: не починається з
59
+ not_start_any: не починається з будь-якого
60
+ not_start_all: не починається з усіх
61
+ end: закінчується на
62
+ end_any: закінчується на будь-який
63
+ end_all: закінчується на всі
64
+ not_end: не закінчується на
65
+ not_end_any: не закінчується на будь-який
66
+ not_end_all: не закінчується на всі
67
+ 'true': так
68
+ 'false': ні
69
+ present: присутній
70
+ blank: порожній
71
+ 'null': нульовий
72
+ not_null: не нульовий
@@ -226,7 +226,7 @@ module Ransack
226
226
  end
227
227
 
228
228
  def casted_values_for_attribute(attr)
229
- validated_values.map { |v| v.cast(predicate.type || attr.type) }
229
+ validated_values.map(&:cast_array)
230
230
  end
231
231
 
232
232
  def formatted_values_for_attribute(attr)
@@ -235,6 +235,9 @@ module Ransack
235
235
  val = attr.ransacker.formatter.call(val)
236
236
  end
237
237
  val = predicate.format(val)
238
+ if val.is_a?(String) && val.include?('%')
239
+ val = Arel::Nodes::Quoted.new(val)
240
+ end
238
241
  val
239
242
  end
240
243
  if predicate.wants_array
@@ -288,12 +291,24 @@ module Ransack
288
291
  def arel_predicate
289
292
  predicate = attributes.map { |attribute|
290
293
  association = attribute.parent
291
- if negative? && attribute.associated_collection?
294
+ parent_table = association.table
295
+
296
+ if negative? && attribute.associated_collection? && not_nested_condition(attribute, parent_table)
292
297
  query = context.build_correlated_subquery(association)
293
298
  context.remove_association(association)
294
- if self.predicate_name == 'not_null' && self.value
295
- query.where(format_predicate(attribute))
296
- Arel::Nodes::In.new(context.primary_key, Arel.sql(query.to_sql))
299
+
300
+ case self.predicate_name
301
+ when 'not_null'
302
+ if self.value
303
+ query.where(format_predicate(attribute))
304
+ Arel::Nodes::In.new(context.primary_key, Arel.sql(query.to_sql))
305
+ else
306
+ query.where(format_predicate(attribute).not)
307
+ Arel::Nodes::NotIn.new(context.primary_key, Arel.sql(query.to_sql))
308
+ end
309
+ when 'not_cont'
310
+ query.where(attribute.attr.matches(formatted_values_for_attribute(attribute)))
311
+ Arel::Nodes::NotIn.new(context.primary_key, Arel.sql(query.to_sql))
297
312
  else
298
313
  query.where(format_predicate(attribute).not)
299
314
  Arel::Nodes::NotIn.new(context.primary_key, Arel.sql(query.to_sql))
@@ -315,6 +330,10 @@ module Ransack
315
330
  predicate
316
331
  end
317
332
 
333
+ def not_nested_condition(attribute, parent_table)
334
+ parent_table.class != Arel::Nodes::TableAlias && attribute.name.starts_with?(parent_table.name)
335
+ end
336
+
318
337
  private
319
338
 
320
339
  def combinator_method
@@ -324,6 +343,13 @@ module Ransack
324
343
  def format_predicate(attribute)
325
344
  arel_pred = arel_predicate_for_attribute(attribute)
326
345
  arel_values = formatted_values_for_attribute(attribute)
346
+
347
+ # For LIKE predicates, wrap the value in Arel::Nodes.build_quoted to prevent
348
+ # ActiveRecord normalization from affecting wildcard patterns
349
+ if like_predicate?(arel_pred)
350
+ arel_values = Arel::Nodes.build_quoted(arel_values)
351
+ end
352
+
327
353
  predicate = attr_value_for_attribute(attribute).public_send(arel_pred, arel_values)
328
354
 
329
355
  if in_predicate?(predicate)
@@ -340,8 +366,12 @@ module Ransack
340
366
  predicate.class == Arel::Nodes::In || predicate.class == Arel::Nodes::NotIn
341
367
  end
342
368
 
369
+ def like_predicate?(arel_predicate)
370
+ arel_predicate == 'matches' || arel_predicate == 'does_not_match'
371
+ end
372
+
343
373
  def casted_array?(predicate)
344
- predicate.value.is_a?(Array) && predicate.is_a?(Arel::Nodes::Casted)
374
+ predicate.is_a?(Arel::Nodes::Casted) && predicate.value.is_a?(Array)
345
375
  end
346
376
 
347
377
  def format_values_for(predicate)
@@ -53,7 +53,7 @@ module Ransack
53
53
  end
54
54
 
55
55
  def []=(key, value)
56
- conditions.reject! { |c| c.key == key.to_s }
56
+ conditions.reject! { |c| c.key == key.to_s && c&.value == value&.value }
57
57
  self.conditions << value
58
58
  end
59
59
 
@@ -8,7 +8,7 @@ module Ransack
8
8
 
9
9
  class << self
10
10
  def extract(context, str)
11
- return unless str
11
+ return if str.blank?
12
12
  attr, direction = str.split(/\s+/, 2)
13
13
  self.new(context).build(name: attr, dir: direction)
14
14
  end
@@ -43,6 +43,14 @@ module Ransack
43
43
  end
44
44
  end
45
45
 
46
+ def cast_array
47
+ if value.is_a?(Array)
48
+ cast_to_date(value)
49
+ else
50
+ value
51
+ end
52
+ end
53
+
46
54
  def cast_to_date(val)
47
55
  if val.respond_to?(:to_date)
48
56
  val.to_date rescue nil
@@ -80,7 +88,7 @@ module Ransack
80
88
  end
81
89
 
82
90
  def cast_to_integer(val)
83
- val.blank? ? nil : val.to_i
91
+ val.respond_to?(:to_i) && !val.blank? ? val.to_i : nil
84
92
  end
85
93
 
86
94
  def cast_to_float(val)
@@ -69,7 +69,7 @@ module Ransack
69
69
  else
70
70
  sort = Nodes::Sort.extract(@context, sort)
71
71
  end
72
- self.sorts << sort
72
+ self.sorts << sort if sort
73
73
  end
74
74
  when Hash
75
75
  args.each do |index, attrs|
@@ -1,3 +1,3 @@
1
1
  module Ransack
2
- VERSION = '4.3.0'
2
+ VERSION = '4.4.0'
3
3
  end
data/lib/ransack.rb CHANGED
@@ -1,3 +1,11 @@
1
+ require 'active_support/dependencies/autoload'
2
+ require 'active_support/deprecation'
3
+ require 'active_support/version'
4
+
5
+ if ::ActiveSupport.version >= ::Gem::Version.new("7.1")
6
+ require 'active_support/deprecator'
7
+ end
8
+
1
9
  require 'active_support/core_ext'
2
10
  require 'ransack/configuration'
3
11
  require 'polyamorous/polyamorous'
@@ -2,7 +2,6 @@ require 'spec_helper'
2
2
 
3
3
  module Polyamorous
4
4
  describe JoinAssociation do
5
-
6
5
  let(:join_dependency) { new_join_dependency Note, {} }
7
6
  let(:reflection) { Note.reflect_on_association(:notable) }
8
7
  let(:parent) { join_dependency.send(:join_root) }
@@ -2,7 +2,6 @@ require 'spec_helper'
2
2
 
3
3
  module Polyamorous
4
4
  describe JoinDependency do
5
-
6
5
  context 'with symbol joins' do
7
6
  subject { new_join_dependency Person, articles: :comments }
8
7