Class: DataTable::Table

Inherits:
Object
  • Object
show all
Defined in:
lib/data-table/table.rb

Overview

Config Options

id: the html id title: the title of the data table subtitle: the subtitle of the data table css_class: an extra css class to get applied to the table empty_text: the text to display of the collection is empty display_header => false: hide the column headers for the data table alternate_rows => false: turn off alternating of row css classes alternate_cols => true: turn on alternating of column classes, defaults to false

columns: an array of hashes of the column specs for this table

group_by: an array of columns to group on

subtotals: an array of hashes that contain the subtotal information for each column that should be subtotaled totals: an array of hashes that contain the total information for each column that should be totaled

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(collection) ⇒ Table

Returns a new instance of Table.



30
31
32
33
34
35
36
37
38
39
# File 'lib/data-table/table.rb', line 30

def initialize(collection)
  @collection = collection
  @grouped_collection = nil
  default_options!
  @columns = []
  @groupings = []
  @grouped_data = false
  @subtotals = []
  @totals = []
end

Instance Attribute Details

#alternate_colsObject

Returns the value of attribute alternate_cols.



26
27
28
# File 'lib/data-table/table.rb', line 26

def alternate_cols
  @alternate_cols
end

#alternate_rowsObject

Returns the value of attribute alternate_rows.



26
27
28
# File 'lib/data-table/table.rb', line 26

def alternate_rows
  @alternate_rows
end

#collectionObject (readonly)

Returns the value of attribute collection.



23
24
25
# File 'lib/data-table/table.rb', line 23

def collection
  @collection
end

#columnsObject (readonly)

Returns the value of attribute columns.



23
24
25
# File 'lib/data-table/table.rb', line 23

def columns
  @columns
end

#css_classObject

Returns the value of attribute css_class.



26
27
28
# File 'lib/data-table/table.rb', line 26

def css_class
  @css_class
end

#custom_headersObject

Returns the value of attribute custom_headers.



26
27
28
# File 'lib/data-table/table.rb', line 26

def custom_headers
  @custom_headers
end

#display_headerObject

Returns the value of attribute display_header.



26
27
28
# File 'lib/data-table/table.rb', line 26

def display_header
  @display_header
end

#empty_textObject

Returns the value of attribute empty_text.



26
27
28
# File 'lib/data-table/table.rb', line 26

def empty_text
  @empty_text
end

#grouped_dataObject (readonly)

Returns the value of attribute grouped_data.



23
24
25
# File 'lib/data-table/table.rb', line 23

def grouped_data
  @grouped_data
end

#hide_if_emptyObject

Returns the value of attribute hide_if_empty.



26
27
28
# File 'lib/data-table/table.rb', line 26

def hide_if_empty
  @hide_if_empty
end

#idObject

Returns the value of attribute id.



26
27
28
# File 'lib/data-table/table.rb', line 26

def id
  @id
end

#repeat_headers_for_groupsObject

Returns the value of attribute repeat_headers_for_groups.



26
27
28
# File 'lib/data-table/table.rb', line 26

def repeat_headers_for_groups
  @repeat_headers_for_groups
end

#subtotal_calculationsObject (readonly)

Returns the value of attribute subtotal_calculations.



23
24
25
# File 'lib/data-table/table.rb', line 23

def subtotal_calculations
  @subtotal_calculations
end

#subtotalsObject (readonly)

Returns the value of attribute subtotals.



23
24
25
# File 'lib/data-table/table.rb', line 23

def subtotals
  @subtotals
end

#titleObject

Returns the value of attribute title.



26
27
28
# File 'lib/data-table/table.rb', line 26

def title
  @title
end

#total_calculationsObject (readonly)

Returns the value of attribute total_calculations.



23
24
25
# File 'lib/data-table/table.rb', line 23

def total_calculations
  @total_calculations
end

#totalsObject (readonly)

Returns the value of attribute totals.



23
24
25
# File 'lib/data-table/table.rb', line 23

def totals
  @totals
end

Instance Method Details

#calculate(data, column_name, function, path = nil) ⇒ Object

TODO: Write test for this



