ransack 1.7.0 → 1.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +40 -22
  3. data/CHANGELOG.md +176 -27
  4. data/CONTRIBUTING.md +30 -19
  5. data/Gemfile +8 -3
  6. data/README.md +131 -58
  7. data/Rakefile +5 -2
  8. data/lib/ransack.rb +10 -5
  9. data/lib/ransack/adapters.rb +43 -23
  10. data/lib/ransack/adapters/active_record.rb +2 -2
  11. data/lib/ransack/adapters/active_record/3.0/compat.rb +5 -5
  12. data/lib/ransack/adapters/active_record/3.0/context.rb +5 -3
  13. data/lib/ransack/adapters/active_record/3.1/context.rb +1 -4
  14. data/lib/ransack/adapters/active_record/base.rb +12 -1
  15. data/lib/ransack/adapters/active_record/context.rb +148 -55
  16. data/lib/ransack/adapters/active_record/ransack/constants.rb +53 -53
  17. data/lib/ransack/adapters/active_record/ransack/context.rb +3 -1
  18. data/lib/ransack/adapters/active_record/ransack/nodes/condition.rb +20 -28
  19. data/lib/ransack/adapters/mongoid/base.rb +21 -6
  20. data/lib/ransack/adapters/mongoid/context.rb +9 -5
  21. data/lib/ransack/configuration.rb +24 -3
  22. data/lib/ransack/constants.rb +11 -22
  23. data/lib/ransack/context.rb +20 -13
  24. data/lib/ransack/helpers/form_builder.rb +5 -6
  25. data/lib/ransack/helpers/form_helper.rb +50 -69
  26. data/lib/ransack/locale/da.yml +70 -0
  27. data/lib/ransack/locale/id.yml +70 -0
  28. data/lib/ransack/locale/ja.yml +70 -0
  29. data/lib/ransack/locale/pt-BR.yml +70 -0
  30. data/lib/ransack/locale/{zh.yml → zh-CN.yml} +1 -1
  31. data/lib/ransack/locale/zh-TW.yml +70 -0
  32. data/lib/ransack/nodes.rb +1 -1
  33. data/lib/ransack/nodes/attribute.rb +4 -1
  34. data/lib/ransack/nodes/bindable.rb +18 -6
  35. data/lib/ransack/nodes/condition.rb +58 -28
  36. data/lib/ransack/nodes/grouping.rb +15 -4
  37. data/lib/ransack/nodes/sort.rb +9 -5
  38. data/lib/ransack/predicate.rb +6 -2
  39. data/lib/ransack/search.rb +6 -5
  40. data/lib/ransack/translate.rb +2 -2
  41. data/lib/ransack/version.rb +1 -1
  42. data/ransack.gemspec +4 -4
  43. data/spec/mongoid/adapters/mongoid/base_spec.rb +20 -1
  44. data/spec/mongoid/nodes/condition_spec.rb +15 -0
  45. data/spec/mongoid/support/mongoid.yml +5 -0
  46. data/spec/mongoid/support/schema.rb +4 -0
  47. data/spec/mongoid_spec_helper.rb +13 -9
  48. data/spec/ransack/adapters/active_record/base_spec.rb +249 -71
  49. data/spec/ransack/adapters/active_record/context_spec.rb +16 -18
  50. data/spec/ransack/helpers/form_builder_spec.rb +5 -2
  51. data/spec/ransack/helpers/form_helper_spec.rb +84 -14
  52. data/spec/ransack/nodes/condition_spec.rb +24 -0
  53. data/spec/ransack/nodes/grouping_spec.rb +56 -0
  54. data/spec/ransack/predicate_spec.rb +5 -5
  55. data/spec/ransack/search_spec.rb +79 -70
  56. data/spec/support/schema.rb +43 -29
  57. metadata +17 -12
@@ -44,7 +44,7 @@ module Ransack
44
44
  self.conditions << condition if condition.valid?
45
45
  end
46
46
  end
47
- self.conditions.uniq!
47
+ remove_duplicate_conditions!
48
48
  end
49
49
  alias :c= :conditions=
50
50
 
@@ -68,7 +68,7 @@ module Ransack
68
68
  def respond_to?(method_id)
69
69
  super or begin
70
70
  method_name = method_id.to_s
71
- writer = method_name.sub!(/\=$/, Constants::EMPTY)
71
+ writer = method_name.sub!(/\=$/, ''.freeze)
72
72
  attribute_method?(method_name) ? true : false
73
73
  end
74
74
  end
@@ -114,7 +114,7 @@ module Ransack
114
114
 
115
115
  def method_missing(method_id, *args)
116
116
  method_name = method_id.to_s
117
- writer = method_name.sub!(/\=$/, Constants::EMPTY)
117
+ writer = method_name.sub!(/\=$/, ''.freeze)
118
118
  if attribute_method?(method_name)
119
119
  if writer
120
120
  write_attribute(method_name, *args)
@@ -169,7 +169,7 @@ module Ransack
169
169
  ]
170
170
  .reject { |e| e[1].blank? }
171
171
  .map { |v| "#{v[0]}: #{v[1]}" }
172
- .join(Constants::COMMA_SPACE)
172
+ .join(', '.freeze)
173
173
  "Grouping <#{data}>"
174
174
  end
175
175
 
@@ -195,6 +195,17 @@ module Ransack
195
195
  Predicate.detect_and_strip_from_string!(string)
196
196
  string
