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.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/.rspec +2 -0
- data/.travis.yml +4 -0
- data/Guardfile +48 -0
- data/README.md +128 -12
- data/Rakefile +5 -0
- data/assigments_table.html +56 -0
- data/data-table.gemspec +19 -12
- data/examples/all_features.rb +161 -0
- data/lib/data-table.rb +62 -3
- data/lib/data-table/column.rb +75 -0
- data/lib/data-table/enum.rb +57 -0
- data/lib/data-table/table.rb +443 -0
- data/lib/data-table/version.rb +2 -2
- data/rebuild_gem.rb +11 -0
- data/spec/column_spec.rb +41 -0
- data/spec/data_table_spec.rb +22 -0
- data/spec/enum_spec.rb +36 -0
- data/spec/spec_helper.rb +18 -0
- data/spec/table_spec.rb +100 -0
- metadata +91 -17
- data/lib/data-table/data_table.rb +0 -385
- data/lib/data-table/data_table_column.rb +0 -60
data/lib/data-table/version.rb
CHANGED
@@ -1,3 +1,3 @@
|
|
1
|
-
|
2
|
-
VERSION =
|
1
|
+
module DataTable
|
2
|
+
VERSION = '2.0'.freeze
|
3
3
|
end
|
data/rebuild_gem.rb
ADDED
@@ -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`
|
data/spec/column_spec.rb
ADDED
@@ -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
|
data/spec/enum_spec.rb
ADDED
@@ -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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|
data/spec/table_spec.rb
ADDED
@@ -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:
|
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:
|
14
|
-
dependencies:
|
15
|
-
|
16
|
-
|
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/
|
30
|
-
- lib/data-table/
|
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:
|
121
|
+
rubygems_version: 2.6.11
|
53
122
|
signing_key:
|
54
|
-
specification_version:
|
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
|