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.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +632 -512
  3. data/app/assets/javascripts/dataTables/buttons/buttons.html5.js +176 -177
  4. data/app/assets/javascripts/dataTables/buttons/buttons.print.js +2 -0
  5. data/app/assets/javascripts/dataTables/buttons/dataTables.buttons.js +14 -14
  6. data/app/assets/javascripts/dataTables/dataTables.bootstrap.js +1 -1
  7. data/app/assets/javascripts/dataTables/jquery.dataTables.js +246 -217
  8. data/app/assets/javascripts/effective_datatables.js +2 -3
  9. data/app/assets/javascripts/effective_datatables/events.js.coffee +7 -0
  10. data/app/assets/javascripts/effective_datatables/filters.js.coffee +6 -0
  11. data/app/assets/javascripts/effective_datatables/initialize.js.coffee +42 -39
  12. data/app/assets/javascripts/effective_datatables/reset.js.coffee +7 -0
  13. data/app/assets/javascripts/vendor/jquery.delayedChange.js +1 -1
  14. data/app/assets/stylesheets/dataTables/dataTables.bootstrap.css +0 -1
  15. data/app/assets/stylesheets/effective_datatables.scss +1 -2
  16. data/app/assets/stylesheets/effective_datatables/{_scopes.scss → _filters.scss} +1 -1
  17. data/app/assets/stylesheets/effective_datatables/_overrides.scss +1 -1
  18. data/app/controllers/effective/datatables_controller.rb +2 -4
  19. data/app/helpers/effective_datatables_helper.rb +56 -91
  20. data/app/helpers/effective_datatables_private_helper.rb +55 -64
  21. data/app/models/effective/datatable.rb +103 -177
  22. data/app/models/effective/datatable_column.rb +28 -0
  23. data/app/models/effective/datatable_column_tool.rb +110 -0
  24. data/app/models/effective/datatable_dsl_tool.rb +28 -0
  25. data/app/models/effective/datatable_value_tool.rb +142 -0
  26. data/app/models/effective/effective_datatable/attributes.rb +25 -0
  27. data/app/models/effective/effective_datatable/collection.rb +38 -0
  28. data/app/models/effective/effective_datatable/compute.rb +154 -0
  29. data/app/models/effective/effective_datatable/cookie.rb +29 -0
  30. data/app/models/effective/effective_datatable/dsl.rb +14 -8
  31. data/app/models/effective/effective_datatable/dsl/bulk_actions.rb +5 -6
  32. data/app/models/effective/effective_datatable/dsl/charts.rb +7 -9
  33. data/app/models/effective/effective_datatable/dsl/datatable.rb +107 -57
  34. data/app/models/effective/effective_datatable/dsl/filters.rb +50 -0
  35. data/app/models/effective/effective_datatable/format.rb +157 -0
  36. data/app/models/effective/effective_datatable/hooks.rb +0 -18
  37. data/app/models/effective/effective_datatable/params.rb +34 -0
  38. data/app/models/effective/effective_datatable/resource.rb +108 -0
  39. data/app/models/effective/effective_datatable/state.rb +178 -0
  40. data/app/views/effective/datatables/_actions_column.html.haml +9 -42
  41. data/app/views/effective/datatables/_bulk_actions_column.html.haml +1 -1
  42. data/app/views/effective/datatables/_bulk_actions_dropdown.html.haml +2 -3
  43. data/app/views/effective/datatables/_chart.html.haml +1 -1
  44. data/app/views/effective/datatables/_datatable.html.haml +7 -25
  45. data/app/views/effective/datatables/_filters.html.haml +21 -0
  46. data/app/views/effective/datatables/_reset.html.haml +2 -0
  47. data/app/views/effective/datatables/_resource_column.html.haml +8 -0
  48. data/app/views/effective/datatables/index.html.haml +0 -1
  49. data/config/effective_datatables.rb +9 -32
  50. data/lib/effective_datatables.rb +2 -6
  51. data/lib/effective_datatables/engine.rb +1 -1
  52. data/lib/effective_datatables/version.rb +1 -1
  53. data/lib/generators/effective_datatables/install_generator.rb +2 -2
  54. metadata +39 -19
  55. data/app/assets/javascripts/dataTables/colreorder/dataTables.colReorder.js +0 -27
  56. data/app/assets/javascripts/dataTables/jszip/jszip.js +0 -9155
  57. data/app/assets/javascripts/effective_datatables/scopes.js.coffee +0 -9
  58. data/app/models/effective/active_record_datatable_tool.rb +0 -242
  59. data/app/models/effective/array_datatable_tool.rb +0 -97
  60. data/app/models/effective/effective_datatable/ajax.rb +0 -101
  61. data/app/models/effective/effective_datatable/charts.rb +0 -20
  62. data/app/models/effective/effective_datatable/dsl/scopes.rb +0 -23
  63. data/app/models/effective/effective_datatable/helpers.rb +0 -24
  64. data/app/models/effective/effective_datatable/options.rb +0 -309
  65. data/app/models/effective/effective_datatable/rendering.rb +0 -365
  66. 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: 5eed7737db95090d9972bd497d03167274bbcc33
