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
@@ -16,9 +16,9 @@ end
16
16
 
17
17
  require 'ransack/translate'
18
18
  require 'ransack/search'
19
+ require 'ransack/ransacker'
19
20
  require 'ransack/adapters/active_record'
20
21
  require 'ransack/helpers'
21
22
  require 'action_controller'
22
23
 
23
- ActiveRecord::Base.extend Ransack::Adapters::ActiveRecord::Base
24
24
  ActionController::Base.helper Ransack::Helpers::FormHelper
@@ -1,2 +1,18 @@
1
- require 'ransack/adapters/active_record/base'
2
- require 'ransack/adapters/active_record/context'
1
+ case ActiveRecord::VERSION::STRING
2
+ when /^3\.0\./
3
+ require 'ransack/adapters/active_record/3.0/base'
4
+ require 'ransack/adapters/active_record/3.0/join_dependency'
5
+ require 'ransack/adapters/active_record/3.0/join_association'
6
+ require 'ransack/adapters/active_record/3.0/context'
7
+
8
+ ActiveRecord::Base.extend Ransack::Adapters::ActiveRecord::Base
9
+ ActiveRecord::Associations::ClassMethods::JoinDependency.send :include, Ransack::Adapters::ActiveRecord::JoinDependency
10
+ else
11
+ require 'ransack/adapters/active_record/base'
12
+ require 'ransack/adapters/active_record/join_dependency'
13
+ require 'ransack/adapters/active_record/join_association'
14
+ require 'ransack/adapters/active_record/context'
15
+
16
+ ActiveRecord::Base.extend Ransack::Adapters::ActiveRecord::Base
17
+ ActiveRecord::Associations::JoinDependency.send :include, Ransack::Adapters::ActiveRecord::JoinDependency
18
+ end
@@ -0,0 +1,34 @@
1
+ module Ransack
2
+ module Adapters
3
+ module ActiveRecord
4
+ module Base
5
+
6
+ def self.extended(base)
7
+ alias :search :ransack unless base.method_defined? :search
8
+ base.instance_eval do
9
+ class_attribute :_ransackers
10
+ self._ransackers ||= {}
11
+ end
12
+ end
13
+
14
+ def ransack(params = {}, options = {})
15
+ Search.new(self, params, options)
16
+ end
17
+
18
+ def ransacker(name, opts = {}, &block)
19
+ Ransacker.new(self, name, opts, &block)
20
+ end
21
+
22
+ # TODO: Let's actually do some authorization. Whitelist-only.
23
+ def ransackable_attributes(auth_object)
24
+ column_names + _ransackers.keys
25
+ end
26
+
27
+ def ransackable_associations(auth_object)
28
+ reflect_on_all_associations.map {|a| a.name.to_s}
29
+ end
30
+
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,23 @@
1
+ # UGLY, UGLY MONKEY PATCHES FOR BACKWARDS COMPAT!!! AVERT YOUR EYES!!
2
+ if Arel::Nodes::And < Arel::Nodes::Binary
3
+ class Ransack::Visitor
4
+ def visit_Ransack_Nodes_And(object)
5
+ nodes = object.values.map {|o| accept(o)}.compact
6
+ return nil unless nodes.size > 0
7
+
8
+ if nodes.size > 1
9
+ nodes.inject(&:and)
10
+ else
11
+ nodes.first
12
+ end
13
+ end
14
+ end
15
+ end
16
+
17
+ class ::ActiveRecord::Associations::ClassMethods::JoinDependency::JoinBase
18
+ def table
19
+ Arel::Table.new(table_name, :as => aliased_table_name,
20
+ :engine => active_record.arel_engine,
21
+ :columns => active_record.columns)
22
+ end
23
+ end
@@ -0,0 +1,168 @@
1
+ require 'ransack/context'
2
+ require 'active_record'
3
+ require 'ransack/adapters/active_record/3.0/compat'
4
+
5
+ module Ransack
6
+
7
+ module Adapters
8
+ module ActiveRecord
9
+ class Context < ::Ransack::Context
10
+ # Because the AR::Associations namespace is insane
11
+ JoinDependency = ::ActiveRecord::Associations::ClassMethods::JoinDependency
12
+ JoinBase = JoinDependency::JoinBase
13
+
14
+ def evaluate(search, opts = {})
15
+ viz = Visitor.new
16
+ relation = @object.where(viz.accept(search.base)).order(viz.accept(search.sorts))
17
+ opts[:distinct] ? relation.select("DISTINCT #{@klass.quoted_table_name}.*") : relation
18
+ end
19
+
20
+ def attribute_method?(str, klass = @klass)
21
+ exists = false
22
+
23
+ if ransackable_attribute?(str, klass)
24
+ exists = true
25
+ elsif (segments = str.split(/_/)).size > 1
26
+ remainder = []
27
+ found_assoc = nil
28
+ while !found_assoc && remainder.unshift(segments.pop) && segments.size > 0 do
29
+ assoc, poly_class = unpolymorphize_association(segments.join('_'))
30
+ if found_assoc = get_association(assoc, klass)
31
+ exists = attribute_method?(remainder.join('_'), poly_class || found_assoc.klass)
32
+ end
33
+ end
34
+ end
35
+
36
+ exists
37
+ end
38
+
39
+ def table_for(parent)
40
+ parent.table
41
+ end
42
+
43
+ def klassify(obj)
44
+ if Class === obj && ::ActiveRecord::Base > obj
45
+ obj
46
+ elsif obj.respond_to? :klass
47
+ obj.klass
48
+ elsif obj.respond_to? :active_record
49
+ obj.active_record
50
+ else
51
+ raise ArgumentError, "Don't know how to klassify #{obj}"
52
+ end
53
+ end
54
+
55
+ def type_for(attr)
56
+ return nil unless attr
57
+ name = attr.name.to_s
58
+ table = attr.relation.name
59
+
60
+ unless @engine.connection.table_exists?(table)
61
+ raise "No table named #{table} exists"
62
+ end
63
+
64
+ # TODO: optimize
65
+ @engine.connection.columns(table).detect {|c| c.name == name}.type
66
+ end
67
+
68
+ private
69
+
70
+ def get_parent_and_attribute_name(str, parent = @base)
71
+ attr_name = nil
72
+
73
+ if ransackable_attribute?(str, klassify(parent))
74
+ attr_name = str
75
+ elsif (segments = str.split(/_/)).size > 1
76
+ remainder = []
77
+ found_assoc = nil
78
+ while remainder.unshift(segments.pop) && segments.size > 0 && !found_assoc do
79
+ assoc, klass = unpolymorphize_association(segments.join('_'))
80
+ if ransackable_association?(assoc, klassify(parent))
81
+ found_assoc = get_association(assoc, parent)
82
+ join = build_or_find_association(found_assoc.name, parent, klass)
83
+ parent, attr_name = get_parent_and_attribute_name(remainder.join('_'), join)
84
+ end
85
+ end
86
+ end
87
+
88
+ [parent, attr_name]
89
+ end
90
+
91
+ def ransackable_attribute?(str, klass)
92
+ klass.ransackable_attributes(auth_object).include? str
93
+ end
94
+
95
+ def ransackable_association?(str, klass)
96
+ klass.ransackable_associations(auth_object).include? str
97
+ end
98
+
99
+ def get_association(str, parent = @base)
100
+ klassify(parent).reflect_on_all_associations.detect {|a| a.name.to_s == str}
101
+ end
102
+
103
+ def join_dependency(relation)
104
+ if relation.respond_to?(:join_dependency) # Squeel will enable this
105
+ relation.join_dependency
106
+ else
107
+ build_join_dependency(relation)
108
+ end
109
+ end
110
+
111
+ def build_join_dependency(relation)
112
+ buckets = relation.joins_values.group_by do |join|
113
+ case join
114
+ when String
115
+ 'string_join'
116
+ when Hash, Symbol, Array
117
+ 'association_join'
118
+ when ::ActiveRecord::Associations::ClassMethods::JoinDependency::JoinAssociation
119
+ 'stashed_join'
120
+ when Arel::Nodes::Join
121
+ 'join_node'
122
+ else
123
+ raise 'unknown class: %s' % join.class.name
124
+ end
125
+ end
126
+
127
+ association_joins = buckets['association_join'] || []
128
+ stashed_association_joins = buckets['stashed_join'] || []
129
+ join_nodes = buckets['join_node'] || []
130
+ string_joins = (buckets['string_join'] || []).map { |x|
131
+ x.strip
132
+ }.uniq
133
+
134
+ join_list = relation.send :custom_join_sql, (string_joins + join_nodes)
135
+
136
+ join_dependency = JoinDependency.new(
137
+ relation.klass,
138
+ association_joins,
139
+ join_list
140
+ )
141
+
142
+ join_nodes.each do |join|
143
+ join_dependency.table_aliases[join.left.name.downcase] = 1
144
+ end
145
+
146
+ join_dependency.graft(*stashed_association_joins)
147
+ end
148
+
149
+ def build_or_find_association(name, parent = @base, klass = nil)
150
+ found_association = @join_dependency.join_associations.detect do |assoc|
151
+ assoc.reflection.name == name &&
152
+ assoc.parent == parent &&
153
+ (!klass || assoc.klass == klass)
154
+ end
155
+ unless found_association
156
+ @join_dependency.send(:build_polymorphic, name.to_sym, parent, Arel::Nodes::OuterJoin, klass)
157
+ found_association = @join_dependency.join_associations.last
158
+ # Leverage the stashed association functionality in AR
159
+ @object = @object.joins(found_association)
160
+ end
161
+
162
+ found_association
163
+ end
164
+
165
+ end
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,44 @@
1
+ require 'active_record'
2
+
3
+ module Ransack
4
+ module Adapters
5
+ module ActiveRecord
6
+ class JoinAssociation < ::ActiveRecord::Associations::ClassMethods::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 ||= joins.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
+ @joins << 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
@@ -5,10 +5,27 @@ module Ransack
5
5
 
