acts_as_data_table 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|