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 +0 -0
- data/Manifest +6 -0
- data/README +102 -0
- data/Rakefile +16 -0
- data/init.rb +1 -0
- data/lib/search_scope.rb +159 -0
- data/search_scope.gemspec +31 -0
- metadata +66 -0
data/CHANGELOG
ADDED
File without changes
|
data/Manifest
ADDED
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'
|
data/lib/search_scope.rb
ADDED
@@ -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
|
+
|