effective_datatables 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +581 -0
  4. data/Rakefile +23 -0
  5. data/app/assets/images/effective_datatables/copy_csv_xls_pdf.swf +0 -0
  6. data/app/assets/javascripts/effective_datatables.bootstrap2.js +11 -0
  7. data/app/assets/javascripts/effective_datatables.js +12 -0
  8. data/app/assets/javascripts/effective_datatables/initialize.js.coffee +65 -0
  9. data/app/assets/javascripts/vendor/jquery.dataTables.columnFilter.js +829 -0
  10. data/app/assets/stylesheets/effective_datatables.bootstrap2.css.scss +8 -0
  11. data/app/assets/stylesheets/effective_datatables.css.scss +8 -0
  12. data/app/assets/stylesheets/effective_datatables/_overrides.scss +72 -0
  13. data/app/controllers/effective/datatables_controller.rb +36 -0
  14. data/app/helpers/effective_datatables_helper.rb +74 -0
  15. data/app/models/effective/access_denied.rb +17 -0
  16. data/app/models/effective/active_record_datatable_tool.rb +87 -0
  17. data/app/models/effective/array_datatable_tool.rb +66 -0
  18. data/app/models/effective/datatable.rb +352 -0
  19. data/app/views/effective/datatables/_datatable.html.haml +11 -0
  20. data/app/views/effective/datatables/_spacer_template.html +1 -0
  21. data/config/routes.rb +11 -0
  22. data/lib/effective_datatables.rb +32 -0
  23. data/lib/effective_datatables/engine.rb +20 -0
  24. data/lib/effective_datatables/version.rb +3 -0
  25. data/lib/generators/effective_datatables/install_generator.rb +17 -0
  26. data/lib/generators/templates/README +1 -0
  27. data/lib/generators/templates/effective_datatables.rb +23 -0
  28. data/lib/tasks/effective_datatables_tasks.rake +17 -0
  29. data/spec/dummy/README.rdoc +261 -0
  30. data/spec/dummy/Rakefile +7 -0
  31. data/spec/dummy/app/assets/javascripts/application.js +15 -0
  32. data/spec/dummy/app/assets/stylesheets/application.css +13 -0
  33. data/spec/dummy/app/controllers/application_controller.rb +3 -0
  34. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  35. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  36. data/spec/dummy/config.ru +4 -0
  37. data/spec/dummy/config/application.rb +59 -0
  38. data/spec/dummy/config/boot.rb +10 -0
  39. data/spec/dummy/config/database.yml +25 -0
  40. data/spec/dummy/config/environment.rb +5 -0
  41. data/spec/dummy/config/environments/development.rb +37 -0
  42. data/spec/dummy/config/environments/production.rb +67 -0
  43. data/spec/dummy/config/environments/test.rb +37 -0
  44. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  45. data/spec/dummy/config/initializers/inflections.rb +15 -0
  46. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  47. data/spec/dummy/config/initializers/secret_token.rb +7 -0
  48. data/spec/dummy/config/initializers/session_store.rb +8 -0
  49. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  50. data/spec/dummy/config/locales/en.yml +5 -0
  51. data/spec/dummy/config/routes.rb +58 -0
  52. data/spec/dummy/db/development.sqlite3 +0 -0
  53. data/spec/dummy/db/schema.rb +16 -0
  54. data/spec/dummy/db/test.sqlite3 +0 -0
  55. data/spec/dummy/log/development.log +17 -0
  56. data/spec/dummy/log/test.log +1 -0
  57. data/spec/dummy/public/404.html +26 -0
  58. data/spec/dummy/public/422.html +26 -0
  59. data/spec/dummy/public/500.html +25 -0
  60. data/spec/dummy/public/favicon.ico +0 -0
  61. data/spec/dummy/script/rails +6 -0
  62. data/spec/effective_datatables_spec.rb +7 -0
  63. data/spec/spec_helper.rb +34 -0
  64. data/spec/support/factories.rb +1 -0
  65. metadata +217 -0
