effective_datatables 1.8.3 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 51442e1874c5619504602bba266af6ebdd589a2a
4
- data.tar.gz: 51f2e7929b20e801d9b6b30b70fd4c0c028503d6
3
+ metadata.gz: 42cbf488b1f1cf5d77c3ffd2dc352b06eee67d9c
4
+ data.tar.gz: d94cd33a3001743a072162855700dcdbf97e6dd2
5
5
  SHA512:
6
- metadata.gz: 06f07d33e9eaf67ea873f21650b505338e1928a31ce5138264aa5eed6cb1715b8c84c0916a2ea2c7ba0d08443e22cbd0c6acbaf4a9bcfde8cfa1bc320b09d166
7
- data.tar.gz: 055b33ce39c9b1745c74f73d105f4d16134e0bdea5c1e5e7587ccda73bbfa815826499c8166a3144b2f9acc548718df71117af048bac4b56f99a042162c42e90
6
+ metadata.gz: 69dab91b291914705aa7e9903b706772d73ec708b9a5c0ebdb0bb9494481fccdb0f0c8d124de634b3dbf952287fe99d62e5f5f48f43e718a38d2e7f85f3a5b44
7
+ data.tar.gz: 4b44b67e3385891730e63cea4c236967792d34959a4c65662cc7c9d4243eae39d5df61be52194b74f8eb49f84be2db522b052eafddf3d02ca7e8b34a8c08b7c9
data/README.md CHANGED
@@ -67,10 +67,12 @@ This model exists at `/app/models/effective/datatables/posts.rb`:
67
67
  module Effective
68
68
  module Datatables
69
69
  class Posts < Effective::Datatable
70
- table_column :id
71
- table_column :user # if Post belongs_to :user
72
- table_column :title
73
- table_column :created_at
70
+ datatable do
71
+ table_column :id
72
+ table_column :user # if Post belongs_to :user
73
+ table_column :title
74
+ table_column :created_at
75
+ end
74
76
 
75
77
  def collection
76
78
  Post.all
@@ -88,7 +90,7 @@ We're going to display this DataTable on the posts#index action
88
90
  ```ruby
89
91
  class PostsController < ApplicationController
90
92
  def index
91
- @datatable = Effective::Datatables::Posts.new()
93
+ @datatable = Effective::Datatables::Posts.new
92
94
  end
93
95
  end
94
96
  ```
@@ -128,31 +130,33 @@ For example: `/app/models/effective/datatables/posts.rb`:
128
130
  module Effective
129
131
  module Datatables
130
132
  class Posts < Effective::Datatable
131
- default_order :created_at, :desc
132
- default_entries 25
133
+ datatable do
134
+ default_order :created_at, :desc
135
+ default_entries 25
133
136
 
134
- table_column :id, :visible => false
137
+ table_column :id, :visible => false
135
138
 
136
- table_column :created_at, :width => '25%'
139
+ table_column :created_at, :width => '25%'
137
140
 
138
- table_column :updated_at, :proc => Proc.new { |post| nicetime(post.updated_at) } # just a standard helper as defined in helpers/application_helper.rb
141
+ table_column :updated_at, :proc => Proc.new { |post| nicetime(post.updated_at) } # just a standard helper as defined in helpers/application_helper.rb
139
142
 
140
- table_column :user
143
+ table_column :user
141
144
 
142
- table_column :post_category_id, :filter => {:type => :select, :values => Proc.new { PostCategory.all } } do |post|
143
- post.post_category.name.titleize
144
- end
145
+ table_column :post_category_id, :filter => {:type => :select, :values => Proc.new { PostCategory.all } } do |post|
146
+ post.post_category.name.titleize
147
+ end
145
148
 
146
- array_column :comments do |post|
147
- content_tag(:ul) do
148
- post.comments.where(:archived => false).map do |comment|
149
- content_tag(:li, comment.title)
150
- end.join('').html_safe
149
+ array_column :comments do |post|
150
+ content_tag(:ul) do
151
+ post.comments.where(:archived => false).map do |comment|
152
+ content_tag(:li, comment.title)
153
+ end.join('').html_safe
154
+ end
151
155
  end
152
- end
153
156
 
154
- table_column :title, :label => 'Post Title', :class => 'col-title'
155
- table_column :actions, :sortable => false, :filter => false, :partial => '/posts/actions'
157
+ table_column :title, :label => 'Post Title', :class => 'col-title'
158
+ table_column :actions, :sortable => false, :filter => false, :partial => '/posts/actions'
159
+ end
156
160
 
157
161
  def collection
158
162
  Post.where(:archived => false).includes(:post_category)
@@ -211,10 +215,12 @@ Define your collection as an Array of Arrays, declare only array_columns, and ev
211
215
  module Effective
212
216
  module Datatables
213
217
  class ArrayBackedDataTable < Effective::Datatable
214
- array_column :id
215
- array_column :first_name
216
- array_column :last_name
217
- array_column :email
218
+ datatable do
219
+ array_column :id
220
+ array_column :first_name
221
+ array_column :last_name
222
+ array_column :email
223
+ end
218
224
 
219
225
  def collection
