ransack 1.7.0 → 1.8.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 (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