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 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