4
- data.tar.gz: fc60bd7d87417e5ced94f4d6dbd49256acb49a97
3
+ metadata.gz: d48516e2bb16d37adae464ea8c9ed86c11310787
4
+ data.tar.gz: b34bd2c14d20f351a99142c57e8897b16163ca1c
5
5
  SHA512:
6
- metadata.gz: 8877d69b948273ec1bea41665817a76f12652bacd442fbaa64dfcc4066462d198d96d949aa6b420ede4f52c749c3ddd63230522c6813fa297806d854fe4cd40d
7
- data.tar.gz: 33b000a3a093f9cf0f6c08379b924538b34fed19e3cac1aee3b9de9a79cfe54ea6af05ef1fad3b6f7dcf177f435e7ba896eb9f73a7745db715584d113973b0aa
6
+ metadata.gz: a40f0467539c7a0b08784bc00e54bd4bee2d57e8a86fd0e527b623eaac322a5355670b37004199ac26ac67cb2daa6fa75c30f649f431712bb7a5530221c0aae5
7
+ data.tar.gz: ff9153ff5e7e69400b1a82f91209c10e672adbe952ac998df2a9c663e73da73f763aa651a09e423307e6e2a4db80f99e83785522ad0f6b7d8272c8319a486169
data/README.md CHANGED
@@ -1,16 +1,44 @@
1
1
  # Effective DataTables
2
2
 
3
- Uniquely powerful server-side searching, sorting and filtering of any ActiveRecord or Array collection as well as post-rendered content displayed as a frontend jQuery Datatable.
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
- Use a simple DSL in just one ruby file to implement all features
5
+ Powerful server-side searching, sorting and filtering of ActiveRecord classes, with `belongs_to` and `has_many` relationships.
6
6
 
7
- Search raw database tables and ruby post-rendered results at the same time
7
+ Does the right thing with searching sql columns as well as computed values from both ActiveRecord and Array collections.
8
8
 
9
- Packages the jQuery DataTables assets for use in a Rails 3.2.x & Rails 4.x application using Twitter Bootstrap 3
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
- ## Getting Started
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
- ## Usage
73
+ # Quick Start
46
74
 
47
- We create a model, initialize it within our controller, then render it from a view
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
- ### The Model
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
- table_column :id
61
- table_column :user # if Post belongs_to :user
62
- table_column :comments # if Post has_many :comments
63
- table_column :title
64
- table_column :created_at
65
- actions_column
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
- def collection
94
+ collection do
69
95
  Post.all
70
96
  end
71
-
72
97
  end
73
98
  ```
74
99
 
75
- ### The Controller
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
- ### The View
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
- ## How It Works
121
+ # Usage
97
122
 
98
- When the jQuery DataTable is first initialized on the front-end, it makes an AJAX request back to the server asking for data.
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
- The effective_datatables gem intercepts this request and returns the appropriate results.
125
+ Here is an advanced example:
101
126
 
102
- Whenever a search, sort, filter or pagination is initiated on the front end, that request is interpretted by the server and the appropriate results returned.
127
+ ## The Model
103
128
 
104
- Due to the unique search/filter ability of this gem, a mix of raw database tables and processed results may be worked with at the same time.
129
+ This model exists at `/app/datatables/posts_datatable.rb`:
105
130
 
131
+ ```ruby
132
+ class PostsDatatable < Effective::Datatable
106
133
 