6
6
  def self.extended(base)
7
7
  alias :search :ransack unless base.method_defined? :search
8
+ base.instance_eval do
9
+ class_attribute :_ransackers
10
+ self._ransackers ||= {}
11
+ end
8
12
  end
9
13
 
10
- def ransack(params = {})
11
- Search.new(self, params)
14
+ def ransack(params = {}, options = {})
15
+ Search.new(self, params, options)
16
+ end
17
+
18
+ def ransacker(name, opts = {}, &block)
19
+ Ransacker.new(self, name, opts, &block)
20
+ end
21
+
22
+ # TODO: Let's actually do some authorization. Whitelist-only.
23
+ def ransackable_attributes(auth_object)
24
+ column_names + _ransackers.keys
25
+ end
26
+
27
+ def ransackable_associations(auth_object)
28
+ reflect_on_all_associations.map {|a| a.name.to_s}
12
29
  end
13
30
 
14
31
  end
@@ -8,24 +8,25 @@ module Ransack
8
8
  # Because the AR::Associations namespace is insane
9
9
  JoinDependency = ::ActiveRecord::Associations::JoinDependency
10
10
  JoinPart = JoinDependency::JoinPart
11
- JoinAssociation = JoinDependency::JoinAssociation
12
11
 
13
12
  def evaluate(search, opts = {})
