pollyanna 1.0.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.
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Henning Koch
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.
@@ -0,0 +1,15 @@
1
+ = Pollyanna - very simple search
2
+
3
+ == Make a model searchable
4
+
5
+ - Add a text column "search_text" to a model
6
+ - Have the model include Pollyanna::Searchable
7
+ - Overwrite #search_text to define which text is indexed upon saving
8
+ - Get a scope for your match with Model.search("query words here")
9
+ - There's some more goodness I don't care to document. Use the code, Luke!
10
+
11
+ == Credits
12
+
13
+ Henning Koch
14
+
15
+ {www.makandra.de}[http://www.makandra.de/]
@@ -0,0 +1,37 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+
5
+ desc 'Default: run unit tests.'
6
+ task :default => :test
7
+
8
+ desc 'Test the pollyanna gem.'
9
+ Rake::TestTask.new(:test) do |t|
10
+ t.libs << 'lib'
11
+ t.pattern = 'test/**/*_test.rb'
12
+ t.verbose = true
13
+ end
14
+
15
+ desc 'Generate documentation for the pollyanna gem.'
16
+ Rake::RDocTask.new(:rdoc) do |rdoc|
17
+ rdoc.rdoc_dir = 'rdoc'
18
+ rdoc.title = 'pollyanna'
19
+ rdoc.options << '--line-numbers' << '--inline-source'
20
+ rdoc.rdoc_files.include('README')
21
+ rdoc.rdoc_files.include('lib/**/*.rb')
22
+ end
23
+
24
+ begin
25
+ require 'jeweler'
26
+ Jeweler::Tasks.new do |gemspec|
27
+ gemspec.name = "pollyanna"
28
+ gemspec.summary = "Very simple search for your ActiveRecord models."
29
+ gemspec.email = "github@makandra.de"
30
+ gemspec.homepage = "http://github.com/makandra/pollyanna"
31
+ gemspec.description = "Very simple search for your ActiveRecord models."
32
+ gemspec.authors = ["Henning Koch"]
33
+ end
34
+ rescue LoadError
35
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
36
+ end
37
+
@@ -0,0 +1,39 @@
1
+ module Pollyanna
2
+ class Search
3
+
4
+ def initialize(query, table_name)
5
+ @words = (query || "").strip.downcase.split(/[\ \,\;]+/)
6
+ @table_name = table_name
7
+ end
8
+
9
+ def scope_options(options = {})
10
+ by = options && options.delete(:by)
11
+ { :conditions => conditions_for_find(by) }
12
+ end
13
+
14
+ # # Run the search on a Ruby collection
15
+ # def select(objects, &content_method)
16
+ # content_method ||= &:search_text
17
+ # objects.select do |object|
18
+ # content = content_method.call(object).andand.downcase || ""
19
+ # @words.all? { |word| content.include? word }
20
+ # end
21
+ # end
22
+
23
+ private
24
+
25
+ def conditions_for_find(by = nil)
26
+ unless @words.empty?
27
+ by ||= :search_text
28
+ patterns = @words.collect { |word| "%#{escape_query_word word}%" }
29
+ likes = ["#{@table_name}.#{by} LIKE ?"] * patterns.count
30
+ [likes.join(" AND "), *patterns]
31
+ end
32
+ end
33
+
34
+ def escape_query_word(word)
35
+ word.gsub("%", "\\%").gsub("_", "\\_")
36
+ end
37
+
38
+ end
39
+ end
@@ -0,0 +1,17 @@
1
+ module Pollyanna
2
+ module Searchable
3
+
4
+ def self.included(klass)
5
+ klass.class_eval do
6
+ before_save :set_search_text
7
+ # Use Proc.new so the number of arguments does not matter
8
+ named_scope :search, Proc.new { |query, options| Search.new(query, table_name).scope_options(options) }
9
+ end
10
+ end
11
+
12
+ def set_search_text
13
+ self.search_text = search_text if respond_to?(:"search_text=")
14
+ end
15
+
16
+ end
17
+ end
@@ -0,0 +1,2 @@
1
+ class ApplicationController < ActionController::Base
2
+ end
@@ -0,0 +1,12 @@
1
+ class Movie < ActiveRecord::Base
2
+ include Pollyanna::Searchable
3
+
4
+ def search_text
5
+ "#{title} #{year}"
6
+ end
7
+
8
+ def <=>(other)
9
+ title.downcase <=> other.downcase
10
+ end
11
+
12
+ end
@@ -0,0 +1,114 @@
1
+ # Allow customization of the rails framework path
2
+ RAILS_FRAMEWORK_ROOT = (ENV['RAILS_FRAMEWORK_ROOT'] || "#{File.dirname(__FILE__)}/../../../../../../vendor/rails") unless defined?(RAILS_FRAMEWORK_ROOT)
3
+
4
+ # Don't change this file!
5
+ # Configure your app in config/environment.rb and config/environments/*.rb
6
+
7
+ RAILS_ROOT = "#{File.dirname(__FILE__)}/.." unless defined?(RAILS_ROOT)
8
+
9
+ module Rails
10
+ class << self
11
+ def boot!
12
+ unless booted?
13
+ preinitialize
14
+ pick_boot.run
15
+ end
16
+ end
17
+
18
+ def booted?
19
+ defined? Rails::Initializer
20
+ end
21
+
22
+ def pick_boot
23
+ (vendor_rails? ? VendorBoot : GemBoot).new
24
+ end
25
+
26
+ def vendor_rails?
27
+ File.exist?(RAILS_FRAMEWORK_ROOT)
28
+ end
29
+
30
+ def preinitialize
31
+ load(preinitializer_path) if File.exist?(preinitializer_path)
32
+ end
33
+
34
+ def preinitializer_path
35
+ "#{RAILS_ROOT}/config/preinitializer.rb"
36
+ end
37
+ end
38
+
39
+ class Boot
40
+ def run
41
+ load_initializer
42
+ Rails::Initializer.run(:set_load_path)
43
+ end
44
+ end
45
+
46
+ class VendorBoot < Boot
47
+ def load_initializer
48
+ require "#{RAILS_FRAMEWORK_ROOT}/railties/lib/initializer"
49
+ Rails::Initializer.run(:install_gem_spec_stubs)
50
+ end
51
+ end
52
+
53
+ class GemBoot < Boot
54
+ def load_initializer
55
+ self.class.load_rubygems
56
+ load_rails_gem
57
+ require 'initializer'
58
+ end
59
+
60
+ def load_rails_gem
61
+ if version = self.class.gem_version
62
+ gem 'rails', version
63
+ else
64
+ gem 'rails'
65
+ end
66
+ rescue Gem::LoadError => load_error
67
+ $stderr.puts %(Missing the Rails #{version} gem. Please `gem install -v=#{version} rails`, update your RAILS_GEM_VERSION setting in config/environment.rb for the Rails version you do have installed, or comment out RAILS_GEM_VERSION to use the latest version installed.)
68
+ exit 1
69
+ end
70
+
71
+ class << self
72
+ def rubygems_version
73
+ Gem::RubyGemsVersion rescue nil
74
+ end
75
+
76
+ def gem_version
77
+ if defined? RAILS_GEM_VERSION
78
+ RAILS_GEM_VERSION
79
+ elsif ENV.include?('RAILS_GEM_VERSION')
80
+ ENV['RAILS_GEM_VERSION']
81
+ else
82
+ parse_gem_version(read_environment_rb)
83
+ end
84
+ end
85
+
86
+ def load_rubygems
87
+ require 'rubygems'
88
+ min_version = '1.1.1'
89
+ unless rubygems_version >= min_version
90
+ $stderr.puts %Q(Rails requires RubyGems >= #{min_version} (you have #{rubygems_version}). Please `gem update --system` and try again.)
91
+ exit 1
92
+ end
93
+
94
+ rescue LoadError
95
+ $stderr.puts %Q(Rails requires RubyGems >= #{min_version}. Please install RubyGems and try again: http://rubygems.rubyforge.org)
96
+ exit 1
97
+ end
98
+
99
+ def parse_gem_version(text)
100
+ $1 if text =~ /^[^#]*RAILS_GEM_VERSION\s*=\s*["']([!~<>=]*\s*[\d.]+)["']/
101
+ end
102
+
103
+ private
104
+ def read_environment_rb
105
+ environment_rb = "#{RAILS_ROOT}/config/environment.rb"
106
+ environment_rb = "#{HELPER_RAILS_ROOT}/config/environment.rb" unless File.exists?(environment_rb)
107
+ File.read(environment_rb)
108
+ end
109
+ end
110
+ end
111
+ end
112
+
113
+ # All that for this:
114
+ Rails.boot!
@@ -0,0 +1,21 @@
1
+ in_memory:
2
+ adapter: sqlite3
3
+ database: ":memory:"
4
+ verbosity: quiet
5
+ sqlite:
6
+ adapter: sqlite
7
+ dbfile: plugin_test.sqlite.db
8
+ sqlite3:
9
+ adapter: sqlite3
10
+ dbfile: plugin_test.sqlite3.db
11
+ postgresql:
12
+ adapter: postgresql
13
+ username: postgres
14
+ password: postgres
15
+ database: plugin_test
16
+ mysql:
17
+ adapter: mysql
18
+ host: localhost
19
+ username: root
20
+ password:
21
+ database: plugin_test
@@ -0,0 +1,14 @@
1
+ require File.join(File.dirname(__FILE__), 'boot')
2
+
3
+ Rails::Initializer.run do |config|
4
+ config.cache_classes = false
5
+ config.whiny_nils = true
6
+ config.action_controller.session = { :key => "_myapp_session", :secret => "gwirofjweroijger8924rt2zfwehfuiwehb1378rifowenfoqwphf23" }
7
+ config.plugin_locators.unshift(
8
+ Class.new(Rails::Plugin::Locator) do
9
+ def plugins
10
+ [Rails::Plugin.new(File.expand_path('.'))]
11
+ end
12
+ end
13
+ ) unless defined?(PluginTestHelper::PluginLocator)
14
+ end
@@ -0,0 +1,4 @@
1
+ ActionController::Routing::Routes.draw do |map|
2
+ map.connect ':controller/:action/:id'
3
+ map.connect ':controller/:action/:id.:format'
4
+ end
@@ -0,0 +1,16 @@
1
+ class CreateMovies < ActiveRecord::Migration
2
+
3
+ def self.up
4
+ create_table :movies do |t|
5
+ t.string :title
6
+ t.integer :year
7
+ t.text :search_text
8
+ t.string :director
9
+ end
10
+ end
11
+
12
+ def self.down
13
+ drop_table :movies
14
+ end
15
+
16
+ end
@@ -0,0 +1,4 @@
1
+ # Loads fixtures into the database when running the test app via the console
2
+ (ENV['FIXTURES'] ? ENV['FIXTURES'].split(/,/) : Dir.glob(File.join(Rails.root, '../fixtures/*.{yml,csv}'))).each do |fixture_file|
3
+ Fixtures.create_fixtures(File.join(Rails.root, '../fixtures'), File.basename(fixture_file, '.*'))
4
+ end
@@ -0,0 +1 @@
1
+ *.log
@@ -0,0 +1,7 @@
1
+ irb = RUBY_PLATFORM =~ /(:?mswin|mingw)/ ? 'irb.bat' : 'irb'
2
+ libs = " -r irb/completion"
3
+ libs << " -r test/test_helper"
4
+ libs << " -r console_app"
5
+ libs << " -r console_with_helpers"
6
+ libs << " -r console_with_fixtures"
7
+ exec "#{irb} #{libs} --simple-prompt"
@@ -0,0 +1,2 @@
1
+ --exclude "spec/*,gems/*"
2
+ --rails
@@ -0,0 +1,50 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ describe Pollyanna::Search do
4
+
5
+ describe 'scope_options' do
6
+
7
+ it "should hand a :by option to conditions_for_find" do
8
+ search = Pollyanna::Search.new("foo", "movies")
9
+ search.should_receive(:conditions_for_find).with(:title)
10
+ search.scope_options(:by => :title)
11
+ end
12
+
13
+ it "should return an options hash for a Rails scope" do
14
+ search = Pollyanna::Search.new("foo", "movies")
15
+ search.should_receive(:conditions_for_find).and_return("sql query")
16
+ search.scope_options.should == { :conditions => "sql query" }
17
+ end
18
+
19
+ end
20
+
21
+ describe 'conditions_for_find' do
22
+
23
+ it "should return a LIKE query for the given query, looking in the search_text column by default and using the table name to disambiguate things" do
24
+ search = Pollyanna::Search.new("word", "movies")
25
+ search.send(:conditions_for_find).should == ['movies.search_text LIKE ?', "%word%"]
26
+ end
27
+
28
+ it "should look in another column if the #by argument is given" do
29
+ search = Pollyanna::Search.new("word", "movies")
30
+ search.send(:conditions_for_find, "words").should == ['movies.words LIKE ?', "%word%"]
31
+ end
32
+
33
+ it "should return a conjunction of LIKE queries for queries with multiple words" do
34
+ search = Pollyanna::Search.new("foo bar", "movies")
35
+ search.send(:conditions_for_find).should == ['movies.search_text LIKE ? AND movies.search_text LIKE ?', '%foo%', '%bar%']
36
+ end
37
+
38
+ it "should escape underscores in the query" do
39
+ search = Pollyanna::Search.new("foo_bar", "movies")
40
+ search.send(:conditions_for_find).should == ['movies.search_text LIKE ?', "%foo\\_bar%"]
41
+ end
42
+
43
+ it "should escape percent signs in the query" do
44
+ search = Pollyanna::Search.new("foo%bar", "movies")
45
+ search.send(:conditions_for_find).should == ['movies.search_text LIKE ?', "%foo\\%bar%"]
46
+ end
47
+
48
+ end
49
+
50
+ end
@@ -0,0 +1,33 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ describe Pollyanna::Searchable do
4
+
5
+ before(:each) do
6
+ @starship_troopers = Movie.create!(:title => "Starship Troopers", :year => 1997, :director => "Paul Verhoeven")
7
+ @fifth_element = Movie.create!(:title => "The Fifth Element", :year => 1997, :director => "Luc Besson")
8
+ @matrix = Movie.create!(:title => "The Matrix", :year => 1999, :director => "Wachowski brothers")
9
+ end
10
+
11
+ it 'should index its search_text upon save' do
12
+ @starship_troopers.should_receive("search_text=")
13
+ @starship_troopers.save
14
+ end
15
+
16
+ it "use the search_text column to find matches" do
17
+ Movie.search("1997").all.should == [@starship_troopers, @fifth_element]
18
+ end
19
+
20
+ it "should find single words" do
21
+ Movie.search("The").all.should == [@fifth_element, @matrix]
22
+ end
23
+
24
+ it "should find multiple words using AND" do
25
+ Movie.search("Matrix the").all.should == [@matrix]
26
+ end
27
+
28
+ it "should find records by another column than search_text" do
29
+ Movie.search("Paul").should be_empty
30
+ Movie.search("Paul", :by => :director).should == [@starship_troopers]
31
+ end
32
+
33
+ end
@@ -0,0 +1,4 @@
1
+ --colour
2
+ --format progress
3
+ --loadby mtime
4
+ --reverse
@@ -0,0 +1,20 @@
1
+ $: << File.join(File.dirname(__FILE__), "/../lib" )
2
+
3
+ # Set the default environment to sqlite3's in_memory database
4
+ ENV['RAILS_ENV'] ||= 'in_memory'
5
+
6
+ # Load the Rails environment and testing framework
7
+ require "#{File.dirname(__FILE__)}/app_root/config/environment"
8
+ require "#{File.dirname(__FILE__)}/../init"
9
+ require 'spec/rails'
10
+
11
+ # Undo changes to RAILS_ENV
12
+ silence_warnings {RAILS_ENV = ENV['RAILS_ENV']}
13
+
14
+ # Run the migrations
15
+ ActiveRecord::Migrator.migrate("#{Rails.root}/db/migrate")
16
+
17
+ Spec::Runner.configure do |config|
18
+ config.use_transactional_fixtures = true
19
+ config.use_instantiated_fixtures = false
20
+ end
metadata ADDED
@@ -0,0 +1,93 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pollyanna
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Henning Koch
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2010-02-13 00:00:00 +01:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: Very simple search for your ActiveRecord models.
17
+ email: github@makandra.de
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - README.rdoc
24
+ files:
25
+ - MIT-LICENSE
26
+ - README.rdoc
27
+ - Rakefile
28
+ - lib/pollyanna/search.rb
29
+ - lib/pollyanna/searchable.rb
30
+ - spec/app_root/app/controllers/application_controller.rb
31
+ - spec/app_root/app/models/movie.rb
32
+ - spec/app_root/config/boot.rb
33
+ - spec/app_root/config/database.yml
34
+ - spec/app_root/config/environment.rb
35
+ - spec/app_root/config/environments/in_memory.rb
36
+ - spec/app_root/config/environments/mysql.rb
37
+ - spec/app_root/config/environments/postgresql.rb
38
+ - spec/app_root/config/environments/sqlite.rb
39
+ - spec/app_root/config/environments/sqlite3.rb
40
+ - spec/app_root/config/routes.rb
41
+ - spec/app_root/db/migrate/001_create_movies.rb
42
+ - spec/app_root/lib/console_with_fixtures.rb
43
+ - spec/app_root/log/.gitignore
44
+ - spec/app_root/script/console
45
+ - spec/rcov.opts
46
+ - spec/search_spec.rb
47
+ - spec/searchable_spec.rb
48
+ - spec/spec.opts
49
+ - spec/spec_helper.rb
50
+ has_rdoc: true
51
+ homepage: http://github.com/makandra/pollyanna
52
+ licenses: []
53
+
54
+ post_install_message:
55
+ rdoc_options:
56
+ - --charset=UTF-8
57
+ require_paths:
58
+ - lib
59
+ required_ruby_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: "0"
64
+ version:
65
+ required_rubygems_version: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: "0"
70
+ version:
71
+ requirements: []
72
+
73
+ rubyforge_project:
74
+ rubygems_version: 1.3.5
75
+ signing_key:
76
+ specification_version: 3
77
+ summary: Very simple search for your ActiveRecord models.
78
+ test_files:
79
+ - spec/search_spec.rb
80
+ - spec/app_root/app/models/movie.rb
81
+ - spec/app_root/app/controllers/application_controller.rb
82
+ - spec/app_root/config/environment.rb
83
+ - spec/app_root/config/environments/mysql.rb
84
+ - spec/app_root/config/environments/postgresql.rb
85
+ - spec/app_root/config/environments/sqlite3.rb
86
+ - spec/app_root/config/environments/in_memory.rb
87
+ - spec/app_root/config/environments/sqlite.rb
88
+ - spec/app_root/config/boot.rb
89
+ - spec/app_root/config/routes.rb
90
+ - spec/app_root/db/migrate/001_create_movies.rb
91
+ - spec/app_root/lib/console_with_fixtures.rb
92
+ - spec/searchable_spec.rb
93
+ - spec/spec_helper.rb