ruport 0.2.9 → 0.3.8
Sign up to get free protection for your applications and to get access to all the features.
- data/ACKNOWLEDGEMENTS +33 -0
- data/AUTHORS +13 -1
- data/CHANGELOG +76 -1
- data/README +208 -89
- data/Rakefile +12 -8
- data/TODO +14 -122
- data/lib/ruport.rb +58 -0
- data/lib/ruport/config.rb +114 -0
- data/lib/ruport/data_row.rb +144 -0
- data/lib/ruport/data_set.rb +221 -0
- data/lib/ruport/format.rb +116 -0
- data/lib/ruport/format/builder.rb +29 -5
- data/lib/ruport/format/document.rb +77 -0
- data/lib/ruport/format/open_node.rb +36 -0
- data/lib/ruport/parser.rb +202 -0
- data/lib/ruport/query.rb +208 -0
- data/lib/ruport/query/sql_split.rb +33 -0
- data/lib/ruport/report.rb +116 -0
- data/lib/ruport/report/mailer.rb +17 -15
- data/test/{addressbook.csv → samples/addressbook.csv} +0 -0
- data/test/samples/car_ads.txt +505 -0
- data/test/{data.csv → samples/data.csv} +0 -0
- data/test/samples/document.xml +22 -0
- data/test/samples/five_lines.txt +5 -0
- data/test/samples/five_paragraphs.txt +9 -0
- data/test/samples/ross_report.txt +58530 -0
- data/test/samples/ruport_test.sql +8 -0
- data/test/samples/stonecodeblog.sql +279 -0
- data/test/{test.sql → samples/test.sql} +2 -1
- data/test/{test.yaml → samples/test.yaml} +0 -0
- data/test/tc_builder.rb +7 -4
- data/test/tc_config.rb +41 -0
- data/test/tc_data_row.rb +16 -26
- data/test/tc_data_set.rb +60 -41
- data/test/tc_database.rb +25 -0
- data/test/tc_document.rb +42 -0
- data/test/tc_element.rb +18 -0
- data/test/tc_page.rb +42 -0
- data/test/tc_query.rb +55 -0
- data/test/tc_reading.rb +60 -0
- data/test/tc_report.rb +31 -0
- data/test/tc_section.rb +45 -0
- data/test/tc_sql_split.rb +18 -0
- data/test/tc_state.rb +142 -0
- data/test/ts_all.rb +6 -3
- data/test/ts_format.rb +5 -0
- data/test/ts_parser.rb +10 -0
- metadata +102 -60
- data/bin/ruport +0 -104
- data/lib/ruport/format/chart.rb +0 -1
- data/lib/ruport/report/data_row.rb +0 -79
- data/lib/ruport/report/data_set.rb +0 -153
- data/lib/ruport/report/engine.rb +0 -201
- data/lib/ruport/report/fake_db.rb +0 -54
- data/lib/ruport/report/fake_engine.rb +0 -26
- data/lib/ruport/report/fake_mailer.rb +0 -23
- data/lib/ruport/report/sql.rb +0 -95
- data/lib/ruportlib.rb +0 -11
- data/test/tc_engine.rb +0 -102
- data/test/tc_mailer.rb +0 -21
@@ -0,0 +1,221 @@
|
|
1
|
+
# data_set.rb : Ruby Reports core datastructure.
|
2
|
+
#
|
3
|
+
# Author: Gregory T. Brown (gregory.t.brown at gmail dot com)
|
4
|
+
#
|
5
|
+
# Copyright (c) 2006, All Rights Reserved.
|
6
|
+
#
|
7
|
+
# This is free software. You may modify and redistribute this freely under
|
8
|
+
# your choice of the GNU General Public License or the Ruby License.
|
9
|
+
#
|
10
|
+
# See LICENSE and COPYING for details
|
11
|
+
module Ruport
|
12
|
+
|
13
|
+
# The DataSet is the core datastructure for Ruport. It provides methods that
|
14
|
+
# allow you to compare and combine query results, data loaded in from CSVs,
|
15
|
+
# and user-defined sets of data.
|
16
|
+
#
|
17
|
+
# It is tightly integrated with Ruport's formatting and query systems, so if
|
18
|
+
# you'd like to take advantage of these models, you will probably find DataSet
|
19
|
+
# useful.
|
20
|
+
#
|
21
|
+
# Sample Usage:
|
22
|
+
#
|
23
|
+
# my_data = [[1,2,3],[4,5,6],[7,8,9]]
|
24
|
+
#
|
25
|
+
# ds = Ruport::DataSet.new([:col1, :col2, :col3],my_data)
|
26
|
+
# ds << [ 10, 11, 12]
|
27
|
+
# ds << { :col3 => 15, :col1 => 13, :col2 => 14 }
|
28
|
+
# puts ds.select_columns(:col1, :col3).to_csv
|
29
|
+
#
|
30
|
+
# Output:
|
31
|
+
#
|
32
|
+
# col1,col3
|
33
|
+
# 1,3
|
34
|
+
# 4,6
|
35
|
+
# 7,9
|
36
|
+
# 10,12
|
37
|
+
# 13,15
|
38
|
+
#
|
39
|
+
# The wild and crazy might want to try the Array hack:
|
40
|
+
#
|
41
|
+
# puts [[1,2,3],[4,5,6],[7,8,9]].to_ds(%w[col1 col2 col3])
|
42
|
+
#
|
43
|
+
# Output:
|
44
|
+
#
|
45
|
+
# fields: ( col1, col2, col3 )
|
46
|
+
# row0: ( 1, 2, 3 )
|
47
|
+
# row1: ( 4, 5, 6 )
|
48
|
+
# row2: ( 7, 8, 9 )
|
49
|
+
#
|
50
|
+
class DataSet
|
51
|
+
|
52
|
+
include Enumerable
|
53
|
+
|
54
|
+
# DataSets must be given a set of fields to be defined.
|
55
|
+
#
|
56
|
+
# These field names will define the columns for the DataSet and how you
|
57
|
+
# access them.
|
58
|
+
#
|
59
|
+
# data = Ruport::DataSet.new %w[ id name phone ]
|
60
|
+
#
|
61
|
+
# You can optionally pass in some content as well. (Must be Enumerable)
|
62
|
+
#
|
63
|
+
# content = [ %w[ a1 gregory 203-525-0523 ],
|
64
|
+
# %w[ a2 james 555-555-5555 ] ]
|
65
|
+
#
|
66
|
+
# data = Ruport::DataSet.new(%w[ id name phone],content)
|
67
|
+
def initialize(fields=[], content=nil, default=nil)
|
68
|
+
@fields = fields
|
69
|
+
@data = []
|
70
|
+
@default = default
|
71
|
+
content.each { |r| self << r } if content
|
72
|
+
end
|
73
|
+
|
74
|
+
#an array which contains column names
|
75
|
+
attr_accessor :fields
|
76
|
+
|
77
|
+
#the default value to fill empty cells with
|
78
|
+
attr_accessor :default
|
79
|
+
|
80
|
+
#data holds the elements of the Row
|
81
|
+
attr_reader :data
|
82
|
+
|
83
|
+
#provides a deep copy of the DataSet.
|
84
|
+
def clone
|
85
|
+
DataSet.new(@fields,@data)
|
86
|
+
end
|
87
|
+
|
88
|
+
#Allows ordinal access to rows
|
89
|
+
#
|
90
|
+
# my_data[2] -> Ruport::DataRow
|
91
|
+
def [](index)
|
92
|
+
@data[index]
|
93
|
+
end
|
94
|
+
|
95
|
+
#allows setting of rows (providing a DataRow is passed in)
|
96
|
+
def []=(index,value)
|
97
|
+
throw "Invalid object type" unless value.kind_of?(DataRow)
|
98
|
+
@data[index] = value
|
99
|
+
end
|
100
|
+
|
101
|
+
# appends a row to the DataSet
|
102
|
+
# can be added as an array or a keyed hash-like object.
|
103
|
+
#
|
104
|
+
# Columns left undefined will be filled with DataSet#default values.
|
105
|
+
#
|
106
|
+
# data << [ 1, 2, 3 ]
|
107
|
+
# data << { :some_field_name => 3, :other => 2, :another => 1 }
|
108
|
+
def << ( stuff, filler=@default )
|
109
|
+
@data << DataRow.new(stuff,@fields,:filler => filler)
|
110
|
+
end
|
111
|
+
|
112
|
+
# checks if one dataset equals another
|
113
|
+
def eql?(data2)
|
114
|
+
return false unless ( @data.length == data2.data.length and
|
115
|
+
@fields.eql?(data2.fields) )
|
116
|
+
@data.each_with_index do |row, r_index|
|
117
|
+
row.each_with_index do |field, f_index|
|
118
|
+
return false unless field.eql?(data2[r_index][f_index])
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
return true
|
123
|
+
end
|
124
|
+
|
125
|
+
# checks if one dataset equals another
|
126
|
+
def ==(data2)
|
127
|
+
eql?(data2)
|
128
|
+
end
|
129
|
+
|
130
|
+
# Returns true if DataSet contains no rows, false otherwise.
|
131
|
+
def empty?
|
132
|
+
return @data.empty?
|
133
|
+
end
|
134
|
+
|
135
|
+
# Allows loading of CSV files or YAML dumps. Returns a DataSet
|
136
|
+
#
|
137
|
+
# FasterCSV will be used if it is installed.
|
138
|
+
#
|
139
|
+
# my_data = Ruport::DataSet.load("foo.csv")
|
140
|
+
# my_data = Ruport::DataSet.load("foo.yaml")
|
141
|
+
# my_data = Ruport::DataSet.load("foo.yml")
|
142
|
+
def self.load ( source, default="")
|
143
|
+
case source
|
144
|
+
when /\.(yaml|yml)/
|
145
|
+
return YAML.load(File.open(source))
|
146
|
+
when /\.csv/
|
147
|
+
csv_klass = defined?(FasterCSV) ? FasterCSV : CSV
|
148
|
+
input = csv_klass.read(source) if source =~ /\.csv/
|
149
|
+
loaded_data = self.new
|
150
|
+
loaded_data.fields = input[0]
|
151
|
+
loaded_data.default = default
|
152
|
+
input[1..-1].each { |row| loaded_data << row }
|
153
|
+
return loaded_data
|
154
|
+
else
|
155
|
+
raise "Invalid file type"
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# Iterates through the rows, yielding a DataRow for each.
|
160
|
+
def each(&action)
|
161
|
+
@data.each(&action)
|
162
|
+
end
|
163
|
+
|
164
|
+
# Returns a new DataSet composed of the fields specified.
|
165
|
+
def select_columns(*fields)
|
166
|
+
rows = fields.inject([]) { |s,e| s << map { |row| row[e] } }.transpose
|
167
|
+
my_data = DataSet.new(fields,rows)
|
168
|
+
end
|
169
|
+
|
170
|
+
# Returns a new DataSet with the specified fields removed
|
171
|
+
def remove_columns(*fields)
|
172
|
+
select_fields(*(@fields-fields))
|
173
|
+
end
|
174
|
+
|
175
|
+
# removes the specified fields from this DataSet (DESTRUCTIVE!)
|
176
|
+
def remove_columns!(*fields)
|
177
|
+
@fields -= fields
|
178
|
+
@data = select_fields(*(@fields)).to_a
|
179
|
+
end
|
180
|
+
|
181
|
+
# uses Format::Builder to render DataSets in various ready to output
|
182
|
+
# formats.
|
183
|
+
#
|
184
|
+
# data.as(:html) -> String
|
185
|
+
#
|
186
|
+
# data.as(:text) do |builder|
|
187
|
+
# builder.range = 2..4 -> String
|
188
|
+
# builder.header = "My Title"
|
189
|
+
# end
|
190
|
+
#
|
191
|
+
# To add new formats to this function, simply re-open Format::Builder
|
192
|
+
# and add methods like <tt>render_my_format_name</tt>.
|
193
|
+
#
|
194
|
+
# This will enable <tt>data.as(:my_format_name)</tt>
|
195
|
+
def as(format,&action)
|
196
|
+
builder = Format::Builder.new( self )
|
197
|
+
builder.format = format
|
198
|
+
action.call(builder) if block_given?
|
199
|
+
builder.render
|
200
|
+
end
|
201
|
+
|
202
|
+
# Converts a DataSet to CSV
|
203
|
+
def to_csv; as(:csv) end
|
204
|
+
|
205
|
+
# Converts a Dataset to html
|
206
|
+
def to_html; as(:html) end
|
207
|
+
|
208
|
+
# Readable string representation of the DataSet
|
209
|
+
def to_s; as(:text) end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
class Array
|
214
|
+
|
215
|
+
# Will convert Arrays of Enumerable objects to DataSets.
|
216
|
+
# May have dragons.
|
217
|
+
def to_ds(fields,default=nil)
|
218
|
+
Ruport::DataSet.new(fields,to_a,default)
|
219
|
+
end
|
220
|
+
|
221
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# format.rb : Ruby Reports formatting module
|
2
|
+
#
|
3
|
+
# Author: Gregory T. Brown (gregory.t.brown at gmail dot com)
|
4
|
+
#
|
5
|
+
# Copyright (c) 2006, All Rights Reserved.
|
6
|
+
#
|
7
|
+
# This is free software. You may modify and redistribute this freely under
|
8
|
+
# your choice of the GNU General Public License or the Ruby License.
|
9
|
+
#
|
10
|
+
# See LICENSE and COPYING for details
|
11
|
+
%w[builder open_node document].each { |lib| require "ruport/format/#{lib}" }
|
12
|
+
begin; require "faster_csv"; rescue LoadError; require "csv"; end
|
13
|
+
begin; require "pdf/writer"; rescue LoadError; nil; end
|
14
|
+
module Ruport
|
15
|
+
|
16
|
+
|
17
|
+
# Ruport's Format model is meant to help get your data in a suitable format for
|
18
|
+
# output. Rather than make too many assumptions about how you will want your
|
19
|
+
# data to look, a number of tools have been built so that you can quickly define
|
20
|
+
# those things yourself.
|
21
|
+
#
|
22
|
+
# There are three main sets of functionality the Ruport::Format model provides.
|
23
|
+
# * Structured printable document support ( Format::Document and friends)
|
24
|
+
# * Text filter support ( Report#render and the Format class)
|
25
|
+
# * Support for DataSet Formatting ( Format::Builder)
|
26
|
+
#
|
27
|
+
# The support for structured printable documents is currently geared towards PDF
|
28
|
+
# support and needs some additional work to be truly useful. Suggestions would
|
29
|
+
# be much appreciated.
|
30
|
+
#
|
31
|
+
# Format::Builder lets you define functions that will be used via DataSet#as
|
32
|
+
# This is primary geared towards tabular data output, but there is no reason why
|
33
|
+
# DataSet#as and the <tt>render_foo</tt> methods of Format::Builder cannot be
|
34
|
+
# adapted to fit whatever needs you may need.
|
35
|
+
#
|
36
|
+
# The filters implemented in the Format class are meant to process strings or
|
37
|
+
# entire templates. The Format class will soon automatically build a
|
38
|
+
# Ruport::Parser for any string input. By default, filters are provided to
|
39
|
+
# process erb, pure ruby, and redcloth. It is trivial to extend this
|
40
|
+
# functionality though.
|
41
|
+
#
|
42
|
+
# This is best shown by a simple example:
|
43
|
+
#
|
44
|
+
# a = Ruport::Report.new
|
45
|
+
# Ruport::Format.register_filter :reverser do
|
46
|
+
# content.reverse
|
47
|
+
# end
|
48
|
+
# a.render "somestring", :filters => [:reverser]
|
49
|
+
#
|
50
|
+
# Output: "gnirtsemos"
|
51
|
+
#
|
52
|
+
# Filters can be combined, and you can run them in different orders to obtain
|
53
|
+
# different results.
|
54
|
+
#
|
55
|
+
# See the source for the built in filters for ideas.
|
56
|
+
#
|
57
|
+
# Also, see Report#render for how to bind Format objects to your own classes.
|
58
|
+
#
|
59
|
+
# When combined, filters, data set output templates, and structured printable
|
60
|
+
# document facilities create a complete Formatting system.
|
61
|
+
#
|
62
|
+
# This part of Ruport is under active development. Please do feel free to
|
63
|
+
# submit feature requests or suggestions.
|
64
|
+
class Format
|
65
|
+
|
66
|
+
# To hook up a Format object to your current class, you need to pass it a
|
67
|
+
# binding. This way, when filters are being processed, they will be
|
68
|
+
# evaluated in the context of the object they are being called from, rather
|
69
|
+
# than within an instance of Format.
|
70
|
+
#
|
71
|
+
def initialize(klass_binding)
|
72
|
+
@binding = klass_binding
|
73
|
+
end
|
74
|
+
|
75
|
+
# This is the text to be processed by the filters
|
76
|
+
attr_accessor :content
|
77
|
+
|
78
|
+
# This is the binding to the object Format is tied to
|
79
|
+
attr_accessor :binding
|
80
|
+
|
81
|
+
# Processes the ERB text in <tt>@content</tt> in the context
|
82
|
+
# of the object that Format is bound to.
|
83
|
+
def filter_erb
|
84
|
+
ERB.new(@content).result(@binding)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Processes the RedCloth text in <tt>@content</tt> in the context
|
88
|
+
# of the object that Format is bound to.
|
89
|
+
def filter_red_cloth
|
90
|
+
RedCloth.new(@content).to_html
|
91
|
+
end
|
92
|
+
|
93
|
+
# Processes the ruby code in <tt>@content</tt> in the context
|
94
|
+
# of the object that Format is bound to.
|
95
|
+
#
|
96
|
+
# (Does an eval on the binding)
|
97
|
+
def filter_ruby
|
98
|
+
eval(@content,@binding)
|
99
|
+
end
|
100
|
+
|
101
|
+
# Takes a name and a block and creates a filter method
|
102
|
+
# This will define methods in the form of
|
103
|
+
# <tt>Format#filter_my_filter_name</tt>.
|
104
|
+
#
|
105
|
+
# Example:
|
106
|
+
#
|
107
|
+
# Format.register_filter :no_ohz do
|
108
|
+
# content.gsub(/O/i,"")
|
109
|
+
# end
|
110
|
+
def Format.register_filter(name,&filter_proc)
|
111
|
+
define_method "filter_#{name}".to_sym, &filter_proc
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
@@ -1,5 +1,5 @@
|
|
1
1
|
module Ruport
|
2
|
-
|
2
|
+
class Format
|
3
3
|
class Builder
|
4
4
|
|
5
5
|
def initialize( data_set )
|
@@ -20,10 +20,12 @@ module Ruport
|
|
20
20
|
end
|
21
21
|
|
22
22
|
def render_csv
|
23
|
+
csv_klass = defined?(FasterCSV) ? FasterCSV : CSV
|
24
|
+
fields = @original.fields
|
23
25
|
( @header ? "#{@header}\n\n" : "" ) +
|
24
|
-
@data.inject(
|
25
|
-
out <<
|
26
|
-
|
26
|
+
@data.inject(csv_klass.generate_line(fields).chomp + "\n" ) { |out,r|
|
27
|
+
out << csv_klass.generate_line(fields.map { |f| r[f] }).chomp + "\n"
|
28
|
+
} + ( @footer ? "\n#{@footer}\n" : "" )
|
27
29
|
end
|
28
30
|
|
29
31
|
def render_html
|
@@ -58,7 +60,29 @@ module Ruport
|
|
58
60
|
end + (@footer ? "#{@footer}\n" : "" )
|
59
61
|
|
60
62
|
end
|
61
|
-
|
63
|
+
def render_pdf
|
64
|
+
|
65
|
+
return unless defined? PDF::Writer
|
66
|
+
pdf = PDF::Writer.new
|
67
|
+
pdf.margins_cm(0)
|
68
|
+
@data.each do |page|
|
69
|
+
unless page.eql?(@data.pages.first)
|
70
|
+
pdf.start_new_page
|
71
|
+
end
|
72
|
+
page.each do |section|
|
73
|
+
section.each do |element|
|
74
|
+
pdf.y = pdf.cm2pts(element.top)
|
75
|
+
pdf.text element.content,
|
76
|
+
:left => pdf.cm2pts(element.left),
|
77
|
+
:right => pdf.cm2pts(element.right),
|
78
|
+
:justification => element.align || :center
|
79
|
+
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
pdf.render
|
84
|
+
|
85
|
+
end
|
62
86
|
end
|
63
87
|
end
|
64
88
|
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require "ostruct"
|
2
|
+
require "rexml/document"
|
3
|
+
module Ruport
|
4
|
+
class Format
|
5
|
+
class Document < OpenStruct
|
6
|
+
include Enumerable
|
7
|
+
|
8
|
+
def initialize(name,options={})
|
9
|
+
super(options)
|
10
|
+
self.name = name
|
11
|
+
self.pages ||= []
|
12
|
+
end
|
13
|
+
|
14
|
+
def each
|
15
|
+
self.pages.each { |p| yield(p) }
|
16
|
+
end
|
17
|
+
|
18
|
+
def add_page(name,options={})
|
19
|
+
options[:document] = self
|
20
|
+
self.pages << Format::Page.new(name,options)
|
21
|
+
end
|
22
|
+
|
23
|
+
def <<(page)
|
24
|
+
page.document = self
|
25
|
+
self.pages << page.dup
|
26
|
+
end
|
27
|
+
|
28
|
+
def [](page_name)
|
29
|
+
return self.pages[page_name] if page_name.kind_of? Integer
|
30
|
+
self.pages.find { |p| p.name.eql?(page_name) }
|
31
|
+
end
|
32
|
+
|
33
|
+
def clone
|
34
|
+
cloned = self.clone
|
35
|
+
cloned.pages = self.pages.clone
|
36
|
+
return cloned
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class Page < Format::OpenNode
|
41
|
+
|
42
|
+
def initialize(name,options={})
|
43
|
+
super(:page,:document,:sections,name,options)
|
44
|
+
end
|
45
|
+
|
46
|
+
def add_section(name,options={})
|
47
|
+
add_child(Format::Section,name,options)
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
class Section < Format::OpenNode
|
53
|
+
|
54
|
+
def initialize(name, options={})
|
55
|
+
super(:section,:page,:elements,name,options)
|
56
|
+
end
|
57
|
+
|
58
|
+
def add_element(name,options={})
|
59
|
+
add_child(Format::Element,name,options)
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
class Element < OpenStruct
|
65
|
+
|
66
|
+
def initialize(name,options={})
|
67
|
+
super(options)
|
68
|
+
self.name = name
|
69
|
+
end
|
70
|
+
|
71
|
+
def to_s
|
72
|
+
self.content
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|