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