107
- ### Effective::Datatable Model & DSL
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
- Once your controller and view are set up to render a Datatable, the model is the central point to configure all behaviour.
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
- This single model file contains just 1 required method and responds to only 3 DSL commands.
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
- For example: `/app/datatables/posts_datatable.rb`:
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
- ```ruby
116
- class PostsDatatable < Effective::Datatable
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
- default_order :created_at, :desc
119
- default_entries 25
191
+ length 25 # 5, 10, 25, 50, 100, 1000, :all
192
+ order :updated_at, :desc
120
193
 
121
- table_column :id, :visible => false
194
+ # Renders a column of checkboxes to select items for any bulk_actions
195
+ bulk_actions_col
122
196
 
123
- table_column :created_at, :width => '25%'
197
+ col :id, visible: false
198
+ col :updated_at, visible: false
124
199
 
125
- table_column :updated_at, :proc => Proc.new { |post| nicetime(post.updated_at) } # just a standard helper as defined in helpers/application_helper.rb
200
+ col :created_at, label: 'Created' do |post|
201
+ time_ago_in_words(post.created_at)
202
+ end
126
203
 
127
- table_column :user
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
- table_column :post_category_id, :filter => {:as => :select, :collection => Proc.new { PostCategory.all } } do |post|
130
- post.post_category.name.titleize
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
- array_column :comments do |post|
134
- content_tag(:ul) do
135
- post.comments.where(:archived => false).map do |comment|
136
- content_tag(:li, comment.title)
137
- end.join('').html_safe
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
- table_column :title, :label => 'Post Title', :class => 'col-title'
142
- actions_column
143
- end
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
- def collection
146
- Post.where(:archived => false).includes(:post_category)
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
- ### The collection
260
+ ## The Controller
153
261
 
154
- A required method `def collection` must be defined to return the base ActiveRecord collection.
262
+ Any options used to initialize a datatable become the `attributes`. Use these to configure datatables behavior.
155
263
 
156
- It can be as simple or as complex as you'd like:
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
- def collection
160
- Post.all
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
- or (complex example):
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
- if attributes[:user_id].present?
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
- def query_total
186
- "SUM((order_items.price * order_items.quantity) + (CASE order_items.tax_exempt WHEN true THEN 0 ELSE ((order_items.price * order_items.quantity) * order_items.tax_rate) END))"
187
- end
278
+ ```
279
+ <h1>All Posts</h1>
280
+ <%= render_datatable(@datatable) %>
188
281
  ```
189
282
 
190
- ### Array Backed collection
283
+ or, the datatable, filter and charts may be rendered individually:
191
284
 
192
- Don't want to use ActiveRecord? Not a problem.
285
+ ```
286
+ <h1>All Posts</h1>
287
+ <p>
288
+ <%= render_datatable_filters(@datatable) %>
289
+ </p>
193
290
 
194
- Define your collection as an Array of Arrays, declare only array_columns, and everything works as expected.
291
+ <p>
292
+ <%= render_datatable_charts(@datatable) %>
293
+ </p>
195
294
 
