data-table 1.0.0

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