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 +20 -0
- data/README.md +6 -0
- data/Rakefile +40 -0
- data/app/views/shared/_filtered_results.html.erb +15 -0
- data/lib/smart_filters/before_filter.rb +11 -0
- data/lib/smart_filters/smart_filter.rb +80 -0
- data/lib/smart_filters/view_helpers.rb +55 -0
- data/lib/smart_filters.rb +7 -0
- data/lib/tasks/smart_filters.rake +4 -0
- data/rails/init.rb +1 -0
- data/spec/factories.rb +9 -0
- data/spec/lib/smart_filters_spec.rb +126 -0
- data/spec/smart_filters_plugin.sqlite3.db +0 -0
- data/spec/spec.opts +3 -0
- data/spec/spec_helper.rb +9 -0
- data/spec/support/database.yml +17 -0
- data/spec/support/db.rb +46 -0
- data/spec/support/debug.log +6159 -0
- data/spec/support/schema.rb +13 -0
- metadata +85 -0
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
|
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
|
Binary file
|
data/spec/spec.opts
ADDED
data/spec/spec_helper.rb
ADDED
@@ -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
|
data/spec/support/db.rb
ADDED
@@ -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
|