ruport 0.1.0 → 0.2.0
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.
- 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
|
+
|