ransack 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. data/lib/ransack.rb +1 -1
  2. data/lib/ransack/adapters/active_record.rb +18 -2
  3. data/lib/ransack/adapters/active_record/3.0/base.rb +34 -0
  4. data/lib/ransack/adapters/active_record/3.0/compat.rb +23 -0
  5. data/lib/ransack/adapters/active_record/3.0/context.rb +168 -0
  6. data/lib/ransack/adapters/active_record/3.0/join_association.rb +44 -0
  7. data/lib/ransack/adapters/active_record/3.0/join_dependency.rb +63 -0
  8. data/lib/ransack/adapters/active_record/base.rb +19 -2
  9. data/lib/ransack/adapters/active_record/context.rb +45 -33
  10. data/lib/ransack/adapters/active_record/join_association.rb +44 -0
  11. data/lib/ransack/adapters/active_record/join_dependency.rb +63 -0
  12. data/lib/ransack/context.rb +25 -65
  13. data/lib/ransack/helpers/form_builder.rb +10 -4
  14. data/lib/ransack/helpers/form_helper.rb +1 -0
  15. data/lib/ransack/locale/en.yml +1 -0
  16. data/lib/ransack/nodes.rb +1 -0
  17. data/lib/ransack/nodes/attribute.rb +21 -4
  18. data/lib/ransack/nodes/bindable.rb +29 -0
  19. data/lib/ransack/nodes/condition.rb +30 -28
  20. data/lib/ransack/nodes/sort.rb +5 -3
  21. data/lib/ransack/nodes/value.rb +84 -100
  22. data/lib/ransack/predicate.rb +1 -11
  23. data/lib/ransack/ransacker.rb +26 -0
  24. data/lib/ransack/search.rb +3 -2
  25. data/lib/ransack/version.rb +1 -1
  26. data/lib/ransack/visitor.rb +64 -0
  27. data/ransack.gemspec +3 -3
  28. data/spec/console.rb +1 -2
  29. data/spec/ransack/adapters/active_record/base_spec.rb +18 -0
  30. data/spec/ransack/adapters/active_record/context_spec.rb +2 -2
  31. data/spec/ransack/helpers/form_builder_spec.rb +4 -0
  32. data/spec/ransack/search_spec.rb +25 -2
  33. data/spec/spec_helper.rb +2 -3
  34. data/spec/support/schema.rb +8 -0
  35. metadata +16 -8
