data-table 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.
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in data-table.gemspec
4
+ gemspec
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,20 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "data-table/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "data-table"
7
+ s.version = DataTable::VERSION
8
+ s.authors = ["Steve Erickson", "Jeff Fraser"]
9
+ s.email = ["sixfeetover@gmail.com"]
10
+ s.homepage = "https://github.com/sixfeetover/data-table"
11
+ s.summary = %q{Turn arrays of hashes or models in to an HTML table.}
12
+ s.description = %q{data-table is a simple gem that provides a DSL for allowing you do turn an array of hashes or ActiveRecord objects into an HTML table.}
13
+
14
+ s.rubyforge_project = "data-table"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+ end
@@ -0,0 +1,3 @@
1
+ require "data-table/version"
2
+ require "data-table/data_table"
3
+ require "data-table/data_table_column"
@@ -0,0 +1,385 @@
1
+ ##
2
+ # Config Options
3
+ #
4
+ # id: the html id
5
+ # title: the title of the data table
6
+ # subtitle: the subtitle of the data table
7
+ # css_class: an extra css class to get applied to the table
8
+ # empty_text: the text to display of the collection is empty
9
+ # display_header => false: hide the column headers for the data table
10
+ # alternate_rows => false: turn off alternating of row css classes
11
+ # alternate_cols => true: turn on alternating of column classes, defaults to false
12
+ #
13
+ # columns: an array of hashes of the column specs for this table
14
+ #
15
+ # group_by: an array of columns to group on
16
+ # pivot_on: an array of columns to pivot on
17
+ #
18
+ # subtotals: an array of hashes that contain the subtotal information for each column that should be subtotaled
19
+ # totals: an array of hashes that contain the total information for each column that should be totaled
20
+ #
21
+ ##
22
+
23
+ class DataTable
24
+
25
+ #############
26
+ # CONFIG
27
+ #############
28
+ attr_reader :grouped_data, :pivoted_data, :subtotals, :totals, :subtotal_calculations, :total_calculations
29
+ attr_accessor :id, :title, :css_class, :empty_text, :alternate_rows, :alternate_cols, :display_header, :hide_if_empty, :repeat_headers_for_groups, :custom_headers
30
+
31
+ def initialize(collection)
32
+ @collection = collection
33
+ default_options!
34
+
35
+ @columns = []
36
+
37
+ @groupings, @pivot_columns = [], []
38
+ @pivoted_data, @grouped_data = false, false
39
+ @subtotals, @totals = {}, {}
40
+ end
41
+
42
+ def default_options!
43
+ @id = ''
44
+ @title = ''
45
+ @subtitle = ''
46
+ @css_class = ''
47
+ @empty_text = 'No records found'
48
+ @hide_if_empty = false
49
+ @display_header = true
50
+ @alternate_rows = true
51
+ @alternate_cols = false
52
+ @subtotal_title = "Subtotal:"
53
+ @total_title = "Total:"
54
+ @repeat_headers_for_groups = false
55
+ @custom_headers = []
56
+ @row_attributes = nil
57
+ end
58
+
59
+ def self.default_css_styles
60
+ <<-CSS_STYLE
61
+ .data_table {width: 100%; empty-cells: show}
62
+ .data_table td, .data_table th {padding: 3px}
63
+
64
+ .data_table caption {font-size: 2em; font-weight: bold}
65
+
66
+ .data_table thead {}
67
+ .data_table thead th {background-color: #ddd; border-bottom: 1px solid #bbb;}
68
+
69
+ .data_table tbody {}
70
+ .data_table tbody tr.alt {background-color: #eee;}
71
+
72
+ .data_table .group_header th {text-align: left;}
73
+
74
+ .data_table .subtotal {}
75
+ .data_table .subtotal td {border-top: 1px solid #000;}
76
+
77
+ .data_table tfoot {}
78
+ .data_table tfoot td {border-top: 1px solid #000;}
79
+
80
+ .empty_data_table {text-align: center; background-color: #ffc;}
81
+
82
+ /* Data Types */
83
+ .data_table .number, .data_table .money {text-align: right}
84
+ .data_table .text {text-align: left}
85
+ CSS_STYLE
86
+ end
87
+
88
+ # Define a new column for the table
89
+ def column(id, title="", opts={}, &b)
90
+ @columns << DataTableColumn.new(id, title, opts, &b)
91
+ end
92
+
93
+ def prepare_data
94
+ self.pivot_data! if @pivoted_data
95
+ self.group_data! if @grouped_data
96
+
97
+ self.calculate_subtotals! if has_subtotals?
98
+ self.calculate_totals! if has_totals?
99
+ end
100
+
101
+ #############
102
+ # GENERAL RENDERING
103
+ #############
104
+
105
+ def self.render(collection, &blk)
106
+ # make a new table
107
+ t = self.new(collection)
108
+
109
+ # yield it to the block for configuration
110
+ yield t
111
+
112
+ # modify the data structure if necessary and do calculations
113
+ t.prepare_data
114
+
115
+ # render the table
116
+ t.render.html_safe
117
+ end
118
+
119
+ def render
120
+ render_data_table
121
+ end
122
+
123
+ def render_data_table
124
+ html = "<table id='#{@id}' class='data_table #{@css_class}' cellspacing='0' cellpadding='0'>"
125
+ html << "<caption>#{@title}</caption>" if @title
126
+ html << render_data_table_header if @display_header
127
+ if @collection.any?
128
+ html << render_data_table_body(@collection)
129
+ html << render_totals if has_totals?
130
+ else
131
+ html << "<tr><td class='empty_data_table' colspan='#{@columns.size}'>#{@empty_text}</td></tr>"
132
+ end
133
+ html << "</table>"
134
+ end
135
+
136
+ def render_data_table_header
137
+ html = "<thead>"
138
+
139
+ html << render_custom_table_header unless @custom_headers.empty?
140
+
141
+ html << "<tr>"
142
+ @columns.each do |col|
143
+ html << col.render_column_header
144
+ end
145
+ html << "</tr></thead>"
146
+ end
147
+
148
+ def render_custom_table_header
149
+ html = "<tr>"
150
+ @custom_headers.each do |h|
151
+ html << "<th class=\"#{h[:css]}\" colspan=\"#{h[:colspan]}\">#{h[:text]}</th>"
152
+ end
153
+ html << "</tr>"
154
+ end
155
+
156
+ def render_data_table_body(collection)
157
+ if @grouped_data
158
+ render_grouped_data_table_body(collection)
159
+ else
160
+ "<tbody>#{render_rows(collection)}</tbody>"
161
+ end
162
+ end
163
+
164
+ def render_rows(collection)
165
+ html = ""
166
+ collection.each_with_index do |row, row_index|
167
+ css_class = @alternate_rows && row_index % 2 == 1 ? 'alt ' : ''
168
+ if @row_style && style = @row_style.call(row, row_index)
169
+ css_class << style
170
+ end
171
+
172
+ attributes = @row_attributes.nil? ? {} : @row_attributes.call(row)
173
+ html << render_row(row, row_index, css_class, attributes)
174
+ end
175
+ html
176
+ end
177
+
178
+ def render_row(row, row_index, css_class='', row_attributes={})
179
+ if row_attributes.nil?
180
+ attributes = ''
181
+ else
182
+ attributes = row_attributes.map {|attr, val| "#{attr}='#{val}'"}.join " "
183
+ end
184
+
185
+ html = "<tr class='row_#{row_index} #{css_class}' #{attributes}>"
186
+ @columns.each_with_index do |col, col_index|
187
+ cell = row[col.name] rescue nil
188
+ html << col.render_cell(cell, row, row_index, col_index)
189
+ end
190
+ html << "</tr>"
191
+ end
192
+
193
+ # define a custom block to be used to determine the css class for a row.
194
+ def row_style(&b)
195
+ @row_style = b
196
+ end
197
+
198
+ def custom_header(&blk)
199
+ instance_eval(&blk)
200
+ end
201
+
202
+ def th(header_text, options)
203
+ @custom_headers << options.merge(:text => header_text)
204
+ end
205
+
206
+ def row_attributes(&b)
207
+ @row_attributes = b
208
+ end
209
+
210
+ #############
211
+ # GROUPING
212
+ #############
213
+
214
+ # TODO: allow for group column only, block only and group column and block
215
+ def group_by(group_column, &blk)
216
+ @grouped_data = true
217
+ @groupings = group_column
218
+ @columns.reject!{|c| c.name == group_column}
219
+ end
220
+
221
+ def group_data!
222
+ @collection = @collection.group_by {|row| row[@groupings] }
223
+ end
224
+
225
+ def render_grouped_data_table_body(collection)
226
+ html = ""
227
+ collection.keys.each do |group_name|
228
+ html << render_group(group_name, collection[group_name])
229
+ end
230
+ html
231
+ end
232
+
233
+ def render_group_header(group_header)
234
+ html = "<tr class='group_header'>"
235
+ if @repeat_headers_for_groups
236
+ @columns.each_with_index do |col, i|
237
+ html << (i == 0 ? "<th>#{group_header}</th>" : col.render_column_header)
238
+ end
239
+ else
240
+ html << "<th colspan='#{@columns.size}'>#{group_header}</th>"
241
+ end
242
+ html << "</tr>"
243
+ html
244
+ end
245
+
246
+ def render_group(group_header, group_data)
247
+ html = "<tbody class='#{group_header.to_s.downcase.gsub(/[^A-Za-z0-9]+/, '_')}'>" #replace non-letters and numbers with '_'
248
+ html << render_group_header(group_header)
249
+ html << render_rows(group_data)
250
+ html << render_subtotals(group_header, group_data) if has_subtotals?
251
+ html << "</tbody>"
252
+ end
253
+
254
+
255
+ #############
256
+ # PIVOTING
257
+ #############
258
+
259
+ def pivot_on(pivot_column)
260
+ @pivoted_data = true
261
+ @pivot_column = pivot_column
262
+ end
263
+
264
+ def pivot_data!
265
+ @collection.pivot_on
266
+ end
267
+
268
+ #############
269
+ # TOTALS AND SUBTOTALS
270
+ #############
271
+ def render_totals
272
+ html = "<tfoot><tr>"
273
+ @columns.each do |col|
274
+ html << col.render_cell(@total_calculations[col.name])
275
+ end
276
+ html << "</tr></tfoot>"
277
+ end
278
+
279
+ def render_subtotals(group_header, group_data)
280
+ html = "<tr class='subtotal'>"
281
+ @columns.each do |col|
282
+ html << col.render_cell(@subtotal_calculations[group_header][col.name])
283
+ end
284
+ html << "</tr>"
285
+ end
286
+
287
+ # define a new total column definition.
288
+ # total columns take the name of the column that should be totaled
289
+ # they also take a default aggregate function name and/or a block
290
+ # if only a default function is given, then it is used to calculate the total
291
+ # if only a block is given then only it is used to calculated the total
292
+ # if both a block and a function are given then the default aggregate function is called first
293
+ # then its result is passed into the block for further processing.
294
+ def subtotal(column_name, function=nil, &b)
295
+ function_or_block = function || b
296
+ f = function && block_given? ? [function, b] : function_or_block
297
+ @subtotals << {column_name => f}
298
+ end
299
+
300
+ def has_subtotals?
301
+ !@subtotals.empty?
302
+ end
303
+
304
+ # define a new total column definition.
305
+ # total columns take the name of the column that should be totaled
306
+ # they also take a default aggregate function name and/or a block
307
+ # if only a default function is given, then it is used to calculate the total
308
+ # if only a block is given then only it is used to calculated the total
309
+ # if both a block and a function are given then the default aggregate function is called first
310
+ # then its result is passed into the block for further processing.
311
+ def total(column_name, function=nil, &b)
312
+ function_or_block = function || b
313
+ f = function && block_given? ? [function, b] : function_or_block
314
+ @totals << {column_name => f}
315
+ end
316
+
317
+ def has_totals?
318
+ !@totals.empty?
319
+ end
320
+
321
+ def calculate_totals!
322
+ @total_calculations = {}
323
+
324
+ @totals.each do |column_name, function|
325
+ collection = @collection.is_a?(Hash) ? @collection.values.flatten : @collection
326
+ result = calculate(collection, column_name, function)
327
+ @total_calculations[column_name] = result
328
+ end
329
+ end
330
+
331
+ def calculate_subtotals!
332
+ @subtotal_calculations = Hash.new { |h,k| h[k] = {} }
333
+
334
+ #ensure that we are dealing with a grouped results set.
335
+ unless @grouped_data
336
+ raise 'Subtotals only work with grouped results sets'
337
+ end
338
+
339
+ @collection.each do |group_name, group_data|
340
+ @subtotals.each do |column_name, function|
341
+ result = calculate(group_data, column_name, function)
342
+ @subtotal_calculations[group_name][column_name] = result
343
+ end
344
+ end
345
+
346
+ end
347
+
348
+ def calculate(data, column_name, function)
349
+
350
+ col = @columns.select { |column| column.name == column_name }
351
+
352
+ if function.is_a?(Proc)
353
+ case function.arity
354
+ when 1; function.call(data)
355
+ when 2; function.call(data, col.first)
356
+ end
357
+ elsif function.is_a?(Array)
358
+ result = self.send("calculate_#{function[0].to_s}", data, column_name)
359
+ case function[1].arity
360
+ when 1; function[1].call(result)
361
+ when 2; function[1].call(result, col.first)
362
+ end
363
+ else
364
+ self.send("calculate_#{function.to_s}", data, column_name)
365
+ end
366
+ end
367
+
368
+ def calculate_sum(collection, column_name)
369
+ collection.inject(0) {|sum, row| sum += row[column_name].to_f }
370
+ end
371
+
372
+ def calculate_avg(collection, column_name)
373
+ sum = calculate_sum(collection, column_name)
374
+ sum / collection.size
375
+ end
376
+
377
+ def calculate_max(collection, column_name)
378
+ collection.collect{|r| r[column_name].to_f }.max
379
+ end
380
+
381
+ def calculate_min(collection, column_name)
382
+ collection.collect{|r| r[column_name].to_f }.min
383
+ end
384
+
385
+ end
@@ -0,0 +1,60 @@
1
+ class DataTableColumn
2
+
3
+ attr_reader :name
4
+ attr_accessor :display, :index, :options, :css_class, :attributes
5
+
6
+ def initialize(name, description="", opts={}, &renderer)
7
+ @name, @description, = name, description
8
+ @data_type = opts[:data_type] || :text
9
+ @help_text = opts[:help_text] || ""
10
+ @css_class = opts[:css_class]
11
+ @attributes = opts[:attributes] || {}
12
+ @width = opts[:width]
13
+ @options = opts
14
+
15
+ @display = true
16
+ @index = 0
17
+ @renderer = renderer
18
+ end
19
+
20
+ def render_cell(cell_data, row=nil, row_index=0, col_index=0)
21
+ html = ""
22
+ if @renderer && row
23
+ cell_data = cell_data.to_s
24
+ html << case @renderer.arity
25
+ when 1; @renderer.call(cell_data).to_s
26
+ when 2; @renderer.call(cell_data, row).to_s
27
+ when 3; @renderer.call(cell_data, row, row_index).to_s
28
+ when 4; @renderer.call(cell_data, row, row_index, self).to_s
29
+ when 5; @renderer.call(cell_data, row, row_index, self, col_index).to_s
30
+ end
31
+ else
32
+ html << cell_data.to_s
33
+ end
34
+
35
+ html << "</td>"
36
+ # Doing this here b/c you can't change @css_class if this is done before the renderer is called
37
+ html = "<td class='#{css_class_names}' #{custom_attributes}>" + html
38
+ end
39
+
40
+ def render_column_header
41
+ header = "<th class='#{css_class_names}' #{custom_attributes}"
42
+ header << "title='#{@help_text}' " if @help_text
43
+ header << "style='width: #{@width}'" if @width
44
+ header << ">#{@description}</th>"
45
+ header
46
+ end
47
+
48
+ def custom_attributes
49
+ @attributes.map{|k, v| "#{k}='#{v}'"}.join ' '
50
+ end
51
+
52
+ def css_class_names
53
+ class_names = []
54
+ class_names << @name.to_s
55
+ class_names << @data_type.to_s
56
+ class_names << @css_class
57
+ class_names.compact.join(' ')
58
+ end
59
+
60
+ end
@@ -0,0 +1,3 @@
1
+ class DataTable
2
+ VERSION = "1.0.0"
3
+ end
metadata ADDED
@@ -0,0 +1,55 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: data-table
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Steve Erickson
9
+ - Jeff Fraser
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2011-09-09 00:00:00.000000000Z
14
+ dependencies: []
15
+ description: data-table is a simple gem that provides a DSL for allowing you do turn
16
+ an array of hashes or ActiveRecord objects into an HTML table.
17
+ email:
18
+ - sixfeetover@gmail.com
19
+ executables: []
20
+ extensions: []
21
+ extra_rdoc_files: []
22
+ files:
23
+ - .gitignore
24
+ - Gemfile
25
+ - Rakefile
26
+ - data-table.gemspec
27
+ - lib/data-table.rb
28
+ - lib/data-table/data_table.rb
29
+ - lib/data-table/data_table_column.rb
30
+ - lib/data-table/version.rb
31
+ homepage: https://github.com/sixfeetover/data-table
32
+ licenses: []
33
+ post_install_message:
34
+ rdoc_options: []
35
+ require_paths:
36
+ - lib
37
+ required_ruby_version: !ruby/object:Gem::Requirement
38
+ none: false
39
+ requirements:
40
+ - - ! '>='
41
+ - !ruby/object:Gem::Version
42
+ version: '0'
43
+ required_rubygems_version: !ruby/object:Gem::Requirement
44
+ none: false
45
+ requirements:
46
+ - - ! '>='
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ requirements: []
50
+ rubyforge_project: data-table
51
+ rubygems_version: 1.8.8
52
+ signing_key:
53
+ specification_version: 3
54
+ summary: Turn arrays of hashes or models in to an HTML table.
55
+ test_files: []