smart_filters 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Rizwan Reza
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.md ADDED
@@ -0,0 +1,6 @@
1
+ Smart Filters
2
+ =============
3
+
4
+ Smart Filters is an implementation of what you see in the Smart Playlist dialog in iTunes but using ActiveRecord model as the table and columns as the data. It is wise enough to select different criteria based on the column type.
5
+
6
+ Copyright (c) 2010 Rizwan Reza for Monaqasat, released under the MIT license.
data/Rakefile ADDED
@@ -0,0 +1,40 @@
1
+ require 'rake'
2
+ require 'rake/rdoctask'
3
+ require 'rake/gempackagetask'
4
+
5
+ desc 'Generate documentation for the smart_filters plugin.'
6
+ Rake::RDocTask.new(:rdoc) do |rdoc|
7
+ rdoc.rdoc_dir = 'rdoc'
8
+ rdoc.title = 'SmartFilters'
9
+ rdoc.options << '--line-numbers' << '--inline-source'
10
+ rdoc.rdoc_files.include('README.md')
11
+ rdoc.rdoc_files.include('lib/**/*.rb')
12
+ end
13
+
14
+ PKG_FILES = FileList[
15
+ '[a-zA-Z]*',
16
+ 'app/**/*',
17
+ 'lib/**/*',
18
+ 'rails/**/*',
19
+ 'spec/**/*'
20
+ ]
21
+
22
+ spec = Gem::Specification.new do |s|
23
+ s.name = "smart_filters"
24
+ s.version = "0.0.1"
25
+ s.author = "Rizwan Reza"
26
+ s.email = "contact@rizwanreza.com"
27
+ s.homepage = "http://github.com/Monaqasat/smart_filters"
28
+ s.platform = Gem::Platform::RUBY
29
+ s.summary = "Quickly create smart filters for any ActiveRecord model."
30
+ s.files = PKG_FILES.to_a
31
+ s.description = "Smart Filters is an implementation of what you see in the Smart Playlist dialog in iTunes but using ActiveRecord model as the table and columns as the data. It is wise enough to select different criteria based on the column type."
32
+ s.require_path = "lib"
33
+ s.has_rdoc = false
34
+ s.extra_rdoc_files = ["README.md"]
35
+ end
36
+
37
+ desc 'Turn this plugin into a gem.'
38
+ Rake::GemPackageTask.new(spec) do |pkg|
39
+ pkg.gem_spec = spec
40
+ end
@@ -0,0 +1,15 @@
1
+ <table>
2
+ <tr>
3
+ <% @filtered_results.first.attributes.each do |column, value| %>
4
+ <th><%= column.humanize %></th>
5
+ <% end %>
6
+ </tr>
7
+
8
+ <% @filtered_results.each do |record| %>
9
+ <tr>
10
+ <% record.attributes.each do |attribute, value| %>
11
+ <td><%=h value %></td>
12
+ <% end %>
13
+ </tr>
14
+ <% end %>
15
+ </table>
@@ -0,0 +1,11 @@
1
+ def sort_smart_filter
2
+ if params[:smart_filter]
3
+ search = params[:smart_filter]
4
+ hash = {}
5
+ search.delete_if {|column, value| value[:value] == "" }
6
+ search.each do |column, value|
7
+ hash.merge!({column.to_sym => {value[:criteria] => value[:value]}})
8
+ end
9
+ @filtered_results = params[:smart_filter][:model].constantize.smart_filter(hash)
10
+ end
11
+ end
@@ -0,0 +1,80 @@
1
+ module SmartFilter
2
+ def smart_filter(options)
3
+ @conds = []
4
+ columns.each do |column|
5
+ if options[column.name.to_sym]
6
+ case options[column.name.to_sym].keys.first
7
+ when "contains" then @conds << contains(column.name, options[column.name.to_sym]["contains"])
8
+ when "does_not_contain" then @conds << does_not_contain(column.name, options[column.name.to_sym]["does_not_contain"])
9
+ when "is" then @conds << is(column.name, options[column.name.to_sym]["is"])
10
+ when "starts_with" then @conds << starts_with(column.name, options[column.name.to_sym]["starts_with"])
11
+ when "ends_with" then @conds << ends_with(column.name, options[column.name.to_sym]["ends_with"])
12
+ when "equals_to" then @conds << equals_to(column.name, options[column.name.to_sym]["equals_to"])
13
+ when "greater_than" then @conds << greater_than(column.name, options[column.name.to_sym]["greater_than"])
14
+ when "less_than" then @conds << less_than(column.name, options[column.name.to_sym]["less_than"])
15
+ when "between" then @conds << between(column.name,
16
+ options[column.name.to_sym]["between"].first,
17
+ options[column.name.to_sym]["between"].last)
18
+ else
19
+ return []
20
+ end
21
+ end
22
+ end
23
+ return find(:all,
24
+ :conditions => conditions)
25
+ end
26
+
27
+ private
28
+
29
+ def conditions
30
+ @conds.flatten!
31
+ @final = []
32
+ @terms = []
33
+ @conds.each_with_index do |condition, index|
34
+ if (index + 1) % 2 == 0
35
+ if condition.is_a?(Hash)
36
+ @terms << condition.to_a.flatten
37
+ else
38
+ @terms << condition
39
+ end
40
+ elsif (index + 1) % 2 != 0
41
+ @final << condition
42
+ end
43
+ end
44
+ return [@final.join(' AND '), @terms].flatten
45
+ end
46
+
47
+ def contains(column, term)
48
+ ["#{column} LIKE ?", "%#{term}%"]
49
+ end
50
+
51
+ def does_not_contain(column, term)
52
+ ["#{column} NOT LIKE ?", "%#{term}%"]
53
+ end
54
+
55
+ def between(column, start, finish)
56
+ ["#{column} BETWEEN ? AND ?", {start => finish}]
57
+ end
58
+
59
+ def is(column, term)
60
+ ["#{column} = ?", term]
61
+ end
62
+
63
+ def starts_with(column, term)
64
+ ["#{column} LIKE ?", "#{term}%"]
65
+ end
66
+
67
+ def ends_with(column, term)
68
+ ["#{column} LIKE ?", "%#{term}"]
69
+ end
70
+
71
+ alias :equals_to :is
72
+
73
+ def greater_than(column, term)
74
+ ["#{column} > ?", term]
75
+ end
76
+
77
+ def less_than(column, term)
78
+ ["#{column} < ?", term]
79
+ end
80
+ end
@@ -0,0 +1,55 @@
1
+ module ViewHelpers
2
+ def smart_filter(model, cols, &block)
3
+ body = capture(&block)
4
+ html = ""
5
+ html << "<form action='/address_books' method='get'>"
6
+ html << "<input type='hidden' name='smart_filter[model]' value='#{model}'>"
7
+ columns(model, cols).each do |column|
8
+
9
+ html << content_tag(:label, column.capitalize, :for => "#{column}")
10
+ html << content_tag(:select, :name => "smart_filter[#{column}][criteria]", :id => "name-criteria") do
11
+ criteria_options(model, column)
12
+ end
13
+ html << tag("input", { :type => 'text', :name => "smart_filter[#{column}][value]", :placeholder => "String" })
14
+ html << '<br>'
15
+ end
16
+ html << "<input type='submit'>"
17
+ html << "</form>"
18
+ html << render(:partial => 'shared/filtered_results') if @filtered_results
19
+ html << body unless @filtered_results
20
+ concat html
21
+ end
22
+
23
+ private
24
+
25
+ def columns(model, cols)
26
+ if cols == :all
27
+ all_cols = model.column_names
28
+ all_cols.delete("id")
29
+ all_cols
30
+ else
31
+ cols
32
+ end
33
+ end
34
+
35
+ def criteria_options(model, column)
36
+ if model.columns_hash[column].type == :string or model.columns_hash[column].type == :text
37
+ html = content_tag(:option, :value => "contains") do
38
+ "Contains"
39
+ end
40
+ html << content_tag(:option, :value => "does_not_contain") do
41
+ "Does not Contain"
42
+ end
43
+ html << content_tag(:option, :value => "is") do
44
+ "Is"
45
+ end
46
+ html << content_tag(:option, :value => "starts_with") do
47
+ "Starts with"
48
+ end
49
+ html << content_tag(:option, :value => "ends_with") do
50
+ "Ends with"
51
+ end
52
+ end
53
+ html
54
+ end
55
+ end
@@ -0,0 +1,7 @@
1
+ require 'smart_filters/smart_filter.rb'
2
+ require 'smart_filters/view_helpers'
3
+ require 'smart_filters/before_filter'
4
+
5
+ ActionController::Base.send :before_filter, :sort_smart_filter
6
+ ActionView::Base.send :include, ViewHelpers
7
+ ActiveRecord::Base.send :extend, SmartFilter
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :smart_filters do
3
+ # # Task goes here
4
+ # end
data/rails/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'smart_filters'
data/spec/factories.rb ADDED
@@ -0,0 +1,9 @@
1
+ Factory.define :address_book do |a|
2
+ a.name Faker::Name.name
3
+ a.address "#{Faker::Address.street_address}\n#{Faker::Address.city} #{Faker::Address.us_state} #{Faker::Address.zip_code}\nUSA"
4
+ a.company Faker::Company.name
5
+ a.email Faker::Internet.email
6
+ a.zipcode Faker::Address.zip_code
7
+ a.phone Faker::PhoneNumber.phone_number
8
+ a.domain Faker::Internet.domain_name
9
+ end
@@ -0,0 +1,126 @@
1
+ require 'spec_helper'
2
+
3
+ describe SmartFilter do
4
+ before(:all) do
5
+ load_schema
6
+ class AddressBook < ActiveRecord::Base; end
7
+ end
8
+
9
+ describe ".smart_filter" do
10
+ let(:bob) { Factory(:address_book, :name => "Bob Martin", :zipcode => 12345) }
11
+ let(:david) { Factory(:address_book, :name => "David Henderson", :zipcode => 12347) }
12
+ let(:zheimer) { Factory(:address_book, :name => "Clarke Zheimer", :zipcode => 12348) }
13
+
14
+
15
+ before(:each) do
16
+ bob.save!
17
+ david.save!
18
+ zheimer.save!
19
+ end
20
+
21
+ it "returns an empty array if the criteria is unknown" do
22
+ AddressBook.smart_filter({:name => {"magic" => "abracadabra"}}).should == []
23
+ end
24
+
25
+ it "returns all records if the column doesn't exist" do
26
+ AddressBook.smart_filter({:magician => {"magic" => "abracadabra"}}).should == AddressBook.find(:all)
27
+ end
28
+
29
+ context "when the argument contains more than one filter" do
30
+ it "returns the record matching all the criteria" do
31
+ AddressBook.smart_filter({:name => {"contains" => "Bob"},
32
+ :address => {"contains" => "Abracarab"}}).should be_empty
33
+ puts AddressBook.smart_filter({:name => {"contains" => "Bob"},
34
+ :name => {"contains" => "Martin"}}).inspect
35
+ AddressBook.smart_filter({:name => {"contains" => "Bob"},
36
+ :name => {"contains" => "Martin"}}).should have(1).item
37
+ end
38
+ end
39
+
40
+ context "when the column to apply smart filtering is string or text" do
41
+ context "when the criteria is 'contains'" do
42
+
43
+ it "returns the record with the column that contains the given string" do
44
+ AddressBook.smart_filter({:name => {"contains" => "Bob"}}).first.name.should == bob.name
45
+ AddressBook.smart_filter({:name => {"contains" => "Bob"}}).first.name.should include(bob.name.split.first)
46
+ end
47
+
48
+ end
49
+
50
+ context "when the criteria is 'is'" do
51
+
52
+ it "returns the record with the column of the exact given string" do
53
+ AddressBook.smart_filter({:name => {"is" => "Bob Martin"}}).first.name.should == bob.name
54
+ end
55
+
56
+ end
57
+
58
+ context "when the criteria is 'does_not_contain'" do
59
+
60
+ it "returns the record with the column that does not contain the given string" do
61
+ AddressBook.smart_filter({:name => {"does_not_contain" => "Bob Martin"}}).first.name.should_not == bob.name
62
+ end
63
+
64
+ end
65
+
66
+ context "when the criteria is starts_with" do
67
+
68
+ it "returns the record with the column that starts with the given string" do
69
+ AddressBook.smart_filter({:name => {"starts_with" => "David"}}).first.name.should =~ /^David[.]*/
70
+ end
71
+
72
+ end
73
+
74
+ context "when the criteria is ends_with" do
75
+
76
+ it "returns the record with the column that ends with the given string" do
77
+ AddressBook.smart_filter({:name => {"ends_with" => "Henderson"}}).first.name.should =~ /[.]*Henderson$/
78
+ end
79
+
80
+ end
81
+
82
+ end
83
+
84
+ context "when the column to apply smart filtering is integer" do
85
+
86
+ context "when the criteria is equals_to" do
87
+
88
+ it "returns records with the column that equals the given integer" do
89
+ AddressBook.smart_filter({:zipcode => {"equals_to" => "12345"}}).first.zipcode.should == 12345
90
+ end
91
+
92
+ end
93
+
94
+ context "when the criteria is greater_than" do
95
+ it "returns records with the column that is greater than the given integer" do
96
+ AddressBook.smart_filter({:zipcode => {"greater_than" => "12345"}}).first.zipcode.should > 12345
97
+ end
98
+ end
99
+
100
+ context "when the criteria is less_than" do
101
+ it "returns records with the column that is less than the given integer" do
102
+ AddressBook.smart_filter({:zipcode => {"less_than" => "12346"}}).first.zipcode.should < 12346
103
+ end
104
+ end
105
+
106
+ context "when the criteria is between" do
107
+ it "returns records with the column that is between the the given integers" do
108
+ AddressBook.smart_filter({:zipcode => {"between" => ["12343", "12348"]}}).should have(3).items
109
+ AddressBook.smart_filter({:zipcode => {"between" => ["12343", "12348"]}}).each do |contact|
110
+ contact.should satisfy { |c| c.zipcode >= 12343 && c.zipcode <= 12348 }
111
+ end
112
+ end
113
+ end
114
+
115
+ end
116
+
117
+ context "when the column to apply smart filtering is boolean" do
118
+
119
+ end
120
+
121
+ context "when the column to apply smart filtering is date or date/time" do
122
+
123
+ end
124
+
125
+ end
126
+ end
data/spec/spec.opts ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --format nested
3
+ --loadby mtime
@@ -0,0 +1,9 @@
1
+ ENV['RAILS_ENV'] = 'test'
2
+ ENV['RAILS_ROOT'] ||= File.dirname(__FILE__) + '/../../../..'
3
+
4
+ require File.expand_path(File.join(ENV['RAILS_ROOT'], 'config/environment.rb'))
5
+ Dir[File.expand_path(File.join(File.dirname(__FILE__),'support','**','*.rb'))].each {|f| require f}
6
+
7
+ Spec::Runner.configure do |config|
8
+ # Configuration
9
+ end
@@ -0,0 +1,17 @@
1
+ sqlite3:
2
+ :adapter: sqlite3
3
+ :database: vendor/plugins/smart_filters/spec/smart_filters_plugin.sqlite3.db
4
+
5
+ # postgresql:
6
+ # :adapter: postgresql
7
+ # :username: postgres
8
+ # :password: postgres
9
+ # :database: yaffle_plugin_test
10
+ # :min_messages: ERROR
11
+
12
+ mysql:
13
+ :adapter: mysql
14
+ :host: localhost
15
+ :username: root
16
+ :password:
17
+ :database: smart_filters_plugin_test
@@ -0,0 +1,46 @@
1
+ def load_schema
2
+ config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
3
+ ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log")
4
+
5
+ db_adapter = ENV['DB']
6
+
7
+ # no db passed, try one of these fine config-free DBs before bombing.
8
+ db_adapter ||=
9
+ begin
10
+ require 'rubygems'
11
+ require 'sqlite'
12
+ 'sqlite'
13
+ rescue MissingSourceFile
14
+ begin
15
+ require 'sqlite3'
16
+ 'sqlite3'
17
+ rescue MissingSourceFile
18
+ end
19
+ end
20
+
21
+ if db_adapter.nil?
22
+ raise "No DB Adapter selected. Pass the DB= option to pick one, or install Sqlite or Sqlite3."
23
+ end
24
+
25
+ ActiveRecord::Base.establish_connection(config[db_adapter])
26
+ load(File.dirname(__FILE__) + "/schema.rb")
27
+ require File.dirname(__FILE__) + '/../../rails/init.rb'
28
+ end
29
+
30
+ def drop_database
31
+ config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
32
+ begin
33
+ case config['adapter']
34
+ when 'mysql'
35
+ ActiveRecord::Base.establish_connection(config)
36
+ ActiveRecord::Base.connection.drop_database config['database']
37
+ when /^sqlite/
38
+ FileUtils.rm(File.join(RAILS_ROOT, config['database']))
39
+ when 'postgresql'
40
+ ActiveRecord::Base.establish_connection(config.merge('database' => 'postgres', 'schema_search_path' => 'public'))
41
+ ActiveRecord::Base.connection.drop_database config['database']
42
+ end
43
+ rescue Exception => e
44
+ puts "Couldn't drop #{config['database']} : #{e.inspect}"
45
+ end
46
+ end