wvanbergen-scoped_search 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
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,57 @@
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 +search_for+ that uses SQL %LIKE% conditions for searching.
4
+
5
+ == Installation
6
+
7
+ You can enable scoped_search as a Ruby gem. First, you have to install the gem on your system.
8
+
9
+ gem sources -a http://gems.github.com
10
+ sudo gem install wvanbergen-scoped_search
11
+
12
+ Now, you must enable the gem in your +environment.rb+ configuration:
13
+
14
+ Rails::Initializer.run do |config|
15
+ ...
16
+ config.gem 'wvanbergen-scoped_search', :lib => 'scoped_search'
17
+ end
18
+
19
+ You can use scoped_search as a Rails plugin as well, but this is deprecated. Simply download or
20
+ clone scoped_search into your +vendor/plugins+ directory of your project.
21
+
22
+ == Usage
23
+
24
+ First, you have to specify in what columns should be searched:
25
+
26
+ class Project < ActiveRecord::Base
27
+ searchable_on :name, :description
28
+ end
29
+
30
+ 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).
31
+
32
+ Project.search_for(params[:q]).each { |project| ... }
33
+
34
+ The search query language is simple. It supports these constructs:
35
+
36
+ * words: some search keywords
37
+ * phrases: "a single search phrase"
38
+ * negation: "look for this" -"but do not look for this phrase and this" -word
39
+
40
+ This functionality is build on named_scope. The searchable_on statement creates
41
+ a named_scope "search_for". Because of this, you can actually chain the call with
42
+ other scopes. For example, this can be very useful if you only want to search in
43
+ projects that are accessible by a given user.
44
+
45
+ class Project < ActiveRecord::Base
46
+ searchable_on :name, :description
47
+ named_scope :accessible_by, lambda { |user| ... }
48
+ end
49
+
50
+ # using chained named_scopes and will_paginate
51
+ Project.accessible_by(current_user).search_for(params[:q]).paginate(:page => params[:page], :include => :tasks)
52
+
53
+ == Disclaimer
54
+
55
+ This Rails plugin is written by Willem van Bergen for the Floorplanner.com website. It is
56
+ released under the <b>MIT license</b>. Please contact me (willem AT vanbergen DOT org if
57
+ 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,14 @@
1
+ TODO items for named_scope
2
+ ==========================
3
+ Contact willem AT vanbergen DOT org if you want to help out
4
+
5
+
6
+ New functionality:
7
+ - Search model associations as well
8
+ - Allow OR-queries
9
+
10
+ Refactoring:
11
+ - Better separation of query building and query string parser
12
+
13
+ Testing:
14
+ - More tests for the query string parser
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'scoped_search'
@@ -0,0 +1,70 @@
1
+ module ScopedSearch::QueryStringParser
2
+
3
+ def to_search_query
4
+ items = lex_for_query_string_parsing
5
+ search_conditions = []
6
+
7
+ negate = false
8
+ items.each do |item|
9
+ case item
10
+ when :not
11
+ negate = true
12
+ else
13
+ search_conditions << (negate ? [item, :not] : [item])
14
+ negate = false
15
+ end
16
+ end
17
+ return search_conditions
18
+ end
19
+
20
+ def lex_for_query_string_parsing
21
+ terms = []
22
+ current_term = ""
23
+ quoted_string_openend = false
24
+
25
+ self.each_char do |char|
26
+
27
+ case char
28
+ when /\s/
29
+ if quoted_string_openend
30
+ current_term << char
31
+ elsif current_term.length > 0
32
+ terms << current_term
33
+ current_term = ""
34
+ end
35
+
36
+ when '-'
37
+ if quoted_string_openend || current_term.length > 0
38
+ current_term << char
39
+ else
40
+ terms << :not
41
+ end
42
+
43
+ when '"'
44
+ if quoted_string_openend
45
+ if current_term.length > 0
46
+ if current_term[-1,1] == "\\"
47
+ current_term[-1] = char
48
+ else
49
+ terms << current_term
50
+ current_term = ""
51
+ quoted_string_openend = false
52
+ end
53
+ else
54
+ quoted_string_openend = false
55
+ end
56
+
57
+ else
58
+ quoted_string_openend = true
59
+ end
60
+
61
+ else
62
+ current_term << char
63
+ end
64
+
65
+ end
66
+ terms << current_term if current_term.length > 0
67
+ return terms
68
+ end
69
+
70
+ end
@@ -0,0 +1,53 @@
1
+
2
+
3
+ module ScopedSearch
4
+
5
+ module ClassMethods
6
+
7
+ # Creates a named scope in the class it was called upon
8
+ def searchable_on(*fields)
9
+ self.cattr_accessor :scoped_search_fields
10
+ self.scoped_search_fields = fields
11
+ self.named_scope :search_for, lambda { |keywords| self.build_scoped_search_conditions(keywords) }
12
+ end
13
+
14
+ # Build a hash that is used for the named_scope search_for.
15
+ # This function will split the search_string into keywords, and search for all the keywords
16
+ # in the fields that were provided to searchable_on
17
+ def build_scoped_search_conditions(search_string)
18
+ if search_string.nil? || search_string.strip.blank?
19
+ return { :conditions => nil }
20
+ else
21
+ conditions = []
22
+ query_params = {}
23
+ class << search_string; include ScopedSearch::QueryStringParser; end # TODO: fix me!
24
+
25
+ search_string.to_search_query.each_with_index do |search_condition, index|
26
+ keyword_name = "keyword_#{index}".to_sym
27
+ query_params[keyword_name] = "%#{search_condition.first}%"
28
+
29
+ # a keyword may be found in any of the provided fields, so join the conitions with OR
30
+ if search_condition.length == 2 && search_condition.last == :not
31
+ keyword_conditions = self.scoped_search_fields.map do |field|
32
+ field_name = connection.quote_table_name(table_name) + "." + connection.quote_column_name(field)
33
+ "(#{field_name} NOT LIKE :#{keyword_name.to_s} OR #{field_name} IS NULL)"
34
+ end
35
+ conditions << "(#{keyword_conditions.join(' AND ')})"
36
+ else
37
+ keyword_conditions = self.scoped_search_fields.map do |field|
38
+ field_name = connection.quote_table_name(table_name) + "." + connection.quote_column_name(field)
39
+ "#{field_name} LIKE :#{keyword_name.to_s}"
40
+ end
41
+ conditions << "(#{keyword_conditions.join(' OR ')})"
42
+ end
43
+ end
44
+
45
+ # all keywords must be matched, so join the conditions with AND
46
+ return { :conditions => [conditions.join(' AND '), query_params] }
47
+ end
48
+ end
49
+ end
50
+ end
51
+
52
+ require 'scoped_search/query_string_parser'
53
+ ActiveRecord::Base.send(:extend, ScopedSearch::ClassMethods)
@@ -0,0 +1,40 @@
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 3, SearchTestModel.search_for('123').count
27
+ assert_equal 3, SearchTestModel.search_for('haLL').count
28
+ assert_equal 1, SearchTestModel.search_for('456').count
29
+ assert_equal 2, SearchTestModel.search_for('ha 23').count
30
+ assert_equal 0, SearchTestModel.search_for('wi').count
31
+
32
+ assert_equal 1, SearchTestModel.search_for('-hallo').count
33
+ assert_equal 4, SearchTestModel.search_for('-wi').count
34
+ assert_equal 3, SearchTestModel.search_for('-789').count
35
+ assert_equal 2, SearchTestModel.search_for('123 -456').count
36
+ end
37
+
38
+ end
39
+
40
+
@@ -0,0 +1,74 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ class String
4
+ include ScopedSearch::QueryStringParser
5
+ end
6
+
7
+ class QueryStringParserTest < Test::Unit::TestCase
8
+
9
+ def test_empty_search_query
10
+ parsed = ''.lex_for_query_string_parsing
11
+ assert_equal 0, parsed.length
12
+
13
+ parsed = "\t \n".lex_for_query_string_parsing
14
+ assert_equal 0, parsed.length
15
+ end
16
+
17
+ def test_single_keyword
18
+
19
+ parsed = 'hallo'.lex_for_query_string_parsing
20
+ assert_equal 1, parsed.length
21
+ assert_equal 'hallo', parsed.first
22
+
23
+ parsed = ' hallo '.lex_for_query_string_parsing
24
+ assert_equal 1, parsed.length
25
+ assert_equal 'hallo', parsed.first
26
+ end
27
+
28
+ def test_multiple_keywords
29
+ parsed = ' hallo willem'.lex_for_query_string_parsing
30
+ assert_equal 2, parsed.length
31
+ assert_equal 'willem', parsed.last
32
+ end
33
+
34
+ def test_quoted_keywords
35
+ parsed = ' "hallo"'.lex_for_query_string_parsing
36
+ assert_equal 1, parsed.length
37
+ assert_equal 'hallo', parsed.first
38
+
39
+ parsed = ' "hallo willem"'.lex_for_query_string_parsing
40
+ assert_equal 1, parsed.length
41
+ assert_equal 'hallo willem', parsed.first
42
+
43
+ parsed = ' "hallo willem'.lex_for_query_string_parsing
44
+ assert_equal 1, parsed.length
45
+ assert_equal 'hallo willem', parsed.first
46
+
47
+ parsed = ' "hallo wi"llem"'.lex_for_query_string_parsing
48
+ assert_equal 2, parsed.length
49
+ assert_equal 'hallo wi', parsed.first
50
+ assert_equal 'llem', parsed.last
51
+ end
52
+
53
+ def test_quote_escaping
54
+ parsed = ' "hallo wi\\"llem"'.lex_for_query_string_parsing
55
+ assert_equal 1, parsed.length
56
+ assert_equal 'hallo wi"llem', parsed.first
57
+
58
+ parsed = '"\\"hallo willem\\""'.lex_for_query_string_parsing
59
+ assert_equal 1, parsed.length
60
+ assert_equal '"hallo willem"', parsed.first
61
+ end
62
+
63
+ def test_negation
64
+ parsed = '-willem'.lex_for_query_string_parsing
65
+ assert_equal 2, parsed.length
66
+ assert_equal :not, parsed.first
67
+
68
+ parsed = '123 -"456 789"'.lex_for_query_string_parsing
69
+ assert_equal 3, parsed.length
70
+ assert_equal '123', parsed[0]
71
+ assert_equal :not, parsed[1]
72
+ assert_equal '456 789', parsed[2]
73
+ end
74
+ end
data/test/tasks.rake ADDED
@@ -0,0 +1,8 @@
1
+ require 'rake/testtask'
2
+
3
+ desc 'Test the scoped_search plugin.'
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.pattern = 'test/**/*_test.rb'
6
+ t.verbose = true
7
+ t.libs << 'test'
8
+ end
@@ -0,0 +1,30 @@
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 => "123", :text_field => "Hallo", :ignored_field => "123 willem")
26
+ create!(:string_field => "456", :text_field => "Hallo 123", :ignored_field => "123")
27
+ create!(:string_field => "789", :text_field => "HALLO", :ignored_field => "123456");
28
+ create!(:string_field => "123", :text_field => nil, :ignored_field => "123456");
29
+ end
30
+ end
metadata ADDED
@@ -0,0 +1,67 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: wvanbergen-scoped_search
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.2
5
+ platform: ruby
6
+ authors:
7
+ - Willem van Bergen
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-09-04 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ 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.
17
+ email: willem@vanbergen.org
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files: []
23
+
24
+ files:
25
+ - LICENSE
26
+ - README.rdoc
27
+ - Rakefile
28
+ - TODO
29
+ - init.rb
30
+ - lib
31
+ - lib/scoped_search
32
+ - lib/scoped_search.rb
33
+ - lib/scoped_search/query_string_parser.rb
34
+ - test
35
+ - test/query_building_test.rb
36
+ - test/query_string_parser_test.rb
37
+ - test/tasks.rake
38
+ - test/test_helper.rb
39
+ has_rdoc: false
40
+ homepage: http://github.com/wvanbergen/scoped_search/wikis
41
+ post_install_message:
42
+ rdoc_options: []
43
+
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: "0"
57
+ version:
58
+ requirements: []
59
+
60
+ rubyforge_project:
61
+ rubygems_version: 1.2.0
62
+ signing_key:
63
+ specification_version: 2
64
+ summary: A Rails plugin to search your models using a named_scope
65
+ test_files:
66
+ - test/query_building_test.rb
67
+ - test/query_string_parser_test.rb