197
197
  end
198
+
199
+ def remove_duplicate_conditions!
200
+ # If self.conditions.uniq! is called without passing a block, then
201
+ # conditions differing only by ransacker_args within attributes are
202
+ # wrongly considered equal and are removed.
203
+ self.conditions.uniq! do |c|
204
+ c.attributes.map { |a| [a.name, a.ransacker_args] }.flatten +
205
+ [c.predicate.name] +
206
+ c.values.map { |v| v.value }
207
+ end
208
+ end
198
209
  end
199
210
  end
200
211
  end
@@ -3,7 +3,7 @@ module Ransack
3
3
  class Sort < Node
4
4
  include Bindable
5
5
 
6
- attr_reader :name, :dir
6
+ attr_reader :name, :dir, :ransacker_args
7
7
  i18n_word :asc, :desc
8
8
 
9
9
  class << self
@@ -16,7 +16,7 @@ module Ransack
16
16
 
17
17
  def build(params)
18
18
  params.with_indifferent_access.each do |key, value|
19
- if key.match(/^(name|dir)$/)
19
+ if key.match(/^(name|dir|ransacker_args)$/)
20
20
  self.send("#{key}=", value)
21
21
  end
22
22
  end
@@ -32,19 +32,23 @@ module Ransack
32
32
 
33
33
  def name=(name)
34
34
  @name = name
35
- context.bind(self, name) unless name.blank?
35
+ context.bind(self, name)
36
36
  end
37
37
 
38
38
  def dir=(dir)
39
39
  dir = dir.downcase if dir
40
40
  @dir =
41
- if Constants::ASC_DESC.include?(dir)
41
+ if ['asc'.freeze, 'desc'.freeze].freeze.include?(dir)
42
42
  dir
43
43
  else
44
- Constants::ASC
44
+ 'asc'.freeze
45
45
  end
46
46
  end
47
47
 
48
+ def ransacker_args=(ransack_args)
49
+ @ransacker_args = ransack_args
50
+ end
51
+
48
52
  end
49
53
  end
50
54
  end
@@ -10,7 +10,7 @@ module Ransack
10
10
  end
11
11
 
12
12
  def names_by_decreasing_length
13
- names.sort { |a,b| b.length <=> a.length }
13
+ names.sort { |a, b| b.length <=> a.length }
14
14
  end
15
15
 
16
16
  def named(name)
@@ -19,7 +19,7 @@ module Ransack
19
19
 
20
20
  def detect_and_strip_from_string!(str)
21
21
  if p = detect_from_string(str)
22
- str.sub! /_#{p}$/, Constants::EMPTY
22
+ str.sub! /_#{p}$/, ''.freeze
23
23
  p
24
24
  end
25
25
  end
@@ -74,5 +74,9 @@ module Ransack
74
74
  vals.any? { |v| validator.call(type ? v.cast(type) : v.value) }
75
75
  end
76
76
 
77
+ def negative?
78
+ @name.include?("not_".freeze)
79
+ end
80
+
77
81
  end
78
82
  end
@@ -1,6 +1,6 @@
1
1
  require 'ransack/nodes'
2
2
  require 'ransack/context'
3
- Ransack::Adapters.require_search
3
+ Ransack::Adapters.object_mapper.require_search
4
4
  require 'ransack/naming'
5
5
 
6
6
  module Ransack
@@ -15,6 +15,7 @@ module Ransack
15
15
  :translate, :to => :base
16
16
 
17
17
  def initialize(object, params = {}, options = {})
18
+ params = params.to_unsafe_h if params.respond_to?(:to_unsafe_h)
18
19
  if params.is_a? Hash
19
20
  params = params.dup
20
21
  params.delete_if { |k, v| [*v].all?{ |i| i.blank? && i != false } }
@@ -37,7 +38,7 @@ module Ransack
37
38
 
38
39
  def build(params)
39
40
  collapse_multiparameter_attributes!(params).each do |key, value|
40
- if Constants::S_SORTS.include?(key)
41
+ if ['s'.freeze, 'sorts'.freeze].freeze.include?(key)
41
42
  send("#{key}=", value)
42
43
  elsif base.attribute_method?(key)
43
44
  base.send("#{key}=", value)
@@ -92,7 +93,7 @@ module Ransack
92
93
 
93
94
  def method_missing(method_id, *args)
94
95
  method_name = method_id.to_s
95
- getter_name = method_name.sub(/=$/, Constants::EMPTY)
96
+ getter_name = method_name.sub(/=$/, ''.freeze)
96
97
  if base.attribute_method?(getter_name)
97
98
  base.send(method_id, *args)
98
99
  elsif @context.ransackable_scope?(getter_name, @context.object)
@@ -113,8 +114,8 @@ module Ransack
113
114
  [:base, base.inspect]
114
115
  ]
115
116
  .compact
116
- .map { |d| d.join(Constants::COLON_SPACE) }
117
- .join(Constants::COMMA_SPACE)
117
+ .map { |d| d.join(': '.freeze) }
118
+ .join(', '.freeze)
118
119
 
119
120
  "Ransack::Search<#{details}>"
120
121
  end
@@ -25,7 +25,7 @@ module Ransack
25
25
  |x| x.respond_to?(:model_name)
26
26
  }
27
27
  predicate = Predicate.detect_from_string(original_name)
