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
@@ -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)