ruport 0.2.0 → 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +26 -0
- data/README +1 -1
- data/Rakefile +1 -1
- data/TODO +8 -3
- data/bin/ruport +5 -5
- data/lib/ruport/report/data_row.rb +67 -0
- data/lib/ruport/report/data_set.rb +104 -0
- data/lib/ruport/report/engine.rb +214 -0
- data/lib/ruport/report/fake_db.rb +54 -0
- data/lib/ruport/report/fake_engine.rb +46 -0
- data/lib/ruport/report/mailer.rb +46 -0
- data/lib/ruport/report/sql.rb +95 -0
- data/lib/ruportlib.rb +8 -7
- data/test/tc_data_row.rb +1 -1
- data/test/tc_data_set.rb +4 -4
- data/test/{tc_report.rb → tc_engine.rb} +3 -3
- data/test/ts_all.rb +1 -1
- metadata +10 -10
- data/lib/ruport/db/data_row.rb +0 -64
- data/lib/ruport/db/data_set.rb +0 -101
- data/lib/ruport/db/mailer.rb +0 -42
- data/lib/ruport/db/mock_db.rb +0 -49
- data/lib/ruport/db/mock_report.rb +0 -44
- data/lib/ruport/db/report.rb +0 -212
- data/lib/ruport/db/sql.rb +0 -93
data/CHANGELOG
CHANGED
@@ -1,3 +1,29 @@
|
|
1
|
+
The current version of Ruby Reports is 0.2.2
|
2
|
+
|
3
|
+
changes since Ruport 0.2.0:
|
4
|
+
|
5
|
+
- Report becomes Engine
|
6
|
+
- MockDB becomes FakeDB
|
7
|
+
- MockReport becomes MockEngine
|
8
|
+
|
9
|
+
- Modules are added so now,
|
10
|
+
|
11
|
+
Ruport::Report::Engine,
|
12
|
+
Ruport::Report::DataSet is the full name of these classes.
|
13
|
+
|
14
|
+
- 'require ruportlib' runs include Ruport,
|
15
|
+
so you can type Report::Engine.new instead of Ruport::Report::Engine.new
|
16
|
+
|
17
|
+
- The assert that was supposed to be an assert_equal that
|
18
|
+
David Black caught was fixed.
|
19
|
+
|
20
|
+
- Examples are updated and now on the RubyForge FRS in many formats.
|
21
|
+
(With .DS_Store and other garbage files removed! ;) )
|
22
|
+
|
23
|
+
- Formatting of code was cleaned up and improved.
|
24
|
+
|
25
|
+
- Generates a test.rb that does not use deprecated functions now.
|
26
|
+
|
1
27
|
changes since Ruport 0.1.0:
|
2
28
|
|
3
29
|
CLEANUP / ORGANIZATION:
|
data/README
CHANGED
@@ -7,7 +7,7 @@ of databases via DBI. It also provides helper methods and utilities to generate
|
|
7
7
|
It happens to be alpha software, so if it offends your mother, burns out the innards of your computer, or threatens you in any other way, you may want to wait until further releases to play around with it.
|
8
8
|
|
9
9
|
There is a set of examples and a brief walkthrough for this application
|
10
|
-
available at http://ruport.rubyforge.org/
|
10
|
+
available at http://ruport.rubyforge.org/example.zip which I strongly
|
11
11
|
recommend going through
|
12
12
|
|
13
13
|
Otherwise, here is a brief set of installation and setup instructions which will
|
data/Rakefile
CHANGED
data/TODO
CHANGED
@@ -1,5 +1,11 @@
|
|
1
1
|
TODO:
|
2
2
|
|
3
|
+
Bugfixes:
|
4
|
+
- Fix potential failure in Ruport::Report::DataSet#eql?
|
5
|
+
- Is there an OS X packaging problem with the gems?
|
6
|
+
- Fix the manual that is in the example package so it it reads FakeDB instead
|
7
|
+
of MockDB
|
8
|
+
|
3
9
|
Improvement:
|
4
10
|
|
5
11
|
- Mail system
|
@@ -104,9 +110,8 @@ Other:
|
|
104
110
|
|
105
111
|
Ruport needs to be modified to support as many databases as possibly
|
106
112
|
internally. The system was written using MySQL on Gentoo Linux
|
107
|
-
and Mac OS X.3 / OS X.4, Windows 2000 / XP
|
108
|
-
|
109
|
-
and Microsoft SQL Server on Windows 2000 (via ODBC),
|
113
|
+
and Mac OS X.3 / OS X.4, Windows 2000 / XP and Microsoft SQL Server
|
114
|
+
on Windows 2000 (via ODBC),
|
110
115
|
|
111
116
|
so the support for these platforms are best. An effort will be given to
|
112
117
|
make Ruport less hostile in other environments.
|
data/bin/ruport
CHANGED
@@ -61,11 +61,11 @@ if ARGV[0].eql?("generate")
|
|
61
61
|
|
62
62
|
File.open("#{ARGV[1]}/config/ruport.yaml", "w") { |f| f.puts(conf) }
|
63
63
|
File.open("#{ARGV[1]}/templates/test.rb", "w") { |f|
|
64
|
-
f.puts
|
64
|
+
f.puts('query ("show tables;") do |data| @report << data.to_csv end ')
|
65
65
|
}
|
66
66
|
exit
|
67
67
|
elsif ARGV[0].eql?("-v")
|
68
|
-
puts "Ruport Version 0.2.
|
68
|
+
puts "Ruport Version 0.2.2 \nA ruby report generation system by Gregory " +
|
69
69
|
"Brown.\nThis application is Free Software under the GPL/Ruby License. " +
|
70
70
|
"\nAll praise and/or criticism can be directed to "+
|
71
71
|
"gregory.t.brown@gmail.com"
|
@@ -79,17 +79,17 @@ config = YAML.load(File.open("config/ruport.yaml"))
|
|
79
79
|
mailer = nil
|
80
80
|
|
81
81
|
if config.has_key?(:mail_host)
|
82
|
-
mailer = Mailer.new( config[:mail_host], config[:mail_address],
|
82
|
+
mailer = Report::Mailer.new( config[:mail_host], config[:mail_address],
|
83
83
|
config[:mail_account], config[:mail_password],
|
84
84
|
25, config[:mail_authentication] )
|
85
85
|
end
|
86
86
|
|
87
|
-
report = Report.new(
|
87
|
+
report = Report::Engine.new(
|
88
88
|
"#{config[:driver]}:#{config[:database]}",
|
89
89
|
config[:db_user], config[:db_password], mailer )
|
90
90
|
|
91
91
|
if config[:driver].eql?("ruport")
|
92
|
-
report =
|
92
|
+
report = Report::FakeEngine.new( "#{config[:driver]}:#{config[:database]}",
|
93
93
|
config[:db_user], config[:db_password], mailer )
|
94
94
|
end
|
95
95
|
|
@@ -0,0 +1,67 @@
|
|
1
|
+
#!/usr/local/bin/ruby
|
2
|
+
# data_row.rb
|
3
|
+
# Created by Gregory Thomas Brown on 2005-08-02
|
4
|
+
# Copyright 2005 (Gregory Brown) All rights reserved.
|
5
|
+
#
|
6
|
+
# This product is free software, you may distribute it as such
|
7
|
+
# under your choice of the Ruby license or the GNU GPL
|
8
|
+
# See LICENSE for details
|
9
|
+
|
10
|
+
module Ruport
|
11
|
+
module Report
|
12
|
+
class DataRow
|
13
|
+
|
14
|
+
attr_accessor :fields
|
15
|
+
|
16
|
+
# DataRows are essentially arrays with named ordinal fields and
|
17
|
+
# awareness of their oddness and position.
|
18
|
+
def initialize( data, fields, oddness=nil, position=nil )
|
19
|
+
@data = data
|
20
|
+
@fields = fields
|
21
|
+
@oddness = oddness
|
22
|
+
@position = position
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
# Lets you access individual fields
|
27
|
+
#
|
28
|
+
# i.e. row["phone"]
|
29
|
+
def [](key)
|
30
|
+
key.kind_of?(String) ? @data[@fields.index(key)] : @data[key]
|
31
|
+
end
|
32
|
+
|
33
|
+
# Lets you set field values
|
34
|
+
#
|
35
|
+
# i.e. row["phone"] = '2038291203'
|
36
|
+
def []=(key,value)
|
37
|
+
key.kind_of?(String) ? @data[@fields.index(key)] = value :
|
38
|
+
@data[key] = value
|
39
|
+
end
|
40
|
+
|
41
|
+
# Converts the DataRow to a plain old Array
|
42
|
+
def to_a
|
43
|
+
@data
|
44
|
+
end
|
45
|
+
|
46
|
+
# Converts the DataRow to a string representation
|
47
|
+
# for outputting to screen.
|
48
|
+
def to_s
|
49
|
+
"[" + @data.join(",") + "]"
|
50
|
+
end
|
51
|
+
|
52
|
+
# implements
|
53
|
+
# DataRow#first? DataRow#last? DataRow#middle?
|
54
|
+
# DataRow#odd? DataRow#even? which are all conditional methods.
|
55
|
+
def method_missing(method)
|
56
|
+
if %[last? first? middle?].include? method.to_s
|
57
|
+
return @position.eql?(method.to_s[0..-2].to_sym)
|
58
|
+
elsif %[odd? even?].include? method.to_s
|
59
|
+
return @oddness.eql?(method.to_s[0..-2].to_sym)
|
60
|
+
end
|
61
|
+
super
|
62
|
+
end
|
63
|
+
|
64
|
+
attr_accessor :position
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
#!/usr/local/bin/ruby
|
2
|
+
# data_set.rb
|
3
|
+
# Created by Gregory Thomas Brown on 2005-08-02
|
4
|
+
# Copyright 2005 (Gregory Brown) All rights reserved.
|
5
|
+
#
|
6
|
+
# This product is free software, you may distribute it as such
|
7
|
+
# under your choice of the Ruby license or the GNU GPL
|
8
|
+
# See LICENSE for details
|
9
|
+
module Ruport
|
10
|
+
module Report
|
11
|
+
class DataSet
|
12
|
+
def initialize
|
13
|
+
@data = []
|
14
|
+
end
|
15
|
+
|
16
|
+
#fields are the column names, default is the default cell value
|
17
|
+
attr_accessor :fields, :default
|
18
|
+
|
19
|
+
#data is the core Array that holds all the rows
|
20
|
+
attr_reader :data
|
21
|
+
|
22
|
+
|
23
|
+
#Allows ordinal access to rows
|
24
|
+
def [](index)
|
25
|
+
@data[index]
|
26
|
+
end
|
27
|
+
|
28
|
+
#allows setting of rows (providing a DataRow is passed in)
|
29
|
+
def []=(index,value)
|
30
|
+
throw "Invalid object type" unless value.kind_of?(DataRow)
|
31
|
+
@data[index] = value
|
32
|
+
end
|
33
|
+
|
34
|
+
# appends a row to the DataSet
|
35
|
+
# can be added as an array or a keyed hash.
|
36
|
+
# i.e if our DataSet have @fields = [:a, :b, :c]
|
37
|
+
# data << [ 1, 2, 3] and data << { :a => 1, :b => 2, :c => 3 }
|
38
|
+
# are equivalent.
|
39
|
+
def << ( stuff )
|
40
|
+
new_row = Array.new
|
41
|
+
fields.each_with_index do |key, index|
|
42
|
+
if stuff.kind_of?(Array)
|
43
|
+
new_row[index] = stuff.shift || @default
|
44
|
+
else
|
45
|
+
new_row[index] = stuff[key] || @default
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
oddness = (@data.length % 2 == 0 ? :even : :odd)
|
50
|
+
position = (@data.length == 0 ? :first : :last)
|
51
|
+
@data[@data.length - 1].position = :middle if @data.length > 1
|
52
|
+
@data << DataRow.new(new_row,@fields,oddness,position)
|
53
|
+
end
|
54
|
+
|
55
|
+
# This works in best case scenario. It should return true if
|
56
|
+
# both DataSets have the same values. Still working out the kinks here.
|
57
|
+
def eql?(data2)
|
58
|
+
return false unless (@data.length == data2.data.length)
|
59
|
+
0.upto(@data.length - 1) do |index|
|
60
|
+
(@fields + data2.fields).uniq.each do |key|
|
61
|
+
return false unless @data[index][key] == data2[index][key]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
return true
|
65
|
+
end
|
66
|
+
|
67
|
+
|
68
|
+
# Allows loading of CSV files or YAML dumps. Returns a DataSet
|
69
|
+
def self.load ( source, default="")
|
70
|
+
|
71
|
+
return YAML.load(File.open(source)) if source =~ /\.(yaml|yml)/
|
72
|
+
|
73
|
+
input = CSV.read(source) if source =~ /\.csv/
|
74
|
+
loaded_data = self.new
|
75
|
+
loaded_data.fields = input[0]
|
76
|
+
loaded_data.default = default
|
77
|
+
input[1..-1].each do |row|
|
78
|
+
loaded_data << row
|
79
|
+
end
|
80
|
+
return loaded_data
|
81
|
+
end
|
82
|
+
|
83
|
+
# Converts a DataSet to CSV
|
84
|
+
def to_csv
|
85
|
+
output = CSV.generate_line(@fields) + "\n"
|
86
|
+
@data.each do |row|
|
87
|
+
output << CSV.generate_line(@fields.map { |f| row[f] }) + "\n"
|
88
|
+
end
|
89
|
+
return output
|
90
|
+
end
|
91
|
+
|
92
|
+
# Works like a standard each iterator
|
93
|
+
def each(&action)
|
94
|
+
@data[0..-1].each(&action)
|
95
|
+
end
|
96
|
+
|
97
|
+
# Works like a standard each_with_index iterator
|
98
|
+
def each_with_index(&action)
|
99
|
+
@data[0..-1].each_with_index(&action)
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,214 @@
|
|
1
|
+
#!/usr/local/bin/ruby
|
2
|
+
# engine.rb
|
3
|
+
# Created by Gregory Thomas Brown on 2005-08-02
|
4
|
+
# Copyright 2005 (Gregory Brown) All rights reserved.
|
5
|
+
#
|
6
|
+
# This product is free software, you may distribute it as such
|
7
|
+
# under your choice of the Ruby license or the GNU GPL
|
8
|
+
# See LICENSE for details
|
9
|
+
#
|
10
|
+
# Special thanks and acknowledgement go to James Edward Gray II
|
11
|
+
# for providing the original source code for this application
|
12
|
+
require "dbi"
|
13
|
+
require "rubygems"
|
14
|
+
require "erb"
|
15
|
+
require "csv"
|
16
|
+
require "yaml"
|
17
|
+
require "date"
|
18
|
+
require "logger"
|
19
|
+
require "fileutils"
|
20
|
+
module Ruport
|
21
|
+
module Report
|
22
|
+
class Engine
|
23
|
+
def initialize( dsn, user, password, mailer=nil )
|
24
|
+
@dsn = dsn
|
25
|
+
@user = user
|
26
|
+
@password = password
|
27
|
+
@report_name = ""
|
28
|
+
@mailer = mailer
|
29
|
+
File.exists?("log") or FileUtils.mkdir("log")
|
30
|
+
@logger = Logger.new("log/ruport.log")
|
31
|
+
@generate = true
|
32
|
+
@report = ""
|
33
|
+
end
|
34
|
+
|
35
|
+
attr_accessor :query_table, :file
|
36
|
+
attr_reader :mailer, :generate, :config, :logger
|
37
|
+
|
38
|
+
# * DEPRECATED: Use query() *
|
39
|
+
#
|
40
|
+
# Takes a query, an optional sourcetype, and a block which is passed the
|
41
|
+
# results row by row. When passed a query in string form, it adds the
|
42
|
+
# SELECT clause to the string and executes the query. When passed a
|
43
|
+
# filename and a source :file, it looks in queries/ for the file specified.
|
44
|
+
# When given a database query label, it looks in config[query_table] for a
|
45
|
+
# query with the label specified. If no source is specified, it uses
|
46
|
+
# string by default for the source.
|
47
|
+
#
|
48
|
+
# Example:
|
49
|
+
#
|
50
|
+
# select ( "* FROM test" )
|
51
|
+
# Passes "SELECT * FROM test" to the database
|
52
|
+
#
|
53
|
+
# select ( "test.sql", :file )
|
54
|
+
# Passes the contents of queries/test.sql to the database
|
55
|
+
#
|
56
|
+
# select ( "TEST", :db )
|
57
|
+
# Calls the query TEST stored in the database and query_table specified in
|
58
|
+
# config/ruport.yaml
|
59
|
+
|
60
|
+
def select( query, source = :string, &action )
|
61
|
+
source != :string || query = "SELECT " + query
|
62
|
+
execute( query, source ) do |sth|
|
63
|
+
@column_names = sth.column_names
|
64
|
+
@first_row = true
|
65
|
+
sth.fetch do |row|
|
66
|
+
action.call(row) if block_given?
|
67
|
+
@first_row = false
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# * DEPRECATED, Use query() *
|
73
|
+
#
|
74
|
+
# Takes a query and an optional sourcetype and then runs the query
|
75
|
+
# against the database. The output is not returned. This is useful for
|
76
|
+
# doing construction and destruction actions.
|
77
|
+
def execute( query, source = :string )
|
78
|
+
query = get_query(source, query)
|
79
|
+
DBI.connect(@dsn, @user, @password) do |dbh|
|
80
|
+
dbh.prepare(query) do |sth|
|
81
|
+
sth.execute()
|
82
|
+
yield(sth)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# Takes a query and an optional sourcetype and then runs the query
|
88
|
+
# against the database. The return from the query is then converted
|
89
|
+
# into a DataSet which can then be manipulated.
|
90
|
+
#
|
91
|
+
# Example:
|
92
|
+
# query("SELECT * FROM address_book") do |data|
|
93
|
+
# @report << data.to_csv
|
94
|
+
# end
|
95
|
+
#
|
96
|
+
# The above would select all from the _address_book_ table
|
97
|
+
# and then convert the DataSet to a csv and then add it to the report
|
98
|
+
|
99
|
+
def query ( statement, source = :string, &action )
|
100
|
+
query_text = get_query(source,statement)
|
101
|
+
DBI.connect(@dsn, @user, @password) do |dbh|
|
102
|
+
data = DataSet.new
|
103
|
+
sth = dbh.execute(query_text)
|
104
|
+
if block_given?
|
105
|
+
data.fields = sth.column_names
|
106
|
+
sth.each { |row| data << row }
|
107
|
+
action.call(data)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# Evaluates _code_ from _filename_ as pure ruby code for files ending in
|
113
|
+
# .rb, and as ERb templates for anything else.
|
114
|
+
def eval_report( filename, code )
|
115
|
+
if filename =~ /\.rb/
|
116
|
+
eval(code)
|
117
|
+
else
|
118
|
+
ERB.new(code, 0, "%").run(binding)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# Loads a yaml file into the @config variable which is accessible in
|
123
|
+
# templates. If _interactive_ is set to true, the user will
|
124
|
+
# be prompted to enter information for each key.
|
125
|
+
def load_yaml(file, interactive=false)
|
126
|
+
file = "config/#{file}" unless File.exists? file
|
127
|
+
@config = YAML.load(File.open(file))
|
128
|
+
if (interactive)
|
129
|
+
@config.keys.map { |c| c.to_s }.sort.each do |property|
|
130
|
+
$stderr.print "#{property} (#{@config[property.to_sym]}): "
|
131
|
+
entered = $stdin.gets.strip
|
132
|
+
@config[property] = entered unless entered.eql?("")
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# Creates an SQL Object. The block is evaluated within the SQL
|
138
|
+
# instance so you may use any methods available to the SQL class.
|
139
|
+
# The generated query is returned.
|
140
|
+
#
|
141
|
+
# Example
|
142
|
+
# fetch { from :address_book order :name }
|
143
|
+
# => "SELECT * FROM address_book ORDER BY name
|
144
|
+
|
145
|
+
def fetch( &dsl )
|
146
|
+
begin
|
147
|
+
SQL.new(:SELECT, &dsl).to_s
|
148
|
+
rescue Exception
|
149
|
+
@logger.fatal "Error generating SQL."
|
150
|
+
raise "Error generating SQL."
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# Returns the query string for queries of type string, file, or db
|
155
|
+
def get_query(type,query)
|
156
|
+
case (type)
|
157
|
+
when :string
|
158
|
+
query
|
159
|
+
when :file
|
160
|
+
load_file( query )
|
161
|
+
when :db
|
162
|
+
select ( "query FROM #{@query_table} WHERE " +
|
163
|
+
"label LIKE '#{query}';" ) do |row| return row["query"] end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
# Loads a query from file. Accepts absolute paths, relative paths from
|
168
|
+
# the toplevel directory, and relative paths from _queries/_.
|
169
|
+
# If you've been putting all your database queries in _queries_, you can
|
170
|
+
# just call them by name.
|
171
|
+
def load_file( query_file )
|
172
|
+
begin
|
173
|
+
File.read( query_file ).strip
|
174
|
+
rescue
|
175
|
+
@logger.fatal "Could not open #{query_file}"
|
176
|
+
raise "Could not open #{query_file}"; exit
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
# Generates the report. If @pre or @post are defined with lambdas,
|
181
|
+
# they will be called before and after the main code. generate_report
|
182
|
+
# will automatically be run at the end of the execution
|
183
|
+
# of a ruport template unless @generate is set to false.
|
184
|
+
#
|
185
|
+
# If @file != nil, ruport will print to the
|
186
|
+
# file with the specified name. Otherwise, it will print to STDOUT by
|
187
|
+
# default. @file can be set implictly by a file argument, i.e.
|
188
|
+
# ruport templates/foo.rb reports/foo_out.html => @file =
|
189
|
+
# 'reports/foo_out.html' It can also be set explicitly within a template.
|
190
|
+
# A good way to define a default value is @file ||= "reports/somefile"
|
191
|
+
#
|
192
|
+
# Lastly, if you have your mailer configuration set up and
|
193
|
+
# you've specified recipients, the contents of @mailer.body will
|
194
|
+
# be automatically emailed by this fuction.
|
195
|
+
#
|
196
|
+
# The source for this function is probably easier to read than this
|
197
|
+
# explanation, so you may want to start there.
|
198
|
+
def generate_report
|
199
|
+
@pre.call if @pre
|
200
|
+
if (@file.nil?)
|
201
|
+
puts(@report)
|
202
|
+
else
|
203
|
+
File.open(@file,"w") { |f| f.puts @report }
|
204
|
+
end
|
205
|
+
unless @mailer.nil? || @mailer.recipients.empty?
|
206
|
+
@mailer.body = @report unless not @mailer.body.empty?
|
207
|
+
@mailer.send_report(@report_name)
|
208
|
+
end
|
209
|
+
@post.call if @post
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|