filterable-models 0.0.2
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/.gitignore +4 -0
- data/Gemfile +4 -0
- data/Rakefile +1 -0
- data/filterable-models.gemspec +20 -0
- data/lib/filterable-models/base.rb +230 -0
- data/lib/filterable-models/helper.rb +145 -0
- data/lib/filterable-models/version.rb +5 -0
- data/lib/filterable-models.rb +7 -0
- metadata +73 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "filterable-models/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "filterable-models"
|
7
|
+
s.version = Filterable::Models::VERSION
|
8
|
+
s.authors = ["Marcelo Ribeiro"]
|
9
|
+
s.email = ["ribeirodesenv@gmail.com"]
|
10
|
+
s.homepage = ""
|
11
|
+
s.summary = %q{Filter functionality for Rails models}
|
12
|
+
s.description = %q{Start filtering your models and adding a filter form to your scaffolds pretty fast}
|
13
|
+
|
14
|
+
s.rubyforge_project = "filterable-models"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
end
|
@@ -0,0 +1,230 @@
|
|
1
|
+
module Filterable::Models
|
2
|
+
module Base
|
3
|
+
|
4
|
+
def self.included(recipient)
|
5
|
+
recipient.extend(ModelClassMethods)
|
6
|
+
recipient.class_eval do
|
7
|
+
include ModelInstanceMethods
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
module ModelClassMethods
|
12
|
+
|
13
|
+
def filters(filters)
|
14
|
+
@filters = {}
|
15
|
+
@filterable = {}
|
16
|
+
@sorted_filterable = []
|
17
|
+
@filters = filters
|
18
|
+
|
19
|
+
if filters.is_a?(Array)
|
20
|
+
# Ruby 1.8 hash sorting is unpredictable, so we can use an array
|
21
|
+
# to take care of the order and make it behave as expected
|
22
|
+
filters.each do |hash|
|
23
|
+
prepare_filters(hash)
|
24
|
+
end
|
25
|
+
else
|
26
|
+
# Filters is a hash, so lets just move on
|
27
|
+
prepare_filters(filters)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def prepare_filters(hash)
|
32
|
+
hash.map do |key, value|
|
33
|
+
if value.is_a?(Hash)
|
34
|
+
value.map do |v_key, v_value|
|
35
|
+
@filterable["#{key.to_s.pluralize}.#{v_key}".to_sym] = v_value
|
36
|
+
@sorted_filterable << {"#{key.to_s.pluralize}.#{v_key}".to_sym => v_value}
|
37
|
+
end
|
38
|
+
else
|
39
|
+
@filterable[key] = value
|
40
|
+
@sorted_filterable << {key => value}
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def filter_options_for(attribute_name, options = [])
|
46
|
+
@filter_options ||= {}
|
47
|
+
@filter_options[attribute_name] = options
|
48
|
+
end
|
49
|
+
|
50
|
+
def filter_fields_per_column(fields_per_column = 5)
|
51
|
+
@fields_per_column = fields_per_column
|
52
|
+
end
|
53
|
+
|
54
|
+
def filter(filters = {})
|
55
|
+
filters ||= {}
|
56
|
+
filters = filters[self.to_s.underscore] ||= {}
|
57
|
+
@filter_conditions = []
|
58
|
+
@filter_conditions_values = []
|
59
|
+
@current_joins = []
|
60
|
+
|
61
|
+
keys = []
|
62
|
+
|
63
|
+
filters.map do |key, value|
|
64
|
+
|
65
|
+
unless value.blank?
|
66
|
+
|
67
|
+
if value.is_a?(Hash)
|
68
|
+
value.keys.collect{|k| k.to_s}.sort.each do |v_key|
|
69
|
+
v_key = v_key.to_sym
|
70
|
+
v_value = value[v_key]
|
71
|
+
|
72
|
+
keys << "#{key.to_s.pluralize}.#{v_key}".to_sym
|
73
|
+
end
|
74
|
+
@current_joins = build_joins(@current_joins, filters)
|
75
|
+
else
|
76
|
+
keys << key.to_sym
|
77
|
+
end
|
78
|
+
|
79
|
+
keys.each do |key|
|
80
|
+
|
81
|
+
if key.to_s =~ /_from/
|
82
|
+
key = key.to_s.gsub("_from", "").to_sym
|
83
|
+
elsif key.to_s =~ /_to/
|
84
|
+
key = key.to_s.gsub("_to", "").to_sym
|
85
|
+
end
|
86
|
+
|
87
|
+
if @filterable.has_key?(key)
|
88
|
+
filter_type = @filterable[key]
|
89
|
+
|
90
|
+
# related object attribute
|
91
|
+
if key.to_s =~ /\./
|
92
|
+
v_keys = key.to_s.split(".")
|
93
|
+
if filter_type =~ /_range/
|
94
|
+
value_from = filters[v_keys[0].singularize.to_sym]["#{v_keys[1]}_from".to_sym]
|
95
|
+
value_to = filters[v_keys[0].singularize.to_sym]["#{v_keys[1]}_to".to_sym]
|
96
|
+
else
|
97
|
+
value = filters[v_keys[0].singularize.to_sym][v_keys[1].to_sym]
|
98
|
+
end
|
99
|
+
else
|
100
|
+
if filter_type =~ /_range/
|
101
|
+
value_from = filters["#{key}_from".to_sym]
|
102
|
+
value_to = filters["#{key}_to".to_sym]
|
103
|
+
else
|
104
|
+
value = filters[key]
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
unless value.blank?
|
109
|
+
if value == 'false'
|
110
|
+
value = false
|
111
|
+
end
|
112
|
+
|
113
|
+
if value == 'true'
|
114
|
+
value = true
|
115
|
+
end
|
116
|
+
|
117
|
+
compare_key = key.to_s
|
118
|
+
unless key.to_s =~ /\./
|
119
|
+
compare_key = "#{self.name.to_s.tableize}.#{key.to_s}"
|
120
|
+
end
|
121
|
+
case filter_type
|
122
|
+
when "string"
|
123
|
+
@filter_conditions << "#{compare_key} LIKE ?"
|
124
|
+
@filter_conditions_values << "%#{value}%"
|
125
|
+
when "exact_string"
|
126
|
+
@filter_conditions << "#{compare_key} = ?"
|
127
|
+
@filter_conditions_values << value
|
128
|
+
when "date_range", "float_range"
|
129
|
+
if value_from != "" && value_to != ""
|
130
|
+
@filter_conditions << "(#{compare_key} >= ? and #{compare_key} <= ?)"
|
131
|
+
@filter_conditions_values << value_from
|
132
|
+
@filter_conditions_values << value_to
|
133
|
+
elsif value_from != ""
|
134
|
+
@filter_conditions << "#{compare_key} >= ?"
|
135
|
+
@filter_conditions_values << value_from
|
136
|
+
elsif value_to != ""
|
137
|
+
@filter_conditions << "#{compare_key} <= ?"
|
138
|
+
@filter_conditions_values << value_to
|
139
|
+
end
|
140
|
+
when "drop_down"
|
141
|
+
@filter_conditions << "#{compare_key} = ?"
|
142
|
+
@filter_conditions_values << value
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
filter_scope = where([@filter_conditions.join(" AND ")] + @filter_conditions_values).group("#{self.name.tableize}.id")
|
151
|
+
if @current_joins.any?
|
152
|
+
filter_scope = filter_scope.joins(@current_joins)
|
153
|
+
end
|
154
|
+
|
155
|
+
filter_scope
|
156
|
+
end
|
157
|
+
|
158
|
+
def build_joins(current_joins = {}, filters = {})
|
159
|
+
@self_object_name = self.name.underscore
|
160
|
+
@self_table_name = @self_object_name.pluralize
|
161
|
+
@self_filter_joins = []
|
162
|
+
|
163
|
+
filters.map do |key, value|
|
164
|
+
|
165
|
+
if value.is_a?(Hash)
|
166
|
+
unless filters_are_empty(value)
|
167
|
+
|
168
|
+
@self_object = self.new
|
169
|
+
|
170
|
+
if @self_object.attributes.include?("#{key}_id")
|
171
|
+
# belogns to
|
172
|
+
@self_filter_joins += [key.to_sym]
|
173
|
+
else
|
174
|
+
@related_object = key.to_s.camelize.constantize.new
|
175
|
+
|
176
|
+
if @related_object.attributes.include?("#{@self_object_name}_id")
|
177
|
+
# has many - need to figure out how to solve it for has one.
|
178
|
+
@self_filter_joins += ["#{key.to_s.pluralize}".to_sym]
|
179
|
+
elsif @self_object.methods.include?("#{key}_ids")
|
180
|
+
# has and_belongs_to_many
|
181
|
+
@self_filter_joins += ["#{key.to_s.pluralize}".to_sym]
|
182
|
+
end
|
183
|
+
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
end
|
189
|
+
|
190
|
+
if @self_filter_joins.size > 0
|
191
|
+
current_joins ||= []
|
192
|
+
current_joins += @self_filter_joins
|
193
|
+
end
|
194
|
+
|
195
|
+
current_joins
|
196
|
+
end
|
197
|
+
|
198
|
+
def filters_are_empty(filters)
|
199
|
+
filters.map do |key, value|
|
200
|
+
return false unless value.blank?
|
201
|
+
end
|
202
|
+
true
|
203
|
+
end
|
204
|
+
|
205
|
+
def filterable
|
206
|
+
@filterable
|
207
|
+
end
|
208
|
+
|
209
|
+
def filter_options
|
210
|
+
@filter_options
|
211
|
+
end
|
212
|
+
|
213
|
+
def fields_per_column
|
214
|
+
@fields_per_column ||= 5
|
215
|
+
end
|
216
|
+
|
217
|
+
def filters_hash
|
218
|
+
@filters
|
219
|
+
end
|
220
|
+
|
221
|
+
def sorted_filterable
|
222
|
+
@sorted_filterable
|
223
|
+
end
|
224
|
+
|
225
|
+
end
|
226
|
+
|
227
|
+
module ModelInstanceMethods
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
module Filterable::Models
|
2
|
+
module Helper
|
3
|
+
def filter_form_for(object, params = {}, options = {})
|
4
|
+
@params = params
|
5
|
+
form_html = %{<form action="" method="get">} + "\n"
|
6
|
+
elements_html = ""
|
7
|
+
|
8
|
+
klass = object.to_s.camelize.constantize
|
9
|
+
|
10
|
+
return if klass.filterable.blank?
|
11
|
+
|
12
|
+
filter_fields_count = 1
|
13
|
+
|
14
|
+
klass.sorted_filterable.each do |filterable|
|
15
|
+
|
16
|
+
filterable.map do |key, value|
|
17
|
+
key = key.to_sym
|
18
|
+
value = klass.filterable[key]
|
19
|
+
|
20
|
+
case value
|
21
|
+
when 'string'
|
22
|
+
elements_html += text_filter_tag(object, key) + "\n"
|
23
|
+
when 'exact_string'
|
24
|
+
elements_html += text_filter_tag(object, key) + "\n"
|
25
|
+
when 'date_range', 'float_range'
|
26
|
+
elements_html += text_filter_tag(object, key, "from") + "\n"
|
27
|
+
elements_html += text_filter_tag(object, key, "to") + "\n"
|
28
|
+
filter_fields_count += 1
|
29
|
+
when 'drop_down'
|
30
|
+
elements_html += select_filter_tag(object, key) + "\n"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
if filter_fields_count >= (klass.fields_per_column)
|
35
|
+
elements_html += "</ul><ul>" + "\n"
|
36
|
+
filter_fields_count = 1
|
37
|
+
else
|
38
|
+
filter_fields_count += 1
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
form_html += content_tag(:ul, elements_html.html_safe) + "\n"
|
44
|
+
form_html += %{<div style="clear:both;"><!-- --></div>}.html_safe
|
45
|
+
form_html += submit_tag(t("Filter").html_safe)
|
46
|
+
form_html += "</form>"
|
47
|
+
|
48
|
+
content_tag(:div, form_html.html_safe, {:class => "filter-form"}.merge(options)).html_safe
|
49
|
+
end
|
50
|
+
|
51
|
+
def text_filter_tag(object, key, extra = nil)
|
52
|
+
filter_element_data = get_filter_element_data(object, key, extra)
|
53
|
+
filter_field_label_text = filter_element_data[:label_text]
|
54
|
+
filter_field_id = filter_element_data[:element_id]
|
55
|
+
filter_field_name = filter_element_data[:element_name]
|
56
|
+
filter_field_value = filter_element_data[:element_value]
|
57
|
+
|
58
|
+
content_tag(:li,
|
59
|
+
label_tag(filter_field_id, filter_field_label_text) + "<br/>".html_safe +
|
60
|
+
text_field_tag(filter_field_name, filter_field_value)).html_safe
|
61
|
+
end
|
62
|
+
|
63
|
+
def select_filter_tag(object, key, extra = nil)
|
64
|
+
filter_element_data = get_filter_element_data(object, key, extra)
|
65
|
+
filter_field_label_text = filter_element_data[:label_text]
|
66
|
+
filter_field_id = filter_element_data[:element_id]
|
67
|
+
filter_field_name = filter_element_data[:element_name]
|
68
|
+
filter_field_value = filter_element_data[:element_value]
|
69
|
+
|
70
|
+
content_tag(:li,
|
71
|
+
label_tag(filter_field_id, filter_field_label_text) + "<br/>".html_safe +
|
72
|
+
select_tag(filter_field_name, select_values_for(object, key, filter_field_value))).html_safe
|
73
|
+
end
|
74
|
+
|
75
|
+
def select_values_for(object, key, value)
|
76
|
+
klass = object.to_s.camelize.constantize
|
77
|
+
filter_options = klass.filter_options[key]
|
78
|
+
|
79
|
+
options_html = content_tag(:option, t("Please Select"), :value => "")
|
80
|
+
|
81
|
+
if filter_options.is_a?(Hash)
|
82
|
+
filter_options.keys.collect{|k| k.to_s}.sort.each do |option_key|
|
83
|
+
selected = {}
|
84
|
+
if value == filter_options[option_key]
|
85
|
+
selected = {:selected => "selected"}
|
86
|
+
end
|
87
|
+
options_html += content_tag(:option, t(option_key.to_s.titleize), {:value => filter_options[option_key]}.merge(selected))
|
88
|
+
end
|
89
|
+
elsif filter_options.is_a?(Array)
|
90
|
+
filter_options.each do |option|
|
91
|
+
selected = {}
|
92
|
+
if value == option
|
93
|
+
selected = {:selected => "selected"}
|
94
|
+
end
|
95
|
+
options_html += content_tag(:option, t(option.titleize), {:value => option}.merge(selected))
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
options_html.html_safe
|
100
|
+
end
|
101
|
+
|
102
|
+
def get_filter_element_data(object, key, extra)
|
103
|
+
params_prefix = :filters
|
104
|
+
@params ||= {}
|
105
|
+
@params[params_prefix] ||= {}
|
106
|
+
@params[params_prefix][object] ||= {}
|
107
|
+
|
108
|
+
if key.to_s =~ /\./
|
109
|
+
key_parts = key.to_s.split(".")
|
110
|
+
key_object = key_parts.first.singularize.to_sym
|
111
|
+
key_object_att = key_parts.last.to_sym
|
112
|
+
|
113
|
+
@params[params_prefix][object][key_object] ||= {}
|
114
|
+
key = "#{key_object}_#{key_object_att}"
|
115
|
+
|
116
|
+
if extra.nil?
|
117
|
+
element_name = "#{params_prefix}[#{object}][#{key_object}][#{key_object_att}]"
|
118
|
+
element_value = @params[params_prefix][object][key_object][key_object_att]
|
119
|
+
element_id = "#{params_prefix}_#{object}_#{key}"
|
120
|
+
else
|
121
|
+
element_name = "#{params_prefix}[#{object}][#{key_object}][#{key_object_att}_#{extra}]"
|
122
|
+
element_value = @params[params_prefix][object][key_object]["#{key_object_att}_#{extra}"]
|
123
|
+
element_id = "#{params_prefix}_#{object}_#{key}_#{extra}"
|
124
|
+
end
|
125
|
+
else
|
126
|
+
if extra.nil?
|
127
|
+
element_name = "#{params_prefix}[#{object}][#{key}]"
|
128
|
+
element_value = @params[params_prefix][object][key]
|
129
|
+
element_id = "#{params_prefix}_#{object}_#{key}"
|
130
|
+
else
|
131
|
+
element_name = "#{params_prefix}[#{object}][#{key}_#{extra}]"
|
132
|
+
element_value = @params[params_prefix][object]["#{key}_#{extra}".to_sym]
|
133
|
+
element_id = "#{params_prefix}_#{object}_#{key}_#{extra}"
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
element_data = {}
|
138
|
+
element_data[:label_text] = t("#{key.to_s.humanize}#{extra ? "_#{extra}" : ""}".to_s.titleize) + ": "
|
139
|
+
element_data[:element_id] = element_id
|
140
|
+
element_data[:element_name] = element_name
|
141
|
+
element_data[:element_value] = element_value
|
142
|
+
element_data
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
@@ -0,0 +1,7 @@
|
|
1
|
+
require "filterable-models/version"
|
2
|
+
require "filterable-models/base"
|
3
|
+
require "filterable-models/helper"
|
4
|
+
|
5
|
+
ActiveRecord::Base.send :include, Filterable::Models::Base
|
6
|
+
ActionView::Helpers::FormTagHelper.send :include, Filterable::Models::Helper
|
7
|
+
ActionView::Base.send :include, Filterable::Models::Helper
|
metadata
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: filterable-models
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 27
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 2
|
10
|
+
version: 0.0.2
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Marcelo Ribeiro
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-09-04 00:00:00 Z
|
19
|
+
dependencies: []
|
20
|
+
|
21
|
+
description: Start filtering your models and adding a filter form to your scaffolds pretty fast
|
22
|
+
email:
|
23
|
+
- ribeirodesenv@gmail.com
|
24
|
+
executables: []
|
25
|
+
|
26
|
+
extensions: []
|
27
|
+
|
28
|
+
extra_rdoc_files: []
|
29
|
+
|
30
|
+
files:
|
31
|
+
- .gitignore
|
32
|
+
- Gemfile
|
33
|
+
- Rakefile
|
34
|
+
- filterable-models.gemspec
|
35
|
+
- lib/filterable-models.rb
|
36
|
+
- lib/filterable-models/base.rb
|
37
|
+
- lib/filterable-models/helper.rb
|
38
|
+
- lib/filterable-models/version.rb
|
39
|
+
homepage: ""
|
40
|
+
licenses: []
|
41
|
+
|
42
|
+
post_install_message:
|
43
|
+
rdoc_options: []
|
44
|
+
|
45
|
+
require_paths:
|
46
|
+
- lib
|
47
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
48
|
+
none: false
|
49
|
+
requirements:
|
50
|
+
- - ">="
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
hash: 3
|
53
|
+
segments:
|
54
|
+
- 0
|
55
|
+
version: "0"
|
56
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
hash: 3
|
62
|
+
segments:
|
63
|
+
- 0
|
64
|
+
version: "0"
|
65
|
+
requirements: []
|
66
|
+
|
67
|
+
rubyforge_project: filterable-models
|
68
|
+
rubygems_version: 1.8.10
|
69
|
+
signing_key:
|
70
|
+
specification_version: 3
|
71
|
+
summary: Filter functionality for Rails models
|
72
|
+
test_files: []
|
73
|
+
|