ruport 0.6.0 → 0.6.1
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 +4 -0
- data/CHANGELOG +10 -1
- data/Rakefile +1 -1
- data/examples/simple_table_interface.rb +20 -0
- data/lib/ruport.rb +87 -69
- data/lib/ruport/attempt.rb +1 -1
- data/lib/ruport/config.rb +198 -166
- data/lib/ruport/data.rb +6 -1
- data/lib/ruport/data/collection.rb +15 -8
- data/lib/ruport/data/groupable.rb +68 -6
- data/lib/ruport/data/record.rb +73 -34
- data/lib/ruport/data/record.rb~ +236 -0
- data/lib/ruport/data/set.rb +48 -13
- data/lib/ruport/data/table.rb +164 -74
- data/lib/ruport/data/table.rb.rej +67 -0
- data/lib/ruport/data/table.rb~ +153 -68
- data/lib/ruport/data/taggable.rb +37 -9
- data/lib/ruport/format.rb +1 -1
- data/lib/ruport/mailer.rb +41 -27
- data/lib/ruport/meta_tools.rb +26 -11
- data/lib/ruport/query.rb +102 -68
- data/lib/ruport/query/sql_split.rb +1 -1
- data/lib/ruport/report.rb +84 -58
- data/lib/ruport/report/graph.rb +1 -1
- data/lib/ruport/report/invoice.rb +1 -1
- data/test/test_query.rb +305 -48
- data/test/test_query.rb.rej +161 -0
- data/test/test_query.rb~ +337 -0
- data/test/test_record.rb +6 -0
- data/test/test_table.rb +18 -0
- data/test/test_table.rb~ +336 -0
- data/test/unit.log +180 -6
- metadata +8 -2
data/lib/ruport/format.rb
CHANGED
data/lib/ruport/mailer.rb
CHANGED
@@ -6,37 +6,47 @@
|
|
6
6
|
# See LICENSE for details
|
7
7
|
require "net/smtp"
|
8
8
|
require "forwardable"
|
9
|
-
module Ruport
|
10
9
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
10
|
+
module Ruport
|
11
|
+
|
12
|
+
#
|
13
|
+
# === Overview
|
14
|
+
#
|
15
|
+
# This class uses SMTP to provide a simple mail sending mechanism.
|
16
|
+
# It also uses MailFactory to provide attachment and HTML email support.
|
17
|
+
#
|
18
|
+
# === Example
|
19
|
+
#
|
20
|
+
# Here is a simple example of a message which attaches a README file:
|
21
|
+
#
|
22
|
+
# require "ruport"
|
23
|
+
#
|
24
|
+
# Ruport.configure do |config|
|
25
|
+
# config.mailer :default,
|
26
|
+
# :host => "mail.adelphia.net",
|
27
|
+
# :address => "gregory.t.brown@gmail.com"
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# mailer = Ruport::Mailer.new
|
31
|
+
#
|
32
|
+
# mailer.attach "README"
|
33
|
+
#
|
34
|
+
# mailer.deliver :to => "gregory.t.brown@gmail.com",
|
35
|
+
# :from => "gregory.t.brown@gmail.com",
|
36
|
+
# :subject => "Hey there",
|
37
|
+
# :text => "This is what you asked for"
|
38
|
+
#
|
31
39
|
class Mailer
|
32
40
|
extend Forwardable
|
33
41
|
|
34
|
-
|
35
|
-
# Creates a new Mailer object. Optionally, can select a mailer specified
|
36
|
-
# by Ruport::Config.
|
37
42
|
#
|
38
|
-
#
|
39
|
-
#
|
43
|
+
# Creates a new Mailer object. Optionally, you can select a mailer
|
44
|
+
# specified by Ruport::Config.
|
45
|
+
#
|
46
|
+
# Example:
|
47
|
+
#
|
48
|
+
# a = Mailer.new # uses the :default mailer
|
49
|
+
# a = Mailer.new :foo # uses :foo mail config from Ruport::Config
|
40
50
|
#
|
41
51
|
def initialize( mailer_label=:default )
|
42
52
|
select_mailer(mailer_label);
|
@@ -49,10 +59,14 @@ module Ruport
|
|
49
59
|
:subject, :subject=, :attach,
|
50
60
|
:text, :text=, :html, :html= )
|
51
61
|
|
52
|
-
#
|
62
|
+
#
|
63
|
+
# Sends the message.
|
64
|
+
#
|
65
|
+
# Example:
|
53
66
|
#
|
54
67
|
# mailer.deliver :from => "gregory.t.brown@gmail.com",
|
55
68
|
# :to => "greg7224@gmail.com"
|
69
|
+
#
|
56
70
|
def deliver(options={})
|
57
71
|
options.each { |k,v| send("#{k}=",v) if respond_to? "#{k}=" }
|
58
72
|
|
data/lib/ruport/meta_tools.rb
CHANGED
@@ -1,11 +1,18 @@
|
|
1
|
+
# Gets raised if you try to add the same action to an object more than once.
|
1
2
|
class ActionAlreadyDefinedError < RuntimeError; end
|
3
|
+
|
2
4
|
module Ruport
|
5
|
+
|
6
|
+
#
|
7
|
+
# === Overview
|
8
|
+
#
|
3
9
|
# This module provides a few tools for doing some manipulations of the
|
4
|
-
#
|
5
|
-
# formatting system, and might be
|
10
|
+
# singleton class of an object. These are used in the implementation of
|
11
|
+
# Ruport's formatting system, and might be useful for other things.
|
6
12
|
#
|
7
13
|
module MetaTools
|
8
|
-
#
|
14
|
+
#
|
15
|
+
# Allows you to define an attribute accessor on the singleton class.
|
9
16
|
#
|
10
17
|
# Example:
|
11
18
|
#
|
@@ -14,21 +21,27 @@ module Ruport
|
|
14
21
|
# attribute :foo
|
15
22
|
# end
|
16
23
|
#
|
17
|
-
# A.foo
|
18
|
-
# A.foo = 7
|
24
|
+
# A.foo #=> nil
|
25
|
+
# A.foo = 7 #=> 7
|
26
|
+
#
|
19
27
|
def attribute(sym,value = nil)
|
20
28
|
singleton_class.send(:attr_accessor, sym )
|
21
29
|
self.send("#{sym}=",value)
|
22
30
|
end
|
23
31
|
|
24
|
-
# Same as attribute, but takes an array of attributes
|
25
32
|
#
|
26
|
-
#
|
33
|
+
# Same as <tt>attribute</tt>, but takes an Array of attributes.
|
34
|
+
#
|
35
|
+
# Example:
|
36
|
+
#
|
37
|
+
# attributes [:foo,:bar,:baz]
|
38
|
+
#
|
27
39
|
def attributes(syms)
|
28
40
|
syms.each { |s| attribute s }
|
29
41
|
end
|
30
42
|
|
31
|
-
#
|
43
|
+
#
|
44
|
+
# Allows you to define a method on the singleton class.
|
32
45
|
#
|
33
46
|
# Example:
|
34
47
|
#
|
@@ -37,15 +50,17 @@ module Ruport
|
|
37
50
|
# action(:bar) { |x| x + 1 }
|
38
51
|
# end
|
39
52
|
#
|
40
|
-
# A.bar(3)
|
53
|
+
# A.bar(3) #=> 4
|
54
|
+
#
|
41
55
|
def action(name,&block)
|
42
56
|
raise ActionAlreadyDefinedError if respond_to? name
|
43
57
|
singleton_class.send(:define_method, name, &block)
|
44
58
|
end
|
59
|
+
|
45
60
|
end
|
46
61
|
end
|
47
62
|
|
48
63
|
class Module
|
49
|
-
|
50
|
-
|
64
|
+
# Provides the singleton_class object.
|
65
|
+
def singleton_class; (class << self; self; end); end
|
51
66
|
end
|
data/lib/ruport/query.rb
CHANGED
@@ -3,49 +3,59 @@ require "ruport/query/sql_split"
|
|
3
3
|
|
4
4
|
module Ruport
|
5
5
|
|
6
|
-
# Query offers a way to interact with databases via DBI. It supports
|
7
|
-
# returning result sets in either Ruport's native Data::Table, or in their raw
|
8
|
-
# form as DBI::Rows.
|
9
6
|
#
|
10
|
-
#
|
11
|
-
#
|
7
|
+
# === Overview
|
8
|
+
#
|
9
|
+
# Query offers a way to interact with databases via DBI. It supports
|
10
|
+
# returning result sets in either Ruport's native Data::Table, or in their
|
11
|
+
# raw form as DBI::Rows.
|
12
|
+
#
|
13
|
+
# It offers basic caching support, the ability to instantiate a generator
|
14
|
+
# for a result set, and the ability to quickly and easily swap between data
|
12
15
|
# sources.
|
16
|
+
#
|
13
17
|
class Query
|
14
18
|
|
15
19
|
|
16
20
|
include Enumerable
|
17
21
|
|
18
|
-
# Queries are initialized with some SQL and a number of options that effect
|
19
|
-
# their operation. They are NOT executed at initialization.
|
20
22
|
#
|
21
|
-
#
|
22
|
-
#
|
23
|
-
#
|
23
|
+
# Queries are initialized with some SQL and a number of options that
|
24
|
+
# affect their operation. They are NOT executed at initialization. This
|
25
|
+
# is important to note as they will not query the database until either
|
26
|
+
# Query#result, Query#execute, Query#generator, or an Enumerable method
|
27
|
+
# is called on them.
|
24
28
|
#
|
25
29
|
# This kind of laziness is supposed to be A Good Thing, and
|
26
30
|
# as long as you keep it in mind, it should not cause any problems.
|
27
31
|
#
|
28
|
-
# The SQL can be single or multistatement, but the resulting Data::Table
|
29
|
-
# consist only of the result of the last statement which returns
|
30
|
-
#
|
31
|
-
#
|
32
|
-
#
|
33
|
-
#
|
34
|
-
#
|
35
|
-
#
|
36
|
-
#
|
37
|
-
#
|
38
|
-
#
|
39
|
-
#
|
40
|
-
# <tt
|
41
|
-
#
|
42
|
-
# <tt>:
|
43
|
-
#
|
44
|
-
# <tt>:
|
45
|
-
#
|
46
|
-
# <tt>:
|
47
|
-
#
|
48
|
-
#
|
32
|
+
# The SQL can be single or multistatement, but the resulting Data::Table
|
33
|
+
# will consist only of the result of the last statement which returns
|
34
|
+
# something.
|
35
|
+
#
|
36
|
+
# Available options:
|
37
|
+
#
|
38
|
+
# <b><tt>:source</tt></b>:: A source specified in
|
39
|
+
# Ruport::Config.sources, defaults to
|
40
|
+
# <tt>:default</tt>.
|
41
|
+
# <b><tt>:origin</tt></b>:: Query origin, defaults to
|
42
|
+
# <tt>:string</tt>, but it can be set to
|
43
|
+
# <tt>:file</tt>, loading the path
|
44
|
+
# specified by the <tt>sql</tt>
|
45
|
+
# parameter.
|
46
|
+
# <b><tt>:dsn</tt></b>:: If specifed, the Query object will
|
47
|
+
# manually override Ruport::Config.
|
48
|
+
# <b><tt>:user</tt></b>:: If a DSN is specified, the user can
|
49
|
+
# be set with this option.
|
50
|
+
# <b><tt>:password</tt></b>:: If a DSN is specified, the password
|
51
|
+
# can be set with this option.
|
52
|
+
# <b><tt>:raw_data</tt></b>:: When set to true, DBI::Rows will be
|
53
|
+
# returned instead of a Data::Table.
|
54
|
+
# <b><tt>:cache_enabled</tt></b>:: When set to true, Query will download
|
55
|
+
# results only once, and then return
|
56
|
+
# cached values until the cache has been
|
57
|
+
# cleared.
|
58
|
+
#
|
49
59
|
# Examples:
|
50
60
|
#
|
51
61
|
# # uses Ruport::Config's default source
|
@@ -63,6 +73,7 @@ module Ruport
|
|
63
73
|
#
|
64
74
|
# # explicitly use a file, even if it doesn't end in .sql
|
65
75
|
# Ruport::Query.new("foo",:origin => :file)
|
76
|
+
#
|
66
77
|
def initialize(sql, options={})
|
67
78
|
options = { :source => :default, :origin => :string }.merge(options)
|
68
79
|
options[:binding] ||= binding
|
@@ -87,103 +98,124 @@ module Ruport
|
|
87
98
|
@cached_data = nil
|
88
99
|
end
|
89
100
|
|
90
|
-
#
|
101
|
+
#
|
102
|
+
# Set this to <tt>true</tt> to get DBI:Rows, <tt>false</tt> to get Ruport
|
103
|
+
# constructs.
|
104
|
+
#
|
91
105
|
attr_accessor :raw_data
|
92
106
|
|
93
|
-
#
|
94
|
-
# ruport when caching
|
107
|
+
# The data stored by Ruport when caching.
|
95
108
|
attr_accessor :cached_data
|
96
109
|
|
97
|
-
#
|
110
|
+
# The original SQL for the Query object
|
98
111
|
attr_reader :sql
|
99
112
|
|
100
|
-
#
|
101
|
-
#
|
113
|
+
#
|
114
|
+
# This will set the <tt>dsn</tt>, <tt>username</tt>, and <tt>password</tt>
|
115
|
+
# to one specified by a source in Ruport::Config.
|
116
|
+
#
|
102
117
|
def select_source(label)
|
103
118
|
@dsn = Ruport::Config.sources[label].dsn
|
104
119
|
@user = Ruport::Config.sources[label].user
|
105
120
|
@password = Ruport::Config.sources[label].password
|
106
121
|
end
|
107
122
|
|
108
|
-
#
|
123
|
+
#
|
124
|
+
# Standard <tt>each</tt> iterator, iterates through the result set row by
|
125
|
+
# row.
|
126
|
+
#
|
109
127
|
def each(&action)
|
110
128
|
Ruport.log(
|
111
129
|
"no block given!", :status => :fatal,
|
112
130
|
:level => :log_only, :exception => LocalJumpError
|
113
131
|
) unless action
|
114
132
|
fetch(&action)
|
133
|
+
self
|
115
134
|
end
|
116
135
|
|
117
|
-
#
|
118
|
-
# DBI::Row objects
|
136
|
+
#
|
137
|
+
# Grabs the result set as a Data::Table or an Array of DBI::Row objects
|
138
|
+
# if in <tt>raw_data</tt> mode.
|
139
|
+
#
|
119
140
|
def result; fetch; end
|
120
141
|
|
121
142
|
# Runs the query without returning its results.
|
122
143
|
def execute; fetch; nil; end
|
123
144
|
|
124
|
-
#
|
145
|
+
# Clears the contents of the cache.
|
125
146
|
def clear_cache
|
126
147
|
@cached_data = nil
|
127
148
|
end
|
128
149
|
|
129
|
-
#
|
130
|
-
# cache
|
150
|
+
#
|
151
|
+
# Clears the contents of the cache, then runs the query, filling the
|
152
|
+
# cache with the new result.
|
153
|
+
#
|
131
154
|
def update_cache
|
132
155
|
return unless @cache_enabled
|
133
156
|
clear_cache; fetch
|
134
157
|
end
|
135
158
|
|
136
|
-
#
|
137
|
-
# caching is
|
159
|
+
#
|
160
|
+
# Turns on caching. New data will not be loaded until the cache is clear
|
161
|
+
# or caching is disabled.
|
162
|
+
#
|
138
163
|
def enable_caching
|
139
164
|
@cache_enabled = true
|
140
165
|
end
|
141
166
|
|
142
|
-
# Turns off caching and flushes the cached data
|
167
|
+
# Turns off caching and flushes the cached data.
|
143
168
|
def disable_caching
|
144
169
|
clear_cache
|
145
170
|
@cache_enabled = false
|
146
171
|
end
|
147
172
|
|
148
|
-
#
|
149
|
-
#
|
173
|
+
#
|
174
|
+
# Returns a Data::Table, even if in <tt>raw_data</tt> mode.
|
175
|
+
# This doesn't work with raw data if the cache is enabled and filled.
|
176
|
+
#
|
150
177
|
def to_table
|
151
178
|
data_flag, @raw_data = @raw_data, false
|
152
179
|
data = fetch; @raw_data = data_flag; return data
|
153
180
|
end
|
154
181
|
|
155
|
-
# Returns a csv dump of the query
|
182
|
+
# Returns a csv dump of the query.
|
156
183
|
def to_csv
|
157
184
|
Format.table :plugin => :csv, :data => fetch
|
158
185
|
end
|
159
186
|
|
160
|
-
# Returns a Generator object of the result set
|
187
|
+
# Returns a Generator object of the result set.
|
161
188
|
def generator
|
162
189
|
Generator.new(fetch)
|
163
190
|
end
|
164
191
|
|
165
192
|
private
|
166
193
|
|
167
|
-
def query_data(
|
168
|
-
|
194
|
+
def query_data(query_text, params=@params)
|
169
195
|
require "dbi"
|
170
196
|
|
171
197
|
data = @raw_data ? [] : Data::Table.new
|
172
198
|
DBI.connect(@dsn, @user, @password) do |dbh|
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
199
|
+
dbh.execute(query_text, *(params || [])) do |sth|
|
200
|
+
# Work-around for inconsistent DBD behavior w/ resultless queries
|
201
|
+
names = sth.column_names rescue []
|
202
|
+
if names.empty?
|
203
|
+
# Work-around for SQLite3 DBD bug
|
204
|
+
sth.cancel rescue nil
|
205
|
+
return nil
|
206
|
+
end
|
207
|
+
|
208
|
+
data.column_names = names unless @raw_data
|
209
|
+
sth.each do |row|
|
210
|
+
row = row.to_a
|
211
|
+
row = Data::Record.new(row, :attributes => names) unless @raw_data
|
212
|
+
yield row if block_given?
|
213
|
+
data << row if !block_given? || @cache_enabled
|
214
|
+
end
|
177
215
|
end
|
178
|
-
return unless sth.fetchable?
|
179
|
-
results = sth.fetch_all
|
180
|
-
data.column_names = sth.column_names unless @raw_data
|
181
|
-
results.each { |row| data << row.to_a }
|
182
|
-
sth.finish
|
183
216
|
end
|
184
217
|
data
|
185
|
-
|
186
|
-
end
|
218
|
+
end
|
187
219
|
|
188
220
|
def get_query(type,query)
|
189
221
|
type.eql?(:file) ? load_file( query ) : query
|
@@ -196,17 +228,19 @@ module Ruport
|
|
196
228
|
end
|
197
229
|
end
|
198
230
|
|
199
|
-
def fetch
|
231
|
+
def fetch(&block)
|
200
232
|
data = nil
|
201
233
|
if @cache_enabled and @cached_data
|
202
234
|
data = @cached_data
|
235
|
+
data.each { |r| yield(r) } if block_given?
|
203
236
|
else
|
204
|
-
@statements.
|
237
|
+
final = @statements.size - 1
|
238
|
+
@statements.each_with_index do |query_text, index|
|
239
|
+
data = query_data(query_text, &(index == final ? block : nil))
|
240
|
+
end
|
241
|
+
@cached_data = data if @cache_enabled
|
205
242
|
end
|
206
|
-
data.each { |r| yield(r) } if block_given?
|
207
|
-
@cached_data = data if @cache_enabled
|
208
243
|
return data
|
209
244
|
end
|
210
|
-
|
211
245
|
end
|
212
246
|
end
|