ransack 1.5.1 → 1.6.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 +47 -3
- data/CHANGELOG.md +106 -18
- data/CONTRIBUTING.md +56 -23
- data/Gemfile +16 -5
- data/README.md +114 -38
- data/Rakefile +30 -2
- data/lib/ransack.rb +9 -0
- data/lib/ransack/adapters/active_record/3.0/compat.rb +11 -8
- data/lib/ransack/adapters/active_record/3.0/context.rb +14 -22
- data/lib/ransack/adapters/active_record/3.1/context.rb +14 -22
- data/lib/ransack/adapters/active_record/context.rb +36 -31
- data/lib/ransack/adapters/active_record/ransack/constants.rb +113 -0
- data/lib/ransack/adapters/active_record/ransack/context.rb +64 -0
- data/lib/ransack/adapters/active_record/ransack/nodes/condition.rb +48 -0
- data/lib/ransack/adapters/active_record/ransack/translate.rb +12 -0
- data/lib/ransack/adapters/active_record/ransack/visitor.rb +24 -0
- data/lib/ransack/adapters/mongoid.rb +13 -0
- data/lib/ransack/adapters/mongoid/3.2/.gitkeep +0 -0
- data/lib/ransack/adapters/mongoid/attributes/attribute.rb +37 -0
- data/lib/ransack/adapters/mongoid/attributes/order_predications.rb +17 -0
- data/lib/ransack/adapters/mongoid/attributes/predications.rb +141 -0
- data/lib/ransack/adapters/mongoid/base.rb +126 -0
- data/lib/ransack/adapters/mongoid/context.rb +208 -0
- data/lib/ransack/adapters/mongoid/inquiry_hash.rb +23 -0
- data/lib/ransack/adapters/mongoid/ransack/constants.rb +88 -0
- data/lib/ransack/adapters/mongoid/ransack/context.rb +60 -0
- data/lib/ransack/adapters/mongoid/ransack/nodes/condition.rb +27 -0
- data/lib/ransack/adapters/mongoid/ransack/translate.rb +13 -0
- data/lib/ransack/adapters/mongoid/ransack/visitor.rb +24 -0
- data/lib/ransack/adapters/mongoid/table.rb +35 -0
- data/lib/ransack/configuration.rb +22 -4
- data/lib/ransack/constants.rb +26 -120
- data/lib/ransack/context.rb +32 -60
- data/lib/ransack/helpers/form_builder.rb +50 -36
- data/lib/ransack/helpers/form_helper.rb +148 -104
- data/lib/ransack/naming.rb +11 -11
- data/lib/ransack/nodes.rb +2 -0
- data/lib/ransack/nodes/bindable.rb +12 -4
- data/lib/ransack/nodes/condition.rb +5 -22
- data/lib/ransack/nodes/grouping.rb +9 -10
- data/lib/ransack/nodes/sort.rb +3 -2
- data/lib/ransack/nodes/value.rb +1 -2
- data/lib/ransack/predicate.rb +3 -3
- data/lib/ransack/search.rb +46 -13
- data/lib/ransack/translate.rb +8 -8
- data/lib/ransack/version.rb +1 -1
- data/lib/ransack/visitor.rb +4 -16
- data/ransack.gemspec +1 -0
- data/spec/mongoid/adapters/mongoid/base_spec.rb +276 -0
- data/spec/mongoid/adapters/mongoid/context_spec.rb +56 -0
- data/spec/mongoid/configuration_spec.rb +66 -0
- data/spec/mongoid/dependencies_spec.rb +8 -0
- data/spec/mongoid/helpers/ransack_helper.rb +11 -0
- data/spec/mongoid/nodes/condition_spec.rb +34 -0
- data/spec/mongoid/nodes/grouping_spec.rb +13 -0
- data/spec/mongoid/predicate_spec.rb +155 -0
- data/spec/mongoid/search_spec.rb +446 -0
- data/spec/mongoid/support/mongoid.yml +6 -0
- data/spec/mongoid/support/schema.rb +128 -0
- data/spec/mongoid/translate_spec.rb +14 -0
- data/spec/mongoid_spec_helper.rb +59 -0
- data/spec/ransack/adapters/active_record/base_spec.rb +68 -35
- data/spec/ransack/dependencies_spec.rb +3 -1
- data/spec/ransack/helpers/form_builder_spec.rb +6 -6
- data/spec/ransack/helpers/form_helper_spec.rb +114 -47
- data/spec/ransack/nodes/condition_spec.rb +2 -2
- data/spec/ransack/search_spec.rb +2 -6
- data/spec/ransack/translate_spec.rb +1 -1
- data/spec/spec_helper.rb +2 -3
- data/spec/support/schema.rb +9 -0
- metadata +49 -4
@@ -0,0 +1,208 @@
|
|
1
|
+
require 'ransack/context'
|
2
|
+
require 'polyamorous'
|
3
|
+
|
4
|
+
module Ransack
|
5
|
+
module Adapters
|
6
|
+
module Mongoid
|
7
|
+
class Context < ::Ransack::Context
|
8
|
+
|
9
|
+
# Because the AR::Associations namespace is insane
|
10
|
+
# JoinDependency = ::Mongoid::Associations::JoinDependency
|
11
|
+
# JoinPart = JoinDependency::JoinPart
|
12
|
+
|
13
|
+
def initialize(object, options = {})
|
14
|
+
super
|
15
|
+
# @arel_visitor = @engine.connection.visitor
|
16
|
+
end
|
17
|
+
|
18
|
+
def relation_for(object)
|
19
|
+
object.all
|
20
|
+
end
|
21
|
+
|
22
|
+
def type_for(attr)
|
23
|
+
return nil unless attr && attr.valid?
|
24
|
+
name = attr.arel_attribute.name.to_s
|
25
|
+
# table = attr.arel_attribute.relation.table_name
|
26
|
+
|
27
|
+
# schema_cache = @engine.connection.schema_cache
|
28
|
+
# raise "No table named #{table} exists" unless schema_cache.table_exists?(table)
|
29
|
+
# schema_cache.columns_hash(table)[name].type
|
30
|
+
|
31
|
+
# when :date
|
32
|
+
# when :datetime, :timestamp, :time
|
33
|
+
# when :boolean
|
34
|
+
# when :integer
|
35
|
+
# when :float
|
36
|
+
# when :decimal
|
37
|
+
# else # :string
|
38
|
+
|
39
|
+
name = '_id' if name == 'id'
|
40
|
+
|
41
|
+
t = object.klass.fields[name].type
|
42
|
+
|
43
|
+
t.to_s.demodulize.underscore.to_sym
|
44
|
+
end
|
45
|
+
|
46
|
+
def evaluate(search, opts = {})
|
47
|
+
viz = Visitor.new
|
48
|
+
relation = @object.where(viz.accept(search.base))
|
49
|
+
if search.sorts.any?
|
50
|
+
ary_sorting = viz.accept(search.sorts)
|
51
|
+
sorting = {}
|
52
|
+
ary_sorting.each do |s|
|
53
|
+
sorting.merge! Hash[s.map { |k, d| [k.to_s == 'id' ? '_id' : k, d] }]
|
54
|
+
end
|
55
|
+
relation = relation.order_by(sorting)
|
56
|
+
# relation = relation.except(:order)
|
57
|
+
# .reorder(viz.accept(search.sorts))
|
58
|
+
end
|
59
|
+
# -- mongoid has different distinct method
|
60
|
+
# opts[:distinct] ? relation.distinct : relation
|
61
|
+
relation
|
62
|
+
end
|
63
|
+
|
64
|
+
def attribute_method?(str, klass = @klass)
|
65
|
+
exists = false
|
66
|
+
if ransackable_attribute?(str, klass)
|
67
|
+
exists = true
|
68
|
+
elsif (segments = str.split(/_/)).size > 1
|
69
|
+
remainder = []
|
70
|
+
found_assoc = nil
|
71
|
+
while !found_assoc && remainder.unshift(
|
72
|
+
segments.pop) && segments.size > 0 do
|
73
|
+
assoc, poly_class = unpolymorphize_association(
|
74
|
+
segments.join('_')
|
75
|
+
)
|
76
|
+
if found_assoc = get_association(assoc, klass)
|
77
|
+
exists = attribute_method?(remainder.join('_'),
|
78
|
+
poly_class || found_assoc.klass
|
79
|
+
)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
exists
|
84
|
+
end
|
85
|
+
|
86
|
+
def table_for(parent)
|
87
|
+
# parent.table
|
88
|
+
Ransack::Adapters::Mongoid::Table.new(parent)
|
89
|
+
end
|
90
|
+
|
91
|
+
def klassify(obj)
|
92
|
+
if Class === obj && obj.ancestors.include?(::Mongoid::Document)
|
93
|
+
obj
|
94
|
+
elsif obj.respond_to? :klass
|
95
|
+
obj.klass
|
96
|
+
elsif obj.respond_to? :base_klass
|
97
|
+
obj.base_klass
|
98
|
+
else
|
99
|
+
raise ArgumentError, "Don't know how to klassify #{obj}"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
private
|
104
|
+
|
105
|
+
def get_parent_and_attribute_name(str, parent = @base)
|
106
|
+
attr_name = nil
|
107
|
+
|
108
|
+
if ransackable_attribute?(str, klassify(parent))
|
109
|
+
attr_name = str
|
110
|
+
elsif (segments = str.split(/_/)).size > 1
|
111
|
+
remainder = []
|
112
|
+
found_assoc = nil
|
113
|
+
while remainder.unshift(
|
114
|
+
segments.pop) && segments.size > 0 && !found_assoc do
|
115
|
+
assoc, klass = unpolymorphize_association(segments.join('_'))
|
116
|
+
if found_assoc = get_association(assoc, parent)
|
117
|
+
join = build_or_find_association(found_assoc.name, parent, klass)
|
118
|
+
parent, attr_name = get_parent_and_attribute_name(
|
119
|
+
remainder.join('_'), join
|
120
|
+
)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
[parent, attr_name]
|
126
|
+
end
|
127
|
+
|
128
|
+
def get_association(str, parent = @base)
|
129
|
+
klass = klassify parent
|
130
|
+
ransackable_association?(str, klass) &&
|
131
|
+
klass.reflect_on_all_associations_all.detect { |a| a.name.to_s == str }
|
132
|
+
end
|
133
|
+
|
134
|
+
def join_dependency(relation)
|
135
|
+
if relation.respond_to?(:join_dependency) # Squeel will enable this
|
136
|
+
relation.join_dependency
|
137
|
+
else
|
138
|
+
build_join_dependency(relation)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# Checkout active_record/relation/query_methods.rb +build_joins+ for
|
143
|
+
# reference. Lots of duplicated code maybe we can avoid it
|
144
|
+
def build_join_dependency(relation)
|
145
|
+
buckets = relation.joins_values.group_by do |join|
|
146
|
+
case join
|
147
|
+
when String
|
148
|
+
Constants::STRING_JOIN
|
149
|
+
when Hash, Symbol, Array
|
150
|
+
Constants::ASSOCIATION_JOIN
|
151
|
+
when JoinDependency, JoinDependency::JoinAssociation
|
152
|
+
Constants::STASHED_JOIN
|
153
|
+
when Arel::Nodes::Join
|
154
|
+
Constants::JOIN_NODE
|
155
|
+
else
|
156
|
+
raise 'unknown class: %s' % join.class.name
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
association_joins = buckets[Constants::ASSOCIATION_JOIN] || []
|
161
|
+
|
162
|
+
stashed_association_joins = buckets[Constants::STASHED_JOIN] || []
|
163
|
+
|
164
|
+
join_nodes = buckets[Constants::JOIN_NODE] || []
|
165
|
+
|
166
|
+
string_joins = (buckets[Constants::STRING_JOIN] || [])
|
167
|
+
.map { |x| x.strip }
|
168
|
+
.uniq
|
169
|
+
|
170
|
+
join_list = relation.send :custom_join_ast,
|
171
|
+
relation.table.from(relation.table), string_joins
|
172
|
+
|
173
|
+
join_dependency = JoinDependency.new(
|
174
|
+
relation.klass, association_joins, join_list
|
175
|
+
)
|
176
|
+
|
177
|
+
join_nodes.each do |join|
|
178
|
+
join_dependency.alias_tracker.aliases[join.left.name.downcase] = 1
|
179
|
+
end
|
180
|
+
|
181
|
+
join_dependency # ActiveRecord::Associations::JoinDependency
|
182
|
+
end
|
183
|
+
|
184
|
+
# ActiveRecord method
|
185
|
+
def build_or_find_association(name, parent = @base, klass = nil)
|
186
|
+
found_association = @join_dependency.join_associations
|
187
|
+
.detect do |assoc|
|
188
|
+
assoc.reflection.name == name &&
|
189
|
+
assoc.parent == parent &&
|
190
|
+
(!klass || assoc.reflection.klass == klass)
|
191
|
+
end
|
192
|
+
unless found_association
|
193
|
+
@join_dependency.send(
|
194
|
+
:build,
|
195
|
+
Polyamorous::Join.new(name, @join_type, klass),
|
196
|
+
parent
|
197
|
+
)
|
198
|
+
found_association = @join_dependency.join_associations.last
|
199
|
+
# Leverage the stashed association functionality in AR
|
200
|
+
@object = @object.joins(found_association)
|
201
|
+
end
|
202
|
+
found_association
|
203
|
+
end
|
204
|
+
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Ransack
|
2
|
+
module Adapters
|
3
|
+
module Mongoid
|
4
|
+
class InquiryHash < Hash
|
5
|
+
|
6
|
+
def or(other)
|
7
|
+
{ '$or' => [ self, other] }.to_inquiry
|
8
|
+
end
|
9
|
+
|
10
|
+
def and(other)
|
11
|
+
{ '$and' => [ self, other] }.to_inquiry
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class Hash
|
20
|
+
def to_inquiry
|
21
|
+
::Ransack::Adapters::Mongoid::InquiryHash[self]
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module Ransack
|
2
|
+
module Constants
|
3
|
+
DERIVED_PREDICATES = [
|
4
|
+
[CONT, {
|
5
|
+
:arel_predicate => 'matches',
|
6
|
+
:formatter => proc { |v| "#{escape_regex(v)}" }
|
7
|
+
}
|
8
|
+
],
|
9
|
+
['not_cont', {
|
10
|
+
:arel_predicate => 'does_not_match',
|
11
|
+
:formatter => proc { |v| "#{escape_regex(v)}" }
|
12
|
+
}
|
13
|
+
],
|
14
|
+
['start', {
|
15
|
+
:arel_predicate => 'matches',
|
16
|
+
:formatter => proc { |v| "\\A#{escape_regex(v)}" }
|
17
|
+
}
|
18
|
+
],
|
19
|
+
['not_start', {
|
20
|
+
:arel_predicate => 'does_not_match',
|
21
|
+
:formatter => proc { |v| "\\A#{escape_regex(v)}" }
|
22
|
+
}
|
23
|
+
],
|
24
|
+
['end', {
|
25
|
+
:arel_predicate => 'matches',
|
26
|
+
:formatter => proc { |v| "#{escape_regex(v)}\\Z" }
|
27
|
+
}
|
28
|
+
],
|
29
|
+
['not_end', {
|
30
|
+
:arel_predicate => 'does_not_match',
|
31
|
+
:formatter => proc { |v| "#{escape_regex(v)}\\Z" }
|
32
|
+
}
|
33
|
+
],
|
34
|
+
['true', {
|
35
|
+
:arel_predicate => 'eq',
|
36
|
+
:compounds => false,
|
37
|
+
:type => :boolean,
|
38
|
+
:validator => proc { |v| TRUE_VALUES.include?(v) }
|
39
|
+
}
|
40
|
+
],
|
41
|
+
['false', {
|
42
|
+
:arel_predicate => 'eq',
|
43
|
+
:compounds => false,
|
44
|
+
:type => :boolean,
|
45
|
+
:validator => proc { |v| TRUE_VALUES.include?(v) },
|
46
|
+
:formatter => proc { |v| !v }
|
47
|
+
}
|
48
|
+
],
|
49
|
+
['present', {
|
50
|
+
:arel_predicate => proc { |v| v ? 'not_eq_all' : 'eq_any' },
|
51
|
+
:compounds => false,
|
52
|
+
:type => :boolean,
|
53
|
+
:validator => proc { |v| BOOLEAN_VALUES.include?(v) },
|
54
|
+
:formatter => proc { |v| [nil, ''] }
|
55
|
+
}
|
56
|
+
],
|
57
|
+
['blank', {
|
58
|
+
:arel_predicate => proc { |v| v ? 'eq_any' : 'not_eq_all' },
|
59
|
+
:compounds => false,
|
60
|
+
:type => :boolean,
|
61
|
+
:validator => proc { |v| BOOLEAN_VALUES.include?(v) },
|
62
|
+
:formatter => proc { |v| [nil, ''] }
|
63
|
+
}
|
64
|
+
],
|
65
|
+
['null', {
|
66
|
+
:arel_predicate => proc { |v| v ? 'eq' : 'not_eq' },
|
67
|
+
:compounds => false,
|
68
|
+
:type => :boolean,
|
69
|
+
:validator => proc { |v| BOOLEAN_VALUES.include?(v)},
|
70
|
+
:formatter => proc { |v| nil }
|
71
|
+
}
|
72
|
+
],
|
73
|
+
['not_null', {
|
74
|
+
:arel_predicate => proc { |v| v ? 'not_eq' : 'eq' },
|
75
|
+
:compounds => false,
|
76
|
+
:type => :boolean,
|
77
|
+
:validator => proc { |v| BOOLEAN_VALUES.include?(v) },
|
78
|
+
:formatter => proc { |v| nil } }
|
79
|
+
]
|
80
|
+
]
|
81
|
+
|
82
|
+
module_function
|
83
|
+
# does nothing
|
84
|
+
def escape_regex(unescaped)
|
85
|
+
Regexp.escape(unescaped)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'ransack/visitor'
|
2
|
+
|
3
|
+
module Ransack
|
4
|
+
class Context
|
5
|
+
# attr_reader :arel_visitor
|
6
|
+
|
7
|
+
class << self
|
8
|
+
|
9
|
+
def for_class(klass, options = {})
|
10
|
+
if klass.ancestors.include?(::Mongoid::Document)
|
11
|
+
Adapters::Mongoid::Context.new(klass, options)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def for_object(object, options = {})
|
16
|
+
case object
|
17
|
+
when ActiveRecord::Relation
|
18
|
+
Adapters::ActiveRecord::Context.new(object.klass, options)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
end # << self
|
23
|
+
|
24
|
+
def initialize(object, options = {})
|
25
|
+
@object = relation_for(object)
|
26
|
+
@klass = @object.klass
|
27
|
+
# @join_dependency = join_dependency(@object)
|
28
|
+
# @join_type = options[:join_type] || Arel::OuterJoin
|
29
|
+
@search_key = options[:search_key] || Ransack.options[:search_key]
|
30
|
+
|
31
|
+
@base = @object.klass
|
32
|
+
# @engine = @base.arel_engine
|
33
|
+
|
34
|
+
# @default_table = Arel::Table.new(
|
35
|
+
# @base.table_name, :as => @base.aliased_table_name, :engine => @engine
|
36
|
+
# )
|
37
|
+
@bind_pairs = Hash.new do |hash, key|
|
38
|
+
parent, attr_name = get_parent_and_attribute_name(key.to_s)
|
39
|
+
if parent && attr_name
|
40
|
+
hash[key] = [parent, attr_name]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def klassify(obj)
|
46
|
+
if Class === obj && ::ActiveRecord::Base > obj
|
47
|
+
obj
|
48
|
+
elsif obj.respond_to? :klass
|
49
|
+
obj.klass
|
50
|
+
elsif obj.respond_to? :active_record # Rails 3
|
51
|
+
obj.active_record
|
52
|
+
elsif obj.respond_to? :base_klass # Rails 4
|
53
|
+
obj.base_klass
|
54
|
+
else
|
55
|
+
raise ArgumentError, "Don't know how to klassify #{obj.inspect}"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Ransack
|
2
|
+
module Nodes
|
3
|
+
class Condition
|
4
|
+
|
5
|
+
def arel_predicate
|
6
|
+
predicates = attributes.map do |attr|
|
7
|
+
attr.attr.send(
|
8
|
+
arel_predicate_for_attribute(attr),
|
9
|
+
formatted_values_for_attribute(attr)
|
10
|
+
)
|
11
|
+
end
|
12
|
+
|
13
|
+
if predicates.size > 1
|
14
|
+
case combinator
|
15
|
+
when 'and'
|
16
|
+
Arel::Nodes::Grouping.new(Arel::Nodes::And.new(predicates))
|
17
|
+
when 'or'
|
18
|
+
predicates.inject(&:or)
|
19
|
+
end
|
20
|
+
else
|
21
|
+
predicates.first
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
end # Condition
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Ransack
|
2
|
+
module Translate
|
3
|
+
|
4
|
+
def self.i18n_key(klass)
|
5
|
+
# if ActiveRecord::VERSION::MAJOR == 3 && ActiveRecord::VERSION::MINOR == 0
|
6
|
+
# klass.model_name.i18n_key.to_s.tr('.', '/')
|
7
|
+
# else
|
8
|
+
# klass.model_name.i18n_key.to_s
|
9
|
+
# end
|
10
|
+
klass.model_name.i18n_key.to_s
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Ransack
|
2
|
+
class Visitor
|
3
|
+
def visit_and(object)
|
4
|
+
nodes = object.values.map { |o| accept(o) }.compact
|
5
|
+
return nil unless nodes.size > 0
|
6
|
+
|
7
|
+
if nodes.size > 1
|
8
|
+
nodes.inject(&:and)
|
9
|
+
else
|
10
|
+
nodes.first
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def quoted?(object)
|
15
|
+
case object
|
16
|
+
when Arel::Nodes::SqlLiteral, Bignum, Fixnum
|
17
|
+
false
|
18
|
+
else
|
19
|
+
true
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|