196
- ```ruby
197
- class ArrayBackedDatatable < Effective::Datatable
198
- datatable do
199
- array_column :id
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
- def collection
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
- end
302
+ ```
303
+ <%= render_datatable(@datatable, simple: true) %>
215
304
  ```
216
305
 
217
- ## table_column
306
+ # DSL
218
307
 
219
- This is the main DSL method that you will interact with.
308
+ The effective_datatables DSL is made up of 5 sections: `collection`, `datatable`, `filters` `bulk_actions`, `charts`
220
309
 
221
- table_column defines a 1:1 mapping between a SQL database table column and a frontend jQuery Datatables table column. It creates a column.
310
+ As well, a datatable can be initialized with `attributes`.
222
311
 
223
- table_column performs searching and sorting on the raw database records, before any results are rendered.
312
+ ## attributes
224
313
 
225
- Options may be passed to specify the display, search, sort and filter behaviour for that column.
314
+ When initialized with a Hash, that hash is available throughout the entire datatable as `attributes`.
226
315
 
227
- When the given name of the table_column matches an ActiveRecord attribute, the options are set intelligently based on the underlying datatype.
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
- # The name of the table column as per the Database
231
- # This column is detected as an Integer, therefore it is :type => :integer
232
- # Any SQL used to search this field will take the form of "id = ?"
233
- table_column :id
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
- # As per our 'complex' example above, using the .select('customers.stripe_customer_id AS stripe_customer_id') syntax to create a faux database table
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
- # The name of the table column as per the Database
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
- # If the name of the table column matches a belongs_to in our collection's main class
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
- # Will have the same behaviour as declaring
252
- datatable do
253
- if attributes[:user_id].blank?
254
- table_column :user_id, :filter => {:as => :select, :collection => Proc.new { User.all.map { |user| [user.id, user.to_s] }.sort { |x, y| x[1] <=> y[1] } } } do |post|
255
- post.user.to_s
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
- All table_columns are `visible: true`, `sortable: true` by default.
353
+ ## collection
262
354
 
263
- ## array_column
355
+ The `collection do ... end` block must return an ActiveRecord relation or an Array of Arrays.
264
356
 
265
- `array_column` accepts the same options as `table_column` and behaves identically on the frontend.
357
+ ```ruby
358
+ collection do
359
+ Post.all
360
+ end
361
+ ```
266
362
 
267
- The difference occurs with sorting and filtering:
363
+ or
268
364
 
269
- array_columns perform searching and sorting on the computed results after all columns have been rendered.
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
- With a `table_column`, the frontend sends some search terms to the server, the raw database table is searched & sorted using standard ActiveRecord .where() and .order(), the appropriate rows returned, and then each row is rendered as per the rendering options.
373
+ or
272
374
 
273
- With an `array_column`, the front end sends some search terms to the server, all rows are returned and rendered, and then the rendered output is searched & sorted.
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
- This allows the output of an `array_column` to be anything complex that cannot be easily computed from the database.
386
+ or
276
387
 
277
- When searching & sorting with a mix of table_columns and array_columns, all the table_columns are processed first so the most work is put on the database, the least on rails.
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
- If you're overriding the `search_column` or `order_column` behaviour of an `array_column`, keep in mind that all values will be strings.
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
- This has the side effect of ordering an `array_column` of numbers, as if they were strings. To keep them ordered as numbers, call:
397
+ calc = TimeEntryCalculator.new(entries)
282
398
 
283
- ```ruby
284
- array_column :price, type: :number do |product|
285
- number_to_currency(product.price)
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 above code will output the price as a currency, but still sort the values as numbers rather than as strings.
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
- ### General Options
409
+ All searching and ordering is also done by effective_datatables.
293
410
 
294
- The following options control the general behaviour of the column:
411
+ Your collection method should not contain a `.order()`, or implement search in any way.
295
412
 
296
- ```ruby
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
- ### Display Options
415
+ ## datatable
302
416
 
303
- The following options control the display behaviour of the column:
417
+ The `datatable do ... end` block configures a table of data.
304
418
 
305
- ```ruby
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
- ### Filtering Options
421
+ ### col
315
422
 
316
- Setting a filter will create an appropriate text/number/select input in the header row of the column.
423
+ This is the main DSL method that you will interact with.
317
424
 
318
- The following options control the filtering behaviour of the column:
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
- ```ruby
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
- :filter => {:as => :number}
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
- :filter => {:as => :select, :collection => ['One', 'Two'], :selected => 'Two'}
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
- :filter => {:as => :grouped_select, :collection => {'Active' => Events.active, 'Past' => Events.past }}
333
- :filter => {:as => :grouped_select, :collection => {'Active' => [['Event A', 1], ['Event B', 2]], 'Past' => [['Event C', 3], ['Event D', 4]]} }
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
- Some additional, lesser used options include:
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
- ```ruby
339
- :filter => {:fuzzy => true} # Will use an ILIKE/includes rather than = when filtering. Use this for selects.
340
- :filter => {sql_operation => :having} # Will use .having() instead of .where() to handle aggregate columns (autodetected)
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
- ### Rendering Options
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
- There are a few different ways to render each column's output.
459
+ `:boolean`, `:currency`, `:datetime`, `:date`, `:decimal`, `:duration`, `:email`, `:float`, `:integer`, `:percentage`, `:price`, `:resource`, `:string`, `:text`
348
460
 
349
- Any standard view helpers like `link_to` or `simple_format` and any custom helpers available to your views will be available.
461
+ These settings are loosely based on the regular datatypes, with some custom effective types thrown in:
350
462
 