14
- relation = @object.where(accept(search.base)).order(accept(search.sorts))
15
- opts[:distinct] ? relation.group(@klass.arel_table[@klass.primary_key]) : relation
13
+ viz = Visitor.new
14
+ relation = @object.where(viz.accept(search.base)).order(viz.accept(search.sorts))
15
+ opts[:distinct] ? relation.select("DISTINCT #{@klass.quoted_table_name}.*") : relation
16
16
  end
17
17
 
18
18
  def attribute_method?(str, klass = @klass)
19
19
  exists = false
20
20
 
21
- if column = get_column(str, klass)
21
+ if ransackable_attribute?(str, klass)
22
22
  exists = true
23
23
  elsif (segments = str.split(/_/)).size > 1
24
24
  remainder = []
25
25
  found_assoc = nil
26
26
  while !found_assoc && remainder.unshift(segments.pop) && segments.size > 0 do
27
- if found_assoc = get_association(segments.join('_'), klass)
28
- exists = attribute_method?(remainder.join('_'), found_assoc.klass)
27
+ assoc, poly_class = unpolymorphize_association(segments.join('_'))
28
+ if found_assoc = get_association(assoc, klass)
29
+ exists = attribute_method?(remainder.join('_'), poly_class || found_assoc.klass)
29
30
  end
30
31
  end
31
32
  end
@@ -33,20 +34,10 @@ module Ransack
33
34
  exists
34
35
  end
35
36
 
