with_filters 0.1.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.
- 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
|