with_filters 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +8 -0
- data/.yardopts +1 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +63 -0
- data/Rakefile +1 -0
- data/app/views/with_filters/_filter_form.html.erb +14 -0
- data/app/views/with_filters/filter/_check_box.html.erb +2 -0
- data/app/views/with_filters/filter/_check_boxes.html.erb +5 -0
- data/app/views/with_filters/filter/_radio.html.erb +5 -0
- data/app/views/with_filters/filter/_select.html.erb +2 -0
- data/app/views/with_filters/filter/_select_range.html.erb +4 -0
- data/app/views/with_filters/filter/_text.html.erb +2 -0
- data/app/views/with_filters/filter/_text_range.html.erb +4 -0
- data/changelog.md +2 -0
- data/lib/generators/with_filters/theme/theme_generator.rb +43 -0
- data/lib/with_filters/action_view_extension.rb +110 -0
- data/lib/with_filters/active_record_extension.rb +26 -0
- data/lib/with_filters/active_record_model_extension.rb +163 -0
- data/lib/with_filters/engine.rb +5 -0
- data/lib/with_filters/hash_extraction.rb +31 -0
- data/lib/with_filters/models/action.rb +14 -0
- data/lib/with_filters/models/filter/base.rb +36 -0
- data/lib/with_filters/models/filter/base_range.rb +42 -0
- data/lib/with_filters/models/filter/check_box.rb +20 -0
- data/lib/with_filters/models/filter/choice.rb +23 -0
- data/lib/with_filters/models/filter/collection.rb +28 -0
- data/lib/with_filters/models/filter/filter.rb +59 -0
- data/lib/with_filters/models/filter/radio.rb +7 -0
- data/lib/with_filters/models/filter/select.rb +22 -0
- data/lib/with_filters/models/filter/select_range.rb +15 -0
- data/lib/with_filters/models/filter/text.rb +30 -0
- data/lib/with_filters/models/filter/text_range.rb +15 -0
- data/lib/with_filters/models/filter_form.rb +93 -0
- data/lib/with_filters/value_prep/boolean_prep.rb +10 -0
- data/lib/with_filters/value_prep/date_prep.rb +10 -0
- data/lib/with_filters/value_prep/date_time_prep.rb +51 -0
- data/lib/with_filters/value_prep/default_prep.rb +88 -0
- data/lib/with_filters/value_prep/value_prep.rb +28 -0
- data/lib/with_filters/version.rb +3 -0
- data/lib/with_filters.rb +32 -0
- data/spec/active_record_model_extension_spec.rb +435 -0
- data/spec/dummy/README.rdoc +261 -0
- data/spec/dummy/Rakefile +7 -0
- data/spec/dummy/app/assets/javascripts/application.js +15 -0
- data/spec/dummy/app/assets/stylesheets/application.css +13 -0
- data/spec/dummy/app/controllers/application_controller.rb +3 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/mailers/.gitkeep +0 -0
- data/spec/dummy/app/models/.gitkeep +0 -0
- data/spec/dummy/app/models/date_time_tester.rb +2 -0
- data/spec/dummy/app/models/field_format_tester.rb +2 -0
- data/spec/dummy/app/models/nobel_prize.rb +3 -0
- data/spec/dummy/app/models/nobel_prize_winner.rb +3 -0
- data/spec/dummy/app/views/layouts/application.html.erb +14 -0
- data/spec/dummy/config/application.rb +56 -0
- data/spec/dummy/config/boot.rb +10 -0
- data/spec/dummy/config/database.yml +25 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +37 -0
- data/spec/dummy/config/environments/production.rb +67 -0
- data/spec/dummy/config/environments/test.rb +37 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/inflections.rb +15 -0
- data/spec/dummy/config/initializers/mime_types.rb +5 -0
- data/spec/dummy/config/initializers/secret_token.rb +7 -0
- data/spec/dummy/config/initializers/session_store.rb +8 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +5 -0
- data/spec/dummy/config/routes.rb +58 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/db/migrate/20111227224959_additional_columns.rb +73 -0
- data/spec/dummy/db/migrate/20120127203225_create_nobel_prize_winners.rb +30 -0
- data/spec/dummy/db/migrate/20120203212237_create_nobel_prizes.rb +34 -0
- data/spec/dummy/db/migrate/20120209051208_modify_updated_at.rb +13 -0
- data/spec/dummy/db/migrate/20120210163052_change_created_at.rb +17 -0
- data/spec/dummy/db/migrate/20120214172946_fix_einstein.rb +9 -0
- data/spec/dummy/db/migrate/20120227200013_create_date_time_testers.rb +25 -0
- data/spec/dummy/db/migrate/20120309202722_create_field_format_testers.rb +22 -0
- data/spec/dummy/db/migrate/20120310195447_update_field_format_testers.rb +15 -0
- data/spec/dummy/db/schema.rb +51 -0
- data/spec/dummy/db/test.sqlite3 +0 -0
- data/spec/dummy/lib/assets/.gitkeep +0 -0
- data/spec/dummy/log/.gitkeep +0 -0
- data/spec/dummy/public/404.html +26 -0
- data/spec/dummy/public/422.html +26 -0
- data/spec/dummy/public/500.html +25 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/dummy/script/rails +6 -0
- data/spec/generators/with_filters_theme_spec.rb +33 -0
- data/spec/hash_extraction_spec.rb +51 -0
- data/spec/helpers/action_view_extension_spec.rb +345 -0
- data/spec/models/action.rb +20 -0
- data/spec/models/filter/base_range_spec.rb +32 -0
- data/spec/models/filter/base_spec.rb +76 -0
- data/spec/models/filter/check_box_spec.rb +36 -0
- data/spec/models/filter/choice_spec.rb +24 -0
- data/spec/models/filter/collection_spec.rb +72 -0
- data/spec/models/filter/filter_spec.rb +35 -0
- data/spec/models/filter/select_spec.rb +12 -0
- data/spec/models/filter/text_spec.rb +16 -0
- data/spec/models/filter_form_spec.rb +212 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/value_prep/boolean_prep_spec.rb +13 -0
- data/spec/value_prep/date_prep_spec.rb +28 -0
- data/spec/value_prep/date_time_prep_spec.rb +106 -0
- data/spec/value_prep/default_prep_spec.rb +43 -0
- data/with_filters.gemspec +27 -0
- metadata +280 -0
data/.gitignore
ADDED
data/.yardopts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--no-private --markup markdown
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Aaron Lasseigne
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person
|
4
|
+
obtaining a copy of this software and associated documentation
|
5
|
+
files (the "Software"), to deal in the Software without
|
6
|
+
restriction, including without limitation the rights to use,
|
7
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
8
|
+
copies of the Software, and to permit persons to whom the
|
9
|
+
Software is furnished to do so, subject to the following
|
10
|
+
conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be
|
13
|
+
included in all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
17
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
19
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
20
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
21
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
# with_filters
|
2
|
+
|
3
|
+
Add filtering to tables, lists, etc.
|
4
|
+
|
5
|
+
This project follows [Semantic Versioning](http://semver.org/).
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
$ gem install with_filters
|
10
|
+
|
11
|
+
If you're using Bundler, add this to your Gemfile:
|
12
|
+
|
13
|
+
gem 'with_filters', '~> 0.1.0'
|
14
|
+
|
15
|
+
## Support
|
16
|
+
|
17
|
+
<table>
|
18
|
+
<tr>
|
19
|
+
<td><strong>Ruby</strong></td>
|
20
|
+
<td>1.9</td>
|
21
|
+
</tr>
|
22
|
+
<tr>
|
23
|
+
<td><strong>Rails</strong></td>
|
24
|
+
<td>3.1</td>
|
25
|
+
</tr>
|
26
|
+
<tr>
|
27
|
+
<td><strong>Database Framework</strong></td>
|
28
|
+
<td>ActiveRecord</td>
|
29
|
+
</tr>
|
30
|
+
</table>
|
31
|
+
|
32
|
+
## Usage
|
33
|
+
|
34
|
+
In your controller:
|
35
|
+
|
36
|
+
@data = Data.with_filters(params, fields: {
|
37
|
+
email_address: {match: :contains},
|
38
|
+
full_name: ->(value, scope) {
|
39
|
+
first_word, second_word = value.strip.split(/\s+/)
|
40
|
+
|
41
|
+
if second_word
|
42
|
+
scope.where(['first_name LIKE ? OR last_name LIKE ?', first_word, first_word])
|
43
|
+
else
|
44
|
+
scope.where(['first_name LIKE ? AND last_name LIKE ?', first_word, second_word])
|
45
|
+
end
|
46
|
+
}
|
47
|
+
}).order('created_at DESC').page(params[:page] || 1)
|
48
|
+
|
49
|
+
In your view:
|
50
|
+
|
51
|
+
<%= filter_form_for(@data) do |f| %>
|
52
|
+
<%# pass through variables needed in the form but not associated with filtering %>
|
53
|
+
<% if params[:order] %>
|
54
|
+
<% f.hidden(:order, name: 'order', value: params[:order]) %>
|
55
|
+
<% end %>
|
56
|
+
|
57
|
+
<% f.input(:id) %>
|
58
|
+
<% f.input_range(:created_at, label: 'Account Created On') %> <%# creates a filter to search a range %>
|
59
|
+
<% f.input(:full_name) %>
|
60
|
+
<% f.input(:email_address) %>
|
61
|
+
|
62
|
+
<% f.action(:submit, label: 'Filter') %>
|
63
|
+
<% end %>
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,14 @@
|
|
1
|
+
<%= form_tag(nil, filter_form.attrs) do %>
|
2
|
+
<%= with_filters_hidden(filter_form.hidden_filters) %>
|
3
|
+
|
4
|
+
<% filter_form.filters.each do |filter| %>
|
5
|
+
<%= with_filters_input(filter) %><br/>
|
6
|
+
<% end %>
|
7
|
+
|
8
|
+
<% if filter_form.actions.any? %>
|
9
|
+
<br/>
|
10
|
+
<% filter_form.actions.each do |action| %>
|
11
|
+
<%= with_filters_action_tag(action) %>
|
12
|
+
<% end %>
|
13
|
+
<% end %>
|
14
|
+
<% end %>
|
data/changelog.md
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
module WithFilters
|
2
|
+
class ThemeGenerator < Rails::Generators::NamedBase
|
3
|
+
desc('Create a new theme.')
|
4
|
+
|
5
|
+
VIEW_PATH = 'app/views/with_filters'
|
6
|
+
|
7
|
+
source_root File.expand_path("../../../../../#{VIEW_PATH}", __FILE__)
|
8
|
+
|
9
|
+
argument :partial, required: false
|
10
|
+
|
11
|
+
# A generator to create a theme. Any files missing from the theme will fall
|
12
|
+
# back to the original theme. It's recommended that you only generate the
|
13
|
+
# files you plan to change.
|
14
|
+
#
|
15
|
+
# @example Create an entire theme.
|
16
|
+
# $ rails generate with_filters:theme foo
|
17
|
+
#
|
18
|
+
# @example Create a single file.
|
19
|
+
# $ rails generate with_filters:theme foo filter_form
|
20
|
+
#
|
21
|
+
# @since 0.1.0
|
22
|
+
def create_theme
|
23
|
+
empty_directory(VIEW_PATH)
|
24
|
+
|
25
|
+
if partial
|
26
|
+
if partial.index('/')
|
27
|
+
(extra_dirs, partial_name) = partial.match(/^(.*)\/(.*)$/).captures
|
28
|
+
|
29
|
+
empty_directory("#{VIEW_PATH}/#{file_name}/#{extra_dirs}")
|
30
|
+
|
31
|
+
copy_file(
|
32
|
+
"#{extra_dirs}/_#{partial_name.match(/^text_as_/) ? 'text' : partial_name}.html.erb",
|
33
|
+
"#{VIEW_PATH}/#{file_name}/#{extra_dirs}/_#{partial_name}.html.erb"
|
34
|
+
)
|
35
|
+
else
|
36
|
+
copy_file("_#{partial}.html.erb", "#{VIEW_PATH}/#{file_name}/_#{partial}.html.erb")
|
37
|
+
end
|
38
|
+
else
|
39
|
+
directory('', "#{VIEW_PATH}/#{file_name}")
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
module WithFilters
|
2
|
+
module ActionViewExtension
|
3
|
+
include WithFilters::HashExtraction
|
4
|
+
|
5
|
+
# Create a filter form.
|
6
|
+
#
|
7
|
+
# @param records [ActiveRecord::Relation, Array] The data being filtered.
|
8
|
+
# @param options [Hash]
|
9
|
+
# @option options [String] :theme The theme to use when rendering the form.
|
10
|
+
# @option options [Varied] remaining ("{'novalidate: 'novalidate', method: 'get'}")
|
11
|
+
# All other options are passed as options to the `form_tag` helper.
|
12
|
+
#
|
13
|
+
# @example
|
14
|
+
# <%= filter_form_for(@data, theme: 'foo', class: 'bar') do |f|
|
15
|
+
# f.input(:full_name)
|
16
|
+
# f.input(:email)
|
17
|
+
# end %>
|
18
|
+
#
|
19
|
+
# @since 0.1.0
|
20
|
+
def filter_form_for(records, options = {})
|
21
|
+
filter_form = WithFilters::FilterForm.new(records, self.extract_hash_value(params, records.with_filters_data[:param_namespace]) || {}, options)
|
22
|
+
yield(filter_form)
|
23
|
+
|
24
|
+
render(partial: filter_form.to_partial_path, locals: {filter_form: filter_form})
|
25
|
+
end
|
26
|
+
|
27
|
+
# Create hidden inputs.
|
28
|
+
#
|
29
|
+
# @param [Array<Filter>] hidden_filters
|
30
|
+
#
|
31
|
+
# @since 0.1.0
|
32
|
+
def with_filters_hidden(hidden_filters)
|
33
|
+
hidden_filters.map{|hidden_filter|
|
34
|
+
hidden_field_tag(hidden_filter.field_name, hidden_filter.value, hidden_filter.attrs)
|
35
|
+
}.join("\n").html_safe
|
36
|
+
end
|
37
|
+
|
38
|
+
# Create an input based on the type of `filter` provided.
|
39
|
+
#
|
40
|
+
# @param [Filter] filter
|
41
|
+
#
|
42
|
+
# @since 0.1.0
|
43
|
+
def with_filters_input(filter)
|
44
|
+
render(partial: filter.to_partial_path, locals: {filter: filter})
|
45
|
+
end
|
46
|
+
|
47
|
+
# Create an text like `input` tag based on the `filter`.
|
48
|
+
#
|
49
|
+
# @param [Filter] filter
|
50
|
+
#
|
51
|
+
# @since 0.1.0
|
52
|
+
def with_filters_text_field_tag(filter)
|
53
|
+
text_field_tag(filter.field_name, filter.value, filter.attrs)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Create a `label` tag based on the `filter`.
|
57
|
+
#
|
58
|
+
# @param [Filter] filter
|
59
|
+
#
|
60
|
+
# @since 0.1.0
|
61
|
+
def with_filters_label_tag(filter)
|
62
|
+
label_tag(filter.field_name, filter.label, filter.label_attrs)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Create a `label` tag for individual fields or a `div` tag in its place in
|
66
|
+
# the case of fields where the `label` tags are used for individual items.
|
67
|
+
#
|
68
|
+
# @param [Filter] filter
|
69
|
+
#
|
70
|
+
# @since 0.1.0
|
71
|
+
def with_filters_label(filter)
|
72
|
+
if [WithFilters::Filter::Radio, WithFilters::Filter::CheckBox].include?(filter.class) and filter.collection.any?
|
73
|
+
content_tag(:div, filter.label, filter.label_attrs)
|
74
|
+
else
|
75
|
+
with_filters_label_tag(filter)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Create a `select` tag based on the `filter`.
|
80
|
+
#
|
81
|
+
# @param [Filter] filter
|
82
|
+
#
|
83
|
+
# @since 0.1.0
|
84
|
+
def with_filters_select_tag(filter)
|
85
|
+
collection = filter.collection
|
86
|
+
unless filter.collection.is_a?(String)
|
87
|
+
collection = filter.collection.map do |choice|
|
88
|
+
html_attributes = choice.attrs.length > 0 ? ' ' + choice.attrs.map {|k, v| %(#{k.to_s}="#{v}")}.join(' ') : ''
|
89
|
+
selected_attribute = choice.selected? ? ' selected="selected"' : ''
|
90
|
+
|
91
|
+
%(<option value="#{ERB::Util.html_escape(choice.value)}"#{selected_attribute}#{html_attributes}>#{ERB::Util.html_escape(choice.label)}</option>)
|
92
|
+
end.join("\n")
|
93
|
+
end
|
94
|
+
|
95
|
+
select_tag(filter.field_name, collection.html_safe, filter.attrs)
|
96
|
+
end
|
97
|
+
|
98
|
+
# @param [WithFilters::Action] action
|
99
|
+
#
|
100
|
+
# @since 0.1.0
|
101
|
+
def with_filters_action_tag(action)
|
102
|
+
case action.type
|
103
|
+
when :submit
|
104
|
+
submit_tag(action.attrs.delete(:value), action.attrs)
|
105
|
+
when :reset
|
106
|
+
button_tag(action.attrs.delete(:value) || 'Reset', action.attrs)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module WithFilters
|
2
|
+
# @private
|
3
|
+
module ActiveRecordExtension
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
class << self
|
8
|
+
alias_method_chain :inherited, :with_filters
|
9
|
+
end
|
10
|
+
|
11
|
+
# Attach the ActiveRecord extensions to children of ActiveRecord that were initiated before we loaded WithFilters.
|
12
|
+
self.descendants.each do |descendant|
|
13
|
+
descendant.send(:include, WithFilters::ActiveRecordModelExtension) if descendant.superclass == ActiveRecord::Base
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# @private
|
18
|
+
module ClassMethods
|
19
|
+
# Attaches the ActiveRecord extensions to children of ActiveRecord so we don't pollute ActiveRecord::Base.
|
20
|
+
def inherited_with_with_filters(base)
|
21
|
+
inherited_without_with_filters(base)
|
22
|
+
base.send(:include, WithFilters::ActiveRecordModelExtension) if base.superclass == ActiveRecord::Base
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,163 @@
|
|
1
|
+
module WithFilters
|
2
|
+
module ActiveRecordModelExtension
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
# @private
|
6
|
+
module AddData
|
7
|
+
# @return [Hash<:column_types, :param_namespace>] Properties relating to the current filters.
|
8
|
+
#
|
9
|
+
# @since 0.1.0
|
10
|
+
attr_accessor :with_filters_data
|
11
|
+
end
|
12
|
+
|
13
|
+
# Attaches conditions to a scope.
|
14
|
+
#
|
15
|
+
# @scope class
|
16
|
+
# @overload with_filters(params = nil, options = {})
|
17
|
+
# @param [Hash] params A hash of values to filter on.
|
18
|
+
# @param [Hash] options
|
19
|
+
# @option options [Symbol] :param_namespace ('primary table name') The namespace
|
20
|
+
# of the filters inside `params`.
|
21
|
+
# @option options [Hash, Proc] :fields When a hash is passed this is a mapping
|
22
|
+
# of custom fields to field options. When a proc is passed it is sent `value`
|
23
|
+
# and `scope` and is expected to return the scope.
|
24
|
+
# @option options [String] :fields[:column] The database column to map the field to.
|
25
|
+
# @option options [Symbol] :fields[:match] Determines the way the filter is matched.
|
26
|
+
# Accepts `:exact`, `:contains`, `:begins_with` and `:ends_with`.
|
27
|
+
#
|
28
|
+
# @example Basic
|
29
|
+
# with_filters(params)
|
30
|
+
#
|
31
|
+
# @example With a custom matching and field name.
|
32
|
+
# with_filters(params, {fields: {
|
33
|
+
# given_name: {column: 'first_name', match: :contains}
|
34
|
+
# }})
|
35
|
+
#
|
36
|
+
# @example With a custom field using a proc.
|
37
|
+
# with_filters(params, {fields: {
|
38
|
+
# full_name: ->(value, scope) {
|
39
|
+
# first_word, second_word = value.strip.split(/\s+/)
|
40
|
+
#
|
41
|
+
# if second_word
|
42
|
+
# scope.where(['first_name LIKE ? OR last_name LIKE ?', first_word, first_word])
|
43
|
+
# else
|
44
|
+
# scope.where(['first_name LIKE ? AND last_name LIKE ?', first_word, second_word])
|
45
|
+
# end
|
46
|
+
# }
|
47
|
+
# }})
|
48
|
+
#
|
49
|
+
# @return [ActiveRecord::Relation]
|
50
|
+
#
|
51
|
+
# @note Switched from `scope` to class method because of a bug in Rails 3.2.1 where
|
52
|
+
# `joins_values` aren't available in scopes.
|
53
|
+
#
|
54
|
+
# @since 0.1.0
|
55
|
+
included do
|
56
|
+
extend WithFilters::HashExtraction
|
57
|
+
def self.with_filters(params = nil, options = {})
|
58
|
+
relation = self.scoped.extend(AddData).extending do
|
59
|
+
def to_a
|
60
|
+
a = super.extend(AddData)
|
61
|
+
a.with_filters_data = self.with_filters_data
|
62
|
+
a
|
63
|
+
end
|
64
|
+
end
|
65
|
+
param_namespace = options.delete(:param_namespace) || relation.table_name.to_sym
|
66
|
+
relation.with_filters_data = {
|
67
|
+
param_namespace: param_namespace,
|
68
|
+
column_types: find_column_types(relation, options[:fields] || {})
|
69
|
+
}
|
70
|
+
|
71
|
+
scoped_params = params ? self.extract_hash_value(params, param_namespace) || {} : {}
|
72
|
+
scoped_params.each do |field, value|
|
73
|
+
# skip blank entries
|
74
|
+
value.reject!{|v| v.blank?} if value.is_a?(Array)
|
75
|
+
if (value.is_a?(String) and value.blank?) or
|
76
|
+
(value.is_a?(Array) and value.empty?) or
|
77
|
+
(value.is_a?(Hash) and not (value[:start].present? and value[:stop].present?))
|
78
|
+
next
|
79
|
+
end
|
80
|
+
|
81
|
+
field_options = {}
|
82
|
+
field_options = options[:fields][field] if options[:fields] and options[:fields][field]
|
83
|
+
|
84
|
+
if field_options.is_a?(Proc)
|
85
|
+
relation = field_options.call(value, relation)
|
86
|
+
else
|
87
|
+
db_column_table_name, db_column_name = (field_options.delete(:column) || field).to_s.split('.')
|
88
|
+
if db_column_name.nil?
|
89
|
+
db_column_name = db_column_table_name
|
90
|
+
db_column_table_name = relation.column_names.include?(db_column_name) ? self.table_name : nil
|
91
|
+
end
|
92
|
+
|
93
|
+
quoted_field = relation.connection.quote_column_name(db_column_name)
|
94
|
+
quoted_field = "#{db_column_table_name}.#{quoted_field}" if db_column_table_name
|
95
|
+
|
96
|
+
value = WithFilters::ValuePrep.prepare(relation.with_filters_data[:column_types][field.to_sym], value, field_options)
|
97
|
+
|
98
|
+
# attach filter
|
99
|
+
relation = case value.class.name.to_sym
|
100
|
+
when :Array
|
101
|
+
relation.where([Array.new(value.size, "#{quoted_field} LIKE ?").join(' OR '), *value])
|
102
|
+
when :Hash
|
103
|
+
if ![:datetime, :timestamp].include?(relation.with_filters_data[:column_types][field]) or Date._parse(value[:start])[:sec_fraction]
|
104
|
+
relation.where(["#{quoted_field} BETWEEN :start AND :stop", value])
|
105
|
+
else
|
106
|
+
relation.where(["#{quoted_field} >= :start AND #{quoted_field} < :stop", value])
|
107
|
+
end
|
108
|
+
when :String, :FalseClass, :TrueClass, :Date, :Time
|
109
|
+
relation.where(["#{quoted_field} LIKE ?", value])
|
110
|
+
else
|
111
|
+
relation
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
relation
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# @private
|
121
|
+
module ClassMethods
|
122
|
+
# @param [ActiveRecord::Relation] relation A relation to find the columns of.
|
123
|
+
# @param [Hash] field_options
|
124
|
+
#
|
125
|
+
# @return [Hash<String>] A mapping of fields to their database types.
|
126
|
+
#
|
127
|
+
# @since 0.1.0
|
128
|
+
def find_column_types(relation, field_options)
|
129
|
+
field_options = field_options.reject{|k, v| v.is_a?(Proc)}
|
130
|
+
|
131
|
+
# primary table column types
|
132
|
+
column_types = Hash[*relation.columns.map{|column|
|
133
|
+
[
|
134
|
+
field_options.detect{|field, options|
|
135
|
+
[column.name, "#{relation.table_name}.#{column.name}"].include?(options[:column].to_s)
|
136
|
+
}.try(:first) || column.name.to_sym,
|
137
|
+
column.type
|
138
|
+
]
|
139
|
+
}.flatten]
|
140
|
+
|
141
|
+
# non-primary table columns
|
142
|
+
(relation.joins_values + relation.includes_values).uniq.map{|join|
|
143
|
+
# convert string joins to table names
|
144
|
+
if join.is_a?(String)
|
145
|
+
join.scan(/\G(?:(?:,|\bjoin\s)\s*(\w+))/i)
|
146
|
+
else
|
147
|
+
join
|
148
|
+
end
|
149
|
+
}.flatten.map do |table_name|
|
150
|
+
ActiveRecord::Base::connection.columns(table_name.to_s.tableize).each do |column|
|
151
|
+
column_name = field_options.detect{|field, options|
|
152
|
+
[column.name, "#{table_name}.#{column.name}"].include?(options[:column].to_s)
|
153
|
+
}.try(:first) || column.name.to_sym
|
154
|
+
|
155
|
+
column_types.reverse_merge!(column_name => column.type)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
column_types
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module WithFilters
|
2
|
+
# @private
|
3
|
+
module HashExtraction
|
4
|
+
# Extracts the value from a hash and takes nesting into account.
|
5
|
+
#
|
6
|
+
# @param [Hash] hash The hash to search.
|
7
|
+
# @param [String, Symbol] key The key or nested key to search for.
|
8
|
+
#
|
9
|
+
# @return [Object, nil]
|
10
|
+
#
|
11
|
+
# @example Key is a symbol.
|
12
|
+
# extract_hash_key({foo: 'bar'}, :foo) # => 'bar'
|
13
|
+
#
|
14
|
+
# @example Key is a string with nesting.
|
15
|
+
# extract_hash_key({foo: {bar: 'baz'}}, 'foo[:bar]') # => 'baz'
|
16
|
+
#
|
17
|
+
# @since 0.1.0
|
18
|
+
def extract_hash_value(hash, key)
|
19
|
+
key = key.to_s
|
20
|
+
hash = hash.stringify_keys
|
21
|
+
|
22
|
+
if hash[key]
|
23
|
+
hash[key]
|
24
|
+
else
|
25
|
+
first_key, remaining_content = key.to_s.match(/^([^\[]+)(.*)$/).captures
|
26
|
+
|
27
|
+
eval "hash[first_key]#{remaining_content.gsub(/\[/, '["').gsub(/]/, '"]')} rescue nil"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module WithFilters
|
2
|
+
module Filter
|
3
|
+
# @private
|
4
|
+
class Base
|
5
|
+
attr_reader :to_partial_path, :label, :label_attrs, :field_name, :value, :attrs, :collection
|
6
|
+
|
7
|
+
def initialize(name, namespace, value, options = {})
|
8
|
+
@value = value
|
9
|
+
|
10
|
+
@theme = options.delete(:theme)
|
11
|
+
@label = options.delete(:label) || name.to_s.titleize
|
12
|
+
@label_attrs = options.delete(:label_attrs) || {}
|
13
|
+
@field_name = options[:field_name] || "#{namespace}[#{name}]"
|
14
|
+
@collection = options[:collection] ? Collection.new(@field_name, options.delete(:collection) || [], {selected: value}) : nil
|
15
|
+
@attrs = options
|
16
|
+
|
17
|
+
@to_partial_path = create_partial_path
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def create_partial_path
|
23
|
+
partial_path = self.class.name.underscore
|
24
|
+
|
25
|
+
if @theme
|
26
|
+
themed_partial_path = partial_path.split(File::SEPARATOR).insert(1, @theme)
|
27
|
+
if Dir.glob(File.join(Rails.root, 'app', 'views', *themed_partial_path).sub(/([^#{File::SEPARATOR}]+?)$/, '_\1.*')).any?
|
28
|
+
partial_path = themed_partial_path.join(File::SEPARATOR)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
partial_path
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module WithFilters
|
2
|
+
module Filter
|
3
|
+
# @private
|
4
|
+
class BaseStart < Base
|
5
|
+
def initialize(name, namespace, value, options = {})
|
6
|
+
super
|
7
|
+
|
8
|
+
@field_name << '[start]'
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
# @private
|
13
|
+
class BaseStop < Base
|
14
|
+
def initialize(name, namespace, value, options = {})
|
15
|
+
super
|
16
|
+
|
17
|
+
@field_name << '[stop]'
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# @private
|
22
|
+
class BaseRange < Base
|
23
|
+
attr_reader :start, :stop
|
24
|
+
|
25
|
+
def initialize(name, namespace, value, options = {})
|
26
|
+
start_attrs = options.delete(:start) || {}
|
27
|
+
stop_attrs = options.delete(:stop) || {}
|
28
|
+
|
29
|
+
super
|
30
|
+
|
31
|
+
start_attrs.reverse_merge!(@attrs)
|
32
|
+
start_attrs.reverse_merge!(label: self.label, label_attrs: self.label_attrs, collection: @collection)
|
33
|
+
stop_attrs.reverse_merge!(@attrs)
|
34
|
+
stop_attrs.reverse_merge!(label: self.label, label_attrs: self.label_attrs, collection: @collection)
|
35
|
+
|
36
|
+
base_class_name = self.class.to_s.match(/^(.*)Range$/).captures.first
|
37
|
+
@start = "#{base_class_name}Start".constantize.new(name, namespace, value[:start], start_attrs)
|
38
|
+
@stop = "#{base_class_name}Stop".constantize.new(name, namespace, value[:stop], stop_attrs)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module WithFilters
|
2
|
+
module Filter
|
3
|
+
# @private
|
4
|
+
class CheckBox < Base
|
5
|
+
def initialize(name, namespace, value, options = {})
|
6
|
+
super
|
7
|
+
|
8
|
+
@to_partial_path << 'es' unless self.collection.nil?
|
9
|
+
end
|
10
|
+
|
11
|
+
def selected?
|
12
|
+
if self.collection.nil?
|
13
|
+
value.to_s == 'on'
|
14
|
+
else
|
15
|
+
value.present?
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|