28
- attributes_str = original_name.sub(/_#{predicate}$/, Constants::EMPTY)
28
+ attributes_str = original_name.sub(/_#{predicate}$/, ''.freeze)
29
29
  attribute_names = attributes_str.split(/_and_|_or_/)
30
30
  combinator = attributes_str.match(/_and_/) ? :and : :or
31
31
  defaults = base_ancestors.map do |klass|
@@ -74,7 +74,7 @@ module Ransack
74
74
  def self.attribute_name(context, name, include_associations = nil)
75
75
  @context, @name = context, name
76
76
  @assoc_path = context.association_path(name)
77
- @attr_name = @name.sub(/^#{@assoc_path}_/, Constants::EMPTY)
77
+ @attr_name = @name.sub(/^#{@assoc_path}_/, ''.freeze)
78
78
  associated_class = @context.traverse(@assoc_path) if @assoc_path.present?
79
79
  @include_associated = include_associations && associated_class
80
80
 
@@ -1,3 +1,3 @@
1
1
  module Ransack
2
- VERSION = '1.7.0'
2
+ VERSION = '1.8.0'
3
3
  end
@@ -20,14 +20,14 @@ Gem::Specification.new do |s|
20
20
  s.add_dependency 'activerecord', '>= 3.0'
21
21
  s.add_dependency 'activesupport', '>= 3.0'
22
22
  s.add_dependency 'i18n'
23
- s.add_dependency 'polyamorous', '~> 1.2'
24
- s.add_development_dependency 'rspec', '~> 2'
23
+ s.add_dependency 'polyamorous', '~> 1.3'
24
+ s.add_development_dependency 'rspec', '~> 3'
25
25
  s.add_development_dependency 'machinist', '~> 1.0.6'
26
26
  s.add_development_dependency 'faker', '~> 0.9.5'
27
27
  s.add_development_dependency 'sqlite3', '~> 1.3.3'
28
28
  s.add_development_dependency 'pg'
29
- s.add_development_dependency 'mysql2', '0.3.18'
30
- s.add_development_dependency 'pry', '0.9.12.2'
29
+ s.add_development_dependency 'mysql2', '0.3.20'
30
+ s.add_development_dependency 'pry', '0.10'
31
31
 
32
32
  s.files = `git ls-files`.split("\n")
33
33
 
@@ -20,7 +20,9 @@ module Ransack
20
20
 
21
21
  context 'with scopes' do
22
22
  before do
23
- Person.stub :ransackable_scopes => [:active, :over_age]
23
+ allow(Person)
24
+ .to receive(:ransackable_scopes)
25
+ .and_return([:active, :over_age])
24
26
  end
25
27
 
26
28
  it "applies true scopes" do
@@ -50,6 +52,21 @@ module Ransack
50
52
  end
51
53
  end
52
54
 
55
+ describe '#ransack_alias' do
56
+ it 'translates an alias to the correct attributes' do
57
+ p = Person.create!(name: 'Meatloaf', email: 'babies@example.com')
58
+
59
+ s = Person.ransack(term_cont: 'atlo')
60
+ expect(s.result.to_a).to eq [p]
61
+
62
+ s = Person.ransack(term_cont: 'babi')
63
+ expect(s.result.to_a).to eq [p]
64
+
65
+ s = Person.ransack(term_cont: 'nomatch')
66
+ expect(s.result.to_a).to eq []
67
+ end
68
+ end
69
+
53
70
  describe '#ransacker' do
54
71
  # For infix tests
55
72
  def self.sane_adapter?
@@ -213,6 +230,7 @@ module Ransack
213
230
  it { should include 'name' }
214
231
  it { should include 'reversed_name' }
215
232
  it { should include 'doubled_name' }
233
+ it { should include 'term' }
216
234
  it { should include 'only_search' }
217
235
  it { should_not include 'only_sort' }
218
236
  it { should_not include 'only_admin' }
@@ -224,6 +242,7 @@ module Ransack
224
242
  it { should include 'name' }
225
243
  it { should include 'reversed_name' }
226
244
  it { should include 'doubled_name' }
245
+ it { should include 'term' }
227
246
  it { should include 'only_search' }
228
247
  it { should_not include 'only_sort' }
229
248
  it { should include 'only_admin' }
@@ -4,6 +4,21 @@ module Ransack
4
4
  module Nodes
5
5
  describe Condition do
6
6
 
7
+ context 'with an alias' do
8
+ subject {
9
+ Condition.extract(
10
+ Context.for(Person), 'term_start', Person.first(2).map(&:name)
11
+ )
12
+ }
13
+
14
+ specify { expect(subject.combinator).to eq 'or' }
15
+ specify { expect(subject.predicate.name).to eq 'start' }
16
+
17
+ it 'converts the alias to the correct attributes' do
18
+ expect(subject.attributes.map(&:name)).to eq(['name', 'email'])
19
+ end
20
+ end
21
+
7
22
  context 'with multiple values and an _any predicate' do
8
23
  subject { Condition.extract(Context.for(Person), 'name_eq_any', Person.first(2).map(&:name)) }
9
24
 
@@ -1,4 +1,9 @@
1
1
  test:
2
+ clients:
3
+ default:
4
+ database: ransack_mongoid_test
5
+ hosts:
6
+ - localhost:27017
2
7
  sessions:
3
8
  default:
4
9
  database: ransack_mongoid_test
@@ -1,6 +1,8 @@
1
1
  require 'mongoid'
2
2
 
3
3
  Mongoid.load!(File.expand_path("../mongoid.yml", __FILE__), :test)
4
+ Mongo::Logger.logger.level = Logger::WARN if defined?(Mongo)
5
+ Mongoid.purge!
4
6
 
5
7
  class Person
6
8
  include Mongoid::Document
@@ -20,6 +22,8 @@ class Person
20
22
  has_many :articles
21
23
  has_many :comments
22
24
 
25
+ ransack_alias :term, :name_or_email
26
+
23
27
  # has_many :authored_article_comments, :through => :articles,
24
28
  # :source => :comments, :foreign_key => :person_id
25
29
 
@@ -9,10 +9,9 @@ I18n.enforce_available_locales = false
9
9
  Time.zone = 'Eastern Time (US & Canada)'
10
10
  I18n.load_path += Dir[File.join(File.dirname(__FILE__), 'support', '*.yml')]
11
11
 
12
- Dir[File.expand_path('../{mongoid/helpers,mongoid/support,blueprints}/*.rb', __FILE__)]
13
- .each do |f|
14
- require f
15
- end
12
+ Dir[File.expand_path('../{mongoid/helpers,mongoid/support,blueprints}/*.rb',
13
+ __FILE__)]
14
+ .each { |f| require f }
16
15
 
17
16
  Sham.define do
18
17
  name { Faker::Name.name }
@@ -31,11 +30,16 @@ RSpec.configure do |config|
31
30
  config.alias_it_should_behave_like_to :it_has_behavior, 'has behavior'
32
31
 
33
32
  config.before(:suite) do
34
- puts '=' * 80
35
- connection_name = Mongoid.default_session.inspect
36
- puts "Running specs against #{connection_name}, Mongoid #{
37
- Mongoid::VERSION}, Moped #{Moped::VERSION} and Origin #{Origin::VERSION}..."
38
- puts '=' * 80
33
+ if ENV['DB'] == 'mongoid4'
34
+ message = "Running Ransack specs with #{Mongoid.default_session.inspect
35
+ }, Mongoid #{Mongoid::VERSION}, Moped #{Moped::VERSION
36
+ }, Origin #{Origin::VERSION} and Ruby #{RUBY_VERSION}"
37
+ else
38
+ message = "Running Ransack specs with #{Mongoid.default_client.inspect
39
+ }, Mongoid #{Mongoid::VERSION}, Mongo driver #{Mongo::VERSION}"
40
+ end
41
+ line = '=' * message.length
42
+ puts line, message, line
39
43
  Schema.create