381
382
383
384
385
386
387
388
389
390
391
392
# File 'lib/data-table/table.rb', line 381

def calculate(data, column_name, function, path = nil)
  column = @columns.select { |col| col.name == column_name }
  if function.is_a?(Proc)
    calculate_with_proc(function, data, column, path)
  elsif function.is_a?(Array) && function[1].is_a?(Proc)
    calculate_array_and_proc(function, data, column_name, path)
  elsif function.is_a?(Array)
    calculate_many(function, data, column_name, path)
  else
    send("calculate_#{function}", data, column_name)
  end
end

#calculate_array_and_proc(function, data, column = nil, path = nil) ⇒ Object



402
403
404
405
406
407
408
409
# File 'lib/data-table/table.rb', line 402

def calculate_array_and_proc(function, data, column = nil, path = nil)
  result = send("calculate_#{function[0]}", data, column)
  case function[1].arity
  when 1 then function[1].call(result)
  when 2 then function[1].call(result, column.first)
  when 3 then function[1].call(result, column.first, path.last)
  end
end

#calculate_avg(collection, column_name) ⇒ Object



425
426
427
428
# File 'lib/data-table/table.rb', line 425

def calculate_avg(collection, column_name)
  sum = calculate_sum(collection, column_name)
  sum / collection.size
end

#calculate_many(function, data, column_name, _path = nil) ⇒ Object



411
412
413
414
415
416
417
418
419
# File 'lib/data-table/table.rb', line 411

def calculate_many(function, data, column_name, _path = nil)
  function.each do |func|
    if func.is_a? Array
      send("calculate_#{func[0]}", data, column_name)
    else
      send("calculate_#{func}", data, column_name)
    end
  end
end

#calculate_max(collection, column_name) ⇒ Object



430
431
432
# File 'lib/data-table/table.rb', line 430

def calculate_max(collection, column_name)
  collection.collect { |r| r[column_name].to_f }.max
end

#calculate_min(collection, column_name) ⇒ Object



434
435
436
# File 'lib/data-table/table.rb', line 434

def calculate_min(collection, column_name)
  collection.collect { |r| r[column_name].to_f }.min
end

#calculate_parent_subtotalsObject



360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
# File 'lib/data-table/table.rb', line 360

def calculate_parent_subtotals
  @parent_subtotals = Hash.new { |h, k| h[k] = [] }
  # Iterate over all the parent groups
  parent_groups = @groupings.slice(0, @groupings.count - 1).compact
  parent_groups.count.times do
    # Group by each parent on the fly
    @subtotals.each_with_index do |subtotal, index|
      @collection.group_by_recursive(parent_groups).each_pair_with_parents do |group_name, data, parents|
        subtotal.each do |s|
          path = parents + [group_name]
          result = calculate(data, s[0], s[1], path)
          @parent_subtotals[path][index] ||= {} if @parent_subtotals[path][index].nil?
          @parent_subtotals[path][index][s[0]] = {s[1] => result}
        end
      end
    end
    parent_groups.pop
  end
end

#calculate_subtotals!Object



343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
# File 'lib/data-table/table.rb', line 343

def calculate_subtotals!
  raise 'Subtotals only work with grouped results sets' unless @grouped_data
  @subtotal_calculations ||= Hash.new { |h, k| h[k] = [] }
  @subtotals.each_with_index do |subtotal_type, index|
    subtotal_type.each do |subtotal|
      @collection.each_pair_with_parents(@groupings.count) do |group_name, group_data, parents|
        path = parents + [group_name]
        result = calculate(group_data, subtotal[0], subtotal[1], path)
        (0..index).each do |index|
          @subtotal_calculations[path][index] ||= {}
        end
        @subtotal_calculations[path][index][subtotal[0]] = {subtotal[1] => result}
      end
    end
  end
end

#calculate_sum(collection, column_name) ⇒ Object



421
422
423
# File 'lib/data-table/table.rb', line 421

def calculate_sum(collection, column_name)
  collection.inject(0) { |sum, row| sum + row[column_name].to_f }
end

#calculate_totals!Object