@@ -0,0 +1,44 @@
1
+ require 'active_record'
2
+
3
+ module Ransack
4
+ module Adapters
5
+ module ActiveRecord
6
+ class JoinAssociation < ::ActiveRecord::Associations::JoinDependency::JoinAssociation
7
+
8
+ def initialize(reflection, join_dependency, parent = nil, polymorphic_class = nil)
9
+ if polymorphic_class && ::ActiveRecord::Base > polymorphic_class
10
+ swapping_reflection_klass(reflection, polymorphic_class) do |reflection|
11
+ super(reflection, join_dependency, parent)
12
+ end
13
+ else
14
+ super(reflection, join_dependency, parent)
15
+ end
16
+ end
17
+
18
+ def swapping_reflection_klass(reflection, klass)
19
+ reflection = reflection.clone
20
+ original_polymorphic = reflection.options.delete(:polymorphic)
21
+ reflection.instance_variable_set(:@klass, klass)
22
+ yield reflection
23
+ ensure
24
+ reflection.options[:polymorphic] = original_polymorphic
25
+ end
26
+
27
+ def ==(other)
28
+ super && active_record == other.active_record
29
+ end
30
+
31
+ def build_constraint(reflection, table, key, foreign_table, foreign_key)
32
+ if reflection.options[:polymorphic]
33
+ super.and(
34
+ foreign_table[reflection.foreign_type].eq(reflection.klass.name)
35
+ )
36
+ else
37
+ super
38
+ end
39
+ end
40
+
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,63 @@
1
+ require 'active_record'
2
+
3
+ module Ransack
4
+ module Adapters
5
+ module ActiveRecord
6
+ module JoinDependency
7
+
8
+ # Yes, I'm using alias_method_chain here. No, I don't feel too
9
+ # bad about it. JoinDependency, or, to call it by its full proper
10
+ # name, ::ActiveRecord::Associations::JoinDependency, is one of the
11
+ # most "for internal use only" chunks of ActiveRecord.
12
+ def self.included(base)
13
+ base.class_eval do
14
+ alias_method_chain :graft, :ransack
15
+ end
16
+ end
17
+
18
+ def graft_with_ransack(*associations)
19
+ associations.each do |association|
20
+ join_associations.detect {|a| association == a} ||
21
+ build_polymorphic(association.reflection.name, association.find_parent_in(self) || join_base, association.join_type, association.reflection.klass)
22
+ end
23
+ self
24
+ end
25
+
26
+ # Should only be called by Ransack, and only with a single association name
27
+ def build_polymorphic(association, parent = nil, join_type = Arel::OuterJoin, klass = nil)
28
+ parent ||= join_parts.last
29
+ reflection = parent.reflections[association] or
30
+ raise ::ActiveRecord::ConfigurationError, "Association named '#{ association }' was not found; perhaps you misspelled it?"
31
+ unless join_association = find_join_association_respecting_polymorphism(reflection, parent, klass)
32
+ @reflections << reflection
33
+ join_association = build_join_association_respecting_polymorphism(reflection, parent, klass)
34
+ join_association.join_type = join_type
35
+ @join_parts << join_association
36
+ cache_joined_association(join_association)
37
+ end
38
+
39
+ join_association
40
+ end
41
+
42
+ def find_join_association_respecting_polymorphism(reflection, parent, klass)
43
+ if association = find_join_association(reflection, parent)
44
+ unless reflection.options[:polymorphic]
45
+ association
46
+ else
47
+ association if association.active_record == klass
48
+ end
49
+ end
50
+ end
51
+
52
+ def build_join_association_respecting_polymorphism(reflection, parent, klass = nil)
53
+ if reflection.options[:polymorphic] && klass
54
+ JoinAssociation.new(reflection, self, parent, klass)
55
+ else
56
+ JoinAssociation.new(reflection, self, parent)
57
+ end
58
+ end
59
+
60
+ end
61
+ end
62
+ end
63
+ end
@@ -1,6 +1,9 @@
1
+ require 'ransack/visitor'
2
+
1
3
  module Ransack
2
4
  class Context
3
5
  attr_reader :search, :object, :klass, :base, :engine, :arel_visitor
6
+ attr_accessor :auth_object
4
7
 
5
8
  class << self
6
9
 
@@ -36,9 +39,10 @@ module Ransack
36
39
  @engine = @base.arel_engine
37
40
  @arel_visitor = Arel::Visitors.visitor_for @engine
38
41
  @default_table = Arel::Table.new(@base.table_name, :as => @base.aliased_table_name, :engine => @engine)
39
- @attributes = Hash.new do |hash, key|
40
- if attribute = get_attribute(key.to_s)
41
- hash[key] = attribute
42
+ @bind_pairs = Hash.new do |hash, key|
43
+ parent, attr_name = get_parent_and_attribute_name(key.to_s)
44
+ if parent && attr_name
45
+ hash[key] = [parent, attr_name]
42
46
  end
43
47
  end
44
48
  end
@@ -46,7 +50,12 @@ module Ransack
46
50
  # Convert a string representing a chain of associations and an attribute
47
51
  # into the attribute itself
48
52
  def contextualize(str)
49
- @attributes[str]
53
+ parent, attr_name = @bind_pairs[str]
54
+ table_for(parent)[attr_name]
55
+ end
56
+
57
+ def bind(object, str)
58
+ object.parent, object.attr_name = @bind_pairs[str]
50
59
  end
51
60
 
52
61
  def traverse(str, base = @base)
@@ -56,8 +65,9 @@ module Ransack
56
65
  association_parts = []
57
66
  found_assoc = nil
58
67
  while !found_assoc && segments.size > 0 && association_parts << segments.shift do
59
- if found_assoc = get_association(association_parts.join('_'), base)
60
- base = traverse(segments.join('_'), found_assoc.klass)
68
+ assoc, klass = unpolymorphize_association(association_parts.join('_'))
69
+ if found_assoc = get_association(assoc, base)
70
+ base = traverse(segments.join('_'), klass || found_assoc.klass)
61
71
  end
62
72
  end
63
73
  raise ArgumentError, "No association matches #{str}" unless found_assoc
@@ -74,10 +84,11 @@ module Ransack
74
84
  association_parts = []
