meta_search 0.3.0 → 0.5.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/.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
|