40
44
  end
41
45
 
@@ -20,50 +20,66 @@ module Ransack
20
20
 
21
21
  context 'with scopes' do
22
22
  before do
23
- Person.stub :ransackable_scopes => [:active, :over_age, :of_age]
23
+ allow(Person)
24
+ .to receive(:ransackable_scopes)
25
+ .and_return([:active, :over_age, :of_age])
24
26
  end
25
27
 
26
- it "applies true scopes" do
28
+ it 'applies true scopes' do
27
29
  s = Person.ransack('active' => true)
28
30
  expect(s.result.to_sql).to (include 'active = 1')
29
31
  end
30
32
 
31
- it "applies stringy true scopes" do
33
+ it 'applies stringy true scopes' do
32
34
  s = Person.ransack('active' => 'true')
33
35
  expect(s.result.to_sql).to (include 'active = 1')
34
36
  end
35
37
 
36
- it "applies stringy boolean scopes with true value in an array" do
38
+ it 'applies stringy boolean scopes with true value in an array' do
37
39
  s = Person.ransack('of_age' => ['true'])
38
40
  expect(s.result.to_sql).to (include 'age >= 18')
39
41
  end
40
42
 
41
- it "applies stringy boolean scopes with false value in an array" do
43
+ it 'applies stringy boolean scopes with false value in an array' do
42
44
  s = Person.ransack('of_age' => ['false'])
43
45
  expect(s.result.to_sql).to (include 'age < 18')
44
46
  end
45
47
 
46
- it "ignores unlisted scopes" do
48
+ it 'ignores unlisted scopes' do
47
49
  s = Person.ransack('restricted' => true)
48
50
  expect(s.result.to_sql).to_not (include 'restricted')
49
51
  end
50
52
 
51
- it "ignores false scopes" do
53
+ it 'ignores false scopes' do
52
54
  s = Person.ransack('active' => false)
53
55
  expect(s.result.to_sql).not_to (include 'active')
54
56
  end
55
57
 
56
- it "ignores stringy false scopes" do
58
+ it 'ignores stringy false scopes' do
57
59
  s = Person.ransack('active' => 'false')
58
60
  expect(s.result.to_sql).to_not (include 'active')
59
61
  end
60
62
 
61
- it "passes values to scopes" do
63
+ it 'passes values to scopes' do
62
64
  s = Person.ransack('over_age' => 18)
63
65
  expect(s.result.to_sql).to (include 'age > 18')
64
66
  end
65
67
 
66
- it "chains scopes" do
68
+ # TODO: Implement a way to pass true/false values like 0 or 1 to
69
+ # scopes (e.g. with `in` / `not_in` predicates), without Ransack
70
+ # converting them to true/false boolean values instead.
71
+
72
+ # it 'passes true values to scopes', focus: true do
73
+ # s = Person.ransack('over_age' => 1)
74
+ # expect(s.result.to_sql).to (include 'age > 1')
75
+ # end
76
+
77
+ # it 'passes false values to scopes', focus: true do
78
+ # s = Person.ransack('over_age' => 0)
79
+ # expect(s.result.to_sql).to (include 'age > 0')
80
+ # end
81
+
82
+ it 'chains scopes' do
67
83
  s = Person.ransack('over_age' => 18, 'active' => true)
