ransack 1.7.0 → 1.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +40 -22
- data/CHANGELOG.md +176 -27
- data/CONTRIBUTING.md +30 -19
- data/Gemfile +8 -3
- data/README.md +131 -58
- data/Rakefile +5 -2
- data/lib/ransack.rb +10 -5
- data/lib/ransack/adapters.rb +43 -23
- data/lib/ransack/adapters/active_record.rb +2 -2
- data/lib/ransack/adapters/active_record/3.0/compat.rb +5 -5
- data/lib/ransack/adapters/active_record/3.0/context.rb +5 -3
- data/lib/ransack/adapters/active_record/3.1/context.rb +1 -4
- data/lib/ransack/adapters/active_record/base.rb +12 -1
- data/lib/ransack/adapters/active_record/context.rb +148 -55
- data/lib/ransack/adapters/active_record/ransack/constants.rb +53 -53
- data/lib/ransack/adapters/active_record/ransack/context.rb +3 -1
- data/lib/ransack/adapters/active_record/ransack/nodes/condition.rb +20 -28
- data/lib/ransack/adapters/mongoid/base.rb +21 -6
- data/lib/ransack/adapters/mongoid/context.rb +9 -5
- data/lib/ransack/configuration.rb +24 -3
- data/lib/ransack/constants.rb +11 -22
- data/lib/ransack/context.rb +20 -13
- data/lib/ransack/helpers/form_builder.rb +5 -6
- data/lib/ransack/helpers/form_helper.rb +50 -69
- data/lib/ransack/locale/da.yml +70 -0
- data/lib/ransack/locale/id.yml +70 -0
- data/lib/ransack/locale/ja.yml +70 -0
- data/lib/ransack/locale/pt-BR.yml +70 -0
- data/lib/ransack/locale/{zh.yml → zh-CN.yml} +1 -1
- data/lib/ransack/locale/zh-TW.yml +70 -0
- data/lib/ransack/nodes.rb +1 -1
- data/lib/ransack/nodes/attribute.rb +4 -1
- data/lib/ransack/nodes/bindable.rb +18 -6
- data/lib/ransack/nodes/condition.rb +58 -28
- data/lib/ransack/nodes/grouping.rb +15 -4
- data/lib/ransack/nodes/sort.rb +9 -5
- data/lib/ransack/predicate.rb +6 -2
- data/lib/ransack/search.rb +6 -5
- data/lib/ransack/translate.rb +2 -2
- data/lib/ransack/version.rb +1 -1
- data/ransack.gemspec +4 -4
- data/spec/mongoid/adapters/mongoid/base_spec.rb +20 -1
- data/spec/mongoid/nodes/condition_spec.rb +15 -0
- data/spec/mongoid/support/mongoid.yml +5 -0
- data/spec/mongoid/support/schema.rb +4 -0
- data/spec/mongoid_spec_helper.rb +13 -9
- data/spec/ransack/adapters/active_record/base_spec.rb +249 -71
- data/spec/ransack/adapters/active_record/context_spec.rb +16 -18
- data/spec/ransack/helpers/form_builder_spec.rb +5 -2
- data/spec/ransack/helpers/form_helper_spec.rb +84 -14
- data/spec/ransack/nodes/condition_spec.rb +24 -0
- data/spec/ransack/nodes/grouping_spec.rb +56 -0
- data/spec/ransack/predicate_spec.rb +5 -5
- data/spec/ransack/search_spec.rb +79 -70
- data/spec/support/schema.rb +43 -29
- metadata +17 -12
@@ -44,7 +44,7 @@ module Ransack
|
|
44
44
|
self.conditions << condition if condition.valid?
|
45
45
|
end
|
46
46
|
end
|
47
|
-
|
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!(/\=$/,
|
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!(/\=$/,
|
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(
|
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
|
data/lib/ransack/nodes/sort.rb
CHANGED
@@ -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)
|
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
|
41
|
+
if ['asc'.freeze, 'desc'.freeze].freeze.include?(dir)
|
42
42
|
dir
|
43
43
|
else
|
44
|
-
|
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
|
data/lib/ransack/predicate.rb
CHANGED
@@ -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}$/,
|
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
|
data/lib/ransack/search.rb
CHANGED
@@ -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
|
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(/=$/,
|
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(
|
117
|
-
.join(
|
117
|
+
.map { |d| d.join(': '.freeze) }
|
118
|
+
.join(', '.freeze)
|
118
119
|
|
119
120
|
"Ransack::Search<#{details}>"
|
120
121
|
end
|
data/lib/ransack/translate.rb
CHANGED
@@ -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}$/,
|
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}_/,
|
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
|
|
data/lib/ransack/version.rb
CHANGED
data/ransack.gemspec
CHANGED
@@ -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.
|
24
|
-
s.add_development_dependency 'rspec', '~>
|
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.
|
30
|
-
s.add_development_dependency 'pry', '0.
|
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
|
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,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
|
|
data/spec/mongoid_spec_helper.rb
CHANGED
@@ -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',
|
13
|
-
|
14
|
-
|
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
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
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
|
23
|
+
allow(Person)
|
24
|
+
.to receive(:ransackable_scopes)
|
25
|
+
.and_return([:active, :over_age, :of_age])
|
24
26
|
end
|
25
27
|
|
26
|
-
it
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
-
|
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 = { :
|
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
|
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(:
|
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(:
|
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
|
120
|
-
s = Person.ransack(:
|
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
|
125
|
-
s = Person.ransack(:
|
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
|
130
|
-
s = Person.ransack(:
|
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
|
135
|
-
s = Person.ransack(:
|
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
|
251
|
+
it 'should function correctly when nil is passed in' do
|
140
252
|
s = Person.ransack(nil)
|
141
253
|
end
|
142
254
|
|
143
|
-
it
|
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
|
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
|
-
{
|
154
|
-
|
155
|
-
|
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
|
163
|
-
s = Person.ransack(:
|
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
|
168
|
-
p = Person.create!(:
|
169
|
-
s = Person.ransack(:
|
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
|
174
|
-
p = Person.create!(:
|
175
|
-
s = Person.ransack(:
|
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
|
180
|
-
|
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
|
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
|
200
|
-
it
|
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
|
208
|
-
p = Person.create!(:
|
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(:
|
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(:
|
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(:
|
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
|
221
|
-
p = Person.create!(:
|
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(:
|
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(:
|
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(:
|
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
|
234
|
-
p = Person.create!(:
|
235
|
-
s = Person.ransack(:
|
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
|
-
|
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
|
-
|
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
|
403
|
+
it 'does not sort by `only_search` field' do
|
250
404
|
s = Person.ransack(
|
251
|
-
|
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
|
260
|
-
s = Person.ransack(:
|
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
|
268
|
-
s = Person.ransack(:
|
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
|
429
|
+
it 'allows sort by `only_admin` field, if auth_object: :admin' do
|
276
430
|
s = Person.ransack(
|
277
|
-
{
|
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
|
440
|
+
it 'does not sort by `only_admin` field, if auth_object: nil' do
|
287
441
|
s = Person.ransack(
|
288
|
-
|
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
|
450
|
+
it 'allows search by `only_admin` field, if auth_object: :admin' do
|
297
451
|
s = Person.ransack(
|
298
|
-
{ :
|
299
|
-
{ :
|
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
|
308
|
-
s = Person.ransack(:
|
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
|