ruport 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/AUTHORS +1 -0
- data/CHANGELOG +45 -0
- data/README +32 -13
- data/Rakefile +48 -51
- data/TODO +36 -23
- data/bin/ruport +75 -57
- data/lib/ruport/db/data_row.rb +64 -0
- data/lib/ruport/db/data_set.rb +101 -0
- data/lib/ruport/db/mailer.rb +42 -0
- data/lib/ruport/db/mock_db.rb +49 -0
- data/lib/ruport/db/mock_report.rb +44 -0
- data/lib/ruport/db/report.rb +212 -0
- data/lib/ruport/db/sql.rb +93 -0
- data/lib/ruport/format/chart.rb +1 -0
- data/lib/ruportlib.rb +7 -0
- data/test/tc_data_row.rb +53 -0
- data/test/tc_data_set.rb +67 -0
- data/test/tc_report.rb +104 -0
- data/test/ts_all.rb +4 -0
- metadata +20 -20
- data/bin/ruport.rb +0 -4
- data/lib/mailer.rb +0 -44
- data/lib/query.rb +0 -140
- data/lib/sql.rb +0 -93
- data/setup.rb +0 -1360
- data/tests/tc_ruport.rb +0 -69
@@ -0,0 +1,64 @@
|
|
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
|
+
|
11
|
+
class DataRow
|
12
|
+
|
13
|
+
attr_accessor :fields
|
14
|
+
|
15
|
+
|
16
|
+
# DataRows are essentially arrays with named ordinal fields and awareness of
|
17
|
+
# 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 for outputting to screen.
|
47
|
+
def to_s
|
48
|
+
"[" + @data.join(",") + "]"
|
49
|
+
end
|
50
|
+
|
51
|
+
# implements
|
52
|
+
# DataRow#first? DataRow#last? DataRow#middle? DataRow#odd? DataRow#even?
|
53
|
+
# which are all conditional methods.
|
54
|
+
def method_missing(method)
|
55
|
+
if %[last? first? middle?].include? method.to_s
|
56
|
+
return @position.eql?(method.to_s[0..-2].to_sym)
|
57
|
+
elsif %[odd? even?].include? method.to_s
|
58
|
+
return @oddness.eql?(method.to_s[0..-2].to_sym)
|
59
|
+
end
|
60
|
+
super
|
61
|
+
end
|
62
|
+
|
63
|
+
attr_accessor :position
|
64
|
+
end
|
@@ -0,0 +1,101 @@
|
|
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
|
+
|
10
|
+
class DataSet
|
11
|
+
def initialize
|
12
|
+
@data = []
|
13
|
+
end
|
14
|
+
|
15
|
+
#fields are the column names, default is the default cell value
|
16
|
+
attr_accessor :fields, :default
|
17
|
+
|
18
|
+
#data is the core Array that holds all the rows
|
19
|
+
attr_reader :data
|
20
|
+
|
21
|
+
|
22
|
+
#Allows ordinal access to rows
|
23
|
+
def [](index)
|
24
|
+
@data[index]
|
25
|
+
end
|
26
|
+
|
27
|
+
#allows setting of rows (providing a DataRow is passed in)
|
28
|
+
def []=(index,value)
|
29
|
+
throw "Invalid object type" unless value.kind_of?(DataRow)
|
30
|
+
@data[index] = value
|
31
|
+
end
|
32
|
+
|
33
|
+
# appends a row to the DataSet
|
34
|
+
# can be added as an array or a keyed hash.
|
35
|
+
# i.e if our DataSet have @fields = [:a, :b, :c]
|
36
|
+
# data << [ 1, 2, 3] and data << { :a => 1, :b => 2, :c => 3 }
|
37
|
+
# are equivalent.
|
38
|
+
def << ( stuff )
|
39
|
+
new_row = Array.new
|
40
|
+
fields.each_with_index do |key, index|
|
41
|
+
if stuff.kind_of?(Array)
|
42
|
+
new_row[index] = stuff.shift || @default
|
43
|
+
else
|
44
|
+
new_row[index] = stuff[key] || @default
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
oddness = (@data.length % 2 == 0 ? :even : :odd)
|
49
|
+
position = (@data.length == 0 ? :first : :last)
|
50
|
+
@data[@data.length - 1].position = :middle if @data.length > 1
|
51
|
+
@data << DataRow.new(new_row,@fields,oddness,position)
|
52
|
+
end
|
53
|
+
|
54
|
+
# This works in best case scenario. It should return true if both DataSets
|
55
|
+
# have the same values. Still working out the kinks here.
|
56
|
+
def eql?(data2)
|
57
|
+
return false unless (@data.length == data2.data.length)
|
58
|
+
0.upto(@data.length - 1) do |index|
|
59
|
+
(@fields + data2.fields).uniq.each do |key|
|
60
|
+
return false unless @data[index][key] == data2[index][key]
|
61
|
+
end
|
62
|
+
end
|
63
|
+
return true
|
64
|
+
end
|
65
|
+
|
66
|
+
|
67
|
+
# Allows loading of CSV files or YAML dumps. Returns a DataSet
|
68
|
+
def self.load ( source, default="")
|
69
|
+
|
70
|
+
return YAML.load(File.open(source)) if source =~ /\.(yaml|yml)/
|
71
|
+
|
72
|
+
input = CSV.read(source) if source =~ /\.csv/
|
73
|
+
loaded_data = self.new
|
74
|
+
loaded_data.fields = input[0]
|
75
|
+
loaded_data.default = default
|
76
|
+
input[1..-1].each do |row|
|
77
|
+
loaded_data << row
|
78
|
+
end
|
79
|
+
return loaded_data
|
80
|
+
end
|
81
|
+
|
82
|
+
# Converts a DataSet to CSV
|
83
|
+
def to_csv
|
84
|
+
output = CSV.generate_line(@fields) + "\n"
|
85
|
+
@data.each do |row|
|
86
|
+
output << CSV.generate_line(@fields.map { |f| row[f] }) + "\n"
|
87
|
+
end
|
88
|
+
return output
|
89
|
+
end
|
90
|
+
|
91
|
+
# Works like a standard each iterator
|
92
|
+
def each(&action)
|
93
|
+
@data[0..-1].each(&action)
|
94
|
+
end
|
95
|
+
|
96
|
+
# Works like a standard each_with_index iterator
|
97
|
+
def each_with_index(&action)
|
98
|
+
@data[0..-1].each_with_index(&action)
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# mailer.rb
|
2
|
+
# Created by Gregory Brown on 2005-08-16
|
3
|
+
# Copyright 2005 (Gregory Brown) All Rights Reserved.
|
4
|
+
# This product is free software, you may distribute it as such
|
5
|
+
# under your choice of the Ruby license or the GNU GPL
|
6
|
+
# See LICENSE for details
|
7
|
+
|
8
|
+
require "net/smtp"
|
9
|
+
class Mailer
|
10
|
+
|
11
|
+
# Creates a new Mailer object. User must specify their mail host, email
|
12
|
+
# address, and account. Password, port, and authentication method are all
|
13
|
+
# optional.
|
14
|
+
def initialize(host, address, account, password=nil, port=25, authentication=nil)
|
15
|
+
@host = host
|
16
|
+
@account = account
|
17
|
+
@password = password
|
18
|
+
@address = address
|
19
|
+
@port = port
|
20
|
+
@auth = authentication
|
21
|
+
@recipients = []
|
22
|
+
@body = ""
|
23
|
+
end
|
24
|
+
|
25
|
+
#A list of email addresses to send the message to.
|
26
|
+
attr_accessor :recipients
|
27
|
+
|
28
|
+
#The body of the message to be sent
|
29
|
+
attr_accessor :body
|
30
|
+
|
31
|
+
# This takes _report_name_ as argument and sends the contents of @body to
|
32
|
+
# @recipients
|
33
|
+
def send_report(report_name="No Subject")
|
34
|
+
return if @body.empty?
|
35
|
+
Net::SMTP.start(@host,@port,@host,@account,@password,@auth) do |smtp|
|
36
|
+
smtp.send_message( "Subject: #{report_name}\n\n#{@body}",
|
37
|
+
@address,
|
38
|
+
@recipients )
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
#!/usr/local/bin/ruby
|
2
|
+
# mock_db.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
|
+
class MockDB
|
11
|
+
# Creates a new MockDB e.g.
|
12
|
+
#
|
13
|
+
# MockDB.new([ "ruport", "ruport_example",
|
14
|
+
# "localhost", "test", "123"] )
|
15
|
+
#
|
16
|
+
# Which would create a fake database ruport_example at localhost with the
|
17
|
+
# username test and the password 123 using the driver 'ruport'
|
18
|
+
|
19
|
+
def initialize(args)
|
20
|
+
@db_driver, @db_name, @db_host, @username, @password = *args
|
21
|
+
@data = Hash.new
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
# Verifies account info and retrieves information
|
26
|
+
def process( dsn, user, password, query )
|
27
|
+
if ( dsn.eql?("#{@db_driver}:#{@db_name}:#{@db_host}") and
|
28
|
+
user.eql?(@username) and password.eql?(@password) )
|
29
|
+
@data[query]
|
30
|
+
else
|
31
|
+
throw "Processing Error"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
# Allows query to be looked up in the data
|
37
|
+
def [](query)
|
38
|
+
@data[query]
|
39
|
+
end
|
40
|
+
|
41
|
+
# Used to setup fake queries E.g:
|
42
|
+
#
|
43
|
+
# self.fake_db["SELECT * FROM address_book"] =
|
44
|
+
# DataSet.load("data/addressbook.csv")
|
45
|
+
def []=(query, contents)
|
46
|
+
@data[query] = contents
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
#!/usr/local/bin/ruby
|
2
|
+
# mock_report.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
|
+
class MockReport < Report
|
11
|
+
|
12
|
+
attr_accessor :fake_db
|
13
|
+
|
14
|
+
def execute (query, source = :string)
|
15
|
+
query = get_query(source, query)
|
16
|
+
yield(@fake_db.process(@dsn,@user,@password, query))
|
17
|
+
end
|
18
|
+
|
19
|
+
def select( query, source = :string, &action )
|
20
|
+
source != :string || query = "SELECT " + query
|
21
|
+
execute( query, source ) do |table|
|
22
|
+
if table.kind_of?(DataSet)
|
23
|
+
@column_names = table.fields
|
24
|
+
else
|
25
|
+
@column_names = table[0].keys
|
26
|
+
end
|
27
|
+
@first_row = true
|
28
|
+
table.each do |row|
|
29
|
+
row = row.to_hash if table.kind_of?(DataSet)
|
30
|
+
action.call(row) if block_given?
|
31
|
+
@first_row = false
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def query ( query, source = :string, &action)
|
37
|
+
execute( query, source ) do |table|
|
38
|
+
action.call(table)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
|
@@ -0,0 +1,212 @@
|
|
1
|
+
#!/usr/local/bin/ruby
|
2
|
+
# report.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
|
+
class Report
|
21
|
+
def initialize( dsn, user, password, mailer=nil )
|
22
|
+
@dsn = dsn
|
23
|
+
@user = user
|
24
|
+
@password = password
|
25
|
+
@report_name = ""
|
26
|
+
@mailer = mailer
|
27
|
+
File.exists?("log") or FileUtils.mkdir("log")
|
28
|
+
@logger = Logger.new("log/ruport.log")
|
29
|
+
@generate = true
|
30
|
+
@report = ""
|
31
|
+
end
|
32
|
+
|
33
|
+
attr_accessor :query_table, :file
|
34
|
+
attr_reader :mailer, :generate, :config, :logger
|
35
|
+
|
36
|
+
# * DEPRECATED: Use query() *
|
37
|
+
#
|
38
|
+
# Takes a query, an optional sourcetype, and a block which is passed the
|
39
|
+
# results row by row. When passed a query in string form, it adds the
|
40
|
+
# SELECT clause to the string and executes the query. When passed a
|
41
|
+
# filename and a source :file, it looks in queries/ for the file specified.
|
42
|
+
# When given a database query label, it looks in config[query_table] for a
|
43
|
+
# query with the label specified. If no source is specified, it uses
|
44
|
+
# string by default for the source.
|
45
|
+
#
|
46
|
+
# Example:
|
47
|
+
#
|
48
|
+
# select ( "* FROM test" )
|
49
|
+
# Passes "SELECT * FROM test" to the database
|
50
|
+
#
|
51
|
+
# select ( "test.sql", :file )
|
52
|
+
# Passes the contents of queries/test.sql to the database
|
53
|
+
#
|
54
|
+
# select ( "TEST", :db )
|
55
|
+
# Calls the query TEST stored in the database and query_table specified in
|
56
|
+
# config/ruport.yaml
|
57
|
+
|
58
|
+
def select( query, source = :string, &action )
|
59
|
+
source != :string || query = "SELECT " + query
|
60
|
+
execute( query, source ) do |sth|
|
61
|
+
@column_names = sth.column_names
|
62
|
+
@first_row = true
|
63
|
+
sth.fetch do |row|
|
64
|
+
action.call(row) if block_given?
|
65
|
+
@first_row = false
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# * DEPRECATED, Use query() *
|
71
|
+
#
|
72
|
+
# Takes a query and an optional sourcetype and then runs the query
|
73
|
+
# against the database. The output is not returned. This is useful for
|
74
|
+
# doing construction and destruction actions.
|
75
|
+
def execute( query, source = :string )
|
76
|
+
query = get_query(source, query)
|
77
|
+
DBI.connect(@dsn, @user, @password) do |dbh|
|
78
|
+
dbh.prepare(query) do |sth|
|
79
|
+
sth.execute()
|
80
|
+
yield(sth)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Takes a query and an optional sourcetype and then runs the query against the
|
86
|
+
# database. The return from the query is then converted into a DataSet which
|
87
|
+
# can then be manipulated.
|
88
|
+
#
|
89
|
+
# Example:
|
90
|
+
# query("SELECT * FROM address_book") do |data|
|
91
|
+
# @report << data.to_csv
|
92
|
+
# end
|
93
|
+
#
|
94
|
+
# The above would select all from the _address_book_ table and then convert
|
95
|
+
# the DataSet to a csv and then add it to the report
|
96
|
+
|
97
|
+
def query ( statement, source = :string, &action )
|
98
|
+
query_text = get_query(source,statement)
|
99
|
+
DBI.connect(@dsn, @user, @password) do |dbh|
|
100
|
+
data = DataSet.new
|
101
|
+
sth = dbh.execute(query_text)
|
102
|
+
if block_given?
|
103
|
+
data.fields = sth.column_names
|
104
|
+
sth.each { |row| data << row }
|
105
|
+
action.call(data)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# Evaluates _code_ from _filename_ as pure ruby code for files ending in
|
111
|
+
# .rb, and as ERb templates for anything else.
|
112
|
+
def eval_report( filename, code )
|
113
|
+
if filename =~ /\.rb/
|
114
|
+
eval(code)
|
115
|
+
else
|
116
|
+
ERB.new(code, 0, "%").run(binding)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# Loads a yaml file into the @config variable which is accessible in
|
121
|
+
# templates. If _interactive_ is set to true, the user will be prompted to
|
122
|
+
# enter information for each key.
|
123
|
+
def load_yaml(file, interactive=false)
|
124
|
+
file = "config/#{file}" unless File.exists? file
|
125
|
+
@config = YAML.load(File.open(file))
|
126
|
+
if (interactive)
|
127
|
+
@config.keys.map { |c| c.to_s }.sort.each do |property|
|
128
|
+
$stderr.print "#{property} (#{@config[property.to_sym]}): "
|
129
|
+
entered = $stdin.gets.strip
|
130
|
+
@config[property] = entered unless entered.eql?("")
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# Creates an SQL Object. The block is evaluated within the SQL instance so
|
136
|
+
# you may use any methods available to the SQL class. The generated query is
|
137
|
+
# returned.
|
138
|
+
#
|
139
|
+
# Example
|
140
|
+
# fetch { from :address_book order :name }
|
141
|
+
# => "SELECT * FROM address_book ORDER BY name
|
142
|
+
|
143
|
+
def fetch( &dsl )
|
144
|
+
begin
|
145
|
+
SQL.new(:SELECT, &dsl).to_s
|
146
|
+
rescue Exception
|
147
|
+
@logger.fatal "Error generating SQL."
|
148
|
+
raise "Error generating SQL."
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# Returns the query string for queries of type string, file, or db
|
153
|
+
def get_query(type,query)
|
154
|
+
case (type)
|
155
|
+
when :string
|
156
|
+
query
|
157
|
+
when :file
|
158
|
+
load_file( query )
|
159
|
+
when :db
|
160
|
+
select ( "query FROM #{@query_table} WHERE " +
|
161
|
+
"label LIKE '#{query}';" ) do |row| return row["query"] end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
# Loads a query from file. Accepts absolute paths, relative paths from
|
166
|
+
# the toplevel directory, and relative paths from _queries/_.
|
167
|
+
# If you've been putting all your database queries in _queries_, you can
|
168
|
+
# just call them by name.
|
169
|
+
def load_file( query_file )
|
170
|
+
begin
|
171
|
+
File.read( query_file ).strip
|
172
|
+
rescue
|
173
|
+
@logger.fatal "Could not open #{query_file}"
|
174
|
+
raise "Could not open #{query_file}"; exit
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
# Generates the report. If @pre or @post are defined with lambdas, they will
|
179
|
+
# be called before and after the main code. generate_report will
|
180
|
+
# automatically be run at the end of the execution of a ruport template unless
|
181
|
+
# @generate is set to false.
|
182
|
+
#
|
183
|
+
#If @file != nil, ruport will print to the
|
184
|
+
# file with the specified name. Otherwise, it will print to STDOUT by
|
185
|
+
# default. @file can be set implictly by a file argument, i.e.
|
186
|
+
# ruport templates/foo.rb reports/foo_out.html => @file =
|
187
|
+
# 'reports/foo_out.html' It can also be set explicitly within a template. A
|
188
|
+
# good way to define a default value is @file ||= "reports/somefile"
|
189
|
+
#
|
190
|
+
# Lastly, if you have your mailer configuration set up and you've specified
|
191
|
+
# recipients, the contents of @mailer.body will be automatically emailed by
|
192
|
+
# this fuction.
|
193
|
+
#
|
194
|
+
# I am aware of the fact that this function is a bear. It will be cleaned up
|
195
|
+
# in future releases
|
196
|
+
|
197
|
+
def generate_report
|
198
|
+
@pre.call if @pre
|
199
|
+
if (@file.nil?)
|
200
|
+
puts(@report)
|
201
|
+
else
|
202
|
+
File.open(@file,"w") { |f| f.puts @report }
|
203
|
+
end
|
204
|
+
unless @mailer.nil? || @mailer.recipients.empty?
|
205
|
+
@mailer.body = @report unless not @mailer.body.empty?
|
206
|
+
@mailer.send_report(@report_name)
|
207
|
+
end
|
208
|
+
@post.call if @post
|
209
|
+
end
|
210
|
+
|
211
|
+
end
|
212
|
+
|