75
85
  if (segments = str.split(/_/)).size > 0
76
86
  while segments.size > 0 && !base.columns_hash[segments.join('_')] && association_parts << segments.shift do
77
- if found_assoc = get_association(association_parts.join('_'), base)
87
+ assoc, klass = unpolymorphize_association(association_parts.join('_'))
88
+ if found_assoc = get_association(assoc, base)
78
89
  path += association_parts
79
90
  association_parts = []
80
- base = klassify(found_assoc)
91
+ base = klassify(klass || found_assoc)
81
92
  end
82
93
  end
83
94
  end
@@ -85,67 +96,16 @@ module Ransack
85
96
  path.join('_')
86
97
  end
87
98
 
88
- def searchable_columns(str = '')
89
- traverse(str).column_names
90
- end
91
-
92
- def accept(object)
93
- visit(object)
94
- end
95
-
96
- def can_accept?(object)
97
- respond_to? DISPATCH[object.class]
98
- end
99
-
100
- def visit_Array(object)
101
- object.map {|o| accept(o)}.compact
102
- end
103
-
104
- def visit_Ransack_Nodes_Condition(object)
105
- object.apply_predicate if object.valid?
106
- end
107
-
108
- def visit_Ransack_Nodes_And(object)
109
- nodes = object.values.map {|o| accept(o)}.compact
110
- return nil unless nodes.size > 0
111
-
112
- if nodes.size > 1
113
- Arel::Nodes::Grouping.new(Arel::Nodes::And.new(nodes))
99
+ def unpolymorphize_association(str)
100
+ if (match = str.match(/_of_(.+?)_type$/)) && Kernel.const_defined?(match.captures.first)
101
+ [match.pre_match, Kernel.const_get(match.captures.first)]
114
102
  else
115
- nodes.first
103
+ [str, nil]
116
104
  end
117
105
  end
118
106
 
119
- def visit_Ransack_Nodes_Sort(object)
120
- object.attr.send(object.dir) if object.valid?
121
- end
122
-
123
- def visit_Ransack_Nodes_Or(object)
124
- nodes = object.values.map {|o| accept(o)}.compact
125
- return nil unless nodes.size > 0
126
-
127
- if nodes.size > 1
128
- nodes.inject(&:or)
129
- else
130
- nodes.first
131
- end
132
- end
133
-
134
- def quoted?(object)
135
- case object
136
- when Arel::Nodes::SqlLiteral, Bignum, Fixnum
137
- false
138
- else
139
- true
140
- end
141
- end
142
-
143
- def visit(object)
144
- send(DISPATCH[object.class], object)
145
- end
146
-
147
- DISPATCH = Hash.new do |hash, klass|
148
- hash[klass] = "visit_#{klass.name.gsub('::', '_')}"
107
+ def searchable_attributes(str = '')
108
+ traverse(str).ransackable_attributes(auth_object)
149
109
  end
150
110
 
151
111
  end
@@ -11,6 +11,12 @@ module Ransack
11
11
  super(method, text, options, &block)
12
12
  end
13
13
 
14
+ def submit(value=nil, options={})
15
+ value, options = nil, value if value.is_a?(Hash)
16
+ value ||= Translate.word(:search).titleize
17
+ super(value, options)
18
+ end
19
+
14
20
  def attribute_select(options = {}, html_options = {})
15
21
  raise ArgumentError, "attribute_select must be called inside a search FormBuilder!" unless object.respond_to?(:context)
16
22
  options[:include_blank] = true unless options.has_key?(:include_blank)
@@ -19,7 +25,7 @@ module Ransack
19
25
  collection = bases.map do |base|
