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.
- 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
|