68
84
  expect(s.result.to_sql).to (include 'age > 18')
69
85
  expect(s.result.to_sql).to (include 'active = 1')
@@ -75,16 +91,112 @@ module Ransack
75
91
  end
76
92
 
77
93
  it 'does not modify the parameters' do
78
- params = { :name_eq => '' }
94
+ params = { name_eq: '' }
79
95
  expect { Person.ransack(params) }.not_to change { params }
80
96
  end
81
97
  end
82
98
 
99
+ context 'negative conditions on HABTM associations' do
100
+ let(:medieval) { Tag.create!(name: 'Medieval') }
101
+ let(:fantasy) { Tag.create!(name: 'Fantasy') }
102
+ let(:arthur) { Article.create!(title: 'King Arthur') }
103
+ let(:marco) { Article.create!(title: 'Marco Polo') }
104
+
105
+ before do
106
+ marco.tags << medieval
107
+ arthur.tags << medieval
108
+ arthur.tags << fantasy
109
+ end
110
+
111
+ it 'removes redundant joins from top query' do
112
+ s = Article.ransack(tags_name_not_eq: "Fantasy")
113
+ sql = s.result.to_sql
114
+
115
+ expect(sql).to_not include('LEFT OUTER JOIN')
116
+ end
117
+
118
+ it 'handles != for single values' do
119
+ s = Article.ransack(tags_name_not_eq: "Fantasy")
120
+ articles = s.result.to_a
121
+
122
+ expect(articles).to include marco
123
+ expect(articles).to_not include arthur
124
+ end
125
+
126
+ it 'handles NOT IN for multiple attributes' do
127
+ s = Article.ransack(tags_name_not_in: ["Fantasy", "Scifi"])
128
+ articles = s.result.to_a
129
+
130
+ expect(articles).to include marco
131
+ expect(articles).to_not include arthur
132
+ end
133
+ end
134
+
135
+ context 'negative conditions on self-referenced associations' do
136
+ let(:pop) { Person.create!(name: 'Grandpa') }
137
+ let(:dad) { Person.create!(name: 'Father') }
138
+ let(:mom) { Person.create!(name: 'Mother') }
139
+ let(:son) { Person.create!(name: 'Grandchild') }
140
+
141
+ before do
142
+ son.parent = dad
143
+ dad.parent = pop
144
+ dad.children << son
145
+ mom.children << son
146
+ pop.children << dad
147
+ son.save! && dad.save! && mom.save! && pop.save!
148
+ end
149
+
150
+ it 'handles multiple associations and aliases' do
151
+ s = Person.ransack(
152
+ c: {
153
+ '0' => { a: ['name'], p: 'not_eq', v: ['Father'] },
154
+ '1' => {
155
+ a: ['children_name', 'parent_name'],
156
+ p: 'not_eq', v: ['Father'], m: 'or'
157
+ },
158
+ '2' => { a: ['children_salary'], p: 'eq', v: [nil] }
159
+ })
160
+ people = s.result
161
+
162
+ expect(people.to_a).to include son
163
+ expect(people.to_a).to include mom
164
+ expect(people.to_a).to_not include dad # rule '0': 'name'
165
+ expect(people.to_a).to_not include pop # rule '1': 'children_name'
166
+ end
167
+ end
168
+
169
+ describe '#ransack_alias' do
170
+ it 'translates an alias to the correct attributes' do
171
+ p = Person.create!(name: 'Meatloaf', email: 'babies@example.com')
172
+
173
+ s = Person.ransack(term_cont: 'atlo')
174
+ expect(s.result.to_a).to eq [p]
175
+
176
+ s = Person.ransack(term_cont: 'babi')
177
+ expect(s.result.to_a).to eq [p]
178
+
179
+ s = Person.ransack(term_cont: 'nomatch')
180
+ expect(s.result.to_a).to eq []
181
+ end
182
+
183
+ it 'also works with associations' do
184
+ dad = Person.create!(name: 'Birdman')
185
+ son = Person.create!(name: 'Weezy', parent: dad)
186
+
187
+ s = Person.ransack(daddy_eq: 'Birdman')
188
+ expect(s.result.to_a).to eq [son]
189
+
190
+ s = Person.ransack(daddy_eq: 'Drake')
191
+ expect(s.result.to_a).to eq []
192
+ end
193
+ end
194
+
83
195
  describe '#ransacker' do
84
196
  # For infix tests
85
197
  def self.sane_adapter?
86
198
  case ::ActiveRecord::Base.connection.adapter_name
87
- when "SQLite3", "PostgreSQL"
199
+ when 'SQLite3', 'PostgreSQL'
88
200
  true
89
201
  else
90
202
  false
@@ -102,87 +214,111 @@ module Ransack
102
214
  # end
103
215
 
104
216
  it 'creates ransack attributes' do
105
- s = Person.ransack(:reversed_name_eq => 'htimS cirA')
217
+ s = Person.ransack(reversed_name_eq: 'htimS cirA')
106
218
  expect(s.result.size).to eq(1)
107
219
 
108
220
  expect(s.result.first).to eq Person.where(name: 'Aric Smith').first