20
26
  [
21
27
  Translate.association(base, :context => object.context),
22
- object.context.searchable_columns(base).map do |c|
28
+ object.context.searchable_attributes(base).map do |c|
23
29
  [
24
30
  attr_from_base_and_column(base, c),
25
31
  Translate.attribute(attr_from_base_and_column(base, c), :context => object.context)
@@ -32,7 +38,7 @@ module Ransack
32
38
  objectify_options(options), @default_options.merge(html_options)
33
39
  )
34
40
  else
35
- collection = object.context.searchable_columns(bases.first).map do |c|
41
+ collection = object.context.searchable_attributes(bases.first).map do |c|
36
42
  [
37
43
  attr_from_base_and_column(bases.first, c),
38
44
  Translate.attribute(attr_from_base_and_column(bases.first, c), :context => object.context)
@@ -53,7 +59,7 @@ module Ransack
53
59
  collection = bases.map do |base|
54
60
  [
55
61
  Translate.association(base, :context => object.context),
56
- object.context.searchable_columns(base).map do |c|
62
+ object.context.searchable_attributes(base).map do |c|
57
63
  [
58
64
  attr_from_base_and_column(base, c),
59
65
  Translate.attribute(attr_from_base_and_column(base, c), :context => object.context)
@@ -69,7 +75,7 @@ module Ransack
69
75
  objectify_options(options), @default_options.merge(html_options)
70
76
  )
71
77
  else
72
- collection = object.context.searchable_columns(bases.first).map do |c|
78
+ collection = object.context.searchable_attributes(bases.first).map do |c|
73
79
  [
74
80
  attr_from_base_and_column(bases.first, c),
75
81
  Translate.attribute(attr_from_base_and_column(bases.first, c), :context => object.context)
@@ -16,6 +16,7 @@ module Ransack
16
16
  :id => options[:as] ? "#{options[:as]}_search" : "#{search.klass.to_s.underscore}_search",
17
17
  :method => :get
18
18
  }
19
+ options[:as] ||= 'q'
19
20
  options[:html].reverse_merge!(html_options)
20
21
  options[:builder] ||= FormBuilder
21
22
 
@@ -1,5 +1,6 @@
1
1
  en:
2
2
  ransack:
3
+ search: "search"
3
4
  predicate: "predicate"
4
5
  and: "and"
5
6
  or: "or"
@@ -1,3 +1,4 @@
1
+ require 'ransack/nodes/bindable'
1
2
  require 'ransack/nodes/node'
2
3
  require 'ransack/nodes/attribute'
3
4
  require 'ransack/nodes/value'
@@ -1,8 +1,12 @@
1
1
  module Ransack
2
2
  module Nodes
3
3
  class Attribute < Node
4
- attr_reader :name, :attr
5
- delegate :blank?, :==, :to => :name
4
+ include Bindable
5
+
6
+ attr_reader :name
7
+
8
+ delegate :blank?, :present?, :==, :to => :name
9
+ delegate :engine, :to => :context
6
10
 
7
11
  def initialize(context, name = nil)
8
12
  super(context)
@@ -11,11 +15,23 @@ module Ransack
11
15
 
12
16
  def name=(name)
13
17
  @name = name
14
- @attr = contextualize(name) unless name.blank?
18
+ context.bind(self, name) unless name.blank?
15
19
  end
16
20
 
17
21
  def valid?
18
- @attr
22
+ bound? && attr
23
+ end
24
+
25
+ def type
26
+ if ransacker
27
+ return ransacker.type
28
+ else
29
+ context.type_for(attr)
30
+ end
31
+ end
32
+
33
+ def cast_value(value)
34
+ value.cast_to_type(type)
19
35
  end
20
36
 
21
37
  def eql?(other)
@@ -31,6 +47,7 @@ module Ransack
31
47
  def persisted?
32
48
  false
33
49
  end
50
+
34
51
  end
35
52
  end
36
53
  end
@@ -0,0 +1,29 @@
1
+ module Ransack
2
+ module Nodes
3
+ module Bindable
4
+
5
+ attr_accessor :parent, :attr_name
6
+
7
+ def attr
8
+ @attr ||= ransacker ? ransacker.attr_from(self) : context.table_for(parent)[attr_name]
9
+ end
10
+
11
+ def ransacker
12
+ klass._ransackers[attr_name]
13
+ end
14
+
15
+ def klass
16
+ @klass ||= context.klassify(parent)
17
+ end
18
+
19
+ def bound?
20
+ attr_name.present? && parent.present?
21
+ end
22
+
23
+ def reset_binding!
24
+ @parent = @attr_name = @attr = @klass = nil
25
+ end
26
+
27
+ end
28
+ end
29
+ end
@@ -4,6 +4,8 @@ module Ransack
4
4
  i18n_word :attribute, :predicate, :combinator, :value
5
5
  i18n_alias :a => :attribute, :p => :predicate, :m => :combinator, :v => :value
6
6
 
7
+ delegate :cast_value, :to => :first_attribute
8
+
7
9
  attr_reader :predicate
8
10
 
9
11
  class << self
@@ -42,6 +44,10 @@ module Ransack
42
44
  values.size <= 1 || predicate.compound || %w(in not_in).include?(predicate.name)
43
45
  end
44
46
 
47
+ def first_attribute
48
+ attributes.first
49
+ end
50
+
45
51
  def attributes
46
52
  @attributes ||= []
47
53
  end
@@ -74,12 +80,12 @@ module Ransack
74
80
  case args
75
81
  when Array
76
82
  args.each do |val|
77
- val = Value.new(@context, val, current_type)
83
+ val = Value.new(@context, val)
78
84
  self.values << val
79
85
  end
80
86
  when Hash
81
87
  args.each do |index, attrs|
82
- val = Value.new(@context, attrs[:value], current_type)
88
+ val = Value.new(@context, attrs[:value])
83
89
  self.values << val
84
90
  end
85
91
  else
@@ -105,13 +111,13 @@ module Ransack
105
111
  end
106
112
 
107
113
  def build_value(val = nil)
108
- Value.new(@context, val, current_type).tap do |value|
114
+ Value.new(@context, val).tap do |value|
109
115
  self.values << value
110
116
  end
111
117
  end
112
118
 
113
119
  def value
114
- predicate.compound ? values.map(&:value) : values.first.value
120
+ predicate.compound ? values.map {|v| cast_value(v)} : cast_value(values.first)
115
121
  end
116
122
 
117
123
  def build(params)
@@ -121,8 +127,6 @@ module Ransack
121
127
  end
122
128
  end
123
129
 
124
- set_value_types!
125
-
126
130
  self
127
131
  end
128
132
 
@@ -162,48 +166,46 @@ module Ransack
162
166
  end
163
167
  alias :p :predicate_name
164
168
 
165
- def apply_predicate
166
- attributes = arel_attributes.compact
169
+ def arel_predicate
170
+ predicates = attributes.map do |attr|
171
+ attr.attr.send(predicate.arel_predicate, formatted_values_for_attribute(attr))
172
+ end
167
173
 
168
- if attributes.size > 1
174
+ if predicates.size > 1
169
175
  case combinator
170
176
  when 'and'
171
- Arel::Nodes::Grouping.new(Arel::Nodes::And.new(
172
- attributes.map {|a| a.send(predicate.arel_predicate, predicate.format(values))}
173
- ))
177
+ Arel::Nodes::Grouping.new(Arel::Nodes::And.new(predicates))
174
178
  when 'or'
175
- attributes.inject(attributes.shift.send(predicate.arel_predicate, predicate.format(values))) do |memo, a|
176
- memo.or(a.send(predicate.arel_predicate, predicate.format(values)))
177
- end
179
+ predicates.inject(&:or)
178
180
  end
179
181
  else
180
- attributes.first.send(predicate.arel_predicate, predicate.format(values))
182
+ predicates.first
181
183
  end
182
184
  end
183
185
 
184
- private
186
+ def validated_values
187
+ values.select {|v| predicate.validator ? predicate.validator.call(v.value) : v.present?}
188
+ end
185
189
 
186
- def set_value_types!
187
- self.values.each {|v| v.type = current_type}
190
+ def casted_values_for_attribute(attr)
191
+ validated_values.map {|v| v.cast_to_type(predicate.type || attr.type)}
188
192
  end
189
193
 
190
- def current_type
191
- if predicate && predicate.type
192
- predicate.type
193
- elsif attributes.size > 0
194
- @context.type_for(attributes.first.attr)
194
+ def formatted_values_for_attribute(attr)
195
+ casted_values_for_attribute(attr).map do |val|
196
+ val = attr.ransacker.formatter.call(val) if attr.ransacker && attr.ransacker.formatter
197
+ val = predicate.formatter.call(val) if predicate.formatter
198
+ val
195
199
  end
196
200
  end
197
201
 
202
+ private
203
+
198
204
  def valid_combinator?
199
205
  attributes.size < 2 ||
200
206
  ['and', 'or'].include?(combinator)
201
207
  end
202
208
 
203
- def arel_attributes
204
- attributes.map(&:attr)
205
- end
206
-
207
209
  end
208
210
  end
209
211
  end