TODO: Refactor to shorten method. Also revise tests.



326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
# File 'lib/data-table/table.rb', line 326

def calculate_totals!
  @total_calculations = []
  @totals.each_with_index do |row, index|
    next if row.nil?

    if @collection.is_a?(Hash)
      collection = []
      @collection.each_pair_recursive { |_k, v| collection.concat(v) }
    end
    collection = @collection if @collection.is_a? Array
    @total_calculations[index] = {} if @total_calculations[index].nil?
    row.each do |item|
      @total_calculations[index][item[0]] = calculate(collection, item[0], item[1])
    end
  end
end

#calculate_with_proc(function, data, column = nil, path = nil) ⇒ Object



394
395
396
397
398
399
400
# File 'lib/data-table/table.rb', line 394

def calculate_with_proc(function, data, column = nil, path = nil)
  case function.arity
  when 1 then function.call(data)
  when 2 then function.call(data, column.first)
  when 3 then function.call(data, column.first, path.last)
  end
end

#column(id, title = '', opts = {}, &b) ⇒ Object

Define a new column for the table



59
60
61
# File 'lib/data-table/table.rb', line 59

def column(id, title = '', opts = {}, &b)
  @columns << DataTable::Column.new(id, title, opts, &b)
end

#custom_header(&blk) ⇒ Object



156
157
158
# File 'lib/data-table/table.rb', line 156

def custom_header(&blk)
  instance_eval(&blk)
end

#default_options!Object



41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# File 'lib/data-table/table.rb', line 41

def default_options!
  @id = ''
  @title = ''
  @subtitle = ''
  @css_class = ''
  @empty_text = 'No records found'
  @hide_if_empty = false
  @display_header = true
  @alternate_rows = true
  @alternate_cols = false
  @subtotal_title = 'Subtotal:'
  @total_title = 'Total:'
  @repeat_headers_for_groups = false
  @custom_headers = []
  @row_attributes = nil
end

#group_by(group_column, level = {level: 0}, &_blk) ⇒ Object

TODO: allow for group column only, block only and group column and block



173
174
175
176
177
178
179
180
# File 'lib/data-table/table.rb', line 173

def group_by(group_column, level = {level: 0}, &_blk)
  if level.nil? && @groupings.count >= 1
    raise 'a level designation is required when using multiple groupings.'
  end
  @grouped_data = true
  @groupings[level ? level[:level] : 0] = group_column
  @columns.reject! { |c| c.name == group_column }
end

#group_data!Object



182
183
184
185
186
187
188
189
# File 'lib/data-table/table.rb', line 182

def group_data!
  @groupings.compact!
  @collection = if @groupings.count > 1
                  collection.group_by_recursive(@groupings)
                else
                  collection.group_by { |row| row[@groupings.first] }
                end
end

#prepare_dataObject



63
64
65
66
67
68
# File 'lib/data-table/table.rb', line 63

def prepare_data
  calculate_parent_subtotals if @groupings.count > 1
  group_data! if @grouped_data
  calculate_subtotals! if subtotals?
  calculate_totals! if totals?
end

#renderObject

GENERAL RENDERING



73
74
75
# File 'lib/data-table/table.rb', line 73

def render
  render_data_table
end

#render_custom_table_headerObject



102
103
104
105
106
107
108
# File 'lib/data-table/table.rb', line 102

def render_custom_table_header
  html = "<tr class='custom-header'>"
  @custom_headers.each do |h|
    html << "<th class=\"#{h[:css]}\" colspan=\"#{h[:colspan]}\" style=\"#{h[:style]}\">#{h[:text]}</th>"
  end
  html << '</tr>'
end

#render_data_tableObject



77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/data-table/table.rb', line 77

def render_data_table
  html = "<table id='#{@id}' class='data_table #{@css_class}' cellspacing='0' cellpadding='0'>"
  html << "<caption>#{@title}</caption>" if @title
  html << render_data_table_header if @display_header
  if @collection.any?
    html << render_data_table_body(@collection)
    html << render_totals if totals?
  else
    html << "<tr><td class='empty_data_table' colspan='#{@columns.size}'>#{@empty_text}</td></tr>"
  end
  html << '</table>'