220
226
  [
@@ -268,8 +274,12 @@ table_column :created_at
268
274
  table_column :user
269
275
 
270
276
  # Will have the same behaviour as declaring
271
- table_column :user_id, :if => Proc.new { attributes[:user_id].blank? }, :filter => {:type => :select, :values => Proc.new { User.all.map { |user| [user.id, user.to_s] }.sort { |x, y| x[1] <=> y[1] } } } do |post|
272
- post.user.to_s
277
+ datatable do
278
+ if attributes[:user_id].blank?
279
+ table_column :user_id, :filter => {:type => :select, :values => Proc.new { User.all.map { |user| [user.id, user.to_s] }.sort { |x, y| x[1] <=> y[1] } } } do |post|
280
+ post.user.to_s
281
+ end
282
+ end
273
283
  end
274
284
  ```
275
285
 
@@ -299,7 +309,6 @@ The following options control the general behaviour of the column:
299
309
  ```ruby
300
310
  :column => 'users.id' # Set this if you're doing something tricky with the database. Used internally for .order() and .where() clauses
301
311
  :type => :string # Derived from the ActiveRecord attribute default datatype. Controls searching behaviour. Valid options include :string, :text, :datetime, :integer, :boolean, :year
302
- :if => Proc.new { attributes[:user_id].blank? } # Excludes this table_column entirely if false. See "Initialize with attributes" section of this README below
303
312
  ```
304
313
 
305
314
  ### Display Options
@@ -503,23 +512,28 @@ class PostsController < ApplicationController
503
512
  end
504
513
  ```
505
514
 
506
- Scope the query to the passed user in in your collection method:
515
+ And then in your datatable:
507
516
 
508
517
  ```ruby
509
- def collection
510
- if attributes[:user_id]
511
- Post.where(:user_id => attributes[:user_id])
512
- else
513
- Post.all
514
- end
515
- end
516
- ```
518
+ module Effective
519
+ module Datatables
520
+ class Posts < Effective::Datatable
521
+ datatable do
522
+ if attributes[:user_id].blank?
523
+ table_column :user_id { |post| post.user.email }
524
+ end
525
+ end
517
526
 
518
- and remove the table_column when a user_id is present:
527
+ def collection
528
+ if attributes[:user_id]
529
+ Post.where(user_id: attributes[:user_id])
530
+ else
531
+ Post.all
532
+ end
533
+ end
519
534
 
520
- ```ruby
521
- table_column :user_id, :if => Proc.new { attributes[:user_id].blank? } do |post|
522
- post.user.email
535
+ end
536
+ end
523
537
  end
524
538
  ```
525
539
 
@@ -531,14 +545,6 @@ Any non-private methods defined in the datatable model will be available to your
531
545
  module Effective
532
546
  module Datatables
533
547
  class Posts < Effective::Datatable
534
- table_column :title do |post|
535
- format_post_title(post)
536
- end
537
-
538
- def collection
539
- Post.all
540
- end
541
-
542
548
  def format_post_title(post)
543
549
  if post.title.start_with?('important')
544
550
  link_to(post.title.upcase, post_path(post))
@@ -547,6 +553,15 @@ module Effective
547
553
  end
548
554
  end
549
555
 
556
+ datatable do
557
+ table_column :title do |post|
558
+ format_post_title(post)
559
+ end
560
+ end
561
+
562
+ def collection
563
+ Post.all
564
+ end
550
565
  end
551
566
  end
552
567
  end
@@ -577,7 +592,7 @@ end
577
592
  When working with an ActiveRecord collection that implements [effective_obfuscation](https://github.com/code-and-effect/effective_obfuscation) for the ID column,
578
593
  that column's filters and sorting will be automatically configured.
579
594
 
580
- Just define `table_column :id`.
595
+ Just define `table_column :id`
581
596
 
582
597
  Unfortunately, due to the effective_obfuscation algorithm, sorting and filtering by partial values is not supported.
583
598
 
@@ -588,7 +603,7 @@ So the column may not be sorted, and may only be filtered by typing the entire 1
588
603
  When working with an ActiveRecord collection that implements [effective_roles](https://github.com/code-and-effect/effective_roles),
589
604
  the filters and sorting will be automatically configured.
590
605
 
591
- Just define `table_column :roles`.
606
+ Just define `table_column :roles`
592
607
 
593
608
  The `EffectiveRoles.roles` collection will be used for the filter values, and sorting will be done by roles_mask.
594
609
 
@@ -680,10 +695,12 @@ In this example, a User belongs_to an Applicant. But instead of using the built
680
695
  module Effective
681
696
  module Datatables
682
697
  class Applicants < Effective::Datatable
683
- table_column :id, visible: true
698
+ datatable do
699
+ table_column :id, visible: true
684
700
 
685
- table_column :user, :type => :string, :column => 'users.email' do |applicant|
686
- link_to applicant.user.try(:email), edit_admin_user_path(applicant.user)
701
+ table_column :user, :type => :string, :column => 'users.email' do |applicant|
702
+ link_to applicant.user.try(:email), edit_admin_user_path(applicant.user)
703
+ end
687
704
  end
688
705
 
689
706
  def collection
@@ -14,7 +14,7 @@ initializeDataTables = ->
14
14
  pagingType: 'simple_numbers'
15
15
  language: { 'lengthMenu': 'Show _MENU_ per page'}
16
16
  lengthMenu: [[10, 25, 50, 100, 250, 1000, -1], ['10', '25', '50', '100', '250', '1000', 'All']]
17
- iDisplayLength: datatable.data('default-entries')
17
+ iDisplayLength: datatable.data('display-entries')
18
18
  columnDefs: [
19
19
  { visible: false, targets: datatable.data('non-visible') },
20
20
  { sortable: false, targets: datatable.data('non-sortable') }
@@ -4,7 +4,7 @@ module Effective
4
4
 
5
5
  # This will respond to both a GET and a POST
6
6
  def show
7
- @datatable = Effective::Datatable.find(params[:id], params[:attributes])
7
+ @datatable = find_datatable(params[:id]).try(:new, params[:attributes])
8
8
  @datatable.view = view_context if !@datatable.nil?
9
9
 
10
10
  EffectiveDatatables.authorized?(self, :index, @datatable.try(:collection_class) || Effective::Datatable)
@@ -24,6 +24,10 @@ module Effective
24
24
 
25
25
  private
26
26
 
27
+ def find_datatable(id)
28
+ "effective/datatables/#{id}".classify.tap { |klass| klass << 's' if id.to_s.end_with?('s') }.safe_constantize
29
+ end
30
+
27
31
  def error_json
28
32
  {
29
33
  :draw => params[:draw].to_i,
@@ -54,12 +54,7 @@ module EffectiveDatatablesHelper
54
54
  end
55
55
 
56
56
  def datatable_default_order(datatable)
57
- [
58
- if datatable.default_order.present?
59
- index = (datatable.table_columns.values.find { |options| options[:name] == datatable.default_order.keys.first.to_s }[:index] rescue nil)
60
- [index, datatable.default_order.values.first] if index.present?
61
- end || [0, 'asc']
62
- ].to_json()
57
+ [datatable.order_index, datatable.order_direction.downcase].to_json()
63
58
  end
64
59
 
65
60
  def datatable_widths(datatable)
@@ -4,77 +4,23 @@ module Effective
4
4
 
5
5
  delegate :render, :link_to, :mail_to, :to => :@view
6
6
 
7
- class << self
8
- def all
9
- EffectiveDatatables.datatables.map { |klass| klass.new() }
10
- end
11
-
12
- def find(obj, attributes = nil)
13
- obj = obj.respond_to?(:to_param) ? obj.to_param : obj
14
- EffectiveDatatables.datatables.find { |klass| klass.name.underscore.parameterize == obj }.try(:new, attributes.presence || {})
15
- end
16
-
17
- def table_column(name, options = {}, proc = nil, &block)
18
- if block_given?
19
- raise "You cannot use :partial => '' with the block syntax" if options[:partial]
20
- raise "You cannot use :proc => ... with the block syntax" if options[:proc]
21
- options[:block] = block
22
- end
23
- raise "You cannot use both :partial => '' and proc => ..." if options[:partial] && options[:proc]
24
-
25
- send(:attr_accessor, name)
26
- (@table_columns ||= HashWithIndifferentAccess.new())[name] = options
27
- end
28
-
29
- def table_columns(*names)
30
- names.each { |name| table_column(name) }
31
- end
32
-
33
- def array_column(name, options = {}, proc = nil, &block)
34
- table_column(name, options.merge!({:array_column => true}), proc, &block)
35
- end
36
-
37
- def actions_column(options = {}, proc = nil, &block)
38
- show = options.fetch(:show, false)
39
- edit = options.fetch(:edit, true)
40
- destroy = options.fetch(:destroy, true)
41
- name = options.fetch(:name, 'actions')
42
-
43
- opts = {
44
- sortable: false,
45
- filter: false,
46
- partial_local: :resource,
47
- partial_locals: { show_action: show, edit_action: edit, destroy_action: destroy }
48
- }
49
- opts[:partial] = '/effective/datatables/actions_column' unless (block_given? || proc.present?)
50
-
51
- table_column(name, opts, proc, &block)
52
- end
53
-
54
- def array_columns(*names)
55
- names.each { |name| array_column(name) }
56
- end
57
-
58
- def default_order(name, direction = :asc)
59
- @default_order = {name => direction}
60
- end
61
-
62
- def default_entries(entries)
63
- @default_entries = entries
64
- end
7
+ include Effective::EffectiveDatatable::Dsl
8
+ extend Effective::EffectiveDatatable::Dsl::ClassMethods
65
9
 
66
- def model_name # Searching & Filters
67
- @model_name ||= ActiveModel::Name.new(self)
68
- end
69
- end
10
+ include Effective::EffectiveDatatable::Ajax
11
+ include Effective::EffectiveDatatable::Options
12
+ include Effective::EffectiveDatatable::Rendering
70
13
 
71
14
  def initialize(*args)
72
- if args.present?
73
- raise 'Effective::Datatable.new() can only be called with a Hash like arguments' unless args.first.kind_of?(Hash)
15
+ if args.present? && args.first != nil
16
+ raise "#{self.class.name}.new() can only be initialized with a Hash like arguments" unless args.first.kind_of?(Hash)
74
17
  args.first.each { |k, v| self.attributes[k] = v }
75
18
  end
76
19
 
77
- unless active_record_collection? || (collection.kind_of?(Array) && collection.first.kind_of?(Array))
20
+ initialize_datatable # This creates @table_columns based on the DSL datatable do .. end block
21
+ initialize_options # This normalizes all the options
22
+
23
+ unless active_record_collection? || array_collection?
78
24
  raise "Unsupported collection type. Should be ActiveRecord class, ActiveRecord relation, or an Array of Arrays [[1, 'something'], [2, 'something else']]"
79
25
  end
80
26
 
@@ -82,6 +28,10 @@ module Effective
82
28
  search_terms.each { |column, term| self.send("#{column}=", term) }
83
29
  end
84
30
 
31
+ def table_columns
32
+ @table_columns
33
+ end
34
+
85
35
  # Any attributes set on initialize will be echoed back and available to the class
86
36
  def attributes
87
37
  @attributes ||= HashWithIndifferentAccess.new()
@@ -94,8 +44,12 @@ module Effective
94
44
  @model_name ||= ActiveModel::Name.new(self.class)
95
45
  end
96
46
 
47
+ def self.model_name # Searching & Filters
48
+ @model_name ||= ActiveModel::Name.new(self)
49
+ end
50
+
97
51
  def to_param
98
- self.class.name.underscore.parameterize
52
+ @to_param ||= self.class.name.underscore.sub('effective/datatables/', '')
99
53
  end
100
54
 
101
55
  def collection
@@ -106,30 +60,6 @@ module Effective
106
60
  collection.respond_to?(:klass) ? collection.klass : self.class
107
61
  end
108
62
 
109
- def finalize(collection) # Override me if you like
110
- collection
111
- end
112
-
113
- # Select only col[:if] == true columns, and then set the col[:index] accordingly
114
- def table_columns
115
- @table_columns ||= table_columns_with_defaults().select do |_, col|
116
- col[:if] == nil || (col[:if].respond_to?(:call) ? (view || self).instance_exec(&col[:if]) : col[:if])
117
- end.each_with_index { |(_, col), index| col[:index] = index }
118
- end
119
-
120
- # This is for the ColReorder plugin
121
- # It sends us a list of columns that are different than our table_columns order
122
- # So this method just returns an array of column names, as per ColReorder
123
- def display_table_columns
124
- if params[:columns].present?
125
- HashWithIndifferentAccess.new().tap do |display_columns|
126
- params[:columns].each do |_, values|
127
- display_columns[values[:name]] = table_columns[values[:name]]
128
- end
129
- end
130
- end
131
- end
132
-
133
63
  def to_json
134
64
  raise 'Effective::Datatable to_json called with a nil view. Please call render_datatable(@datatable) or @datatable.view = view before this method' unless view.present?
135
65
 
@@ -149,97 +79,16 @@ module Effective
149
79
  total_records.to_i == 0
150
80
  end
151
81
 
152
- def order_name
153
- @order_name ||= begin
154
- if params[:order] && params[:columns]
155
- order_column_index = (params[:order].first[1][:column] rescue '0')
156
- (params[:columns][order_column_index] || {})[:name]
157
- elsif default_order.present?
158
- default_order.keys.first
159
- end || table_columns.keys.first
160
- end
161
- end
162
-
163
- def order_direction
164
- @order_direction ||= if params[:order].present?
165
- params[:order].first[1][:dir] == 'desc' ? 'DESC' : 'ASC'
166
- elsif default_order.present?
167
- default_order.values.first.to_s.downcase == 'desc' ? 'DESC' : 'ASC'
168
- else
169
- 'ASC'
170
- end
171
- end
172
-
173
- def default_order
174
- self.class.instance_variable_get(:@default_order)
175
- end
176
-
177
- def default_entries
178
- @default_entries ||= begin
179
- entries = (self.class.instance_variable_get(:@default_entries).presence || EffectiveDatatables.default_entries)
180
- entries = -1 if entries.to_s.downcase == 'all'
181
- [10, 25, 50, 100, 250, 1000, -1].include?(entries) ? entries : 25
182
- end
183
- end
184
-
185
- def search_terms
186
- @search_terms ||= HashWithIndifferentAccess.new().tap do |terms|
187
- if params[:columns].present? # This is an AJAX request from the DataTable
188
- (params[:columns] || {}).each do |_, column|
189
- next if table_columns[column[:name]].blank? || (column[:search] || {})[:value].blank?
190
-
191
- terms[column[:name]] = column[:search][:value]
192
- end
193
- else # This is the initial render, and we have to apply default search terms only
194
- table_columns.each do |name, values|
195
- terms[name] = values[:filter][:selected] if values[:filter][:selected].present?
196
- end
197
- end
198
- end
199
- end
200
-
201
- # This is here so classes that inherit from Datatables can can override the specific where clauses on a search column
202
- def search_column(collection, table_column, search_term)
203
- if table_column[:array_column]
204
- array_tool.search_column_with_defaults(collection, table_column, search_term)
205
- else
206
- table_tool.search_column_with_defaults(collection, table_column, search_term)
207
- end
208
- end
209
-
210
- def per_page
211
- length = (params[:length].presence || default_entries).to_i
212
-
213
- if length == -1
214
- 9999999
215
- elsif length > 0
216
- length
217
- else
218
- 25
219
- end
220
- end
221
-
222
- def per_page=(length)
223
- case length
224
- when Integer
225
- params[:length] = length
226
- when :all
227
- params[:length] = -1
228
- end
229
- end
230
-
231
- def page
232
- params[:start].to_i / per_page + 1
233
- end
234
-
235
82
  def total_records
236
83
  @total_records ||= (
237
- if active_record_collection? && collection_class.connection.respond_to?(:unprepared_statement)
84
+ if active_record_collection?
238
85
  # https://github.com/rails/rails/issues/15331
239
- collection_sql = collection_class.connection.unprepared_statement { collection.to_sql }
240
- (collection_class.connection.execute("SELECT COUNT(*) FROM (#{collection_sql}) AS datatables_total_count").first['count'] rescue 1).to_i
241
- elsif active_record_collection?
242
- (collection_class.connection.execute("SELECT COUNT(*) FROM (#{collection.to_sql}) AS datatables_total_count").first['count'] rescue 1).to_i
86
+ if collection_class.connection.respond_to?(:unprepared_statement)
87
+ collection_sql = collection_class.connection.unprepared_statement { collection.to_sql }
88
+ (collection_class.connection.execute("SELECT COUNT(*) FROM (#{collection_sql}) AS datatables_total_count").first['count'] rescue 1).to_i
89
+ else
90
+ (collection_class.connection.execute("SELECT COUNT(*) FROM (#{collection.to_sql}) AS datatables_total_count").first['count'] rescue 1).to_i
91
+ end
243
92
  else
244
93
  collection.size
245
94
  end
@@ -268,116 +117,8 @@ module Effective
268
117
  @order_direction = nil
269
118
  end
270
119
 
271
-
272
120
  protected
273
121
 
274
- # So the idea here is that we want to do as much as possible on the database in ActiveRecord
275
- # And then run any array_columns through in post-processed results
276
- def table_data
277
- col = collection
278
-
279
- if active_record_collection?
280
- col = table_tool.order(col)
281
- col = table_tool.search(col)
282
-
283
- if table_tool.search_terms.present? && array_tool.search_terms.blank?
284
- if collection_class.connection.respond_to?(:unprepared_statement)
285
- # https://github.com/rails/rails/issues/15331
286
- col_sql = collection_class.connection.unprepared_statement { col.to_sql }
287
- self.display_records = (collection_class.connection.execute("SELECT COUNT(*) FROM (#{col_sql}) AS datatables_filtered_count").first['count'] rescue 1).to_i
288
- else
289
- self.display_records = (collection_class.connection.execute("SELECT COUNT(*) FROM (#{col.to_sql}) AS datatables_filtered_count").first['count'] rescue 1).to_i
290
- end
291
- end
292
- end
293
-
294
- if array_tool.search_terms.present?
295
- col = self.arrayize(col)
296
- col = array_tool.search(col)
297
- self.display_records = col.size
298
- end
299
-
300
- if array_tool.order_column.present?
301
- col = self.arrayize(col)
302
- col = array_tool.order(col)
303
- end
304
-
305
- self.display_records ||= total_records
306
-
307
- if col.kind_of?(Array)
308
- col = array_tool.paginate(col)
309
- else
310
- col = table_tool.paginate(col)
311
- col = self.arrayize(col)
312
- end
313
-
314
- col = self.finalize(col)
315
- end
316
-
317
- def arrayize(collection)
318
- return collection if @arrayized # Prevent the collection from being arrayized more than once
319
- @arrayized = true
320
-
321
- # We want to use the render :collection for each column that renders partials
322
- rendered = {}
323
- table_columns.each do |name, opts|
324
- if opts[:partial]
325
- locals = {
326
- datatable: self,
327
- table_column: table_columns[name],
328
- controller_namespace: view.controller_path.split('/')[0...-1].map { |path| path.downcase.to_sym if path.present? }.compact,
329
- show_action: (opts[:partial_locals] || {})[:show_action],
330
- edit_action: (opts[:partial_locals] || {})[:edit_action],
331
- destroy_action: (opts[:partial_locals] || {})[:destroy_action]
332
- }
333
- locals.merge!(opts[:partial_locals]) if opts[:partial_locals]
334
-
335
- rendered[name] = (render(
336
- :partial => opts[:partial],
337
- :as => opts[:partial_local],
338
- :collection => collection,
339
- :formats => :html,
340
- :locals => locals,
341
- :spacer_template => '/effective/datatables/spacer_template',
342
- ) || '').split('EFFECTIVEDATATABLESSPACER')
343
- end
344
- end
345
-
346
- collection.each_with_index.map do |obj, index|
347
- (display_table_columns || table_columns).map do |name, opts|
348
- value = if opts[:partial]
349
- rendered[name][index]
350
- elsif opts[:block]
351
- view.instance_exec(obj, collection, self, &opts[:block])
352
- elsif opts[:proc]
353
- view.instance_exec(obj, collection, self, &opts[:proc])
354
- elsif opts[:type] == :belongs_to
355
- val = (obj.send(name) rescue nil).to_s
356
- elsif opts[:type] == :obfuscated_id
357
- (obj.send(:to_param) rescue nil).to_s
358
- elsif opts[:type] == :effective_roles
359
- (obj.send(:roles) rescue []).join(', ')
360
- else
361
- val = (obj.send(name) rescue nil)
362
- val = (obj[opts[:array_index]] rescue nil) if val == nil
363
- val
364
- end
365
-
366
- # Last minute formatting of dates
367
- case value
368
- when Date
369
- value.strftime(EffectiveDatatables.date_format)
370
- when Time, DateTime
371
- value.strftime(EffectiveDatatables.datetime_format)
372
- else
373
- value.to_s
374
- end
375
- end
376
- end
377
- end
378
-
379
- private
380
-
381
122
  def params
382
123
  view.try(:params) || HashWithIndifferentAccess.new()
383
124
  end
@@ -391,110 +132,11 @@ module Effective
391
132
  end
392
133
 
393
134
  def active_record_collection?
394
- if @active_record_collection.nil?
395
- @active_record_collection = (collection.ancestors.include?(ActiveRecord::Base) rescue false)
396
- else
397
- @active_record_collection
398
- end
399
- end
400
-
401
- def table_columns_with_defaults
402
- unless self.class.instance_variable_get(:@table_columns_initialized)
403
- self.class.instance_variable_set(:@table_columns_initialized, true)
404
- initalize_table_columns(self.class.instance_variable_get(:@table_columns))
405
- end
406
-
407
- self.class.instance_variable_get(:@table_columns)
408
- end
409
-
410
- def initalize_table_columns(cols)
411
- sql_table = (collection.table rescue nil)
412
-
413
- # Here we identify all belongs_to associations and build up a Hash like:
414
- # {:user => {:foreign_key => 'user_id', :klass => User}, :order => {:foreign_key => 'order_id', :klass => Effective::Order}}
415
- belong_tos = (collection.ancestors.first.reflect_on_all_associations(:belongs_to) rescue []).inject(HashWithIndifferentAccess.new()) do |retval, bt|
416
- unless bt.options[:polymorphic]
417
- begin
418
- klass = bt.klass || bt.foreign_type.sub('_type', '').classify.constantize
419
- rescue => e
420
- klass = nil
421
- end
422
-
423
- retval[bt.name] = {:foreign_key => bt.foreign_key, :klass => klass} if bt.foreign_key.present? && klass.present?
424
- end
425
-
426
- retval
427
- end
428
-
429
- cols.each_with_index do |(name, _), index|
430
- # If this is a belongs_to, add an :if clause specifying a collection scope if
431
- if belong_tos.key?(name)
432
- cols[name][:if] ||= Proc.new { attributes[belong_tos[name][:foreign_key]].blank? }
433
- end
434
-
435
- sql_column = (collection.columns rescue []).find do |column|
436
- column.name == name.to_s || (belong_tos.key?(name) && column.name == belong_tos[name][:foreign_key])
437
- end
438
-
439
- cols[name][:array_column] ||= false
440
- cols[name][:array_index] = index # The index of this column in the collection, regardless of hidden table_columns
441
- cols[name][:name] ||= name
442
- cols[name][:label] ||= name.titleize
443
- cols[name][:column] ||= (sql_table && sql_column) ? "\"#{sql_table.name}\".\"#{sql_column.name}\"" : name
444
- cols[name][:width] ||= nil
445
- cols[name][:sortable] = true if cols[name][:sortable] == nil
446
- cols[name][:type] ||= (belong_tos.key?(name) ? :belongs_to : sql_column.try(:type).presence) || :string
447
- cols[name][:class] = "col-#{cols[name][:type]} col-#{name} #{cols[name][:class]}".strip
448
-
449
- # EffectiveObfuscation
450
- if name == 'id' && defined?(EffectiveObfuscation) && collection.respond_to?(:deobfuscate)
451
- cols[name][:sortable] = false
452
- cols[name][:type] = :obfuscated_id
453
- end
454
-
455
- # EffectiveRoles, if you do table_column :roles, everything just works
456
- if name == 'roles' && defined?(EffectiveRoles) && collection.respond_to?(:with_role)
457
- cols[name][:sortable] = true
458
- cols[name][:column] = sql_table.present? ? "\"#{sql_table.name}\".\"roles_mask\"" : name
459
- cols[name][:type] = :effective_roles
460
- end
461
-
462
- if sql_table.present? && sql_column.blank? # This is a SELECT AS column
463
- cols[name][:sql_as_column] = true
464
- end
465
-
466
- cols[name][:filter] = initialize_table_column_filter(cols[name][:filter], cols[name][:type], belong_tos[name])
467
-
468
- if cols[name][:partial]
469
- cols[name][:partial_local] ||= (sql_table.try(:name) || cols[name][:partial].split('/').last(2).first.presence || 'obj').singularize.to_sym
470
- end
471
- end
135
+ collection.ancestors.include?(ActiveRecord::Base) rescue false
472
136
  end
473
137
 
474
- def initialize_table_column_filter(filter, col_type, belongs_to)
475
- return {type: :null} if filter == false
476
-
477
- filter = {type: filter.to_sym} if filter.kind_of?(String)
478
- filter = {} unless filter.kind_of?(Hash)
479
-
480
- # This is a fix for passing filter[:selected] == false, it needs to be 'false'
481
- filter[:selected] = filter[:selected].to_s unless filter[:selected].nil?
482
-
483
- case col_type
484
- when :belongs_to
485
- {
486
- type: :select,
487
- values: Proc.new { belongs_to[:klass].all.map { |obj| [obj.to_s, obj.id] }.sort { |x, y| x[1] <=> y[1] } }
488
- }
489
- when :effective_roles
490
- {type: :select, values: EffectiveRoles.roles}
491
- when :integer
492
- {type: :number}
493
- when :boolean
494
- {type: :boolean, values: [true, false]}
495
- else
496
- {type: :string}
497
- end.merge(filter.symbolize_keys)
138
+ def array_collection?
139
+ collection.kind_of?(Array) && collection.first.kind_of?(Array)
498
140
  end
499
141
 
500
142
  end
@@ -0,0 +1,105 @@
1
+ # This is extended as class level into Datatable
2
+
3
+ module Effective
4
+ module EffectiveDatatable
5
+ module Ajax
6
+
7
+ # This is for the ColReorder plugin
8
+ # It sends us a list of columns that are different than our table_columns order
9
+ # So this method just returns an array of column names, as per ColReorder
10
+ def display_table_columns
11
+ if params[:columns].present?
12
+ HashWithIndifferentAccess.new().tap do |display_columns|
13
+ params[:columns].each do |_, values|
14
+ display_columns[values[:name]] = table_columns[values[:name]]
15
+ end
16
+ end
17
+ end
18
+ end
19
+
20
+ def order_name
21
+ @order_name ||= begin
22
+ if params[:order] && params[:columns]
23
+ order_column_index = (params[:order].first[1][:column] rescue '0')
24
+ (params[:columns][order_column_index] || {})[:name]
25
+ elsif @default_order.present?
26
+ @default_order.keys.first
27
+ end || table_columns.keys.first
28
+ end
29
+ end
30
+
31
+ def order_index
32
+ (table_columns[order_name][:index] || 0) rescue 0
33
+ end
34
+
35
+ def order_direction
36
+ @order_direction ||= if params[:order].present?
37
+ params[:order].first[1][:dir] == 'desc' ? 'DESC' : 'ASC'
38
+ elsif @default_order.present?
39
+ @default_order.values.first.to_s.downcase == 'desc' ? 'DESC' : 'ASC'
40
+ else
41
+ 'ASC'
42
+ end
43
+ end
44
+
45
+ def display_entries
46
+ @display_entries ||= begin
47
+ entries = (@default_entries.presence || EffectiveDatatables.default_entries)
48
+ entries = -1 if entries.to_s.downcase == 'all'
49
+ [10, 25, 50, 100, 250, 1000, -1].include?(entries) ? entries : 25
50
+ end
51
+ end
52
+
53
+ def search_terms
54
+ @search_terms ||= HashWithIndifferentAccess.new().tap do |terms|
55
+ if params[:columns].present? # This is an AJAX request from the DataTable
56
+ (params[:columns] || {}).each do |_, column|
57
+ next if table_columns[column[:name]].blank? || (column[:search] || {})[:value].blank?
58
+
59
+ terms[column[:name]] = column[:search][:value]
60
+ end
61
+ else # This is the initial render, and we have to apply default search terms only
62
+ table_columns.each do |name, values|
63
+ terms[name] = values[:filter][:selected] if values[:filter][:selected].present?
64
+ end
65
+ end
66
+ end
67
+ end
68
+
69
+ # This is here so classes that inherit from Datatables can can override the specific where clauses on a search column
70
+ def search_column(collection, table_column, search_term)
71
+ if table_column[:array_column]
72
+ array_tool.search_column_with_defaults(collection, table_column, search_term)
73
+ else
74
+ table_tool.search_column_with_defaults(collection, table_column, search_term)
75
+ end
76
+ end
77
+
78
+ def per_page
79
+ length = (params[:length].presence || display_entries).to_i
80
+
81
+ if length == -1
82
+ 9999999
83
+ elsif length > 0
84
+ length
85
+ else
86
+ 25
87
+ end
88
+ end
89
+
90
+ def per_page=(length)
91
+ case length
92
+ when Integer
93
+ params[:length] = length
94
+ when :all
95
+ params[:length] = -1
96
+ end
97
+ end
98
+
99
+ def page
100
+ params[:start].to_i / per_page + 1
101
+ end
102
+
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,57 @@
1
+ # This is extended as class level into Datatable
2
+
3
+ module Effective
4
+ module EffectiveDatatable
5
+ module Dsl
6
+
7
+ module ClassMethods
8
+ def datatable(&block)
9
+ define_method('initialize_datatable') { instance_exec(&block) }
10
+ end
11
+ end
12
+
13
+ # Instance Methods inside the datatable do .. end block
14
+ def default_order(name, direction = :asc)
15
+ @default_order = {name => direction}
16
+ end
17
+
18
+ def default_entries(entries)
19
+ @default_entries = entries
20
+ end
21
+
22
+ def table_column(name, options = {}, proc = nil, &block)
23
+ if block_given?
24
+ raise "You cannot use partial: ... with the block syntax" if options[:partial]
25
+ raise "You cannot use proc: ... with the block syntax" if options[:proc]
26
+ options[:block] = block
27
+ end
28
+ raise "You cannot use both partial: ... and proc: ..." if options[:partial] && options[:proc]
29
+
30
+ self.class.send(:attr_accessor, name)
31
+ (@table_columns ||= HashWithIndifferentAccess.new())[name] = options
32
+ end
33
+
34
+ def array_column(name, options = {}, proc = nil, &block)
35
+ table_column(name, options.merge!(array_column: true), proc, &block)
36
+ end
37
+
38
+ def actions_column(options = {}, proc = nil, &block)
39
+ show = options.fetch(:show, false)
40
+ edit = options.fetch(:edit, true)
41
+ destroy = options.fetch(:destroy, true)
42
+ name = options.fetch(:name, 'actions')
43
+
44
+ opts = {
45
+ sortable: false,
46
+ filter: false,
47
+ partial_local: :resource,
48
+ partial_locals: { show_action: show, edit_action: edit, destroy_action: destroy }
49
+ }
50
+ opts[:partial] = '/effective/datatables/actions_column' unless (block_given? || proc.present?)
51
+
52
+ table_column(name, opts, proc, &block)
53
+ end
54
+
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,113 @@
1
+ # This is extended as class level into Datatable
2
+
3
+ module Effective
4
+ module EffectiveDatatable
5
+ module Options
6
+
7
+ def initialize_options
8
+ @table_columns = initialize_column_options(@table_columns)
9
+ end
10
+
11
+ protected
12
+
13
+ def initialize_column_options(cols)
14
+ sql_table = (collection.table rescue nil)
15
+
16
+ # Here we identify all belongs_to associations and build up a Hash like:
17
+ # {user: {foreign_key: 'user_id', klass: User}, order: {foreign_key: 'order_id', klass: Effective::Order}}
18
+ belong_tos = (collection.ancestors.first.reflect_on_all_associations(:belongs_to) rescue []).inject(HashWithIndifferentAccess.new()) do |retval, bt|
19
+ unless bt.options[:polymorphic]
20
+ begin
21
+ klass = bt.klass || bt.foreign_type.sub('_type', '').classify.constantize
22
+ rescue => e
23
+ klass = nil
24
+ end
25
+
26
+ retval[bt.name] = {foreign_key: bt.foreign_key, klass: klass} if bt.foreign_key.present? && klass.present?
27
+ end
28
+
29
+ retval
30
+ end
31
+
32
+ table_columns = cols.each_with_index do |(name, _), index|
33
+ # If this is a belongs_to, add an :if clause specifying a collection scope if
34
+ if belong_tos.key?(name)
35
+ cols[name][:if] ||= Proc.new { attributes[belong_tos[name][:foreign_key]].blank? }
36
+ end
37
+
38
+ sql_column = (collection.columns rescue []).find do |column|
39
+ column.name == name.to_s || (belong_tos.key?(name) && column.name == belong_tos[name][:foreign_key])
40
+ end
41
+
42
+ cols[name][:array_column] ||= false
43
+ cols[name][:array_index] = index # The index of this column in the collection, regardless of hidden table_columns
44
+ cols[name][:name] ||= name
45
+ cols[name][:label] ||= name.titleize
46
+ cols[name][:column] ||= (sql_table && sql_column) ? "\"#{sql_table.name}\".\"#{sql_column.name}\"" : name
47
+ cols[name][:width] ||= nil
48
+ cols[name][:sortable] = true if cols[name][:sortable] == nil
49
+ cols[name][:type] ||= (belong_tos.key?(name) ? :belongs_to : sql_column.try(:type).presence) || :string
50
+ cols[name][:class] = "col-#{cols[name][:type]} col-#{name} #{cols[name][:class]}".strip
51
+
52
+ # EffectiveObfuscation
53
+ if name == 'id' && defined?(EffectiveObfuscation) && collection.respond_to?(:deobfuscate)
54
+ cols[name][:sortable] = false
55
+ cols[name][:type] = :obfuscated_id
56
+ end
57
+
58
+ # EffectiveRoles, if you do table_column :roles, everything just works
59
+ if name == 'roles' && defined?(EffectiveRoles) && collection.respond_to?(:with_role)
60
+ cols[name][:sortable] = true
61
+ cols[name][:column] = sql_table.present? ? "\"#{sql_table.name}\".\"roles_mask\"" : name
62
+ cols[name][:type] = :effective_roles
63
+ end
64
+
65
+ if sql_table.present? && sql_column.blank? # This is a SELECT AS column
66
+ cols[name][:sql_as_column] = true
67
+ end
68
+
69
+ cols[name][:filter] = initialize_table_column_filter(cols[name][:filter], cols[name][:type], belong_tos[name])
70
+
71
+ if cols[name][:partial]
72
+ cols[name][:partial_local] ||= (sql_table.try(:name) || cols[name][:partial].split('/').last(2).first.presence || 'obj').singularize.to_sym
73
+ end
74
+ end
75
+
76
+ # After everything is initialized
77
+ # Compute any col[:if] and assign an index
78
+ table_columns.select do |_, col|
79
+ col[:if] == nil || (col[:if].respond_to?(:call) ? (view || self).instance_exec(&col[:if]) : col[:if])
80
+ end.each_with_index { |(_, col), index| col[:index] = index }
81
+
82
+ end
83
+
84
+ def initialize_table_column_filter(filter, col_type, belongs_to)
85
+ return {type: :null} if filter == false
86
+
87
+ filter = {type: filter.to_sym} if filter.kind_of?(String)
88
+ filter = {} unless filter.kind_of?(Hash)
89
+
90
+ # This is a fix for passing filter[:selected] == false, it needs to be 'false'
91
+ filter[:selected] = filter[:selected].to_s unless filter[:selected].nil?
92
+
93
+ case col_type
94
+ when :belongs_to
95
+ {
96
+ type: :select,
97
+ values: Proc.new { belongs_to[:klass].all.map { |obj| [obj.to_s, obj.id] }.sort { |x, y| x[1] <=> y[1] } }
98
+ }
99
+ when :effective_roles
100
+ {type: :select, values: EffectiveRoles.roles}
101
+ when :integer
102
+ {type: :number}
103
+ when :boolean
104
+ {type: :boolean, values: [true, false]}
105
+ else
106
+ {type: :string}
107
+ end.merge(filter.symbolize_keys)
108
+ end
109
+
110
+
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,120 @@
1
+ # This is extended as class level into Datatable
2
+
3
+ module Effective
4
+ module EffectiveDatatable
5
+ module Rendering
6
+
7
+ def finalize(collection) # Override me if you like
8
+ collection
9
+ end
10
+
11
+ protected
12
+
13
+ # So the idea here is that we want to do as much as possible on the database in ActiveRecord
14
+ # And then run any array_columns through in post-processed results
15
+ def table_data
16
+ col = collection
17
+
18
+ if active_record_collection?
19
+ col = table_tool.order(col)
20
+ col = table_tool.search(col)
21
+
22
+ if table_tool.search_terms.present? && array_tool.search_terms.blank?
23
+ if collection_class.connection.respond_to?(:unprepared_statement)
24
+ # https://github.com/rails/rails/issues/15331
25
+ col_sql = collection_class.connection.unprepared_statement { col.to_sql }
26
+ self.display_records = (collection_class.connection.execute("SELECT COUNT(*) FROM (#{col_sql}) AS datatables_filtered_count").first['count'] rescue 1).to_i
27
+ else
28
+ self.display_records = (collection_class.connection.execute("SELECT COUNT(*) FROM (#{col.to_sql}) AS datatables_filtered_count").first['count'] rescue 1).to_i
29
+ end
30
+ end
31
+ end
32
+
33
+ if array_tool.search_terms.present?
34
+ col = self.arrayize(col)
35
+ col = array_tool.search(col)
36
+ self.display_records = col.size
37
+ end
38
+
39
+ if array_tool.order_column.present?
40
+ col = self.arrayize(col)
41
+ col = array_tool.order(col)
42
+ end
43
+
44
+ self.display_records ||= total_records
45
+
46
+ if col.kind_of?(Array)
47
+ col = array_tool.paginate(col)
48
+ else
49
+ col = table_tool.paginate(col)
50
+ col = self.arrayize(col)
51
+ end
52
+
53
+ col = self.finalize(col)
54
+ end
55
+
56
+ def arrayize(collection)
57
+ return collection if @arrayized # Prevent the collection from being arrayized more than once
58
+ @arrayized = true
59
+
60
+ # We want to use the render :collection for each column that renders partials
61
+ rendered = {}
62
+ table_columns.each do |name, opts|
63
+ if opts[:partial]
64
+ locals = {
65
+ datatable: self,
66
+ table_column: table_columns[name],
67
+ controller_namespace: view.controller_path.split('/')[0...-1].map { |path| path.downcase.to_sym if path.present? }.compact,
68
+ show_action: (opts[:partial_locals] || {})[:show_action],
69
+ edit_action: (opts[:partial_locals] || {})[:edit_action],
70
+ destroy_action: (opts[:partial_locals] || {})[:destroy_action]
71
+ }
72
+ locals.merge!(opts[:partial_locals]) if opts[:partial_locals]
73
+
74
+ rendered[name] = (render(
75
+ :partial => opts[:partial],
76
+ :as => opts[:partial_local],
77
+ :collection => collection,
78
+ :formats => :html,
79
+ :locals => locals,
80
+ :spacer_template => '/effective/datatables/spacer_template',
81
+ ) || '').split('EFFECTIVEDATATABLESSPACER')
82
+ end
83
+ end
84
+
85
+ collection.each_with_index.map do |obj, index|
86
+ (display_table_columns || table_columns).map do |name, opts|
87
+ value = if opts[:partial]
88
+ rendered[name][index]
89
+ elsif opts[:block]
90
+ view.instance_exec(obj, collection, self, &opts[:block])
91
+ elsif opts[:proc]
92
+ view.instance_exec(obj, collection, self, &opts[:proc])
93
+ elsif opts[:type] == :belongs_to
94
+ val = (obj.send(name) rescue nil).to_s
95
+ elsif opts[:type] == :obfuscated_id
96
+ (obj.send(:to_param) rescue nil).to_s
97
+ elsif opts[:type] == :effective_roles
98
+ (obj.send(:roles) rescue []).join(', ')
99
+ else
100
+ val = (obj.send(name) rescue nil)
101
+ val = (obj[opts[:array_index]] rescue nil) if val == nil
102
+ val
103
+ end
104
+
105
+ # Last minute formatting of dates
106
+ case value
107
+ when Date
108
+ value.strftime(EffectiveDatatables.date_format)
109
+ when Time, DateTime
110
+ value.strftime(EffectiveDatatables.datetime_format)
111
+ else
112
+ value.to_s
113
+ end
114
+ end
115
+ end
116
+ end
117
+
118
+ end # / Rendering
119
+ end
120
+ end
@@ -10,5 +10,5 @@
10
10
 
11
11
  - if destroy_action
12
12
  - url = (polymorphic_path([*controller_namespace, resource]) rescue nil) || (polymorphic_path(resource) rescue nil)
13
- %a{href: (polymorphic_path([*controller_namespace, resource]) rescue '#'), title: 'Delete', data: {method: :delete, confirm: 'Are you sure?'}}
13
+ %a{href: (url || '#'), title: 'Delete', data: {method: :delete, confirm: 'Are you sure?'}}
14
14
  %span.glyphicon.glyphicon-trash
@@ -14,7 +14,7 @@
14
14
  'non-visible' => datatable_non_visible(datatable),
15
15
  'widths' => datatable_widths(datatable),
16
16
  'default-order' => datatable_default_order(datatable),
17
- 'default-entries' => datatable.default_entries,
17
+ 'display-entries' => datatable.display_entries,
18
18
  'display-records' => (datatable.to_json[:recordsFiltered] || 0),
19
19
  'total-records' => (datatable.to_json[:recordsTotal] || 0),
20
20
  'column-classes' => datatable_column_classes(datatable)
@@ -20,15 +20,4 @@ module EffectiveDatatables
20
20
  true
21
21
  end
22
22
 
23
- def self.datatables
24
- Rails.env.development? ? read_datatables : (@@datatables ||= read_datatables)
25
- end
26
-
27
- private
28
-
29
- def self.read_datatables
30
- Rails.application.eager_load! unless Rails.configuration.cache_classes
31
- Effective::Datatable.descendants.map { |klass| klass }.compact
32
- end
33
-
34
23
  end
@@ -1,3 +1,3 @@
1
1
  module EffectiveDatatables
2
- VERSION = '1.8.3'.freeze
2
+ VERSION = '2.0.0'.freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: effective_datatables
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.8.3
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Code and Effect
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-07-06 00:00:00.000000000 Z
11
+ date: 2015-07-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -123,6 +123,10 @@ files:
123
123
  - app/models/effective/active_record_datatable_tool.rb
124
124
  - app/models/effective/array_datatable_tool.rb
125
125
  - app/models/effective/datatable.rb
126
+ - app/models/effective/effective_datatable/ajax.rb
127
+ - app/models/effective/effective_datatable/dsl.rb
128
+ - app/models/effective/effective_datatable/options.rb
129
+ - app/models/effective/effective_datatable/rendering.rb
126
130
  - app/views/effective/datatables/_actions_column.html.haml
127
131
  - app/views/effective/datatables/_datatable.html.haml
128
132
  - app/views/effective/datatables/_spacer_template.html