351
- All of the following rendering options can be used interchangeably:
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
- Block format (really, this is your cleanest option):
470
+ The column will be formatted as per its `as:` setting, unless a custom format block is present:
354
471
 
355
472
  ```ruby
356
- table_column :created_at do |post|
357
- if post.created_at > (Time.zone.now-1.year)
358
- link_to('this year', post_path(post))
473
+ col :approved do |post|
474
+ if post.approved?
475
+ content_tag(:span, 'Approved', 'badge badge-approved')
359
476
  else
360
- link_to(post.created_at.strftime("%Y-%m-%d %H:%M:%S"), post_path(post))
477
+ content_tag(:span, 'Draft', 'badge badge-draft')
361
478
  end
362
479
  end
363
480
  ```
364
481
 
365
- Proc format:
482
+ You can also set custom search and sort on a per-column basis. See Advanced Search and Sort below.
366
483
 
367
- ```ruby
368
- table_column :created_at, :proc => Proc.new { |post| link_to(post.created_at, post_path(post)) }
369
- ```
484
+ ### val
370
485
 
371
- Partial format:
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
- table_column :actions, :partial => '/posts/actions' # render this partial for each row of the table
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
- then in your `/app/views/posts/_actions.html.erb` file:
498
+ So, `val` yields the object from the collection to the first/compute block, and stores the result.
378
499
 
379
- ```erb
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
- The local object name will either match the database table singular name `post`, the name of the partial `actions`, or `obj` unless overridden with:
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
- table_column :actions, :partial => '/posts/actions', :partial_local => 'the_post'
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
- There are also a built in helper, `datatables_admin_path?` to considering if the current screen is in the `/admin` namespace:
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
- table_column :created_at do |post|
394
- if datatables_admin_path?
395
- link_to admin_posts_path(post)
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 request object is available to the table_column, so you could just as easily call:
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
- ```ruby
405
- request.referer.include?('/admin/')
406
- ```
547
+ ### length
407
548
 
408
- ### Column Header Rendering Options
549
+ Sets the default number of rows per page. Valid lengths are `5`, `10`, `25`, `50`, `100`, `250`, `1000`, `:all`
409
550
 
410
- You can override the default rendering and define a partial to use for the header `<th>`:
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
- table_column :special, :header_partial => '/posts/special_header'
554
+ length 100
414
555
  ```
415
556
 
416
- The following locals will be available in the header partial:
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
- form # The SimpleForm FormBuilder instance
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
- ## actions_column
569
+ ### aggregate
570
+
571
+ The `aggregate` command inserts a row in the table's `tfoot`.
426
572
 
427
- Creates a column with links to this resource's `show`, `edit` and `destroy` actions.
573
+ The only option available is `:label`.
428
574
 
429
- Sets `responsivePriority: 0` so the column is last to collapse when the table is shrunk down.
575
+ You can only have one aggregate per datatable. (Unfortunately, this is a limit of the jQuery Datatables)
430
576
 
431
- Override the default actions by passing your own partial:
577
+ There is built in support for automatic `:total` and `:average` aggregates:
432
578
 
433
579
  ```ruby
434
- actions_column partial: 'admin/posts/actions'
580
+ aggregate :total|:average
435
581
  ```
436
582
 
437
- or just extend the default by
583
+ or write your own:
438
584
 
439
585
  ```ruby
440
- actions_column do |post|
441
- unless post.approved?
442
- glyphicon_to('ok', approve_post_path(post), title: 'Approve')
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
- ### Showing action buttons
596
+ In the above example, `values` is an Array containing all row's values for one column at a time.
448
597
 
449
- The show/edit/destroy action buttons can be configured to always show, always hide, or to consider the current_user's permission level.
598
+ ## filters
450
599
 
451
- To always show / hide:
600
+ Creates a single form with fields for each `filter` and a single radio input field for all `scopes`.
452
601
 
453
- ```ruby
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
- To authorize based on the current_user and the `config.authorization_method`:
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
- ```ruby
460
- actions_column show: :authorize
461
- ```
606
+ ### scope
462
607
 
463
- The above will call the effective_datatables `config.authorization_method` just once to see if the current_user has permission to show/edit/destroy the collection class.
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
- The action button will be displayed if `EffectiveDatatables.authorized?(controller, :edit, Post)` returns true.
610
+ Only supported for ActiveRecord based collections. They must exist as regular scopes on the model.
466
611
 