109
221
  end
110
222
 
111
223
  it 'can be accessed through associations' do
112
- s = Person.ransack(:children_reversed_name_eq => 'htimS cirA')
224
+ s = Person.ransack(children_reversed_name_eq: 'htimS cirA')
113
225
  expect(s.result.to_sql).to match(
114
226
  /#{quote_table_name("children_people")}.#{
115
227
  quote_column_name("name")} = 'Aric Smith'/
116
228
  )
117
229
  end
118
230
 
119
- it 'allows an "attribute" to be an InfixOperation' do
120
- s = Person.ransack(:doubled_name_eq => 'Aric SmithAric Smith')
231
+ it 'allows an attribute to be an InfixOperation' do
232
+ s = Person.ransack(doubled_name_eq: 'Aric SmithAric Smith')
121
233
  expect(s.result.first).to eq Person.where(name: 'Aric Smith').first
122
234
  end if defined?(Arel::Nodes::InfixOperation) && sane_adapter?
123
235
 
124
- it "doesn't break #count if using InfixOperations" do
125
- s = Person.ransack(:doubled_name_eq => 'Aric SmithAric Smith')
236
+ it 'does not break #count if using InfixOperations' do
237
+ s = Person.ransack(doubled_name_eq: 'Aric SmithAric Smith')
126
238
  expect(s.result.count).to eq 1
127
239
  end if defined?(Arel::Nodes::InfixOperation) && sane_adapter?
128
240
 
129
- it "should remove empty key value pairs from the params hash" do
130
- s = Person.ransack(:children_reversed_name_eq => '')
241
+ it 'should remove empty key value pairs from the params hash' do
242
+ s = Person.ransack(children_reversed_name_eq: '')
131
243
  expect(s.result.to_sql).not_to match /LEFT OUTER JOIN/
132
244
  end
133
245
 
134
- it "should keep proper key value pairs in the params hash" do
135
- s = Person.ransack(:children_reversed_name_eq => 'Testing')
246
+ it 'should keep proper key value pairs in the params hash' do
247
+ s = Person.ransack(children_reversed_name_eq: 'Testing')
136
248
  expect(s.result.to_sql).to match /LEFT OUTER JOIN/
137
249
  end
138
250
 
139
- it "should function correctly when nil is passed in" do
251
+ it 'should function correctly when nil is passed in' do
140
252
  s = Person.ransack(nil)
141
253
  end
142
254
 
143
- it "should function correctly when a blank string is passed in" do
255
+ it 'should function correctly when a blank string is passed in' do
144
256
  s = Person.ransack('')
145
257
  end
146
258
 
147
- it "should function correctly with a multi-parameter attribute" do
259
+ it 'should function correctly with a multi-parameter attribute' do
148
260
  ::ActiveRecord::Base.default_timezone = :utc
149
261
  Time.zone = 'UTC'
150
262
 
151
263
  date = Date.current
