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 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 s strings
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 # or @search.relation to lazy load in view
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
- The default Where types are listed at MetaSearch::Where. Options for the search method are documented at MetaSearch::Searches::Base.
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 a new Where (or 5). To do so,
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.6
1
+ 0.9.7
@@ -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
- join = build_or_find_association(found_assoc.name, parent)
81
- attribute = get_attribute(remainder.join('_'), join)
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?(method_name, include_private = false)
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 = method_name.to_s
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
- if method_id.to_s =~ /^meta_sort=?$/
123
- build_sort_method
124
- self.send(method_id, *args)
125
- elsif match = method_id.to_s.match(/^(.*)\(([0-9]+).*\)$/) # Multiparameter reader
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(method_id)
130
- build_named_method(match)
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
- build_attribute_method(attribute, predicate)
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 build_sort_method
181
- singleton_class.instance_eval do
182
- define_method(:meta_sort) do
183
- search_attributes['meta_sort']
184
- end
183
+ def get_sort
184
+ search_attributes['meta_sort']
185
+ end
185
186
 
186
- define_method(:meta_sort=) do |val|
187
- column, direction = val.split('.')
188
- direction ||= 'asc'
189
- if ['asc','desc'].include?(direction) && attribute = get_attribute(column)
190
- search_attributes['meta_sort'] = val
191
- @relation = @relation.order(attribute.send(direction).to_sql)
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
- type = column_type(remainder.join('_'), found_assoc.klass)
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 build_named_method(name)
214
- meth = @base._metasearch_methods[name]
215
- singleton_class.instance_eval do
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
- define_method("#{name}=") do |val|
221
- search_attributes[name] = meth.cast_param(val)
222
- if meth.validate(search_attributes[name])
223
- return_value = meth.evaluate(@relation, search_attributes[name])
224
- if return_value.is_a?(ActiveRecord::Relation)
225
- @relation = return_value
226
- else
227
- raise NonRelationReturnedError, "Custom search methods must return an ActiveRecord::Relation. #{name} returned a #{return_value.class}"
228
- end
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 build_attribute_method(attribute, predicate)
235
- singleton_class.instance_eval do
236
- define_method("#{attribute}_#{predicate}") do
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
- define_method("#{attribute}_#{predicate}=") do |val|
241
- where = Where.new(predicate)
242
- search_attributes["#{attribute}_#{predicate}"] = cast_attributes(where.cast || column_type(attribute), val)
243
- if where.validate(search_attributes["#{attribute}_#{predicate}"])
244
- arel_attribute = get_attribute(attribute)
245
- @relation = where.evaluate(@relation, arel_attribute, search_attributes["#{attribute}_#{predicate}"])
246
- end
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
- if where.types.include?(column_type(attribute))
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
@@ -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
- ActiveRecord::ConnectionAdapters::Column.value_to_boolean(val)
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? ? nil : ActiveRecord::ConnectionAdapters::Column.value_to_decimal(val)
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
@@ -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, attribute, param)
102
+ def evaluate(relation, attributes, param)
101
103
  if splat_param?
102
- relation.where(attribute.send(predicate, *format_param(param)))
104
+ conditions = attributes.map {|a| a.send(predicate, *format_param(param))}
103
105
  else
104
- relation.where(attribute.send(predicate, format_param(param)))
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.6"
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-09-29}
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
@@ -7,7 +7,7 @@ class TestViewHelpers < ActionView::TestCase
7
7
  include MetaSearch::Helpers::UrlHelper
8
8
 
9
9
  router = ActionDispatch::Routing::RouteSet.new
10
- router.draw do |map|
10
+ router.draw do
11
11
  resources :developers
12
12
  resources :companies
13
13
  resources :projects
metadata CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
5
5
  segments:
6
6
  - 0
7
7
  - 9
8
- - 6
9
- version: 0.9.6
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-09-29 00:00:00 -04:00
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