effective_datatables 2.12.2 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|