effective_datatables 1.8.3 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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