search-scope 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG ADDED
File without changes
data/Manifest ADDED
@@ -0,0 +1,6 @@
1
+ CHANGELOG
2
+ init.rb
3
+ lib/search_scope.rb
4
+ Manifest
5
+ Rakefile
6
+ README
data/README ADDED
@@ -0,0 +1,102 @@
1
+ export PATH=$PATH:/opt/local/bin:/opt/local/sbin:/opt/local/lib/postgresql83/bin
2
+
3
+
4
+ = search_scope
5
+
6
+ Rails gem for simplify searching a model by defining custom named_scopes.
7
+
8
+ == Install
9
+
10
+ gem install search_scope
11
+
12
+ == Usage
13
+
14
+ ...coming soon
15
+
16
+ == Tutorial
17
+
18
+ #this tutorial will show you how to create a rails app and search using search_scope
19
+
20
+ #create your new app
21
+ rails search_scope_test
22
+ cd search_scope_test
23
+
24
+ #add models
25
+ ./script/generate model post title:string author_id:integer body:text
26
+ ./script/generate model author name:string code:string
27
+ rake db:migrate
28
+
29
+ #add to environment.rb (below the other config.gem examples)
30
+ config.gem 'search-scope', :version => '0.1.0'
31
+
32
+ #install the gem (from the command line)
33
+ rake gems:install
34
+
35
+ #edit the models
36
+ class Author < ActiveRecord::Base
37
+ has_many :posts
38
+
39
+ sort_search_by :name
40
+ search_scope :name
41
+ search_scope :code, :search_type => :exact_match
42
+ end
43
+
44
+ class Post < ActiveRecord::Base
45
+ belongs_to :author
46
+
47
+ sort_search_by :title
48
+ sort_search_by :author_name, :label => 'Author', :order => "authors.name, title", :include => :author
49
+
50
+ search_scope :title
51
+ search_scope :body
52
+ search_scope :author_name, lambda { |term| { :conditions => ["authors.name LIKE ?", "%#{term}%"], :include => :author } }
53
+
54
+ def description
55
+ "\"#{title}\" by #{author.name}"
56
+ end
57
+ end
58
+
59
+ #add some data (from the command line)
60
+ ./script/console
61
+
62
+ joe = Author.create :name => 'Joe Schmo', :code => 'joe'
63
+ jack = Author.create :name => 'Jack Sprat', :code => 'jack'
64
+
65
+ Post.create :title => 'Hello World!!', :body => 'Just saying hi.', :author_id => joe.id
66
+ Post.create :title => 'I am NOT Jack!', :body => 'The OTHER guy is Jack, not me.', :author_id => joe.id
67
+ Post.create :title => 'My last name is Schmo, not Blow.', :body => 'Seriously, get it right people.', :author_id => joe.id
68
+ Post.create :title => 'It\'s cold out.', :body => 'Can\'t wait for summer.', :author_id => joe.id
69
+
70
+ Post.create :title => 'Hello World!!', :body => 'Just saying hi.', :author_id => jack.id
71
+ Post.create :title => 'I\'m in the dog house.', :body => 'I bought my wife a gym membership for Christmas. oops.', :author_id => jack.id
72
+ Post.create :title => 'Steak is delicious.', :body => 'No, seriously, it is. Even Joe likes it.', :author_id => jack.id
73
+
74
+
75
+ #do some searching (from script/console)
76
+ Author.count #=> 2
77
+ Post.count #=> 7
78
+
79
+ Author.search(:name => 'joe').size # => 1
80
+ Author.search(:name => 'j').size # => 2
81
+ Author.search(:code => 'j').size # => 0
82
+ Author.search(:code => 'joe').size # => 1
83
+
84
+
85
+ puts Post.search(:title => 'cold out').collect(&:description).join("\n") # => "It's cold out." by Joe Schmo
86
+
87
+ Post.search(:title => 'hello').size # => 2
88
+ Post.search(:title => 'hello', :author_name => 'joe').size # => 1
89
+
90
+ #sorting
91
+ puts Post.search(:title => 'a', :sort_by => :title).collect(&:description).join("\n")
92
+ puts Post.search(:title => 'a', :sort_by => :author_name).collect(&:description).join("\n")
93
+
94
+ #quick_search
95
+ puts Post.search(:quick_search => 'jack', :sort_by => :title).collect(&:description).join("\n")
96
+ puts Post.search(:quick_search => 'jack', :sort_by => :author_name).collect(&:description).join("\n")
97
+
98
+
99
+
100
+
101
+
102
+
data/Rakefile ADDED
@@ -0,0 +1,15 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'echoe'
4
+
5
+ Echoe.new('search-scope', '0.1.0') do |p|
6
+ p.description = "Simplify searching a model by defining custom named_scopes."
7
+ p.url = "http://rubyforge.org/projects/search-scope"
8
+ p.author = "Ryan Owens"
9
+ p.email = "ryan@infoether.com"
10
+ p.ignore_pattern = ["tmp/*", "script/*", "search_scope_notes.txt"]
11
+ p.runtime_dependencies = ['activerecord', '>= 2.2.2']
12
+ p.development_dependencies = []
13
+ end
14
+
15
+ Dir["#{File.dirname(__FILE__)}/tasks/*.rake"].sort.each { |ext| load ext }
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'search_score'
@@ -0,0 +1,159 @@
1
+ module SearchScope
2
+
3
+ class SortScope
4
+ attr_reader :name, :label, :order, :include_model
5
+ def initialize(name, label, order, include_model)
6
+ @name, @label, @order, @include_model = name, label, order, include_model
7
+ end
8
+ end
9
+
10
+ def sort_search_by(name, options={})
11
+ options[:label] ||= name.to_s.titleize
12
+ options[:order] ||= name.to_s
13
+ raise "you must supply a Symbol for a name to new_sortable_by (#{name.inspect})" unless name.is_a? Symbol
14
+ return if sort_choices_hash.keys.include? name.to_s
15
+ #TODO put this back and get rid of the return above once I figure out how to reset the class vars when the class is reloaded
16
+ # raise "there is already a sortable defined for the name (#{name.inspect}" if sort_choices_hash.keys.include? name.to_s
17
+ sort_choices_hash[name] = SortScope.new(name, options[:label], options[:order], options[:include])
18
+ sort_choices << sort_choices_hash[name]
19
+ end
20
+
21
+ def search_scope(name, options = {}, &block)
22
+ puts "***search_scope: #{name.inspect} - #{options.inspect}"
23
+ #default the search to a LIKE search if nothing is given
24
+ if options.blank?
25
+ options = lambda { |term| { :conditions => ["#{table_name}.#{name} LIKE ?", "%#{term}%"] } }
26
+ elsif options.is_a?(Hash) && options[:search_type]
27
+ case options[:search_type]
28
+ when :exact_match
29
+ options = lambda { |term| { :conditions => ["#{table_name}.#{name} = ?", term] } }
30
+ else
31
+ raise "unknown search_type for search_scope: (#{name} - #{options[:search_type]})"
32
+ end
33
+ end
34
+ search_scope_keys << name unless search_scope_keys.include? name
35
+ if options.is_a? Proc
36
+ quick_search_scopes << name if options.arity == 1
37
+ end
38
+ named_scope("search_#{name}".intern, options, &block)
39
+ end
40
+
41
+ def search_scope_keys
42
+ @search_scope_keys ||= []
43
+ end
44
+
45
+ def sort_choices
46
+ @sort_choices ||= []
47
+ end
48
+
49
+ def sort_choices_hash
50
+ @sort_choices_hash ||= HashWithIndifferentAccess.new
51
+ end
52
+
53
+ #for now, we'll say that by definition, if a search_scope defines a lambda with one term, then it is a quick_search_scope
54
+ def quick_search_scopes
55
+ @quick_search_scopes ||= []
56
+ end
57
+
58
+ def sort_search_by_options(sort_by)
59
+ choices_hash = sort_choices_hash[sort_by]
60
+ return {} unless choices_hash
61
+ hash = {
62
+ :order => choices_hash.order,
63
+ :include => choices_hash.include_model,
64
+ }
65
+ hash
66
+ end
67
+
68
+ def search_scopes(options={})
69
+ scopes = []
70
+ search_scope_keys.each do |key|
71
+
72
+ #if the scope key isn't in the params, don't include it
73
+ next unless options[key]
74
+ # add the scope once for each term passed in (space delimited). this allows a search for 'star wars' to return only items where both terms match
75
+ terms = options[key].split.compact
76
+ terms.each do |term|
77
+ scopes << [key, term]
78
+ end
79
+ end
80
+ # scopes << [:sort_search_scope, options[:sort_by]] if options[:sort_by]
81
+ scopes
82
+ end
83
+
84
+ def get_search_scope_from_object(object, scope, *args)
85
+ scope_name = "search_#{scope}".intern
86
+ scope = object.send(scope_name, *args)
87
+ end
88
+
89
+ #this gets all of the options from the quick_search named scopes and builds a quick_search from them
90
+ #the quick_search is one that matches any of the named scopes, not all of them.
91
+ #TODO look into better ways of doing this, and figure out the proper name.
92
+ def quick_search_scope_options(quick_search_terms)
93
+ conditions = []
94
+ includes = []
95
+ aggregate_scope = self
96
+
97
+ quick_search_scopes.each do |scope|
98
+ term_conditions = []
99
+ terms = quick_search_terms.split.compact
100
+ terms.each_with_index do |term,index|
101
+ # quick_search_scope = self.send(scope, term)
102
+ quick_search_scope = get_search_scope_from_object(self, scope, term)
103
+ query_options = quick_search_scope.proxy_options
104
+ term_conditions << self.sanitize_sql_for_conditions(query_options[:conditions])
105
+ #only do this once, the first time
106
+ if query_options[:include] && index == 0
107
+ includes << query_options[:include] unless includes.include? query_options[:include]
108
+ end
109
+ extra_options = query_options.keys - [:conditions, :include]
110
+ raise "search_scope with quick_search does not support the #{extra_options.first.inspect} option at this time (#{scope.inspect})" if extra_options.first
111
+ end
112
+ conditions << term_conditions.collect{|c|"(#{c})"}.join(' OR ') #ORing makes sure any of the terms exist somewhere in any of the fields. I think this is what we actually need, plus "relevance" (does that mean sphinx?)
113
+ # conditions << term_conditions.collect{|c|"(#{c})"}.join(' AND ') #ANDing this will make it so that all the terms MUST appear in one field, eg author first and last name
114
+ end
115
+ conditions_sql = conditions.collect{|c|"(#{c})"}.join(' OR ')
116
+ {:conditions => conditions_sql, :include => includes}
117
+ end
118
+
119
+ #this searches by chaining all of the named_scopes (search_scopes) that were included in the params
120
+ def search(params={})
121
+ paginate = params.delete :paginate
122
+ aggregate_scope = self
123
+ search_scopes(params).each do |scope|
124
+ if scope.is_a? Symbol
125
+ # aggregate_scope = aggregate_scope.send(scope)
126
+ aggregate_scope = get_search_scope_from_object(aggregate_scope, scope)
127
+ elsif scope.is_a? Array
128
+ # aggregate_scope = aggregate_scope.send(*scope)
129
+ aggregate_scope = get_search_scope_from_object(aggregate_scope, *scope)
130
+ else
131
+ raise "unsupported type for search scope: #{scope.inspect}"
132
+ end
133
+ end
134
+ unless params[:quick_search].blank?
135
+ aggregate_scope = aggregate_scope.scoped quick_search_scope_options(params[:quick_search])
136
+ end
137
+ if params[:sort_by]
138
+ aggregate_scope = aggregate_scope.scoped sort_search_by_options(params[:sort_by])
139
+ end
140
+ if paginate
141
+ aggregate_scope.paginate(:all, :page => params[:page])
142
+ else
143
+ aggregate_scope.find(:all)
144
+ end
145
+ end
146
+
147
+ #this is for use with will_paginate
148
+ def paginate_search(params={})
149
+ params[:paginate] = true
150
+ search params
151
+ end
152
+
153
+ end
154
+
155
+ #include into Rails when the gem is loaded
156
+ class ActiveRecord::Base
157
+ extend SearchScope
158
+ end
159
+
@@ -0,0 +1,37 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{search-scope}
5
+ s.version = "0.1.0"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Ryan Owens"]
9
+ s.date = %q{2009-01-06}
10
+ s.description = %q{Simplify searching a model by defining custom named_scopes.}
11
+ s.email = %q{ryan@infoether.com}
12
+ s.extra_rdoc_files = ["CHANGELOG", "lib/search_scope.rb", "README"]
13
+ s.files = ["CHANGELOG", "init.rb", "lib/search_scope.rb", "Manifest", "Rakefile", "README", "search-scope.gemspec"]
14
+ s.has_rdoc = true
15
+ s.homepage = %q{http://rubyforge.org/projects/search-scope}
16
+ s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Search-scope", "--main", "README"]
17
+ s.require_paths = ["lib"]
18
+ s.rubyforge_project = %q{search-scope}
19
+ s.rubygems_version = %q{1.3.1}
20
+ s.summary = %q{Simplify searching a model by defining custom named_scopes.}
21
+
22
+ if s.respond_to? :specification_version then
23
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
24
+ s.specification_version = 2
25
+
26
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
27
+ s.add_runtime_dependency(%q<activerecord>, [">= 0"])
28
+ s.add_runtime_dependency(%q<>=>, ["= 2.2.2"])
29
+ else
30
+ s.add_dependency(%q<activerecord>, [">= 0"])
31
+ s.add_dependency(%q<>=>, ["= 2.2.2"])
32
+ end
33
+ else
34
+ s.add_dependency(%q<activerecord>, [">= 0"])
35
+ s.add_dependency(%q<>=>, ["= 2.2.2"])
36
+ end
37
+ end
metadata ADDED
@@ -0,0 +1,85 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: search-scope
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Ryan Owens
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-01-06 00:00:00 -05:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: activerecord
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: ">="
27
+ type: :runtime
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "="
32
+ - !ruby/object:Gem::Version
33
+ version: 2.2.2
34
+ version:
35
+ description: Simplify searching a model by defining custom named_scopes.
36
+ email: ryan@infoether.com
37
+ executables: []
38
+
39
+ extensions: []
40
+
41
+ extra_rdoc_files:
42
+ - CHANGELOG
43
+ - lib/search_scope.rb
44
+ - README
45
+ files:
46
+ - CHANGELOG
47
+ - init.rb
48
+ - lib/search_scope.rb
49
+ - Manifest
50
+ - Rakefile
51
+ - README
52
+ - search-scope.gemspec
53
+ has_rdoc: true
54
+ homepage: http://rubyforge.org/projects/search-scope
55
+ post_install_message:
56
+ rdoc_options:
57
+ - --line-numbers
58
+ - --inline-source
59
+ - --title
60
+ - Search-scope
61
+ - --main
62
+ - README
63
+ require_paths:
64
+ - lib
65
+ required_ruby_version: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: "0"
70
+ version:
71
+ required_rubygems_version: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: "1.2"
76
+ version:
77
+ requirements: []
78
+
79
+ rubyforge_project: search-scope
80
+ rubygems_version: 1.3.1
81
+ signing_key:
82
+ specification_version: 2
83
+ summary: Simplify searching a model by defining custom named_scopes.
84
+ test_files: []
85
+