467
- To call authorize on each individual resource:
612
+ The currently selected scope will be automatically applied. You shouldn't consider it in your collection block.
468
613
 
469
614
  ```ruby
470
- actions_column show: :authorize_each
615
+ filters do
616
+ scope :approved
617
+ scope :for_user, current_user
618
+ end
471
619
  ```
472
620
 
473
- Or via a Proc:
621
+ Must match the scopes in your `app/models/post.rb`:
474
622
 
475
623
  ```ruby
476
- actions_column show: Proc.new { |resource| can?(:show, resource.parent) }
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
- See the `config/initializers/effective_datatable.rb` file for more information.
630
+ ### filter
480
631
 
481
- ## bulk_actions_column
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
- Creates a column of checkboxes to select one, some, or all rows and adds a bulk actions dropdown button.
634
+ effective_datatables looks at the default value, and tries to cast the incoming (String) value into that datatype.
484
635
 
485
- When one or more checkboxes are checked, the bulk actions dropdown is enabled and any defined `bulk_action`s will be available to click.
636
+ This ensures that calling `filters[:name]` always return a value. The default can be nil.
486
637
 
487
- Clicking a bulk action makes an AJAX POST request with the parameters `ids: [1, 2, 3]` as per the selected rows.
638
+ You can override the parsing on a per-filter basis.
488
639
 
489
- By default, the method used to determine each row's checkbox value is `to_param`. To call a different method use `bulk_actions_column(resource_method: :slug) do ... end`.
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
- datatable do
501
- bulk_actions_column do
502
- bulk_action 'Approve all', bulk_approve_posts_path, data: {confirm: 'Approve all selected posts?'}
503
- bulk_action_divider
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
- You still need to write your own controller action to process the bulk action. Something like:
650
+ and apply these to your `collection do ... end` block by calling `filters[:start_date]`:
513
651
 
514
652
  ```ruby
515
- class PostsController < ApplicationController
516
- def bulk_approve
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
- # You should probably write this inside a transaction. This is just an example.
520
- begin
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
- and in your `routes.rb`:
664
+ The filter command has the following options:
531
665
 
532
666
  ```ruby
533
- resources :posts do
534
- collection do
535
- post :bulk_approve
536
- end
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
- ## scopes
673
+ Any other option given will be yielded to SimpleForm as `input_html` options.
541
674
 
542
- When declaring a scope, a form field will automatically be placed above the datatable that can filter on the collection.
675
+ ## bulk_actions
543
676
 
544
- The value of the scope, its default value, will be available for use anywehre in your datatable via the `attributes` hash.
677
+ Creates a single dropdown menu with a link to each action, download or content.
545
678
 
546
- ```ruby
547
- scopes do
548
- scope :start_date, Time.zone.now-3.months, filter: { input_html: { class: 'datepicker' } }
549
- end
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
- (scopes is declared outside of the `datatable do ... end` block)
685
+ A controller action must be created to accept a POST with an array of selected ids, `params[:ids]`.
553
686
 
554
- and then in your collection, or any `table_column` block:
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
- def collection
558
- Post.where('updated_at > ?', attributes[:start_date])
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
- As well, you need to change the controller where you define the datatable to be aware of the scope params.
695
+ In your `routes` file:
563
696
 
564
697
  ```ruby
565
- @datatable = PostsDatatable.new(params[:scopes])
698
+ resources :posts do
699
+ collection do
700
+ post :bulk_approve
701
+ end
702
+ end
566
703
  ```
567
704
 
568
- And to display the scopes anywhere in your view:
705
+ In your `PostsController`:
569
706
 
570
707
  ```ruby
571
- = render_datatable_scopes(@datatable)
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
- So initially, the `:start_date` will have the value of `Time.zone.now-3.months` and when submitted by the form, the value will be set there.
721
+ ### bulk_action_divider
575
722
 
576
- The form value will come back as a string, so you may need to `Time.zone.parse` that value.
723
+ Inserts a menu divider `<li class='divider' role='separator'></li>`
577
724
 
578
- Pass `scope :start_date, Time.zone.now-3.months, fallback: true` to fallback to the default value when the form submission is not present.
725
+ ### bulk_download
579
726
 
580
- Any `filter: { ... }` options will be passed straight into simple_form.
727
+ So it turns out there are some http issues with using an AJAX action to download a file.
581
728
 
582
- ### current_scope / model scopes
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
- You can also use scopes as defined on your ActiveRecord model
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
- scopes do
590
- scope :all
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
- The front end will render these klass scopes as a radio buttons / button group.
739
+ ```ruby
740
+ def bulk_export_report
741
+ authorize! :export, Post
742
+
743
+ @posts = Post.where(id: params[:ids])
604
744
 
