data-table 1.0.1 → 2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,3 @@
1
- class DataTable
2
- VERSION = "1.0.1"
1
+ module DataTable
2
+ VERSION = '2.0'.freeze
3
3
  end
@@ -0,0 +1,11 @@
1
+ puts "Removing old gem file"
2
+ `rm data-table*.gem`
3
+
4
+ puts "Building new data-table gem"
5
+ `gem build ./data-table.gemspec`
6
+
7
+ puts "Uninstalling old data-table gem"
8
+ `gem uninstall -a data-table`
9
+
10
+ puts "Installing new build of data-table gem"
11
+ `gem install ./data-table*.gem`
@@ -0,0 +1,41 @@
1
+ require 'spec_helper'
2
+
3
+ describe DataTable::Column do
4
+ it "should store the name" do
5
+ column = DataTable::Column.new(:thing)
6
+ expect(column.name).to eq(:thing)
7
+ end
8
+
9
+ it "should add the column name as a css class" do
10
+ column = DataTable::Column.new(:thing)
11
+ expect(column.css_class_names).to include('thing')
12
+ end
13
+
14
+ it "should render a td tag" do
15
+ column = DataTable::Column.new(:thing)
16
+ expect(column.render_cell("Data")).to eq(%(<td class='thing text' >Data</td>))
17
+ end
18
+
19
+ it "should render the column header" do
20
+ column = DataTable::Column.new(:thing, 'Thing')
21
+ expect(column.render_column_header).to eq(%(<th class='thing ' >Thing</th>))
22
+ end
23
+
24
+ it "should add custom attributes to the td tag" do
25
+ options = {
26
+ attributes: {
27
+ 'data-type' => 'text',
28
+ 'data-id' => 1
29
+ }
30
+ }
31
+ column = DataTable::Column.new(:thing, 'Thing', options)
32
+ expect(column.custom_attributes).to eq("data-type='text' data-id='1'")
33
+ expect(column.render_cell('Data')).to include("data-type='text'")
34
+ end
35
+
36
+ it "should use the block for rendering" do
37
+ square = lambda { |v| v.to_i ** 2 }
38
+ column = DataTable::Column.new(:amount, 'Amount', &square)
39
+ expect(column.render_cell(5, amount: 5)).to eq(%(<td class='amount numeric' >25</td>))
40
+ end
41
+ end
@@ -0,0 +1,22 @@
1
+ require 'spec_helper'
2
+
3
+ describe DataTable do
4
+ let(:collection) {
5
+ [
6
+ {:name => 'Luke Skywalker', :class => 'Jedi Knight'},
7
+ {:name => 'Emporer Palpatine', :class => 'Sith Lord'},
8
+ {:name => 'Mithrander', :class => 'Wizard'},
9
+ {:name => 'Aragorn', :class => 'Ranger'}
10
+ ]
11
+ }
12
+
13
+
14
+ it "should render the collection" do
15
+ html = DataTable.render(collection) do |t|
16
+ t.column :name, 'Name'
17
+ t.column :class, 'Class'
18
+ end
19
+
20
+ expect(html).to eq(%{<table id='' class='data_table ' cellspacing='0' cellpadding='0'><caption></caption><thead><tr><th class='name ' >Name</th><th class='class ' >Class</th></tr></thead><tbody><tr class='row_0 ' ><td class='name text' >Luke Skywalker</td><td class='class text' >Jedi Knight</td></tr><tr class='row_1 alt ' ><td class='name text' >Emporer Palpatine</td><td class='class text' >Sith Lord</td></tr><tr class='row_2 ' ><td class='name text' >Mithrander</td><td class='class text' >Wizard</td></tr><tr class='row_3 alt ' ><td class='name text' >Aragorn</td><td class='class text' >Ranger</td></tr></tbody></table>})
21
+ end
22
+ end
@@ -0,0 +1,36 @@
1
+ require 'spec_helper'
2
+
3
+ describe Enumerable do
4
+ context "with a non-empty collection of hashes" do
5
+ let(:collection) {
6
+ [
7
+ {name: 'Luke Skywalker', class: 'Jedi Knight', world: 'Star Wars', power_level: 50},
8
+ {name: 'Emporer Palpatine', class: 'Sith Lord', world: 'Star Wars', power_level: 95},
9
+ {name: 'Mithrander', class: 'Wizard', world: 'Middle Earth', power_level: 9001},
10
+ {name: 'Aragorn', class: 'Ranger', world: 'Middle Earth', power_level: 80}
11
+ ]
12
+ }
13
+
14
+ let(:groupings) { [:class] }
15
+
16
+ it "should transform a collection into nested hash based on and array of groups" do
17
+ expect(
18
+ collection.group_by_recursive(groupings)
19
+ ).to eq(
20
+ {
21
+ "Jedi Knight"=>[{:name=>"Luke Skywalker", :class=>"Jedi Knight", :world=>"Star Wars", :power_level=>50}],
22
+ "Sith Lord"=>[{:name=>"Emporer Palpatine", :class=>"Sith Lord", :world=>"Star Wars", :power_level=>95}],
23
+ "Wizard"=>[{:name=>"Mithrander", :class=>"Wizard", :world=>"Middle Earth", :power_level=>9001}],
24
+ "Ranger"=>[{:name=>"Aragorn", :class=>"Ranger", :world=>"Middle Earth", :power_level=>80}]
25
+ }
26
+ )
27
+ end
28
+
29
+ it "should traverse a nested hash" do
30
+ grouped_collection = collection.group_by_recursive(groupings)
31
+ ungrouped = []
32
+ grouped_collection.each_pair_recursive { |_k, v| ungrouped.concat(v) }
33
+ expect(ungrouped).to eq(collection)
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,18 @@
1
+ require 'data-table'
2
+ require 'pry'
3
+ # This file was generated by the `rspec --init` command. Conventionally, all
4
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
5
+ # Require this file using `require "spec_helper"` to ensure that it is only
6
+ # loaded once.
7
+ #
8
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
9
+ RSpec.configure do |config|
10
+ config.run_all_when_everything_filtered = true
11
+ config.filter_run :focus
12
+
13
+ # Run specs in random order to surface order dependencies. If you find an
14
+ # order dependency and want to debug it, you can fix the order by providing
15
+ # the seed, which is printed after each run.
16
+ # --seed 1234
17
+ config.order = 'random'
18
+ end
@@ -0,0 +1,100 @@
1
+ require 'spec_helper'
2
+
3
+ describe DataTable::Table do
4
+ context "with a non-empty collection of hashes" do
5
+ let(:collection) {
6
+ [
7
+ {:name => 'Luke Skywalker', :class => 'Jedi Knight', :world => 'Star Wars', :power_level => 50},
8
+ {:name => 'Emporer Palpatine', :class => 'Sith Lord', :world => 'Star Wars', :power_level => 95},
9
+ {:name => 'Mithrander', :class => 'Wizard', :world => 'Middle Earth', :power_level => 9001},
10
+ {:name => 'Aragorn', :class => 'Ranger', :world => 'Middle Earth', :power_level => 80}
11
+ ]
12
+ }
13
+
14
+ let(:data_table) {DataTable::Table.new(collection)}
15
+
16
+ it "should add a column do @columns" do
17
+ data_table.column(:name, 'Name')
18
+ expect(data_table.columns).to_not be_empty
19
+ expect(data_table.columns.first.class).to be(DataTable::Column)
20
+ end
21
+
22
+ it "should render the collection" do
23
+ data_table.column(:name, 'Name')
24
+ data_table.column(:class, 'Class')
25
+ expect(data_table.render).to \
26
+ eq(%{<table id='' class='data_table ' cellspacing='0' cellpadding='0'><caption></caption><thead><tr><th class='name ' >Name</th><th class='class ' >Class</th></tr></thead><tbody><tr class='row_0 ' ><td class='name text' >Luke Skywalker</td><td class='class text' >Jedi Knight</td></tr><tr class='row_1 alt ' ><td class='name text' >Emporer Palpatine</td><td class='class text' >Sith Lord</td></tr><tr class='row_2 ' ><td class='name text' >Mithrander</td><td class='class text' >Wizard</td></tr><tr class='row_3 alt ' ><td class='name text' >Aragorn</td><td class='class text' >Ranger</td></tr></tbody></table>})
27
+ end
28
+
29
+ it "should group the records" do
30
+ grouping_column = :world
31
+
32
+ data_table.group_by grouping_column, level: 0
33
+ data_table.column(:name, 'Name')
34
+ data_table.column(:class, 'Class')
35
+ expect(data_table.grouped_data).to be true
36
+ data_table.prepare_data
37
+ expect(data_table.collection).to eq(collection.group_by {|g| g[grouping_column]})
38
+ expect(data_table.render).to eq(%{<table id='' class='data_table ' cellspacing='0' cellpadding='0'><caption></caption><thead><tr><th class='name ' >Name</th><th class='class ' >Class</th></tr></thead><tbody class='star_wars'><tr class='group_header level_0'><th colspan='2'>Star Wars</th></tr><tr class='row_0 ' ><td class='name text' >Luke Skywalker</td><td class='class text' >Jedi Knight</td></tr><tr class='row_1 alt ' ><td class='name text' >Emporer Palpatine</td><td class='class text' >Sith Lord</td></tr></tbody><tbody class='middle_earth'><tr class='group_header level_0'><th colspan='2'>Middle Earth</th></tr><tr class='row_0 ' ><td class='name text' >Mithrander</td><td class='class text' >Wizard</td></tr><tr class='row_1 alt ' ><td class='name text' >Aragorn</td><td class='class text' >Ranger</td></tr></tbody></table>})
39
+ end
40
+
41
+ it "should do totaling" do
42
+ data_table.column :power_level
43
+ data_table.total :power_level, :sum, 0
44
+ data_table.calculate_totals!
45
+ expect(data_table.total_calculations).to eq([{:power_level=>9226.0}])
46
+ end
47
+
48
+ it "should do custom formatting for the total" do
49
+ data_table.column :power_level
50
+ data_table.total :power_level, :avg, 0 do |average|
51
+ "#{average / 100.0}%"
52
+ end
53
+ data_table.calculate_totals!
54
+ expect(data_table.total_calculations).to eq([{:power_level=>"23.065%"}])
55
+ end
56
+
57
+ it "should do custom totalling" do
58
+ data_table.column :power_level
59
+ data_table.total :power_level do |collection|
60
+ collection.inject(0) { |sum, c| sum + c[:power_level] }
61
+ end
62
+ data_table.calculate_totals!
63
+ expect(data_table.total_calculations).to eq([{:power_level=>9226}])
64
+ end
65
+
66
+ it "should do sub-totaling" do
67
+ data_table.group_by :world, level: 0
68
+ data_table.column :power_level
69
+ data_table.subtotal :power_level, :sum, 0
70
+
71
+ data_table.prepare_data
72
+ expect(data_table.subtotal_calculations).to eq({["Star Wars"]=>[{:power_level=>{:sum=>145.0}}], ["Middle Earth"]=>[{:power_level=>{:sum=>9081.0}}]})
73
+ end
74
+
75
+ it "should render a custom header" do
76
+ data_table.custom_header do
77
+ th 'Two Columns', :colspan => 2
78
+ th 'One Column', :colspan => 1
79
+ end
80
+ expect(data_table.render_custom_table_header).to eq(%{<tr class='custom-header'><th class="" colspan="2" style="">Two Columns</th><th class="" colspan="1" style="">One Column</th></tr>})
81
+ end
82
+ end
83
+
84
+ context "with an empty collection" do
85
+ let(:collection) {Array.new}
86
+ let(:data_table) {DataTable::Table.new(collection)}
87
+
88
+ it "should render a table with the 'no records' message" do
89
+ expect(data_table.render).to \
90
+ eq(%{<table id='' class='data_table ' cellspacing='0' cellpadding='0'><caption></caption><thead><tr></tr></thead><tr><td class='empty_data_table' colspan='0'>No records found</td></tr></table>})
91
+ end
92
+
93
+ it "should render a custom empty text notice" do
94
+ text = "Nothing to see here"
95
+ data_table.empty_text = text
96
+ expect(data_table.render).to \
97
+ eq(%{<table id='' class='data_table ' cellspacing='0' cellpadding='0'><caption></caption><thead><tr></tr></thead><tr><td class='empty_data_table' colspan='0'>#{text}</td></tr></table>})
98
+ end
99
+ end
100
+ end
metadata CHANGED
@@ -1,8 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: data-table
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
5
- prerelease:
4
+ version: '2.0'
6
5
  platform: ruby
7
6
  authors:
8
7
  - Steve Erickson
@@ -10,47 +9,122 @@ authors:
10
9
  autorequire:
11
10
  bindir: bin
12
11
  cert_chain: []
13
- date: 2011-10-26 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.
12
+ date: 2017-04-03 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rake
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '12'
21
+ type: :development
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: '12'
28
+ - !ruby/object:Gem::Dependency
29
+ name: rspec
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: '3'
35
+ type: :development
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '3'
42
+ - !ruby/object:Gem::Dependency
43
+ name: guard
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '2'
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: '2'
56
+ - !ruby/object:Gem::Dependency
57
+ name: guard-rspec
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - "~>"
61
+ - !ruby/object:Gem::Version
62
+ version: '4'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: '4'
70
+ description: |-
71
+ data-table is a simple gem that provides a DSL for
72
+ turning an array of hashes or ActiveRecord objects into an
73
+ HTML table.
17
74
  email:
18
75
  - sixfeetover@gmail.com
19
76
  executables: []
20
77
  extensions: []
21
78
  extra_rdoc_files: []
22
79
  files:
23
- - .gitignore
80
+ - ".gitignore"
81
+ - ".rspec"
82
+ - ".travis.yml"
24
83
  - Gemfile
84
+ - Guardfile
25
85
  - README.md
26
86
  - Rakefile
87
+ - assigments_table.html
27
88
  - data-table.gemspec
89
+ - examples/all_features.rb
28
90
  - lib/data-table.rb
29
- - lib/data-table/data_table.rb
30
- - lib/data-table/data_table_column.rb
91
+ - lib/data-table/column.rb
92
+ - lib/data-table/enum.rb
93
+ - lib/data-table/table.rb
31
94
  - lib/data-table/version.rb
95
+ - rebuild_gem.rb
96
+ - spec/column_spec.rb
97
+ - spec/data_table_spec.rb
98
+ - spec/enum_spec.rb
99
+ - spec/spec_helper.rb
100
+ - spec/table_spec.rb
32
101
  homepage: https://github.com/sixfeetover/data-table
33
- licenses: []
102
+ licenses:
103
+ - Nonstandard
104
+ metadata: {}
34
105
  post_install_message:
35
106
  rdoc_options: []
36
107
  require_paths:
37
108
  - lib
38
109
  required_ruby_version: !ruby/object:Gem::Requirement
39
- none: false
40
110
  requirements:
41
- - - ! '>='
111
+ - - ">="
42
112
  - !ruby/object:Gem::Version
43
113
  version: '0'
44
114
  required_rubygems_version: !ruby/object:Gem::Requirement
45
- none: false
46
115
  requirements:
47
- - - ! '>='
116
+ - - ">="
48
117
  - !ruby/object:Gem::Version
49
118
  version: '0'
50
119
  requirements: []
51
120
  rubyforge_project: data-table
52
- rubygems_version: 1.8.8
121
+ rubygems_version: 2.6.11
53
122
  signing_key:
54
- specification_version: 3
123
+ specification_version: 4
55
124
  summary: Turn arrays of hashes or models in to an HTML table.
56
- test_files: []
125
+ test_files:
126
+ - spec/column_spec.rb
127
+ - spec/data_table_spec.rb
128
+ - spec/enum_spec.rb
129
+ - spec/spec_helper.rb
130
+ - spec/table_spec.rb
@@ -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.merge!({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.merge!({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