effective_datatables 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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