ruport 0.5.4 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- data/AUTHORS +13 -4
- data/CHANGELOG +15 -1
- data/Rakefile +1 -1
- data/bin/rope +5 -1
- data/examples/basic_grouping.rb +13 -3
- data/examples/latex_table.rb +17 -0
- data/examples/line_graph_report.rb +23 -0
- data/examples/line_graph_report.rb.rej +15 -0
- data/examples/line_graph_report.rb~ +23 -0
- data/examples/line_plotter.rb +1 -0
- data/examples/sql_erb.rb +20 -0
- data/examples/template.rb +1 -1
- data/examples/text_processors.rb +3 -9
- data/lib/ruport.rb +2 -3
- data/lib/ruport.rb~ +69 -0
- data/lib/ruport/attempt.rb +63 -0
- data/lib/ruport/data.rb +1 -1
- data/lib/ruport/data.rb.rej +5 -0
- data/lib/ruport/data.rb~ +1 -0
- data/lib/ruport/data/groupable.rb +20 -0
- data/lib/ruport/data/record.rb +18 -13
- data/lib/ruport/data/table.rb +92 -26
- data/lib/ruport/data/table.rb~ +329 -0
- data/lib/ruport/format.rb +7 -1
- data/lib/ruport/format/engine/table.rb +2 -1
- data/lib/ruport/format/plugin.rb +8 -8
- data/lib/ruport/format/plugin/csv_plugin.rb +5 -1
- data/lib/ruport/format/plugin/html_plugin.rb +12 -8
- data/lib/ruport/format/plugin/latex_plugin.rb +50 -0
- data/lib/ruport/format/plugin/text_plugin.rb +38 -33
- data/lib/ruport/meta_tools.rb +3 -3
- data/lib/ruport/query.rb +21 -9
- data/lib/ruport/report.rb +35 -20
- data/lib/ruport/report/graph.rb +14 -0
- data/lib/ruport/system_extensions.rb +9 -8
- data/test/_test_groupable.rb +0 -0
- data/test/samples/data.tsv +3 -0
- data/test/samples/erb_test.sql +1 -0
- data/test/samples/query_test.sql +1 -0
- data/test/test_collection.rb +1 -1
- data/test/test_format.rb +1 -1
- data/test/test_format_engine.rb +17 -0
- data/test/test_groupable.rb +41 -0
- data/test/test_invoice.rb +1 -1
- data/test/test_latex.rb +20 -0
- data/test/test_plugin.rb +59 -29
- data/test/test_query.rb +12 -6
- data/test/test_record.rb +23 -4
- data/test/test_record.rb.rej +46 -0
- data/test/test_report.rb +32 -7
- data/test/test_table.rb +152 -4
- data/test/ts_all.rb +21 -0
- data/test/unit.log +61 -154
- metadata +32 -12
- data/lib/ruport/rails.rb +0 -2
- data/lib/ruport/rails/reportable.rb +0 -58
data/lib/ruport/format.rb
CHANGED
@@ -75,6 +75,10 @@ module Ruport
|
|
75
75
|
options[:auto_render] = false; simple_interface(engine,options) })
|
76
76
|
end
|
77
77
|
|
78
|
+
%w[engine plugin].each { |lib|
|
79
|
+
require "ruport/format/#{lib}"
|
80
|
+
}
|
81
|
+
|
78
82
|
@@filters = Hash.new
|
79
83
|
|
80
84
|
# To hook up a Format object to your current class, you need to pass it a
|
@@ -147,7 +151,9 @@ module Ruport
|
|
147
151
|
|
148
152
|
options[:auto_render] ? my_engine.render : my_engine.dup
|
149
153
|
end
|
154
|
+
|
155
|
+
|
156
|
+
|
150
157
|
end
|
151
158
|
end
|
152
159
|
|
153
|
-
%w[engine plugin].each { |lib| require "ruport/format/#{lib}" }
|
@@ -5,7 +5,8 @@ module Ruport
|
|
5
5
|
renderer do
|
6
6
|
super
|
7
7
|
active_plugin.rendered_field_names = ""
|
8
|
-
build_field_names if (data.respond_to?(:column_names) &&
|
8
|
+
build_field_names if (data.respond_to?(:column_names) &&
|
9
|
+
!data.column_names.empty? &&
|
9
10
|
data.column_names && show_field_names)
|
10
11
|
a = active_plugin.render_table
|
11
12
|
end
|
data/lib/ruport/format/plugin.rb
CHANGED
@@ -47,14 +47,14 @@ module Ruport
|
|
47
47
|
singleton_class.send( :define_method, :build_field_names, &block)
|
48
48
|
end
|
49
49
|
|
50
|
-
def register_on(
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
klass.accept_format_plugin(self)
|
50
|
+
def register_on(*args)
|
51
|
+
args.each { |klass|
|
52
|
+
if klass.kind_of? Symbol
|
53
|
+
klass = Format::Engine.engine_classes[klass]
|
54
|
+
end
|
57
55
|
|
56
|
+
klass.accept_format_plugin(self)
|
57
|
+
}
|
58
58
|
rescue NoMethodError
|
59
59
|
p caller
|
60
60
|
end
|
@@ -72,5 +72,5 @@ module Ruport
|
|
72
72
|
end
|
73
73
|
end
|
74
74
|
end
|
75
|
-
plugins = %w[text csv pdf svg html]
|
75
|
+
plugins = %w[text csv pdf svg html latex]
|
76
76
|
plugins.each { |p| require "ruport/format/plugin/#{p}_plugin" }
|
@@ -5,7 +5,11 @@ module Ruport
|
|
5
5
|
helper(:init_plugin) { |eng| require "fastercsv" }
|
6
6
|
|
7
7
|
format_field_names do
|
8
|
-
|
8
|
+
if data.column_names.empty?
|
9
|
+
""
|
10
|
+
else
|
11
|
+
FasterCSV.generate { |csv| csv << data.column_names }
|
12
|
+
end
|
9
13
|
end
|
10
14
|
|
11
15
|
renderer :table do
|
@@ -2,21 +2,25 @@ module Ruport
|
|
2
2
|
class Format::Plugin
|
3
3
|
class HTMLPlugin < Format::Plugin
|
4
4
|
|
5
|
-
rendering_options :red_cloth_enabled => true,
|
6
|
-
|
7
|
-
|
5
|
+
rendering_options :red_cloth_enabled => true, :erb_enabled => true
|
6
|
+
|
8
7
|
renderer :document
|
9
8
|
|
10
9
|
renderer :table do
|
11
|
-
|
10
|
+
data.inject("\t<table>\n" + rendered_field_names) do |s,r|
|
12
11
|
row = r.map { |e| e.to_s.empty? ? " " : e }
|
13
|
-
|
14
|
-
|
15
|
-
|
12
|
+
classstr = defined?(r.tags) ?
|
13
|
+
r.tags.inject("") {|cs,c| cs + " class='#{c}'" } : ""
|
14
|
+
s + "\t\t<tr#{classstr}>\n\t\t\t<td#{classstr}>" +
|
15
|
+
row.to_a.join("</td>\n\t\t\t<td#{classstr}>") +
|
16
|
+
"</td>\n\t\t</tr>\n"
|
17
|
+
end + "\t</table>"
|
16
18
|
end
|
17
19
|
|
18
20
|
format_field_names do
|
19
|
-
|
21
|
+
"\t\t<tr>\n\t\t\t<th>" +
|
22
|
+
data.column_names.join("</th>\n\t\t\t<th>") +
|
23
|
+
"</th>\n\t\t</tr>\n"
|
20
24
|
end
|
21
25
|
|
22
26
|
plugin_name :html
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Ruport
|
2
|
+
class Format::Plugin
|
3
|
+
class LatexPlugin < Format::Plugin
|
4
|
+
|
5
|
+
helper(:init_plugin) { |eng|
|
6
|
+
@report_header = "\\documentclass[11pt]{article}\n"
|
7
|
+
@report_header << "\\RequirePackage{lscape,longtable}\n"
|
8
|
+
@report_header << "\\begin{document}\n"
|
9
|
+
|
10
|
+
@report_footer = "\\end{document}\n"
|
11
|
+
}
|
12
|
+
|
13
|
+
renderer :table do
|
14
|
+
self.options = {} if self.options.nil?
|
15
|
+
|
16
|
+
@body = "\\begin{longtable}[c]{ "
|
17
|
+
@data.column_names.each do
|
18
|
+
@body << " p{2cm} "
|
19
|
+
end
|
20
|
+
@body << " }\n"
|
21
|
+
@body << "\\hline\n"
|
22
|
+
counter = 0
|
23
|
+
@data.column_names.each do |t|
|
24
|
+
@body << " & " unless counter == 0
|
25
|
+
@body << "\\textsc{#{t}}"
|
26
|
+
counter += 1
|
27
|
+
end
|
28
|
+
@body << "\\\\\n"
|
29
|
+
@body << "\\hline\n"
|
30
|
+
@body << "\\endhead\n"
|
31
|
+
@body << "\\endfoot\n"
|
32
|
+
@body << "\\hline\n"
|
33
|
+
@data.each do |r|
|
34
|
+
@body << r.data.join(" & ") + "\\\\\n"
|
35
|
+
@body << "\\hline\n"
|
36
|
+
end
|
37
|
+
unless options[:caption].nil?
|
38
|
+
@body << "\\caption[#{options[:caption]}]{#{options[:caption]}}\n"
|
39
|
+
end
|
40
|
+
@body << "\\end{longtable}\n"
|
41
|
+
|
42
|
+
@report_header + @body + @report_footer
|
43
|
+
end
|
44
|
+
|
45
|
+
plugin_name :latex
|
46
|
+
register_on :table_engine
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -9,56 +9,61 @@ module Ruport
|
|
9
9
|
require "ruport/system_extensions"
|
10
10
|
|
11
11
|
return "" if data.length == 0;
|
12
|
-
|
13
|
-
|
12
|
+
|
13
|
+
calculate_max_col_widths
|
14
|
+
|
15
|
+
width = self.right_margin || SystemExtensions.terminal_width
|
16
|
+
|
17
|
+
s = "#{rendered_field_names}#{hr}"
|
18
|
+
|
14
19
|
data.each { |r|
|
20
|
+
line = Array.new
|
15
21
|
r.each_with_index { |f,i|
|
16
|
-
|
22
|
+
line << f.to_s.center(max_col_width[i])
|
17
23
|
}
|
24
|
+
s += "| #{line.join(' | ')} |\n"
|
18
25
|
}
|
19
|
-
|
20
|
-
a = data.inject(th){ |s,r|
|
21
|
-
s + "| #{r.to_a.join(' | ')} |\n"
|
22
|
-
} << hr
|
26
|
+
s += hr
|
23
27
|
|
24
|
-
|
25
|
-
|
26
|
-
a.to_a.each { |r|
|
28
|
+
s.split("\n").each { |r|
|
27
29
|
r.gsub!(/\A.{#{width+1},}/) { |m| m[0,width-2] + ">>" }
|
28
|
-
}.join
|
30
|
+
}.join("\n") + "\n"
|
31
|
+
|
29
32
|
end
|
30
33
|
|
31
34
|
format_field_names do
|
32
|
-
return "" if data.length == 0
|
33
|
-
|
34
|
-
|
35
|
+
return "" if data.length == 0
|
36
|
+
calculate_max_col_widths
|
37
|
+
c=data.column_names.dup
|
38
|
+
c.each_with_index { |f,i|
|
39
|
+
c[i] = f.to_s.center(max_col_width[i])
|
35
40
|
}
|
36
|
-
"#{hr}| #{
|
41
|
+
"#{hr}| #{c.to_a.join(' | ')} |\n"
|
37
42
|
end
|
38
43
|
|
39
|
-
action :max_col_width do
|
40
|
-
|
41
|
-
d = Data::Table.new :column_names => f, :data => data
|
42
|
-
|
43
|
-
cw = d.map { |r| r[index].to_s.length }.max
|
44
|
-
|
45
|
-
return cw unless d.column_names
|
46
|
-
|
47
|
-
nw = (index.kind_of?(Integer) ? d.column_names[index] : index ).to_s.length
|
48
|
-
|
49
|
-
[cw,nw].max
|
44
|
+
action :max_col_width do
|
45
|
+
@max_col_width
|
50
46
|
end
|
51
47
|
|
52
|
-
action :
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
48
|
+
action :calculate_max_col_widths do
|
49
|
+
@max_col_width=Array.new
|
50
|
+
if defined?(data.column_names)
|
51
|
+
data.column_names.each_index do |i|
|
52
|
+
@max_col_width[i] = data.column_names[i].to_s.length
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
data.each {|r|
|
57
|
+
r.each_with_index { |f,i|
|
58
|
+
if !max_col_width[i] || f.to_s.length > max_col_width[i]
|
59
|
+
max_col_width[i] = f.to_s.length
|
60
|
+
end
|
61
|
+
}
|
62
|
+
}
|
58
63
|
end
|
59
64
|
|
60
65
|
action :hr do
|
61
|
-
len = data[0].to_a.length * 3 +
|
66
|
+
len = max_col_width.inject(data[0].to_a.length * 3) {|s,e| s+e} + 1
|
62
67
|
"+" + "-"*(len-2) + "+\n"
|
63
68
|
end
|
64
69
|
|
data/lib/ruport/meta_tools.rb
CHANGED
@@ -5,7 +5,7 @@ module Ruport
|
|
5
5
|
# formatting system, and might be helpful for other things.
|
6
6
|
#
|
7
7
|
module MetaTools
|
8
|
-
#
|
8
|
+
# Allows you to define an attribute accessor on the singleton_class.
|
9
9
|
#
|
10
10
|
# Example:
|
11
11
|
#
|
@@ -21,14 +21,14 @@ module Ruport
|
|
21
21
|
self.send("#{sym}=",value)
|
22
22
|
end
|
23
23
|
|
24
|
-
#
|
24
|
+
# Same as attribute, but takes an array of attributes
|
25
25
|
#
|
26
26
|
# e.g. attributes [:foo,:bar,:baz]
|
27
27
|
def attributes(syms)
|
28
28
|
syms.each { |s| attribute s }
|
29
29
|
end
|
30
30
|
|
31
|
-
#
|
31
|
+
# Allows you to define a method on the singleton_class
|
32
32
|
#
|
33
33
|
# Example:
|
34
34
|
#
|
data/lib/ruport/query.rb
CHANGED
@@ -59,11 +59,18 @@ module Ruport
|
|
59
59
|
# :user => "greg", :password => "chunky_bacon" )
|
60
60
|
#
|
61
61
|
# # uses a SQL file stored on disk
|
62
|
-
# Ruport::Query.new("my_query.sql"
|
62
|
+
# Ruport::Query.new("my_query.sql")
|
63
|
+
#
|
64
|
+
# # explicitly use a file, even if it doesn't end in .sql
|
65
|
+
# Ruport::Query.new("foo",:origin => :file)
|
63
66
|
def initialize(sql, options={})
|
64
67
|
options = { :source => :default, :origin => :string }.merge(options)
|
65
|
-
|
66
|
-
|
68
|
+
options[:binding] ||= binding
|
69
|
+
options[:origin] = :file if sql =~ /.sql$/
|
70
|
+
q = Format.document :data => get_query(options[:origin],sql),
|
71
|
+
:plugin => :text, :class_binding => options[:binding]
|
72
|
+
@statements = SqlSplit.new(q)
|
73
|
+
@sql = @statements.join
|
67
74
|
|
68
75
|
if options[:dsn]
|
69
76
|
Ruport::Config.source :temp, :dsn => options[:dsn],
|
@@ -76,6 +83,7 @@ module Ruport
|
|
76
83
|
|
77
84
|
@raw_data = options[:raw_data]
|
78
85
|
@cache_enabled = options[:cache_enabled]
|
86
|
+
@params = options[:params]
|
79
87
|
@cached_data = nil
|
80
88
|
end
|
81
89
|
|
@@ -156,18 +164,22 @@ module Ruport
|
|
156
164
|
|
157
165
|
private
|
158
166
|
|
159
|
-
def query_data( query_text )
|
167
|
+
def query_data( query_text, params=@params )
|
160
168
|
|
161
169
|
require "dbi"
|
162
170
|
|
163
171
|
data = @raw_data ? [] : Data::Table.new
|
164
172
|
DBI.connect(@dsn, @user, @password) do |dbh|
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
results.each { |row| data << row.to_a }
|
173
|
+
if params
|
174
|
+
sth = dbh.execute(query_text,*params)
|
175
|
+
else
|
176
|
+
sth = dbh.execute(query_text)
|
170
177
|
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
|
171
183
|
end
|
172
184
|
data
|
173
185
|
rescue NoMethodError; nil
|
data/lib/ruport/report.rb
CHANGED
@@ -7,11 +7,11 @@
|
|
7
7
|
|
8
8
|
#load the needed standard libraries.
|
9
9
|
%w[erb yaml date logger fileutils].each { |lib| require lib }
|
10
|
-
|
11
|
-
require "ruport/report/invoice"
|
10
|
+
%w[invoice graph].each { |lib| require "ruport/report/"+lib }
|
12
11
|
require "forwardable"
|
13
12
|
|
14
13
|
module Ruport
|
14
|
+
|
15
15
|
# === Overview
|
16
16
|
#
|
17
17
|
# The Ruport::Report class povides a high level interface to most of Ruport's
|
@@ -72,6 +72,7 @@ module Ruport
|
|
72
72
|
class Report
|
73
73
|
extend Forwardable
|
74
74
|
|
75
|
+
include Ruport::Data::TableHelper
|
75
76
|
# When initializing a report, you can provide a default mailer and source by
|
76
77
|
# giving a name of a valid source or mailer you've defined via
|
77
78
|
# Ruport::Config
|
@@ -131,7 +132,7 @@ module Ruport
|
|
131
132
|
def query(sql, options={})
|
132
133
|
options[:origin] ||= :string
|
133
134
|
options[:source] ||= @source
|
134
|
-
|
135
|
+
options[:binding] ||= binding
|
135
136
|
q = options[:query_obj] || Query.new(sql, options)
|
136
137
|
if options[:yield_type].eql?(:by_row)
|
137
138
|
q.each { |r| yield(r) }
|
@@ -178,16 +179,13 @@ module Ruport
|
|
178
179
|
string
|
179
180
|
end
|
180
181
|
|
181
|
-
# preserved for backwards compatibility. please do not use.
|
182
|
-
alias_method :render, :process_text
|
183
|
-
|
184
182
|
# This allows you to create filters to be used by process_text
|
185
183
|
#
|
186
184
|
# The block is evaluated in the context of the instance.
|
187
185
|
#
|
188
186
|
# E.g
|
189
187
|
#
|
190
|
-
# text_processor(:unix_newlines) {
|
188
|
+
# text_processor(:unix_newlines) { |r| r.gsub(/\r\n/,"\n") }
|
191
189
|
def text_processor(label,&block)
|
192
190
|
Format.register_filter(label, &block)
|
193
191
|
end
|
@@ -196,8 +194,8 @@ module Ruport
|
|
196
194
|
# Writes the contents of <tt>results</tt> to file. If a filename is
|
197
195
|
# specified, it will use it. Otherwise, it will try to write to the file
|
198
196
|
# specified by the <tt>file</tt> attribute.
|
199
|
-
def write(my_file=file)
|
200
|
-
File.open(my_file,"w") { |f| f <<
|
197
|
+
def write(my_file=file,my_results=results)
|
198
|
+
File.open(my_file,"w") { |f| f << my_results }
|
201
199
|
end
|
202
200
|
|
203
201
|
# Like Report#write, but will append to a file rather than overwrite it if
|
@@ -235,22 +233,39 @@ module Ruport
|
|
235
233
|
# (print something out, write to file, email, etc)
|
236
234
|
#
|
237
235
|
# Finally, it tries to call cleanup.
|
238
|
-
def run(
|
239
|
-
reports
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
236
|
+
def run(options={})
|
237
|
+
options[:reports] ||= [self.new]
|
238
|
+
|
239
|
+
process = lambda do
|
240
|
+
options[:reports].each { |rep|
|
241
|
+
rep.prepare if rep.respond_to? :prepare
|
242
|
+
rep.results = rep.generate
|
243
|
+
yield(rep) if block_given?
|
244
|
+
rep.cleanup if rep.respond_to? :cleanup
|
245
|
+
}
|
246
|
+
end
|
247
|
+
|
248
|
+
if options[:tries] && (options[:interval] || options[:timeout])
|
249
|
+
code = Attempt.new { |a|
|
250
|
+
a.tries = options[:tries]
|
251
|
+
a.interval = options[:interval] if options[:interval]
|
252
|
+
a.timeout = options[:timeout] if options[:timeout]
|
253
|
+
a.log_level = options[:log_level]
|
254
|
+
}
|
255
|
+
code.attempt(&process)
|
256
|
+
else
|
257
|
+
process.call
|
258
|
+
end
|
246
259
|
end
|
260
|
+
|
247
261
|
end
|
248
262
|
|
249
263
|
# this method passes <tt>self</tt> to Report.run
|
250
264
|
#
|
251
265
|
# Please see the class method for details.
|
252
|
-
def run(
|
253
|
-
self
|
266
|
+
def run(options={},&block)
|
267
|
+
options[:reports] ||= [self]
|
268
|
+
self.class.run(options,&block)
|
254
269
|
end
|
255
270
|
|
256
271
|
|
@@ -282,7 +297,7 @@ module Ruport
|
|
282
297
|
m = Mailer.new
|
283
298
|
m.to = adds
|
284
299
|
yield(m)
|
285
|
-
m.select_mailer
|
300
|
+
m.send(:select_mailer,@mailer)
|
286
301
|
m.deliver :from => m.from, :to => m.to
|
287
302
|
end
|
288
303
|
|