simple_search 0.0.0

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