@@ -0,0 +1,8 @@
1
+ /*
2
+ *= require dataTables/jquery.dataTables
3
+ *= require dataTables/bootstrap/2/jquery.dataTables.bootstrap
4
+ *= require dataTables/extras/dataTables.tableTools
5
+ *= require dataTables/extras/dataTables.colVis
6
+ */
7
+
8
+ @import 'effective_datatables/overrides';
@@ -0,0 +1,8 @@
1
+ /*
2
+ *= require dataTables/jquery.dataTables
3
+ *= require dataTables/bootstrap/3/jquery.dataTables.bootstrap
4
+ *= require dataTables/extras/dataTables.tableTools
5
+ *= require dataTables/extras/dataTables.colVis
6
+ */
7
+
8
+ @import 'effective_datatables/overrides';
@@ -0,0 +1,72 @@
1
+ .dataTables_wrapper { // This is the Header
2
+ overflow-x: auto;
3
+
4
+ .row {
5
+ margin-left: 0px;
6
+ margin-right: 0px;
7
+ }
8
+
9
+ table.dataTable.sorting-hidden {
10
+ thead {
11
+ .sorting_asc { background: none; cursor: default; }
12
+ .sorting_desc { background: none; cursor: default; }
13
+ .sorting { cursor: default; }
14
+
15
+ tr, th {
16
+ padding-left: 8px;
17
+ padding-right: 8px;
18
+ }
19
+ }
20
+ }
21
+
22
+ table.dataTable {
23
+ border-collapse: collapse;
24
+ box-sizing: border-box;
25
+
26
+ .form-inline, .form-control { width: 100%; }
27
+ thead, tr, th { padding-left: 6px; }
28
+ }
29
+
30
+ .dataTables_processing {
31
+ height: 60px;
32
+ }
33
+
34
+ .dataTables_paginate {
35
+ overflow: hidden;
36
+
37
+ .pagination {
38
+ padding: 0px;
39
+
40
+ .paginate_button {
41
+ padding: 0px;
42
+
43
+ &:hover {
44
+ background: none;
45
+ border: 1px solid transparent;
46
+ }
47
+
48
+ &:active {
49
+ background: none;
50
+ box-shadow: none;
51
+ outline: none;
52
+ }
53
+ }
54
+ }
55
+ }
56
+
57
+ .DTTT_container {
58
+ float: none;
59
+ width: 190px;
60
+ margin: auto;
61
+ }
62
+ }
63
+
64
+ ul.ColVis_collection {
65
+ width: 240px;
66
+ }
67
+
68
+ div.DTTT_print_info {
69
+ h6 { color: black; }
70
+ p { color: black; }
71
+ }
72
+
@@ -0,0 +1,36 @@
1
+ module Effective
2
+ class DatatablesController < ApplicationController
3
+ skip_log_page_views if defined?(EffectiveLogging)
4
+
5
+ def show
6
+ @datatable = Effective::Datatable.find(params[:id], params[:attributes])
7
+ @datatable.view = view_context if @datatable.present?
8
+
9
+ EffectiveDatatables.authorized?(self, :index, @datatable.collection_class)
10
+
11
+ respond_to do |format|
12
+ format.html
13
+ format.json {
14
+ if Rails.env.production?
15
+ render :json => (@datatable.to_json rescue error_json)
16
+ else
17
+ render :json => @datatable.to_json
18
+ end
19
+ }
20
+ end
21
+
22
+ end
23
+
24
+ private
25
+
26
+ def error_json
27
+ {
28
+ :sEcho => params[:sEcho].to_i,
29
+ :aaData => [],
30
+ :iTotalRecords => 0,
31
+ :iTotalDisplayRecords => 0,
32
+ }.to_json
33
+ end
34
+
35
+ end
36
+ end
@@ -0,0 +1,74 @@
1
+ module EffectiveDatatablesHelper
2
+ def render_datatable(datatable, opts = {})
3
+ datatable.view = self
4
+ locals = {:style => :full, :filterable => true, :sortable => true, :table_class => 'table-bordered table-striped'}.merge(opts)
5
+ locals[:table_class] = 'sorting-hidden ' + locals[:table_class].to_s if locals[:sortable] == false
6
+
7
+ render :partial => 'effective/datatables/datatable', :locals => locals.merge(:datatable => datatable)
8
+ end
9
+
10
+ def render_simple_datatable(datatable, opts = {})
11
+ datatable.view = self
12
+ locals = {:style => :simple, :filterable => false, :sortable => false, :table_class => ''}.merge(opts)
13
+ locals[:table_class] = 'sorting-hidden ' + locals[:table_class].to_s if locals[:sortable] == false
14
+
15
+ render :partial => 'effective/datatables/datatable', :locals => locals.merge(:datatable => datatable)
16
+ end
17
+
18
+ def datatable_filter(datatable, filterable = true)
19
+ return false unless filterable
20
+
21
+ filters = datatable.table_columns.values.map { |options, _| options[:filter] || {:type => 'null'} }
22
+
23
+ # Process any Procs
24
+ filters.each do |filter|
25
+ if filter[:values].respond_to?(:call)
26
+ filter[:values] = filter[:values].call()
27
+
28
+ if filter[:values].kind_of?(ActiveRecord::Relation) || (filter[:values].kind_of?(Array) && filter[:values].first.kind_of?(ActiveRecord::Base))
29
+ filter[:values] = filter[:values].map { |obj| [obj.id, obj.to_s] }
30
+ end
31
+ end
32
+ end
33
+
34
+ filters.to_json()
35
+ end
36
+
37
+ def datatable_non_sortable(datatable, sortable = true)
38
+ [].tap do |nonsortable|
39
+ datatable.table_columns.values.each_with_index { |options, x| nonsortable << x if options[:sortable] == false || sortable == false }
40
+ end.to_json()
41
+ end
42
+
43
+ def datatable_non_visible(datatable)
44
+ [].tap do |nonvisible|
45
+ datatable.table_columns.values.each_with_index do |options, x|
46
+ visible = (options[:visible].respond_to?(:call) ? datatable.instance_exec(&options[:visible]) : options[:visible])
47
+ nonvisible << x if visible == false
48
+ end
49
+ end.to_json()
50
+ end
51
+
52
+ def datatable_default_order(datatable)
53
+ [
54
+ if datatable.default_order.present?
55
+ index = (datatable.table_columns.values.find { |options| options[:name] == datatable.default_order.keys.first.to_s }[:index] rescue nil)
56
+ [index, datatable.default_order.values.first] if index.present?
57
+ end || [0, 'asc']
58
+ ].to_json()
59
+ end
60
+
61
+ def datatable_widths(datatable)
62
+ datatable.table_columns.values.map { |options| {'sWidth' => options[:width]} if options[:width] }.to_json()
63
+ end
64
+
65
+ def datatables_admin_path?
66
+ (attributes[:admin_path] || request.referer.downcase.include?('/admin/')) rescue false
67
+ end
68
+
69
+ # TODO: Improve on this
70
+ def datatables_active_admin_path?
71
+ attributes[:active_admin_path] rescue false
72
+ end
73
+
74
+ end
@@ -0,0 +1,17 @@
1
+ unless defined?(Effective::AccessDenied)
2
+ module Effective
3
+ class AccessDenied < StandardError
4
+ attr_reader :action, :subject
5
+
6
+ def initialize(message = nil, action = nil, subject = nil)
7
+ @message = message
8
+ @action = action
9
+ @subject = subject
10
+ end
11
+
12
+ def to_s
13
+ @message || I18n.t(:'unauthorized.default', :default => 'Access Denied')
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,87 @@
1
+ module Effective
2
+ class ActiveRecordDatatableTool
3
+ attr_accessor :table_columns
4
+
5
+ delegate :order_column_index, :order_direction, :page, :per_page, :search_column, :to => :@datatable
6
+
7
+ def initialize(datatable, table_columns)
8
+ @datatable = datatable
9
+ @table_columns = table_columns
10
+ end
11
+
12
+ def order_column
13
+ @order_column ||= table_columns.find { |_, values| values[:index] == order_column_index }.try(:second) # This pulls out the values
14
+ end
15
+
16
+ def search_terms
17
+ @search_terms ||= @datatable.search_terms.select { |name, search_term| table_columns.key?(name) }
18
+ end
19
+
20
+ def order(collection)
21
+ if order_column.present?
22
+ collection.order("#{order_column[:column]} #{order_direction} NULLS #{order_direction == 'ASC' ? 'LAST' : 'FIRST'}")
23
+ else
24
+ collection
25
+ end
26
+ end
27
+
28
+ def search(collection)
29
+ search_terms.each do |name, search_term|
30
+ column_search = search_column(collection, table_columns[name], search_term)
31
+ raise 'search_column must return an ActiveRecord::Relation object' unless column_search.kind_of?(ActiveRecord::Relation)
32
+ collection = column_search
33
+ end
34
+ collection
35
+ end
36
+
37
+ def search_column_with_defaults(collection, table_column, term)
38
+ column = table_column[:column]
39
+
40
+ case table_column[:type]
41
+ when :string, :text
42
+ if table_column[:filter][:type] == :select && table_column[:filter][:fuzzy] != true
43
+ collection.where("#{column} = :term", :term => term)
44
+ else
45
+ collection.where("#{column} ILIKE :term", :term => "%#{term}%")
46
+ end
47
+ when :datetime
48
+ begin
49
+ digits = term.scan(/(\d+)/).flatten.map(&:to_i)
50
+ start_at = Time.zone.local(*digits)
51
+
52
+ case digits.length
53
+ when 1 # Year
54
+ end_at = start_at.end_of_year
55
+ when 2 # Year-Month
56
+ end_at = start_at.end_of_month
57
+ when 3 # Year-Month-Day
58
+ end_at = start_at.end_of_day
59
+ when 4 # Year-Month-Day Hour
60
+ end_at = start_at.end_of_hour
61
+ when 5 # Year-Month-Day Hour-Minute
62
+ end_at = start_at.end_of_minute
63
+ when 6
64
+ end_at = start_at + 1.second
65
+ else
66
+ end_at = start_at
67
+ end
68
+
69
+ collection.where("#{column} >= :start_at AND #{column} <= :end_at", :start_at => start_at, :end_at => end_at)
70
+ rescue => e
71
+ collection
72
+ end
73
+ when :integer
74
+ collection.where("#{column} = :term", :term => term.to_i)
75
+ when :year
76
+ collection.where("EXTRACT(YEAR FROM #{column}) = :term", :term => term.to_i)
77
+ else
78
+ collection.where("#{column} = :term", :term => term)
79
+ end
80
+ end
81
+
82
+ def paginate(collection)
83
+ collection.page(page).per(per_page)
84
+ end
85
+
86
+ end
87
+ end
@@ -0,0 +1,66 @@
1
+ module Effective
2
+ # The collection is an Array of Arrays
3
+ class ArrayDatatableTool
4
+ attr_accessor :table_columns
5
+
6
+ delegate :order_column_index, :order_direction, :page, :per_page, :search_column, :to => :@datatable
7
+
8
+ def initialize(datatable, table_columns)
9
+ @datatable = datatable
10
+ @table_columns = table_columns
11
+ end
12
+
13
+ def order_column
14
+ @order_column ||= table_columns.find { |_, values| values[:index] == order_column_index }.try(:second) # This pulls out the values
15
+ end
16
+
17
+ def search_terms
18
+ @search_terms ||= @datatable.search_terms.select { |name, search_term| table_columns.key?(name) }
19
+ end
20
+
21
+ def order(collection)
22
+ if order_column.present?
23
+ if order_direction == 'ASC'
24
+ collection.sort! { |x, y| x[order_column[:index]] <=> y[order_column[:index]] }
25
+ else
26
+ collection.sort! { |x, y| y[order_column[:index]] <=> x[order_column[:index]] }
27
+ end
28
+ end
29
+
30
+ collection
31
+ end
32
+
33
+ def search(collection)
34
+ search_terms.each do |name, search_term|
35
+ column_search = search_column(collection, table_columns[name], search_term)
36
+ raise 'search_column must return an Array object' unless column_search.kind_of?(Array)
37
+ collection = column_search
38
+ end
39
+ collection
40
+ end
41
+
42
+ def search_column_with_defaults(collection, table_column, search_term)
43
+ search_term = search_term.downcase
44
+
45
+ collection.select! do |row|
46
+ value = row[table_column[:index]].to_s.downcase
47
+
48
+ if table_column[:filter][:type] == :select && table_column[:filter][:fuzzy] != true
49
+ value == search_term
50
+ else
51
+ value.include?(search_term)
52
+ end
53
+ end || collection
54
+ end
55
+
56
+ def paginate(collection)
57
+ Kaminari.paginate_array(collection).page(page).per(per_page)
58
+ end
59
+ end
60
+ end
61
+
62
+ # [
63
+ # [1, 'title 1'],
64
+ # [2, 'title 2'],
65
+ # [3, 'title 3']
66
+ # ]
@@ -0,0 +1,352 @@
1
+ module Effective
2
+ class Datatable
3
+ attr_accessor :total_records, :display_records, :view, :attributes
4
+
5
+ delegate :render, :link_to, :mail_to, :to => :@view
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
+ (@table_columns ||= HashWithIndifferentAccess.new())[name] = options
26
+ end
27
+
28
+ def table_columns(*names)
29
+ names.each { |name| table_column(name) }
30
+ end
31
+
32
+ def array_column(name, options = {}, proc = nil, &block)
33
+ table_column(name, options.merge({:array_column => true}), proc, &block)
34
+ end
35
+
36
+ def array_columns(*names)
37
+ names.each { |name| array_column(name) }
38
+ end
39
+
40
+ def default_order(name, direction = :asc)
41
+ @default_order = {name => direction}
42
+ end
43
+ end
44
+
45
+ def initialize(*args)
46
+ if args.present?
47
+ raise 'Effective::Datatable.new() can only be called with a Hash like arguments' unless args.first.kind_of?(Hash)
48
+ args.first.each { |k, v| self.attributes[k] = v }
49
+ end
50
+
51
+ unless active_record_collection? || (collection.kind_of?(Array) && collection.first.kind_of?(Array))
52
+ raise "Unsupported collection type. Should be ActiveRecord class, ActiveRecord relation, or an Array of Arrays [[1, 'something'], [2, 'something else']]"
53
+ end
54
+ end
55
+
56
+ # Any attributes set on initialize will be echoed back and available to the class
57
+ def attributes
58
+ @attributes ||= HashWithIndifferentAccess.new()
59
+ end
60
+
61
+ def to_param
62
+ self.class.name.underscore.parameterize
63
+ end
64
+
65
+ def collection
66
+ raise "You must define a collection. Something like an ActiveRecord User.all or an Array of Arrays [[1, 'something'], [2, 'something else']]"
67
+ end
68
+
69
+ def collection_class
70
+ collection.respond_to?(:klass) ? collection.klass : self.class
71
+ end
72
+
73
+ def finalize(collection) # Override me if you like
74
+ collection
75
+ end
76
+
77
+ # Select only col[:if] == true columns, and then set the col[:index] accordingly
78
+ def table_columns
79
+ @table_columns ||= table_columns_with_defaults().select do |_, col|
80
+ col[:if] == nil || (col[:if].respond_to?(:call) ? (view || self).instance_exec(&col[:if]) : col[:if])
81
+ end.each_with_index { |(_, col), index| col[:index] = index }
82
+ end
83
+
84
+ def to_json(options = {})
85
+ {
86
+ :sEcho => params[:sEcho].to_i,
87
+ :aaData => table_data || [],
88
+ :iTotalRecords => (
89
+ unless total_records.kind_of?(Hash)
90
+ total_records.to_i
91
+ else
92
+ (total_records.keys.map(&:first).uniq.count rescue 1)
93
+ end),
94
+ :iTotalDisplayRecords => (
95
+ unless display_records.kind_of?(Hash)
96
+ display_records.to_i
97
+ else
98
+ (display_records.keys.map(&:first).uniq.count rescue 1)
99
+ end)
100
+ }
101
+ end
102
+
103
+ # Wish these were protected
104
+
105
+ def order_column_index
106
+ params[:iSortCol_0].to_i
107
+ end
108
+
109
+ def order_direction
110
+ params[:sSortDir_0].try(:downcase) == 'desc' ? 'DESC' : 'ASC'
111
+ end
112
+
113
+ def default_order
114
+ self.class.instance_variable_get(:@default_order)
115
+ end
116
+
117
+ def search_terms
118
+ @search_terms ||= HashWithIndifferentAccess.new().tap do |terms|
119
+ table_columns.keys.each_with_index do |col, x|
120
+ unless (params["sVisible_#{x}"] == 'false' && table_columns[col][:filter][:when_hidden] != true)
121
+ terms[col] = params["sSearch_#{x}"] if params["sSearch_#{x}"].present?
122
+ end
123
+ end
124
+ end
125
+ end
126
+
127
+ # This is here so classes that inherit from Datatables can can override the specific where clauses on a search column
128
+ def search_column(collection, table_column, search_term)
129
+ if table_column[:array_column]
130
+ array_tool.search_column_with_defaults(collection, table_column, search_term)
131
+ else
132
+ table_tool.search_column_with_defaults(collection, table_column, search_term)
133
+ end
134
+ end
135
+
136
+ def per_page
137
+ length = params[:iDisplayLength].to_i
138
+
139
+ if length == -1
140
+ 9999999
141
+ elsif length > 0
142
+ length
143
+ else
144
+ 10
145
+ end
146
+ end
147
+
148
+ def page
149
+ params[:iDisplayStart].to_i / per_page + 1
150
+ end
151
+
152
+ def view=(view_context)
153
+ @view = view_context
154
+ @view.formats = [:html]
155
+ @view.class.send(:attr_accessor, :attributes)
156
+ @view.attributes = self.attributes
157
+ end
158
+
159
+ protected
160
+
161
+ # So the idea here is that we want to do as much as possible on the database in ActiveRecord
162
+ # And then run any array_columns through in post-processed results
163
+ def table_data
164
+ col = collection
165
+
166
+ if active_record_collection?
167
+ self.total_records = (col.select('*').reorder(nil).count rescue 1)
168
+
169
+ col = table_tool.order(col)
170
+ col = table_tool.search(col)
171
+
172
+ if table_tool.search_terms.present? && array_tool.search_terms.blank?
173
+ self.display_records = (col.select('*').reorder(nil).count rescue 1)
174
+ end
175
+ else
176
+ self.total_records = col.size
177
+ end
178
+
179
+ if array_tool.search_terms.present?
180
+ col = self.arrayize(col)
181
+ col = array_tool.search(col)
182
+ self.display_records = col.size
183
+ end
184
+
185
+ if array_tool.order_column.present?
186
+ col = self.arrayize(col)
187
+ col = array_tool.order(col)
188
+ end
189
+
190
+ self.display_records ||= total_records
191
+
192
+ if col.kind_of?(Array)
193
+ col = array_tool.paginate(col)
194
+ else
195
+ col = table_tool.paginate(col)
196
+ col = self.arrayize(col)
197
+ end
198
+
199
+ col = self.finalize(col)
200
+ end
201
+
202
+ def arrayize(collection)
203
+ return collection if @arrayized # Prevent the collection from being arrayized more than once
204
+ @arrayized = true
205
+
206
+ # We want to use the render :collection for each column that renders partials
207
+ rendered = {}
208
+ table_columns.each do |name, opts|
209
+ if opts[:partial]
210
+ rendered[name] = (render(
211
+ :partial => opts[:partial],
212
+ :as => opts[:partial_local],
213
+ :collection => collection,
214
+ :formats => :html,
215
+ :locals => {:datatable => self},
216
+ :spacer_template => '/effective/datatables/spacer_template',
217
+ ) || '').split('EFFECTIVEDATATABLESSPACER')
218
+ end
219
+ end
220
+
221
+ collection.each_with_index.map do |obj, index|
222
+ table_columns.map do |name, opts|
223
+ value = if opts[:partial]
224
+ rendered[name][index]
225
+ elsif opts[:block]
226
+ view.instance_exec(obj, collection, self, &opts[:block])
227
+ elsif opts[:proc]
228
+ view.instance_exec(obj, collection, self, &opts[:proc])
229
+ elsif opts[:type] == :belongs_to
230
+ val = (obj.send(name) rescue nil).to_s
231
+ else
232
+ val = (obj.send(name) rescue nil)
233
+ val = (obj[opts[:array_index]] rescue nil) if val == nil
234
+ val
235
+ end
236
+
237
+ # Last minute formatting of dates
238
+ case value
239
+ when Date
240
+ value.strftime("%Y-%m-%d")
241
+ when Time
242
+ value.strftime("%Y-%m-%d %H:%M:%S")
243
+ when DateTime
244
+ value.strftime("%Y-%m-%d %H:%M:%S")
245
+ else
246
+ value
247
+ end
248
+ end
249
+ end
250
+ end
251
+
252
+ private
253
+
254
+ def params
255
+ view.try(:params) || HashWithIndifferentAccess.new()
256
+ end
257
+
258
+ def table_tool
259
+ @table_tool ||= ActiveRecordDatatableTool.new(self, table_columns.select { |_, col| col[:array_column] == false })
260
+ end
261
+
262
+ def array_tool
263
+ @array_tool ||= ArrayDatatableTool.new(self, table_columns.select { |_, col| col[:array_column] == true })
264
+ end
265
+
266
+ def active_record_collection?
267
+ @active_record_collection ||= (collection.ancestors.include?(ActiveRecord::Base) rescue false)
268
+ end
269
+
270
+ def table_columns_with_defaults
271
+ unless self.class.instance_variable_get(:@table_columns_initialized)
272
+ self.class.instance_variable_set(:@table_columns_initialized, true)
273
+ initalize_table_columns(self.class.instance_variable_get(:@table_columns))
274
+ end
275
+
276
+ self.class.instance_variable_get(:@table_columns)
277
+ end
278
+
279
+ def initalize_table_columns(cols)
280
+ sql_table = (collection.table rescue nil)
281
+
282
+ # Here we identify all belongs_to associations and build up a Hash like:
283
+ # {:user => {:foreign_key => 'user_id', :klass => User}, :order => {:foreign_key => 'order_id', :klass => Effective::Order}}
284
+ belong_tos = (collection.ancestors.first.reflect_on_all_associations(:belongs_to) rescue []).inject(HashWithIndifferentAccess.new()) do |retval, bt|
285
+ unless bt.options[:polymorphic]
286
+ begin
287
+ klass = bt.klass || bt.foreign_type.gsub('_type', '').classify.constantize
288
+ rescue => e
289
+ klass = nil
290
+ end
291
+
292
+ retval[bt.name] = {:foreign_key => bt.foreign_key, :klass => klass} if bt.foreign_key.present? && klass.present?
293
+ end
294
+
295
+ retval
296
+ end
297
+
298
+ cols.each_with_index do |(name, _), index|
299
+ # If this is a belongs_to, add an :if clause specifying a collection scope if
300
+ if belong_tos.key?(name)
301
+ cols[name][:if] ||= Proc.new { attributes[belong_tos[name][:foreign_key]].blank? } # :if => Proc.new { attributes[:user_id].blank? }
302
+ end
303
+
304
+ sql_column = (collection.columns rescue []).find do |column|
305
+ column.name == name.to_s || (belong_tos.key?(name) && column.name == belong_tos[name][:foreign_key])
306
+ end
307
+
308
+ cols[name][:array_column] ||= false
309
+ cols[name][:array_index] = index # The index of this column in the collection, regardless of hidden table_columns
310
+ cols[name][:name] ||= name
311
+ cols[name][:label] ||= name.titleize
312
+ cols[name][:column] ||= (sql_table && sql_column) ? "\"#{sql_table.name}\".\"#{sql_column.name}\"" : name
313
+ cols[name][:type] ||= (belong_tos.key?(name) ? :belongs_to : sql_column.try(:type)).presence || :string
314
+ cols[name][:width] ||= nil
315
+ cols[name][:sortable] = true if cols[name][:sortable] == nil
316
+ cols[name][:filter] = initialize_table_column_filter(cols[name][:filter], cols[name][:type], belong_tos[name])
317
+
318
+ if cols[name][:partial]
319
+ cols[name][:partial_local] ||= (sql_table.try(:name) || cols[name][:partial].split('/').last(2).first.presence || 'obj').singularize.to_sym
320
+ end
321
+ end
322
+ end
323
+
324
+ def initialize_table_column_filter(filter, col_type, belongs_to)
325
+ return {:type => :null, :when_hidden => false} if filter == false
326
+
327
+ if filter.kind_of?(Symbol)
328
+ filter = {:type => filter}
329
+ elsif filter.kind_of?(String)
330
+ filter = {:type => filter.to_sym}
331
+ elsif filter.kind_of?(Hash) == false
332
+ filter = {}
333
+ end
334
+
335
+ case col_type # null, number, select, number-range, date-range, checkbox, text(default)
336
+ when :belongs_to
337
+ {
338
+ :type => :select,
339
+ :when_hidden => false,
340
+ :values => Proc.new { belongs_to[:klass].all.map { |obj| [obj.id, obj.to_s] }.sort { |x, y| x[1] <=> y[1] } }
341
+ }.merge(filter)
342
+ when :integer
343
+ {:type => :number, :when_hidden => false}.merge(filter)
344
+ when :boolean
345
+ {:type => :select, :when_hidden => false, :values => [true, false]}.merge(filter)
346
+ else
347
+ {:type => :text, :when_hidden => false}.merge(filter)
348
+ end
349
+ end
350
+
351
+ end
352
+ end