ransack 1.5.1 → 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +47 -3
  3. data/CHANGELOG.md +106 -18
  4. data/CONTRIBUTING.md +56 -23
  5. data/Gemfile +16 -5
  6. data/README.md +114 -38
  7. data/Rakefile +30 -2
  8. data/lib/ransack.rb +9 -0
  9. data/lib/ransack/adapters/active_record/3.0/compat.rb +11 -8
  10. data/lib/ransack/adapters/active_record/3.0/context.rb +14 -22
  11. data/lib/ransack/adapters/active_record/3.1/context.rb +14 -22
  12. data/lib/ransack/adapters/active_record/context.rb +36 -31
  13. data/lib/ransack/adapters/active_record/ransack/constants.rb +113 -0
  14. data/lib/ransack/adapters/active_record/ransack/context.rb +64 -0
  15. data/lib/ransack/adapters/active_record/ransack/nodes/condition.rb +48 -0
  16. data/lib/ransack/adapters/active_record/ransack/translate.rb +12 -0
  17. data/lib/ransack/adapters/active_record/ransack/visitor.rb +24 -0
  18. data/lib/ransack/adapters/mongoid.rb +13 -0
  19. data/lib/ransack/adapters/mongoid/3.2/.gitkeep +0 -0
  20. data/lib/ransack/adapters/mongoid/attributes/attribute.rb +37 -0
  21. data/lib/ransack/adapters/mongoid/attributes/order_predications.rb +17 -0
  22. data/lib/ransack/adapters/mongoid/attributes/predications.rb +141 -0
  23. data/lib/ransack/adapters/mongoid/base.rb +126 -0
  24. data/lib/ransack/adapters/mongoid/context.rb +208 -0
  25. data/lib/ransack/adapters/mongoid/inquiry_hash.rb +23 -0
  26. data/lib/ransack/adapters/mongoid/ransack/constants.rb +88 -0
  27. data/lib/ransack/adapters/mongoid/ransack/context.rb +60 -0
  28. data/lib/ransack/adapters/mongoid/ransack/nodes/condition.rb +27 -0
  29. data/lib/ransack/adapters/mongoid/ransack/translate.rb +13 -0
  30. data/lib/ransack/adapters/mongoid/ransack/visitor.rb +24 -0
  31. data/lib/ransack/adapters/mongoid/table.rb +35 -0
  32. data/lib/ransack/configuration.rb +22 -4
  33. data/lib/ransack/constants.rb +26 -120
  34. data/lib/ransack/context.rb +32 -60
  35. data/lib/ransack/helpers/form_builder.rb +50 -36
  36. data/lib/ransack/helpers/form_helper.rb +148 -104
  37. data/lib/ransack/naming.rb +11 -11
  38. data/lib/ransack/nodes.rb +2 -0
  39. data/lib/ransack/nodes/bindable.rb +12 -4
  40. data/lib/ransack/nodes/condition.rb +5 -22
  41. data/lib/ransack/nodes/grouping.rb +9 -10
  42. data/lib/ransack/nodes/sort.rb +3 -2
  43. data/lib/ransack/nodes/value.rb +1 -2
  44. data/lib/ransack/predicate.rb +3 -3
  45. data/lib/ransack/search.rb +46 -13
  46. data/lib/ransack/translate.rb +8 -8
  47. data/lib/ransack/version.rb +1 -1
  48. data/lib/ransack/visitor.rb +4 -16
  49. data/ransack.gemspec +1 -0
  50. data/spec/mongoid/adapters/mongoid/base_spec.rb +276 -0
  51. data/spec/mongoid/adapters/mongoid/context_spec.rb +56 -0
  52. data/spec/mongoid/configuration_spec.rb +66 -0
  53. data/spec/mongoid/dependencies_spec.rb +8 -0
  54. data/spec/mongoid/helpers/ransack_helper.rb +11 -0
  55. data/spec/mongoid/nodes/condition_spec.rb +34 -0
  56. data/spec/mongoid/nodes/grouping_spec.rb +13 -0
  57. data/spec/mongoid/predicate_spec.rb +155 -0
  58. data/spec/mongoid/search_spec.rb +446 -0
  59. data/spec/mongoid/support/mongoid.yml +6 -0
  60. data/spec/mongoid/support/schema.rb +128 -0
  61. data/spec/mongoid/translate_spec.rb +14 -0
  62. data/spec/mongoid_spec_helper.rb +59 -0
  63. data/spec/ransack/adapters/active_record/base_spec.rb +68 -35
  64. data/spec/ransack/dependencies_spec.rb +3 -1
  65. data/spec/ransack/helpers/form_builder_spec.rb +6 -6
  66. data/spec/ransack/helpers/form_helper_spec.rb +114 -47
  67. data/spec/ransack/nodes/condition_spec.rb +2 -2
  68. data/spec/ransack/search_spec.rb +2 -6
  69. data/spec/ransack/translate_spec.rb +1 -1
  70. data/spec/spec_helper.rb +2 -3
  71. data/spec/support/schema.rb +9 -0
  72. 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