gbdev-scoped_search 0.2.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/LICENSE +20 -0
- data/README.rdoc +54 -0
- data/Rakefile +70 -0
- data/TODO +20 -0
- data/init.rb +1 -0
- data/lib/scoped_search.rb +67 -0
- data/lib/scoped_search/query_language_parser.rb +71 -0
- data/test/query_language_test.rb +140 -0
- data/test/search_for_test.rb +43 -0
- data/test/tasks.rake +8 -0
- data/test/test_helper.rb +41 -0
- metadata +70 -0
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2008 Willem van Bergen
|
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,54 @@
|
|
1
|
+
= scoped_search
|
2
|
+
|
3
|
+
This simple plugin will make it easy to search your ActiveRecord models. Searching is performed using a query string, which should be passed to the named_scope <tt>search_for</tt> that uses SQL %LIKE% conditions for searching. You can specify what fields should be used for searching.
|
4
|
+
|
5
|
+
== Installation
|
6
|
+
|
7
|
+
You can enable <tt>scoped_search</tt> as a Ruby gem. You must enable the gem in your <tt>environment.rb</tt> configuration:
|
8
|
+
|
9
|
+
Rails::Initializer.run do |config|
|
10
|
+
...
|
11
|
+
config.gem 'wvanbergen-scoped_search', :lib => 'scoped_search', :source => 'http://gems.github.com/'
|
12
|
+
end
|
13
|
+
|
14
|
+
Make sure the gem is installed by running <tt>rake gems:install</tt> in you project root. You can also install the gem by running <tt>sudo gem install wvanbergen-scoped_search -s http://gems.github.com</tt>
|
15
|
+
|
16
|
+
You can use scoped_search as a Rails plugin as well, but this is deprecated. Simply download or
|
17
|
+
clone scoped_search into your +vendor/plugins+ directory of your project.
|
18
|
+
|
19
|
+
== Usage
|
20
|
+
|
21
|
+
First, you have to specify in what columns should be searched:
|
22
|
+
|
23
|
+
class Project < ActiveRecord::Base
|
24
|
+
searchable_on :name, :description
|
25
|
+
end
|
26
|
+
|
27
|
+
Now, the +search_for+ scope is available for queries. You should pass a query string to the scope. This can be empty or nil, in which case all no search conditions are set (and all records will be returned).
|
28
|
+
|
29
|
+
Project.search_for(params[:q]).each { |project| ... }
|
30
|
+
|
31
|
+
The search query language is simple. It supports these constructs:
|
32
|
+
|
33
|
+
* words: some search keywords
|
34
|
+
* phrases: "a single search phrase"
|
35
|
+
* negation: "look for this" -"but do not look for this phrase and this" -word
|
36
|
+
|
37
|
+
This functionality is build on named_scope. The searchable_on statement creates
|
38
|
+
a named_scope "search_for". Because of this, you can actually chain the call with
|
39
|
+
other scopes. For example, this can be very useful if you only want to search in
|
40
|
+
projects that are accessible by a given user.
|
41
|
+
|
42
|
+
class Project < ActiveRecord::Base
|
43
|
+
searchable_on :name, :description
|
44
|
+
named_scope :accessible_by, lambda { |user| ... }
|
45
|
+
end
|
46
|
+
|
47
|
+
# using chained named_scopes and will_paginate
|
48
|
+
Project.accessible_by(current_user).search_for(params[:q]).paginate(:page => params[:page], :include => :tasks)
|
49
|
+
|
50
|
+
== Disclaimer
|
51
|
+
|
52
|
+
This Rails plugin is written by Willem van Bergen for the Floorplanner.com website. It is
|
53
|
+
released under the <b>MIT license</b>. Please contact me (willem AT vanbergen DOT org if
|
54
|
+
you have any suggestions or remarks.
|
data/Rakefile
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
|
3
|
+
load 'test/tasks.rake'
|
4
|
+
|
5
|
+
desc 'Default: run unit tests.'
|
6
|
+
task :default => :test
|
7
|
+
|
8
|
+
namespace :gem do
|
9
|
+
|
10
|
+
desc "Sets the version and date of the scoped_search gem. Requires the VERSION environment variable."
|
11
|
+
task :version => [:manifest] do
|
12
|
+
|
13
|
+
require 'date'
|
14
|
+
|
15
|
+
new_version = ENV['VERSION']
|
16
|
+
raise "VERSION is required" unless /\d+(\.\d+)*/ =~ new_version
|
17
|
+
|
18
|
+
spec_file = Dir['*.gemspec'].first
|
19
|
+
|
20
|
+
spec = File.read(spec_file)
|
21
|
+
spec.gsub!(/^(\s*s\.version\s*=\s*)('|")(.+)('|")(\s*)$/) { "#{$1}'#{new_version}'#{$5}" }
|
22
|
+
spec.gsub!(/^(\s*s\.date\s*=\s*)('|")(.+)('|")(\s*)$/) { "#{$1}'#{Date.today.strftime('%Y-%m-%d')}'#{$5}" }
|
23
|
+
File.open(spec_file, 'w') { |f| f << spec }
|
24
|
+
end
|
25
|
+
|
26
|
+
task :tag => [:version] do
|
27
|
+
|
28
|
+
new_version = ENV['VERSION']
|
29
|
+
raise "VERSION is required" unless /\d+(\.\d+)*/ =~ new_version
|
30
|
+
|
31
|
+
sh "git add scoped_search.gemspec .manifest"
|
32
|
+
sh "git commit -m \"Set gem version to #{new_version}\""
|
33
|
+
sh "git push origin"
|
34
|
+
sh "git tag -a \"scoped_search-#{new_version}\" -m \"Tagged version #{new_version}\""
|
35
|
+
sh "git push --tags"
|
36
|
+
end
|
37
|
+
|
38
|
+
desc "Builds a ruby gem for scoped_search"
|
39
|
+
task :build => [:manifest] do
|
40
|
+
system %[gem build scoped_search.gemspec]
|
41
|
+
end
|
42
|
+
|
43
|
+
desc %{Update ".manifest" with the latest list of project filenames. Respect\
|
44
|
+
.gitignore by excluding everything that git ignores. Update `files` and\
|
45
|
+
`test_files` arrays in "*.gemspec" file if it's present.}
|
46
|
+
task :manifest do
|
47
|
+
list = Dir['**/*'].sort
|
48
|
+
spec_file = Dir['*.gemspec'].first
|
49
|
+
list -= [spec_file] if spec_file
|
50
|
+
|
51
|
+
File.read('.gitignore').each_line do |glob|
|
52
|
+
glob = glob.chomp.sub(/^\//, '')
|
53
|
+
list -= Dir[glob]
|
54
|
+
list -= Dir["#{glob}/**/*"] if File.directory?(glob) and !File.symlink?(glob)
|
55
|
+
puts "excluding #{glob}"
|
56
|
+
end
|
57
|
+
|
58
|
+
if spec_file
|
59
|
+
spec = File.read spec_file
|
60
|
+
spec.gsub! /^(\s* s.(test_)?files \s* = \s* )( \[ [^\]]* \] | %w\( [^)]* \) )/mx do
|
61
|
+
assignment = $1
|
62
|
+
bunch = $2 ? list.grep(/^test.*_test\.rb$/) : list
|
63
|
+
'%s%%w(%s)' % [assignment, bunch.join(' ')]
|
64
|
+
end
|
65
|
+
|
66
|
+
File.open(spec_file, 'w') {|f| f << spec }
|
67
|
+
end
|
68
|
+
File.open('.manifest', 'w') {|f| f << list.join("\n") }
|
69
|
+
end
|
70
|
+
end
|
data/TODO
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
TODO items for named_scope
|
2
|
+
==========================
|
3
|
+
Contact willem AT vanbergen DOT org if you want to help out
|
4
|
+
|
5
|
+
|
6
|
+
New features:
|
7
|
+
- Search fields of associations as well
|
8
|
+
- Allow other search operators than %LIKE%
|
9
|
+
|
10
|
+
Refactoring:
|
11
|
+
- Create a separate class to build the actual SQL queries.
|
12
|
+
- For searchable_on(*fields) make it so that instead of fields it accepts options (a hash) where
|
13
|
+
:only and :except can be values. That way it is possible for all fields to be loaded except
|
14
|
+
the ones specified with :except.
|
15
|
+
- Add checks for field types because the latest version of PostgreSQL (version 8.3.3) is more
|
16
|
+
strict about searching for strings in columns that are not string types.
|
17
|
+
|
18
|
+
Documentation & testing:
|
19
|
+
- Put something useful in the wiki
|
20
|
+
- Add rdoc en comments to code
|
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'scoped_search'
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module ScopedSearch
|
2
|
+
|
3
|
+
module ClassMethods
|
4
|
+
|
5
|
+
def self.extended(base)
|
6
|
+
require 'scoped_search/query_language_parser'
|
7
|
+
end
|
8
|
+
|
9
|
+
# Creates a named scope in the class it was called upon
|
10
|
+
def searchable_on(*fields)
|
11
|
+
self.cattr_accessor :scoped_search_fields
|
12
|
+
self.scoped_search_fields = fields
|
13
|
+
self.named_scope :search_for, lambda { |keywords| self.build_scoped_search_conditions(keywords) }
|
14
|
+
end
|
15
|
+
|
16
|
+
# Build a hash that is used for the named_scope search_for.
|
17
|
+
# This function will split the search_string into keywords, and search for all the keywords
|
18
|
+
# in the fields that were provided to searchable_on
|
19
|
+
def build_scoped_search_conditions(search_string)
|
20
|
+
if search_string.nil? || search_string.strip.blank?
|
21
|
+
return { :conditions => nil }
|
22
|
+
else
|
23
|
+
conditions = []
|
24
|
+
query_params = {}
|
25
|
+
|
26
|
+
QueryLanguageParser.parse(search_string).each_with_index do |search_condition, index|
|
27
|
+
keyword_name = "keyword_#{index}".to_sym
|
28
|
+
query_params[keyword_name] = "%#{search_condition.first}%"
|
29
|
+
|
30
|
+
# a keyword may be found in any of the provided fields, so join the conitions with OR
|
31
|
+
if search_condition.length == 2 && search_condition.last == :not
|
32
|
+
keyword_conditions = self.scoped_search_fields.map do |field|
|
33
|
+
field_name = connection.quote_table_name(table_name) + "." + connection.quote_column_name(field)
|
34
|
+
"(#{field_name} NOT LIKE :#{keyword_name.to_s} OR #{field_name} IS NULL)"
|
35
|
+
end
|
36
|
+
conditions << "(#{keyword_conditions.join(' AND ')})"
|
37
|
+
elsif search_condition.length == 2 && search_condition.last == :or
|
38
|
+
word1, word2 = query_params[keyword_name].split(' OR ')
|
39
|
+
|
40
|
+
query_params.delete(keyword_name)
|
41
|
+
keyword_name_a = "#{keyword_name.to_s}a".to_sym
|
42
|
+
keyword_name_b = "#{keyword_name.to_s}b".to_sym
|
43
|
+
query_params[keyword_name_a] = word1
|
44
|
+
query_params[keyword_name_b] = word2
|
45
|
+
|
46
|
+
keyword_conditions = self.scoped_search_fields.map do |field|
|
47
|
+
field_name = connection.quote_table_name(table_name) + "." + connection.quote_column_name(field)
|
48
|
+
"(#{field_name} LIKE :#{keyword_name_a.to_s} OR #{field_name} LIKE :#{keyword_name_b.to_s})"
|
49
|
+
end
|
50
|
+
conditions << "(#{keyword_conditions.join(' OR ')})"
|
51
|
+
else
|
52
|
+
keyword_conditions = self.scoped_search_fields.map do |field|
|
53
|
+
field_name = connection.quote_table_name(table_name) + "." + connection.quote_column_name(field)
|
54
|
+
"#{field_name} LIKE :#{keyword_name.to_s}"
|
55
|
+
end
|
56
|
+
conditions << "(#{keyword_conditions.join(' OR ')})"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# all keywords must be matched, so join the conditions with AND
|
61
|
+
return { :conditions => [conditions.join(' AND '), query_params] }
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
ActiveRecord::Base.send(:extend, ScopedSearch::ClassMethods)
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module ScopedSearch
|
2
|
+
|
3
|
+
class QueryLanguageParser
|
4
|
+
|
5
|
+
def parse_query(query = nil)
|
6
|
+
return build_conditions_tree(tokenize(query))
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.parse(query)
|
10
|
+
self.new.parse_query(query)
|
11
|
+
end
|
12
|
+
|
13
|
+
protected
|
14
|
+
|
15
|
+
def build_conditions_tree(tokens)
|
16
|
+
conditions_tree = []
|
17
|
+
|
18
|
+
negate = false
|
19
|
+
tokens.each do |item|
|
20
|
+
case item
|
21
|
+
when :not
|
22
|
+
negate = true
|
23
|
+
else
|
24
|
+
if /^.+[ ]OR[ ].+$/ =~ item
|
25
|
+
conditions_tree << [item, :or]
|
26
|
+
else
|
27
|
+
conditions_tree << (negate ? [item, :not] : [item, :like])
|
28
|
+
negate = false
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
return conditions_tree
|
33
|
+
end
|
34
|
+
|
35
|
+
# **Patterns**
|
36
|
+
# Each pattern is sperated by a "|". With regular expressions the order of the expression does matter.
|
37
|
+
#
|
38
|
+
# ([\w]+[ ]OR[ ][\w]+)
|
39
|
+
# ([\w]+[ ]OR[ ]["][\w ]+["])
|
40
|
+
# (["][\w ]+["][ ]OR[ ][\w]+)
|
41
|
+
# (["][\w ]+["][ ]OR[ ]["][\w ]+["])
|
42
|
+
# Any two combinations of letters, numbers and underscores that are seperated by " OR " (a single space must
|
43
|
+
# be on each side of the "OR").
|
44
|
+
# THESE COULD BE COMBINED BUT BECAUSE OF THE WAY PARSING WORKS THIS IS NOT DONE ON PURPOSE!!
|
45
|
+
#
|
46
|
+
# ([-]?[\w]+)
|
47
|
+
# Any combination of letters, numbers and underscores that may or may not have a dash in front.
|
48
|
+
#
|
49
|
+
# ([-]?["][\w ]+["])
|
50
|
+
# Any combination of letters, numbers, underscores and spaces within double quotes that may or may not have a dash in front.
|
51
|
+
def tokenize(query)
|
52
|
+
pattern = ['([\w]+[ ]OR[ ][\w]+)',
|
53
|
+
'([\w]+[ ]OR[ ]["][\w ]+["])',
|
54
|
+
'(["][\w ]+["][ ]OR[ ][\w]+)',
|
55
|
+
'(["][\w ]+["][ ]OR[ ]["][\w ]+["])',
|
56
|
+
'([-]?[\w]+)',
|
57
|
+
'([-]?["][\w ]+["])']
|
58
|
+
pattern = Regexp.new(pattern.join('|'))
|
59
|
+
|
60
|
+
tokens = []
|
61
|
+
matches = query.scan(pattern).flatten.compact
|
62
|
+
matches.each { |match|
|
63
|
+
tokens << :not unless match.index('-').nil?
|
64
|
+
# Remove any escaped quotes and any dashes - the dash usually the first character.
|
65
|
+
# Remove any additional spaces - more that one.
|
66
|
+
tokens << match.gsub(/[-"]/,'').gsub(/[ ]{2,}/, ' ')
|
67
|
+
}
|
68
|
+
return tokens
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper'
|
2
|
+
|
3
|
+
class QueryLanguageTest < Test::Unit::TestCase
|
4
|
+
|
5
|
+
# change this function if you switch to another query language parser
|
6
|
+
def parse_query(query)
|
7
|
+
ScopedSearch::QueryLanguageParser.parse(query)
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_empty_search_query
|
11
|
+
parsed = parse_query('')
|
12
|
+
assert_equal 0, parsed.length
|
13
|
+
|
14
|
+
parsed = parse_query("\t \n")
|
15
|
+
assert_equal 0, parsed.length
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_single_keyword
|
19
|
+
parsed = parse_query('hallo')
|
20
|
+
assert_equal 1, parsed.length
|
21
|
+
assert_equal 'hallo', parsed.first.first
|
22
|
+
|
23
|
+
parsed = parse_query(' hallo ')
|
24
|
+
assert_equal 1, parsed.length
|
25
|
+
assert_equal 'hallo', parsed.first.first
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_multiple_keywords
|
29
|
+
parsed = parse_query(' hallo willem')
|
30
|
+
assert_equal 2, parsed.length
|
31
|
+
assert_equal 'willem', parsed.last.first
|
32
|
+
|
33
|
+
parsed = parse_query(" hallo willem van\tbergen ")
|
34
|
+
assert_equal 4, parsed.length
|
35
|
+
assert_equal 'hallo', parsed[0].first
|
36
|
+
assert_equal 'willem', parsed[1].first
|
37
|
+
assert_equal 'van', parsed[2].first
|
38
|
+
assert_equal 'bergen', parsed[3].first
|
39
|
+
end
|
40
|
+
|
41
|
+
def test_quoted_keywords
|
42
|
+
parsed = parse_query(' "hallo"')
|
43
|
+
assert_equal 1, parsed.length
|
44
|
+
assert_equal 'hallo', parsed.first.first
|
45
|
+
|
46
|
+
parsed = parse_query(' "hallo willem"')
|
47
|
+
assert_equal 1, parsed.length
|
48
|
+
assert_equal 'hallo willem', parsed.first.first
|
49
|
+
|
50
|
+
parsed = parse_query(' "hallo willem')
|
51
|
+
assert_equal 2, parsed.length
|
52
|
+
assert_equal 'hallo', parsed[0].first
|
53
|
+
assert_equal 'willem', parsed[1].first
|
54
|
+
|
55
|
+
parsed = parse_query(' "hallo wi"llem"')
|
56
|
+
assert_equal 2, parsed.length
|
57
|
+
assert_equal 'hallo wi', parsed[0].first
|
58
|
+
assert_equal 'llem', parsed[1].first
|
59
|
+
end
|
60
|
+
|
61
|
+
def test_quote_escaping
|
62
|
+
parsed = parse_query(' "hallo wi\\"llem"')
|
63
|
+
assert_equal 3, parsed.length
|
64
|
+
assert_equal 'hallo', parsed[0].first
|
65
|
+
assert_equal 'wi', parsed[1].first
|
66
|
+
assert_equal 'llem', parsed[2].first
|
67
|
+
|
68
|
+
parsed = parse_query('"\\"hallo willem\\""')
|
69
|
+
assert_equal 2, parsed.length
|
70
|
+
assert_equal 'hallo', parsed[0].first
|
71
|
+
assert_equal 'willem', parsed[1].first
|
72
|
+
end
|
73
|
+
|
74
|
+
def test_negation
|
75
|
+
parsed = parse_query('-willem')
|
76
|
+
assert_equal 1, parsed.length
|
77
|
+
assert_equal 'willem', parsed[0].first
|
78
|
+
assert_equal :not, parsed[0].last
|
79
|
+
|
80
|
+
parsed = parse_query('123 -"456 789"')
|
81
|
+
assert_equal 2, parsed.length
|
82
|
+
assert_equal '123', parsed[0].first
|
83
|
+
assert_equal :like, parsed[0].last
|
84
|
+
|
85
|
+
assert_equal '456 789', parsed[1].first
|
86
|
+
assert_equal :not, parsed[1].last
|
87
|
+
end
|
88
|
+
|
89
|
+
def test_or
|
90
|
+
parsed = parse_query('Wes OR Hays')
|
91
|
+
assert_equal 1, parsed.length
|
92
|
+
assert_equal 'Wes OR Hays', parsed[0][0]
|
93
|
+
assert_equal :or, parsed[0][1]
|
94
|
+
|
95
|
+
parsed = parse_query('"Man made" OR Dogs')
|
96
|
+
assert_equal 1, parsed.length
|
97
|
+
assert_equal 'Man made OR Dogs', parsed[0][0]
|
98
|
+
assert_equal :or, parsed[0][1]
|
99
|
+
|
100
|
+
parsed = parse_query('Cows OR "Frog Toys"')
|
101
|
+
assert_equal 1, parsed.length
|
102
|
+
assert_equal 'Cows OR Frog Toys', parsed[0][0]
|
103
|
+
assert_equal :or, parsed[0][1]
|
104
|
+
|
105
|
+
parsed = parse_query('"Happy cow" OR "Sad Frog"')
|
106
|
+
assert_equal 1, parsed.length
|
107
|
+
assert_equal 'Happy cow OR Sad Frog', parsed[0][0]
|
108
|
+
assert_equal :or, parsed[0][1]
|
109
|
+
end
|
110
|
+
|
111
|
+
def test_long_string
|
112
|
+
str = 'Wes -Hays "Hello World" -"Goodnight Moon" Bob OR Wes "Happy cow" OR "Sad Frog" "Man made" OR Dogs Cows OR "Frog Toys"'
|
113
|
+
parsed = parse_query(str)
|
114
|
+
assert_equal 8, parsed.length
|
115
|
+
|
116
|
+
assert_equal 'Wes', parsed[0].first
|
117
|
+
assert_equal :like, parsed[0].last
|
118
|
+
|
119
|
+
assert_equal 'Hays', parsed[1].first
|
120
|
+
assert_equal :not, parsed[1].last
|
121
|
+
|
122
|
+
assert_equal 'Hello World', parsed[2].first
|
123
|
+
assert_equal :like, parsed[2].last
|
124
|
+
|
125
|
+
assert_equal 'Goodnight Moon', parsed[3].first
|
126
|
+
assert_equal :not, parsed[3].last
|
127
|
+
|
128
|
+
assert_equal 'Bob OR Wes', parsed[4].first
|
129
|
+
assert_equal :or, parsed[4].last
|
130
|
+
|
131
|
+
assert_equal 'Happy cow OR Sad Frog', parsed[5].first
|
132
|
+
assert_equal :or, parsed[5].last
|
133
|
+
|
134
|
+
assert_equal 'Man made OR Dogs', parsed[6].first
|
135
|
+
assert_equal :or, parsed[6].last
|
136
|
+
|
137
|
+
assert_equal 'Cows OR Frog Toys', parsed[7].first
|
138
|
+
assert_equal :or, parsed[7].last
|
139
|
+
end
|
140
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper'
|
2
|
+
|
3
|
+
class ScopedSearchTest < Test::Unit::TestCase
|
4
|
+
|
5
|
+
def setup
|
6
|
+
setup_db
|
7
|
+
SearchTestModel.create_corpus!
|
8
|
+
end
|
9
|
+
|
10
|
+
def teardown
|
11
|
+
teardown_db
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_enabling
|
15
|
+
assert !SearchTestModel.respond_to?(:search_for)
|
16
|
+
SearchTestModel.searchable_on :string_field, :text_field
|
17
|
+
assert SearchTestModel.respond_to?(:search_for)
|
18
|
+
|
19
|
+
assert_equal ActiveRecord::NamedScope::Scope, SearchTestModel.search_for('test').class
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_search
|
24
|
+
SearchTestModel.searchable_on :string_field, :text_field
|
25
|
+
|
26
|
+
assert_equal 15, SearchTestModel.search_for('').count
|
27
|
+
assert_equal 0, SearchTestModel.search_for('456').count
|
28
|
+
assert_equal 2, SearchTestModel.search_for('hays').count
|
29
|
+
assert_equal 1, SearchTestModel.search_for('hay ob').count
|
30
|
+
assert_equal 13, SearchTestModel.search_for('o').count
|
31
|
+
assert_equal 2, SearchTestModel.search_for('-o').count
|
32
|
+
assert_equal 13, SearchTestModel.search_for('-Jim').count
|
33
|
+
assert_equal 1, SearchTestModel.search_for('Jim -Bush').count
|
34
|
+
assert_equal 1, SearchTestModel.search_for('"Hello World" -"Goodnight Moon"').count
|
35
|
+
assert_equal 2, SearchTestModel.search_for('Wes OR Bob').count
|
36
|
+
assert_equal 3, SearchTestModel.search_for('"Happy cow" OR "Sad Frog"').count
|
37
|
+
assert_equal 3, SearchTestModel.search_for('"Man made" OR Dogs').count
|
38
|
+
assert_equal 2, SearchTestModel.search_for('Cows OR "Frog Toys"').count
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
|
data/test/tasks.rake
ADDED
data/test/test_helper.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'rubygems'
|
3
|
+
require 'active_record'
|
4
|
+
|
5
|
+
require "#{File.dirname(__FILE__)}/../lib/scoped_search"
|
6
|
+
|
7
|
+
def setup_db
|
8
|
+
ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :dbfile => ":memory:")
|
9
|
+
ActiveRecord::Schema.define(:version => 1) do
|
10
|
+
create_table :search_test_models do |t|
|
11
|
+
t.string :string_field
|
12
|
+
t.text :text_field
|
13
|
+
t.string :ignored_field
|
14
|
+
t.timestamps
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def teardown_db
|
20
|
+
ActiveRecord::Base.connection.tables.each { |table| ActiveRecord::Base.connection.drop_table(table) }
|
21
|
+
end
|
22
|
+
|
23
|
+
class SearchTestModel < ActiveRecord::Base
|
24
|
+
def self.create_corpus!
|
25
|
+
create!(:string_field => "Programmer 123", :text_field => nil, :ignored_field => "123456")
|
26
|
+
create!(:string_field => "Jim", :text_field => "Henson", :ignored_field => "123456a")
|
27
|
+
create!(:string_field => "Jim", :text_field => "Bush", :ignored_field => "123456b")
|
28
|
+
create!(:string_field => "Wes", :text_field => "Hays", :ignored_field => "123456c")
|
29
|
+
create!(:string_field => "Bob", :text_field => "Hays", :ignored_field => "123456d")
|
30
|
+
create!(:string_field => "Dogs", :text_field => "Pit Bull", :ignored_field => "123456e")
|
31
|
+
create!(:string_field => "Dogs", :text_field => "Eskimo", :ignored_field => "123456f")
|
32
|
+
create!(:string_field => "Cows", :text_field => "Farms", :ignored_field => "123456g")
|
33
|
+
create!(:string_field => "Hello World", :text_field => "Hello Moon", :ignored_field => "123456h")
|
34
|
+
create!(:string_field => "Hello World", :text_field => "Goodnight Moon", :ignored_field => "123456i")
|
35
|
+
create!(:string_field => "Happy Cow", :text_field => "Sad Cow", :ignored_field => "123456j")
|
36
|
+
create!(:string_field => "Happy Frog", :text_field => "Sad Frog", :ignored_field => "123456k")
|
37
|
+
create!(:string_field => "Excited Frog", :text_field => "Sad Frog", :ignored_field => "123456l")
|
38
|
+
create!(:string_field => "Man made", :text_field => "Woman made", :ignored_field => "123456m")
|
39
|
+
create!(:string_field => "Cat Toys", :text_field => "Frog Toys", :ignored_field => "123456n")
|
40
|
+
end
|
41
|
+
end
|
metadata
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: gbdev-scoped_search
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Willem van Bergen
|
8
|
+
- Wes Hays
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2008-09-13 00:00:00 -07:00
|
14
|
+
default_executable:
|
15
|
+
dependencies: []
|
16
|
+
|
17
|
+
description: Scoped search makes it easy to search your ActiveRecord-based models. It will create a named scope according to a provided query string. The named_scope can be used like any other named_scope, so it can be cchained or combined with will_paginate.
|
18
|
+
email:
|
19
|
+
- willem@vanbergen.org
|
20
|
+
- weshays@gbdev.com
|
21
|
+
executables: []
|
22
|
+
|
23
|
+
extensions: []
|
24
|
+
|
25
|
+
extra_rdoc_files: []
|
26
|
+
|
27
|
+
files:
|
28
|
+
- LICENSE
|
29
|
+
- README.rdoc
|
30
|
+
- Rakefile
|
31
|
+
- TODO
|
32
|
+
- init.rb
|
33
|
+
- lib
|
34
|
+
- lib/scoped_search
|
35
|
+
- lib/scoped_search.rb
|
36
|
+
- lib/scoped_search/query_language_parser.rb
|
37
|
+
- test
|
38
|
+
- test/query_language_test.rb
|
39
|
+
- test/search_for_test.rb
|
40
|
+
- test/tasks.rake
|
41
|
+
- test/test_helper.rb
|
42
|
+
has_rdoc: false
|
43
|
+
homepage: http://github.com/wvanbergen/scoped_search/wikis
|
44
|
+
post_install_message:
|
45
|
+
rdoc_options: []
|
46
|
+
|
47
|
+
require_paths:
|
48
|
+
- lib
|
49
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: "0"
|
54
|
+
version:
|
55
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: "0"
|
60
|
+
version:
|
61
|
+
requirements: []
|
62
|
+
|
63
|
+
rubyforge_project:
|
64
|
+
rubygems_version: 1.2.0
|
65
|
+
signing_key:
|
66
|
+
specification_version: 2
|
67
|
+
summary: A Rails plugin to search your models using a named_scope
|
68
|
+
test_files:
|
69
|
+
- test/query_language_test.rb
|
70
|
+
- test/search_for_test.rb
|