simple_search 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Ernie Miller
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,17 @@
1
+ = simple_search
2
+
3
+ Description goes here.
4
+
5
+ == Note on Patches/Pull Requests
6
+
7
+ * Fork the project.
8
+ * Make your feature addition or bug fix.
9
+ * Add tests for it. This is important so I don't break it in a
10
+ future version unintentionally.
11
+ * Commit, do not mess with rakefile, version, or history.
12
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
13
+ * Send me a pull request. Bonus points for topic branches.
14
+
15
+ == Copyright
16
+
17
+ Copyright (c) 2010 Ernie Miller. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,53 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "simple_search"
8
+ gem.summary = %Q{Add an easy-to-use search method to your ActiveRecord models}
9
+ gem.description = %Q{This gem allows your models to be searchable.}
10
+ gem.email = "ernie@metautonomo.us"
11
+ gem.homepage = "http://github.com/ernie/simple_search"
12
+ gem.authors = ["Ernie Miller"]
13
+ gem.add_development_dependency "thoughtbot-shoulda", ">= 0"
14
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
15
+ end
16
+ Jeweler::GemcutterTasks.new
17
+ rescue LoadError
18
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
19
+ end
20
+
21
+ require 'rake/testtask'
22
+ Rake::TestTask.new(:test) do |test|
23
+ test.libs << 'lib' << 'test'
24
+ test.pattern = 'test/**/test_*.rb'
25
+ test.verbose = true
26
+ end
27
+
28
+ begin
29
+ require 'rcov/rcovtask'
30
+ Rcov::RcovTask.new do |test|
31
+ test.libs << 'test'
32
+ test.pattern = 'test/**/test_*.rb'
33
+ test.verbose = true
34
+ end
35
+ rescue LoadError
36
+ task :rcov do
37
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
38
+ end
39
+ end
40
+
41
+ task :test => :check_dependencies
42
+
43
+ task :default => :test
44
+
45
+ require 'rake/rdoctask'
46
+ Rake::RDocTask.new do |rdoc|
47
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
48
+
49
+ rdoc.rdoc_dir = 'rdoc'
50
+ rdoc.title = "simple_search #{version}"
51
+ rdoc.rdoc_files.include('README*')
52
+ rdoc.rdoc_files.include('lib/**/*.rb')
53
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.0
@@ -0,0 +1,2 @@
1
+ require 'simple_search/search'
2
+ require 'simple_search/searchable'
@@ -0,0 +1,328 @@
1
+ module SimpleSearch
2
+ class Search
3
+ attr_reader :options, :associations
4
+
5
+ def initialize(model, options = {})
6
+ raise StandardError, "#{model.to_s} is not searchable" unless model.metaclass.method_defined?(:search)
7
+ @model = model
8
+ @associations = [*options.delete(:search_associations)].compact
9
+ @association_objects = @associations.map do |a|
10
+ @model.reflect_on_association(a)
11
+ end
12
+ initialize_option_methods
13
+ # Need to make sure to keep multipart date stuff around
14
+ dates = dates_from_hash(Hash[*options.select {|k, v| k =~ /\(.*\)$/}.flatten])
15
+ @options = options.reject{|k, v| !respond_to?(k)}.merge dates
16
+ end
17
+
18
+ def conditions
19
+ conditions = []
20
+ parameters = []
21
+
22
+ return [] if options.empty?
23
+
24
+ @options.keys.select{|k| self.respond_to?("#{k}_conditions".to_sym)}.each do |opt|
25
+ next if self.send(opt.to_sym).blank?
26
+ condition = self.send("#{opt}_conditions".to_sym)
27
+ parameter = self.send("#{opt}_parameters".to_sym)
28
+ if condition.is_a?(Array)
29
+ concat = respond_to?("#{opt}_concat".to_sym) ? send("#{opt}_concat".to_sym) : 'OR'
30
+ condition = '(' + condition.join(" #{concat} ") + ')'
31
+ end
32
+ conditions << condition
33
+ parameters.concat [*parameter]
34
+ end
35
+
36
+ conditions.blank? ? [] : [conditions.join(" AND "), *parameters]
37
+ end
38
+
39
+ private
40
+
41
+ def dates_from_hash(hash)
42
+ h = {}
43
+ hash.keys.collect{|k| k.gsub(/\(.*\)/,'')}.uniq.each do |which|
44
+ part = Proc.new { |n| hash["#{which}(#{n}i)"].to_i }
45
+ y,m,d = part[1], part[2], part[3]
46
+ y = Date.today.year if y.zero?
47
+ if d > Time::days_in_month(m, y)
48
+ d = Time::days_in_month(m, y)
49
+ end
50
+ h[which] = Date.new(y, m, d)
51
+ end
52
+ h
53
+ rescue ArgumentError => e
54
+ return nil
55
+ end
56
+
57
+ def initialize_option_methods
58
+ @model.columns.each do |col|
59
+ add_methods_for(@model, col)
60
+ end
61
+
62
+ @association_objects.each do |assoc|
63
+ assoc.class_name.constantize.columns.each do |col|
64
+ add_methods_for(assoc.class_name.constantize, col, assoc)
65
+ end
66
+ end
67
+ end
68
+
69
+ def add_methods_for(model, col, association = nil)
70
+ if model.respond_to?(:lookup_for) && model.lookups.has_key?(col.name)
71
+ add_lookup_options(col.name, association)
72
+ else
73
+ case col.type
74
+ when :string
75
+ add_string_options(col.name, association)
76
+ when :decimal, :integer, :bigint, :float
77
+ add_numeric_options(col.name, association)
78
+ when :date, :datetime
79
+ add_date_options(col.name, association)
80
+ end
81
+ end
82
+ end
83
+
84
+ def add_lookup_options(column_name, association = nil)
85
+ n = column_name
86
+ if association
87
+ model = association.class_name.constantize
88
+ meth = [association.name.to_s, n].join('_').to_sym
89
+ else
90
+ model = @model
91
+ meth = n.to_sym
92
+ end
93
+ l = model.lookup_for(n)
94
+ t = model.table_name
95
+ metaclass.instance_eval do
96
+ [meth, "#{meth}_not".to_sym].each do |m|
97
+ define_method(m) do
98
+ options[m]
99
+ end
100
+
101
+ define_method("#{m}_parameters".to_sym) do
102
+ options[m]
103
+ end
104
+
105
+ define_method("#{m}_concat".to_sym) do
106
+ options["#{m}_concat".to_sym] == 'AND' ? 'AND' : 'OR'
107
+ end
108
+
109
+ define_method("#{m}_choices".to_sym) do |*filters|
110
+ filters.flatten!
111
+ opts = filters.last.is_a?(Hash) ? filters.pop : {}
112
+
113
+ l.sort {|a,b| a[1] <=> b[1]}.map{|a| a[0]}.inject([]) do |ary, key|
114
+ if filters.blank? ||
115
+ filters.detect{|f|
116
+ f.is_a?(Regexp) ? f.match(key.to_s) : key.to_s == f.to_s
117
+ }
118
+ ary << [l[key], key]
119
+ end
120
+ ary
121
+ end
122
+ end
123
+ end
124
+
125
+ define_method("#{meth}_conditions".to_sym) do
126
+ cond = []
127
+ [*options[meth]].each do
128
+ cond << "FIND_IN_SET(?, #{t}.#{n})"
129
+ end
130
+ cond
131
+ end
132
+
133
+ define_method("#{meth}_not_conditions".to_sym) do
134
+ cond = []
135
+ [*options["#{meth}_not".to_sym]].each do
136
+ cond << "NOT FIND_IN_SET(?, #{t}.#{n})"
137
+ end
138
+ cond
139
+ end
140
+ end
141
+ end
142
+
143
+ def add_string_options(column_name, association = nil)
144
+ n = column_name
145
+ if association
146
+ model = association.class_name.constantize
147
+ meth = [association.name.to_s, n].join('_').to_sym
148
+ else
149
+ model = @model
150
+ meth = n.to_sym
151
+ end
152
+ t = model.table_name
153
+ metaclass.instance_eval do
154
+ [meth, "#{meth}_exact".to_sym, "#{meth}_not".to_sym, "#{meth}_not_exact".to_sym,
155
+ "#{meth}_starts_with".to_sym, "#{meth}_not_starts_with".to_sym].each do |m|
156
+ define_method(m) do
157
+ options[m]
158
+ end
159
+ end
160
+
161
+ [meth, "#{meth}_not".to_sym].each do |m|
162
+ define_method("#{m}_parameters".to_sym) do
163
+ '%' + options[m] + '%'
164
+ end
165
+ end
166
+
167
+ ["#{meth}_starts_with", "#{meth}_not_starts_with".to_sym].each do |m|
168
+ define_method("#{m}_parameters".to_sym) do
169
+ options[m] + '%'
170
+ end
171
+ end
172
+
173
+ ["#{meth}_exact".to_sym, "#{meth}_not_exact".to_sym].each do |m|
174
+ define_method("#{m}_parameters".to_sym) do
175
+ options[m]
176
+ end
177
+ end
178
+
179
+ [meth, "#{meth}_starts_with"].each do |m|
180
+ define_method("#{m}_conditions".to_sym) do
181
+ "#{t}.#{n} LIKE ?"
182
+ end
183
+ end
184
+
185
+ ["#{meth}_not", "#{meth}_not_starts_with"].each do |m|
186
+ define_method("#{m}_conditions".to_sym) do
187
+ "#{t}.#{n} NOT LIKE ?"
188
+ end
189
+ end
190
+
191
+ define_method("#{meth}_exact_conditions".to_sym) do
192
+ "#{t}.#{n} = ?"
193
+ end
194
+
195
+ define_method("#{meth}_not_exact_conditions".to_sym) do
196
+ "#{t}.#{n} != ?"
197
+ end
198
+ end
199
+ end
200
+
201
+ def add_numeric_options(column_name, association = nil)
202
+ n = column_name
203
+ if association
204
+ model = association.class_name.constantize
205
+ meth = [association.name.to_s, n].join('_').to_sym
206
+ else
207
+ model = @model
208
+ meth = n.to_sym
209
+ end
210
+ t = model.table_name
211
+ metaclass.instance_eval do
212
+ [meth, "#{meth}_not".to_sym, "#{meth}_min".to_sym, "#{meth}_max".to_sym].each do |m|
213
+ define_method(m) do
214
+ options[m].blank? ? nil : cast_to_numeric(model, n, options[m])
215
+ end
216
+
217
+ define_method("#{m}_parameters".to_sym) do
218
+ options[m].blank? ? nil : cast_to_numeric(model, n, options[m])
219
+ end
220
+ end
221
+
222
+ define_method("#{meth}_conditions".to_sym) do
223
+ "#{t}.#{n} = ?"
224
+ end
225
+
226
+ define_method("#{meth}_not_conditions".to_sym) do
227
+ "#{t}.#{n} != ?"
228
+ end
229
+
230
+ define_method("#{meth}_min_conditions".to_sym) do
231
+ "#{t}.#{n} >= ?"
232
+ end
233
+
234
+ define_method("#{meth}_max_conditions".to_sym) do
235
+ "#{t}.#{n} <= ?"
236
+ end
237
+ end
238
+ end
239
+
240
+ def add_date_options(column_name, association = nil)
241
+ n = column_name
242
+ if association
243
+ model = association.class_name.constantize
244
+ meth = [association.name.to_s, n].join('_').to_sym
245
+ else
246
+ model = @model
247
+ meth = n.to_sym
248
+ end
249
+ t = model.table_name
250
+ metaclass.instance_eval do
251
+ define_method(meth) do
252
+ options[meth]
253
+ end
254
+
255
+ define_method("#{meth}_parameters".to_sym) do
256
+ case options[meth].class.to_s
257
+ when 'Date', 'Time'
258
+ [cast_to_date_or_time(model, n, options[meth], :from).to_s(:db),
259
+ cast_to_date_or_time(model, n, options[meth], :to).to_s(:db)]
260
+ when 'String'
261
+ date_str = options[meth]
262
+ return nil if date_str.blank?
263
+ from, to = date_str.split(/\s+to\s+/)
264
+ Rails.logger.info "from = #{from}, to = #{to}"
265
+ to ||= from
266
+ from = cast_to_date_or_time(model, n, from, :from).to_s(:db) rescue nil
267
+ to = cast_to_date_or_time(model, n, to, :to).to_s(:db) rescue nil
268
+ from && to ? [from, to] : nil
269
+ end
270
+ end
271
+
272
+ define_method("#{meth}_conditions".to_sym) do
273
+ "#{t}.#{n} BETWEEN ? AND ?"
274
+ end
275
+
276
+ ["#{meth}_from".to_sym, "#{meth}_to".to_sym].each do |m|
277
+ define_method(m) do
278
+ options[m.to_sym]
279
+ end
280
+
281
+ define_method("#{m}_parameters".to_sym) do
282
+ m =~ /_from$/ ?
283
+ cast_to_date_or_time(model, n, options[m], :from).to_s(:db) :
284
+ cast_to_date_or_time(model, n, options[m], :to).to_s(:db)
285
+ end
286
+ end
287
+
288
+ define_method("#{meth}_from_conditions".to_sym) do
289
+ "#{t}.#{n} >= ?"
290
+ end
291
+
292
+ define_method("#{meth}_to_conditions".to_sym) do
293
+ "#{t}.#{n} <= ?"
294
+ end
295
+ end
296
+ end
297
+
298
+ def cast_to_numeric(model, column_name, num)
299
+ col = model.columns_hash[column_name.to_s]
300
+ case col.type
301
+ when :decimal, :integer, :bigint
302
+ if col.type != :decimal || col.precision.zero?
303
+ return num.to_i
304
+ else
305
+ return num.to_f
306
+ end
307
+ when :float
308
+ return num.to_f
309
+ end
310
+ end
311
+
312
+ def cast_to_date_or_time(model, column_name, date_or_time, endpoint)
313
+ col = model.columns_hash[column_name.to_s]
314
+ case col.type
315
+ when :date
316
+ date_or_time = date_or_time.is_a?(String) ? Date.parse(date_or_time) : date_or_time.to_date
317
+ when :datetime
318
+ date_or_time = date_or_time.is_a?(String) ? Time.parse(date_or_time) : date_or_time.to_time
319
+ if endpoint == :to
320
+ date_or_time = date_or_time.end_of_day.in_time_zone
321
+ else
322
+ date_or_time.beginning_of_day.in_time_zone
323
+ end
324
+ end
325
+ date_or_time
326
+ end
327
+ end
328
+ end
@@ -0,0 +1,30 @@
1
+ module SimpleSearch
2
+ module Searchable
3
+ module ClassMethods
4
+ # The "s" parameter is an instance of Search, instantiated from form input.
5
+ def search(s, args = {})
6
+ args.merge!(
7
+ :conditions => s.conditions,
8
+ :include => (s.associations + [*args[:include]]).compact.uniq
9
+ )
10
+ if self.respond_to?(:paginate) && args.has_key?(:page)
11
+ self.paginate(:all, args)
12
+ else
13
+ self.find(:all, args)
14
+ end
15
+ end
16
+
17
+ def search_count(s, args = {})
18
+ args.merge!(
19
+ :conditions => s.conditions,
20
+ :include => (s.associations + [*args[:include]]).compact.uniq
21
+ )
22
+ self.count(args)
23
+ end
24
+ end
25
+
26
+ def self.included(base)
27
+ base.extend(ClassMethods)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,56 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{simple_search}
8
+ s.version = "0.0.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Ernie Miller"]
12
+ s.date = %q{2010-02-24}
13
+ s.description = %q{This gem allows your models to be searchable.}
14
+ s.email = %q{ernie@metautonomo.us}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE",
17
+ "README.rdoc"
18
+ ]
19
+ s.files = [
20
+ ".document",
21
+ ".gitignore",
22
+ "LICENSE",
23
+ "README.rdoc",
24
+ "Rakefile",
25
+ "VERSION",
26
+ "lib/simple_search.rb",
27
+ "lib/simple_search/search.rb",
28
+ "lib/simple_search/searchable.rb",
29
+ "simple_search.gemspec",
30
+ "test/helper.rb",
31
+ "test/test_simple_search.rb"
32
+ ]
33
+ s.homepage = %q{http://github.com/ernie/simple_search}
34
+ s.rdoc_options = ["--charset=UTF-8"]
35
+ s.require_paths = ["lib"]
36
+ s.rubygems_version = %q{1.3.6}
37
+ s.summary = %q{Add an easy-to-use search method to your ActiveRecord models}
38
+ s.test_files = [
39
+ "test/helper.rb",
40
+ "test/test_simple_search.rb"
41
+ ]
42
+
43
+ if s.respond_to? :specification_version then
44
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
45
+ s.specification_version = 3
46
+
47
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
48
+ s.add_development_dependency(%q<thoughtbot-shoulda>, [">= 0"])
49
+ else
50
+ s.add_dependency(%q<thoughtbot-shoulda>, [">= 0"])
51
+ end
52
+ else
53
+ s.add_dependency(%q<thoughtbot-shoulda>, [">= 0"])
54
+ end
55
+ end
56
+
data/test/helper.rb ADDED
@@ -0,0 +1,10 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'shoulda'
4
+
5
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
6
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
7
+ require 'simple_search'
8
+
9
+ class Test::Unit::TestCase
10
+ end
@@ -0,0 +1,7 @@
1
+ require 'helper'
2
+
3
+ class TestSimpleSearch < Test::Unit::TestCase
4
+ should "probably rename this file and start testing for real" do
5
+ flunk "hey buddy, you should probably rename this file and start testing for real"
6
+ end
7
+ end
metadata ADDED
@@ -0,0 +1,86 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: simple_search
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 0
8
+ - 0
9
+ version: 0.0.0
10
+ platform: ruby
11
+ authors:
12
+ - Ernie Miller
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-02-24 00:00:00 -05:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: thoughtbot-shoulda
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 0
29
+ version: "0"
30
+ type: :development
31
+ version_requirements: *id001
32
+ description: This gem allows your models to be searchable.
33
+ email: ernie@metautonomo.us
34
+ executables: []
35
+
36
+ extensions: []
37
+
38
+ extra_rdoc_files:
39
+ - LICENSE
40
+ - README.rdoc
41
+ files:
42
+ - .document
43
+ - .gitignore
44
+ - LICENSE
45
+ - README.rdoc
46
+ - Rakefile
47
+ - VERSION
48
+ - lib/simple_search.rb
49
+ - lib/simple_search/search.rb
50
+ - lib/simple_search/searchable.rb
51
+ - simple_search.gemspec
52
+ - test/helper.rb
53
+ - test/test_simple_search.rb
54
+ has_rdoc: true
55
+ homepage: http://github.com/ernie/simple_search
56
+ licenses: []
57
+
58
+ post_install_message:
59
+ rdoc_options:
60
+ - --charset=UTF-8
61
+ require_paths:
62
+ - lib
63
+ required_ruby_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ segments:
68
+ - 0
69
+ version: "0"
70
+ required_rubygems_version: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ segments:
75
+ - 0
76
+ version: "0"
77
+ requirements: []
78
+
79
+ rubyforge_project:
80
+ rubygems_version: 1.3.6
81
+ signing_key:
82
+ specification_version: 3
83
+ summary: Add an easy-to-use search method to your ActiveRecord models
84
+ test_files:
85
+ - test/helper.rb
86
+ - test/test_simple_search.rb