report 0.0.1
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.
- data/.gitignore +17 -0
- data/.yardopts +2 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +137 -0
- data/Rakefile +10 -0
- data/bin/report +3 -0
- data/lib/report.rb +51 -0
- data/lib/report/body.rb +31 -0
- data/lib/report/body/column.rb +48 -0
- data/lib/report/body/row.rb +20 -0
- data/lib/report/body/rows.rb +21 -0
- data/lib/report/csv.rb +21 -0
- data/lib/report/csv/table.rb +26 -0
- data/lib/report/filename.rb +0 -0
- data/lib/report/formatter.rb +0 -0
- data/lib/report/head.rb +25 -0
- data/lib/report/head/row.rb +27 -0
- data/lib/report/pdf.rb +102 -0
- data/lib/report/pdf/DejaVuSansMono-Bold.ttf +0 -0
- data/lib/report/pdf/DejaVuSansMono-BoldOblique.ttf +0 -0
- data/lib/report/pdf/DejaVuSansMono-Oblique.ttf +0 -0
- data/lib/report/pdf/DejaVuSansMono.ttf +0 -0
- data/lib/report/table.rb +21 -0
- data/lib/report/template.rb +0 -0
- data/lib/report/utils.rb +15 -0
- data/lib/report/version.rb +3 -0
- data/lib/report/xlsx.rb +46 -0
- data/report.gemspec +28 -0
- data/spec/report_spec.rb +360 -0
- data/spec/stamp.pdf +0 -0
- metadata +209 -0
data/.gitignore
ADDED
data/.yardopts
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Seamus Abshere
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,137 @@
|
|
1
|
+
# Report
|
2
|
+
|
3
|
+
DSL for creating clean CSV, XLSX, and PDF reports in Ruby.
|
4
|
+
|
5
|
+
Extracted from Brighter Planet's corporate reporting system.
|
6
|
+
|
7
|
+
## Usage
|
8
|
+
|
9
|
+
class FleetReport < Report
|
10
|
+
attr_reader :batchfile
|
11
|
+
|
12
|
+
def initialize(batchfile)
|
13
|
+
@batchfile = batchfile
|
14
|
+
end
|
15
|
+
|
16
|
+
def description
|
17
|
+
'Fleet sustainability report'
|
18
|
+
end
|
19
|
+
|
20
|
+
def vehicles(type)
|
21
|
+
@batchfile.vehicles.where(:type => type)
|
22
|
+
end
|
23
|
+
|
24
|
+
table 'Cars' do
|
25
|
+
head do
|
26
|
+
row 'Report type', :description
|
27
|
+
row 'Batchfile', :batchfile
|
28
|
+
end
|
29
|
+
body do
|
30
|
+
rows :vehicles, ['car']
|
31
|
+
column 'Vehicle ID', :id
|
32
|
+
column 'CO2 score', :carbon
|
33
|
+
column 'CO2 units', 'kg'
|
34
|
+
column 'Fuel grade'
|
35
|
+
column 'Fuel volume'
|
36
|
+
column 'Odometer'
|
37
|
+
column 'City'
|
38
|
+
column 'State'
|
39
|
+
column 'Postal code', :zip_code
|
40
|
+
column 'Country'
|
41
|
+
column 'Methodology'
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
table 'Trucks' do
|
46
|
+
head do
|
47
|
+
row 'Report type', :description
|
48
|
+
row 'Batchfile', :batchfile
|
49
|
+
end
|
50
|
+
body do
|
51
|
+
rows :vehicles, ['truck']
|
52
|
+
column 'Vehicle ID', :id
|
53
|
+
column 'CO2 score', :carbon
|
54
|
+
column 'CO2 units', 'kg'
|
55
|
+
column 'Fuel grade'
|
56
|
+
column 'Fuel volume'
|
57
|
+
column 'Odometer'
|
58
|
+
column 'City'
|
59
|
+
column 'State'
|
60
|
+
column 'Postal code', :zip_code
|
61
|
+
column 'Country'
|
62
|
+
column 'Methodology'
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
format_pdf(
|
67
|
+
:document => { :page_layout => :landscape },
|
68
|
+
:head => {:width => (10*72)},
|
69
|
+
:body => {:width => (10*72), :header => true}
|
70
|
+
)
|
71
|
+
|
72
|
+
format_xlsx do |xlsx|
|
73
|
+
xlsx.header.right.contents = 'Corporate Reporting Program'
|
74
|
+
xlsx.page_setup.top = 1.5
|
75
|
+
xlsx.page_setup.header = 0
|
76
|
+
xlsx.page_setup.footer = 0
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
b = Batchfile.first
|
81
|
+
fr = FleetReport.new(b)
|
82
|
+
fr.pdf.path
|
83
|
+
fr.csv.paths # note one file per table
|
84
|
+
fr.xlsx.path
|
85
|
+
|
86
|
+
## Real-world usage
|
87
|
+
|
88
|
+
<p><a href="http://brighterplanet.com"><img src="https://s3.amazonaws.com/static.brighterplanet.com/assets/logos/flush-left/inline/green/rasterized/brighter_planet-160-transparent.png" alt="Brighter Planet logo"/></a></p>
|
89
|
+
|
90
|
+
We use `report` for [corporate reporting products at Brighter Planet](http://brighterplanet.com/research) and in production at
|
91
|
+
|
92
|
+
* [Brighter Planet's impact estimate web service](http://impact.brighterplanet.com)
|
93
|
+
* [Brighter Planet's reference data web service](http://data.brighterplanet.com)
|
94
|
+
|
95
|
+
## Inspirations
|
96
|
+
|
97
|
+
### dynamicreports
|
98
|
+
|
99
|
+
http://dynamicreports.rubyforge.org/
|
100
|
+
|
101
|
+
class OrdersReport < DynamicReports::Report
|
102
|
+
title "Orders Report"
|
103
|
+
subtitle "All orders recorded in database"
|
104
|
+
columns :total, :created_at
|
105
|
+
|
106
|
+
chart :total_vs_quantity do
|
107
|
+
columns :total, :quantity
|
108
|
+
label_column "created_at"
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# in the controller
|
113
|
+
def orders
|
114
|
+
@orders = Order.find(:all, :limit => 25)
|
115
|
+
render :text => OrdersReport.on(@orders).to_html, :layout => "application"
|
116
|
+
end
|
117
|
+
|
118
|
+
### reportbuilder
|
119
|
+
|
120
|
+
http://ruby-statsample.rubyforge.org/reportbuilder/
|
121
|
+
|
122
|
+
require "reportbuilder"
|
123
|
+
rb=ReportBuilder.new do
|
124
|
+
text("2")
|
125
|
+
section(:name=>"Section 1") do
|
126
|
+
table(:name=>"Table", :header=>%w{id name}) do
|
127
|
+
row([1,"John"])
|
128
|
+
end
|
129
|
+
end
|
130
|
+
preformatted("Another Text")
|
131
|
+
end
|
132
|
+
rb.name="Html output"
|
133
|
+
puts rb.to_html
|
134
|
+
|
135
|
+
## Copyright
|
136
|
+
|
137
|
+
Copyright 2012 Brighter Planet, Inc.
|
data/Rakefile
ADDED
data/bin/report
ADDED
data/lib/report.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'active_support/core_ext'
|
2
|
+
|
3
|
+
require 'report/version'
|
4
|
+
require 'report/utils'
|
5
|
+
|
6
|
+
require 'report/table'
|
7
|
+
require 'report/filename'
|
8
|
+
require 'report/formatter'
|
9
|
+
require 'report/template'
|
10
|
+
require 'report/head'
|
11
|
+
require 'report/body'
|
12
|
+
require 'report/xlsx'
|
13
|
+
require 'report/csv'
|
14
|
+
require 'report/pdf'
|
15
|
+
|
16
|
+
class Report
|
17
|
+
class << self
|
18
|
+
attr_accessor :tables
|
19
|
+
attr_accessor :pdf_format
|
20
|
+
attr_accessor :xlsx_format
|
21
|
+
|
22
|
+
def table(table_name, &blk)
|
23
|
+
tables << Table.new(table_name, &blk)
|
24
|
+
end
|
25
|
+
|
26
|
+
def format_pdf(hsh)
|
27
|
+
self.pdf_format = hsh
|
28
|
+
end
|
29
|
+
|
30
|
+
def format_xlsx(&blk)
|
31
|
+
self.xlsx_format = blk
|
32
|
+
end
|
33
|
+
|
34
|
+
def inherited(klass)
|
35
|
+
klass.tables = []
|
36
|
+
klass.pdf_format = {}
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def csv
|
41
|
+
@csv ||= Csv.new self
|
42
|
+
end
|
43
|
+
|
44
|
+
def xlsx
|
45
|
+
@xlsx ||= Xlsx.new self
|
46
|
+
end
|
47
|
+
|
48
|
+
def pdf
|
49
|
+
@pdf ||= Pdf.new self
|
50
|
+
end
|
51
|
+
end
|
data/lib/report/body.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'report/body/column'
|
2
|
+
require 'report/body/row'
|
3
|
+
require 'report/body/rows'
|
4
|
+
|
5
|
+
class Report
|
6
|
+
class Body
|
7
|
+
attr_reader :table
|
8
|
+
attr_reader :columns
|
9
|
+
def initialize(table, &blk)
|
10
|
+
@table = table
|
11
|
+
@columns = []
|
12
|
+
instance_eval(&blk)
|
13
|
+
end
|
14
|
+
def rows(*args)
|
15
|
+
@rows = Rows.new(*([self]+args))
|
16
|
+
end
|
17
|
+
def column(*args, &blk)
|
18
|
+
@columns << Column.new(*([self]+args), &blk)
|
19
|
+
end
|
20
|
+
def each(report)
|
21
|
+
@rows.each(report) do |obj|
|
22
|
+
yield Row.new(self, obj)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
def to_a(report)
|
26
|
+
a = []
|
27
|
+
each(report) { |row| a << row.to_a }
|
28
|
+
a
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
class Report
|
2
|
+
class Body
|
3
|
+
class Column
|
4
|
+
attr_reader :body
|
5
|
+
attr_reader :name
|
6
|
+
attr_reader :method_id
|
7
|
+
attr_reader :proc
|
8
|
+
attr_reader :faded
|
9
|
+
attr_reader :row_options
|
10
|
+
def initialize(*args, &proc)
|
11
|
+
if block_given?
|
12
|
+
@proc = proc
|
13
|
+
end
|
14
|
+
@body = args.shift
|
15
|
+
@name = args.shift
|
16
|
+
options = args.extract_options!
|
17
|
+
@method_id = options.delete(:method_id) || args.shift
|
18
|
+
@faded = options.delete(:faded)
|
19
|
+
@row_options = options
|
20
|
+
end
|
21
|
+
def read(obj)
|
22
|
+
if @proc
|
23
|
+
obj.instance_eval(&@proc)
|
24
|
+
elsif method_id
|
25
|
+
obj.send method_id
|
26
|
+
elsif from_name = guesses.detect { |m| obj.respond_to?(m) }
|
27
|
+
obj.send from_name
|
28
|
+
else
|
29
|
+
raise "#{obj.inspect} does not respond to any of #{guesses.inspect}"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
def read_with_options(obj)
|
33
|
+
v = read obj
|
34
|
+
f = case faded
|
35
|
+
when Symbol
|
36
|
+
obj.send faded
|
37
|
+
else
|
38
|
+
faded
|
39
|
+
end
|
40
|
+
{ :value => v, :faded => f }.merge row_options
|
41
|
+
end
|
42
|
+
private
|
43
|
+
def guesses
|
44
|
+
[ name, name.underscore.gsub(/\W/, '_') ]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class Report
|
2
|
+
class Body
|
3
|
+
class Row
|
4
|
+
attr_reader :body
|
5
|
+
attr_reader :obj
|
6
|
+
def initialize(body, obj)
|
7
|
+
@body = body
|
8
|
+
@obj = obj
|
9
|
+
end
|
10
|
+
def to_a
|
11
|
+
body.columns.map { |column| column.read(obj) }
|
12
|
+
end
|
13
|
+
def to_hash
|
14
|
+
body.columns.map do |column|
|
15
|
+
column.read_with_options obj
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class Report
|
2
|
+
class Body
|
3
|
+
class Rows
|
4
|
+
attr_reader :body
|
5
|
+
attr_accessor :method_id
|
6
|
+
attr_accessor :args
|
7
|
+
def initialize(*args)
|
8
|
+
@body = args.shift
|
9
|
+
@method_id = args.shift
|
10
|
+
if args.last.is_a?(Array)
|
11
|
+
@args = args.last
|
12
|
+
end
|
13
|
+
end
|
14
|
+
def each(report, &blk)
|
15
|
+
(args ? report.send(method_id, *args) : report.send(method_id)).each do |obj|
|
16
|
+
blk.call obj
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/report/csv.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'csv'
|
2
|
+
|
3
|
+
require 'report/csv/table'
|
4
|
+
|
5
|
+
class Report
|
6
|
+
class Csv
|
7
|
+
attr_reader :report
|
8
|
+
def initialize(report)
|
9
|
+
@report = report
|
10
|
+
end
|
11
|
+
def paths
|
12
|
+
tables.map { |table| table.path }
|
13
|
+
end
|
14
|
+
private
|
15
|
+
def tables
|
16
|
+
@tables ||= report.class.tables.map do |report_table|
|
17
|
+
Csv::Table.new self, report_table
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
class Report
|
2
|
+
class Csv
|
3
|
+
class Table < Struct.new(:parent, :table)
|
4
|
+
include Report::Utils
|
5
|
+
def path
|
6
|
+
return @path if defined?(@path)
|
7
|
+
tmp_path = tmp_path(:hint => table.name, :extname => '.csv')
|
8
|
+
File.open(tmp_path, 'wb') do |f|
|
9
|
+
if table._head
|
10
|
+
table._head.each(parent.report) do |row|
|
11
|
+
f.write row.to_a.to_csv
|
12
|
+
end
|
13
|
+
f.write [].to_csv
|
14
|
+
end
|
15
|
+
if table._body
|
16
|
+
f.write table._body.columns.map(&:name).to_csv
|
17
|
+
table._body.each(parent.report) do |row|
|
18
|
+
f.write row.to_a.to_csv
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
@path = tmp_path
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
File without changes
|
File without changes
|
data/lib/report/head.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'report/head/row'
|
2
|
+
|
3
|
+
class Report
|
4
|
+
class Head
|
5
|
+
attr_reader :table
|
6
|
+
def initialize(table, &blk)
|
7
|
+
@table = table
|
8
|
+
@rows = []
|
9
|
+
instance_eval(&blk)
|
10
|
+
end
|
11
|
+
def row(*cells)
|
12
|
+
@rows << Row.new(self, cells)
|
13
|
+
end
|
14
|
+
def each(report)
|
15
|
+
@rows.each do |row|
|
16
|
+
yield row.read(report)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
def to_a(report)
|
20
|
+
a = []
|
21
|
+
each(report) { |row| a << row.to_a }
|
22
|
+
a
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class Report
|
2
|
+
class Head
|
3
|
+
class Row
|
4
|
+
attr_reader :head
|
5
|
+
attr_reader :cells
|
6
|
+
def initialize(head, cells)
|
7
|
+
@head = head
|
8
|
+
@cells = cells
|
9
|
+
end
|
10
|
+
def read(report)
|
11
|
+
cells.map do |cell|
|
12
|
+
case cell
|
13
|
+
when String
|
14
|
+
cell
|
15
|
+
when Symbol
|
16
|
+
unless report.respond_to?(cell)
|
17
|
+
raise "#{report.inspect} doesn't respond to #{cell.inspect}"
|
18
|
+
end
|
19
|
+
report.send cell
|
20
|
+
else
|
21
|
+
raise "must pass String or Symbol to head row"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/report/pdf.rb
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
class Report
|
4
|
+
class Pdf
|
5
|
+
DEFAULT_FONT = {
|
6
|
+
:normal => File.expand_path('../pdf/DejaVuSansMono.ttf', __FILE__),
|
7
|
+
:italic => File.expand_path('../pdf/DejaVuSansMono-Oblique.ttf', __FILE__),
|
8
|
+
:bold => File.expand_path('../pdf/DejaVuSansMono-Bold.ttf', __FILE__),
|
9
|
+
:bold_italic => File.expand_path('../pdf/DejaVuSansMono-BoldOblique.ttf', __FILE__),
|
10
|
+
}
|
11
|
+
DEFAULT_DOCUMENT = {
|
12
|
+
:top_margin => 118,
|
13
|
+
:right_margin => 36,
|
14
|
+
:bottom_margin => 72,
|
15
|
+
:left_margin => 36,
|
16
|
+
:page_layout => :landscape,
|
17
|
+
}
|
18
|
+
DEFAULT_HEAD = {}
|
19
|
+
DEFAULT_BODY = {
|
20
|
+
:width => (10*72),
|
21
|
+
:header => true
|
22
|
+
}
|
23
|
+
DEFAULT_NUMBER_PAGES = [
|
24
|
+
'Page <page> of <total>',
|
25
|
+
{:at => [648, -2], :width => 100, :size => 10}
|
26
|
+
]
|
27
|
+
|
28
|
+
include Utils
|
29
|
+
|
30
|
+
attr_reader :report
|
31
|
+
|
32
|
+
def initialize(report)
|
33
|
+
@report = report
|
34
|
+
end
|
35
|
+
|
36
|
+
def path
|
37
|
+
return @path if defined?(@path)
|
38
|
+
require 'prawn'
|
39
|
+
tmp_path = tmp_path(:extname => '.pdf')
|
40
|
+
Prawn::Document.generate(tmp_path, document) do |pdf|
|
41
|
+
|
42
|
+
pdf.font_families.update(font_name => font)
|
43
|
+
pdf.font font_name
|
44
|
+
|
45
|
+
report.class.tables.each do |table|
|
46
|
+
if table._head and (t = table._head.to_a(report)).length > 0
|
47
|
+
pdf.table(t, head)
|
48
|
+
end
|
49
|
+
|
50
|
+
pdf.move_down 20
|
51
|
+
pdf.text table.name, :style => :bold
|
52
|
+
pdf.move_down 10
|
53
|
+
|
54
|
+
if table._body and (t = table._body.to_a(report)).length > 0
|
55
|
+
pdf.table(t, body)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
pdf.number_pages(*number_pages)
|
60
|
+
end
|
61
|
+
|
62
|
+
if stamp
|
63
|
+
raise "#{stamp} not readable or does not exist" unless File.readable?(stamp)
|
64
|
+
require 'posix/spawn'
|
65
|
+
POSIX::Spawn::Child.new 'pdftk', tmp_path, 'stamp', stamp, 'output', "#{tmp_path}.stamped"
|
66
|
+
FileUtils.mv "#{tmp_path}.stamped", tmp_path
|
67
|
+
end
|
68
|
+
|
69
|
+
@path = tmp_path
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def font_name
|
75
|
+
'MainFont'
|
76
|
+
end
|
77
|
+
|
78
|
+
def font
|
79
|
+
DEFAULT_FONT.merge report.class.pdf_format.fetch(:font, {})
|
80
|
+
end
|
81
|
+
|
82
|
+
def document
|
83
|
+
DEFAULT_DOCUMENT.merge report.class.pdf_format.fetch(:document, {})
|
84
|
+
end
|
85
|
+
|
86
|
+
def head
|
87
|
+
DEFAULT_HEAD.merge report.class.pdf_format.fetch(:head, {})
|
88
|
+
end
|
89
|
+
|
90
|
+
def body
|
91
|
+
DEFAULT_BODY.merge report.class.pdf_format.fetch(:body, {})
|
92
|
+
end
|
93
|
+
|
94
|
+
def stamp
|
95
|
+
report.class.pdf_format[:stamp]
|
96
|
+
end
|
97
|
+
|
98
|
+
def number_pages
|
99
|
+
report.class.pdf_format.fetch :number_pages, DEFAULT_NUMBER_PAGES
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
data/lib/report/table.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
class Report
|
2
|
+
class Table
|
3
|
+
attr_reader :name
|
4
|
+
def initialize(name, &blk)
|
5
|
+
@name = name
|
6
|
+
instance_eval(&blk)
|
7
|
+
end
|
8
|
+
def body(&blk)
|
9
|
+
@body = Body.new(self, &blk)
|
10
|
+
end
|
11
|
+
def head(&blk)
|
12
|
+
@head = Head.new(self, &blk)
|
13
|
+
end
|
14
|
+
def _head
|
15
|
+
@head
|
16
|
+
end
|
17
|
+
def _body
|
18
|
+
@body
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
File without changes
|
data/lib/report/utils.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'tmpdir'
|
2
|
+
|
3
|
+
class Report
|
4
|
+
module Utils
|
5
|
+
# stolen from https://github.com/seamusabshere/unix_utils
|
6
|
+
def tmp_path(options = {})
|
7
|
+
ancestor = [ self.class.name, options[:hint] ].compact.join('_')
|
8
|
+
extname = options.fetch(:extname, '.tmp')
|
9
|
+
basename = File.basename ancestor.sub(/^\d{9,}_/, '')
|
10
|
+
basename.gsub! /\W/, '_'
|
11
|
+
time = Time.now.strftime('%H%M%S%L')
|
12
|
+
File.join Dir.tmpdir, [time, '_', basename[0..(234-extname.length)], extname].join
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/report/xlsx.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
class Report
|
4
|
+
class Xlsx
|
5
|
+
include Utils
|
6
|
+
attr_reader :report
|
7
|
+
def initialize(report)
|
8
|
+
@report = report
|
9
|
+
end
|
10
|
+
def path
|
11
|
+
return @path if defined?(@path)
|
12
|
+
require 'xlsx_writer'
|
13
|
+
tmp_path = tmp_path(:extname => '.xlsx')
|
14
|
+
workbook = XlsxWriter::Document.new
|
15
|
+
if f = report.class.xlsx_format
|
16
|
+
f.call workbook
|
17
|
+
end
|
18
|
+
report.class.tables.each do |table|
|
19
|
+
sheet = workbook.add_sheet table.name
|
20
|
+
cursor = 1 # excel row numbers start at 1
|
21
|
+
if table._head
|
22
|
+
table._head.each(report) do |row|
|
23
|
+
sheet.add_row row.to_a
|
24
|
+
cursor += 1
|
25
|
+
end
|
26
|
+
sheet.add_row []
|
27
|
+
cursor += 1
|
28
|
+
end
|
29
|
+
if table._body
|
30
|
+
sheet.add_row table._body.columns.map(&:name)
|
31
|
+
table._body.each(report) do |row|
|
32
|
+
sheet.add_row row.to_hash
|
33
|
+
end
|
34
|
+
sheet.add_autofilter calculate_autofilter(table, cursor)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
FileUtils.mv workbook.path, tmp_path
|
38
|
+
workbook.cleanup
|
39
|
+
@path = tmp_path
|
40
|
+
end
|
41
|
+
private
|
42
|
+
def calculate_autofilter(table, cursor)
|
43
|
+
[ 'A', cursor, ':', XlsxWriter::Cell.excel_column_letter(table._body.columns.length-1), cursor ].join
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/report.gemspec
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/report/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Seamus Abshere"]
|
6
|
+
gem.email = ["seamus@abshere.net"]
|
7
|
+
d = %q{DSL for creating clean CSV, XLSX, and PDF reports in Ruby. Extracted from Brighter Planet's corporate reporting system.}
|
8
|
+
gem.description = d
|
9
|
+
gem.summary = d
|
10
|
+
gem.homepage = "https://github.com/seamusabshere/report"
|
11
|
+
|
12
|
+
gem.files = `git ls-files`.split($\)
|
13
|
+
# gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
14
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
15
|
+
gem.name = "report"
|
16
|
+
gem.require_paths = ["lib"]
|
17
|
+
gem.version = Report::VERSION
|
18
|
+
|
19
|
+
gem.add_runtime_dependency 'activesupport'
|
20
|
+
gem.add_runtime_dependency 'xlsx_writer'
|
21
|
+
gem.add_runtime_dependency 'prawn'
|
22
|
+
gem.add_runtime_dependency 'posix-spawn'
|
23
|
+
|
24
|
+
gem.add_development_dependency 'rspec'
|
25
|
+
gem.add_development_dependency 'remote_table'
|
26
|
+
gem.add_development_dependency 'unix_utils'
|
27
|
+
gem.add_development_dependency 'yard'
|
28
|
+
end
|
data/spec/report_spec.rb
ADDED
@@ -0,0 +1,360 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
# 日
|
3
|
+
require 'report'
|
4
|
+
|
5
|
+
require 'remote_table'
|
6
|
+
require 'unix_utils'
|
7
|
+
require 'posix/spawn'
|
8
|
+
|
9
|
+
class Translation < Struct.new(:language, :translation)
|
10
|
+
class << self
|
11
|
+
def all
|
12
|
+
[ new('English', 'Hello'), new('Russian', 'Здравствуйте') ]
|
13
|
+
end
|
14
|
+
end
|
15
|
+
def backward
|
16
|
+
translation.reverse
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class A1 < Report
|
21
|
+
table 'Hello' do
|
22
|
+
head do
|
23
|
+
row 'World'
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
class A2 < Report
|
28
|
+
table 'How to say hello' do
|
29
|
+
body do
|
30
|
+
rows :translations
|
31
|
+
column 'Language'
|
32
|
+
column 'Translation'
|
33
|
+
end
|
34
|
+
end
|
35
|
+
def translations
|
36
|
+
Translation.all
|
37
|
+
end
|
38
|
+
end
|
39
|
+
class A3 < Report
|
40
|
+
table 'Translations' do
|
41
|
+
head do
|
42
|
+
row 'Report type', :description
|
43
|
+
end
|
44
|
+
body do
|
45
|
+
rows :translations
|
46
|
+
column 'Language'
|
47
|
+
column 'Translation'
|
48
|
+
end
|
49
|
+
end
|
50
|
+
def description
|
51
|
+
"How to say hello in a few languages!"
|
52
|
+
end
|
53
|
+
def translations
|
54
|
+
Translation.all
|
55
|
+
end
|
56
|
+
end
|
57
|
+
class A4 < Report
|
58
|
+
table 'Translations and more' do
|
59
|
+
body do
|
60
|
+
rows :translations
|
61
|
+
column 'Language'
|
62
|
+
column 'Forward', :translation
|
63
|
+
column 'Backward'
|
64
|
+
end
|
65
|
+
end
|
66
|
+
def translations
|
67
|
+
Translation.all
|
68
|
+
end
|
69
|
+
end
|
70
|
+
class A5 < Report
|
71
|
+
table 'InEnglish' do
|
72
|
+
body do
|
73
|
+
rows :translations, ['English']
|
74
|
+
column 'Language'
|
75
|
+
column 'Translation'
|
76
|
+
end
|
77
|
+
end
|
78
|
+
table 'InRussian' do
|
79
|
+
body do
|
80
|
+
rows :translations, ['Russian']
|
81
|
+
column 'Language'
|
82
|
+
column 'Translation'
|
83
|
+
end
|
84
|
+
end
|
85
|
+
def translations(language)
|
86
|
+
Translation.all.select { |t| t.language == language }
|
87
|
+
end
|
88
|
+
end
|
89
|
+
class A6 < Report
|
90
|
+
table 'Translations and more, again' do
|
91
|
+
body do
|
92
|
+
rows :translations
|
93
|
+
column 'Language'
|
94
|
+
column('Forward') { translation }
|
95
|
+
column('Backward') { translation.reverse }
|
96
|
+
end
|
97
|
+
end
|
98
|
+
def translations
|
99
|
+
Translation.all
|
100
|
+
end
|
101
|
+
end
|
102
|
+
class A7 < Report
|
103
|
+
format_xlsx do |xlsx|
|
104
|
+
xlsx.header.right.contents = 'Corporate Reporting Program'
|
105
|
+
xlsx.page_setup.top = 1.5
|
106
|
+
xlsx.page_setup.header = 0
|
107
|
+
xlsx.page_setup.footer = 0
|
108
|
+
end
|
109
|
+
table 'Hello' do
|
110
|
+
head do
|
111
|
+
row 'World'
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
class Numero < Struct.new(:d_e_c_i_m_a_l, :m_o_n_e_y)
|
116
|
+
class << self
|
117
|
+
def all
|
118
|
+
[ new(9.9, 2.5) ]
|
119
|
+
end
|
120
|
+
end
|
121
|
+
def always_true
|
122
|
+
true
|
123
|
+
end
|
124
|
+
def always_false
|
125
|
+
false
|
126
|
+
end
|
127
|
+
end
|
128
|
+
class B1 < Report
|
129
|
+
table 'Numbers' do
|
130
|
+
body do
|
131
|
+
rows :numbers
|
132
|
+
column 'd_e_c_i_m_a_l', :type => :Decimal, :faded => :always_true
|
133
|
+
column 'm_o_n_e_y', :type => :Currency, :faded => :always_false
|
134
|
+
end
|
135
|
+
end
|
136
|
+
def numbers
|
137
|
+
Numero.all
|
138
|
+
end
|
139
|
+
end
|
140
|
+
class B2 < Report
|
141
|
+
format_pdf(
|
142
|
+
:document => { :page_layout => :landscape },
|
143
|
+
:head => {:width => (10*72)},
|
144
|
+
:body => {:width => (10*72), :header => true},
|
145
|
+
:font => {
|
146
|
+
:normal => File.expand_path('../../lib/report/pdf/DejaVuSansMono-Oblique.ttf', __FILE__),
|
147
|
+
},
|
148
|
+
:number_pages => ["Page <page> of <total>", {:at => [648, -2], :width => 100, :size => 10}],
|
149
|
+
:stamp => File.expand_path("../stamp.pdf", __FILE__)
|
150
|
+
)
|
151
|
+
table 'Numbers' do
|
152
|
+
body do
|
153
|
+
rows :numbers
|
154
|
+
column 'd_e_c_i_m_a_l', :type => :Decimal, :faded => true
|
155
|
+
column 'm_o_n_e_y', :type => :Currency
|
156
|
+
end
|
157
|
+
end
|
158
|
+
def numbers
|
159
|
+
Numero.all
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
describe Report do
|
164
|
+
describe '#csv' do
|
165
|
+
it "writes each table to a separate file" do
|
166
|
+
hello = ::CSV.read A1.new.csv.paths.first
|
167
|
+
hello[0][0].should == 'World'
|
168
|
+
end
|
169
|
+
it "constructs a body out of rows and columns" do
|
170
|
+
how_to_say_hello = ::CSV.read A2.new.csv.paths.first, :headers => :first_row
|
171
|
+
how_to_say_hello[0]['Language'].should == 'English'
|
172
|
+
how_to_say_hello[0]['Translation'].should == 'Hello'
|
173
|
+
how_to_say_hello[1]['Language'].should == 'Russian'
|
174
|
+
how_to_say_hello[1]['Translation'].should == 'Здравствуйте'
|
175
|
+
end
|
176
|
+
it "puts a blank row between head and body" do
|
177
|
+
transl_with_head = ::CSV.read A3.new.csv.paths.first, :headers => false
|
178
|
+
transl_with_head[0][0].should == "Report type"
|
179
|
+
transl_with_head[0][1].should == "How to say hello in a few languages!"
|
180
|
+
transl_with_head[4][0].should == "Russian"
|
181
|
+
transl_with_head[4][1].should == 'Здравствуйте'
|
182
|
+
end
|
183
|
+
it "passes arguments on columns" do
|
184
|
+
t = ::CSV.read A4.new.csv.paths.first, :headers => :first_row
|
185
|
+
en = t[0]
|
186
|
+
ru = t[1]
|
187
|
+
en['Language'].should == 'English'
|
188
|
+
en['Forward'].should == 'Hello'
|
189
|
+
en['Backward'].should == 'Hello'.reverse
|
190
|
+
ru['Language'].should == 'Russian'
|
191
|
+
ru['Forward'].should == 'Здравствуйте'
|
192
|
+
ru['Backward'].should == 'Здравствуйте'.reverse
|
193
|
+
end
|
194
|
+
it "passes arguments on rows" do
|
195
|
+
en_path, ru_path = A5.new.csv.paths
|
196
|
+
en = ::CSV.read en_path, :headers => :first_row
|
197
|
+
en.length.should == 1
|
198
|
+
en[0]['Language'].should == 'English'
|
199
|
+
en[0]['Translation'].should == 'Hello'
|
200
|
+
ru = ::CSV.read ru_path, :headers => :first_row
|
201
|
+
ru.length.should == 1
|
202
|
+
ru[0]['Language'].should == 'Russian'
|
203
|
+
ru[0]['Translation'].should == 'Здравствуйте'
|
204
|
+
end
|
205
|
+
it "instance-evals column blocks against row objects" do
|
206
|
+
t = ::CSV.read A6.new.csv.paths.first, :headers => :first_row
|
207
|
+
en = t[0]
|
208
|
+
ru = t[1]
|
209
|
+
en['Language'].should == 'English'
|
210
|
+
en['Forward'].should == 'Hello'
|
211
|
+
en['Backward'].should == 'Hello'.reverse
|
212
|
+
ru['Language'].should == 'Russian'
|
213
|
+
ru['Forward'].should == 'Здравствуйте'
|
214
|
+
ru['Backward'].should == 'Здравствуйте'.reverse
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
describe '#xlsx' do
|
219
|
+
it "writes all tables to the same file" do
|
220
|
+
hello = RemoteTable.new A1.new.xlsx.path, :headers => false
|
221
|
+
hello[0][0].should == 'World'
|
222
|
+
end
|
223
|
+
it "constructs a body out of rows and columns" do
|
224
|
+
how_to_say_hello = RemoteTable.new A2.new.xlsx.path, :headers => :first_row
|
225
|
+
how_to_say_hello[0]['Language'].should == 'English'
|
226
|
+
how_to_say_hello[0]['Translation'].should == 'Hello'
|
227
|
+
how_to_say_hello[1]['Language'].should == 'Russian'
|
228
|
+
how_to_say_hello[1]['Translation'].should == 'Здравствуйте'
|
229
|
+
end
|
230
|
+
it "puts a blank row between head and body" do
|
231
|
+
transl_with_head = RemoteTable.new A3.new.xlsx.path, :headers => false, :keep_blank_rows => true
|
232
|
+
transl_with_head[0][0].should == "Report type"
|
233
|
+
transl_with_head[0][1].should == "How to say hello in a few languages!"
|
234
|
+
transl_with_head[4][0].should == "Russian"
|
235
|
+
transl_with_head[4][1].should == 'Здравствуйте'
|
236
|
+
end
|
237
|
+
it "passes arguments on columns" do
|
238
|
+
t = RemoteTable.new A4.new.xlsx.path, :headers => :first_row
|
239
|
+
en = t[0]
|
240
|
+
ru = t[1]
|
241
|
+
en['Language'].should == 'English'
|
242
|
+
en['Forward'].should == 'Hello'
|
243
|
+
en['Backward'].should == 'Hello'.reverse
|
244
|
+
ru['Language'].should == 'Russian'
|
245
|
+
ru['Forward'].should == 'Здравствуйте'
|
246
|
+
ru['Backward'].should == 'Здравствуйте'.reverse
|
247
|
+
end
|
248
|
+
it "passes arguments on rows" do
|
249
|
+
path = A5.new.xlsx.path
|
250
|
+
en = RemoteTable.new(path, :headers => :first_row, :sheet => 'InEnglish').to_a
|
251
|
+
en.length.should == 1
|
252
|
+
en[0]['Language'].should == 'English'
|
253
|
+
en[0]['Translation'].should == 'Hello'
|
254
|
+
ru = RemoteTable.new(path, :headers => :first_row, :sheet => 'InRussian').to_a
|
255
|
+
ru.length.should == 1
|
256
|
+
ru[0]['Language'].should == 'Russian'
|
257
|
+
ru[0]['Translation'].should == 'Здравствуйте'
|
258
|
+
end
|
259
|
+
it "instance-evals column blocks against row objects" do
|
260
|
+
t = RemoteTable.new A6.new.xlsx.path, :headers => :first_row
|
261
|
+
en = t[0]
|
262
|
+
ru = t[1]
|
263
|
+
en['Language'].should == 'English'
|
264
|
+
en['Forward'].should == 'Hello'
|
265
|
+
en['Backward'].should == 'Hello'.reverse
|
266
|
+
ru['Language'].should == 'Russian'
|
267
|
+
ru['Forward'].should == 'Здравствуйте'
|
268
|
+
ru['Backward'].should == 'Здравствуйте'.reverse
|
269
|
+
end
|
270
|
+
it "accepts a formatter that works on the raw XlsxWriter::Document" do
|
271
|
+
path = A7.new.xlsx.path
|
272
|
+
dir = UnixUtils.unzip path
|
273
|
+
File.read("#{dir}/xl/worksheets/sheet1.xml").should include('Corporate Reporting Program')
|
274
|
+
FileUtils.rm_f path
|
275
|
+
end
|
276
|
+
it "allows setting cell options" do
|
277
|
+
path = B1.new.xlsx.path
|
278
|
+
dir = UnixUtils.unzip path
|
279
|
+
xml = File.read("#{dir}/xl/worksheets/sheet1.xml")
|
280
|
+
xml.should match(/s="2".*2.5/) # Currency
|
281
|
+
xml.should match(/s="9".*9.9/) # faded Decimal
|
282
|
+
FileUtils.rm_f path
|
283
|
+
FileUtils.rm_rf dir
|
284
|
+
end
|
285
|
+
it "automatically adds an autofilter" do
|
286
|
+
path = A2.new.xlsx.path
|
287
|
+
dir = UnixUtils.unzip path
|
288
|
+
xml = File.read("#{dir}/xl/worksheets/sheet1.xml")
|
289
|
+
xml.should include('autoFilter ref="A1:B1')
|
290
|
+
FileUtils.rm_f path
|
291
|
+
FileUtils.rm_rf dir
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
describe '#pdf' do
|
296
|
+
it "writes all tables to the same file" do
|
297
|
+
hello = A1.new.pdf.path
|
298
|
+
child = POSIX::Spawn::Child.new('pdftotext', hello, '-')
|
299
|
+
stdout_utf8 = child.out.force_encoding('UTF-8')
|
300
|
+
stdout_utf8.should include('World')
|
301
|
+
end
|
302
|
+
it "constructs a body out of rows and columns" do
|
303
|
+
how_to_say_hello = A2.new.pdf.path
|
304
|
+
child = POSIX::Spawn::Child.new('pdftotext', how_to_say_hello, '-')
|
305
|
+
stdout_utf8 = child.out.force_encoding('UTF-8')
|
306
|
+
stdout_utf8.should include('English')
|
307
|
+
stdout_utf8.should include('Hello')
|
308
|
+
stdout_utf8.should include('Russian')
|
309
|
+
stdout_utf8.should include('Здравствуйте')
|
310
|
+
end
|
311
|
+
it "puts a blank row between head and body" do
|
312
|
+
transl_with_head = A3.new.pdf.path
|
313
|
+
child = POSIX::Spawn::Child.new('pdftotext', transl_with_head, '-')
|
314
|
+
stdout_utf8 = child.out.force_encoding('UTF-8')
|
315
|
+
stdout_utf8.should include("Report type")
|
316
|
+
stdout_utf8.should include("How to say hello in a few languages!")
|
317
|
+
stdout_utf8.should include('Russian')
|
318
|
+
stdout_utf8.should include('Здравствуйте')
|
319
|
+
end
|
320
|
+
it "passes arguments on columns" do
|
321
|
+
t = A4.new.pdf.path
|
322
|
+
child = POSIX::Spawn::Child.new('pdftotext', t, '-')
|
323
|
+
stdout_utf8 = child.out.force_encoding('UTF-8')
|
324
|
+
stdout_utf8.should include('English')
|
325
|
+
stdout_utf8.should include('Hello')
|
326
|
+
stdout_utf8.should include('Hello'.reverse)
|
327
|
+
stdout_utf8.should include('Russian')
|
328
|
+
stdout_utf8.should include('Здравствуйте')
|
329
|
+
stdout_utf8.should include('Здравствуйте'.reverse)
|
330
|
+
end
|
331
|
+
it "passes arguments on rows" do
|
332
|
+
path = A5.new.pdf.path
|
333
|
+
child = POSIX::Spawn::Child.new('pdftotext', path, '-')
|
334
|
+
stdout_utf8 = child.out.force_encoding('UTF-8')
|
335
|
+
stdout_utf8.should include('InEnglish')
|
336
|
+
stdout_utf8.should include('InRussian')
|
337
|
+
stdout_utf8.should include('English')
|
338
|
+
stdout_utf8.should include('Hello')
|
339
|
+
stdout_utf8.should include('Russian')
|
340
|
+
stdout_utf8.should include('Здравствуйте')
|
341
|
+
end
|
342
|
+
it "instance-evals column blocks against row objects" do
|
343
|
+
t = A6.new.pdf.path
|
344
|
+
child = POSIX::Spawn::Child.new('pdftotext', t, '-')
|
345
|
+
stdout_utf8 = child.out.force_encoding('UTF-8')
|
346
|
+
stdout_utf8.should include('English')
|
347
|
+
stdout_utf8.should include('Hello')
|
348
|
+
stdout_utf8.should include('Hello'.reverse)
|
349
|
+
stdout_utf8.should include('Russian')
|
350
|
+
stdout_utf8.should include('Здравствуйте')
|
351
|
+
stdout_utf8.should include('Здравствуйте'.reverse)
|
352
|
+
end
|
353
|
+
it "accepts pdf formatting options, including the ability to stamp with pdftk" do
|
354
|
+
path = B2.new.pdf.path
|
355
|
+
child = POSIX::Spawn::Child.new('pdftotext', path, '-')
|
356
|
+
stdout_utf8 = child.out.force_encoding('UTF-8')
|
357
|
+
stdout_utf8.should include('Firefox')
|
358
|
+
end
|
359
|
+
end
|
360
|
+
end
|
data/spec/stamp.pdf
ADDED
Binary file
|
metadata
ADDED
@@ -0,0 +1,209 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: report
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Seamus Abshere
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-07-04 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: activesupport
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: xlsx_writer
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: prawn
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: posix-spawn
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :runtime
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: rspec
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
type: :development
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
name: remote_table
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ! '>='
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
type: :development
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ! '>='
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: unix_utils
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
none: false
|
114
|
+
requirements:
|
115
|
+
- - ! '>='
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
none: false
|
122
|
+
requirements:
|
123
|
+
- - ! '>='
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
126
|
+
- !ruby/object:Gem::Dependency
|
127
|
+
name: yard
|
128
|
+
requirement: !ruby/object:Gem::Requirement
|
129
|
+
none: false
|
130
|
+
requirements:
|
131
|
+
- - ! '>='
|
132
|
+
- !ruby/object:Gem::Version
|
133
|
+
version: '0'
|
134
|
+
type: :development
|
135
|
+
prerelease: false
|
136
|
+
version_requirements: !ruby/object:Gem::Requirement
|
137
|
+
none: false
|
138
|
+
requirements:
|
139
|
+
- - ! '>='
|
140
|
+
- !ruby/object:Gem::Version
|
141
|
+
version: '0'
|
142
|
+
description: DSL for creating clean CSV, XLSX, and PDF reports in Ruby. Extracted
|
143
|
+
from Brighter Planet's corporate reporting system.
|
144
|
+
email:
|
145
|
+
- seamus@abshere.net
|
146
|
+
executables: []
|
147
|
+
extensions: []
|
148
|
+
extra_rdoc_files: []
|
149
|
+
files:
|
150
|
+
- .gitignore
|
151
|
+
- .yardopts
|
152
|
+
- Gemfile
|
153
|
+
- LICENSE
|
154
|
+
- README.md
|
155
|
+
- Rakefile
|
156
|
+
- bin/report
|
157
|
+
- lib/report.rb
|
158
|
+
- lib/report/body.rb
|
159
|
+
- lib/report/body/column.rb
|
160
|
+
- lib/report/body/row.rb
|
161
|
+
- lib/report/body/rows.rb
|
162
|
+
- lib/report/csv.rb
|
163
|
+
- lib/report/csv/table.rb
|
164
|
+
- lib/report/filename.rb
|
165
|
+
- lib/report/formatter.rb
|
166
|
+
- lib/report/head.rb
|
167
|
+
- lib/report/head/row.rb
|
168
|
+
- lib/report/pdf.rb
|
169
|
+
- lib/report/pdf/DejaVuSansMono-Bold.ttf
|
170
|
+
- lib/report/pdf/DejaVuSansMono-BoldOblique.ttf
|
171
|
+
- lib/report/pdf/DejaVuSansMono-Oblique.ttf
|
172
|
+
- lib/report/pdf/DejaVuSansMono.ttf
|
173
|
+
- lib/report/table.rb
|
174
|
+
- lib/report/template.rb
|
175
|
+
- lib/report/utils.rb
|
176
|
+
- lib/report/version.rb
|
177
|
+
- lib/report/xlsx.rb
|
178
|
+
- report.gemspec
|
179
|
+
- spec/report_spec.rb
|
180
|
+
- spec/stamp.pdf
|
181
|
+
homepage: https://github.com/seamusabshere/report
|
182
|
+
licenses: []
|
183
|
+
post_install_message:
|
184
|
+
rdoc_options: []
|
185
|
+
require_paths:
|
186
|
+
- lib
|
187
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
188
|
+
none: false
|
189
|
+
requirements:
|
190
|
+
- - ! '>='
|
191
|
+
- !ruby/object:Gem::Version
|
192
|
+
version: '0'
|
193
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
194
|
+
none: false
|
195
|
+
requirements:
|
196
|
+
- - ! '>='
|
197
|
+
- !ruby/object:Gem::Version
|
198
|
+
version: '0'
|
199
|
+
requirements: []
|
200
|
+
rubyforge_project:
|
201
|
+
rubygems_version: 1.8.24
|
202
|
+
signing_key:
|
203
|
+
specification_version: 3
|
204
|
+
summary: DSL for creating clean CSV, XLSX, and PDF reports in Ruby. Extracted from
|
205
|
+
Brighter Planet's corporate reporting system.
|
206
|
+
test_files:
|
207
|
+
- spec/report_spec.rb
|
208
|
+
- spec/stamp.pdf
|
209
|
+
has_rdoc:
|