ransack 1.2.3 → 1.3.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 +3 -4
- data/CONTRIBUTING.md +12 -4
- data/Gemfile +4 -5
- data/README.md +160 -55
- data/lib/ransack.rb +1 -1
- data/lib/ransack/adapters/active_record/3.0/context.rb +16 -0
- data/lib/ransack/adapters/active_record/3.1/context.rb +24 -0
- data/lib/ransack/adapters/active_record/base.rb +6 -0
- data/lib/ransack/adapters/active_record/context.rb +49 -1
- data/lib/ransack/configuration.rb +23 -6
- data/lib/ransack/constants.rb +46 -45
- data/lib/ransack/context.rb +19 -2
- data/lib/ransack/helpers/form_builder.rb +5 -4
- data/lib/ransack/helpers/form_helper.rb +34 -14
- data/lib/ransack/locale/hu.yml +70 -0
- data/lib/ransack/locale/nl.yml +70 -0
- data/lib/ransack/nodes/attribute.rb +2 -2
- data/lib/ransack/nodes/condition.rb +29 -12
- data/lib/ransack/nodes/grouping.rb +6 -6
- data/lib/ransack/nodes/node.rb +1 -1
- data/lib/ransack/nodes/value.rb +1 -1
- data/lib/ransack/predicate.rb +4 -5
- data/lib/ransack/ransacker.rb +1 -1
- data/lib/ransack/search.rb +39 -13
- data/lib/ransack/translate.rb +7 -8
- data/lib/ransack/version.rb +1 -1
- data/ransack.gemspec +5 -5
- data/spec/ransack/adapters/active_record/base_spec.rb +78 -35
- data/spec/ransack/adapters/active_record/context_spec.rb +58 -15
- data/spec/ransack/configuration_spec.rb +18 -18
- data/spec/ransack/dependencies_spec.rb +1 -1
- data/spec/ransack/helpers/form_builder_spec.rb +29 -29
- data/spec/ransack/helpers/form_helper_spec.rb +14 -1
- data/spec/ransack/nodes/condition_spec.rb +21 -2
- data/spec/ransack/predicate_spec.rb +49 -9
- data/spec/ransack/search_spec.rb +178 -143
- data/spec/ransack/translate_spec.rb +1 -1
- data/spec/spec_helper.rb +1 -0
- data/spec/support/schema.rb +26 -21
- metadata +15 -11
@@ -73,6 +73,30 @@ module Ransack
|
|
73
73
|
@engine.connection_pool.columns_hash[table][name].type
|
74
74
|
end
|
75
75
|
|
76
|
+
def join_associations
|
77
|
+
@join_dependency.join_associations
|
78
|
+
end
|
79
|
+
|
80
|
+
# All dependent Arel::Join nodes used in the search query
|
81
|
+
#
|
82
|
+
# This could otherwise be done as `@object.arel.join_sources`, except
|
83
|
+
# that ActiveRecord's build_joins sets up its own JoinDependency.
|
84
|
+
# This extracts what we need to access the joins using our existing
|
85
|
+
# JoinDependency to track table aliases.
|
86
|
+
#
|
87
|
+
def join_sources
|
88
|
+
base = Arel::SelectManager.new(@object.engine, @object.table)
|
89
|
+
joins = @object.joins_values
|
90
|
+
joins.each do |assoc|
|
91
|
+
assoc.join_to(base)
|
92
|
+
end
|
93
|
+
base.join_sources
|
94
|
+
end
|
95
|
+
|
96
|
+
def alias_tracker
|
97
|
+
@join_dependency.alias_tracker
|
98
|
+
end
|
99
|
+
|
76
100
|
private
|
77
101
|
|
78
102
|
def get_parent_and_attribute_name(str, parent = @base)
|
@@ -12,6 +12,7 @@ module Ransack
|
|
12
12
|
end
|
13
13
|
|
14
14
|
def ransack(params = {}, options = {})
|
15
|
+
params = params.presence || {}
|
15
16
|
Search.new(self, params ? params.delete_if {
|
16
17
|
|k, v| v.blank? && v != false } : params, options)
|
17
18
|
end
|
@@ -35,6 +36,11 @@ module Ransack
|
|
35
36
|
reflect_on_all_associations.map { |a| a.name.to_s }
|
36
37
|
end
|
37
38
|
|
39
|
+
# For overriding with a whitelist of symbols
|
40
|
+
def ransackable_scopes(auth_object = nil)
|
41
|
+
[]
|
42
|
+
end
|
43
|
+
|
38
44
|
end
|
39
45
|
end
|
40
46
|
end
|
@@ -78,7 +78,55 @@ module Ransack
|
|
78
78
|
end
|
79
79
|
end
|
80
80
|
|
81
|
-
|
81
|
+
if ::ActiveRecord::VERSION::STRING >= '4.1'
|
82
|
+
|
83
|
+
def join_associations
|
84
|
+
raise NotImplementedError,
|
85
|
+
"ActiveRecord 4.1 and later does not use join_associations. Use join_sources."
|
86
|
+
end
|
87
|
+
|
88
|
+
# All dependent Arel::Join nodes used in the search query
|
89
|
+
#
|
90
|
+
# This could otherwise be done as `@object.arel.join_sources`, except
|
91
|
+
# that ActiveRecord's build_joins sets up its own JoinDependency.
|
92
|
+
# This extracts what we need to access the joins using our existing
|
93
|
+
# JoinDependency to track table aliases.
|
94
|
+
#
|
95
|
+
def join_sources
|
96
|
+
base = Arel::SelectManager.new(@object.engine, @object.table)
|
97
|
+
joins = @join_dependency.join_constraints(@object.joins_values)
|
98
|
+
joins.each do |aliased_join|
|
99
|
+
base.from(aliased_join)
|
100
|
+
end
|
101
|
+
base.join_sources
|
102
|
+
end
|
103
|
+
|
104
|
+
else
|
105
|
+
|
106
|
+
# All dependent JoinAssociation items used in the search query
|
107
|
+
#
|
108
|
+
# Deprecated: this goes away in ActiveRecord 4.1. Use join_sources.
|
109
|
+
#
|
110
|
+
def join_associations
|
111
|
+
@join_dependency.join_associations
|
112
|
+
end
|
113
|
+
|
114
|
+
def join_sources
|
115
|
+
base = Arel::SelectManager.new(@object.engine, @object.table)
|
116
|
+
joins = @object.joins_values
|
117
|
+
joins.each do |assoc|
|
118
|
+
assoc.join_to(base)
|
119
|
+
end
|
120
|
+
base.join_sources
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
124
|
+
|
125
|
+
def alias_tracker
|
126
|
+
@join_dependency.alias_tracker
|
127
|
+
end
|
128
|
+
|
129
|
+
private
|
82
130
|
|
83
131
|
def get_parent_and_attribute_name(str, parent = @base)
|
84
132
|
attr_name = nil
|
@@ -7,7 +7,8 @@ module Ransack
|
|
7
7
|
mattr_accessor :predicates, :options
|
8
8
|
self.predicates = {}
|
9
9
|
self.options = {
|
10
|
-
|
10
|
+
:search_key => :q,
|
11
|
+
:ignore_unknown_conditions => true
|
11
12
|
}
|
12
13
|
|
13
14
|
def configure
|
@@ -20,16 +21,18 @@ module Ransack
|
|
20
21
|
compounds = opts.delete(:compounds)
|
21
22
|
compounds = true if compounds.nil?
|
22
23
|
compounds = false if opts[:wants_array]
|
23
|
-
opts[:arel_predicate] = opts[:arel_predicate].to_s
|
24
24
|
|
25
25
|
self.predicates[name] = Predicate.new(opts)
|
26
26
|
|
27
27
|
['_any', '_all'].each do |suffix|
|
28
|
-
|
28
|
+
compound_name = name + suffix
|
29
|
+
self.predicates[compound_name] = Predicate.new(
|
29
30
|
opts.merge(
|
30
|
-
|
31
|
-
|
32
|
-
|
31
|
+
:name => compound_name,
|
32
|
+
:arel_predicate => arel_predicate_with_suffix(
|
33
|
+
opts[:arel_predicate], suffix
|
34
|
+
),
|
35
|
+
:compound => true
|
33
36
|
)
|
34
37
|
)
|
35
38
|
end if compounds
|
@@ -40,5 +43,19 @@ module Ransack
|
|
40
43
|
self.options[:search_key] = name
|
41
44
|
end
|
42
45
|
|
46
|
+
# raise an error if an unknown predicate, condition or attribute is passed
|
47
|
+
# into a search
|
48
|
+
def ignore_unknown_conditions=(boolean)
|
49
|
+
self.options[:ignore_unknown_conditions] = boolean
|
50
|
+
end
|
51
|
+
|
52
|
+
def arel_predicate_with_suffix(arel_predicate, suffix)
|
53
|
+
if arel_predicate === Proc
|
54
|
+
proc { |v| "#{arel_predicate.call(v)}#{suffix}" }
|
55
|
+
else
|
56
|
+
"#{arel_predicate}#{suffix}"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
43
60
|
end
|
44
61
|
end
|
data/lib/ransack/constants.rb
CHANGED
@@ -2,97 +2,98 @@ module Ransack
|
|
2
2
|
module Constants
|
3
3
|
TRUE_VALUES = [true, 1, '1', 't', 'T', 'true', 'TRUE'].to_set
|
4
4
|
FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE'].to_set
|
5
|
+
BOOLEAN_VALUES = TRUE_VALUES + FALSE_VALUES
|
5
6
|
|
6
7
|
AREL_PREDICATES = %w(eq not_eq matches does_not_match lt lteq gt gteq in not_in)
|
7
8
|
|
8
9
|
DERIVED_PREDICATES = [
|
9
10
|
['cont', {
|
10
|
-
arel_predicate
|
11
|
-
formatter
|
11
|
+
:arel_predicate => 'matches',
|
12
|
+
:formatter => proc { |v| "%#{escape_wildcards(v)}%" }
|
12
13
|
}
|
13
14
|
],
|
14
15
|
['not_cont', {
|
15
|
-
arel_predicate
|
16
|
-
formatter
|
16
|
+
:arel_predicate => 'does_not_match',
|
17
|
+
:formatter => proc { |v| "%#{escape_wildcards(v)}%" }
|
17
18
|
}
|
18
19
|
],
|
19
20
|
['start', {
|
20
|
-
arel_predicate
|
21
|
-
formatter
|
21
|
+
:arel_predicate => 'matches',
|
22
|
+
:formatter => proc { |v| "#{escape_wildcards(v)}%" }
|
22
23
|
}
|
23
24
|
],
|
24
25
|
['not_start', {
|
25
|
-
arel_predicate
|
26
|
-
formatter
|
26
|
+
:arel_predicate => 'does_not_match',
|
27
|
+
:formatter => proc { |v| "#{escape_wildcards(v)}%" }
|
27
28
|
}
|
28
29
|
],
|
29
30
|
['end', {
|
30
|
-
arel_predicate
|
31
|
-
formatter
|
31
|
+
:arel_predicate => 'matches',
|
32
|
+
:formatter => proc { |v| "%#{escape_wildcards(v)}" }
|
32
33
|
}
|
33
34
|
],
|
34
35
|
['not_end', {
|
35
|
-
arel_predicate
|
36
|
-
formatter
|
36
|
+
:arel_predicate => 'does_not_match',
|
37
|
+
:formatter => proc { |v| "%#{escape_wildcards(v)}" }
|
37
38
|
}
|
38
39
|
],
|
39
40
|
['true', {
|
40
|
-
arel_predicate
|
41
|
-
compounds
|
42
|
-
type
|
43
|
-
validator
|
41
|
+
:arel_predicate => 'eq',
|
42
|
+
:compounds => false,
|
43
|
+
:type => :boolean,
|
44
|
+
:validator => proc { |v| TRUE_VALUES.include?(v) }
|
44
45
|
}
|
45
46
|
],
|
46
47
|
['false', {
|
47
|
-
arel_predicate
|
48
|
-
compounds
|
49
|
-
type
|
50
|
-
validator
|
51
|
-
formatter
|
48
|
+
:arel_predicate => 'eq',
|
49
|
+
:compounds => false,
|
50
|
+
:type => :boolean,
|
51
|
+
:validator => proc { |v| TRUE_VALUES.include?(v) },
|
52
|
+
:formatter => proc { |v| !v }
|
52
53
|
}
|
53
54
|
],
|
54
55
|
['present', {
|
55
|
-
arel_predicate
|
56
|
-
compounds
|
57
|
-
type
|
58
|
-
validator
|
59
|
-
formatter
|
56
|
+
:arel_predicate => proc { |v| v ? 'not_eq_all' : 'eq_any' },
|
57
|
+
:compounds => false,
|
58
|
+
:type => :boolean,
|
59
|
+
:validator => proc { |v| BOOLEAN_VALUES.include?(v) },
|
60
|
+
:formatter => proc { |v| [nil, ''] }
|
60
61
|
}
|
61
62
|
],
|
62
63
|
['blank', {
|
63
|
-
arel_predicate
|
64
|
-
compounds
|
65
|
-
type
|
66
|
-
validator
|
67
|
-
formatter
|
64
|
+
:arel_predicate => proc { |v| v ? 'eq_any' : 'not_eq_all' },
|
65
|
+
:compounds => false,
|
66
|
+
:type => :boolean,
|
67
|
+
:validator => proc { |v| BOOLEAN_VALUES.include?(v) },
|
68
|
+
:formatter => proc { |v| [nil, ''] }
|
68
69
|
}
|
69
70
|
],
|
70
71
|
['null', {
|
71
|
-
arel_predicate
|
72
|
-
compounds
|
73
|
-
type
|
74
|
-
validator
|
75
|
-
formatter
|
72
|
+
:arel_predicate => proc { |v| v ? 'eq' : 'not_eq' },
|
73
|
+
:compounds => false,
|
74
|
+
:type => :boolean,
|
75
|
+
:validator => proc { |v| BOOLEAN_VALUES.include?(v)},
|
76
|
+
:formatter => proc { |v| nil }
|
76
77
|
}
|
77
78
|
],
|
78
79
|
['not_null', {
|
79
|
-
arel_predicate
|
80
|
-
compounds
|
81
|
-
type
|
82
|
-
validator
|
83
|
-
formatter
|
80
|
+
:arel_predicate => proc { |v| v ? 'not_eq' : 'eq' },
|
81
|
+
:compounds => false,
|
82
|
+
:type => :boolean,
|
83
|
+
:validator => proc { |v| BOOLEAN_VALUES.include?(v) },
|
84
|
+
:formatter => proc { |v| nil } }
|
84
85
|
]
|
85
86
|
]
|
86
87
|
|
87
|
-
|
88
|
+
module_function
|
88
89
|
# replace % \ to \% \\
|
89
90
|
def escape_wildcards(unescaped)
|
90
91
|
case ActiveRecord::Base.connection.adapter_name
|
91
|
-
when "
|
92
|
-
unescaped
|
93
|
-
else
|
92
|
+
when "Mysql2", "PostgreSQL"
|
94
93
|
# Necessary for PostgreSQL and MySQL
|
95
94
|
unescaped.to_s.gsub(/([\\|\%|.])/, '\\\\\\1')
|
95
|
+
else
|
96
|
+
unescaped
|
96
97
|
end
|
97
98
|
end
|
98
99
|
end
|
data/lib/ransack/context.rb
CHANGED
@@ -2,7 +2,7 @@ require 'ransack/visitor'
|
|
2
2
|
|
3
3
|
module Ransack
|
4
4
|
class Context
|
5
|
-
attr_reader :
|
5
|
+
attr_reader :object, :klass, :base, :engine, :arel_visitor
|
6
6
|
attr_accessor :auth_object, :search_key
|
7
7
|
|
8
8
|
class << self
|
@@ -46,7 +46,7 @@ module Ransack
|
|
46
46
|
end
|
47
47
|
|
48
48
|
@default_table = Arel::Table.new(
|
49
|
-
@base.table_name, as
|
49
|
+
@base.table_name, :as => @base.aliased_table_name, :engine => @engine
|
50
50
|
)
|
51
51
|
@bind_pairs = Hash.new do |hash, key|
|
52
52
|
parent, attr_name = get_parent_and_attribute_name(key.to_s)
|
@@ -77,6 +77,19 @@ module Ransack
|
|
77
77
|
table_for(parent)[attr_name]
|
78
78
|
end
|
79
79
|
|
80
|
+
def chain_scope(scope, args)
|
81
|
+
return unless @klass.method(scope) && args != false
|
82
|
+
@object = if scope_arity(scope) < 1 && args == true
|
83
|
+
@object.public_send(scope)
|
84
|
+
else
|
85
|
+
@object.public_send(scope, *args)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def scope_arity(scope)
|
90
|
+
@klass.method(scope).arity
|
91
|
+
end
|
92
|
+
|
80
93
|
def bind(object, str)
|
81
94
|
object.parent, object.attr_name = @bind_pairs[str]
|
82
95
|
end
|
@@ -143,6 +156,10 @@ module Ransack
|
|
143
156
|
klass.ransackable_associations(auth_object).include? str
|
144
157
|
end
|
145
158
|
|
159
|
+
def ransackable_scope?(str, klass)
|
160
|
+
klass.ransackable_scopes(auth_object).any? { |s| s.to_s == str }
|
161
|
+
end
|
162
|
+
|
146
163
|
def searchable_attributes(str = '')
|
147
164
|
traverse(str).ransackable_attributes(auth_object)
|
148
165
|
end
|
@@ -13,7 +13,7 @@ module Ransack
|
|
13
13
|
text = args.first
|
14
14
|
i18n = options[:i18n] || {}
|
15
15
|
text ||= object.translate(
|
16
|
-
method, i18n.reverse_merge(include_associations
|
16
|
+
method, i18n.reverse_merge(:include_associations => true)
|
17
17
|
) if object.respond_to? :translate
|
18
18
|
super(method, text, options, &block)
|
19
19
|
end
|
@@ -127,7 +127,8 @@ module Ransack
|
|
127
127
|
end
|
128
128
|
|
129
129
|
def combinator_select(options = {}, html_options = {})
|
130
|
-
template_collection_select(
|
130
|
+
template_collection_select(
|
131
|
+
:m, combinator_choices, options, html_options)
|
131
132
|
end
|
132
133
|
|
133
134
|
private
|
@@ -202,7 +203,7 @@ module Ransack
|
|
202
203
|
|
203
204
|
def get_attribute_element(action, base)
|
204
205
|
begin
|
205
|
-
[Translate.association(base, context
|
206
|
+
[Translate.association(base, :context => object.context),
|
206
207
|
collection_for_base(action, base)]
|
207
208
|
rescue UntraversableAssociationError => e
|
208
209
|
nil
|
@@ -214,7 +215,7 @@ module Ransack
|
|
214
215
|
[attr_from_base_and_column(base, c),
|
215
216
|
Translate.attribute(
|
216
217
|
attr_from_base_and_column(base, c),
|
217
|
-
context
|
218
|
+
:context => object.context
|
218
219
|
)
|
219
220
|
]
|
220
221
|
end
|
@@ -2,6 +2,26 @@ module Ransack
|
|
2
2
|
module Helpers
|
3
3
|
module FormHelper
|
4
4
|
|
5
|
+
def asc
|
6
|
+
'asc'.freeze
|
7
|
+
end
|
8
|
+
|
9
|
+
def desc
|
10
|
+
'desc'.freeze
|
11
|
+
end
|
12
|
+
|
13
|
+
def asc_arrow
|
14
|
+
'▲'.freeze
|
15
|
+
end
|
16
|
+
|
17
|
+
def desc_arrow
|
18
|
+
'▼'.freeze
|
19
|
+
end
|
20
|
+
|
21
|
+
def non_breaking_space
|
22
|
+
' '.freeze
|
23
|
+
end
|
24
|
+
|
5
25
|
def search_form_for(record, options = {}, &proc)
|
6
26
|
if record.is_a?(Ransack::Search)
|
7
27
|
search = record
|
@@ -20,15 +40,15 @@ module Ransack
|
|
20
40
|
end
|
21
41
|
options[:html] ||= {}
|
22
42
|
html_options = {
|
23
|
-
class
|
43
|
+
:class => options[:class].present? ?
|
24
44
|
"#{options[:class]}" :
|
25
45
|
"#{search.klass.to_s.underscore}_search",
|
26
|
-
id
|
46
|
+
:id => options[:id].present? ?
|
27
47
|
"#{options[:id]}" :
|
28
48
|
"#{search.klass.to_s.underscore}_search",
|
29
|
-
method
|
49
|
+
:method => :get
|
30
50
|
}
|
31
|
-
options[:as] ||= 'q'
|
51
|
+
options[:as] ||= 'q'.freeze
|
32
52
|
options[:html].reverse_merge!(html_options)
|
33
53
|
options[:builder] ||= FormBuilder
|
34
54
|
|
@@ -45,7 +65,7 @@ module Ransack
|
|
45
65
|
raise TypeError, "First argument must be a Ransack::Search!" unless
|
46
66
|
Search === search
|
47
67
|
|
48
|
-
search_params = params[search.context.search_key] ||
|
68
|
+
search_params = params[search.context.search_key].presence ||
|
49
69
|
{}.with_indifferent_access
|
50
70
|
|
51
71
|
attr_name = attribute.to_s
|
@@ -54,7 +74,7 @@ module Ransack
|
|
54
74
|
if args.size > 0 && !args.first.is_a?(Hash)
|
55
75
|
args.shift.to_s
|
56
76
|
else
|
57
|
-
Translate.attribute(attr_name, context
|
77
|
+
Translate.attribute(attr_name, :context => search.context)
|
58
78
|
end
|
59
79
|
)
|
60
80
|
|
@@ -67,9 +87,9 @@ module Ransack
|
|
67
87
|
current_dir = prev_attr == attr_name ? prev_dir : nil
|
68
88
|
|
69
89
|
if current_dir
|
70
|
-
new_dir = current_dir ==
|
90
|
+
new_dir = current_dir == desc ? asc : desc
|
71
91
|
else
|
72
|
-
new_dir = default_order ||
|
92
|
+
new_dir = default_order || asc
|
73
93
|
end
|
74
94
|
|
75
95
|
html_options = args.first.is_a?(Hash) ? args.shift.dup : {}
|
@@ -77,7 +97,7 @@ module Ransack
|
|
77
97
|
html_options[:class] = [css, html_options[:class]].compact.join(' ')
|
78
98
|
query_hash = {}
|
79
99
|
query_hash[search.context.search_key] = search_params
|
80
|
-
|
100
|
+
.merge(:s => "#{attr_name} #{new_dir}")
|
81
101
|
options.merge!(query_hash)
|
82
102
|
options_for_url = params.merge options
|
83
103
|
|
@@ -90,7 +110,7 @@ module Ransack
|
|
90
110
|
link_to(
|
91
111
|
[ERB::Util.h(name), order_indicator_for(current_dir)]
|
92
112
|
.compact
|
93
|
-
.join(
|
113
|
+
.join(non_breaking_space)
|
94
114
|
.html_safe,
|
95
115
|
url,
|
96
116
|
html_options
|
@@ -100,10 +120,10 @@ module Ransack
|
|
100
120
|
private
|
101
121
|
|
102
122
|
def order_indicator_for(order)
|
103
|
-
if order ==
|
104
|
-
|
105
|
-
elsif order ==
|
106
|
-
|
123
|
+
if order == asc
|
124
|
+
asc_arrow
|
125
|
+
elsif order == desc
|
126
|
+
desc_arrow
|
107
127
|
else
|
108
128
|
nil
|
109
129
|
end
|