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.
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