end

#render_data_table_body(collection) ⇒ Object



110
111
112
113
114
115
116
# File 'lib/data-table/table.rb', line 110

def render_data_table_body(collection)
  if @grouped_data
    render_grouped_data_table_body(collection)
  else
    "<tbody>#{render_rows(collection)}</tbody>"
  end
end

#render_data_table_headerObject



90
91
92
93
94
95
96
97
98
99
100
# File 'lib/data-table/table.rb', line 90

def render_data_table_header
  html = '<thead>'

  html << render_custom_table_header unless @custom_headers.empty?

  html << '<tr>'
  @columns.each do |col|
    html << col.render_column_header
  end
  html << '</tr></thead>'
end

#render_group(group_header, group_data) ⇒ Object



217
218
219
220
221
222
223
224
225
226
227
# File 'lib/data-table/table.rb', line 217

def render_group(group_header, group_data)
  html = "<tbody class='#{group_header.to_s.downcase.gsub(/[^A-Za-z0-9]+/, '_')}'>"
  html << render_group_header(group_header, 0)
  if group_data.is_a? Array
    html << render_rows(group_data)
    html << render_subtotals(group_header, group_data) if subtotals?
  elsif group_data.is_a? Hash
    html << render_group_recursive(group_data, 1, group_header)
  end
  html << '</tbody>'
end

#render_group_header(group_header, index = nil) ⇒ Object



199
200
201
202
203
204
205
206
207
# File 'lib/data-table/table.rb', line 199

def render_group_header(group_header, index = nil)
  css_classes = ['group_header']
  css_classes << ["level_#{index}"] unless index.nil?
  html =  "<tr class='#{css_classes.join(' ')}'>"
  html << "<th colspan='#{@columns.size}'>#{group_header}</th>"
  html << '</tr>'
  repeat_headers(html) if @repeat_headers_for_groups
  html
end

#render_group_recursive(collection, index = 1, group_parent = nil, ancestors = nil) ⇒ Object



229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
# File 'lib/data-table/table.rb', line 229

def render_group_recursive(collection, index = 1, group_parent = nil, ancestors = nil)
  html = ''
  ancestors ||= []
  collection.each_pair do |group_name, group_data|
    ancestors << group_parent unless ancestors[0] == group_parent
    ancestors << group_name unless ancestors.length == @groupings.length
    if group_data.is_a?(Hash)
      html << render_group_header(group_name, index)
      html << render_group_recursive(group_data, index + 1, nil, ancestors)
    elsif group_data.is_a?(Array)
      html << render_group_header(group_name, index)
      html << render_rows(group_data)
      ancestors.pop
      html << render_subtotals(group_name, group_data, ancestors) if subtotals?
    end
  end
  html << render_parent_subtotals(ancestors) if @parent_subtotals
  ancestors.pop
  html
end

#render_grouped_data_table_body(collection) ⇒ Object



191
192
193
194
195
196
197
# File 'lib/data-table/table.rb', line 191

def render_grouped_data_table_body(collection)
  html = ''
  collection.keys.each do |group_name|
    html << render_group(group_name, collection[group_name])
  end
  html
end

#render_parent_subtotals(group_array) ⇒ Object



268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
# File 'lib/data-table/table.rb', line 268

def render_parent_subtotals(group_array)
  html = ''
  @parent_subtotals[group_array].each_with_index do |group, index|
    next if group.nil?

    html << "<tr class='parent_subtotal "
    html << "index_#{index} #{group_array.join('_').gsub(/\s/, '_').downcase}'>"
    @columns.each do |col|
      value = group[col.name] ? group[col.name].values[0] : nil
      html << col.render_cell(value)
    end
    html << '</tr>'
  end
  html
end

#render_row(row, row_index, css_class = '', row_attributes = {}) ⇒ Object



132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/data-table/table.rb', line 132