605
- To determine which scope is selected, you can call `current_scope` or `attributes[:current_scope]` or `attributes[:standard]`
745
+ Post.transaction do
746
+ begin
747
+ cookies[:fileDownload] = true
606
748
 
607
- When no scopes are selected, and no defaults are present, the above will return nil.
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
- It's a bit confusing, but you can mix and match these with regular attribute scopes.
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
- ## aggregates
762
+ render json: { error: 'An error occurred' }
763
+ end
764
+ ```
612
765
 
613
- Each `aggregate` directive adds an additional row to the table's tfoot.
766
+ ### bulk_action_content
614
767
 
615
- This feature is intended to display a sum or average of all the table's currently displayed values.
768
+ Blindly inserts content into the dropdown.
616
769
 
617
770
  ```ruby
618
- aggregate :average do |table_column, values, table_data|
619
- if table_column[:name] == 'user'
620
- 'Average'
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
- The above aggregate block will be called for each currently visible column in a datatable.
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
- It might be the case that the formatted values (strings) are returned, which is why `convert_to_column_type` is used above.
780
+ ## charts
635
781
 
636
- ## table_columns
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
- Quickly create multiple table_columns all with default options:
784
+ No javascript required. Just use the `chart do ... end` block and return an Array of Arrays.
639
785
 
640
786
  ```ruby
641
- table_columns :id, :created_at, :updated_at, :category, :title
642
- ```
643
-
644
- ## default_order
645
-
646
- Sort the table by this field and direction on start up
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
- ```ruby
649
- default_order :created_at, :asc|:desc
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
- ## default_entries
653
-
654
- The number of entries to show per page
804
+ And then render each chart in your view:
655
805
 
656
- ```ruby
657
- default_entries :all
806
+ ```
807
+ <%= render_datatable_chart(@datatable, :breakfast) %>
808
+ <%= render_datatable_chart(@datatable, :posts_per_day) %>
658
809
  ```
659
810
 
660
- Valid options are `10, 25, 50, 100, 250, 1000, :all`
811
+ or all together
661
812
 
662
- ## Additional Functionality
813
+ ```
814
+ <%= render_datatable_charts(@datatable) %>
815
+ ```
663
816
 
664
- There are a few other ways to customize the behaviour of effective_datatables
817
+ All options passed to `chart` are used to initialize the chart javascript.
665
818
 
666
- ### Checking for Empty collection
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
- Check whether the datatable has records by calling `@datatable.empty?` and `@datatable.present?`.
821
+ ## Extras
669
822
 
670
- Keep in mind, these methods look at the collection's total records, not the currently displayed/filtered records.
823
+ The following commands don't quite fit into the DSL, but are present nonetheless.
671
824
 
672
- ### Hide the buttons
825
+ ### simple
673
826
 
674
- To hide the Bulk Actions, Show / Hide Columns, CSV, Excel, Print, etc buttons:
827
+ To render a simple table, without pagination, sorting, filtering, export buttons, per page, and default visibility:
675
828
 
676
- ```ruby
677
- render_datatable(@datatable, buttons: false)
829
+ ```
830
+ <%= render_datatable(@datatable, simple: true) %>
678
831
  ```
679
832
 
680
- ### Override javascript options
833
+ ### index
681
834
 
682
- The javascript options used to initialize a datatable can be overriden as follows:
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
- render_datatable(@datatable, {dom: "<'row'<'col-sm-12'tr>>", autoWidth: true})
838
+ class PostsController < ApplicationController
839
+ def index
840
+ render_datatable_index PostsDatatable.new(self)
841
+ end
842
+ end
686
843
  ```
687
844
 