152
264
  s = Person.ransack(
153
- { "created_at_gteq(1i)" => date.year,
154
- "created_at_gteq(2i)" => date.month,
155
- "created_at_gteq(3i)" => date.day
265
+ { 'created_at_gteq(1i)' => date.year,
266
+ 'created_at_gteq(2i)' => date.month,
267
+ 'created_at_gteq(3i)' => date.day
156
268
  }
157
269
  )
158
270
  expect(s.result.to_sql).to match />=/
159
271
  expect(s.result.to_sql).to match date.to_s
160
272
  end
161
273
 
162
- it "should function correctly when using fields with dots in them" do
163
- s = Person.ransack(:email_cont => "example.com")
274
+ it 'should function correctly when using fields with dots in them' do
275
+ s = Person.ransack(email_cont: 'example.com')
164
276
  expect(s.result.exists?).to be true
165
277
  end
166
278
 
167
- it "should function correctly when using fields with % in them" do
168
- p = Person.create!(:name => "110%-er")
169
- s = Person.ransack(:name_cont => "10%")
279
+ it 'should function correctly when using fields with % in them' do
280
+ p = Person.create!(name: '110%-er')
281
+ s = Person.ransack(name_cont: '10%')
170
282
  expect(s.result.to_a).to eq [p]
171
283
  end
172
284
 
173
- it "should function correctly when using fields with backslashes in them" do
174
- p = Person.create!(:name => "\\WINNER\\")
175
- s = Person.ransack(:name_cont => "\\WINNER\\")
285
+ it 'should function correctly when using fields with backslashes in them' do
286
+ p = Person.create!(name: "\\WINNER\\")
287
+ s = Person.ransack(name_cont: "\\WINNER\\")
176
288
  expect(s.result.to_a).to eq [p]
177
289
  end
178
290
 
179
- context "searching on an `in` predicate with a ransacker" do
180
- it "should function correctly when passing an array of ids" do
291
+ context 'searching by underscores' do
292
+ # when escaping is supported right in LIKE expression without adding extra expressions
293
+ def self.simple_escaping?
294
+ case ::ActiveRecord::Base.connection.adapter_name
295
+ when 'Mysql2', 'PostgreSQL'
296
+ true
297
+ else
298
+ false
299
+ end
300
+ end
301
+
302
+ it 'should search correctly if matches exist' do
303
+ p = Person.create!(name: 'name_with_underscore')
304
+ s = Person.ransack(name_cont: 'name_')
305
+ expect(s.result.to_a).to eq [p]
306
+ end if simple_escaping?
307
+
308
+ it 'should return empty result if no matches' do
309
+ Person.create!(name: 'name_with_underscore')
310
+ s = Person.ransack(name_cont: 'n_')
311
+ expect(s.result.to_a).to eq []
312
+ end if simple_escaping?
313
+ end
314
+
315
+ context 'searching on an `in` predicate with a ransacker' do
316
+ it 'should function correctly when passing an array of ids' do
181
317
  s = Person.ransack(array_users_in: true)
182
318
  expect(s.result.count).to be > 0
183
319
  end
184
320
 
185
- it "should function correctly when passing an array of strings" do
321
+ it 'should function correctly when passing an array of strings' do
186
322
  Person.create!(name: Person.first.id.to_s)
187
323
  s = Person.ransack(array_names_in: true)
188
324
  expect(s.result.count).to be > 0
@@ -196,49 +332,67 @@ module Ransack
196
332
  end
197
333
  end
198
334
 
199
- context "search on an `in` predicate with an array" do
200
- it "should function correctly when passing an array of ids" do
335
+ context 'search on an `in` predicate with an array' do
336
+ it 'should function correctly when passing an array of ids' do
201
337
  array = Person.all.map(&:id)
202
338
  s = Person.ransack(id_in: array)
203
339
  expect(s.result.count).to eq array.size
204
340
  end
205
341
  end
206
342
 
207
- it "should function correctly when an attribute name ends with '_start'" do
208
- p = Person.create!(:new_start => 'Bar and foo', :name => 'Xiang')
343
+ it 'should work correctly when an attribute name ends with _start' do
344
+ p = Person.create!(new_start: 'Bar and foo', name: 'Xiang')
209
345
 
210
- s = Person.ransack(:new_start_end => ' and foo')
346
+ s = Person.ransack(new_start_end: ' and foo')
211
347
  expect(s.result.to_a).to eq [p]
212
348
 
213
- s = Person.ransack(:name_or_new_start_start => 'Xia')
349
+ s = Person.ransack(name_or_new_start_start: 'Xia')
214
350
  expect(s.result.to_a).to eq [p]
215
351
 
216
- s = Person.ransack(:new_start_or_name_end => 'iang')
352
+ s = Person.ransack(new_start_or_name_end: 'iang')
217
353
  expect(s.result.to_a).to eq [p]
218
354
  end
219
355
 
220
- it "should function correctly when an attribute name ends with '_end'" do
221
- p = Person.create!(:stop_end => 'Foo and bar', :name => 'Marianne')
356
+ it 'should work correctly when an attribute name ends with _end' do
357
+ p = Person.create!(stop_end: 'Foo and bar', name: 'Marianne')
222
358
 
223
- s = Person.ransack(:stop_end_start => 'Foo and')
359
+ s = Person.ransack(stop_end_start: 'Foo and')
224
360
  expect(s.result.to_a).to eq [p]
225
361
 
226
- s = Person.ransack(:stop_end_or_name_end => 'anne')
362
+ s = Person.ransack(stop_end_or_name_end: 'anne')
227
363
  expect(s.result.to_a).to eq [p]
228
364
 
229
- s = Person.ransack(:name_or_stop_end_end => ' bar')
365
+ s = Person.ransack(name_or_stop_end_end: ' bar')
230
366
  expect(s.result.to_a).to eq [p]
231
367
  end
232
368
 
233
- it "should function correctly when an attribute name has 'and' in it" do
234
- p = Person.create!(:terms_and_conditions => true)
235
- s = Person.ransack(:terms_and_conditions_eq => true)
369
+ it 'should work correctly when an attribute name has `and` in it' do
370
+ p = Person.create!(terms_and_conditions: true)
371
+ s = Person.ransack(terms_and_conditions_eq: true)
236
372
  expect(s.result.to_a).to eq [p]
237
373
  end
238
374
 
239
- it 'allows sort by "only_sort" field' do
375
+ context 'attribute aliased column names',
376
+ if: Ransack::SUPPORTS_ATTRIBUTE_ALIAS do
377
+ it 'should be translated to original column name' do
378
+ s = Person.ransack(full_name_eq: 'Nicolas Cage')
379
+ expect(s.result.to_sql).to match(
380
+ /WHERE #{quote_table_name("people")}.#{quote_column_name("name")}/
381
+ )
382
+ end
383
+
384
+ it 'should translate on associations' do
385
+ s = Person.ransack(articles_content_cont: 'Nicolas Cage')
386
+ expect(s.result.to_sql).to match(
387
+ /#{quote_table_name("articles")}.#{
388
+ quote_column_name("body")} I?LIKE '%Nicolas Cage%'/
389
+ )
390
+ end
391
+ end
392
+
393
+ it 'allows sort by `only_sort` field' do
240
394
  s = Person.ransack(
241
- "s" => { "0" => { "dir" => "asc", "name" => "only_sort" } }
395
+ 's' => { '0' => { 'dir' => 'asc', 'name' => 'only_sort' } }
242
396
  )
243
397
  expect(s.result.to_sql).to match(
244
398
  /ORDER BY #{quote_table_name("people")}.#{
@@ -246,9 +400,9 @@ module Ransack
246
400
  )
247
401
  end
248
402
 
