ransack_ffcrm 0.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 (56) hide show
  1. data/.gitignore +4 -0
  2. data/.travis.yml +9 -0
  3. data/Gemfile +40 -0
  4. data/LICENSE +20 -0
  5. data/README.md +137 -0
  6. data/Rakefile +19 -0
  7. data/lib/ransack/adapters/active_record/3.0/compat.rb +166 -0
  8. data/lib/ransack/adapters/active_record/3.0/context.rb +161 -0
  9. data/lib/ransack/adapters/active_record/3.1/context.rb +166 -0
  10. data/lib/ransack/adapters/active_record/base.rb +33 -0
  11. data/lib/ransack/adapters/active_record/context.rb +41 -0
  12. data/lib/ransack/adapters/active_record.rb +12 -0
  13. data/lib/ransack/configuration.rb +35 -0
  14. data/lib/ransack/constants.rb +23 -0
  15. data/lib/ransack/context.rb +124 -0
  16. data/lib/ransack/helpers/form_builder.rb +203 -0
  17. data/lib/ransack/helpers/form_helper.rb +75 -0
  18. data/lib/ransack/helpers.rb +2 -0
  19. data/lib/ransack/locale/en.yml +70 -0
  20. data/lib/ransack/naming.rb +53 -0
  21. data/lib/ransack/nodes/attribute.rb +49 -0
  22. data/lib/ransack/nodes/bindable.rb +30 -0
  23. data/lib/ransack/nodes/condition.rb +212 -0
  24. data/lib/ransack/nodes/grouping.rb +183 -0
  25. data/lib/ransack/nodes/node.rb +34 -0
  26. data/lib/ransack/nodes/sort.rb +41 -0
  27. data/lib/ransack/nodes/value.rb +108 -0
  28. data/lib/ransack/nodes.rb +7 -0
  29. data/lib/ransack/predicate.rb +70 -0
  30. data/lib/ransack/ransacker.rb +24 -0
  31. data/lib/ransack/search.rb +123 -0
  32. data/lib/ransack/translate.rb +92 -0
  33. data/lib/ransack/version.rb +3 -0
  34. data/lib/ransack/visitor.rb +68 -0
  35. data/lib/ransack.rb +27 -0
  36. data/ransack_ffcrm.gemspec +30 -0
  37. data/spec/blueprints/articles.rb +5 -0
  38. data/spec/blueprints/comments.rb +5 -0
  39. data/spec/blueprints/notes.rb +3 -0
  40. data/spec/blueprints/people.rb +4 -0
  41. data/spec/blueprints/tags.rb +3 -0
  42. data/spec/console.rb +21 -0
  43. data/spec/helpers/ransack_helper.rb +2 -0
  44. data/spec/ransack/adapters/active_record/base_spec.rb +67 -0
  45. data/spec/ransack/adapters/active_record/context_spec.rb +45 -0
  46. data/spec/ransack/configuration_spec.rb +31 -0
  47. data/spec/ransack/helpers/form_builder_spec.rb +137 -0
  48. data/spec/ransack/helpers/form_helper_spec.rb +38 -0
  49. data/spec/ransack/nodes/condition_spec.rb +15 -0
  50. data/spec/ransack/nodes/grouping_spec.rb +13 -0
  51. data/spec/ransack/predicate_spec.rb +55 -0
  52. data/spec/ransack/search_spec.rb +225 -0
  53. data/spec/spec_helper.rb +47 -0
  54. data/spec/support/en.yml +5 -0
  55. data/spec/support/schema.rb +111 -0
  56. metadata +229 -0
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/.travis.yml ADDED
@@ -0,0 +1,9 @@
1
+ rvm:
2
+ - 1.8.7
3
+ - 1.9.2
4
+ - ree
5
+ - rbx-18mode
6
+ - ruby-head
7
+
8
+ env:
9
+ - RAILS=3-2-stable
data/Gemfile ADDED
@@ -0,0 +1,40 @@
1
+ source "http://rubygems.org"
2
+ gemspec
3
+
4
+ gem 'rake'
5
+
6
+ rails = ENV['RAILS'] || 'master'
7
+ arel = ENV['AREL'] || 'master'
8
+
9
+ arel_opts = case arel
10
+ when /\// # A path
11
+ {:path => arel}
12
+ when /^v/ # A tagged version
13
+ {:git => 'git://github.com/rails/arel.git', :tag => arel}
14
+ else
15
+ {:git => 'git://github.com/rails/arel.git', :branch => arel}
16
+ end
17
+
18
+ gem 'arel', arel_opts
19
+
20
+ case rails
21
+ when /\// # A path
22
+ gem 'activesupport', :path => "#{rails}/activesupport"
23
+ gem 'activemodel', :path => "#{rails}/activemodel"
24
+ gem 'activerecord', :path => "#{rails}/activerecord"
25
+ gem 'actionpack', :path => "#{rails}/activerecord"
26
+ when /^v/ # A tagged version
27
+ git 'git://github.com/rails/rails.git', :tag => rails do
28
+ gem 'activesupport'
29
+ gem 'activemodel'
30
+ gem 'activerecord'
31
+ gem 'actionpack'
32
+ end
33
+ else
34
+ git 'git://github.com/rails/rails.git', :branch => rails do
35
+ gem 'activesupport'
36
+ gem 'activemodel'
37
+ gem 'activerecord'
38
+ gem 'actionpack'
39
+ end
40
+ end
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Ernie Miller
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,137 @@
1
+ # Ransack
2
+
3
+ Ransack is a rewrite of [MetaSearch](http://metautonomo.us/projects/metasearch). While it
4
+ supports many of the same features as MetaSearch, its underlying implementation differs
5
+ greatly from MetaSearch, and _backwards compatibility is not a design goal._
6
+
7
+ Ransack enables the creation of both simple and [advanced](http://ransack-demo.heroku.com)
8
+ search forms against your application's models. If you're looking for something that
9
+ simplifies query generation at the model or controller layer, you're probably not looking
10
+ for Ransack (or MetaSearch, for that matter). Try
11
+ [Squeel](http://metautonomo.us/projects/squeel) instead.
12
+
13
+ ## Getting started
14
+
15
+ In your Gemfile:
16
+
17
+ gem "ransack" # Last officially released gem
18
+ # gem "ransack", :git => "git://github.com/ernie/ransack.git" # Track git repo
19
+
20
+ If you'd like to add your own custom Ransack predicates:
21
+
22
+ Ransack.configure do |config|
23
+ config.add_predicate 'equals_diddly', # Name your predicate
24
+ # What non-compound ARel predicate will it use? (eq, matches, etc)
25
+ :arel_predicate => 'eq',
26
+ # Format incoming values as you see fit. (Default: Don't do formatting)
27
+ :formatter => proc {|v| "#{v}-diddly"},
28
+ # Validate a value. An "invalid" value won't be used in a search.
29
+ # Below is default.
30
+ :validator => proc {|v| v.present?},
31
+ # Should compounds be created? Will use the compound (any/all) version
32
+ # of the arel_predicate to create a corresponding any/all version of
33
+ # your predicate. (Default: true)
34
+ :compounds => true,
35
+ # Force a specific column type for type-casting of supplied values.
36
+ # (Default: use type from DB column)
37
+ :type => :string
38
+ end
39
+
40
+ ## Usage
41
+
42
+ Ransack can be used in one of two modes, simple or advanced.
43
+
44
+ ### Simple Mode
45
+
46
+ This mode works much like MetaSearch, for those of you who are familiar with it, and
47
+ requires very little setup effort.
48
+
49
+ If you're coming from MetaSearch, things to note:
50
+
51
+ 1. The default param key for search params is now `:q`, instead of `:search`. This is
52
+ primarily to shorten query strings, though advanced queries (below) will still
53
+ run afoul of URL length limits in most browsers and require a switch to HTTP
54
+ POST requests.
55
+ 2. `form_for` is now `search_form_for`, and validates that a Ransack::Search object
56
+ is passed to it.
57
+ 3. Common ActiveRecord::Relation methods are no longer delegated by the search object.
58
+ Instead, you will get your search results (an ActiveRecord::Relation in the case of
59
+ the ActiveRecord adapter) via a call to `Search#result`. If passed `:distinct => true`,
60
+ `result` will generate a `SELECT DISTINCT` to avoid returning duplicate rows, even if
61
+ conditions on a join would otherwise result in some.
62
+
63
+ Please note that for many databases, a sort on an associated table's columns will
64
+ result in invalid SQL with `:distinct => true` -- in those cases, you're on your own,
65
+ and will need to modify the result as needed to allow these queries to work. Thankfully,
66
+ 9 times out of 10, sort against the search's base is sufficient, though, as that's
67
+ generally what's being displayed on your results page.
68
+
69
+ In your controller:
70
+
71
+ def index
72
+ @q = Person.search(params[:q])
73
+ @people = @q.result(:distinct => true)
74
+ end
75
+
76
+ In your view:
77
+
78
+ <%= search_form_for @q do |f| %>
79
+ <%= f.label :name_cont %>
80
+ <%= f.text_field :name_cont %>
81
+ <%= f.label :articles_title_start %>
82
+ <%= f.text_field :articles_title_start %>
83
+ <%= f.submit %>
84
+ <% end %>
85
+
86
+ `cont` (contains) and `start` (starts with) are just two of the available search predicates.
87
+ See Constants for a full list.
88
+
89
+ ### Advanced Mode
90
+
91
+ "Advanced" searches (ab)use Rails' nested attributes functionality in order to generate
92
+ complex queries with nested AND/OR groupings, etc. This takes a bit more work but can
93
+ generate some pretty cool search interfaces that put a lot of power in the hands of
94
+ your users. A notable drawback with these searches is that the increased size of the
95
+ parameter string will typically force you to use the HTTP POST method instead of GET. :(
96
+
97
+ This means you'll need to tweak your routes...
98
+
99
+ resources :people do
100
+ collection do
101
+ match 'search' => 'people#search', :via => [:get, :post], :as => :search
102
+ end
103
+ end
104
+
105
+ ... and add another controller action ...
106
+
107
+ def search
108
+ index
109
+ render :index
110
+ end
111
+
112
+ ... and update your `search_form_for` line in the view ...
113
+
114
+ <%= search_form_for @q, :url => search_people_path,
115
+ :html => {:method => :post} do |f| %>
116
+
117
+ Once you've done so, you can make use of the helpers in Ransack::Helpers::FormBuilder to
118
+ construct much more complex search forms, such as the one on the
119
+ [demo page](http://ransack-demo.heroku.com).
120
+
121
+ **more docs to come**
122
+
123
+ ## Contributions
124
+
125
+ If you'd like to support the continued development of Ransack, please consider
126
+ [making a donation](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=48Q9HY64L3TWA).
127
+
128
+ To support the project in other ways:
129
+
130
+ * Use Ransack in your apps, and let me know if you encounter anything that's broken or missing.
131
+ A failing spec is awesome. A pull request is even better!
132
+ * Spread the word on Twitter, Facebook, and elsewhere if Ransack's been useful to you. The more
133
+ people who are using the project, the quicker we can find and fix bugs!
134
+
135
+ ## Copyright
136
+
137
+ Copyright &copy; 2011 [Ernie Miller](http://twitter.com/erniemiller)
data/Rakefile ADDED
@@ -0,0 +1,19 @@
1
+ require 'bundler'
2
+ require 'rspec/core/rake_task'
3
+
4
+ Bundler::GemHelper.install_tasks
5
+
6
+ RSpec::Core::RakeTask.new(:spec) do |rspec|
7
+ rspec.rspec_opts = ['--backtrace']
8
+ end
9
+
10
+ task :default => :spec
11
+
12
+ desc "Open an irb session with Ransack and the sample data used in specs"
13
+ task :console do
14
+ require 'irb'
15
+ require 'irb/completion'
16
+ require 'console'
17
+ ARGV.clear
18
+ IRB.start
19
+ end
@@ -0,0 +1,166 @@
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
24
+
25
+ module Arel
26
+
27
+ class Table
28
+ alias :table_name :name
29
+
30
+ def [] name
31
+ ::Arel::Attribute.new self, name.to_sym
32
+ end
33
+ end
34
+
35
+ module Nodes
36
+ class Node
37
+ def not
38
+ Nodes::Not.new self
39
+ end
40
+ end
41
+
42
+ remove_const :And
43
+ class And < Arel::Nodes::Node
44
+ attr_reader :children
45
+
46
+ def initialize children, right = nil
47
+ unless Array === children
48
+ children = [children, right]
49
+ end
50
+ @children = children
51
+ end
52
+
53
+ def left
54
+ children.first
55
+ end
56
+
57
+ def right
58
+ children[1]
59
+ end
60
+ end
61
+
62
+ class NamedFunction < Arel::Nodes::Function
63
+ attr_accessor :name, :distinct
64
+
65
+ include Arel::Predications
66
+
67
+ def initialize name, expr, aliaz = nil
68
+ super(expr, aliaz)
69
+ @name = name
70
+ @distinct = false
71
+ end
72
+ end
73
+
74
+ class InfixOperation < Binary
75
+ include Arel::Expressions
76
+ include Arel::Predications
77
+
78
+ attr_reader :operator
79
+
80
+ def initialize operator, left, right
81
+ super(left, right)
82
+ @operator = operator
83
+ end
84
+ end
85
+
86
+ class Multiplication < InfixOperation
87
+ def initialize left, right
88
+ super(:*, left, right)
89
+ end
90
+ end
91
+
92
+ class Division < InfixOperation
93
+ def initialize left, right
94
+ super(:/, left, right)
95
+ end
96
+ end
97
+
98
+ class Addition < InfixOperation
99
+ def initialize left, right
100
+ super(:+, left, right)
101
+ end
102
+ end
103
+
104
+ class Subtraction < InfixOperation
105
+ def initialize left, right
106
+ super(:-, left, right)
107
+ end
108
+ end
109
+ end
110
+
111
+ module Visitors
112
+ class ToSql
113
+ def column_for attr
114
+ name = attr.name.to_s
115
+ table = attr.relation.table_name
116
+
117
+ column_cache[table][name]
118
+ end
119
+
120
+ def column_cache
121
+ @column_cache ||= Hash.new do |hash, key|
122
+ hash[key] = Hash[
123
+ @engine.connection.columns(key, "#{key} Columns").map do |c|
124
+ [c.name, c]
125
+ end
126
+ ]
127
+ end
128
+ end
129
+
130
+ def visit_Arel_Nodes_InfixOperation o
131
+ "#{visit o.left} #{o.operator} #{visit o.right}"
132
+ end
133
+
134
+ def visit_Arel_Nodes_NamedFunction o
135
+ "#{o.name}(#{o.distinct ? 'DISTINCT ' : ''}#{o.expressions.map { |x|
136
+ visit x
137
+ }.join(', ')})#{o.alias ? " AS #{visit o.alias}" : ''}"
138
+ end
139
+
140
+ def visit_Arel_Nodes_And o
141
+ o.children.map { |x| visit x }.join ' AND '
142
+ end
143
+
144
+ def visit_Arel_Nodes_Not o
145
+ "NOT (#{visit o.expr})"
146
+ end
147
+
148
+ def visit_Arel_Nodes_Values o
149
+ "VALUES (#{o.expressions.zip(o.columns).map { |value, attr|
150
+ if Nodes::SqlLiteral === value
151
+ visit_Arel_Nodes_SqlLiteral value
152
+ else
153
+ quote(value, attr && column_for(attr))
154
+ end
155
+ }.join ', '})"
156
+ end
157
+ end
158
+ end
159
+
160
+ module Predications
161
+ def as other
162
+ Nodes::As.new self, Nodes::SqlLiteral.new(other)
163
+ end
164
+ end
165
+
166
+ end
@@ -0,0 +1,161 @@
1
+ require 'ransack/context'
2
+ require 'polyamorous'
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 initialize(object, options = {})
15
+ super
16
+ @arel_visitor = Arel::Visitors.visitor_for @engine
17
+ end
18
+
19
+ def evaluate(search, opts = {})
20
+ viz = Visitor.new
21
+ relation = @object.where(viz.accept(search.base))
22
+ if search.sorts.any?
23
+ relation = relation.except(:order).order(viz.accept(search.sorts))
24
+ end
25
+ opts[:distinct] ? relation.select("DISTINCT #{@klass.quoted_table_name}.*") : relation
26
+ end
27
+
28
+ def attribute_method?(str, klass = @klass)
29
+ exists = false
30
+
31
+ if ransackable_attribute?(str, klass)
32
+ exists = true
33
+ elsif (segments = str.split(/_/)).size > 1
34
+ remainder = []
35
+ found_assoc = nil
36
+ while !found_assoc && remainder.unshift(segments.pop) && segments.size > 0 do
37
+ assoc, poly_class = unpolymorphize_association(segments.join('_'))
38
+ if found_assoc = get_association(assoc, klass)
39
+ exists = attribute_method?(remainder.join('_'), poly_class || found_assoc.klass)
40
+ end
41
+ end
42
+ end
43
+
44
+ exists
45
+ end
46
+
47
+ def table_for(parent)
48
+ parent.table
49
+ end
50
+
51
+ def klassify(obj)
52
+ if Class === obj && ::ActiveRecord::Base > obj
53
+ obj
54
+ elsif obj.respond_to? :klass
55
+ obj.klass
56
+ elsif obj.respond_to? :active_record
57
+ obj.active_record
58
+ else
59
+ raise ArgumentError, "Don't know how to klassify #{obj}"
60
+ end
61
+ end
62
+
63
+ def type_for(attr)
64
+ return nil unless attr && attr.valid?
65
+ klassify(attr.parent).columns_hash[attr.arel_attribute.name.to_s].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 found_assoc = get_association(assoc, parent)
81
+ join = build_or_find_association(found_assoc.name, parent, klass)
82
+ parent, attr_name = get_parent_and_attribute_name(remainder.join('_'), join)
83
+ end
84
+ end
85
+ end
86
+
87
+ [parent, attr_name]
88
+ end
89
+
90
+ def get_association(str, parent = @base)
91
+ klass = klassify parent
92
+ ransackable_association?(str, klass) &&
93
+ klass.reflect_on_all_associations.detect {|a| a.name.to_s == str}
94
+ end
95
+
96
+ def join_dependency(relation)
97
+ if relation.respond_to?(:join_dependency) # Squeel will enable this
98
+ relation.join_dependency
99
+ else
100
+ build_join_dependency(relation)
101
+ end
102
+ end
103
+
104
+ def build_join_dependency(relation)
105
+ buckets = relation.joins_values.group_by do |join|
106
+ case join
107
+ when String
108
+ 'string_join'
109
+ when Hash, Symbol, Array
110
+ 'association_join'
111
+ when ::ActiveRecord::Associations::ClassMethods::JoinDependency::JoinAssociation
112
+ 'stashed_join'
113
+ when Arel::Nodes::Join
114
+ 'join_node'
115
+ else
116
+ raise 'unknown class: %s' % join.class.name
117
+ end
118
+ end
119
+
120
+ association_joins = buckets['association_join'] || []
121
+ stashed_association_joins = buckets['stashed_join'] || []
122
+ join_nodes = buckets['join_node'] || []
123
+ string_joins = (buckets['string_join'] || []).map { |x|
124
+ x.strip
125
+ }.uniq
126
+
127
+ join_list = relation.send :custom_join_sql, (string_joins + join_nodes)
128
+
129
+ join_dependency = JoinDependency.new(
130
+ relation.klass,
131
+ association_joins,
132
+ join_list
133
+ )
134
+
135
+ join_nodes.each do |join|
136
+ join_dependency.table_aliases[join.left.name.downcase] = 1
137
+ end
138
+
139
+ join_dependency.graft(*stashed_association_joins)
140
+ end
141
+
142
+ def build_or_find_association(name, parent = @base, klass = nil)
143
+ found_association = @join_dependency.join_associations.detect do |assoc|
144
+ assoc.reflection.name == name &&
145
+ assoc.parent == parent &&
146
+ (!klass || assoc.reflection.klass == klass)
147
+ end
148
+ unless found_association
149
+ @join_dependency.send(:build, Polyamorous::Join.new(name, @join_type, klass), parent)
150
+ found_association = @join_dependency.join_associations.last
151
+ # Leverage the stashed association functionality in AR
152
+ @object = @object.joins(found_association)
153
+ end
154
+
155
+ found_association
156
+ end
157
+
158
+ end
159
+ end
160
+ end
161
+ end