688
- Please see [datatables options](https://datatables.net/reference/option/) for a list of initialization options.
689
-
690
-
691
- ### Customize Filter Behaviour
845
+ will render `views/effective/datatables/index` with the assigned datatable.
692
846
 
693
- This gem does its best to provide "just works" filtering of both raw SQL (table_column) and processed results (array_column) out-of-the-box.
847
+ ## Advanced Search and Sort
694
848
 
695
- It's also very easy to override the filter behaviour on a per-column basis.
849
+ The built-in search and ordering can be overridden on a per-column basis.
696
850
 
697
- Keep in mind, that filter terms applied to hidden columns will still be considered in filter results.
851
+ The only gotcha here is that you must be aware of the type of collection.
698
852
 
699
- To customize filter behaviour, specify a `def search_column` method in the datatables model file.
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
- def search_column(collection, table_column, search_term, sql_column)
705
- if table_column[:name] == 'subscription_types'
706
- collection.where('subscriptions.stripe_plan_id ILIKE ?', "%#{search_term}%")
707
- else
708
- super
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 if the table column being customized is an array_column:
877
+ And in the case of a `col` with an Array-based collection, or any `val`:
714
878
 
715
879
  ```ruby
716
- def search_column(collection, table_column, search_term, index)
717
- if table_column[:name] == 'price'
718
- collection.select! { |row| row[index].include?(search_term) }
719
- else
720
- super
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
- ### Customize Order Behaviour
910
+ The search and sort for each column will be merged together to form the final results.
726
911
 
727
- The order behaviour can be overridden on a per-column basis.
912
+ ### Default search collection
728
913
 
729
- To custom order behaviour, specify a `def order_column` method in the datatables model file.
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
- If the table column being customized is a table_column:
916
+ Add the following to your related model to customize the search collection:
732
917
 
733
918
  ```ruby
734
- def order_column(collection, table_column, direction, sql_column)
735
- if table_column[:name] == 'subscription_types'
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
- And if the table column being customized is an array_column:
924
+ Datatables will look for a `datatables_filter` scope, or `sorted` scope, or fallback to `all`.
745
925
 
746
- ```ruby
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
- Any attributes passed to `.new()` will be persisted through the lifecycle of the datatable.
928
+ ## Dynamic Column Count
763
929
 
764
- You can use this to scope the datatable collection or create even more advanced search behaviour.
930
+ There are some extra steps to be taken if you want to change the number of columns based on `filters`.
765
931
 
766
- In the following example we will hide the User column and scope the collection to a specific user.
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
- In your controller:
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 PostsController < ApplicationController
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
- And then in your datatable:
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
- ```ruby
781
- class PostsDatatable < Effective::Datatable
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
- def collection
789
- if attributes[:user_id]
790
- Post.where(user_id: attributes[:user_id])
791
- else
792
- Post.all
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
- ### Helper methods
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
- Any non-private methods defined in the datatable model will be available to your table_columns and evaluated in the view_context.
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
- ```ruby
803
- class PostsDatatable < Effective::Datatable
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
- datatable do
813
- table_column :title do |post|
814
- format_post_title(post)
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
- def collection
819
- Post.all
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
- You can also get the same functionality by including a regular Rails helper within the datatable model.
990
+ # Additional Functionality
825
991
 
826
- ```ruby
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
- Just define `table_column :id`
994
+ ## Checking for Empty collection
854
995
 
855
- Unfortunately, due to the effective_obfuscation algorithm, sorting and filtering by partial values is not supported.
996
+ Check whether the datatable has records by calling `@datatable.empty?` and `@datatable.present?`.
856
997
 
857
- So the column may not be sorted, and may only be filtered by typing the entire 10-digit number, with or without any formatting.
998
+ ## Override javascript options
858
999
 
859
- ### Effective Roles
1000
+ The javascript options used to initialize a datatable can be overriden as follows:
860
1001
 
861
- When working with an ActiveRecord collection that implements [effective_roles](https://github.com/code-and-effect/effective_roles),
862
- the filters and sorting will be automatically configured.
1002
+ ```ruby
1003
+ render_datatable(@datatable, input_js: { dom: "<'row'<'col-sm-12'tr>>", autoWidth: true })
1004
+ ```
863
1005
 
864
- Just define `table_column :roles`
1006
+ ```ruby
1007
+ render_datatable(@datatable, input_js: { buttons_export_columns: ':visible:not(.col-actions)' })
1008
+ ```
865
1009
 
866
- The `EffectiveRoles.roles` collection will be used for the filter collection, and sorting will be done by roles_mask.
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