meta_search 0.9.6 → 0.9.7
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/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
|