acts_as_data_table 0.0.1
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 +18 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +55 -0
- data/Rakefile +11 -0
- data/acts_as_data_table.gemspec +25 -0
- data/app/coffeescripts/acts_as_data_table.coffee +47 -0
- data/app/helpers/acts_as_data_table_helper.rb +170 -0
- data/config/locales/acts_as_data_table.en.yml +11 -0
- data/generators/acts_as_data_table/acts_as_data_table_generator.rb +13 -0
- data/generators/acts_as_data_table/templates/assets/js/acts_as_data_table.js +37 -0
- data/init.rb +1 -0
- data/lib/acts_as_data_table/multi_column_scopes.rb +116 -0
- data/lib/acts_as_data_table/scope_filters/action_controller.rb +139 -0
- data/lib/acts_as_data_table/scope_filters/active_record.rb +264 -0
- data/lib/acts_as_data_table/scope_filters/form_helper.rb +67 -0
- data/lib/acts_as_data_table/scope_filters/validator.rb +144 -0
- data/lib/acts_as_data_table/session_helper.rb +193 -0
- data/lib/acts_as_data_table/shared/action_controller.rb +25 -0
- data/lib/acts_as_data_table/shared/session.rb +312 -0
- data/lib/acts_as_data_table/sortable_columns/action_controller.rb +111 -0
- data/lib/acts_as_data_table/sortable_columns/active_record.rb +34 -0
- data/lib/acts_as_data_table/sortable_columns/renderers/bootstrap2.rb +17 -0
- data/lib/acts_as_data_table/sortable_columns/renderers/default.rb +82 -0
- data/lib/acts_as_data_table/version.rb +3 -0
- data/lib/acts_as_data_table.rb +71 -0
- data/lib/acts_as_data_table_helper.rb.bak +165 -0
- data/lib/named_scope_filters.rb +273 -0
- data/lib/tasks/acts_as_data_table.rake +4 -0
- data/rails/init.rb +4 -0
- data/test/acts_as_searchable_test.rb +8 -0
- data/test/test_helper.rb +4 -0
- metadata +142 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2015 Stefan Exner
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
# ActsAsDataTable
|
2
|
+
|
3
|
+
This gem adds automatic filtering and sorting to models and controllers in Rails applications.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'acts_as_data_table'
|
11
|
+
```
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install acts_as_data_table
|
20
|
+
|
21
|
+
The `sortable_columns` helper will also need a javascript file if you'd like to
|
22
|
+
use the `CTRL + Click` way of adding new sorting columns.
|
23
|
+
You can easily generated it by using
|
24
|
+
|
25
|
+
$ ruby script/generate acts_as_data_table js
|
26
|
+
|
27
|
+
Please note that this javascript addon requires jQuery.
|
28
|
+
|
29
|
+
## Usage
|
30
|
+
|
31
|
+
The gem consists of 3 parts:
|
32
|
+
|
33
|
+
1. Multi Column Queries, e.g. a full text search over several table columns
|
34
|
+
2. Automatic filtering based on (named) scopes defined in the model
|
35
|
+
3. Automatic sorting (`ORDER BY`) by multiple columns
|
36
|
+
|
37
|
+
### Multi Column Queries
|
38
|
+
|
39
|
+
TODO
|
40
|
+
|
41
|
+
### Scope Filters
|
42
|
+
|
43
|
+
TODO
|
44
|
+
|
45
|
+
### Sortable Columns
|
46
|
+
|
47
|
+
TODO
|
48
|
+
|
49
|
+
## Contributing
|
50
|
+
|
51
|
+
1. Fork it ( https://github.com/stex/acts_as_data_table/fork )
|
52
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
53
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
54
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
55
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
|
3
|
+
desc "Compiles the plugin's coffeescript to a js file"
|
4
|
+
task :make do |t|
|
5
|
+
input_files = Dir['./app/coffeescripts/**/*.coffee']
|
6
|
+
output_directory = './generators/acts_as_data_table/templates/assets/js'
|
7
|
+
|
8
|
+
input_files.each do |file|
|
9
|
+
`coffee -c --no-header -b -o #{output_directory} #{file}`
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'acts_as_data_table/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "acts_as_data_table"
|
8
|
+
spec.version = ActsAsDataTable::VERSION
|
9
|
+
spec.authors = ["Stefan Exner"]
|
10
|
+
spec.email = ["stex@sterex.de"]
|
11
|
+
spec.summary = %q{Adds automatic scope based filtering and column sorting to controllers and models.}
|
12
|
+
spec.description = %q{Adds methods to models and controllers to perform automatic filtering, sorting and multi-column-queries without having to worry about the implementation.}
|
13
|
+
spec.homepage = 'https://www.github.com/stex/acts_as_data_table'
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.7"
|
22
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
23
|
+
|
24
|
+
spec.add_dependency 'rails', '~> 2.3'
|
25
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
#
|
2
|
+
# Handles clicks on table column headers and adds the functionality
|
3
|
+
# for CTRL+Click. The different actions are triggered as follows:
|
4
|
+
#
|
5
|
+
# Normal click: The column will be the only sorting column
|
6
|
+
# CTRL + click on inactive column: Column will be added to sorting list
|
7
|
+
# CTRL + click on active column: Column will be removed from sorting list
|
8
|
+
#
|
9
|
+
jQuery(document).ready () ->
|
10
|
+
jQuery(document).on "click", '[data-init=sortable-column]', (e) ->
|
11
|
+
#Keep the browser from jumping to the top due to the href='#'
|
12
|
+
e.preventDefault()
|
13
|
+
|
14
|
+
urlToggle = jQuery(@).data('urlToggle')
|
15
|
+
urlSetBase = jQuery(@).data('urlSetBase')
|
16
|
+
|
17
|
+
remote = jQuery(@).data('remote')
|
18
|
+
active = jQuery(@).data('active')
|
19
|
+
|
20
|
+
url = urlSetBase
|
21
|
+
|
22
|
+
if e.ctrlKey || e.metaKey
|
23
|
+
url = urlToggle
|
24
|
+
|
25
|
+
if remote
|
26
|
+
jQuery.ajax
|
27
|
+
'url': url,
|
28
|
+
dataType: 'script'
|
29
|
+
else
|
30
|
+
window.location.href = url
|
31
|
+
|
32
|
+
return false
|
33
|
+
|
34
|
+
jQuery(document).on "click", "[data-init=sortable-column-direction]", (e) ->
|
35
|
+
e.preventDefault()
|
36
|
+
|
37
|
+
url = jQuery(@).data('urlChangeDirection')
|
38
|
+
remote = jQuery(@).data('remote')
|
39
|
+
|
40
|
+
if remote
|
41
|
+
jQuery.ajax
|
42
|
+
'url': url,
|
43
|
+
dataType: 'script'
|
44
|
+
else
|
45
|
+
window.location.href = url
|
46
|
+
|
47
|
+
|
@@ -0,0 +1,170 @@
|
|
1
|
+
module ActsAsDataTableHelper
|
2
|
+
require 'ostruct'
|
3
|
+
|
4
|
+
#
|
5
|
+
# Generates a link to add/remove a certain scope filter
|
6
|
+
#
|
7
|
+
# @param [Hash] options
|
8
|
+
# Options to customize the generated link
|
9
|
+
#
|
10
|
+
# @option options [String, Symbol] :scope
|
11
|
+
# The scope within the given +group+ to be added/removed
|
12
|
+
#
|
13
|
+
# @option options [TrueClass, FalseClass] :toggle (false)
|
14
|
+
# If set to +true+, the link will automatically remove the filter
|
15
|
+
# if it's currently active without having to explicitly set +:remove+
|
16
|
+
#
|
17
|
+
# @option options [TrueClass, FalseClass] :remove (false)
|
18
|
+
# If set to +true+, the link will always attempt to remove
|
19
|
+
# the given +:scope+ within the given +group+ and cannot be used
|
20
|
+
# to add it.
|
21
|
+
#
|
22
|
+
def scope_filter_link(group, scope, options = {})
|
23
|
+
caption = options.delete(:caption) || scope_filter_caption(group, scope, options[:args])
|
24
|
+
surrounding_tag = options.delete(:surrounding)
|
25
|
+
remote = options.delete(:remote)
|
26
|
+
args = options[:args]
|
27
|
+
url = scope_filter_link_url(group, scope, options)
|
28
|
+
|
29
|
+
classes = options[:class].try(:split, ' ') || []
|
30
|
+
classes << 'active' if scope && acts_as_data_table_session.active_filter?(group, scope, args)
|
31
|
+
|
32
|
+
options[:class] = classes.join(' ')
|
33
|
+
|
34
|
+
if remote
|
35
|
+
link = link_to_remote caption, :url => url, :method => :get, :html => options
|
36
|
+
else
|
37
|
+
link = link_to caption, url, options
|
38
|
+
end
|
39
|
+
|
40
|
+
surrounding_tag ? content_tag(surrounding_tag, link, :class => options[:class]) : link
|
41
|
+
end
|
42
|
+
|
43
|
+
#
|
44
|
+
# Generates a URL to add/remove/toggle a given scope filter
|
45
|
+
#
|
46
|
+
# @see #scope_filter_link for arguments
|
47
|
+
#
|
48
|
+
# @return [String] The generated URL
|
49
|
+
#
|
50
|
+
def scope_filter_link_url(group, scope, options)
|
51
|
+
args = options.delete(:args)
|
52
|
+
toggle = options.delete(:toggle)
|
53
|
+
auto_remove = scope && toggle && acts_as_data_table_session.active_filter?(group, scope, args)
|
54
|
+
remove = options.delete(:remove) || auto_remove
|
55
|
+
|
56
|
+
if remove
|
57
|
+
url_for({:scope_filters => {:action => 'remove', :group => group}})
|
58
|
+
else
|
59
|
+
url_for({:scope_filters => {:action => 'add', :group => group, :scope => scope, :args => args}})
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
#
|
64
|
+
# Generates a URL to be used as form action for filters which require
|
65
|
+
# dynamic arguments
|
66
|
+
#
|
67
|
+
# @return [String] the generated URL
|
68
|
+
#
|
69
|
+
# @example Using the rails form helper with a scope filter url
|
70
|
+
#
|
71
|
+
# - form_tag(scope_filter_form_url) do
|
72
|
+
# ...
|
73
|
+
#
|
74
|
+
def scope_filter_form_url(group, scope)
|
75
|
+
url_for({:scope_filters => {:action => 'add', :group => group, :scope => scope}})
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
def scope_filter_form(group, scope, options = {}, &proc)
|
80
|
+
content = capture(Acts::DataTable::ScopeFilters::FormHelper.new(self, group, scope), &proc)
|
81
|
+
method = options[:method] || :get
|
82
|
+
if options.delete(:remote)
|
83
|
+
options.delete(:method)
|
84
|
+
form_remote_tag :url => scope_filter_form_url(group, scope), :method => method, :html => options do
|
85
|
+
concat(content)
|
86
|
+
end
|
87
|
+
else
|
88
|
+
form_tag scope_filter_form_url(group, scope), options do
|
89
|
+
concat(content)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
#
|
95
|
+
# @return [String] URL to remove all active scope filters for the current action
|
96
|
+
#
|
97
|
+
def scope_filter_reset_url
|
98
|
+
url_for({:scope_filters => {:action => :reset}})
|
99
|
+
end
|
100
|
+
|
101
|
+
#
|
102
|
+
# Looks up a set argument for a given filter, e.g. to highlight it
|
103
|
+
# in the search results
|
104
|
+
#
|
105
|
+
# @return [Object, NilClass] the argument used for the given scope
|
106
|
+
# or +nil+ if the filter is currently not active
|
107
|
+
#
|
108
|
+
def scope_filter_arg(group, scope, name)
|
109
|
+
Acts::DataTable.lookup_nested_hash(acts_as_data_table_session.active_filters, group.to_s, scope.to_s, name.to_s)
|
110
|
+
end
|
111
|
+
|
112
|
+
#
|
113
|
+
# @return [Hash] Arguments used for the given filter
|
114
|
+
#
|
115
|
+
def scope_filter_args(group, scope)
|
116
|
+
Acts::DataTable.lookup_nested_hash(acts_as_data_table_session.active_filters, group.to_s, scope.to_s) || {}
|
117
|
+
end
|
118
|
+
|
119
|
+
#
|
120
|
+
# Generates a scope filter caption
|
121
|
+
#
|
122
|
+
def scope_filter_caption(group, scope, args = {})
|
123
|
+
model = Acts::DataTable::ScopeFilters::ActionController.get_request_model
|
124
|
+
Acts::DataTable::ScopeFilters::ActiveRecord.scope_filter_caption(model, group, scope, args)
|
125
|
+
end
|
126
|
+
|
127
|
+
def active_scope_filter(group)
|
128
|
+
acts_as_data_table_session.active_filter(group)
|
129
|
+
end
|
130
|
+
|
131
|
+
#----------------------------------------------------------------
|
132
|
+
# Sortable Columns
|
133
|
+
#----------------------------------------------------------------
|
134
|
+
|
135
|
+
def sortable_column(model, column, caption, options = {}, &proc)
|
136
|
+
renderer_class = options.delete(:renderer) || Acts::DataTable::SortableColumns::Renderers.default_renderer
|
137
|
+
|
138
|
+
sortable = sortable_column_data(model, column)
|
139
|
+
sortable[:html_options] = options
|
140
|
+
sortable[:caption] = caption
|
141
|
+
sortable[:remote] = options.delete(:remote)
|
142
|
+
sortable[:remote] = true if sortable[:remote].blank?
|
143
|
+
sortable = OpenStruct.new(sortable)
|
144
|
+
|
145
|
+
#If a block is given, we let the user handle the content of the table
|
146
|
+
#header himself. Otherwise, we'll generate the default links.
|
147
|
+
if block_given?
|
148
|
+
yield sortable
|
149
|
+
else
|
150
|
+
renderer = renderer_class.constantize.new(sortable, self)
|
151
|
+
renderer.to_html
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def sortable_column_data(model, column)
|
156
|
+
sortable = {}
|
157
|
+
|
158
|
+
sortable[:direction] = acts_as_data_table_session.sorting_direction(model, column)
|
159
|
+
sortable[:active] = acts_as_data_table_session.active_column?(model, column)
|
160
|
+
|
161
|
+
urls = {}
|
162
|
+
urls[:toggle] = url_for({:sortable_columns => {:action => :toggle, :model => model, :column => column}})
|
163
|
+
urls[:change_direction] = url_for({:sortable_columns => {:action => :change_direction, :model => model, :column => column}})
|
164
|
+
urls[:set_base] = url_for({:sortable_columns => {:action => :set_base, :model => model, :column => column}})
|
165
|
+
sortable[:urls] = OpenStruct.new(urls)
|
166
|
+
|
167
|
+
sortable
|
168
|
+
end
|
169
|
+
|
170
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
en:
|
2
|
+
acts_as_data_table:
|
3
|
+
scope_filters:
|
4
|
+
add_filter:
|
5
|
+
filter_not_registered: "The filter '%{scope_name}' in group '%{group}' was not properly registered in model '%{model}'"
|
6
|
+
non_matching_arity: "The filter '%{scope_name}' in group '%{group}' registered in model '%{model}' did not get the required amount of arguments"
|
7
|
+
|
8
|
+
validations:
|
9
|
+
general_error: "The scope filter could not be applied successfully"
|
10
|
+
invalid_date: "%{arg_name} ('%{arg_value}') is not a valid date"
|
11
|
+
invalid_record: "The filter argument '%{arg_name}' did not match any valid record in the system."
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class ActsAsDataTableGenerator < Rails::Generator::NamedBase
|
2
|
+
def manifest
|
3
|
+
record do |m|
|
4
|
+
m.file File.join('assets', 'js', 'acts_as_data_table.js'), File.join('public', 'javascripts', 'acts_as_data_table.js')
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
protected
|
9
|
+
|
10
|
+
def banner
|
11
|
+
"Usage: #{$0} acts_as_data_table js"
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
jQuery(document).ready(function() {
|
2
|
+
jQuery(document).on("click", '[data-init=sortable-column]', function(e) {
|
3
|
+
var active, remote, url, urlSetBase, urlToggle;
|
4
|
+
e.preventDefault();
|
5
|
+
urlToggle = jQuery(this).data('urlToggle');
|
6
|
+
urlSetBase = jQuery(this).data('urlSetBase');
|
7
|
+
remote = jQuery(this).data('remote');
|
8
|
+
active = jQuery(this).data('active');
|
9
|
+
url = urlSetBase;
|
10
|
+
if (e.ctrlKey || e.metaKey) {
|
11
|
+
url = urlToggle;
|
12
|
+
}
|
13
|
+
if (remote) {
|
14
|
+
jQuery.ajax({
|
15
|
+
'url': url,
|
16
|
+
dataType: 'script'
|
17
|
+
});
|
18
|
+
} else {
|
19
|
+
window.location.href = url;
|
20
|
+
}
|
21
|
+
return false;
|
22
|
+
});
|
23
|
+
return jQuery(document).on("click", "[data-init=sortable-column-direction]", function(e) {
|
24
|
+
var remote, url;
|
25
|
+
e.preventDefault();
|
26
|
+
url = jQuery(this).data('urlChangeDirection');
|
27
|
+
remote = jQuery(this).data('remote');
|
28
|
+
if (remote) {
|
29
|
+
return jQuery.ajax({
|
30
|
+
'url': url,
|
31
|
+
dataType: 'script'
|
32
|
+
});
|
33
|
+
} else {
|
34
|
+
return window.location.href = url;
|
35
|
+
}
|
36
|
+
});
|
37
|
+
});
|
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/rails/init'
|
@@ -0,0 +1,116 @@
|
|
1
|
+
module Acts
|
2
|
+
module DataTable
|
3
|
+
module MultiColumnScopes
|
4
|
+
def self.included(base)
|
5
|
+
base.send :extend, ClassMethods
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
#
|
10
|
+
# Generates a scope to search for a given string in multiple
|
11
|
+
# columns at once. Columns may either be in the own table
|
12
|
+
# or in associated tables.
|
13
|
+
# It also automatically generates concat queries to enable
|
14
|
+
# full name searches, e.g. for a users table with
|
15
|
+
# separate columns for first and last name (see examples below)
|
16
|
+
#
|
17
|
+
# The result can be used further to chain the query like
|
18
|
+
# every other scope
|
19
|
+
#
|
20
|
+
# @param [String, Symbol] scope_name
|
21
|
+
# The newly generated scope's name, e.g. :full_text
|
22
|
+
#
|
23
|
+
# If the last argument is a Hash, it will be used as options.
|
24
|
+
#
|
25
|
+
# @option options [Boolean] :downcase (true)
|
26
|
+
# If set to +true+, the both searched query and
|
27
|
+
# database values will be converted to lowercase to support
|
28
|
+
# case insensitivity
|
29
|
+
#
|
30
|
+
# @example Basic usage, searching in two columns
|
31
|
+
# has_multi_column_scope :email_or_name, :email, :name
|
32
|
+
#
|
33
|
+
# @example Searching for a full name by concatenating two columns
|
34
|
+
# has_multi_column_scope :email_or_name, :email, [:first_name, :last_name]
|
35
|
+
#
|
36
|
+
# @example Including an association named :title (belongs_to :title)
|
37
|
+
# has_multi_column_scope :name_or_title, [:first_name, :last_name], {:title => :name}, {}
|
38
|
+
# #The empty has at the end is necessary as otherwise the title-hash would
|
39
|
+
# #be taken as options.
|
40
|
+
#
|
41
|
+
# The method does currently not support chained includes, this
|
42
|
+
# will be added in the future (e.g. :user_rooms => {:room => :number})
|
43
|
+
#
|
44
|
+
def has_multi_column_scope(scope_name, *args)
|
45
|
+
options = {:downcase => true}
|
46
|
+
options.merge!(args.pop) if args.size > 1 && args.last.is_a?(Hash)
|
47
|
+
|
48
|
+
include_chain = []
|
49
|
+
|
50
|
+
fields = args.map {|arg| Acts::DataTable::MultiColumnScopes.process_column_arg(arg, include_chain, self)}
|
51
|
+
fields = fields.flatten.map {|f| Acts::DataTable::MultiColumnScopes.database_cast(f)}
|
52
|
+
fields = fields.map {|f| "LOWER(#{f})"} if options[:downcase]
|
53
|
+
|
54
|
+
conditions = fields.map {|f| "#{f} LIKE ?"}.join(' OR ')
|
55
|
+
include_chain = include_chain.uniq.compact
|
56
|
+
|
57
|
+
named_scope scope_name, lambda {|search|
|
58
|
+
if search.present?
|
59
|
+
s = "%#{search}%"
|
60
|
+
s = s.downcase if options[:downcase]
|
61
|
+
{ :include => include_chain, :conditions => [conditions] + Array.new(fields.size, s) }
|
62
|
+
else
|
63
|
+
{}
|
64
|
+
end
|
65
|
+
}
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
#
|
70
|
+
# Processes a single argument for has_multi_column_scope.
|
71
|
+
# Handles
|
72
|
+
# - simple values (e.g. :first_name),
|
73
|
+
# - arrays which will be concatenated with a space character (e.g. [:first_name, :last_name])
|
74
|
+
# - hashes which represent associations on the main model (e.g. :student => [:mat_num])
|
75
|
+
#
|
76
|
+
def self.process_column_arg(arg, includes, model = self)
|
77
|
+
if arg.is_a?(Hash)
|
78
|
+
res = []
|
79
|
+
arg.each do |association, columns|
|
80
|
+
includes << association
|
81
|
+
Array(columns).each do |column|
|
82
|
+
res << process_column_arg(column, includes, association.to_s.singularize.classify.constantize)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
res
|
86
|
+
elsif arg.is_a?(Array)
|
87
|
+
columns = arg.map {|a| process_column_arg(a, includes, model)}
|
88
|
+
columns = columns.map {|c| "TRIM(#{c})"}
|
89
|
+
"CONCAT(#{columns.join(", ' ', ")})"
|
90
|
+
else
|
91
|
+
if model.column_names.include?(arg.to_s)
|
92
|
+
[model.table_name, arg.to_s].join('.')
|
93
|
+
else
|
94
|
+
raise ArgumentError.new "The table '#{model.table_name}' does not have a column named '#{arg}'"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
#
|
100
|
+
# Performs a cast to text-like for various database types
|
101
|
+
#
|
102
|
+
def self.database_cast(content)
|
103
|
+
case ActiveRecord::Base.connection.adapter_name
|
104
|
+
when 'MySQL'
|
105
|
+
"CAST(#{content} AS CHAR(10000) CHARACTER SET utf8)"
|
106
|
+
else
|
107
|
+
"CAST(#{content} AS TEXT)"
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def self.build_scope_name(*args)
|
112
|
+
args.join('_').to_sym
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
module Acts
|
2
|
+
module DataTable
|
3
|
+
module ScopeFilters
|
4
|
+
module ActionController
|
5
|
+
|
6
|
+
def self.included(base)
|
7
|
+
base.send :extend, ClassMethods
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
#
|
12
|
+
# Adds scope filter support to this controller (or parts of it)
|
13
|
+
#
|
14
|
+
# @param [Hash] options
|
15
|
+
# Options to customize the behaviour
|
16
|
+
#
|
17
|
+
# @option options [Array<Symbol>] :only
|
18
|
+
# If given, filters are only applied to actions which are in the given array
|
19
|
+
#
|
20
|
+
# @option options [Array<Symbol>] :except
|
21
|
+
# If given, filters are only applied to actions which _not_ in the given array
|
22
|
+
#
|
23
|
+
# @option options [String, Symbol] :model
|
24
|
+
# The model name which will be used as filter base.
|
25
|
+
# If not given, the name is inferred from the controller name
|
26
|
+
#
|
27
|
+
def scope_filters(options = {})
|
28
|
+
#Include on-demand methods
|
29
|
+
include Acts::DataTable::Shared::ActionController::OnDemand
|
30
|
+
|
31
|
+
#Add helper methods to this controller's views
|
32
|
+
helper :acts_as_data_table
|
33
|
+
|
34
|
+
model_name = (options.delete(:model) || self.name.underscore.split('/').last.sub('_controller', '')).to_s.camelize.singularize
|
35
|
+
|
36
|
+
#Create a custom before filter
|
37
|
+
around_filter(options) do |controller, block|
|
38
|
+
|
39
|
+
sf_params = controller.request.params[:scope_filters]
|
40
|
+
|
41
|
+
begin
|
42
|
+
|
43
|
+
#Set current filters before adding/removing based on the current request
|
44
|
+
#This is needed for certain actions within the session
|
45
|
+
Acts::DataTable::ScopeFilters::ActionController.set_request_filters!(model_name, controller.acts_as_data_table_session.active_filters)
|
46
|
+
|
47
|
+
#Ensure that any scope filter related params are given and
|
48
|
+
#that the given action is valid (add a filter, remove a filter, remove all filters)
|
49
|
+
if sf_params.present? && %w(add remove reset).include?(sf_params[:action])
|
50
|
+
case sf_action = sf_params[:action].to_s
|
51
|
+
when 'add'
|
52
|
+
#Ensure that a group and a scope name are given
|
53
|
+
if [:group, :scope].all? { |p| sf_params[p].present? }
|
54
|
+
unless controller.acts_as_data_table_session.add_filter(sf_params[:group], sf_params[:scope], sf_params[:args])
|
55
|
+
#TODO: add error message if the validation failed or the filter was not available
|
56
|
+
end
|
57
|
+
end
|
58
|
+
when 'remove'
|
59
|
+
#Ensure that a group and a filter name are given
|
60
|
+
if sf_params[:group].present?
|
61
|
+
controller.acts_as_data_table_session.remove_filter!(sf_params[:group])
|
62
|
+
end
|
63
|
+
when 'reset'
|
64
|
+
controller.acts_as_data_table_session.remove_all_filters!
|
65
|
+
else
|
66
|
+
raise ArgumentError.new "Invalid scope filter action '#{sf_action}' was given."
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
#Set the updated filters
|
71
|
+
Acts::DataTable::ScopeFilters::ActionController.set_request_filters!(model_name, controller.acts_as_data_table_session.active_filters)
|
72
|
+
block.call
|
73
|
+
ensure
|
74
|
+
Acts::DataTable::ScopeFilters::ActionController.clear_request_filters!
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
#
|
81
|
+
# Returns the currently active scope filters
|
82
|
+
#
|
83
|
+
# This function should only be used when the automatic scope +with_scope_filters+
|
84
|
+
# is not working due to a different execution time or thread, e.g. a background worker.
|
85
|
+
#
|
86
|
+
def current_scope_filters
|
87
|
+
Acts::DataTable::ScopeFilters::ActionController.get_request_filters
|
88
|
+
end
|
89
|
+
|
90
|
+
#----------------------------------------------------------------
|
91
|
+
# Module Methods
|
92
|
+
#----------------------------------------------------------------
|
93
|
+
|
94
|
+
#
|
95
|
+
# Saves the current request's active filters to the thread space
|
96
|
+
#
|
97
|
+
def self.set_request_filters!(model, filters)
|
98
|
+
Acts::DataTable.ensure_nested_hash!(Thread.current, :scope_filters)
|
99
|
+
|
100
|
+
current_scopes = filters.inject({}) do |h, (group, scope)|
|
101
|
+
h[group] = [scope.keys.first, scope[scope.keys.first]]
|
102
|
+
h
|
103
|
+
end
|
104
|
+
|
105
|
+
Thread.current[:scope_filters] = {:model => model.to_s, :filters => current_scopes}
|
106
|
+
end
|
107
|
+
|
108
|
+
#
|
109
|
+
# Fetches the current request's filter data from the thread space
|
110
|
+
#
|
111
|
+
def self.get_request_data
|
112
|
+
Acts::DataTable.lookup_nested_hash(Thread.current, :scope_filters)
|
113
|
+
end
|
114
|
+
|
115
|
+
#
|
116
|
+
# Fetches the current request's active filters from the thread space
|
117
|
+
#
|
118
|
+
def self.get_request_filters
|
119
|
+
self.get_request_data[:filters]
|
120
|
+
end
|
121
|
+
|
122
|
+
#
|
123
|
+
# @return [ActiveRecord::Base] the model used for filtering in the current request
|
124
|
+
#
|
125
|
+
def self.get_request_model
|
126
|
+
model = self.get_request_data[:model]
|
127
|
+
model.camelize.constantize
|
128
|
+
end
|
129
|
+
|
130
|
+
#
|
131
|
+
# Clears all active filters from the thread space.
|
132
|
+
#
|
133
|
+
def self.clear_request_filters!
|
134
|
+
Thread.current[:scope_filters] = nil
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|