ransack_ffcrm 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
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