ruport 1.0.2 → 1.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 +3 -0
- data/README +11 -5
- data/Rakefile +2 -2
- data/examples/btree/commaleon/commaleon.rb +1 -1
- data/examples/simple_templating_example.rb +34 -0
- data/examples/tattle_rubygems_version.rb +6 -5
- data/examples/trac_ticket_status.rb +2 -2
- data/lib/ruport.rb +5 -4
- data/lib/ruport/acts_as_reportable.rb +63 -37
- data/lib/ruport/data.rb +1 -0
- data/lib/ruport/data/feeder.rb +111 -0
- data/lib/ruport/data/grouping.rb +84 -22
- data/lib/ruport/data/record.rb +1 -1
- data/lib/ruport/data/table.rb +127 -87
- data/lib/ruport/formatter.rb +22 -9
- data/lib/ruport/formatter/csv.rb +27 -3
- data/lib/ruport/formatter/html.rb +26 -6
- data/lib/ruport/formatter/pdf.rb +169 -36
- data/lib/ruport/formatter/template.rb +167 -0
- data/lib/ruport/formatter/text.rb +47 -15
- data/lib/ruport/query.rb +1 -1
- data/lib/ruport/renderer.rb +46 -56
- data/test/acts_as_reportable_test.rb +20 -20
- data/test/csv_formatter_test.rb +26 -1
- data/test/data_feeder_test.rb +88 -0
- data/test/grouping_test.rb +90 -4
- data/test/html_formatter_test.rb +25 -2
- data/test/pdf_formatter_test.rb +69 -3
- data/test/query_test.rb +3 -2
- data/test/record_test.rb +2 -1
- data/test/renderer_test.rb +49 -3
- data/test/sql_split_test.rb +4 -2
- data/test/table_test.rb +159 -65
- data/test/template_test.rb +37 -0
- data/test/text_formatter_test.rb +33 -1
- metadata +9 -4
- data/lib/ruport/renderer.rb.orig +0 -542
- data/test/renderer_test.rb.orig +0 -512
data/AUTHORS
CHANGED
data/README
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
#
|
1
|
+
# -----------------------------------------------------------------
|
2
2
|
# Contents:
|
3
3
|
#
|
4
4
|
# + What Ruport Is
|
@@ -27,6 +27,11 @@
|
|
27
27
|
# functions that will let you build complex reports while maintaining a DRY and
|
28
28
|
# consistent interface.
|
29
29
|
#
|
30
|
+
# To get a quick feel for what you can accomplish with Ruport, take a look at
|
31
|
+
# a few simple examples provided on our web site.
|
32
|
+
#
|
33
|
+
# http://rubyreports.org/examples.html
|
34
|
+
#
|
30
35
|
# Since Ruport's core support is intentionally minimalistic, you may be looking
|
31
36
|
# for some higher level support for specific needs such as graphing, invoices,
|
32
37
|
# report mailing support, etc. For this, you may wish to take a look at the
|
@@ -50,12 +55,13 @@
|
|
50
55
|
# -- formatting
|
51
56
|
#
|
52
57
|
# Ruport relies on PDF::Writer and FasterCSV for its formatting support.
|
53
|
-
# If you want to make use of textile helpers, you'll also need RedCloth
|
58
|
+
# If you want to make use of textile helpers, you'll also need RedCloth.
|
54
59
|
#
|
55
60
|
# -- database interaction
|
56
61
|
#
|
57
62
|
# If you wish to use Ruport to report against a rails project,
|
58
|
-
# a camping project, or do standalone
|
63
|
+
# a camping project, or do standalone acts_as_reportable reports, you'll need
|
64
|
+
# ActiveRecord.
|
59
65
|
#
|
60
66
|
# If you want to use Ruport::Query for raw SQL support, you'll need to
|
61
67
|
# install RubyDBI and whatever database drivers you might need.
|
@@ -72,8 +78,8 @@
|
|
72
78
|
# - The latest stable API documentation is available at:
|
73
79
|
# http://api.rubyreports.org
|
74
80
|
#
|
75
|
-
# - Our Trac is at: http://code.
|
76
|
-
# You may use the username ruport and password blinky to file tickets
|
81
|
+
# - Our Trac is at: http://code.rubyreports.org/ruport
|
82
|
+
# You may use the username ruport and password blinky to file tickets.
|
77
83
|
#
|
78
84
|
# = Hacking
|
79
85
|
#
|
data/Rakefile
CHANGED
@@ -62,7 +62,7 @@ module Commaleon::Helpers
|
|
62
62
|
option :key, :mcsv, :ccsv
|
63
63
|
|
64
64
|
# This setup() idiom has become the default way of doing some
|
65
|
-
# manipulations on the data and
|
65
|
+
# manipulations on the data and options before handing off the
|
66
66
|
# rendering task to the formatters.
|
67
67
|
#
|
68
68
|
# We're using grouping mainly for the renderer support,
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require "ruport"
|
2
|
+
|
3
|
+
Ruport::Formatter::Template.create(:simple) do |t|
|
4
|
+
t.page_format = {
|
5
|
+
:size => "LETTER",
|
6
|
+
:layout => :landscape
|
7
|
+
}
|
8
|
+
t.text_format = {
|
9
|
+
:font_size => 16
|
10
|
+
}
|
11
|
+
t.table_format = {
|
12
|
+
:font_size => 16,
|
13
|
+
:show_headings => false
|
14
|
+
}
|
15
|
+
t.column_format = {
|
16
|
+
:alignment => :center,
|
17
|
+
}
|
18
|
+
t.heading_format = {
|
19
|
+
:alignment => :right
|
20
|
+
}
|
21
|
+
t.grouping_format = {
|
22
|
+
:style => :separated
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
Ruport::Formatter::Template.create(:derived, :base => :simple) do |t|
|
27
|
+
t.table_format[:show_headings] = true
|
28
|
+
end
|
29
|
+
|
30
|
+
t = Table(%w[a b c]) << [1,2,3] << [1,"hello",6] << [2,3,4]
|
31
|
+
g = Grouping(t, :by => "a")
|
32
|
+
|
33
|
+
puts g.to_pdf(:template => :simple)
|
34
|
+
#puts g.to_pdf(:template => :derived)
|
@@ -1,8 +1,10 @@
|
|
1
1
|
# A dump of the database for this example can be found in ./data/tattle.dump
|
2
2
|
|
3
|
+
|
3
4
|
require "active_record"
|
4
5
|
require "ruport"
|
5
6
|
|
7
|
+
|
6
8
|
# Update with your connection parameters
|
7
9
|
ActiveRecord::Base.establish_connection(
|
8
10
|
:adapter => 'mysql',
|
@@ -31,9 +33,8 @@ grouping.each do |name,group|
|
|
31
33
|
end
|
32
34
|
end
|
33
35
|
|
34
|
-
sorted_table = rubygems_versions.sort_rows_by
|
35
|
-
|
36
|
+
sorted_table = rubygems_versions.sort_rows_by("count", :order => :descending)
|
37
|
+
sorted_table.reduce { |r| r["platform"] !~ /darwin/i }
|
38
|
+
g = Grouping(sorted_table, :by => "platform", :order => "name")
|
39
|
+
puts g.to_pdf
|
36
40
|
|
37
|
-
File.open("platforms_gems.html", "w") do |f|
|
38
|
-
f.write g.to_html(:style => :justified)
|
39
|
-
end
|
@@ -41,7 +41,7 @@ class TracSummaryReport
|
|
41
41
|
Grouping(table,:by => :date)
|
42
42
|
end
|
43
43
|
|
44
|
-
def renderable_data
|
44
|
+
def renderable_data(format)
|
45
45
|
summary = feed_data.summary :date,
|
46
46
|
:opened => lambda { |g| g.sigma { |r| r.opened } },
|
47
47
|
:closed => lambda { |g| g.sigma { |r| r.closed } },
|
@@ -54,6 +54,6 @@ end
|
|
54
54
|
|
55
55
|
timeline = "http://stonecode.svnrepository.com/ruport/trac.cgi/timeline"
|
56
56
|
|
57
|
-
report = TracSummaryReport.new(:timeline_uri => timeline, :days =>
|
57
|
+
report = TracSummaryReport.new(:timeline_uri => timeline, :days => 30)
|
58
58
|
puts report.as(:text)
|
59
59
|
|
data/lib/ruport.rb
CHANGED
@@ -12,7 +12,7 @@
|
|
12
12
|
|
13
13
|
module Ruport #:nodoc:#
|
14
14
|
|
15
|
-
VERSION = "1.0
|
15
|
+
VERSION = "1.2.0"
|
16
16
|
|
17
17
|
class FormatterError < RuntimeError #:nodoc:
|
18
18
|
end
|
@@ -79,6 +79,7 @@ module Ruport #:nodoc:#
|
|
79
79
|
end
|
80
80
|
return $? == 0 ? size : [80,24]
|
81
81
|
end
|
82
|
+
|
82
83
|
end
|
83
84
|
|
84
85
|
def terminal_width
|
@@ -89,16 +90,16 @@ module Ruport #:nodoc:#
|
|
89
90
|
|
90
91
|
# quiets warnings for block
|
91
92
|
def quiet #:nodoc:
|
92
|
-
|
93
|
+
warns = $VERBOSE
|
93
94
|
$VERBOSE = nil
|
94
95
|
result = yield
|
95
|
-
$VERBOSE =
|
96
|
+
$VERBOSE = warns
|
96
97
|
return result
|
97
98
|
end
|
98
99
|
|
99
100
|
module_function :quiet
|
100
101
|
|
101
|
-
end
|
102
|
+
end
|
102
103
|
|
103
104
|
require "enumerator"
|
104
105
|
require "ruport/renderer"
|
@@ -93,23 +93,32 @@ module Ruport
|
|
93
93
|
#
|
94
94
|
# Additional options include:
|
95
95
|
#
|
96
|
-
# <b><tt>:only</tt></b>::
|
96
|
+
# <b><tt>:only</tt></b>:: An attribute name or array of attribute
|
97
97
|
# names to include in the results, other
|
98
98
|
# attributes will be excuded.
|
99
|
-
# <b><tt>:except</tt></b>::
|
99
|
+
# <b><tt>:except</tt></b>:: An attribute name or array of attribute
|
100
100
|
# names to exclude from the results.
|
101
|
-
# <b><tt>:methods</tt></b>::
|
101
|
+
# <b><tt>:methods</tt></b>:: A method name or array of method names
|
102
102
|
# whose result(s) will be included in the
|
103
103
|
# table.
|
104
|
-
# <b><tt>:include</tt></b>::
|
104
|
+
# <b><tt>:include</tt></b>:: An associated model or array of associated
|
105
105
|
# models to include in the results.
|
106
|
-
# <b><tt>:
|
106
|
+
# <b><tt>:filters</tt></b>:: A proc or array of procs that set up
|
107
|
+
# conditions to filter the data being added
|
108
|
+
# to the table.
|
109
|
+
# <b><tt>:transforms</tt></b>:: A proc or array of procs that perform
|
110
|
+
# transformations on the data being added
|
111
|
+
# to the table.
|
112
|
+
# <b><tt>:record_class</tt></b>:: Specify the class of the table's
|
107
113
|
# records.
|
114
|
+
# <b><tt>:eager_loading</tt></b>:: Set to false if you don't want to
|
115
|
+
# eager load included associations.
|
108
116
|
#
|
109
|
-
# The
|
110
|
-
#
|
111
|
-
# :include option must be a hash,
|
112
|
-
#
|
117
|
+
# The :only, :except, :methods, and :include options may also be passed
|
118
|
+
# to the :include option in order to specify the output for any
|
119
|
+
# associated models. In this case, the :include option must be a hash,
|
120
|
+
# where the keys are the names of the associations and the values
|
121
|
+
# are hashes of options.
|
113
122
|
#
|
114
123
|
# Any options passed to report_table will disable the options set by
|
115
124
|
# the acts_as_reportable class method.
|
@@ -138,23 +147,21 @@ module Ruport
|
|
138
147
|
# an html version of the table with all columns from books and authors.
|
139
148
|
#
|
140
149
|
# Note: column names for attributes of included models will be qualified
|
141
|
-
# with the
|
142
|
-
# By default, this will not preserve the entire namespace, but you
|
143
|
-
# can get the fully qualified namespace by using the
|
144
|
-
# :preserve_namespace => true option to report_table. So if the
|
145
|
-
# Author model was enclosed in a module called MyModule, you'd
|
146
|
-
# get 'my_module/author.name' as the column name.
|
150
|
+
# with the name of the association.
|
147
151
|
#
|
148
152
|
def report_table(number = :all, options = {})
|
149
153
|
only = options.delete(:only)
|
150
154
|
except = options.delete(:except)
|
151
155
|
methods = options.delete(:methods)
|
152
156
|
includes = options.delete(:include)
|
153
|
-
|
157
|
+
filters = options.delete(:filters)
|
158
|
+
transforms = options.delete(:transforms)
|
154
159
|
record_class = options.delete(:record_class) || Ruport::Data::Record
|
155
160
|
self.aar_columns = []
|
156
161
|
|
157
|
-
options
|
162
|
+
unless options.delete(:eager_loading) == false
|
163
|
+
options[:include] = get_include_for_find(includes)
|
164
|
+
end
|
158
165
|
|
159
166
|
data = [find(number, options)].flatten
|
160
167
|
data = data.map {|r| r.reportable_data(:include => includes,
|
@@ -162,18 +169,25 @@ module Ruport
|
|
162
169
|
:except => except,
|
163
170
|
:methods => methods) }.flatten
|
164
171
|
|
172
|
+
|
165
173
|
table = Ruport::Data::Table.new(:data => data,
|
166
174
|
:column_names => aar_columns,
|
167
|
-
:record_class => record_class
|
168
|
-
|
169
|
-
|
175
|
+
:record_class => record_class,
|
176
|
+
:filters => filters,
|
177
|
+
:transforms => transforms )
|
170
178
|
end
|
171
179
|
|
172
180
|
# Creates a Ruport::Data::Table from an ActiveRecord find_by_sql.
|
173
181
|
#
|
174
182
|
# Additional options include:
|
175
183
|
#
|
176
|
-
# <b><tt>:
|
184
|
+
# <b><tt>:filters</tt></b>:: A proc or array of procs that set up
|
185
|
+
# conditions to filter the data being added
|
186
|
+
# to the table.
|
187
|
+
# <b><tt>:transforms</tt></b>:: A proc or array of procs that perform
|
188
|
+
# transformations on the data being added
|
189
|
+
# to the table.
|
190
|
+
# <b><tt>:record_class</tt></b>:: Specify the class of the table's
|
177
191
|
# records.
|
178
192
|
#
|
179
193
|
# Example:
|
@@ -187,6 +201,8 @@ module Ruport
|
|
187
201
|
#
|
188
202
|
def report_table_by_sql(sql, options = {})
|
189
203
|
record_class = options.delete(:record_class) || Ruport::Data::Record
|
204
|
+
filters = options.delete(:filters)
|
205
|
+
transforms = options.delete(:transforms)
|
190
206
|
self.aar_columns = []
|
191
207
|
|
192
208
|
data = find_by_sql(sql)
|
@@ -194,22 +210,33 @@ module Ruport
|
|
194
210
|
|
195
211
|
table = Ruport::Data::Table.new(:data => data,
|
196
212
|
:column_names => aar_columns,
|
197
|
-
:record_class => record_class
|
213
|
+
:record_class => record_class,
|
214
|
+
:filters => filters,
|
215
|
+
:transforms => transforms)
|
198
216
|
end
|
199
217
|
|
200
218
|
private
|
201
219
|
|
202
220
|
def get_include_for_find(report_option)
|
203
221
|
includes = report_option.blank? ? aar_options[:include] : report_option
|
204
|
-
includes.is_a?(Hash)
|
222
|
+
if includes.is_a?(Hash)
|
223
|
+
result = {}
|
224
|
+
includes.each do |k,v|
|
225
|
+
if v.empty? || !v[:include]
|
226
|
+
result.merge!(k => {})
|
227
|
+
else
|
228
|
+
result.merge!(k => get_include_for_find(v[:include]))
|
229
|
+
end
|
230
|
+
end
|
231
|
+
result
|
232
|
+
elsif includes.is_a?(Array)
|
233
|
+
result = {}
|
234
|
+
includes.each {|i| result.merge!(i => {}) }
|
235
|
+
result
|
236
|
+
else
|
237
|
+
includes
|
238
|
+
end
|
205
239
|
end
|
206
|
-
|
207
|
-
def normalize_column_names(table)
|
208
|
-
renamed = table.column_names.inject({}) do |s,c|
|
209
|
-
s.merge(c => c.sub(/.*\//,""))
|
210
|
-
end
|
211
|
-
table.rename_columns(renamed)
|
212
|
-
end
|
213
240
|
end
|
214
241
|
|
215
242
|
# === Overview
|
@@ -290,10 +317,10 @@ module Ruport
|
|
290
317
|
data_records = []
|
291
318
|
|
292
319
|
if include_has_options
|
293
|
-
assoc_options =
|
294
|
-
|
320
|
+
assoc_options = includes[association].merge({
|
321
|
+
:qualify_attribute_names => association })
|
295
322
|
else
|
296
|
-
assoc_options = { :qualify_attribute_names =>
|
323
|
+
assoc_options = { :qualify_attribute_names => association }
|
297
324
|
end
|
298
325
|
|
299
326
|
association_objects = [send(association)].flatten.compact
|
@@ -327,8 +354,8 @@ module Ruport
|
|
327
354
|
#
|
328
355
|
# Use the :only or :except options to limit the attributes returned.
|
329
356
|
#
|
330
|
-
# Use the :qualify_attribute_names option to append the
|
331
|
-
#
|
357
|
+
# Use the :qualify_attribute_names option to append the association
|
358
|
+
# name to the attribute name as association.attribute
|
332
359
|
#
|
333
360
|
def get_attributes_with_options(options = {})
|
334
361
|
only_or_except =
|
@@ -336,9 +363,8 @@ module Ruport
|
|
336
363
|
{ :only => options[:only], :except => options[:except] }
|
337
364
|
end
|
338
365
|
attrs = attributes(only_or_except)
|
339
|
-
namespace = self.class.to_s.underscore
|
340
366
|
attrs = attrs.inject({}) {|h,(k,v)|
|
341
|
-
h["#{
|
367
|
+
h["#{options[:qualify_attribute_names]}.#{k}"] = v; h
|
342
368
|
} if options[:qualify_attribute_names]
|
343
369
|
attrs
|
344
370
|
end
|
data/lib/ruport/data.rb
CHANGED
@@ -0,0 +1,111 @@
|
|
1
|
+
# Ruport : Extensible Reporting System
|
2
|
+
#
|
3
|
+
# data/feeder.rb provides a data transformation and filtering proxy for ruport
|
4
|
+
#
|
5
|
+
# Copyright August 2007, Gregory Brown / Michael Milner. All Rights Reserved.
|
6
|
+
#
|
7
|
+
# This is free software. Please see the LICENSE and COPYING files for details.
|
8
|
+
|
9
|
+
|
10
|
+
# This class provides a simple way to apply transformations and filters that
|
11
|
+
# get run while you are aggregating data. This is used primarily to build
|
12
|
+
# constrained wrappers to Ruport::Data::Table, but can be used with abstract
|
13
|
+
# data structures as well.
|
14
|
+
#
|
15
|
+
# Table Example:
|
16
|
+
#
|
17
|
+
# t = Table(%w[a b c]) do |feeder|
|
18
|
+
# feeder.filter { |r| r.a < 5 }
|
19
|
+
# feeder.transform { |r| r.b = "B: #{r.b}"}
|
20
|
+
# feeder << [1,2,3]
|
21
|
+
# feeder << [7,1,2]
|
22
|
+
# feeder << { "a" => 3, "b" => 6, "c" => 7 }
|
23
|
+
# end
|
24
|
+
# t.length #=> 2
|
25
|
+
# t.column("b") #=> ["B: 2","B: 6"]
|
26
|
+
#
|
27
|
+
# Filters and transforms are added in a sequential order to a single list of
|
28
|
+
# constraints. You could add some constraints and then append some data, then
|
29
|
+
# add additional constraints, or even build up dynamic constraints if you'd
|
30
|
+
# like.
|
31
|
+
#
|
32
|
+
# Wrapping an arbitrary data object:
|
33
|
+
#
|
34
|
+
# In order to make Data::Feeder work with an object other than Data::Table, it
|
35
|
+
# must implement two things:
|
36
|
+
#
|
37
|
+
# * A method called feed_element that accepts a single argument.
|
38
|
+
# When Feeder#<< is called, the object to be appended is converted by this
|
39
|
+
# method, and then yielded to the filters / transforms.
|
40
|
+
#
|
41
|
+
# * A meaningful #<< method. Feeder#<< simply delegates this to the wrapped
|
42
|
+
# object once the filters and transforms have been applied, so be sure that
|
43
|
+
# the object returned by feed_element is one that can be used by your #<<
|
44
|
+
# method.
|
45
|
+
#
|
46
|
+
# Here is a sample implementation of wrapping a feeder around an Array.
|
47
|
+
#
|
48
|
+
# class Array
|
49
|
+
# def feed_element(element)
|
50
|
+
# element
|
51
|
+
# end
|
52
|
+
# end
|
53
|
+
#
|
54
|
+
# int_array = []
|
55
|
+
# feeder = Ruport::Data::Feeder.new(int_array)
|
56
|
+
# feeder.filter { |r| r.kind_of?(Integer) }
|
57
|
+
#
|
58
|
+
# feeder << 1 << "5" << 4.7 << "kitten" << 4
|
59
|
+
# int_array #=> [1, 4]
|
60
|
+
#
|
61
|
+
class Ruport::Data::Feeder
|
62
|
+
|
63
|
+
# Creates a new Data::Feeder, wrapping the data object provided.
|
64
|
+
def initialize(data)
|
65
|
+
@data = data
|
66
|
+
@constraints = []
|
67
|
+
end
|
68
|
+
|
69
|
+
# Accesses the underlying data object directly
|
70
|
+
attr_reader :data
|
71
|
+
|
72
|
+
# Constrained append operation.
|
73
|
+
#
|
74
|
+
# Before filters and transforms are run, the element to be appended is first
|
75
|
+
# converted by data.feed_element(some_element)
|
76
|
+
#
|
77
|
+
# Filters and transforms are then run sequentially, and if the constraints
|
78
|
+
# are met, it is appended using data << some_element.
|
79
|
+
#
|
80
|
+
def <<(element)
|
81
|
+
feed_element = data.feed_element(element)
|
82
|
+
|
83
|
+
@constraints.each do |type,block|
|
84
|
+
if type == :filter
|
85
|
+
return self unless block[feed_element]
|
86
|
+
else
|
87
|
+
block[feed_element]
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
data << feed_element
|
92
|
+
return self
|
93
|
+
end
|
94
|
+
|
95
|
+
# Creates a filter which must be satisfied for an object to be appended via
|
96
|
+
# the feeder.
|
97
|
+
#
|
98
|
+
# feeder.filter { |r| r.length < 4 }
|
99
|
+
#
|
100
|
+
def filter(&block)
|
101
|
+
@constraints << [:filter,block]
|
102
|
+
end
|
103
|
+
|
104
|
+
# Creates a transformation which may change the object as it is appended.
|
105
|
+
#
|
106
|
+
# feeder.transform { |r| r.a += 10 }
|
107
|
+
def transform(&block)
|
108
|
+
@constraints << [:transform,block]
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|