data-table 1.0.1 → 2.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.
@@ -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