report 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: