meta_search 0.3.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitmodules +6 -0
- data/Gemfile +7 -0
- data/README.rdoc +165 -26
- data/Rakefile +18 -5
- data/VERSION +1 -1
- data/lib/meta_search.rb +28 -18
- data/lib/meta_search/builder.rb +178 -125
- data/lib/meta_search/exceptions.rb +1 -0
- data/lib/meta_search/helpers.rb +3 -0
- data/lib/meta_search/helpers/form_builder.rb +152 -0
- data/lib/meta_search/helpers/form_helper.rb +20 -0
- data/lib/meta_search/helpers/url_helper.rb +39 -0
- data/lib/meta_search/method.rb +129 -0
- data/lib/meta_search/model_compatibility.rb +36 -2
- data/lib/meta_search/searches/active_record.rb +88 -11
- data/lib/meta_search/utility.rb +1 -1
- data/lib/meta_search/where.rb +119 -61
- data/meta_search.gemspec +33 -13
- data/test/fixtures/company.rb +15 -2
- data/test/fixtures/data_types.yml +3 -3
- data/test/fixtures/developer.rb +3 -0
- data/test/helper.rb +10 -6
- data/test/test_search.rb +502 -288
- data/test/test_view_helpers.rb +152 -53
- metadata +82 -15
- data/lib/meta_search/helpers/action_view.rb +0 -168
- data/lib/meta_search/railtie.rb +0 -21
- data/lib/meta_search/searches/base.rb +0 -46
data/.gitmodules
ADDED
data/Gemfile
ADDED
data/README.rdoc
CHANGED
@@ -1,23 +1,28 @@
|
|
1
1
|
= MetaSearch
|
2
2
|
|
3
|
-
|
3
|
+
MetaSearch is extensible searching for your form_for enjoyment. It “wraps” one of your ActiveRecord models, providing methods that allow you to build up search conditions against that model, and has a few extra form helpers to simplify sorting and supplying multiple parameters to your condition methods as well.
|
4
4
|
|
5
5
|
== Getting Started
|
6
6
|
|
7
|
-
|
7
|
+
In your Gemfile:
|
8
8
|
|
9
|
-
gem "meta_search"
|
9
|
+
gem "meta_search" # Last officially released gem
|
10
|
+
# gem "meta_search", :git => "git://github.com/ernie/meta_search.git" # Track git repo
|
11
|
+
|
12
|
+
or, to install as a plugin:
|
13
|
+
|
14
|
+
rails plugin install git://github.com/ernie/meta_search.git
|
10
15
|
|
11
16
|
In your controller:
|
12
17
|
|
13
18
|
def index
|
14
19
|
@search = Article.search(params[:search])
|
15
|
-
@articles = @search.all
|
20
|
+
@articles = @search.all # or @search.relation to lazy load in view
|
16
21
|
end
|
17
22
|
|
18
23
|
In your view:
|
19
24
|
|
20
|
-
|
25
|
+
<%= form_for @search, :url => articles_path, :html => {:method => :get} do |f| %>
|
21
26
|
<%= f.label :title_contains %>
|
22
27
|
<%= f.text_field :title_contains %><br />
|
23
28
|
<%= f.label :comments_created_at_greater_than, 'With comments after' %>
|
@@ -25,21 +30,107 @@ In your view:
|
|
25
30
|
<!-- etc... -->
|
26
31
|
<%= f.submit %>
|
27
32
|
<% end %>
|
28
|
-
|
33
|
+
|
29
34
|
The default Where types are listed at MetaSearch::Where. Options for the search method are documented at MetaSearch::Searches::Base.
|
30
35
|
|
31
36
|
== Advanced usage
|
32
37
|
|
38
|
+
=== Narrowing the scope of a search
|
39
|
+
|
40
|
+
While the most common use case is to simply call Model.search(params[:search]), there
|
41
|
+
may be times where you want to scope your search more tightly. For instance, only allowing
|
42
|
+
users to search their own projects (assuming a current_user method returning the current user):
|
43
|
+
|
44
|
+
@search = current_user.projects.search(params[:search])
|
45
|
+
|
46
|
+
Or, you can build up any relation you like and call the search method on that object:
|
47
|
+
|
48
|
+
@projects_with_awesome_users_search =
|
49
|
+
Project.joins(:user).where(:users => {:awesome => true}).search(params[:search])
|
50
|
+
|
51
|
+
=== Multi-level associations
|
52
|
+
|
53
|
+
MetaSearch will allow you traverse your associations in one form, generating the necessary
|
54
|
+
joins along the way. If you have the following models...
|
55
|
+
|
56
|
+
class Company < ActiveRecord::Base
|
57
|
+
has_many :developers
|
58
|
+
end
|
59
|
+
|
60
|
+
class Developer < ActiveRecord::Base
|
61
|
+
belongs_to :company
|
62
|
+
has_many :notes
|
63
|
+
end
|
64
|
+
|
65
|
+
...you can do this in your form to search your companies by developers with certain notes:
|
66
|
+
|
67
|
+
<%= f.text_field :developers_notes_note %>
|
68
|
+
|
69
|
+
You can travel forward and back through the associations, so this would also work (though
|
70
|
+
be entirely pointless in this case):
|
71
|
+
|
72
|
+
<%= f.text_field :developers_notes_developer_company_name %>
|
73
|
+
|
74
|
+
However, to prevent abuse, this is limited to associations of a total "depth" of 5 levels.
|
75
|
+
This means that while starting from a Company model, as above, you could do
|
76
|
+
Company -> :developers -> :notes -> :developer -> :company, which has gotten you right
|
77
|
+
back where you started, but "travels" through 5 models total.
|
78
|
+
|
33
79
|
=== Adding a new Where
|
34
80
|
|
35
81
|
If none of the built-in search criteria work for you, you can add a new Where (or 5). To do so,
|
36
82
|
create an initializer (<tt>/config/initializers/meta_search.rb</tt>, for instance) and add lines
|
37
83
|
like:
|
38
84
|
|
39
|
-
MetaSearch::Where.add :between, :btw,
|
85
|
+
MetaSearch::Where.add :between, :btw,
|
86
|
+
:predicate => :in,
|
87
|
+
:types => [:integer, :float, :decimal, :date, :datetime, :timestamp, :time],
|
88
|
+
:formatter => Proc.new {|param| Range.new(param.first, param.last)},
|
89
|
+
:validator => Proc.new {|param|
|
90
|
+
param.is_a?(Array) && !(param[0].blank? || param[1].blank?)
|
91
|
+
}
|
40
92
|
|
41
93
|
See MetaSearch::Where for info on the supported options.
|
42
94
|
|
95
|
+
=== Accessing custom search methods (and named scopes!)
|
96
|
+
|
97
|
+
MetaSearch can be given access to any class method on your model to extend its search capabilities.
|
98
|
+
The only rule is that the method must return an ActiveRecord::Relation so that MetaSearch can
|
99
|
+
continue to extend the search with other attributes. Conveniently, scopes (formerly "named scopes")
|
100
|
+
do this already.
|
101
|
+
|
102
|
+
Consider the following model:
|
103
|
+
|
104
|
+
class Company < ActiveRecord::Base
|
105
|
+
has_many :slackers, :class_name => "Developer", :conditions => {:slacker => true}
|
106
|
+
scope :backwards_name, lambda {|name| where(:name => name.reverse)}
|
107
|
+
scope :with_slackers_by_name_and_salary_range,
|
108
|
+
lambda {|name, low, high|
|
109
|
+
joins(:slackers).where(:developers => {:name => name, :salary => low..high})
|
110
|
+
}
|
111
|
+
end
|
112
|
+
|
113
|
+
To allow MetaSearch access to a model method, including a named scope, just use
|
114
|
+
<tt>search_methods</tt> in the model:
|
115
|
+
|
116
|
+
search_methods :backwards_name
|
117
|
+
|
118
|
+
This will allow you to add a text field named :backwards_name to your search form, and
|
119
|
+
it will behave as you might expect.
|
120
|
+
|
121
|
+
In the case of the second scope, we have multiple parameters to pass in, of different
|
122
|
+
types. We can pass the following to <tt>search_methods</tt>:
|
123
|
+
|
124
|
+
search_methods :with_slackers_by_name_and_salary_range,
|
125
|
+
:splat_param => true, :type => [:string, :integer, :integer]
|
126
|
+
|
127
|
+
MetaSearch needs us to tell it that we don't want to keep the array supplied to it as-is, but
|
128
|
+
"splat" it when passing it to the model method. Regarding <tt>:types</tt>: In this case,
|
129
|
+
ActiveRecord would have been smart enough to handle the typecasting for us, but I wanted to
|
130
|
+
demonstrate how we can tell MetaSearch that a given parameter is of a specific database "column type." This is just a hint MetaSearch uses in the same way it does when casting "Where" params based
|
131
|
+
on the DB column being searched. It's also important so that things like dates get handled
|
132
|
+
properly by FormBuilder.
|
133
|
+
|
43
134
|
=== multiparameter_field
|
44
135
|
|
45
136
|
The example Where above adds support for a "between" search, which requires an array with
|
@@ -53,49 +144,97 @@ MetaSearch adds a helper for this:
|
|
53
144
|
lets you sandwich a list of fields, each in hash format, between the attribute and the usual
|
54
145
|
options hash. See MetaSearch::Helpers::FormBuilder for more info.
|
55
146
|
|
147
|
+
=== Compound conditions (any/all)
|
148
|
+
|
149
|
+
All Where types automatically get an "any" and "all" variant. This has the same name and
|
150
|
+
aliases as the original, but is suffixed with _any and _all, for an "OR" or "AND" search,
|
151
|
+
respectively. So, if you want to provide the user with 5 different search boxes to enter
|
152
|
+
possible article titles:
|
153
|
+
|
154
|
+
<%= f.multiparameter_field :title_contains_any,
|
155
|
+
*5.times.inject([]) {|a, b| a << {:field_type => :text_field}} +
|
156
|
+
[:size => 10] %>
|
157
|
+
|
56
158
|
=== check_boxes and collection_check_boxes
|
57
159
|
|
58
160
|
If you need to get an array into your where, and you don't care about parameter order,
|
59
161
|
you might choose to use a select or collection_select with multiple selection enabled,
|
60
162
|
but everyone hates multiple selection boxes. MetaSearch adds a couple of additional
|
61
163
|
helpers, +check_boxes+ and +collection_check_boxes+ to handle multiple selections in a
|
62
|
-
more visually appealing manner.
|
63
|
-
|
164
|
+
more visually appealing manner. They can be called with or without a block. Without a
|
165
|
+
block, you get an array of MetaSearch::Check objects to do with as you please.
|
64
166
|
|
65
|
-
|
66
|
-
|
167
|
+
With a block, each check is yielded to your template, like so:
|
168
|
+
|
169
|
+
<h4>How many heads?</h4>
|
170
|
+
<ul>
|
67
171
|
<% f.check_boxes :number_of_heads_in,
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
172
|
+
[['One', 1], ['Two', 2], ['Three', 3]], :class => 'checkboxy' do |check| %>
|
173
|
+
<li>
|
174
|
+
<%= check.box %>
|
175
|
+
<%= check.label %>
|
176
|
+
</li>
|
73
177
|
<% end %>
|
74
|
-
</
|
178
|
+
</ul>
|
75
179
|
|
76
180
|
Again, full documentation is in MetaSearch::Helpers::FormBuilder.
|
77
181
|
|
78
|
-
===
|
182
|
+
=== Sorting columns
|
183
|
+
|
184
|
+
If you'd like to sort by a specific column in your results (the attributes of the base model)
|
185
|
+
or an association column then supply the <tt>meta_sort</tt> parameter in your form.
|
186
|
+
The parameter takes the form <tt>column.direction</tt> where +column+ is the column name or
|
187
|
+
underscore-separated association_column combination, and +direction+ is one of "asc" or "desc"
|
188
|
+
for ascending or descending, respectively.
|
189
|
+
|
190
|
+
Normally, you won't supply this parameter yourself, but instead will use the helper method
|
191
|
+
<tt>sort_link</tt> in your views, like so:
|
192
|
+
|
193
|
+
<%= sort_link @search, :title %>
|
194
|
+
<%= sort_link @search, :created_at %>
|
195
|
+
|
196
|
+
The <tt>@search</tt> object is the instance of MetaSearch::Builder you got back earlier from
|
197
|
+
your controller. The other required parameter is the attribute name itself. Optionally,
|
198
|
+
you can provide a string as a 3rd parameter to override the default link name, and then
|
199
|
+
additional hashed for the +options+ and +html_options+ hashes for link_to.
|
200
|
+
|
201
|
+
All <tt>sort_link</tt>-generated links will have the CSS class sort_link, as well as a
|
202
|
+
directional class (ascending or descending) if the link is for a currently sorted column,
|
203
|
+
for your styling enjoyment.
|
79
204
|
|
80
|
-
|
81
|
-
|
205
|
+
This feature should hopefully help out those of you migrating from SearchLogic, and a thanks
|
206
|
+
goes out to Ben Johnson for the HTML entities used for the up and down arrows, which provide
|
207
|
+
a nice default look.
|
208
|
+
|
209
|
+
=== Including/excluding attributes and associations
|
210
|
+
|
211
|
+
If you'd like to allow only certain associations or attributes to be searched, you can do
|
212
|
+
so inside your models
|
82
213
|
|
83
214
|
class Article < ActiveRecord::Base
|
84
|
-
|
85
|
-
|
215
|
+
attr_searchable :some_public_data, :some_more_searchable_stuff
|
216
|
+
assoc_searchable :search_this_association_why_dontcha
|
86
217
|
end
|
87
218
|
|
88
|
-
|
89
|
-
|
219
|
+
If you'd rather blacklist attributes and associations rather than whitelist, use the
|
220
|
+
<tt>attr_unsearchable</tt> and <tt>assoc_unsearchable</tt> method instead. If a
|
221
|
+
whitelist is supplied, it takes precedence.
|
222
|
+
|
223
|
+
Excluded attributes on a model will be honored across associations, so if an Article
|
224
|
+
<tt>has_many :comments</tt> and the Comment model looks something like this:
|
90
225
|
|
91
|
-
Comment < ActiveRecord::Base
|
226
|
+
class Comment < ActiveRecord::Base
|
92
227
|
validates_presence_of :user_id, :body
|
93
|
-
|
228
|
+
attr_unsearchable :user_id
|
94
229
|
end
|
95
230
|
|
96
231
|
Then your call to <tt>Article.search</tt> will allow <tt>:comments_body_contains</tt>
|
97
232
|
but not <tt>:comments_user_id_equals</tt> to be passed.
|
98
233
|
|
234
|
+
== Reporting issues
|
235
|
+
|
236
|
+
Please report any issues using {Lighthouse}[http://metautonomous.lighthouseapp.com/projects/53012-metasearch/]. Thanks in advance for helping me improve MetaSearch!
|
237
|
+
|
99
238
|
== Copyright
|
100
239
|
|
101
240
|
Copyright (c) 2010 {Ernie Miller}[http://metautonomo.us]. See LICENSE for details.
|
data/Rakefile
CHANGED
@@ -5,12 +5,20 @@ begin
|
|
5
5
|
require 'jeweler'
|
6
6
|
Jeweler::Tasks.new do |gem|
|
7
7
|
gem.name = "meta_search"
|
8
|
-
gem.summary = %Q{ActiveRecord 3 object-based searching.}
|
9
|
-
gem.description = %Q{
|
8
|
+
gem.summary = %Q{ActiveRecord 3 object-based searching for your form_for enjoyment.}
|
9
|
+
gem.description = %Q{
|
10
|
+
Allows simple search forms to be created against an AR3 model
|
11
|
+
and its associations, has useful view helpers for sort links
|
12
|
+
and multiparameter fields as well.
|
13
|
+
}
|
10
14
|
gem.email = "ernie@metautonomo.us"
|
11
|
-
gem.homepage = "http://metautonomo.us"
|
15
|
+
gem.homepage = "http://metautonomo.us/projects/metasearch/"
|
12
16
|
gem.authors = ["Ernie Miller"]
|
13
|
-
gem.add_development_dependency "
|
17
|
+
gem.add_development_dependency "shoulda"
|
18
|
+
gem.add_dependency "activerecord", ">= 3.0.0.beta4"
|
19
|
+
gem.add_dependency "activesupport", ">= 3.0.0.beta4"
|
20
|
+
gem.add_dependency "actionpack", ">= 3.0.0.beta4"
|
21
|
+
gem.add_dependency "arel", ">= 0.4.0"
|
14
22
|
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
15
23
|
end
|
16
24
|
Jeweler::GemcutterTasks.new
|
@@ -21,6 +29,10 @@ end
|
|
21
29
|
require 'rake/testtask'
|
22
30
|
Rake::TestTask.new(:test) do |test|
|
23
31
|
test.libs << 'lib' << 'test'
|
32
|
+
test.libs << 'vendor/rails/activerecord/lib'
|
33
|
+
test.libs << 'vendor/rails/activesupport/lib'
|
34
|
+
test.libs << 'vendor/rails/actionpack/lib'
|
35
|
+
test.libs << 'vendor/arel/lib'
|
24
36
|
test.pattern = 'test/**/test_*.rb'
|
25
37
|
test.verbose = true
|
26
38
|
end
|
@@ -38,7 +50,8 @@ rescue LoadError
|
|
38
50
|
end
|
39
51
|
end
|
40
52
|
|
41
|
-
|
53
|
+
# Don't check dependencies on test, we're using vendored libraries
|
54
|
+
# task :test => :check_dependencies
|
42
55
|
|
43
56
|
task :default => :test
|
44
57
|
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.5.0
|
data/lib/meta_search.rb
CHANGED
@@ -6,26 +6,36 @@ module MetaSearch
|
|
6
6
|
BOOLEANS = [:boolean]
|
7
7
|
ALL_TYPES = NUMBERS + STRINGS + DATES + TIMES + BOOLEANS
|
8
8
|
|
9
|
+
# Change this only if you know what you're doing. It's here for your protection.
|
10
|
+
MAX_JOIN_DEPTH = 5
|
11
|
+
|
9
12
|
DEFAULT_WHERES = [
|
10
13
|
['equals', 'eq'],
|
11
|
-
['does_not_equal', 'ne', {:types => ALL_TYPES, :
|
12
|
-
['contains', 'like', {:types => STRINGS, :
|
13
|
-
['does_not_contain', 'nlike', {:types => STRINGS, :
|
14
|
-
['starts_with', 'sw', {:types => STRINGS, :
|
15
|
-
['does_not_start_with', 'dnsw', {:types => STRINGS, :
|
16
|
-
['ends_with', 'ew', {:types => STRINGS, :
|
17
|
-
['does_not_end_with', 'dnew', {:types => STRINGS, :
|
18
|
-
['greater_than', 'gt', {:types => (NUMBERS + DATES + TIMES), :
|
19
|
-
['less_than', 'lt', {:types => (NUMBERS + DATES + TIMES), :
|
20
|
-
['greater_than_or_equal_to', 'gte', {:types => (NUMBERS + DATES + TIMES), :
|
21
|
-
['less_than_or_equal_to', 'lte', {:types => (NUMBERS + DATES + TIMES), :
|
22
|
-
['in', {:types => ALL_TYPES, :
|
23
|
-
['not_in', 'ni', {:types => ALL_TYPES, :
|
14
|
+
['does_not_equal', 'ne', 'not_eq', {:types => ALL_TYPES, :predicate => :not_eq}],
|
15
|
+
['contains', 'like', 'matches', {:types => STRINGS, :predicate => :matches, :formatter => '"%#{param}%"'}],
|
16
|
+
['does_not_contain', 'nlike', 'not_matches', {:types => STRINGS, :predicate => :not_matches, :formatter => '"%#{param}%"'}],
|
17
|
+
['starts_with', 'sw', {:types => STRINGS, :predicate => :matches, :formatter => '"#{param}%"'}],
|
18
|
+
['does_not_start_with', 'dnsw', {:types => STRINGS, :predicate => :not_matches, :formatter => '"%#{param}%"'}],
|
19
|
+
['ends_with', 'ew', {:types => STRINGS, :predicate => :matches, :formatter => '"%#{param}"'}],
|
20
|
+
['does_not_end_with', 'dnew', {:types => STRINGS, :predicate => :not_matches, :formatter => '"%#{param}"'}],
|
21
|
+
['greater_than', 'gt', {:types => (NUMBERS + DATES + TIMES), :predicate => :gt}],
|
22
|
+
['less_than', 'lt', {:types => (NUMBERS + DATES + TIMES), :predicate => :lt}],
|
23
|
+
['greater_than_or_equal_to', 'gte', 'gteq', {:types => (NUMBERS + DATES + TIMES), :predicate => :gteq}],
|
24
|
+
['less_than_or_equal_to', 'lte', 'lteq', {:types => (NUMBERS + DATES + TIMES), :predicate => :lteq}],
|
25
|
+
['in', {:types => ALL_TYPES, :predicate => :in}],
|
26
|
+
['not_in', 'ni', 'not_in', {:types => ALL_TYPES, :predicate => :not_in}]
|
24
27
|
]
|
25
|
-
|
26
|
-
RELATION_METHODS = [:joins, :includes, :all, :count, :to_sql, :paginate, :find_each, :first, :last, :each]
|
28
|
+
|
29
|
+
RELATION_METHODS = [:joins, :includes, :to_a, :all, :count, :to_sql, :paginate, :autojoin, :find_each, :first, :last, :each, :arel]
|
27
30
|
end
|
28
31
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
+
require 'active_record'
|
33
|
+
require 'action_view'
|
34
|
+
require 'action_controller'
|
35
|
+
require 'meta_search/searches/active_record'
|
36
|
+
require 'meta_search/helpers'
|
37
|
+
|
38
|
+
ActiveRecord::Base.send(:include, MetaSearch::Searches::ActiveRecord)
|
39
|
+
ActionView::Helpers::FormBuilder.send(:include, MetaSearch::Helpers::FormBuilder)
|
40
|
+
ActionController::Base.helper(MetaSearch::Helpers::UrlHelper)
|
41
|
+
ActionController::Base.helper(MetaSearch::Helpers::FormHelper)
|
data/lib/meta_search/builder.rb
CHANGED
@@ -4,6 +4,12 @@ require 'meta_search/where'
|
|
4
4
|
require 'meta_search/utility'
|
5
5
|
|
6
6
|
module MetaSearch
|
7
|
+
# Raised if you try to access a relation that's joining too many tables to itself.
|
8
|
+
# This is designed to prevent a malicious user from accessing something like
|
9
|
+
# :developers_company_developers_company_developers_company_developers_company_...,
|
10
|
+
# resulting in a query that could cause issues for your database server.
|
11
|
+
class JoinDepthError < StandardError; end
|
12
|
+
|
7
13
|
# Builder is the workhorse of MetaSearch -- it is the class that handles dynamically generating
|
8
14
|
# methods based on a supplied model, and is what gets instantiated when you call your model's search
|
9
15
|
# method. Builder doesn't generate any methods until they're needed, using method_missing to compare
|
@@ -21,59 +27,79 @@ module MetaSearch
|
|
21
27
|
class Builder
|
22
28
|
include ModelCompatibility
|
23
29
|
include Utility
|
24
|
-
|
25
|
-
attr_reader :base, :search_attributes, :
|
26
|
-
delegate *RELATION_METHODS
|
30
|
+
|
31
|
+
attr_reader :base, :search_attributes, :join_dependency
|
32
|
+
delegate *RELATION_METHODS + [:to => :relation]
|
27
33
|
|
28
34
|
# Initialize a new Builder. Requires a base model to wrap, and supports a couple of options
|
29
35
|
# for how it will expose this model and its associations to your controllers/views.
|
30
|
-
def initialize(
|
31
|
-
@
|
36
|
+
def initialize(base_or_relation, opts = {})
|
37
|
+
@relation = base_or_relation.scoped
|
38
|
+
@base = @relation.klass
|
32
39
|
@opts = opts
|
33
|
-
@
|
34
|
-
@join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(@base, [], nil)
|
40
|
+
@join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(@base, @relation.joins_values, nil)
|
35
41
|
@search_attributes = {}
|
36
|
-
@relation = @base.scoped
|
37
42
|
end
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
@
|
43
|
+
|
44
|
+
def relation
|
45
|
+
enforce_join_depth_limit!
|
46
|
+
@relation
|
42
47
|
end
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
48
|
+
|
49
|
+
def get_column(column, base = @base)
|
50
|
+
if base._metasearch_include_attributes.blank?
|
51
|
+
base.columns_hash[column.to_s] unless base._metasearch_exclude_attributes.include?(column.to_s)
|
52
|
+
else
|
53
|
+
base.columns_hash[column.to_s] if base._metasearch_include_attributes.include?(column.to_s)
|
49
54
|
end
|
50
55
|
end
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
assoc
|
57
|
-
assoc.klass.columns_hash[attr.to_s] unless assoc.klass._metasearch_exclude_attributes.include?(attr.to_s)
|
56
|
+
|
57
|
+
def get_association(assoc, base = @base)
|
58
|
+
if base._metasearch_include_associations.blank?
|
59
|
+
base.reflect_on_association(assoc.to_sym) unless base._metasearch_exclude_associations.include?(assoc.to_s)
|
60
|
+
else
|
61
|
+
base.reflect_on_association(assoc.to_sym) if base._metasearch_include_associations.include?(assoc.to_s)
|
58
62
|
end
|
59
63
|
end
|
60
|
-
|
61
|
-
def
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
64
|
+
|
65
|
+
def get_attribute(name, parent = @join_dependency.join_base)
|
66
|
+
attribute = nil
|
67
|
+
if get_column(name, parent.active_record)
|
68
|
+
if parent.is_a?(ActiveRecord::Associations::ClassMethods::JoinDependency::JoinAssociation)
|
69
|
+
relation = parent.relation.is_a?(Array) ? parent.relation.last : parent.relation
|
70
|
+
attribute = relation.table[name]
|
71
|
+
else
|
72
|
+
attribute = @relation.table[name]
|
73
|
+
end
|
74
|
+
elsif (segments = name.to_s.split(/_/)).size > 1
|
75
|
+
remainder = []
|
76
|
+
found_assoc = nil
|
77
|
+
while remainder.unshift(segments.pop) && segments.size > 0 && !found_assoc do
|
78
|
+
if found_assoc = get_association(segments.join('_'), parent.active_record)
|
79
|
+
join = build_or_find_association(found_assoc.name, parent)
|
80
|
+
attribute = get_attribute(remainder.join('_'), join)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
attribute
|
67
85
|
end
|
68
|
-
|
69
|
-
def
|
70
|
-
|
86
|
+
|
87
|
+
def base_includes_association?(base, assoc)
|
88
|
+
if base._metasearch_include_associations.blank?
|
89
|
+
base.reflect_on_association(assoc.to_sym) unless base._metasearch_exclude_associations.include?(assoc.to_s)
|
90
|
+
else
|
91
|
+
base.reflect_on_association(assoc.to_sym) if base._metasearch_include_associations.include?(assoc.to_s)
|
92
|
+
end
|
71
93
|
end
|
72
|
-
|
73
|
-
def
|
74
|
-
|
94
|
+
|
95
|
+
def base_includes_attribute?(base, attribute)
|
96
|
+
if base._metasearch_include_attributes.blank?
|
97
|
+
base.column_names.detect(attribute.to_s) unless base._metasearch_exclude_attributes.include?(attribute.to_s)
|
98
|
+
else
|
99
|
+
base.column_names.detect(attribute.to_s) if base._metasearch_include_attributes.include?(attribute.to_s)
|
100
|
+
end
|
75
101
|
end
|
76
|
-
|
102
|
+
|
77
103
|
# Build the search with the given search options. Options are in the form of a hash
|
78
104
|
# with keys matching the names creted by the Builder's "wheres" as outlined in
|
79
105
|
# MetaSearch::Where
|
@@ -85,139 +111,166 @@ module MetaSearch
|
|
85
111
|
assign_attributes(opts)
|
86
112
|
self
|
87
113
|
end
|
88
|
-
|
114
|
+
|
89
115
|
private
|
90
|
-
|
116
|
+
|
91
117
|
def method_missing(method_id, *args, &block)
|
92
|
-
if
|
118
|
+
if method_id.to_s =~ /^meta_sort=?$/
|
119
|
+
build_sort_method
|
120
|
+
self.send(method_id, *args)
|
121
|
+
elsif match = method_id.to_s.match(/^(.*)\(([0-9]+).*\)$/) # Multiparameter reader
|
93
122
|
method_name, index = match.captures
|
94
123
|
vals = self.send(method_name)
|
95
124
|
vals.is_a?(Array) ? vals[index.to_i - 1] : nil
|
125
|
+
elsif match = matches_named_method(method_id)
|
126
|
+
build_named_method(match)
|
127
|
+
self.send(method_id, *args)
|
96
128
|
elsif match = matches_attribute_method(method_id)
|
97
|
-
|
98
|
-
|
129
|
+
attribute, predicate = match.captures
|
130
|
+
build_attribute_method(attribute, predicate)
|
99
131
|
self.send(preferred_method_name(method_id), *args)
|
100
|
-
elsif match = matches_where_method(method_id)
|
101
|
-
condition = match.captures.first
|
102
|
-
build_where_method(condition, Where.new(condition))
|
103
|
-
self.send(method_id, *args)
|
104
132
|
else
|
105
133
|
super
|
106
134
|
end
|
107
135
|
end
|
108
|
-
|
109
|
-
def
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
136
|
+
|
137
|
+
def build_sort_method
|
138
|
+
singleton_class.instance_eval do
|
139
|
+
define_method(:meta_sort) do
|
140
|
+
search_attributes['meta_sort']
|
141
|
+
end
|
142
|
+
|
143
|
+
define_method(:meta_sort=) do |val|
|
144
|
+
column, direction = val.split('.')
|
145
|
+
direction ||= 'asc'
|
146
|
+
if ['asc','desc'].include?(direction) && attribute = get_attribute(column)
|
147
|
+
search_attributes['meta_sort'] = val
|
148
|
+
@relation = @relation.order(attribute.send(direction).to_sql)
|
149
|
+
end
|
150
|
+
end
|
114
151
|
end
|
115
152
|
end
|
116
|
-
|
117
|
-
def
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
join = build_or_find_association(association)
|
128
|
-
self.send("add_#{type}_where", join.aliased_table_name, attribute, search_attributes["#{association}_#{attribute}_#{type}"])
|
153
|
+
|
154
|
+
def column_type(name, base = @base)
|
155
|
+
type = nil
|
156
|
+
if column = get_column(name, base)
|
157
|
+
type = column.type
|
158
|
+
elsif (segments = name.split(/_/)).size > 1
|
159
|
+
remainder = []
|
160
|
+
found_assoc = nil
|
161
|
+
while remainder.unshift(segments.pop) && segments.size > 0 && !found_assoc do
|
162
|
+
if found_assoc = get_association(segments.join('_'), base)
|
163
|
+
type = column_type(remainder.join('_'), found_assoc.klass)
|
129
164
|
end
|
130
165
|
end
|
131
166
|
end
|
167
|
+
type
|
132
168
|
end
|
133
|
-
|
134
|
-
def
|
135
|
-
|
136
|
-
|
137
|
-
|
169
|
+
|
170
|
+
def build_named_method(name)
|
171
|
+
meth = @base._metasearch_methods[name]
|
172
|
+
singleton_class.instance_eval do
|
173
|
+
define_method(name) do
|
174
|
+
search_attributes[name]
|
138
175
|
end
|
139
|
-
|
140
|
-
define_method("#{
|
141
|
-
search_attributes[
|
142
|
-
|
143
|
-
|
144
|
-
|
176
|
+
|
177
|
+
define_method("#{name}=") do |val|
|
178
|
+
search_attributes[name] = meth.cast_param(val)
|
179
|
+
if meth.validate(search_attributes[name])
|
180
|
+
return_value = meth.eval(@relation, search_attributes[name])
|
181
|
+
if return_value.is_a?(ActiveRecord::Relation)
|
182
|
+
@relation = return_value
|
183
|
+
else
|
184
|
+
raise NonRelationReturnedError, "Custom search methods must return an ActiveRecord::Relation. #{name} returned a #{return_value.class}"
|
185
|
+
end
|
145
186
|
end
|
146
187
|
end
|
147
188
|
end
|
148
189
|
end
|
149
|
-
|
150
|
-
def
|
151
|
-
|
152
|
-
define_method("
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
)
|
190
|
+
|
191
|
+
def build_attribute_method(attribute, predicate)
|
192
|
+
singleton_class.instance_eval do
|
193
|
+
define_method("#{attribute}_#{predicate}") do
|
194
|
+
search_attributes["#{attribute}_#{predicate}"]
|
195
|
+
end
|
196
|
+
|
197
|
+
define_method("#{attribute}_#{predicate}=") do |val|
|
198
|
+
search_attributes["#{attribute}_#{predicate}"] = cast_attributes(column_type(attribute), val)
|
199
|
+
where = Where.new(predicate)
|
200
|
+
if where.validate(search_attributes["#{attribute}_#{predicate}"])
|
201
|
+
arel_attribute = get_attribute(attribute)
|
202
|
+
@relation = where.eval(@relation, arel_attribute, search_attributes["#{attribute}_#{predicate}"])
|
203
|
+
end
|
158
204
|
end
|
159
205
|
end
|
160
206
|
end
|
161
|
-
|
162
|
-
def
|
163
|
-
par = params.map {|p| formatter.call(p)}
|
164
|
-
end
|
165
|
-
|
166
|
-
def build_or_find_association(association)
|
207
|
+
|
208
|
+
def build_or_find_association(association, parent = @join_dependency.join_base)
|
167
209
|
found_association = @join_dependency.join_associations.detect do |assoc|
|
168
|
-
assoc.reflection.name == association.to_sym
|
210
|
+
assoc.reflection.name == association.to_sym &&
|
211
|
+
assoc.parent == parent
|
169
212
|
end
|
170
213
|
unless found_association
|
171
|
-
@
|
172
|
-
@join_dependency.send(:build, association.to_sym, @join_dependency.join_base)
|
214
|
+
@join_dependency.send(:build, association, parent)
|
173
215
|
found_association = @join_dependency.join_associations.last
|
216
|
+
@relation = @relation.joins(found_association)
|
174
217
|
end
|
175
218
|
found_association
|
176
219
|
end
|
177
|
-
|
220
|
+
|
221
|
+
def enforce_join_depth_limit!
|
222
|
+
raise JoinDepthError, "Maximum join depth of #{MAX_JOIN_DEPTH} exceeded." if @join_dependency.join_associations.detect {|ja|
|
223
|
+
gauge_depth_of_join_association(ja) > MAX_JOIN_DEPTH
|
224
|
+
}
|
225
|
+
end
|
226
|
+
|
227
|
+
def gauge_depth_of_join_association(ja)
|
228
|
+
1 + (ja.respond_to?(:parent) ? gauge_depth_of_join_association(ja.parent) : 0)
|
229
|
+
end
|
230
|
+
|
231
|
+
def matches_named_method(name)
|
232
|
+
method_name = name.to_s.sub(/\=$/, '')
|
233
|
+
return method_name if @base._metasearch_methods.has_key?(method_name)
|
234
|
+
end
|
235
|
+
|
178
236
|
def matches_attribute_method(method_id)
|
179
237
|
method_name = preferred_method_name(method_id)
|
180
238
|
where = Where.new(method_id) rescue nil
|
181
239
|
return nil unless method_name && where
|
182
240
|
match = method_name.match("^(.*)_(#{where.name})=?$")
|
183
|
-
attribute,
|
184
|
-
if where.types.include?(
|
241
|
+
attribute, predicate = match.captures
|
242
|
+
if where.types.include?(column_type(attribute))
|
185
243
|
return match
|
186
|
-
elsif match = matches_association(method_name, attribute, condition)
|
187
|
-
association, attribute = match.captures
|
188
|
-
return match if where.types.include?(association_type_for(association, attribute))
|
189
244
|
end
|
190
245
|
nil
|
191
246
|
end
|
192
|
-
|
193
|
-
def
|
194
|
-
method_name = method_id
|
195
|
-
where = Where.new(
|
196
|
-
return nil unless where
|
197
|
-
where.
|
198
|
-
|
199
|
-
end
|
200
|
-
method_name
|
201
|
-
end
|
202
|
-
|
203
|
-
def matches_association(method_id, attribute, condition)
|
247
|
+
|
248
|
+
def matches_association_method(method_id)
|
249
|
+
method_name = preferred_method_name(method_id)
|
250
|
+
where = Where.new(method_id) rescue nil
|
251
|
+
return nil unless method_name && where
|
252
|
+
match = method_name.match("^(.*)_(#{where.name})=?$")
|
253
|
+
attribute, predicate = match.captures
|
204
254
|
self.included_associations.each do |association|
|
205
255
|
test_attribute = attribute.dup
|
206
256
|
if test_attribute.gsub!(/^#{association}_/, '') &&
|
207
|
-
match =
|
208
|
-
return match
|
257
|
+
match = method_name.match("^(#{association})_(#{test_attribute})_(#{predicate})=?$")
|
258
|
+
return match if where.types.include?(association_type_for(association, test_attribute))
|
209
259
|
end
|
210
260
|
end
|
211
261
|
nil
|
212
262
|
end
|
213
|
-
|
214
|
-
def
|
215
|
-
|
216
|
-
|
217
|
-
|
263
|
+
|
264
|
+
def preferred_method_name(method_id)
|
265
|
+
method_name = method_id.to_s
|
266
|
+
where = Where.new(method_name) rescue nil
|
267
|
+
return nil unless where
|
268
|
+
where.aliases.each do |a|
|
269
|
+
break if method_name.sub!(/#{a}(=?)$/, "#{where.name}\\1")
|
218
270
|
end
|
271
|
+
method_name
|
219
272
|
end
|
220
|
-
|
273
|
+
|
221
274
|
def assign_attributes(opts)
|
222
275
|
opts.each_pair do |k, v|
|
223
276
|
self.send("#{k}=", v)
|
@@ -228,17 +281,17 @@ module MetaSearch
|
|
228
281
|
column = self.column(attribute)
|
229
282
|
column.type if column
|
230
283
|
end
|
231
|
-
|
284
|
+
|
232
285
|
def class_for(attribute)
|
233
286
|
column = self.column(attribute)
|
234
287
|
column.klass if column
|
235
288
|
end
|
236
|
-
|
289
|
+
|
237
290
|
def association_type_for(association, attribute)
|
238
291
|
column = self.association_column(association, attribute)
|
239
292
|
column.type if column
|
240
293
|
end
|
241
|
-
|
294
|
+
|
242
295
|
def association_class_for(association, attribute)
|
243
296
|
column = self.association_column(association, attribute)
|
244
297
|
column.klass if column
|