ransack 0.1.0 → 0.2.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 (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