36
- def type_for(attr)
37
- return nil unless attr
38
- name = attr.name.to_s
39
- table = attr.relation.table_name
40
-
41
- unless @engine.connection_pool.table_exists?(table)
42
- raise "No table named #{table} exists"
43
- end
44
-
45
- @engine.connection_pool.columns_hash[table][name].type
37
+ def table_for(parent)
38
+ parent.table
46
39
  end
47
40
 
48
- private
49
-
50
41
  def klassify(obj)
51
42
  if Class === obj && ::ActiveRecord::Base > obj
52
43
  obj
@@ -59,27 +50,47 @@ module Ransack
59
50
  end
60
51
  end
61
52
 
62
- def get_attribute(str, parent = @base)
63
- attribute = nil
53
+ def type_for(attr)
54
+ return nil unless attr
55
+ name = attr.name.to_s
56
+ table = attr.relation.table_name
57
+
58
+ unless @engine.connection_pool.table_exists?(table)
59
+ raise "No table named #{table} exists"
60
+ end
61
+
62
+ @engine.connection_pool.columns_hash[table][name].type
63
+ end
64
+
65
+ private
64
66
 
65
- if column = get_column(str, parent)
66
- attribute = parent.table[str]
67
+ def get_parent_and_attribute_name(str, parent = @base)
68
+ attr_name = nil
69
+
70
+ if ransackable_attribute?(str, klassify(parent))
71
+ attr_name = str
67
72
  elsif (segments = str.split(/_/)).size > 1
68
73
  remainder = []
69
74
  found_assoc = nil
70
75
  while remainder.unshift(segments.pop) && segments.size > 0 && !found_assoc do
71
- if found_assoc = get_association(segments.join('_'), parent)
72
- join = build_or_find_association(found_assoc.name, parent)
73
- attribute = get_attribute(remainder.join('_'), join)
76
+ assoc, klass = unpolymorphize_association(segments.join('_'))
77
+ if ransackable_association?(assoc, klassify(parent))
78
+ found_assoc = get_association(assoc, parent)
79
+ join = build_or_find_association(found_assoc.name, parent, klass)
80
+ parent, attr_name = get_parent_and_attribute_name(remainder.join('_'), join)
74
81
  end
75
82
  end
76
83
  end
77
84
 
78
- attribute
85
+ [parent, attr_name]
86
+ end
87
+
88
+ def ransackable_attribute?(str, klass)
89
+ klass.ransackable_attributes(auth_object).include? str
79
90
  end
80
91
 
81
- def get_column(str, parent = @base)
82
- klassify(parent).columns_hash[str]
92
+ def ransackable_association?(str, klass)
93
+ klass.ransackable_associations(auth_object).include? str
83
94
  end
84
95
 
85
96
  def get_association(str, parent = @base)
@@ -87,7 +98,7 @@ module Ransack
87
98
  end
88
99
 
89
100
  def join_dependency(relation)
90
- if relation.respond_to?(:join_dependency) # MetaWhere will enable this
101
+ if relation.respond_to?(:join_dependency) # Squeel will enable this
91
102
  relation.join_dependency
92
103
  else
93
104
  build_join_dependency(relation)
@@ -101,7 +112,7 @@ module Ransack
101
112
  'string_join'
102
113
  when Hash, Symbol, Array
103
114
  'association_join'
104
- when ActiveRecord::Associations::JoinDependency::JoinAssociation
115
+ when ::ActiveRecord::Associations::JoinDependency::JoinAssociation
105
116
  'stashed_join'
106
117
  when Arel::Nodes::Join
107
118
  'join_node'
@@ -132,13 +143,14 @@ module Ransack
132
143
  join_dependency.graft(*stashed_association_joins)
133
144
  end
134
145
 
135
- def build_or_find_association(name, parent = @base)
146
+ def build_or_find_association(name, parent = @base, klass = nil)
136
147
  found_association = @join_dependency.join_associations.detect do |assoc|
137
148
  assoc.reflection.name == name &&
138
- assoc.parent == parent
149
+ assoc.parent == parent &&
150
+ (!klass || assoc.klass == klass)
139
151
  end
140
152
  unless found_association
141
- @join_dependency.send(:build, name.to_sym, parent, Arel::Nodes::OuterJoin)
153
+ @join_dependency.send(:build_polymorphic, name.to_sym, parent, Arel::Nodes::OuterJoin, klass)
142
154
  found_association = @join_dependency.join_associations.last
143
155
  # Leverage the stashed association functionality in AR
144
156
  @object = @object.joins(found_association)