search-scope 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +0 -0
- data/Manifest +6 -0
- data/README +102 -0
- data/Rakefile +15 -0
- data/init.rb +1 -0
- data/lib/search_scope.rb +159 -0
- data/search-scope.gemspec +37 -0
- metadata +85 -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,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'
|
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,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
|
+
|