249
- it "doesn't sort by 'only_search' field" do
403
+ it 'does not sort by `only_search` field' do
250
404
  s = Person.ransack(
251
- "s" => { "0" => { "dir" => "asc", "name" => "only_search" } }
405
+ 's' => { '0' => { 'dir' => 'asc', 'name' => 'only_search' } }
252
406
  )
253
407
  expect(s.result.to_sql).not_to match(
254
408
  /ORDER BY #{quote_table_name("people")}.#{
@@ -256,25 +410,25 @@ module Ransack
256
410
  )
257
411
  end
258
412
 
259
- it 'allows search by "only_search" field' do
260
- s = Person.ransack(:only_search_eq => 'htimS cirA')
413
+ it 'allows search by `only_search` field' do
414
+ s = Person.ransack(only_search_eq: 'htimS cirA')
261
415
  expect(s.result.to_sql).to match(
262
416
  /WHERE #{quote_table_name("people")}.#{
263
417
  quote_column_name("only_search")} = 'htimS cirA'/
264
418
  )
265
419
  end
266
420
 
267
- it "can't be searched by 'only_sort'" do
268
- s = Person.ransack(:only_sort_eq => 'htimS cirA')
421
+ it 'cannot be searched by `only_sort`' do
422
+ s = Person.ransack(only_sort_eq: 'htimS cirA')
269
423
  expect(s.result.to_sql).not_to match(
270
424
  /WHERE #{quote_table_name("people")}.#{
271
425
  quote_column_name("only_sort")} = 'htimS cirA'/
272
426
  )
273
427
  end
274
428
 
275
- it 'allows sort by "only_admin" field, if auth_object: :admin' do
429
+ it 'allows sort by `only_admin` field, if auth_object: :admin' do
276
430
  s = Person.ransack(
277
- { "s" => { "0" => { "dir" => "asc", "name" => "only_admin" } } },
431
+ { 's' => { '0' => { 'dir' => 'asc', 'name' => 'only_admin' } } },
278
432
  { auth_object: :admin }
279
433
  )
280
434
  expect(s.result.to_sql).to match(
@@ -283,9 +437,9 @@ module Ransack
283
437
  )
284
438
  end
285
439
 
286
- it "doesn't sort by 'only_admin' field, if auth_object: nil" do
440
+ it 'does not sort by `only_admin` field, if auth_object: nil' do
287
441
  s = Person.ransack(
288
- "s" => { "0" => { "dir" => "asc", "name" => "only_admin" } }
442
+ 's' => { '0' => { 'dir' => 'asc', 'name' => 'only_admin' } }
289
443
  )
290
444
  expect(s.result.to_sql).not_to match(
291
445
  /ORDER BY #{quote_table_name("people")}.#{
@@ -293,10 +447,10 @@ module Ransack
293
447
  )
294
448
  end
295
449
 
296
- it 'allows search by "only_admin" field, if auth_object: :admin' do
450
+ it 'allows search by `only_admin` field, if auth_object: :admin' do
297
451
  s = Person.ransack(
298
- { :only_admin_eq => 'htimS cirA' },
299
- { :auth_object => :admin }
452
+ { only_admin_eq: 'htimS cirA' },
453
+ { auth_object: :admin }
300
454
  )
301
455
  expect(s.result.to_sql).to match(
302
456
  /WHERE #{quote_table_name("people")}.#{
@@ -304,8 +458,8 @@ module Ransack
304
458
  )
305
459
  end
306
460
 
307
- it "can't be searched by 'only_admin', if auth_object: nil" do
308
- s = Person.ransack(:only_admin_eq => 'htimS cirA')
461
+ it 'cannot be searched by `only_admin`, if auth_object: nil' do
462
+ s = Person.ransack(only_admin_eq: 'htimS cirA')
309
463
  expect(s.result.to_sql).not_to match(
310
464
  /WHERE #{quote_table_name("people")}.#{
311
465
  quote_column_name("only_admin")} = 'htimS cirA'/
@@ -332,6 +486,25 @@ module Ransack
332
486
  )
333
487
  expect { s.result.first }.to_not raise_error
334
488
  end
489
+
490
+ it 'should allow sort passing arguments to a ransacker' do
491
+ s = Person.ransack(
492
+ s: {
493
+ '0' => {
494
+ name: 'with_arguments', dir: 'desc', ransacker_args: [2, 6]
495
+ }
496
+ }
497
+ )
498
+ expect(s.result.to_sql).to match(
499
+ /ORDER BY \(SELECT MAX\(articles.title\) FROM articles/
500
+ )
501
+ expect(s.result.to_sql).to match(
502
+ /WHERE articles.person_id = people.id AND LENGTH\(articles.body\)/
503
+ )
504
+ expect(s.result.to_sql).to match(
505
+ /BETWEEN 2 AND 6 GROUP BY articles.person_id \) DESC/
506
+ )
507
+ end
335
508
  end
336
509
 
337
510
  describe '#ransackable_attributes' do
@@ -341,9 +514,14 @@ module Ransack
341
514
  it { should include 'name' }
342
515
  it { should include 'reversed_name' }
343
516
  it { should include 'doubled_name' }
517
+ it { should include 'term' }
344
518
  it { should include 'only_search' }
345
519
  it { should_not include 'only_sort' }
346
520
  it { should_not include 'only_admin' }
521
+
522
+ if Ransack::SUPPORTS_ATTRIBUTE_ALIAS
523
+ it { should include 'full_name' }
524
+ end
347
525
  end
348
526
 
349
527
  context 'with auth_object :admin' do