search_scope 0.1.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/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,16 @@
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.project = 'search-scope'
8
+ p.url = "http://rubyforge.org/projects/search-scope"
9
+ p.author = "Ryan Owens"
10
+ p.email = "ryan@infoether.com"
11
+ p.ignore_pattern = ["tmp/*", "script/*", "search_scope_notes.txt"]
12
+ # p.runtime_dependencies = ['activerecord >= 2.2.2']
13
+ p.development_dependencies = []
14
+ end
15
+
16
+ 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,31 @@
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
+ else
28
+ end
29
+ else
30
+ end
31
+ end
metadata ADDED
@@ -0,0 +1,66 @@
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
+
16
+ description: Simplify searching a model by defining custom named_scopes.
17
+ email: ryan@infoether.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - CHANGELOG
24
+ - lib/search_scope.rb
25
+ - README
26
+ files:
27
+ - CHANGELOG
28
+ - init.rb
29
+ - lib/search_scope.rb
30
+ - Manifest
31
+ - Rakefile
32
+ - README
33
+ - search_scope.gemspec
34
+ has_rdoc: true
35
+ homepage: http://rubyforge.org/projects/search-scope
36
+ post_install_message:
37
+ rdoc_options:
38
+ - --line-numbers
39
+ - --inline-source
40
+ - --title
41
+ - Search_scope
42
+ - --main
43
+ - README
44
+ require_paths:
45
+ - lib
46
+ required_ruby_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: "0"
51
+ version:
52
+ required_rubygems_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: "1.2"
57
+ version:
58
+ requirements: []
59
+
60
+ rubyforge_project: search-scope
61
+ rubygems_version: 1.3.1
62
+ signing_key:
63
+ specification_version: 2
64
+ summary: Simplify searching a model by defining custom named_scopes.
65
+ test_files: []
66
+