effective_datatables 2.12.2 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +632 -512
- data/app/assets/javascripts/dataTables/buttons/buttons.html5.js +176 -177
- data/app/assets/javascripts/dataTables/buttons/buttons.print.js +2 -0
- data/app/assets/javascripts/dataTables/buttons/dataTables.buttons.js +14 -14
- data/app/assets/javascripts/dataTables/dataTables.bootstrap.js +1 -1
- data/app/assets/javascripts/dataTables/jquery.dataTables.js +246 -217
- data/app/assets/javascripts/effective_datatables.js +2 -3
- data/app/assets/javascripts/effective_datatables/events.js.coffee +7 -0
- data/app/assets/javascripts/effective_datatables/filters.js.coffee +6 -0
- data/app/assets/javascripts/effective_datatables/initialize.js.coffee +42 -39
- data/app/assets/javascripts/effective_datatables/reset.js.coffee +7 -0
- data/app/assets/javascripts/vendor/jquery.delayedChange.js +1 -1
- data/app/assets/stylesheets/dataTables/dataTables.bootstrap.css +0 -1
- data/app/assets/stylesheets/effective_datatables.scss +1 -2
- data/app/assets/stylesheets/effective_datatables/{_scopes.scss → _filters.scss} +1 -1
- data/app/assets/stylesheets/effective_datatables/_overrides.scss +1 -1
- data/app/controllers/effective/datatables_controller.rb +2 -4
- data/app/helpers/effective_datatables_helper.rb +56 -91
- data/app/helpers/effective_datatables_private_helper.rb +55 -64
- data/app/models/effective/datatable.rb +103 -177
- data/app/models/effective/datatable_column.rb +28 -0
- data/app/models/effective/datatable_column_tool.rb +110 -0
- data/app/models/effective/datatable_dsl_tool.rb +28 -0
- data/app/models/effective/datatable_value_tool.rb +142 -0
- data/app/models/effective/effective_datatable/attributes.rb +25 -0
- data/app/models/effective/effective_datatable/collection.rb +38 -0
- data/app/models/effective/effective_datatable/compute.rb +154 -0
- data/app/models/effective/effective_datatable/cookie.rb +29 -0
- data/app/models/effective/effective_datatable/dsl.rb +14 -8
- data/app/models/effective/effective_datatable/dsl/bulk_actions.rb +5 -6
- data/app/models/effective/effective_datatable/dsl/charts.rb +7 -9
- data/app/models/effective/effective_datatable/dsl/datatable.rb +107 -57
- data/app/models/effective/effective_datatable/dsl/filters.rb +50 -0
- data/app/models/effective/effective_datatable/format.rb +157 -0
- data/app/models/effective/effective_datatable/hooks.rb +0 -18
- data/app/models/effective/effective_datatable/params.rb +34 -0
- data/app/models/effective/effective_datatable/resource.rb +108 -0
- data/app/models/effective/effective_datatable/state.rb +178 -0
- data/app/views/effective/datatables/_actions_column.html.haml +9 -42
- data/app/views/effective/datatables/_bulk_actions_column.html.haml +1 -1
- data/app/views/effective/datatables/_bulk_actions_dropdown.html.haml +2 -3
- data/app/views/effective/datatables/_chart.html.haml +1 -1
- data/app/views/effective/datatables/_datatable.html.haml +7 -25
- data/app/views/effective/datatables/_filters.html.haml +21 -0
- data/app/views/effective/datatables/_reset.html.haml +2 -0
- data/app/views/effective/datatables/_resource_column.html.haml +8 -0
- data/app/views/effective/datatables/index.html.haml +0 -1
- data/config/effective_datatables.rb +9 -32
- data/lib/effective_datatables.rb +2 -6
- data/lib/effective_datatables/engine.rb +1 -1
- data/lib/effective_datatables/version.rb +1 -1
- data/lib/generators/effective_datatables/install_generator.rb +2 -2
- metadata +39 -19
- data/app/assets/javascripts/dataTables/colreorder/dataTables.colReorder.js +0 -27
- data/app/assets/javascripts/dataTables/jszip/jszip.js +0 -9155
- data/app/assets/javascripts/effective_datatables/scopes.js.coffee +0 -9
- data/app/models/effective/active_record_datatable_tool.rb +0 -242
- data/app/models/effective/array_datatable_tool.rb +0 -97
- data/app/models/effective/effective_datatable/ajax.rb +0 -101
- data/app/models/effective/effective_datatable/charts.rb +0 -20
- data/app/models/effective/effective_datatable/dsl/scopes.rb +0 -23
- data/app/models/effective/effective_datatable/helpers.rb +0 -24
- data/app/models/effective/effective_datatable/options.rb +0 -309
- data/app/models/effective/effective_datatable/rendering.rb +0 -365
- data/app/views/effective/datatables/_scopes.html.haml +0 -21
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d48516e2bb16d37adae464ea8c9ed86c11310787
|
4
|
+
data.tar.gz: b34bd2c14d20f351a99142c57e8897b16163ca1c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a40f0467539c7a0b08784bc00e54bd4bee2d57e8a86fd0e527b623eaac322a5355670b37004199ac26ac67cb2daa6fa75c30f649f431712bb7a5530221c0aae5
|
7
|
+
data.tar.gz: ff9153ff5e7e69400b1a82f91209c10e672adbe952ac998df2a9c663e73da73f763aa651a09e423307e6e2a4db80f99e83785522ad0f6b7d8272c8319a486169
|
data/README.md
CHANGED
@@ -1,16 +1,44 @@
|
|
1
1
|
# Effective DataTables
|
2
2
|
|
3
|
-
|
3
|
+
Use a high level DSL and just one ruby file to create a [Datatables jQuery table](http://datatables.net/) for any ActiveRecord class or Array.
|
4
4
|
|
5
|
-
|
5
|
+
Powerful server-side searching, sorting and filtering of ActiveRecord classes, with `belongs_to` and `has_many` relationships.
|
6
6
|
|
7
|
-
|
7
|
+
Does the right thing with searching sql columns as well as computed values from both ActiveRecord and Array collections.
|
8
8
|
|
9
|
-
|
9
|
+
Displays links to associated edit/show/destroy actions based on `current_user` authorized actions.
|
10
|
+
|
11
|
+
Other features include aggregate (total/average) footer rows, bulk actions, show/hide columns, responsive collapsing columns and Google charts.
|
12
|
+
|
13
|
+
This gem includes the jQuery DataTables assets.
|
14
|
+
|
15
|
+
For use with any Rails 3, 4, 5 application already using Twitter Bootstrap 3.
|
10
16
|
|
11
17
|
Works with postgres, mysql, sqlite3 and arrays.
|
12
18
|
|
13
|
-
##
|
19
|
+
## effective_datatables 3.0
|
20
|
+
|
21
|
+
This is the 3.0 release of effective_datatables. It's a complete rewrite, with a similar but totally changed DSL.
|
22
|
+
|
23
|
+
[Effective Datatables 2.0 README](https://github.com/code-and-effect/effective_datatables/tree/2.12.2)
|
24
|
+
|
25
|
+
Previous versions of the gem were excellent, but the 3.0 release has stepped things up.
|
26
|
+
|
27
|
+
Internally, all columns now have separate compute and format methods, removing the need for a ton of internal parsing and type conversions.
|
28
|
+
This allows things like filters, aggregates and searching/sorting to work effectively.
|
29
|
+
|
30
|
+
Column rendering has been improved so all datatable and view methods are callable from anywhere in the DSL.
|
31
|
+
This allows the developer to do things like: include/exclude/configure columns based on the current_user, apply logic around current filters
|
32
|
+
to change columns dynamically, to use regular ifs instead of procs in toggling visibility, and generally removes all weirdness.
|
33
|
+
|
34
|
+
This release adds a dependency on [effective_resources](https://github.com/code-and-effect/effective_resources) for ActiveRecord resource discovery,
|
35
|
+
full sql table fuzzy searching/sorting, attribute parsing, and checking availability & authorization of edit/show actions.
|
36
|
+
|
37
|
+
A cookie has been added to persist the user's selected filters, search, sort, length, column visibility and pagination settings.
|
38
|
+
|
39
|
+
A lot has changed. See below for full details.
|
40
|
+
|
41
|
+
# Getting Started
|
14
42
|
|
15
43
|
```ruby
|
16
44
|
gem 'effective_datatables'
|
@@ -42,49 +70,46 @@ Require the stylesheet on the asset pipeline by adding the following to your app
|
|
42
70
|
*= require effective_datatables
|
43
71
|
```
|
44
72
|
|
45
|
-
|
73
|
+
# Quick Start
|
46
74
|
|
47
|
-
|
75
|
+
All logic for the table exists in its own model file. Once that's built, we initialize in the controller, render in the view.
|
48
76
|
|
49
|
-
|
77
|
+
## The Model
|
50
78
|
|
51
79
|
Start by creating a new datatable.
|
52
80
|
|
53
|
-
Below is a very simple example file, which we will expand upon later.
|
54
|
-
|
55
81
|
This model exists at `/app/datatables/posts_datatable.rb`:
|
56
82
|
|
57
83
|
```ruby
|
58
84
|
class PostsDatatable < Effective::Datatable
|
59
85
|
datatable do
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
86
|
+
col :created_at
|
87
|
+
col :title
|
88
|
+
col :user # Post belongs_to :user
|
89
|
+
col :comments # Post has_many :comments
|
90
|
+
|
91
|
+
actions_col
|
66
92
|
end
|
67
93
|
|
68
|
-
|
94
|
+
collection do
|
69
95
|
Post.all
|
70
96
|
end
|
71
|
-
|
72
97
|
end
|
73
98
|
```
|
74
99
|
|
75
|
-
|
100
|
+
## The Controller
|
76
101
|
|
77
|
-
We're going to display this DataTable on the posts#index action
|
102
|
+
We're going to display this DataTable on the posts#index action.
|
78
103
|
|
79
104
|
```ruby
|
80
105
|
class PostsController < ApplicationController
|
81
106
|
def index
|
82
|
-
@datatable = PostsDatatable.new
|
107
|
+
@datatable = PostsDatatable.new(self)
|
83
108
|
end
|
84
109
|
end
|
85
110
|
```
|
86
111
|
|
87
|
-
|
112
|
+
## The View
|
88
113
|
|
89
114
|
Here we just render the datatable:
|
90
115
|
|
@@ -93,778 +118,898 @@ Here we just render the datatable:
|
|
93
118
|
<%= render_datatable(@datatable) %>
|
94
119
|
```
|
95
120
|
|
96
|
-
|
121
|
+
# Usage
|
97
122
|
|
98
|
-
|
123
|
+
Once your controller and view are set up to render a datatable, the model is the central point to configure all behaviour.
|
99
124
|
|
100
|
-
|
125
|
+
Here is an advanced example:
|
101
126
|
|
102
|
-
|
127
|
+
## The Model
|
103
128
|
|
104
|
-
|
129
|
+
This model exists at `/app/datatables/posts_datatable.rb`:
|
105
130
|
|
131
|
+
```ruby
|
132
|
+
class PostsDatatable < Effective::Datatable
|
106
133
|
|
107
|
-
|
134
|
+
# The collection block is the only required section in a datatable
|
135
|
+
# It has access to the attributes and filters Hashes, representing the current state
|
136
|
+
# It must return an ActiveRecord::Relation or an Array of Arrays
|
137
|
+
collection do
|
138
|
+
scope = Post.all.where(created_at: filters[:start_date]...filters[:end_date])
|
139
|
+
scope = scope.where(user_id: attributes[:user_id]) if attributes[:user_id]
|
140
|
+
scope
|
141
|
+
end
|
108
142
|
|
109
|
-
|
143
|
+
# Everything in the filters block ends up in a single form
|
144
|
+
# The form is submitted by datatables javascript as an AJAX post
|
145
|
+
filters do
|
146
|
+
# Scopes are rendered as a single radio button form field (works well with effective_form_inputs gem)
|
147
|
+
# The scopes only work when your collection is an ActiveRecord class, and they must exist on the model
|
148
|
+
# The current scope is automatically applied by effective_datatables to your collection
|
149
|
+
# You don't have to consider the current scope when writing your collection block
|
150
|
+
scope :all, default: true
|
151
|
+
scope :approved
|
152
|
+
scope :draft
|
153
|
+
scope :for_user, (attributes[:user_id] ? User.find(attributes[:user_id]) : current_user)
|
154
|
+
|
155
|
+
# Each filter has a name and a default value and the default can be nil
|
156
|
+
# Each filter is displayed on the front end form as a single field
|
157
|
+
# The filters are NOT automatically applied to your collection
|
158
|
+
# You are responsible for considering filters in your collection block
|
159
|
+
filter :start_date, Time.zone.now-3.months, required: true
|
160
|
+
filter :end_date, Time.zone.now.end_of_day
|
161
|
+
end
|
110
162
|
|
111
|
-
|
163
|
+
# These are displayed as a dropdown menu next to the datatables built-in buttons.
|
164
|
+
bulk_actions do
|
165
|
+
# bulk_action is just passthrough to link_to(), but the action of POST is forced
|
166
|
+
# POSTs to the given url with params[:ids], an Array of ids for all selected rows
|
167
|
+
# These actions are assumed to change the underlying collection
|
168
|
+
bulk_action 'Approve all', bulk_approve_posts_path, data: { confirm: 'Approve all selected posts?' }
|
169
|
+
bulk_action_divider
|
170
|
+
bulk_action 'Destroy all', bulk_destroy_posts_path, data: { confirm: 'Destroy all selected posts?' }
|
171
|
+
end
|
112
172
|
|
113
|
-
|
173
|
+
# Google Charts
|
174
|
+
# https://developers.google.com/chart/interactive/docs/quick_start
|
175
|
+
# effective_datatables does all the javascript boilerplate. Just return an Array of Arrays.
|
176
|
+
# Charts are updated whenever the current filters and search change
|
177
|
+
charts do
|
178
|
+
chart :posts_per_day, 'LineChart', label: 'Posts per Day', legend: false do |collection|
|
179
|
+
collection.group_by { |post| post.created_at.beginning_of_day }.map do |date, posts|
|
180
|
+
[date.strftime('%F'), posts.length]
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
114
184
|
|
115
|
-
|
116
|
-
|
185
|
+
# Datatables
|
186
|
+
# https://datatables.net/
|
187
|
+
# Each column header has a form field controlled by the search: { as: :string } option
|
188
|
+
# The user's selected filters, search, sort, length, column visibility and pagination settings are saved between visits
|
189
|
+
# on a per-table basis and can be Reset with a button
|
117
190
|
datatable do
|
118
|
-
|
119
|
-
|
191
|
+
length 25 # 5, 10, 25, 50, 100, 1000, :all
|
192
|
+
order :updated_at, :desc
|
120
193
|
|
121
|
-
|
194
|
+
# Renders a column of checkboxes to select items for any bulk_actions
|
195
|
+
bulk_actions_col
|
122
196
|
|
123
|
-
|
197
|
+
col :id, visible: false
|
198
|
+
col :updated_at, visible: false
|
124
199
|
|
125
|
-
|
200
|
+
col :created_at, label: 'Created' do |post|
|
201
|
+
time_ago_in_words(post.created_at)
|
202
|
+
end
|
126
203
|
|
127
|
-
|
204
|
+
# This is a belongs_to column
|
205
|
+
# effective_datatables will try to put in an edit or show link, depending on the current_user's authorization
|
206
|
+
# It will also initialize the search field with PostCategory.all
|
207
|
+
col :post_category, action: :edit
|
128
208
|
|
129
|
-
|
130
|
-
|
209
|
+
if attributes[:user_id].nil? # Show all users, otherwise this table is meant for one user only
|
210
|
+
col :user, search: { collection: User.authors }
|
131
211
|
end
|
132
212
|
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
213
|
+
if can?(:index, Comment)
|
214
|
+
col :comments
|
215
|
+
end
|
216
|
+
|
217
|
+
col :category, search: { collection: Post::CATEGORY } do |survey|
|
218
|
+
Post::CATEGORY.invert[post.category]
|
219
|
+
end
|
220
|
+
|
221
|
+
# This is a computed method, not an attribute on the post database table.
|
222
|
+
# The first block takes the object from the collection do ... end block and does some work on it.
|
223
|
+
# It computes some value. A val.
|
224
|
+
# The first block returns a Float/Integer. All sorting/ordering is then performed on this number.
|
225
|
+
# The second block formats the number and returns a String
|
226
|
+
val :approval_rating do |post|
|
227
|
+
post.approvals.sum { |a| a.rating }
|
228
|
+
end.format do |rating|
|
229
|
+
number_to_percentage(rating, precision: 2)
|
230
|
+
end
|
231
|
+
|
232
|
+
# In a col there is only one block, the format block.
|
233
|
+
# A col takes the value as per the collection do ... end block and just formats it
|
234
|
+
# All sorting/ordering is performed as per the original value.
|
235
|
+
col :approved do |post|
|
236
|
+
if post.approved?
|
237
|
+
content_tag(:span, 'Approved', 'badge badge-approved')
|
238
|
+
else
|
239
|
+
content_tag(:span, 'Draft', 'badge badge-draft')
|
138
240
|
end
|
139
241
|
end
|
140
242
|
|
141
|
-
|
142
|
-
|
143
|
-
|
243
|
+
# Will add a Total row to the table's tfoot
|
244
|
+
# :average is also supported, or you can do a custom block
|
245
|
+
aggregate :total
|
144
246
|
|
145
|
-
|
146
|
-
|
247
|
+
# Uses effective_resources gem to discover the resource path and authorization actions
|
248
|
+
# Puts in icons to show/edit/destroy actions, if authorized to those actions.
|
249
|
+
# Use the actions_col block to add additional actions
|
250
|
+
actions_col show: false do |post|
|
251
|
+
if !post.approved? && can?(:approve, Post)
|
252
|
+
link_to 'Approve', approve_post_path(post) data: { method: :post, confirm: 'Really approve?'}
|
253
|
+
end
|
254
|
+
end
|
147
255
|
end
|
148
256
|
|
149
257
|
end
|
150
258
|
```
|
151
259
|
|
152
|
-
|
260
|
+
## The Controller
|
153
261
|
|
154
|
-
|
262
|
+
Any options used to initialize a datatable become the `attributes`. Use these to configure datatables behavior.
|
155
263
|
|
156
|
-
|
264
|
+
In the above example, when `attributes[:user_id]` is present, the table displays information for just that user.
|
157
265
|
|
158
266
|
```ruby
|
159
|
-
|
160
|
-
|
267
|
+
class PostsController < ApplicationController
|
268
|
+
def index
|
269
|
+
@datatable = PostsDatatable.new(self, user_id: current_user.id)
|
270
|
+
end
|
161
271
|
end
|
162
272
|
```
|
163
273
|
|
164
|
-
|
165
|
-
|
166
|
-
```ruby
|
167
|
-
def collection
|
168
|
-
collection = Effective::Order.unscoped.purchased
|
169
|
-
.joins(:user)
|
170
|
-
.joins(:order_items)
|
171
|
-
.group('users.email')
|
172
|
-
.group('orders.id')
|
173
|
-
.select('users.email AS email')
|
174
|
-
.select('orders.*')
|
175
|
-
.select("#{query_total} AS total")
|
176
|
-
.select("string_agg(order_items.title, '!!OI!!') AS order_items")
|
274
|
+
## The View
|
177
275
|
|
178
|
-
|
179
|
-
collection.where(:user_id => attributes[:user_id])
|
180
|
-
else
|
181
|
-
collection
|
182
|
-
end
|
183
|
-
end
|
276
|
+
Render the datatable with its filters and charts, all together:
|
184
277
|
|
185
|
-
|
186
|
-
|
187
|
-
|
278
|
+
```
|
279
|
+
<h1>All Posts</h1>
|
280
|
+
<%= render_datatable(@datatable) %>
|
188
281
|
```
|
189
282
|
|
190
|
-
|
283
|
+
or, the datatable, filter and charts may be rendered individually:
|
191
284
|
|
192
|
-
|
285
|
+
```
|
286
|
+
<h1>All Posts</h1>
|
287
|
+
<p>
|
288
|
+
<%= render_datatable_filters(@datatable) %>
|
289
|
+
</p>
|
193
290
|
|
194
|
-
|
291
|
+
<p>
|
292
|
+
<%= render_datatable_charts(@datatable) %>
|
293
|
+
</p>
|
195
294
|
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
array_column :first_name
|
201
|
-
array_column :last_name
|
202
|
-
array_column :email
|
203
|
-
end
|
295
|
+
<p>
|
296
|
+
<%= render_datatable(@datatable, charts: false, filters: false) %>
|
297
|
+
</p>
|
298
|
+
```
|
204
299
|
|
205
|
-
|
206
|
-
[
|
207
|
-
[1, 'June', 'Huang', 'june@einstein.com'],
|
208
|
-
[2, 'Leo', 'Stubbs', 'leo@einstein.com'],
|
209
|
-
[3, 'Quincy', 'Pompey', 'quincy@einstein.com'],
|
210
|
-
[4, 'Annie', 'Wojcik', 'annie@einstein.com'],
|
211
|
-
]
|
212
|
-
end
|
300
|
+
or, to render a simple table, (without filters, charts, pagination, sorting, searching, export buttons, per page, or default visibility):
|
213
301
|
|
214
|
-
|
302
|
+
```
|
303
|
+
<%= render_datatable(@datatable, simple: true) %>
|
215
304
|
```
|
216
305
|
|
217
|
-
|
306
|
+
# DSL
|
218
307
|
|
219
|
-
|
308
|
+
The effective_datatables DSL is made up of 5 sections: `collection`, `datatable`, `filters` `bulk_actions`, `charts`
|
220
309
|
|
221
|
-
|
310
|
+
As well, a datatable can be initialized with `attributes`.
|
222
311
|
|
223
|
-
|
312
|
+
## attributes
|
224
313
|
|
225
|
-
|
314
|
+
When initialized with a Hash, that hash is available throughout the entire datatable as `attributes`.
|
226
315
|
|
227
|
-
|
316
|
+
These attributes are serialized and stored in an encrypted cookie. Objects won't work. Keep it simple.
|
317
|
+
|
318
|
+
Attributes cannot be changed by search, filter, or state in any way. They're guaranteed to be the same as when first initialized.
|
228
319
|
|
229
320
|
```ruby
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
321
|
+
class PostsController < ApplicationController
|
322
|
+
def index
|
323
|
+
@datatable = PostsDatatable.new(self, user_id: current_user.id, admin: true)
|
324
|
+
end
|
325
|
+
end
|
326
|
+
```
|
234
327
|
|
235
|
-
|
236
|
-
# This column is detected as a String, therefore it is :type => :string
|
237
|
-
# Any SQL used to search this field will take the form of "customers.stripe_customer_id ILIKE %?%"
|
238
|
-
table_column :stripe_customer_id, :column => 'customers.stripe_customer_id'
|
328
|
+
Use attributes to restrict the collection scope, exclude columns or otherwise tweak the table.
|
239
329
|
|
240
|
-
|
241
|
-
# This column is detected as a DateTime, therefore it is :type => :datetime
|
242
|
-
# Any SQL used to search this field will take the form of
|
243
|
-
# "to_char(#{column} AT TIME ZONE 'GMT', 'YYYY-MM-DD HH24:MI') ILIKE '%?%'"
|
244
|
-
table_column :created_at
|
330
|
+
An example of using `attributes[:user_id]` to make a user specific posts table is above.
|
245
331
|
|
246
|
-
|
247
|
-
# This column will be detected as a belongs_to and some predefined filters will be set up
|
248
|
-
# So declaring the following
|
249
|
-
table_column :user
|
332
|
+
Here we do something similar with `attributes[:admin]`:
|
250
333
|
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
334
|
+
```ruby
|
335
|
+
class PostsDatatable < Effective::Datatable
|
336
|
+
collection do
|
337
|
+
attributes[:admin] ? Post.all : Post.where(draft: false)
|
338
|
+
end
|
339
|
+
|
340
|
+
datatable do
|
341
|
+
col :title
|
342
|
+
|
343
|
+
if attributes[:admin]
|
344
|
+
col :user
|
256
345
|
end
|
346
|
+
|
347
|
+
col :post_category
|
348
|
+
col :comments
|
257
349
|
end
|
258
350
|
end
|
259
351
|
```
|
260
352
|
|
261
|
-
|
353
|
+
## collection
|
262
354
|
|
263
|
-
|
355
|
+
The `collection do ... end` block must return an ActiveRecord relation or an Array of Arrays.
|
264
356
|
|
265
|
-
|
357
|
+
```ruby
|
358
|
+
collection do
|
359
|
+
Post.all
|
360
|
+
end
|
361
|
+
```
|
266
362
|
|
267
|
-
|
363
|
+
or
|
268
364
|
|
269
|
-
|
365
|
+
```ruby
|
366
|
+
collection do
|
367
|
+
scope = Post.includes(:user).where(created_at: filters[:start_date]...filters[:end_date])
|
368
|
+
scope = scope.where(user_id: attributes[:user_id]) if attributes[:user_id]
|
369
|
+
scope
|
370
|
+
end
|
371
|
+
```
|
270
372
|
|
271
|
-
|
373
|
+
or
|
272
374
|
|
273
|
-
|
375
|
+
```ruby
|
376
|
+
collection do
|
377
|
+
[
|
378
|
+
['June', 'Huang', 'june@einstein.com'],
|
379
|
+
['Leo', 'Stubbs', 'leo@einstein.com'],
|
380
|
+
['Quincy', 'Pompey', 'quincy@einstein.com'],
|
381
|
+
['Annie', 'Wojcik', 'annie@einstein.com'],
|
382
|
+
]
|
383
|
+
end
|
384
|
+
```
|
274
385
|
|
275
|
-
|
386
|
+
or
|
276
387
|
|
277
|
-
|
388
|
+
```ruby
|
389
|
+
collection do
|
390
|
+
time_entries = TimeEntry.where(date: filter[:start_date].beginning_of_year...filter[:end_date].end_of_year)
|
391
|
+
.group_by { |time_entry| "#{time_entry.client_id}_#{time_entry.created_at.strftime('%b').downcase}" }
|
278
392
|
|
279
|
-
|
393
|
+
Client.all.map do |client|
|
394
|
+
[client] + [:jan, :feb, :mar, :apr, :may, :jun, :jul, :aug, :sep, :oct, :nov, :dec].map do |month|
|
395
|
+
entries = time_entries["#{client.id}_#{month}"] || []
|
280
396
|
|
281
|
-
|
397
|
+
calc = TimeEntryCalculator.new(entries)
|
282
398
|
|
283
|
-
|
284
|
-
|
285
|
-
|
399
|
+
[calc.duration, calc.bill_duration, calc.overtime, calc.revenue, calc.cost, calc.net]
|
400
|
+
end
|
401
|
+
end
|
286
402
|
end
|
287
403
|
```
|
288
404
|
|
289
|
-
The
|
405
|
+
The collection block is responsible for applying any `attribute` and `filters` logic.
|
290
406
|
|
407
|
+
When an ActiveRecord collection, the `current_scope`, will be applied automatically by effective_datatables.
|
291
408
|
|
292
|
-
|
409
|
+
All searching and ordering is also done by effective_datatables.
|
293
410
|
|
294
|
-
|
411
|
+
Your collection method should not contain a `.order()`, or implement search in any way.
|
295
412
|
|
296
|
-
|
297
|
-
:column => 'users.id' # Set this if you're doing something tricky with the database. Used internally for .order() and .where() clauses
|
298
|
-
:type => :string # Derived from the ActiveRecord attribute default datatype. Controls searching behaviour. Valid options include :string, :text, :datetime, :date, :integer, :boolean, :year
|
299
|
-
```
|
413
|
+
Sometimes it's handy to call `.reorder(nil)` on a scope.
|
300
414
|
|
301
|
-
|
415
|
+
## datatable
|
302
416
|
|
303
|
-
The
|
417
|
+
The `datatable do ... end` block configures a table of data.
|
304
418
|
|
305
|
-
|
306
|
-
:label => 'Nice Label' # Override the default column header label
|
307
|
-
:sortable => true|false # Allow sorting of this column. Otherwise the up/down arrows on the frontend will be disabled.
|
308
|
-
:visible => true|false # Hide this column at startup. Column visbility can be changed on the frontend. By default, hidden column filter terms are ignored.
|
309
|
-
:width => '100%'|'100px' # Set the width of this column. Can be set on one, all or some of the columns. If using percentages, should never add upto more than 100%
|
310
|
-
:class => 'col-example' # Adds an html class to the column's TH and all TD elements. Add more than one class with 'example col-example something'
|
311
|
-
:responsivePriority => 0 # Set which columns collapse when the table is shrunk down. 10000 is the default value.
|
312
|
-
```
|
419
|
+
Initialize the datatable in your controller or view, `@datatable = PostsDatatable.new(self)`, and render it in your view `<%= render_datatable(@datatable) %>`
|
313
420
|
|
314
|
-
###
|
421
|
+
### col
|
315
422
|
|
316
|
-
|
423
|
+
This is the main DSL method that you will interact with.
|
317
424
|
|
318
|
-
|
425
|
+
`col` defines a 1:1 mapping between the underlying SQL database table column or Array index to a frontend jQuery Datatables table column. It creates a column.
|
319
426
|
|
320
|
-
|
321
|
-
table_column :created_at, :filter => false # Disable filtering on this column entirely
|
322
|
-
table_column :created_at, :filter => {...} # Enable filtering with these options
|
427
|
+
Each column's search and sorting is performed on its underlying value, as per the collection.
|
323
428
|
|
324
|
-
|
325
|
-
:filter => {:as => :text}
|
429
|
+
It accepts one optional block used to format the value after any search or sorting is done.
|
326
430
|
|
327
|
-
|
328
|
-
:filter => {:as => :select, :collection => [*2010..(Time.zone.now.year+6)]}
|
329
|
-
:filter => {:as => :select, :collection => Proc.new { PostCategory.all } }
|
330
|
-
:filter => {:as => :select, :collection => Proc.new { User.all.order(:email).map { |obj| [obj.id, obj.email] } } }
|
431
|
+
The following options are available:
|
331
432
|
|
332
|
-
|
333
|
-
:
|
334
|
-
|
433
|
+
```ruby
|
434
|
+
action: :show|:edit|false # :resource and relation columns only. generate links to this action. edit -> show by default
|
435
|
+
as: :string|:integer|etc # Sets the type of column initializing defaults for search, sort and format
|
436
|
+
col_class: 'col-green' # Sets the html class to use on this column's td and th
|
437
|
+
label: 'My label' # The label for this column
|
438
|
+
partial: 'posts/category' # Render this column with a partial. The local will be named resource
|
439
|
+
partial_as: 'category' # The name of the object's local variable, otherwise resource
|
440
|
+
responsive: 10000 # Controls how columns collapse https://datatables.net/reference/option/columns.responsivePriority
|
335
441
|
|
336
|
-
|
442
|
+
# Configure the search behavior. Autodetects by default.
|
443
|
+
search: false
|
444
|
+
search: :string
|
445
|
+
search: { as: :string, fuzzy: true }
|
446
|
+
search: { as: :select, collection: User.all, multiple: true }
|
337
447
|
|
338
|
-
|
339
|
-
:
|
340
|
-
:
|
341
|
-
:filter => {include_blank: false}
|
342
|
-
:filter => {placeholder: false}
|
448
|
+
sort: true|false # Should this column be orderable. true by default
|
449
|
+
sql_column: 'posts.rating' # The sql column to search/sort on. Only needed when doing custom selects or tricky joins.
|
450
|
+
visible: true|false # Show/Hide this column by default
|
343
451
|
```
|
344
452
|
|
345
|
-
|
453
|
+
The `:as` setting determines a column's search, sort and format behaviour.
|
454
|
+
|
455
|
+
It is auto-detected from an ActiveRecord collection's SQL datatype, and set to `:string` for any Array-based collections.
|
456
|
+
|
457
|
+
Valid options for `:as` are as follows:
|
346
458
|
|
347
|
-
|
459
|
+
`:boolean`, `:currency`, `:datetime`, `:date`, `:decimal`, `:duration`, `:email`, `:float`, `:integer`, `:percentage`, `:price`, `:resource`, `:string`, `:text`
|
348
460
|
|
349
|
-
|
461
|
+
These settings are loosely based on the regular datatypes, with some custom effective types thrown in:
|
350
462
|
|
351
|
-
|
463
|
+
- `:currency` expects the underlying datatype to be a Float.
|
464
|
+
- `:duration` expects the underlying datatype to be an Integer representing the number of minutes. 120 == 2 hours
|
465
|
+
- `:email` expects the underlying datatype to be a String
|
466
|
+
- `:percentage` expects the underlying datatype to be an Integer or a Float. 75 == 0.75 == 75%
|
467
|
+
- `:price` expects the underlying datatype to be an Integer representing the number of cents. 5000 == $50.00
|
468
|
+
- `:resource` can be used for an Array based collection which includes an ActiveRecord object
|
352
469
|
|
353
|
-
|
470
|
+
The column will be formatted as per its `as:` setting, unless a custom format block is present:
|
354
471
|
|
355
472
|
```ruby
|
356
|
-
|
357
|
-
if post.
|
358
|
-
|
473
|
+
col :approved do |post|
|
474
|
+
if post.approved?
|
475
|
+
content_tag(:span, 'Approved', 'badge badge-approved')
|
359
476
|
else
|
360
|
-
|
477
|
+
content_tag(:span, 'Draft', 'badge badge-draft')
|
361
478
|
end
|
362
479
|
end
|
363
480
|
```
|
364
481
|
|
365
|
-
|
482
|
+
You can also set custom search and sort on a per-column basis. See Advanced Search and Sort below.
|
366
483
|
|
367
|
-
|
368
|
-
table_column :created_at, :proc => Proc.new { |post| link_to(post.created_at, post_path(post)) }
|
369
|
-
```
|
484
|
+
### val
|
370
485
|
|
371
|
-
|
486
|
+
Shorthand for value, this command also creates a column on the datatable.
|
487
|
+
|
488
|
+
It accepts all the same options as `col` with the additional requirement of a "compute" block.
|
372
489
|
|
373
490
|
```ruby
|
374
|
-
|
491
|
+
val :approval_rating do |post|
|
492
|
+
post.approvals.sum { |a| a.rating }
|
493
|
+
end.format do |rating|
|
494
|
+
number_to_percentage(rating, precision: 2)
|
495
|
+
end
|
375
496
|
```
|
376
497
|
|
377
|
-
|
498
|
+
So, `val` yields the object from the collection to the first/compute block, and stores the result.
|
378
499
|
|
379
|
-
|
380
|
-
<p><%= link_to('View', post_path(post)) %></p>
|
381
|
-
<p><%= link_to('Edit', edit_post_path(post)) %></p>
|
382
|
-
```
|
500
|
+
All searching and sorting for this column will be performed on this computed value.
|
383
501
|
|
384
|
-
|
502
|
+
This is implemented as a full Array search/sort and is much slower for large datasets than a paginated SQL query
|
503
|
+
|
504
|
+
The `.format do ... end` block can then be used to apply custom formatting.
|
505
|
+
|
506
|
+
### bulk_actions_col
|
507
|
+
|
508
|
+
Creates a column of checkboxes for use with the `bulk_actions` section.
|
509
|
+
|
510
|
+
Each input checkbox has a value equal to its row `object.to_param` and gets submitted as an Array of ids, `params[:ids]`
|
511
|
+
|
512
|
+
Use these checkboxes to select all / none / one or more rows for the `bulk_actions do ... end` section (below).
|
513
|
+
|
514
|
+
You can only have one `bulk_actions_col` per datatable.
|
515
|
+
|
516
|
+
### actions_col
|
517
|
+
|
518
|
+
When working with an ActiveRecord based collection, this column will consider the `current_user`'s authorization, and generate
|
519
|
+
glyphicon links to edit, show and destroy actions for any collection class.
|
520
|
+
|
521
|
+
The authorization method is configured via the `config/initializers/effective_datatables.rb` initializer file.
|
522
|
+
|
523
|
+
There are just a few options:
|
385
524
|
|
386
525
|
```ruby
|
387
|
-
|
526
|
+
show: true|false|:authorize
|
527
|
+
edit: true|false|:authorize
|
528
|
+
destroy: true|false|:authorize
|
529
|
+
|
530
|
+
visible: true|false
|
388
531
|
```
|
389
532
|
|
390
|
-
|
533
|
+
When the show, edit and destroy actions are `true` (default), the permission check will be made just once, authorizing the class.
|
534
|
+
When set to `:authorize`, permission to each individual object will be checked.
|
535
|
+
|
536
|
+
Use the block syntax to add additional actions
|
391
537
|
|
392
538
|
```ruby
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
else
|
397
|
-
link_to posts_path(post)
|
398
|
-
end
|
539
|
+
actions_col show: false do |post|
|
540
|
+
(post.approved? ? link_to('Approve', approve_post_path(post)) : '') +
|
541
|
+
glyphicon_to('print', print_ticket_path(ticket), title: 'Print')
|
399
542
|
end
|
400
543
|
```
|
401
544
|
|
402
|
-
The
|
545
|
+
The `glyphicon_to` helper is part of the [effective_resources](https://github.com/code-and-effect/effective_resources) gem, which is a dependency of this gem.
|
403
546
|
|
404
|
-
|
405
|
-
request.referer.include?('/admin/')
|
406
|
-
```
|
547
|
+
### length
|
407
548
|
|
408
|
-
|
549
|
+
Sets the default number of rows per page. Valid lengths are `5`, `10`, `25`, `50`, `100`, `250`, `1000`, `:all`
|
409
550
|
|
410
|
-
|
551
|
+
When not specified, effective_datatables uses the default as per the `config/initializers/effective_datatables.rb` or 25.
|
411
552
|
|
412
553
|
```ruby
|
413
|
-
|
554
|
+
length 100
|
414
555
|
```
|
415
556
|
|
416
|
-
|
557
|
+
### order
|
558
|
+
|
559
|
+
Sets the default order of table rows. The first argument is the column, the second the direction.
|
560
|
+
|
561
|
+
The column must exist as a `col` or `val` and the direction is either `:asc` or `:desc`.
|
562
|
+
|
563
|
+
When not specified, effective_datatables will sort by the first defined column.
|
417
564
|
|
418
565
|
```ruby
|
419
|
-
|
420
|
-
name # The name of your column
|
421
|
-
column # the table_column options
|
422
|
-
filterable # whether the dataTable is filterable
|
566
|
+
order :created_at, :asc|:desc
|
423
567
|
```
|
424
568
|
|
425
|
-
|
569
|
+
### aggregate
|
570
|
+
|
571
|
+
The `aggregate` command inserts a row in the table's `tfoot`.
|
426
572
|
|
427
|
-
|
573
|
+
The only option available is `:label`.
|
428
574
|
|
429
|
-
|
575
|
+
You can only have one aggregate per datatable. (Unfortunately, this is a limit of the jQuery Datatables)
|
430
576
|
|
431
|
-
|
577
|
+
There is built in support for automatic `:total` and `:average` aggregates:
|
432
578
|
|
433
579
|
```ruby
|
434
|
-
|
580
|
+
aggregate :total|:average
|
435
581
|
```
|
436
582
|
|
437
|
-
or
|
583
|
+
or write your own:
|
438
584
|
|
439
585
|
```ruby
|
440
|
-
|
441
|
-
|
442
|
-
|
586
|
+
aggregate :average_as_percentage do |values, column|
|
587
|
+
if column[:name] == :first_name
|
588
|
+
'Average'
|
589
|
+
elsif values.present?
|
590
|
+
average = values.map { |value| value.presence || 0 }.sum / [values.length, 1].max
|
591
|
+
content_tag(:span, number_to_percentage(average, precision: 1))
|
443
592
|
end
|
444
593
|
end
|
445
594
|
```
|
446
595
|
|
447
|
-
|
596
|
+
In the above example, `values` is an Array containing all row's values for one column at a time.
|
448
597
|
|
449
|
-
|
598
|
+
## filters
|
450
599
|
|
451
|
-
|
600
|
+
Creates a single form with fields for each `filter` and a single radio input field for all `scopes`.
|
452
601
|
|
453
|
-
|
454
|
-
actions_column show: false, edit: true, destroy: true, unarchive: true
|
455
|
-
```
|
602
|
+
The form is submitted by an AJAX POST action, or, in some advanced circumstances (see Dynamic Columns below) as a regular POST or even GET.
|
456
603
|
|
457
|
-
|
604
|
+
Initialize the datatable in your controller or view, `@datatable = PostsDatatable.new(self)`, and render its filters anywhere with `<%= render_datatable_filters(@datatable) %>`.
|
458
605
|
|
459
|
-
|
460
|
-
actions_column show: :authorize
|
461
|
-
```
|
606
|
+
### scope
|
462
607
|
|
463
|
-
|
608
|
+
All defined scopes are rendered as a single radio button form field. Works great with the [effective_form_inputs](https://github.com/code-and-effect/effective_form_inputs) gem.
|
464
609
|
|
465
|
-
|
610
|
+
Only supported for ActiveRecord based collections. They must exist as regular scopes on the model.
|
466
611
|
|
467
|
-
|
612
|
+
The currently selected scope will be automatically applied. You shouldn't consider it in your collection block.
|
468
613
|
|
469
614
|
```ruby
|
470
|
-
|
615
|
+
filters do
|
616
|
+
scope :approved
|
617
|
+
scope :for_user, current_user
|
618
|
+
end
|
471
619
|
```
|
472
620
|
|
473
|
-
|
621
|
+
Must match the scopes in your `app/models/post.rb`:
|
474
622
|
|
475
623
|
```ruby
|
476
|
-
|
624
|
+
class Post < ApplicationRecord | ActiveRecord::Base
|
625
|
+
scope :approved, -> { where(draft: false) }
|
626
|
+
scope :for_user, Proc.new { |user| where(user: user) }
|
627
|
+
end
|
477
628
|
```
|
478
629
|
|
479
|
-
|
630
|
+
### filter
|
480
631
|
|
481
|
-
|
632
|
+
Each filter has a name and a default/fallback value. If the form is submitted blank, the default values are used.
|
482
633
|
|
483
|
-
|
634
|
+
effective_datatables looks at the default value, and tries to cast the incoming (String) value into that datatype.
|
484
635
|
|
485
|
-
|
636
|
+
This ensures that calling `filters[:name]` always return a value. The default can be nil.
|
486
637
|
|
487
|
-
|
638
|
+
You can override the parsing on a per-filter basis.
|
488
639
|
|
489
|
-
|
490
|
-
|
491
|
-
This feature has been built with an ActiveRecord collection in mind. To work with an Array backed collection try `resource_method: :first` or similar.
|
492
|
-
|
493
|
-
After the AJAX request is done, the datatable will be redrawn so any changes made to the collection will be displayed immediately.
|
494
|
-
|
495
|
-
You can define any number of `bulk_action`s, and separate them with one or more `bulk_action_divider`s.
|
496
|
-
|
497
|
-
The `bulk_action` method is just an alias for `link_to`, so all the same options will work.
|
640
|
+
Unlike `scope`s, the filters are NOT automatically applied to your collection. You are responsible for considering `filters` in your collection block.
|
498
641
|
|
499
642
|
```ruby
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
bulk_action 'Send emails', bulk_email_posts_path, data: {confirm: 'Really send emails?'}
|
505
|
-
end
|
506
|
-
|
507
|
-
...
|
508
|
-
|
643
|
+
filters do
|
644
|
+
filter :start_date, Time.zone.now-3.months, required: true
|
645
|
+
filter :end_date, nil, parse: -> { |term| Time.zone.local(term).end_of_day }
|
646
|
+
filter :user, current_user, as: :select, collection: User.all
|
509
647
|
end
|
510
648
|
```
|
511
649
|
|
512
|
-
|
650
|
+
and apply these to your `collection do ... end` block by calling `filters[:start_date]`:
|
513
651
|
|
514
652
|
```ruby
|
515
|
-
|
516
|
-
|
517
|
-
@posts = Post.where(id: params[:ids])
|
653
|
+
collection do
|
654
|
+
scope = Post.includes(:post_category, :user).where('created_at > ?', filters[:start_date])
|
518
655
|
|
519
|
-
|
520
|
-
|
521
|
-
@posts.each { |post| post.approve! }
|
522
|
-
render json: { status: 200, message: "Successfully approved #{@posts.length} posts." }
|
523
|
-
rescue => e
|
524
|
-
render json: { status: 500, message: 'An error occured while approving a post.' }
|
525
|
-
end
|
656
|
+
if filters[:end_date].present?
|
657
|
+
scope = scope.where('created_at < ?', filters[:end_date])
|
526
658
|
end
|
659
|
+
|
660
|
+
scope
|
527
661
|
end
|
528
662
|
```
|
529
663
|
|
530
|
-
|
664
|
+
The filter command has the following options:
|
531
665
|
|
532
666
|
```ruby
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
end
|
667
|
+
as: :select|:date|:boolean # Passed to SimpleForm
|
668
|
+
label: 'My label' # Label for this form field
|
669
|
+
parse: -> { |term| term.to_i } # Parse the incoming term (string) into whatever datatype
|
670
|
+
required: true|false # Passed to SimpleForm
|
538
671
|
```
|
539
672
|
|
540
|
-
|
673
|
+
Any other option given will be yielded to SimpleForm as `input_html` options.
|
541
674
|
|
542
|
-
|
675
|
+
## bulk_actions
|
543
676
|
|
544
|
-
|
677
|
+
Creates a single dropdown menu with a link to each action, download or content.
|
545
678
|
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
679
|
+
Along with this section, you must put a `bulk_actions_col` somewhere in your `datatable do ... end` section.
|
680
|
+
|
681
|
+
### bulk_action
|
682
|
+
|
683
|
+
Creates a link that becomes clickable when one or more checkbox/rows are selected as per the `bulk_actions_col` column.
|
551
684
|
|
552
|
-
|
685
|
+
A controller action must be created to accept a POST with an array of selected ids, `params[:ids]`.
|
553
686
|
|
554
|
-
and
|
687
|
+
This is a pass-through to `link_to` and accepts all the same options, except that the method `POST` is forced.
|
555
688
|
|
556
689
|
```ruby
|
557
|
-
|
558
|
-
|
690
|
+
bulk_actions do
|
691
|
+
bulk_action 'Approve all', bulk_approve_posts_path, data: { confirm: 'Approve all selected posts?' }
|
559
692
|
end
|
560
693
|
```
|
561
694
|
|
562
|
-
|
695
|
+
In your `routes` file:
|
563
696
|
|
564
697
|
```ruby
|
565
|
-
|
698
|
+
resources :posts do
|
699
|
+
collection do
|
700
|
+
post :bulk_approve
|
701
|
+
end
|
702
|
+
end
|
566
703
|
```
|
567
704
|
|
568
|
-
|
705
|
+
In your `PostsController`:
|
569
706
|
|
570
707
|
```ruby
|
571
|
-
|
708
|
+
def bulk_approve
|
709
|
+
@posts = Post.where(id: params[:ids])
|
710
|
+
|
711
|
+
# You should probably write this inside a transaction. This is just an example.
|
712
|
+
begin
|
713
|
+
@posts.each { |post| post.approve! }
|
714
|
+
render json: { status: 200, message: "Successfully approved #{@posts.length} posts." }
|
715
|
+
rescue => e
|
716
|
+
render json: { status: 500, message: 'An error occured while approving a post.' }
|
717
|
+
end
|
718
|
+
end
|
572
719
|
```
|
573
720
|
|
574
|
-
|
721
|
+
### bulk_action_divider
|
575
722
|
|
576
|
-
|
723
|
+
Inserts a menu divider `<li class='divider' role='separator'></li>`
|
577
724
|
|
578
|
-
|
725
|
+
### bulk_download
|
579
726
|
|
580
|
-
|
727
|
+
So it turns out there are some http issues with using an AJAX action to download a file.
|
581
728
|
|
582
|
-
|
729
|
+
A workaround for these issues is included via the [jQuery File Download Plugin](http://johnculviner.com/jquery-file-download-plugin-for-ajax-like-feature-rich-file-downloads/)
|
583
730
|
|
584
|
-
|
585
|
-
|
586
|
-
When a scope is passed like follows, without a default value, it is assumed to be a klass level scope:
|
731
|
+
The use case for this feature is to download a csv report generated for the selected rows.
|
587
732
|
|
588
733
|
```ruby
|
589
|
-
|
590
|
-
|
591
|
-
scope :standard, default: true
|
592
|
-
scope :extended
|
593
|
-
scope :archived
|
594
|
-
end
|
595
|
-
|
596
|
-
def collection
|
597
|
-
collection = Post.all
|
598
|
-
collection = collection.send(current_scope) if current_scope
|
599
|
-
collection
|
734
|
+
bulk_actions do
|
735
|
+
bulk_download 'Export Report', bulk_export_report_path
|
600
736
|
end
|
601
737
|
```
|
602
738
|
|
603
|
-
|
739
|
+
```ruby
|
740
|
+
def bulk_export_report
|
741
|
+
authorize! :export, Post
|
742
|
+
|
743
|
+
@posts = Post.where(id: params[:ids])
|
604
744
|
|
605
|
-
|
745
|
+
Post.transaction do
|
746
|
+
begin
|
747
|
+
cookies[:fileDownload] = true
|
606
748
|
|
607
|
-
|
749
|
+
send_data(PostsExporter.new(@posts).export,
|
750
|
+
type: 'text/csv; charset=utf-8; header=present',
|
751
|
+
filename: 'posts-export.csv'
|
752
|
+
)
|
608
753
|
|
609
|
-
|
754
|
+
@posts.update_all(exported_at: Time.zone.now)
|
755
|
+
return
|
756
|
+
rescue => e
|
757
|
+
cookies.delete(:fileDownload)
|
758
|
+
raise ActiveRecord::Rollback
|
759
|
+
end
|
760
|
+
end
|
610
761
|
|
611
|
-
|
762
|
+
render json: { error: 'An error occurred' }
|
763
|
+
end
|
764
|
+
```
|
612
765
|
|
613
|
-
|
766
|
+
### bulk_action_content
|
614
767
|
|
615
|
-
|
768
|
+
Blindly inserts content into the dropdown.
|
616
769
|
|
617
770
|
```ruby
|
618
|
-
|
619
|
-
|
620
|
-
'
|
621
|
-
else
|
622
|
-
average = (values.sum { |value| convert_to_column_type(table_column, value) } / [values.length, 1].max)
|
623
|
-
content_tag(:span, number_to_percentage(average, precision: 0))
|
771
|
+
bulk_actions do
|
772
|
+
bulk_action_content do
|
773
|
+
content_tag(:li, 'Something')
|
624
774
|
end
|
625
775
|
end
|
626
776
|
```
|
627
777
|
|
628
|
-
|
629
|
-
|
630
|
-
Here `table_column` is the table_column being rendered, `values` is an array of all the values in this one column. `table_data` is the whole transposed array of data.
|
631
|
-
|
632
|
-
The values will be whatever datatype each table_column returns.
|
778
|
+
Don't actually use this.
|
633
779
|
|
634
|
-
|
780
|
+
## charts
|
635
781
|
|
636
|
-
|
782
|
+
Create a [Google Chart](https://developers.google.com/chart/interactive/docs/quick_start) based on your searched collection, filters and attributes.
|
637
783
|
|
638
|
-
|
784
|
+
No javascript required. Just use the `chart do ... end` block and return an Array of Arrays.
|
639
785
|
|
640
786
|
```ruby
|
641
|
-
|
642
|
-
|
643
|
-
|
644
|
-
|
645
|
-
|
646
|
-
|
787
|
+
charts do
|
788
|
+
chart :breakfast, 'BarChart' do |collection|
|
789
|
+
[
|
790
|
+
['Bacon', 10],
|
791
|
+
['Eggs', 20],
|
792
|
+
['Toast', 30]
|
793
|
+
]
|
794
|
+
end
|
647
795
|
|
648
|
-
|
649
|
-
|
796
|
+
chart :posts_per_day, 'LineChart', label: 'Posts per Day', legend: false do |collection|
|
797
|
+
collection.group_by { |post| post.created_at.beginning_of_day }.map do |date, posts|
|
798
|
+
[date.strftime('%F'), posts.length]
|
799
|
+
end
|
800
|
+
end
|
801
|
+
end
|
650
802
|
```
|
651
803
|
|
652
|
-
|
653
|
-
|
654
|
-
The number of entries to show per page
|
804
|
+
And then render each chart in your view:
|
655
805
|
|
656
|
-
```
|
657
|
-
|
806
|
+
```
|
807
|
+
<%= render_datatable_chart(@datatable, :breakfast) %>
|
808
|
+
<%= render_datatable_chart(@datatable, :posts_per_day) %>
|
658
809
|
```
|
659
810
|
|
660
|
-
|
811
|
+
or all together
|
661
812
|
|
662
|
-
|
813
|
+
```
|
814
|
+
<%= render_datatable_charts(@datatable) %>
|
815
|
+
```
|
663
816
|
|
664
|
-
|
817
|
+
All options passed to `chart` are used to initialize the chart javascript.
|
665
818
|
|
666
|
-
|
819
|
+
By default, the only package that is loaded is `corechart`, see the `config/initializers/effective_datatables.rb` file to add more packages.
|
667
820
|
|
668
|
-
|
821
|
+
## Extras
|
669
822
|
|
670
|
-
|
823
|
+
The following commands don't quite fit into the DSL, but are present nonetheless.
|
671
824
|
|
672
|
-
###
|
825
|
+
### simple
|
673
826
|
|
674
|
-
To
|
827
|
+
To render a simple table, without pagination, sorting, filtering, export buttons, per page, and default visibility:
|
675
828
|
|
676
|
-
```
|
677
|
-
render_datatable(@datatable,
|
829
|
+
```
|
830
|
+
<%= render_datatable(@datatable, simple: true) %>
|
678
831
|
```
|
679
832
|
|
680
|
-
###
|
833
|
+
### index
|
681
834
|
|
682
|
-
|
835
|
+
If you just want to render a datatable and nothing else, there is a quick way to skip creating a view:
|
683
836
|
|
684
837
|
```ruby
|
685
|
-
|
838
|
+
class PostsController < ApplicationController
|
839
|
+
def index
|
840
|
+
render_datatable_index PostsDatatable.new(self)
|
841
|
+
end
|
842
|
+
end
|
686
843
|
```
|
687
844
|
|
688
|
-
|
689
|
-
|
690
|
-
|
691
|
-
### Customize Filter Behaviour
|
845
|
+
will render `views/effective/datatables/index` with the assigned datatable.
|
692
846
|
|
693
|
-
|
847
|
+
## Advanced Search and Sort
|
694
848
|
|
695
|
-
|
849
|
+
The built-in search and ordering can be overridden on a per-column basis.
|
696
850
|
|
697
|
-
|
851
|
+
The only gotcha here is that you must be aware of the type of collection.
|
698
852
|
|
699
|
-
|
700
|
-
|
701
|
-
If the table column being customized is a table_column:
|
853
|
+
In the case of a `col` and an ActiveRecord-based collection:
|
702
854
|
|
703
855
|
```ruby
|
704
|
-
|
705
|
-
|
706
|
-
|
707
|
-
|
708
|
-
|
856
|
+
collection do
|
857
|
+
Post.all
|
858
|
+
end
|
859
|
+
|
860
|
+
datatable do
|
861
|
+
col :post_category do |post|
|
862
|
+
content_tag(:span, post.post_category, "badge-#{post.post_category}")
|
863
|
+
end.search do |collection, term, column, sql_column|
|
864
|
+
# collection is an ActiveRecord scoped collection
|
865
|
+
# term is the incoming PostCategory ID as per the search
|
866
|
+
# column is this column's attributes Hash
|
867
|
+
# sql_column is the column[:sql_column]
|
868
|
+
categories = current_user.post_categories.where(id: term.to_i)
|
869
|
+
|
870
|
+
collection.where(post_category_id: categories) # Must return an ActiveRecord scope
|
871
|
+
end.sort do |collection, direction, column, sql_column|
|
872
|
+
collection.joins(:post_category).order(:post_category => :title, direction)
|
709
873
|
end
|
710
874
|
end
|
711
875
|
```
|
712
876
|
|
713
|
-
And
|
877
|
+
And in the case of a `col` with an Array-based collection, or any `val`:
|
714
878
|
|
715
879
|
```ruby
|
716
|
-
|
717
|
-
|
718
|
-
|
719
|
-
|
720
|
-
|
880
|
+
collection do
|
881
|
+
Client.all.map do |client|
|
882
|
+
[client, client.first_name client.last_name, client.purchased_time()]
|
883
|
+
end
|
884
|
+
end
|
885
|
+
|
886
|
+
datatable do
|
887
|
+
col :client
|
888
|
+
col :first_name
|
889
|
+
col :last_name
|
890
|
+
|
891
|
+
col :purchased_time do |duration|
|
892
|
+
number_to_duration(duration)
|
893
|
+
end.search do |collection, term, column, index|
|
894
|
+
# collection is an Array of Arrays
|
895
|
+
# term is the incoming value as per the search. "3h30m"
|
896
|
+
# column is the column's attributes Hash
|
897
|
+
# index is this column's index in the collection
|
898
|
+
(hours, minutes) = term.to_s.gsub(/[^0-9|h]/, '').split.map(&:to_i)
|
899
|
+
duration = (hours.to_i * 60) + minutes.to_i
|
900
|
+
|
901
|
+
collection.select! { |row| row[index] == duration } # Must return an Array of Arrays
|
902
|
+
end.sort do |collection, term, column, index|
|
903
|
+
collection.sort! do |x, y|
|
904
|
+
x[index] <=> y[index]
|
905
|
+
end
|
721
906
|
end
|
722
907
|
end
|
723
908
|
```
|
724
909
|
|
725
|
-
|
910
|
+
The search and sort for each column will be merged together to form the final results.
|
726
911
|
|
727
|
-
|
912
|
+
### Default search collection
|
728
913
|
|
729
|
-
|
914
|
+
When using a `col :user` type belongs_to or has_many column, a search collection for that class will be loaded.
|
730
915
|
|
731
|
-
|
916
|
+
Add the following to your related model to customize the search collection:
|
732
917
|
|
733
918
|
```ruby
|
734
|
-
|
735
|
-
|
736
|
-
sql_direction = (direction == :desc ? 'DESC' : 'ASC')
|
737
|
-
collection.joins(:subscriptions).order("subscriptions.stripe_plan_id #{sql_direction}")
|
738
|
-
else
|
739
|
-
super
|
740
|
-
end
|
919
|
+
class Comment < ApplicationRecord
|
920
|
+
scope :datatables_filter, -> { Comment.includes(:user) }
|
741
921
|
end
|
742
922
|
```
|
743
923
|
|
744
|
-
|
924
|
+
Datatables will look for a `datatables_filter` scope, or `sorted` scope, or fallback to `all`.
|
745
925
|
|
746
|
-
|
747
|
-
def order_column(collection, table_column, direction, index)
|
748
|
-
if table_column[:name] == 'price'
|
749
|
-
if direction == :asc
|
750
|
-
collection.sort! { |a, b| a[index].gsub(/\D/, '').to_i <=> b[index].gsub(/\D/, '').to_i }
|
751
|
-
else
|
752
|
-
collection.sort! { |a, b| b[index].gsub(/\D/, '').to_i <=> a[index].gsub(/\D/, '').to_i }
|
753
|
-
end
|
754
|
-
else
|
755
|
-
super
|
756
|
-
end
|
757
|
-
end
|
758
|
-
```
|
759
|
-
|
760
|
-
### Initialize with attributes
|
926
|
+
If there are more than 500 max records, the filter will fallback to a `as: :string`.
|
761
927
|
|
762
|
-
|
928
|
+
## Dynamic Column Count
|
763
929
|
|
764
|
-
|
930
|
+
There are some extra steps to be taken if you want to change the number of columns based on `filters`.
|
765
931
|
|
766
|
-
|
932
|
+
Unfortunately, the DataTables jQuery doesn't support changing columns, so submitting filters needs to be done via POST instead of AJAX.
|
767
933
|
|
768
|
-
|
934
|
+
The following example displays a client column, and one column per month for each month in a date range:
|
769
935
|
|
770
936
|
```ruby
|
771
|
-
class
|
772
|
-
def index
|
773
|
-
@datatable = PostsDatatable.new(:user_id => current_user.try(:id))
|
774
|
-
end
|
775
|
-
end
|
776
|
-
```
|
937
|
+
class TimeEntriesPerClientReport < Effective::Datatable
|
777
938
|
|
778
|
-
|
939
|
+
filters do
|
940
|
+
# This instructs the filters form to use a POST, if available, or GET instead of AJAX
|
941
|
+
# It posts to the current controller/action, and there are no needed changes in your controller
|
942
|
+
changes_columns_count
|
779
943
|
|
780
|
-
|
781
|
-
|
782
|
-
datatable do
|
783
|
-
if attributes[:user_id].blank?
|
784
|
-
table_column :user_id { |post| post.user.email }
|
785
|
-
end
|
944
|
+
filter :start_date, (Time.zone.now - 6.months).beginning_of_month, required: true, label: 'For the month of: ', as: :effective_date_picker
|
945
|
+
filter :end_date, Time.zone.now.end_of_month, required: true, label: 'upto and including the whole month of', as: :effective_date_picker
|
786
946
|
end
|
787
947
|
|
788
|
-
|
789
|
-
|
790
|
-
|
791
|
-
|
792
|
-
|
948
|
+
datatable do
|
949
|
+
length :all
|
950
|
+
|
951
|
+
col :client
|
952
|
+
|
953
|
+
selected_months.each do |month|
|
954
|
+
col month.strftime('%b %Y'), as: :duration
|
793
955
|
end
|
956
|
+
|
957
|
+
actions_col
|
794
958
|
end
|
795
|
-
end
|
796
|
-
```
|
797
959
|
|
798
|
-
|
960
|
+
collection do
|
961
|
+
time_entries = TimeEntry.where(date: filter[:start_date].beginning_of_month...filter[:end_date].end_of_month)
|
962
|
+
.group_by { |time_entry| "#{time_entry.client_id}_#{time_entry.created_at.strftime('%b')}" }
|
799
963
|
|
800
|
-
|
964
|
+
Client.all.map do |client|
|
965
|
+
[client] + selected_months.map do |month|
|
966
|
+
entries = time_entries["#{client.id}_#{month.strftime('%b')}"] || []
|
801
967
|
|
802
|
-
|
803
|
-
|
804
|
-
def format_post_title(post)
|
805
|
-
if post.title.start_with?('important')
|
806
|
-
link_to(post.title.upcase, post_path(post))
|
807
|
-
else
|
808
|
-
link_to(post.title, post_path(post))
|
968
|
+
entries.map { |entry| entry.duration }.sum
|
969
|
+
end
|
809
970
|
end
|
810
971
|
end
|
811
972
|
|
812
|
-
|
813
|
-
|
814
|
-
|
973
|
+
# Returns an array of 2016-Jan-01, 2016-Feb-01 datetimes
|
974
|
+
def selected_months
|
975
|
+
@selected_months ||= [].tap do |months|
|
976
|
+
each_month_between(filter[:start_date].beginning_of_month, filter[:end_date].end_of_month) { |month| months << month }
|
815
977
|
end
|
816
978
|
end
|
817
979
|
|
818
|
-
|
819
|
-
|
980
|
+
# Call with each_month_between(start_date, end_date) { |date| puts date }
|
981
|
+
def each_month_between(start_date, end_date, &block)
|
982
|
+
while start_date <= end_date
|
983
|
+
block.call(start_date)
|
984
|
+
start_date = start_date + 1.month
|
985
|
+
end
|
820
986
|
end
|
821
987
|
end
|
822
988
|
```
|
823
989
|
|
824
|
-
|
990
|
+
# Additional Functionality
|
825
991
|
|
826
|
-
|
827
|
-
module PostHelper
|
828
|
-
end
|
829
|
-
```
|
830
|
-
|
831
|
-
```ruby
|
832
|
-
class PostsDatatable < Effective::Datatable
|
833
|
-
include PostsHelper
|
834
|
-
end
|
835
|
-
```
|
836
|
-
|
837
|
-
## Working with other effective_gems
|
838
|
-
|
839
|
-
### Effective Addresses
|
840
|
-
|
841
|
-
When working with an ActiveRecord collection that implements [effective_addresses](https://github.com/code-and-effect/effective_addresses),
|
842
|
-
the filters and sorting will be automatically configured.
|
843
|
-
|
844
|
-
Just define `table_column :addresses`
|
845
|
-
|
846
|
-
When filtering values in this column, the address1, address2, city, postal code, state code and country code will all be matched.
|
847
|
-
|
848
|
-
### Effective Obfuscation
|
849
|
-
|
850
|
-
When working with an ActiveRecord collection that implements [effective_obfuscation](https://github.com/code-and-effect/effective_obfuscation) for the ID column,
|
851
|
-
that column's filters and sorting will be automatically configured.
|
992
|
+
There are a few other ways to customize the behaviour of effective_datatables
|
852
993
|
|
853
|
-
|
994
|
+
## Checking for Empty collection
|
854
995
|
|
855
|
-
|
996
|
+
Check whether the datatable has records by calling `@datatable.empty?` and `@datatable.present?`.
|
856
997
|
|
857
|
-
|
998
|
+
## Override javascript options
|
858
999
|
|
859
|
-
|
1000
|
+
The javascript options used to initialize a datatable can be overriden as follows:
|
860
1001
|
|
861
|
-
|
862
|
-
|
1002
|
+
```ruby
|
1003
|
+
render_datatable(@datatable, input_js: { dom: "<'row'<'col-sm-12'tr>>", autoWidth: true })
|
1004
|
+
```
|
863
1005
|
|
864
|
-
|
1006
|
+
```ruby
|
1007
|
+
render_datatable(@datatable, input_js: { buttons_export_columns: ':visible:not(.col-actions)' })
|
1008
|
+
```
|
865
1009
|
|
866
|
-
|
1010
|
+
Please see [datatables options](https://datatables.net/reference/option/) for a list of initialization options.
|
867
1011
|
|
1012
|
+
You don't want to actually do this!
|
868
1013
|
|
869
1014
|
## Get access to the raw results
|
870
1015
|
|
@@ -886,20 +1031,6 @@ def finalize(collection)
|
|
886
1031
|
end
|
887
1032
|
```
|
888
1033
|
|
889
|
-
## Customize the datatables JS initializer
|
890
|
-
|
891
|
-
You can customize the initializer javascript passed to datatables.
|
892
|
-
|
893
|
-
The support for this is still pretty limitted.
|
894
|
-
|
895
|
-
```
|
896
|
-
= render_datatable(@datatable, {colReorder: false})
|
897
|
-
```
|
898
|
-
|
899
|
-
```
|
900
|
-
= render_datatable(@datatable, { buttons_export_columns: ':visible:not(.col-actions)' })
|
901
|
-
```
|
902
|
-
|
903
1034
|
## Authorization
|
904
1035
|
|
905
1036
|
All authorization checks are handled via the config.authorization_method found in the `config/initializers/effective_datatables.rb` file.
|
@@ -961,17 +1092,6 @@ end
|
|
961
1092
|
|
962
1093
|
MIT License. Copyright [Code and Effect Inc.](http://www.codeandeffect.com/)
|
963
1094
|
|
964
|
-
|
965
|
-
## Testing
|
966
|
-
|
967
|
-
The test suite for this gem is unfortunately not yet complete.
|
968
|
-
|
969
|
-
Run tests by:
|
970
|
-
|
971
|
-
```ruby
|
972
|
-
rake spec
|
973
|
-
```
|
974
|
-
|
975
1095
|
## Contributing
|
976
1096
|
|
977
1097
|
1. Fork it
|