meta_search 0.9.6 → 0.9.7
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +13 -1
- data/README.rdoc +132 -14
- data/VERSION +1 -1
- data/lib/meta_search/builder.rb +69 -81
- data/lib/meta_search/exceptions.rb +13 -0
- data/lib/meta_search/join_dependency.rb +91 -0
- data/lib/meta_search/utility.rb +18 -10
- data/lib/meta_search/where.rb +7 -3
- data/lib/meta_search.rb +2 -0
- data/meta_search.gemspec +3 -2
- data/test/test_search.rb +66 -0
- data/test/test_view_helpers.rb +1 -1
- metadata +4 -3
data/CHANGELOG
CHANGED
@@ -1,5 +1,17 @@
|
|
1
|
+
Changes since 0.9.6 (2010-09-29):
|
2
|
+
* Support _or_-separated conditions. I'm not crazy about 'em, but it's
|
3
|
+
an oft-requested feature.
|
4
|
+
* Support search on polymorphic belongs_to associations. Uses the same
|
5
|
+
syntax users of Searchlogic are familiar with, association_classname_type.
|
6
|
+
For example: commentable_article_type_contains
|
7
|
+
* Join using left outer joins instead of inner joins. This lets you do
|
8
|
+
some interesting things like search for all articles with no comments via
|
9
|
+
comments_id_is_null.
|
10
|
+
* No longer define method on the metaclass - stick to standard method_missing
|
11
|
+
for both correctness and performance.
|
12
|
+
|
1
13
|
Changes since 0.9.5 (2010-09-28):
|
2
|
-
* Fix issue with formatters supplied
|
14
|
+
* Fix issue with formatters supplied as strings
|
3
15
|
|
4
16
|
Changes since 0.9.4 (2010-09-18):
|
5
17
|
* Rename check_boxes and collection_check_boxes to checks and
|
data/README.rdoc
CHANGED
@@ -17,7 +17,9 @@ In your controller:
|
|
17
17
|
|
18
18
|
def index
|
19
19
|
@search = Article.search(params[:search])
|
20
|
-
@articles = @search.all #
|
20
|
+
@articles = @search.all # load all matching records
|
21
|
+
# @articles = @search.relation # Retrieve the relation, to lazy-load in view
|
22
|
+
# @articles = @search.paginate(:page => params[:page]) # Who doesn't love will_paginate?
|
21
23
|
end
|
22
24
|
|
23
25
|
In your view:
|
@@ -31,7 +33,72 @@ In your view:
|
|
31
33
|
<%= f.submit %>
|
32
34
|
<% end %>
|
33
35
|
|
34
|
-
|
36
|
+
Options for the search method are documented at MetaSearch::Searches::Base.
|
37
|
+
|
38
|
+
== "Wheres", and what they're good for
|
39
|
+
|
40
|
+
Wheres are how MetaSearch does its magic. Wheres have a name (and possible aliases) which are
|
41
|
+
appended to your model and association attributes. When you instantiate a MetaSearch::Builder
|
42
|
+
against a model (manually or by calling your model's +search+ method) the builder responds to
|
43
|
+
methods named for your model's attributes and associations, suffixed by the name of the Where.
|
44
|
+
|
45
|
+
These are the default Wheres, broken down by the types of ActiveRecord columns they can search
|
46
|
+
against:
|
47
|
+
|
48
|
+
=== All data types
|
49
|
+
|
50
|
+
* _equals_ (alias: _eq_) - Just as it sounds.
|
51
|
+
* _does_not_equal_ (aliases: _ne_, _noteq_) - The opposite of equals, oddly enough.
|
52
|
+
* _in_ - Takes an array, matches on equality with any of the items in the array.
|
53
|
+
* _not_in_ (aliases: _ni_, _notin_) - Like above, but negated.
|
54
|
+
* _is_null_ - The column has an SQL NULL value.
|
55
|
+
* _is_not_null_ - The column contains anything but NULL.
|
56
|
+
|
57
|
+
=== Strings
|
58
|
+
|
59
|
+
* _contains_ (aliases: _like_, _matches_) - Substring match.
|
60
|
+
* _does_not_contain_ (aliases: _nlike_, _nmatches_) - Negative substring match.
|
61
|
+
* _starts_with_ (alias: _sw_) - Match strings beginning with the entered term.
|
62
|
+
* _does_not_start_with_ (alias: _dnsw_) - The opposite of above.
|
63
|
+
* _ends_with_ (alias: _ew_) - Match strings ending with the entered term.
|
64
|
+
* _does_not_end_with_ (alias: _dnew_) - Negative of above.
|
65
|
+
|
66
|
+
=== Numbers, dates, and times
|
67
|
+
|
68
|
+
* _greater_than_ (alias: _gt_) - Greater than.
|
69
|
+
* _greater_than_or_equal_to_ (aliases: _gte_, _gteq_) - Greater than or equal to.
|
70
|
+
* _less_than_ (alias: _lt_) - Less than.
|
71
|
+
* _less_than_or_equal_to_ (aliases: _lte_, _lteq_) - Less than or equal to.
|
72
|
+
|
73
|
+
=== Booleans
|
74
|
+
|
75
|
+
* _is_true_ - Is true. Useful for a checkbox like "only show admin users".
|
76
|
+
* _is_false_ - The complement of _is_true_.
|
77
|
+
|
78
|
+
=== Non-boolean data types
|
79
|
+
|
80
|
+
* _is_present_ - As with _is_true_, useful with a checkbox. Not NULL or the empty string.
|
81
|
+
* _is_blank_ - Returns records with a value of NULL or the empty string in the column.
|
82
|
+
|
83
|
+
So, given a model like this...
|
84
|
+
|
85
|
+
class Article < ActiveRecord::Base
|
86
|
+
belongs_to :author
|
87
|
+
has_many :comments
|
88
|
+
has_many :moderations, :through => :comments
|
89
|
+
end
|
90
|
+
|
91
|
+
...you might end up with attributes like <tt>title_contains</tt>,
|
92
|
+
<tt>comments_title_starts_with</tt>, <tt>moderations_value_less_than</tt>,
|
93
|
+
<tt>author_name_equals</tt>, and so on.
|
94
|
+
|
95
|
+
Additionally, all of the above predicate types also have an _any and _all version, which
|
96
|
+
expects an array of the corresponding parameter type, and requires any or all of the
|
97
|
+
parameters to be a match, respectively. So:
|
98
|
+
|
99
|
+
Article.search :author_name_starts_with_any => ['Jim', 'Bob', 'Fred']
|
100
|
+
|
101
|
+
will match articles authored by Jimmy, Bobby, or Freddy, but not Winifred.
|
35
102
|
|
36
103
|
== Advanced usage
|
37
104
|
|
@@ -48,6 +115,32 @@ Or, you can build up any relation you like and call the search method on that ob
|
|
48
115
|
@projects_with_awesome_users_search =
|
49
116
|
Project.joins(:user).where(:users => {:awesome => true}).search(params[:search])
|
50
117
|
|
118
|
+
=== ORed conditions
|
119
|
+
|
120
|
+
If you'd like to match on one of several possible columns, you can do this:
|
121
|
+
|
122
|
+
<%= f.text_field :title_or_description_contains %>
|
123
|
+
<%= f.text_field :title_or_author_name_starts_with %>
|
124
|
+
|
125
|
+
Caveats:
|
126
|
+
|
127
|
+
* Only one match type is supported. You <b>can't</b> do
|
128
|
+
<tt>title_matches_or_description_starts_with</tt> for instance.
|
129
|
+
* If you're matching across associations, remember that the associated table will be
|
130
|
+
INNER JOINed, therefore limiting results to those that at least have a corresponding
|
131
|
+
record in the associated table.
|
132
|
+
|
133
|
+
=== Compound conditions (any/all)
|
134
|
+
|
135
|
+
All Where types automatically get an "any" and "all" variant. This has the same name and
|
136
|
+
aliases as the original, but is suffixed with _any and _all, for an "OR" or "AND" search,
|
137
|
+
respectively. So, if you want to provide the user with 5 different search boxes to enter
|
138
|
+
possible article titles:
|
139
|
+
|
140
|
+
<%= f.multiparameter_field :title_contains_any,
|
141
|
+
*5.times.inject([]) {|a, b| a << {:field_type => :text_field}} +
|
142
|
+
[:size => 10] %>
|
143
|
+
|
51
144
|
=== Multi-level associations
|
52
145
|
|
53
146
|
MetaSearch will allow you to traverse your associations in one form, generating the
|
@@ -76,9 +169,45 @@ This means that while starting from a Company model, as above, you could do
|
|
76
169
|
Company -> :developers -> :notes -> :developer -> :company, which has gotten you right
|
77
170
|
back where you started, but "travels" through 5 models total.
|
78
171
|
|
172
|
+
In the case of polymorphic belongs_to associations, things work a bit differently. Let's say
|
173
|
+
you have the following models:
|
174
|
+
|
175
|
+
class Article < ActiveRecord::Base
|
176
|
+
has_many :comments, :as => :commentable
|
177
|
+
end
|
178
|
+
|
179
|
+
class Post < ActiveRecord::Base
|
180
|
+
has_many :comments, :as => :commentable
|
181
|
+
end
|
182
|
+
|
183
|
+
class Comment < ActiveRecord::Base
|
184
|
+
belongs_to :commentable, :polymorphic => true
|
185
|
+
validates_presence_of :body
|
186
|
+
end
|
187
|
+
|
188
|
+
Your first instinct might be to set up a text field for :commentable_body_contains, but
|
189
|
+
you can't do this. MetaSearch would have no way to know which class lies on the other side
|
190
|
+
of the polymorphic association, so it wouldn't be able to join the correct tables.
|
191
|
+
|
192
|
+
Instead, you'll follow a convention Searchlogic users are already familiar with, using the
|
193
|
+
name of the polymorphic association, then the underscored class name (AwesomeClass becomes
|
194
|
+
awesome_class), then the delimiter "type", to tell MetaSearch anything that follows is an
|
195
|
+
attribute name. For example:
|
196
|
+
|
197
|
+
<%= f.text_field :commentable_article_type_body_contains %>
|
198
|
+
|
199
|
+
If you'd like to match on multiple types of polymorphic associations, you can join them
|
200
|
+
with \_or_, just like any other conditions:
|
201
|
+
|
202
|
+
<%= f.text_field :commentable_article_type_body_or_commentable_post_type_body_contains %>
|
203
|
+
|
204
|
+
It's not pretty, but it works. Alternately, consider creating a custom search method as
|
205
|
+
described below to save yourself some typing if you're creating a lot of these types of
|
206
|
+
search fields.
|
207
|
+
|
79
208
|
=== Adding a new Where
|
80
209
|
|
81
|
-
If none of the built-in search criteria work for you, you can add
|
210
|
+
If none of the built-in search criteria work for you, you can add new Wheres. To do so,
|
82
211
|
create an initializer (<tt>/config/initializers/meta_search.rb</tt>, for instance) and add lines
|
83
212
|
like:
|
84
213
|
|
@@ -144,17 +273,6 @@ MetaSearch adds a helper for this:
|
|
144
273
|
lets you sandwich a list of fields, each in hash format, between the attribute and the usual
|
145
274
|
options hash. See MetaSearch::Helpers::FormBuilder for more info.
|
146
275
|
|
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
|
-
|
158
276
|
=== check_boxes and collection_check_boxes
|
159
277
|
|
160
278
|
If you need to get an array into your where, and you don't care about parameter order,
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.9.
|
1
|
+
0.9.7
|
data/lib/meta_search/builder.rb
CHANGED
@@ -4,12 +4,6 @@ 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
|
-
|
13
7
|
# Builder is the workhorse of MetaSearch -- it is the class that handles dynamically generating
|
14
8
|
# methods based on a supplied model, and is what gets instantiated when you call your model's search
|
15
9
|
# method. Builder doesn't generate any methods until they're needed, using method_missing to compare
|
@@ -77,8 +71,19 @@ module MetaSearch
|
|
77
71
|
found_assoc = nil
|
78
72
|
while remainder.unshift(segments.pop) && segments.size > 0 && !found_assoc do
|
79
73
|
if found_assoc = get_association(segments.join('_'), parent.active_record)
|
80
|
-
|
81
|
-
|
74
|
+
if found_assoc.options[:polymorphic]
|
75
|
+
unless delimiter = remainder.index('type')
|
76
|
+
raise PolymorphicAssociationMissingTypeError, "Polymorphic association specified without a type"
|
77
|
+
end
|
78
|
+
polymorphic_class, attribute_name = remainder[0...delimiter].join('_'),
|
79
|
+
remainder[delimiter + 1...remainder.size].join('_')
|
80
|
+
polymorphic_class = polymorphic_class.classify.constantize
|
81
|
+
join = build_or_find_association(found_assoc.name, parent, polymorphic_class)
|
82
|
+
attribute = get_attribute(attribute_name, join)
|
83
|
+
else
|
84
|
+
join = build_or_find_association(found_assoc.name, parent, found_assoc.klass)
|
85
|
+
attribute = get_attribute(remainder.join('_'), join)
|
86
|
+
end
|
82
87
|
end
|
83
88
|
end
|
84
89
|
end
|
@@ -97,11 +102,11 @@ module MetaSearch
|
|
97
102
|
self
|
98
103
|
end
|
99
104
|
|
100
|
-
def respond_to?(
|
105
|
+
def respond_to?(method_id, include_private = false)
|
101
106
|
return true if super # Hopefully we've already defined the method.
|
102
107
|
|
103
108
|
# Curses! Looks like we'll need to do this the hard way.
|
104
|
-
method_name =
|
109
|
+
method_name = method_id.to_s
|
105
110
|
if RELATION_METHODS.map(&:to_s).include?(method_name)
|
106
111
|
true
|
107
112
|
elsif method_name.match(/^meta_sort=?$/)
|
@@ -119,20 +124,18 @@ module MetaSearch
|
|
119
124
|
private
|
120
125
|
|
121
126
|
def method_missing(method_id, *args, &block)
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
elsif match =
|
127
|
+
method_name = method_id.to_s
|
128
|
+
if method_name =~ /^meta_sort=?$/
|
129
|
+
method_name =~ /=$/ ? set_sort(args.first) : get_sort
|
130
|
+
elsif match = method_name.match(/^(.*)\(([0-9]+).*\)$/) # Multiparameter reader
|
126
131
|
method_name, index = match.captures
|
127
132
|
vals = self.send(method_name)
|
128
133
|
vals.is_a?(Array) ? vals[index.to_i - 1] : nil
|
129
|
-
elsif match = matches_named_method(
|
130
|
-
|
131
|
-
self.send(method_id, *args)
|
134
|
+
elsif match = matches_named_method(method_name)
|
135
|
+
method_name =~ /=$/ ? set_named_method_value(match, args.first) : get_named_method_value(match)
|
132
136
|
elsif match = matches_attribute_method(method_id)
|
133
137
|
attribute, predicate = match.captures
|
134
|
-
|
135
|
-
self.send(preferred_method_name(method_id), *args)
|
138
|
+
method_name =~ /=$/ ? set_attribute_method_value(attribute, predicate, args.first) : get_attribute_method_value(attribute, predicate)
|
136
139
|
else
|
137
140
|
super
|
138
141
|
end
|
@@ -177,20 +180,16 @@ module MetaSearch
|
|
177
180
|
o.is_a?(Array) && o.all?{|obj| obj.is_a?(String)}
|
178
181
|
end
|
179
182
|
|
180
|
-
def
|
181
|
-
|
182
|
-
|
183
|
-
search_attributes['meta_sort']
|
184
|
-
end
|
183
|
+
def get_sort
|
184
|
+
search_attributes['meta_sort']
|
185
|
+
end
|
185
186
|
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
end
|
193
|
-
end
|
187
|
+
def set_sort(val)
|
188
|
+
column, direction = val.split('.')
|
189
|
+
direction ||= 'asc'
|
190
|
+
if ['asc','desc'].include?(direction) && attribute = get_attribute(column)
|
191
|
+
search_attributes['meta_sort'] = val
|
192
|
+
@relation = @relation.order(attribute.send(direction).to_sql)
|
194
193
|
end
|
195
194
|
end
|
196
195
|
|
@@ -203,58 +202,62 @@ module MetaSearch
|
|
203
202
|
found_assoc = nil
|
204
203
|
while remainder.unshift(segments.pop) && segments.size > 0 && !found_assoc do
|
205
204
|
if found_assoc = get_association(segments.join('_'), base)
|
206
|
-
|
205
|
+
if found_assoc.options[:polymorphic]
|
206
|
+
unless delimiter = remainder.index('type')
|
207
|
+
raise PolymorphicAssociationMissingTypeError, "Polymorphic association specified without a type"
|
208
|
+
end
|
209
|
+
polymorphic_class, attribute_name = remainder[0...delimiter].join('_'),
|
210
|
+
remainder[delimiter + 1...remainder.size].join('_')
|
211
|
+
polymorphic_class = polymorphic_class.classify.constantize
|
212
|
+
type = column_type(attribute_name, polymorphic_class)
|
213
|
+
else
|
214
|
+
type = column_type(remainder.join('_'), found_assoc.klass)
|
215
|
+
end
|
207
216
|
end
|
208
217
|
end
|
209
218
|
end
|
210
219
|
type
|
211
220
|
end
|
212
221
|
|
213
|
-
def
|
214
|
-
|
215
|
-
|
216
|
-
define_method(name) do
|
217
|
-
search_attributes[name]
|
218
|
-
end
|
222
|
+
def get_named_method_value(name)
|
223
|
+
search_attributes[name]
|
224
|
+
end
|
219
225
|
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
end
|
226
|
+
def set_named_method_value(name, val)
|
227
|
+
meth = @base._metasearch_methods[name]
|
228
|
+
search_attributes[name] = meth.cast_param(val)
|
229
|
+
if meth.validate(search_attributes[name])
|
230
|
+
return_value = meth.evaluate(@relation, search_attributes[name])
|
231
|
+
if return_value.is_a?(ActiveRecord::Relation)
|
232
|
+
@relation = return_value
|
233
|
+
else
|
234
|
+
raise NonRelationReturnedError, "Custom search methods must return an ActiveRecord::Relation. #{name} returned a #{return_value.class}"
|
230
235
|
end
|
231
236
|
end
|
232
237
|
end
|
233
238
|
|
234
|
-
def
|
235
|
-
|
236
|
-
|
237
|
-
search_attributes["#{attribute}_#{predicate}"]
|
238
|
-
end
|
239
|
+
def get_attribute_method_value(attribute, predicate)
|
240
|
+
search_attributes["#{attribute}_#{predicate}"]
|
241
|
+
end
|
239
242
|
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
end
|
243
|
+
def set_attribute_method_value(attribute, predicate, val)
|
244
|
+
where = Where.new(predicate)
|
245
|
+
attributes = attribute.split(/_or_/)
|
246
|
+
search_attributes["#{attribute}_#{predicate}"] = cast_attributes(where.cast || column_type(attributes.first), val)
|
247
|
+
if where.validate(search_attributes["#{attribute}_#{predicate}"])
|
248
|
+
arel_attributes = attributes.map {|a| get_attribute(a)}
|
249
|
+
@relation = where.evaluate(@relation, arel_attributes, search_attributes["#{attribute}_#{predicate}"])
|
248
250
|
end
|
249
251
|
end
|
250
252
|
|
251
|
-
def build_or_find_association(association, parent = @join_dependency.join_base)
|
253
|
+
def build_or_find_association(association, parent = @join_dependency.join_base, klass = nil)
|
252
254
|
found_association = @join_dependency.join_associations.detect do |assoc|
|
253
255
|
assoc.reflection.name == association.to_sym &&
|
256
|
+
assoc.reflection.klass == klass &&
|
254
257
|
assoc.parent == parent
|
255
258
|
end
|
256
259
|
unless found_association
|
257
|
-
@join_dependency.send(:build, association, parent)
|
260
|
+
@join_dependency.send(:build, association, parent, Arel::OuterJoin, klass)
|
258
261
|
found_association = @join_dependency.join_associations.last
|
259
262
|
@relation = @relation.joins(found_association)
|
260
263
|
end
|
@@ -282,28 +285,13 @@ module MetaSearch
|
|
282
285
|
return nil unless method_name && where
|
283
286
|
match = method_name.match("^(.*)_(#{where.name})=?$")
|
284
287
|
attribute, predicate = match.captures
|
285
|
-
|
288
|
+
attributes = attribute.split(/_or_/)
|
289
|
+
if attributes.all? {|a| where.types.include?(column_type(a))}
|
286
290
|
return match
|
287
291
|
end
|
288
292
|
nil
|
289
293
|
end
|
290
294
|
|
291
|
-
def matches_association_method(method_id)
|
292
|
-
method_name = preferred_method_name(method_id)
|
293
|
-
where = Where.new(method_id) rescue nil
|
294
|
-
return nil unless method_name && where
|
295
|
-
match = method_name.match("^(.*)_(#{where.name})=?$")
|
296
|
-
attribute, predicate = match.captures
|
297
|
-
self.included_associations.each do |association|
|
298
|
-
test_attribute = attribute.dup
|
299
|
-
if test_attribute.gsub!(/^#{association}_/, '') &&
|
300
|
-
match = method_name.match("^(#{association})_(#{test_attribute})_(#{predicate})=?$")
|
301
|
-
return match if where.types.include?(association_type_for(association, test_attribute))
|
302
|
-
end
|
303
|
-
end
|
304
|
-
nil
|
305
|
-
end
|
306
|
-
|
307
295
|
def preferred_method_name(method_id)
|
308
296
|
method_name = method_id.to_s
|
309
297
|
where = Where.new(method_name) rescue nil
|
@@ -1,4 +1,17 @@
|
|
1
1
|
module MetaSearch
|
2
|
+
# Raised when type casting for a column fails.
|
2
3
|
class TypeCastError < StandardError; end
|
4
|
+
|
5
|
+
# Raised if you don't return a relation from a custom search method.
|
3
6
|
class NonRelationReturnedError < StandardError; end
|
7
|
+
|
8
|
+
# Raised if you try to access a relation that's joining too many tables to itself.
|
9
|
+
# This is designed to prevent a malicious user from accessing something like
|
10
|
+
# :developers_company_developers_company_developers_company_developers_company_...,
|
11
|
+
# resulting in a query that could cause issues for your database server.
|
12
|
+
class JoinDepthError < StandardError; end
|
13
|
+
|
14
|
+
# Raised if you try to search on a polymorphic belongs_to association without specifying
|
15
|
+
# its type.
|
16
|
+
class PolymorphicAssociationMissingTypeError < StandardError; end
|
4
17
|
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module MetaSearch
|
2
|
+
|
3
|
+
module JoinDependency
|
4
|
+
|
5
|
+
def self.included(base)
|
6
|
+
base.class_eval do
|
7
|
+
alias_method_chain :build, :metasearch
|
8
|
+
alias_method_chain :graft, :metasearch
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def graft_with_metasearch(*associations)
|
13
|
+
associations.each do |association|
|
14
|
+
join_associations.detect {|a| association == a} ||
|
15
|
+
build(association.reflection.name, association.find_parent_in(self) || join_base, association.join_class, association.reflection.klass)
|
16
|
+
end
|
17
|
+
self
|
18
|
+
end
|
19
|
+
|
20
|
+
protected
|
21
|
+
|
22
|
+
def build_with_metasearch(association, parent = nil, join_class = Arel::InnerJoin, polymorphic_class = nil)
|
23
|
+
parent ||= @joins.last
|
24
|
+
case association
|
25
|
+
when Symbol, String
|
26
|
+
reflection = parent.reflections[association.to_s.intern] or
|
27
|
+
raise ConfigurationError, "Association named '#{ association }' was not found; perhaps you misspelled it?"
|
28
|
+
if reflection.options[:polymorphic]
|
29
|
+
@reflections << reflection
|
30
|
+
@joins << build_polymorphic_join_association(reflection, parent, polymorphic_class).with_join_class(join_class)
|
31
|
+
else
|
32
|
+
@reflections << reflection
|
33
|
+
@joins << build_join_association(reflection, parent).with_join_class(join_class)
|
34
|
+
end
|
35
|
+
else
|
36
|
+
build_without_metasearch(association, parent, join_class)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def build_polymorphic_join_association(reflection, parent, klass)
|
41
|
+
PolymorphicJoinAssociation.new(reflection, self, klass, parent)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class PolymorphicJoinAssociation < ActiveRecord::Associations::ClassMethods::JoinDependency::JoinAssociation
|
46
|
+
|
47
|
+
def initialize(reflection, join_dependency, polymorphic_class, parent = nil)
|
48
|
+
reflection.check_validity!
|
49
|
+
@active_record = polymorphic_class
|
50
|
+
@cached_record = {}
|
51
|
+
@join_dependency = join_dependency
|
52
|
+
@parent = parent || join_dependency.join_base
|
53
|
+
@reflection = reflection.clone
|
54
|
+
@reflection.instance_eval "def klass; #{polymorphic_class} end"
|
55
|
+
@aliased_prefix = "t#{ join_dependency.joins.size }"
|
56
|
+
@parent_table_name = @parent.active_record.table_name
|
57
|
+
@aliased_table_name = aliased_table_name_for(table_name)
|
58
|
+
@join = nil
|
59
|
+
@join_class = Arel::InnerJoin
|
60
|
+
end
|
61
|
+
|
62
|
+
def ==(other)
|
63
|
+
other.class == self.class &&
|
64
|
+
other.reflection == reflection &&
|
65
|
+
other.active_record == active_record &&
|
66
|
+
other.parent == parent
|
67
|
+
end
|
68
|
+
|
69
|
+
def association_join
|
70
|
+
return @join if @join
|
71
|
+
|
72
|
+
aliased_table = Arel::Table.new(table_name, :as => @aliased_table_name, :engine => arel_engine)
|
73
|
+
parent_table = Arel::Table.new(parent.table_name, :as => parent.aliased_table_name, :engine => arel_engine)
|
74
|
+
|
75
|
+
@join = [
|
76
|
+
aliased_table[options[:primary_key] || reflection.klass.primary_key].eq(parent_table[options[:foreign_key] || reflection.primary_key_name]),
|
77
|
+
parent_table[options[:foreign_type]].eq(active_record.base_class.name)
|
78
|
+
]
|
79
|
+
|
80
|
+
unless klass.descends_from_active_record?
|
81
|
+
sti_column = aliased_table[klass.inheritance_column]
|
82
|
+
sti_condition = sti_column.eq(klass.sti_name)
|
83
|
+
klass.descendants.each {|subclass| sti_condition = sti_condition.or(sti_column.eq(subclass.sti_name)) }
|
84
|
+
|
85
|
+
@join << sti_condition
|
86
|
+
end
|
87
|
+
|
88
|
+
@join
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
data/lib/meta_search/utility.rb
CHANGED
@@ -2,6 +2,10 @@ require 'meta_search/exceptions'
|
|
2
2
|
|
3
3
|
module MetaSearch
|
4
4
|
module Utility #:nodoc:
|
5
|
+
|
6
|
+
TRUE_VALUES = [true, 1, '1', 't', 'T', 'true', 'TRUE'].to_set
|
7
|
+
FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE'].to_set
|
8
|
+
|
5
9
|
private
|
6
10
|
|
7
11
|
def array_of_arrays?(vals)
|
@@ -47,13 +51,25 @@ module MetaSearch
|
|
47
51
|
val.in_time_zone rescue nil
|
48
52
|
end
|
49
53
|
when *BOOLEANS
|
50
|
-
|
54
|
+
if val.is_a?(String) && value.blank?
|
55
|
+
nil
|
56
|
+
else
|
57
|
+
TRUE_VALUES.include?(val)
|
58
|
+
end
|
51
59
|
when :integer
|
52
60
|
val.blank? ? nil : val.to_i
|
53
61
|
when :float
|
54
62
|
val.blank? ? nil : val.to_f
|
55
63
|
when :decimal
|
56
|
-
val.blank?
|
64
|
+
if val.blank?
|
65
|
+
nil
|
66
|
+
elsif val.class == BigDecimal
|
67
|
+
val
|
68
|
+
elsif val.respond_to?(:to_d)
|
69
|
+
val.to_d
|
70
|
+
else
|
71
|
+
val.to_s.to_d
|
72
|
+
end
|
57
73
|
else
|
58
74
|
raise TypeCastError, "Unable to cast columns of type #{type}"
|
59
75
|
end
|
@@ -76,13 +92,5 @@ module MetaSearch
|
|
76
92
|
end
|
77
93
|
opts
|
78
94
|
end
|
79
|
-
|
80
|
-
def quote_table_name(name)
|
81
|
-
ActiveRecord::Base.connection.quote_table_name(name)
|
82
|
-
end
|
83
|
-
|
84
|
-
def quote_column_name(name)
|
85
|
-
ActiveRecord::Base.connection.quote_column_name(name)
|
86
|
-
end
|
87
95
|
end
|
88
96
|
end
|
data/lib/meta_search/where.rb
CHANGED
@@ -15,6 +15,8 @@ module MetaSearch
|
|
15
15
|
# * _does_not_equal_ (aliases: _ne_, _noteq_) - The opposite of equals, oddly enough.
|
16
16
|
# * _in_ - Takes an array, matches on equality with any of the items in the array.
|
17
17
|
# * _not_in_ (aliases: _ni_, _notin_) - Like above, but negated.
|
18
|
+
# * _is_null_ - The column has an SQL NULL value.
|
19
|
+
# * _is_not_null_ - The column contains anything but NULL.
|
18
20
|
#
|
19
21
|
# === Strings
|
20
22
|
#
|
@@ -97,12 +99,14 @@ module MetaSearch
|
|
97
99
|
end
|
98
100
|
|
99
101
|
# Evaluate the Where for the given relation, attribute, and parameter(s)
|
100
|
-
def evaluate(relation,
|
102
|
+
def evaluate(relation, attributes, param)
|
101
103
|
if splat_param?
|
102
|
-
|
104
|
+
conditions = attributes.map {|a| a.send(predicate, *format_param(param))}
|
103
105
|
else
|
104
|
-
|
106
|
+
conditions = attributes.map {|a| a.send(predicate, format_param(param))}
|
105
107
|
end
|
108
|
+
|
109
|
+
relation.where(conditions.inject(nil) {|memo, c| memo ? memo.or(c) : c})
|
106
110
|
end
|
107
111
|
|
108
112
|
class << self
|
data/lib/meta_search.rb
CHANGED
@@ -41,9 +41,11 @@ require 'active_record'
|
|
41
41
|
require 'active_support'
|
42
42
|
require 'action_view'
|
43
43
|
require 'action_controller'
|
44
|
+
require 'meta_search/join_dependency'
|
44
45
|
require 'meta_search/searches/active_record'
|
45
46
|
require 'meta_search/helpers'
|
46
47
|
|
48
|
+
ActiveRecord::Associations::ClassMethods::JoinDependency.send(:include, MetaSearch::JoinDependency)
|
47
49
|
ActiveRecord::Base.send(:include, MetaSearch::Searches::ActiveRecord)
|
48
50
|
ActionView::Helpers::FormBuilder.send(:include, MetaSearch::Helpers::FormBuilder)
|
49
51
|
ActionController::Base.helper(MetaSearch::Helpers::UrlHelper)
|
data/meta_search.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{meta_search}
|
8
|
-
s.version = "0.9.
|
8
|
+
s.version = "0.9.7"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Ernie Miller"]
|
12
|
-
s.date = %q{2010-
|
12
|
+
s.date = %q{2010-10-11}
|
13
13
|
s.description = %q{
|
14
14
|
Allows simple search forms to be created against an AR3 model
|
15
15
|
and its associations, has useful view helpers for sort links
|
@@ -37,6 +37,7 @@ Gem::Specification.new do |s|
|
|
37
37
|
"lib/meta_search/helpers/form_builder.rb",
|
38
38
|
"lib/meta_search/helpers/form_helper.rb",
|
39
39
|
"lib/meta_search/helpers/url_helper.rb",
|
40
|
+
"lib/meta_search/join_dependency.rb",
|
40
41
|
"lib/meta_search/method.rb",
|
41
42
|
"lib/meta_search/model_compatibility.rb",
|
42
43
|
"lib/meta_search/searches/active_record.rb",
|
data/test/test_search.rb
CHANGED
@@ -308,6 +308,17 @@ class TestSearch < Test::Unit::TestCase
|
|
308
308
|
end
|
309
309
|
end
|
310
310
|
|
311
|
+
context "where name or company name starts with m" do
|
312
|
+
setup do
|
313
|
+
@s.name_or_company_name_starts_with = "m"
|
314
|
+
end
|
315
|
+
|
316
|
+
should "return Michael Bolton and all employees of Mission Data" do
|
317
|
+
assert_equal @s.all, Developer.where(:name => 'Michael Bolton').all +
|
318
|
+
Company.where(:name => 'Mission Data').first.developers
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
311
322
|
context "where name ends with Miller" do
|
312
323
|
setup do
|
313
324
|
@s.name_ends_with = 'Miller'
|
@@ -400,6 +411,24 @@ class TestSearch < Test::Unit::TestCase
|
|
400
411
|
assert_equal 0, @s.all.size
|
401
412
|
end
|
402
413
|
end
|
414
|
+
|
415
|
+
context "where developer is named Ernie Miller by polymorphic belongs_to against an association" do
|
416
|
+
setup do
|
417
|
+
@s.notes_notable_developer_type_name_equals = "Ernie Miller"
|
418
|
+
end
|
419
|
+
|
420
|
+
should "return one result" do
|
421
|
+
assert_equal 1, @s.all.size
|
422
|
+
end
|
423
|
+
|
424
|
+
should "return a developer named Ernie Miller" do
|
425
|
+
assert_contains @s.all, Developer.where(:name => 'Ernie Miller').first
|
426
|
+
end
|
427
|
+
|
428
|
+
should "not return a developer named Herb Myers" do
|
429
|
+
assert_does_not_contain @s.all, Developer.where(:name => "Herb Myers").first
|
430
|
+
end
|
431
|
+
end
|
403
432
|
end
|
404
433
|
end
|
405
434
|
|
@@ -729,6 +758,43 @@ class TestSearch < Test::Unit::TestCase
|
|
729
758
|
assert_equal 0, @s.all.select {|r| r.name = nil}.size
|
730
759
|
end
|
731
760
|
end
|
761
|
+
|
762
|
+
context "where notes_id is null" do
|
763
|
+
setup do
|
764
|
+
@s.notes_id_is_null = true
|
765
|
+
end
|
766
|
+
|
767
|
+
should "return 2 results" do
|
768
|
+
assert_equal 2, @s.all.size
|
769
|
+
end
|
770
|
+
|
771
|
+
should "contain no results with notes" do
|
772
|
+
assert_equal 0, @s.all.select {|r| r.notes.size > 0}.size
|
773
|
+
end
|
774
|
+
end
|
775
|
+
end
|
776
|
+
end
|
777
|
+
|
778
|
+
[{:name => 'Note', :object => Note},
|
779
|
+
{:name => 'Note as a Relation', :object => Note.scoped}].each do |object|
|
780
|
+
context_a_search_against object[:name], object[:object] do
|
781
|
+
should "allow search on polymorphic belongs_to associations" do
|
782
|
+
@s.notable_project_type_name_contains = 'MetaSearch'
|
783
|
+
assert_equal Project.find_by_name('MetaSearch Development').notes, @s.all
|
784
|
+
end
|
785
|
+
|
786
|
+
should "allow search on multiple polymorphic belongs_to associations" do
|
787
|
+
@s.notable_project_type_name_or_notable_developer_type_name_starts_with = 'M'
|
788
|
+
assert_equal Project.find_by_name('MetaSearch Development').notes +
|
789
|
+
Developer.find_by_name('Michael Bolton').notes,
|
790
|
+
@s.all
|
791
|
+
end
|
792
|
+
|
793
|
+
should "raise an error when attempting to search against polymorphic belongs_to association without a type" do
|
794
|
+
assert_raises ::MetaSearch::PolymorphicAssociationMissingTypeError do
|
795
|
+
@s.notable_name_contains = 'MetaSearch'
|
796
|
+
end
|
797
|
+
end
|
732
798
|
end
|
733
799
|
end
|
734
800
|
end
|
data/test/test_view_helpers.rb
CHANGED
metadata
CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
|
|
5
5
|
segments:
|
6
6
|
- 0
|
7
7
|
- 9
|
8
|
-
-
|
9
|
-
version: 0.9.
|
8
|
+
- 7
|
9
|
+
version: 0.9.7
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Ernie Miller
|
@@ -14,7 +14,7 @@ autorequire:
|
|
14
14
|
bindir: bin
|
15
15
|
cert_chain: []
|
16
16
|
|
17
|
-
date: 2010-
|
17
|
+
date: 2010-10-11 00:00:00 -04:00
|
18
18
|
default_executable:
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
@@ -116,6 +116,7 @@ files:
|
|
116
116
|
- lib/meta_search/helpers/form_builder.rb
|
117
117
|
- lib/meta_search/helpers/form_helper.rb
|
118
118
|
- lib/meta_search/helpers/url_helper.rb
|
119
|
+
- lib/meta_search/join_dependency.rb
|
119
120
|
- lib/meta_search/method.rb
|
120
121
|
- lib/meta_search/model_compatibility.rb
|
121
122
|
- lib/meta_search/searches/active_record.rb
|