def render_row(row, row_index, css_class = '', row_attributes = {})
  attributes = if row_attributes.nil?
                 ''
               else
                 row_attributes.map { |attr, val| "#{attr}='#{val}'" }.join ' '
               end

  html = "<tr class='row_#{row_index} #{css_class}' #{attributes}>"
  @columns.each_with_index do |col, col_index|
    cell = begin
             row[col.name]
           rescue
             nil
           end
    html << col.render_cell(cell, row, row_index, col_index)
  end
  html << '</tr>'
end

#render_rows(collection) ⇒ Object



118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/data-table/table.rb', line 118

def render_rows(collection)
  html = ''
  collection.each_with_index do |row, row_index|
    css_class = @alternate_rows && row_index.odd? ? 'alt ' : ''
    if @row_style && style = @row_style.call(row, row_index)
      css_class << style
    end

    attributes = @row_attributes.nil? ? {} : @row_attributes.call(row)
    html << render_row(row, row_index, css_class, attributes)
  end
  html
end

#render_subtotals(group_header, _group_data = nil, ancestors = nil) ⇒ Object

ancestors should be an array



285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
# File 'lib/data-table/table.rb', line 285

def render_subtotals(group_header, _group_data = nil, ancestors = nil)
  html = ''
  path = ancestors.nil? ? [] : ancestors.dup
  path << group_header

  is_first_subtotal = true

  @subtotal_calculations[path].each_with_index do |group, index|
    next if group.empty?
    
    html << "<tr class='subtotal index_#{index} #{'first' if is_first_subtotal}'>"
    @columns.each do |col|
      value = group[col.name] ? group[col.name].values[0] : nil
      html << col.render_cell(value)
    end
    html << '</tr>'

    is_first_subtotal = false
  end
  html
end

#render_totalsObject

TOTALS AND SUBTOTALS



253
254
255
256
257
258
259
260
261
262
263
264
265
266
# File 'lib/data-table/table.rb', line 253

def render_totals
  html = '<tfoot>'
  @total_calculations.each_with_index do |totals_row, index|
    next if totals_row.nil?
    
    html << "<tr class='total index_#{index}'>"
    @columns.each do |col|
      value = totals_row[col.name] ||= nil
      html << col.render_cell(value)
    end
    html << '</tr>'
  end
  html << '</tfoot>'
end

#repeat_headers(html) ⇒ Object



209
210
211
212
213
214
215
# File 'lib/data-table/table.rb', line 209

def repeat_headers(html)
  html << "<tr class='col_headers'>"
  @columns.each_with_index do |col, _i|
    html << col.render_column_header
  end
  html << '</tr>'
end

#row_attributes(&b) ⇒ Object



164
165
166
# File 'lib/data-table/table.rb', line 164

def row_attributes(&b)
  @row_attributes = b
end

#row_style(&b) ⇒ Object

define a custom block to be used to determine the css class for a row.



152
153
154
# File 'lib/data-table/table.rb', line 152

def row_style(&b)
  @row_style = b
end

#subtotal(column_name, function = nil, index = 0, &block) ⇒ Object



307
308
309
310
# File 'lib/data-table/table.rb', line 307

def subtotal(column_name, function = nil, index = 0, &block)
  raise 'You must supply an index value' if @subtotals.count >= 1 && index.nil?
  total_row @subtotals, column_name, function, index, &block
end

#subtotals?Boolean

Returns:

  • (Boolean)


312
313
314
# File 'lib/data-table/table.rb', line 312

def subtotals?
  !@subtotals.empty?
end

#th(header_text, options) ⇒ Object



160
161
162
# File 'lib/data-table/table.rb', line 160

def th(header_text, options)
  @custom_headers << options.merge(text: header_text)
end

#total(column_name, function = nil, index = 0, &block) ⇒ Object



316
317
318
319
# File 'lib/data-table/table.rb', line 316

def total(column_name, function = nil, index = 0, &block)
  raise 'You must supply an index value' if @totals.count >= 1 && index.nil?
  total_row @totals, column_name, function, index, &block
end

#totals?Boolean

Returns:

  • (Boolean)


321
322
323
# File 'lib/data-table/table.rb', line 321

def totals?
  !@totals.empty?
end