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.
- data/.gitignore +4 -0
- data/.travis.yml +9 -0
- data/Gemfile +40 -0
- data/LICENSE +20 -0
- data/README.md +137 -0
- data/Rakefile +19 -0
- data/lib/ransack/adapters/active_record/3.0/compat.rb +166 -0
- data/lib/ransack/adapters/active_record/3.0/context.rb +161 -0
- data/lib/ransack/adapters/active_record/3.1/context.rb +166 -0
- data/lib/ransack/adapters/active_record/base.rb +33 -0
- data/lib/ransack/adapters/active_record/context.rb +41 -0
- data/lib/ransack/adapters/active_record.rb +12 -0
- data/lib/ransack/configuration.rb +35 -0
- data/lib/ransack/constants.rb +23 -0
- data/lib/ransack/context.rb +124 -0
- data/lib/ransack/helpers/form_builder.rb +203 -0
- data/lib/ransack/helpers/form_helper.rb +75 -0
- data/lib/ransack/helpers.rb +2 -0
- data/lib/ransack/locale/en.yml +70 -0
- data/lib/ransack/naming.rb +53 -0
- data/lib/ransack/nodes/attribute.rb +49 -0
- data/lib/ransack/nodes/bindable.rb +30 -0
- data/lib/ransack/nodes/condition.rb +212 -0
- data/lib/ransack/nodes/grouping.rb +183 -0
- data/lib/ransack/nodes/node.rb +34 -0
- data/lib/ransack/nodes/sort.rb +41 -0
- data/lib/ransack/nodes/value.rb +108 -0
- data/lib/ransack/nodes.rb +7 -0
- data/lib/ransack/predicate.rb +70 -0
- data/lib/ransack/ransacker.rb +24 -0
- data/lib/ransack/search.rb +123 -0
- data/lib/ransack/translate.rb +92 -0
- data/lib/ransack/version.rb +3 -0
- data/lib/ransack/visitor.rb +68 -0
- data/lib/ransack.rb +27 -0
- data/ransack_ffcrm.gemspec +30 -0
- data/spec/blueprints/articles.rb +5 -0
- data/spec/blueprints/comments.rb +5 -0
- data/spec/blueprints/notes.rb +3 -0
- data/spec/blueprints/people.rb +4 -0
- data/spec/blueprints/tags.rb +3 -0
- data/spec/console.rb +21 -0
- data/spec/helpers/ransack_helper.rb +2 -0
- data/spec/ransack/adapters/active_record/base_spec.rb +67 -0
- data/spec/ransack/adapters/active_record/context_spec.rb +45 -0
- data/spec/ransack/configuration_spec.rb +31 -0
- data/spec/ransack/helpers/form_builder_spec.rb +137 -0
- data/spec/ransack/helpers/form_helper_spec.rb +38 -0
- data/spec/ransack/nodes/condition_spec.rb +15 -0
- data/spec/ransack/nodes/grouping_spec.rb +13 -0
- data/spec/ransack/predicate_spec.rb +55 -0
- data/spec/ransack/search_spec.rb +225 -0
- data/spec/spec_helper.rb +47 -0
- data/spec/support/en.yml +5 -0
- data/spec/support/schema.rb +111 -0
- metadata +229 -0
data/.gitignore
ADDED
data/.travis.yml
ADDED
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 © 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
|