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.
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
@@ -0,0 +1,2 @@
1
+ --no-private
2
+ --readme README.md
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in report.gemspec
4
+ gemspec
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.
@@ -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.
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env rake
2
+ require 'bundler/gem_tasks'
3
+ require 'rspec/core/rake_task'
4
+
5
+ RSpec::Core::RakeTask.new(:spec)
6
+
7
+ task :default => :spec
8
+
9
+ require 'yard'
10
+ YARD::Rake::YardocTask.new
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'report'
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,3 @@
1
+ class Report
2
+ VERSION = "0.0.1"
3
+ end
@@ -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
@@ -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
@@ -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
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: