data-table 1.0.0 → 2.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,385 +0,0 @@
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
@@ -1,60 +0,0 @@
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