ruport 1.4.0 → 1.6.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.
@@ -1,6 +1,6 @@
1
1
  # Ruport : Extensible Reporting System
2
2
  #
3
- # renderer/grouping.rb : Group data renderer for Ruby Reports
3
+ # controller/grouping.rb : Group data controller for Ruby Reports
4
4
  #
5
5
  # Written by Michael Milner, 2007.
6
6
  # Copyright (C) 2007, All Rights Reserved
@@ -10,7 +10,7 @@
10
10
  #
11
11
  module Ruport
12
12
 
13
- # This class implements the basic renderer for a single group of data.
13
+ # This class implements the basic controller for a single group of data.
14
14
  #
15
15
  # == Supported Formatters
16
16
  #
@@ -29,13 +29,13 @@ module Ruport
29
29
  # * build_group_body
30
30
  # * build_group_footer
31
31
  #
32
- class Renderer::Group < Renderer
32
+ class Controller::Group < Controller
33
33
  options { |o| o.show_table_headers = true }
34
34
 
35
35
  stage :group_header, :group_body, :group_footer
36
36
  end
37
37
 
38
- # This class implements the basic renderer for data groupings in Ruport
38
+ # This class implements the basic controller for data groupings in Ruport
39
39
  # (a collection of Groups).
40
40
  #
41
41
  # == Supported Formatters
@@ -57,7 +57,7 @@ module Ruport
57
57
  # * build_grouping_footer
58
58
  # * finalize_grouping
59
59
  #
60
- class Renderer::Grouping < Renderer
60
+ class Controller::Grouping < Controller
61
61
  options do |o|
62
62
  o.show_group_headers = true
63
63
  o.style = :inline
@@ -1,11 +1,11 @@
1
- # renderer/table.rb : Tabular data renderer for Ruby Reports
1
+ # controller/table.rb : Tabular data controller for Ruby Reports
2
2
  #
3
3
  # Written by Gregory Brown, December 2006. Copyright 2006, All Rights Reserved
4
4
  # This is Free Software, please see LICENSE and COPYING for details.
5
5
 
6
6
  module Ruport
7
7
 
8
- # This class implements the basic renderer for table rows.
8
+ # This class implements the basic controller for table rows.
9
9
  #
10
10
  # == Supported Formatters
11
11
  #
@@ -17,11 +17,11 @@ module Ruport
17
17
  #
18
18
  # * build_row
19
19
  #
20
- class Renderer::Row < Renderer
20
+ class Controller::Row < Controller
21
21
  stage :row
22
22
  end
23
23
 
24
- # This class implements the basic tabular data renderer for Ruport.
24
+ # This class implements the basic tabular data controller for Ruport.
25
25
  #
26
26
  # == Supported Formatters
27
27
  #
@@ -42,7 +42,7 @@ module Ruport
42
42
  # * build_table_footer
43
43
  # * finalize_table
44
44
  #
45
- class Renderer::Table < Renderer
45
+ class Controller::Table < Controller
46
46
  options { |o| o.show_table_headers = true }
47
47
 
48
48
  prepare :table
@@ -44,7 +44,7 @@ module Ruport::Data
44
44
  super
45
45
  end
46
46
 
47
- include Ruport::Renderer::Hooks
47
+ include Ruport::Controller::Hooks
48
48
  renders_as_group
49
49
 
50
50
  def self.inherited(base) #:nodoc:
@@ -340,7 +340,7 @@ module Ruport::Data
340
340
 
341
341
  alias_method :sum, :sigma
342
342
 
343
- include Ruport::Renderer::Hooks
343
+ include Ruport::Controller::Hooks
344
344
  renders_as_grouping
345
345
 
346
346
  def self.inherited(base) #:nodoc:
@@ -18,7 +18,9 @@ module Ruport::Data
18
18
  #
19
19
  class Record
20
20
 
21
- private :id
21
+ if RUBY_VERSION < "1.9"
22
+ private :id
23
+ end
22
24
 
23
25
  include Enumerable
24
26
 
@@ -231,7 +233,7 @@ module Ruport::Data
231
233
  # Internals / Helpers #
232
234
  #######################
233
235
 
234
- include Ruport::Renderer::Hooks
236
+ include Ruport::Controller::Hooks
235
237
  renders_as_row
236
238
 
237
239
  def self.inherited(base) #:nodoc:
@@ -24,6 +24,130 @@ module Ruport::Data
24
24
  #
25
25
  class Table
26
26
 
27
+ class Pivot #:nodoc:
28
+
29
+ def initialize(table, group_col, pivot_col, summary_col, options = {})
30
+ @table = table
31
+ @group_column = group_col
32
+ @pivot_column = pivot_col
33
+ @summary_column = summary_col
34
+ @pivot_order = options[:pivot_order]
35
+ end
36
+
37
+ def convert_row_order_to_group_order(row_order_spec)
38
+ case row_order_spec
39
+ when Array
40
+ proc {|group|
41
+ row_order_spec.map {|e| group[0][e].to_s }
42
+ }
43
+ when Proc
44
+ proc {|group|
45
+ if row_order_spec.arity == 2
46
+ row_order_spec.call(group[0], group.name)
47
+ else
48
+ row_order_spec.call(group[0])
49
+ end
50
+ }
51
+ when NilClass
52
+ nil
53
+ else
54
+ proc {|group| group[0][row_order_spec].to_s }
55
+ end
56
+ end
57
+
58
+ def columns_from_pivot
59
+ ordering = convert_row_order_to_group_order(@pivot_order)
60
+ pivot_column_grouping = Grouping(@table, :by => @pivot_column)
61
+ pivot_column_grouping.each {|n,g| g.add_column(n) { n }}
62
+ pivot_column_grouping.sort_grouping_by!(ordering) if ordering
63
+ result = []
64
+ pivot_column_grouping.each {|name,_| result << name }
65
+ result
66
+ end
67
+
68
+ def group_column_entries
69
+ @table.map {|row| row[@group_column]}.uniq
70
+ end
71
+
72
+ def to_table
73
+ result = Table()
74
+ result.add_column(@group_column)
75
+ pivoted_columns = columns_from_pivot
76
+ pivoted_columns.each { |name| result.add_column(name) }
77
+ outer_grouping = Grouping(@table, :by => @group_column)
78
+ group_column_entries.each {|outer_group_name|
79
+ outer_group = outer_grouping[outer_group_name]
80
+ pivot_values = pivoted_columns.inject({}) do |hsh, e|
81
+ matching_rows = outer_group.rows_with(@pivot_column => e)
82
+ hsh[e] = matching_rows.first && matching_rows.first[@summary_column]
83
+ hsh
84
+ end
85
+ result << [outer_group_name] + pivoted_columns.map {|e|
86
+ pivot_values[e]
87
+ }
88
+ }
89
+ result
90
+ end
91
+
92
+ end
93
+
94
+ # Creates a new table with values from the specified pivot column
95
+ # transformed into columns.
96
+ #
97
+ # Required options:
98
+ # <b><tt>:group_by</tt></b>:: The name of a column whose unique
99
+ # values should become rows in the new
100
+ # table.
101
+ #
102
+ # <b><tt>:values</tt></b>:: The name of a column that should supply
103
+ # the values for the pivoted columns.
104
+ #
105
+ # Optional:
106
+ # <b><tt>:pivot_order</tt></b>:: An ordering specification for the
107
+ # pivoted columns, in terms of the source
108
+ # rows. If this is a Proc there is an
109
+ # optional second argument that receives
110
+ # the name of the pivot column, which due
111
+ # to implementation oddity currently is
112
+ # removed from the row provided in the
113
+ # first argument. This wart will likely
114
+ # be fixed in a future version.
115
+ #
116
+ # Example:
117
+ #
118
+ # Given a table <em>my_table</em>:
119
+ # +-------------------------+
120
+ # | Group | Segment | Value |
121
+ # +-------------------------+
122
+ # | A | 1 | 0 |
123
+ # | A | 2 | 1 |
124
+ # | B | 1 | 2 |
125
+ # | B | 2 | 3 |
126
+ # +-------------------------+
127
+ #
128
+ # Pivoting the table on the Segment column:
129
+ #
130
+ # my_table.pivot('Segment', :group_by => 'Group', :values => 'Value',
131
+ # :pivot_order => proc {|row, name| name})
132
+ #
133
+ # Yields a new table like this:
134
+ # +---------------+
135
+ # | Group | 1 | 2 |
136
+ # +---------------+
137
+ # | A | 0 | 1 |
138
+ # | B | 2 | 3 |
139
+ # +---------------+
140
+ #
141
+ def pivot(pivot_column, options = {})
142
+ group_column = options[:group_by] ||
143
+ raise(ArgumentError, ":group_by option required")
144
+ value_column = options[:values] ||
145
+ raise(ArgumentError, ":values option required")
146
+ Pivot.new(
147
+ self, group_column, pivot_column, value_column, options
148
+ ).to_table
149
+ end
150
+
27
151
  # === Overview
28
152
  #
29
153
  # This module provides facilities for creating tables from csv data.
@@ -62,8 +186,7 @@ module Ruport::Data
62
186
 
63
187
  options = {:has_names => true,
64
188
  :csv_options => {} }.merge(options)
65
-
66
- # if people want to use FCSV's header support, let them
189
+
67
190
  adjust_options_for_fcsv_headers(options)
68
191
 
69
192
  table = self.new(options) do |feeder|
@@ -111,7 +234,7 @@ module Ruport::Data
111
234
  include Enumerable
112
235
  extend FromCSV
113
236
 
114
- include Ruport::Renderer::Hooks
237
+ include Ruport::Controller::Hooks
115
238
  renders_as_table
116
239
 
117
240
  def self.inherited(base) #:nodoc:
@@ -403,7 +526,8 @@ module Ruport::Data
403
526
  # table.column_names.include?("a") #=> false
404
527
  #
405
528
  def rename_column(old_name,new_name)
406
- self.column_names[column_names.index(old_name)] = new_name
529
+ index = column_names.index(old_name) or return
530
+ self.column_names[index] = new_name
407
531
  each { |r| r.rename_attribute(old_name,new_name,false)}
408
532
  end
409
533
 
@@ -11,7 +11,7 @@ module Ruport
11
11
  # Formatter is the base class for Ruport's format implementations.
12
12
  #
13
13
  # Typically, a Formatter will implement one or more output types,
14
- # and be registered with one or more Renderer classes.
14
+ # and be registered with one or more Controller classes.
15
15
  #
16
16
  # This class provides all the necessary base functionality to make
17
17
  # use of Ruport's rendering system, including option handling, data
@@ -20,35 +20,35 @@ module Ruport
20
20
  # The following example should provide a general idea of how formatters
21
21
  # work, but see the built in formatters for reference implementations.
22
22
  #
23
- # A simple Renderer definition is included to help show the example in
23
+ # A simple Controller definition is included to help show the example in
24
24
  # context, but you can also build your own custom interface to formatter
25
25
  # if you wish.
26
26
  #
27
- # class ReverseRenderer < Ruport::Renderer
27
+ # class ReverseController < Ruport::Controller
28
28
  # stage :reversed_header, :reversed_body
29
29
  # end
30
30
  #
31
31
  # class ReversedText < Ruport::Formatter
32
32
  #
33
- # # Hooks formatter up to renderer
34
- # renders :txt, :for => ReverseRenderer
33
+ # # Hooks formatter up to controller
34
+ # renders :txt, :for => ReverseController
35
35
  #
36
- # # Implements ReverseRenderer's :reversed_header hook
37
- # # but can be used by any renderer
36
+ # # Implements ReverseController's :reversed_header hook
37
+ # # but can be used by any controller
38
38
  # def build_reversed_header
39
39
  # output << "#{options.header_text}\n"
40
40
  # output << "The reversed text will follow\n"
41
41
  # end
42
42
  #
43
- # # Implements ReverseRenderer's :reversed_body hook
44
- # # but can be used by any renderer
43
+ # # Implements ReverseController's :reversed_body hook
44
+ # # but can be used by any controller
45
45
  # def build_reversed_body
46
46
  # output << data.reverse << "\n"
47
47
  # end
48
48
  #
49
49
  # end
50
50
  #
51
- # puts ReverseRenderer.render_txt(:data => "apple",
51
+ # puts ReverseController.render_txt(:data => "apple",
52
52
  # :header_text => "Hello Mike, Hello Joe!")
53
53
  #
54
54
  # -----
@@ -64,48 +64,40 @@ module Ruport
64
64
  # capabilities within your custom formatters
65
65
  #
66
66
  module RenderingTools
67
- # Iterates through <tt>data</tt> and passes
68
- # each row to render_row with the given options.
69
- def render_data_by_row(options={},&block)
70
- data.each do |r|
71
- render_row(r,options,&block)
72
- end
73
- end
74
-
75
- # Uses Renderer::Row to render the Row object with the
67
+ # Uses Controller::Row to render the Row object with the
76
68
  # given options.
77
69
  #
78
70
  # Sets the <tt>:io</tt> attribute by default to the existing
79
71
  # formatter's <tt>output</tt> object.
80
72
  def render_row(row,options={},&block)
81
- render_helper(Renderer::Row,row,options,&block)
73
+ render_helper(Controller::Row,row,options,&block)
82
74
  end
83
75
 
84
- # Uses Renderer::Table to render the Table object with the
76
+ # Uses Controller::Table to render the Table object with the
85
77
  # given options.
86
78
  #
87
79
  # Sets the :io attribute by default to the existing formatter's
88
80
  # output object.
89
81
  def render_table(table,options={},&block)
90
- render_helper(Renderer::Table,table,options,&block)
82
+ render_helper(Controller::Table,table,options,&block)
91
83
  end
92
84
 
93
- # Uses Renderer::Group to render the Group object with the
85
+ # Uses Controller::Group to render the Group object with the
94
86
  # given options.
95
87
  #
96
88
  # Sets the :io attribute by default to the existing formatter's
97
89
  # output object.
98
90
  def render_group(group,options={},&block)
99
- render_helper(Renderer::Group,group,options,&block)
91
+ render_helper(Controller::Group,group,options,&block)
100
92
  end
101
93
 
102
- # Uses Renderer::Grouping to render the Grouping object with the
94
+ # Uses Controller::Grouping to render the Grouping object with the
103
95
  # given options.
104
96
  #
105
97
  # Sets the :io attribute by default to the existing formatter's
106
98
  # output object.
107
99
  def render_grouping(grouping,options={},&block)
108
- render_helper(Renderer::Grouping,grouping,options,&block)
100
+ render_helper(Controller::Grouping,grouping,options,&block)
109
101
  end
110
102
 
111
103
  # Iterates through the data in the grouping and renders each group
@@ -135,21 +127,21 @@ module Ruport
135
127
 
136
128
  include RenderingTools
137
129
 
138
- # Set by the <tt>:data</tt> attribute from Renderer#render
139
- attr_accessor :data
130
+ # Set by the <tt>:data</tt> attribute from Controller#render
131
+ attr_reader :data
140
132
 
141
- # Set automatically by Renderer#render(format) or Renderer#render_format
133
+ # Set automatically by Controller#render(format) or Controller#render_format
142
134
  attr_accessor :format
143
135
 
144
- # Set automatically by Renderer#render as a Renderer::Options object built
136
+ # Set automatically by Controller#render as a Controller::Options object built
145
137
  # by the hash provided.
146
138
  attr_writer :options
147
139
 
148
- # Registers the formatter with one or more Renderers.
140
+ # Registers the formatter with one or more Controllers.
149
141
  #
150
- # renders :pdf, :for => MyRenderer
151
- # render :text, :for => [MyRenderer,YourRenderer]
152
- # renders [:csv,:html], :for => YourRenderer
142
+ # renders :pdf, :for => MyController
143
+ # render :text, :for => [MyController,YourController]
144
+ # renders [:csv,:html], :for => YourController
153
145
  #
154
146
  def self.renders(fmts,options={})
155
147
  Array(fmts).each do |format|
@@ -164,7 +156,7 @@ module Ruport
164
156
  # following syntax:
165
157
  #
166
158
  # class ReversedText < Ruport::Formatter
167
- # renders :txt, :for => ReverseRenderer
159
+ # renders :txt, :for => ReverseController
168
160
  #
169
161
  # build :reversed_header do
170
162
  # output << "#{options.header_text}\n"
@@ -196,11 +188,18 @@ module Ruport
196
188
  @output ||= ""
197
189
  end
198
190
 
199
- # Provides a Renderer::Options object for storing formatting options.
191
+ # Provides a Controller::Options object for storing formatting options.
200
192
  def options
201
- @options ||= Renderer::Options.new
193
+ @options ||= Controller::Options.new
202
194
  end
203
195
 
196
+ # Sets the data object, making a local copy using #dup. This may have
197
+ # a significant overhead for large tables, so formatters which don't
198
+ # modify the data object may wish to override this.
199
+ def data=(val)
200
+ @data = val.dup
201
+ end
202
+
204
203
  # Clears the output.
205
204
  def clear_output
206
205
  @output.replace("")
@@ -14,7 +14,7 @@
14
14
  module Ruport
15
15
 
16
16
  # This formatter implements the CSV format for Ruport's Row, Table, Group
17
- # and Grouping renderers. It is a light wrapper around
17
+ # and Grouping controllers. It is a light wrapper around
18
18
  # James Edward Gray II's FasterCSV.
19
19
  #
20
20
  # === Rendering Options
@@ -23,14 +23,22 @@ module Ruport
23
23
  #
24
24
  # <tt>:format_options</tt> A hash of FasterCSV options
25
25
  #
26
+ # <tt>:formatter</tt> An existing FasterCSV object to write to
27
+ #
26
28
  # <tt>:show_table_headers</tt> True by default
27
29
  #
28
30
  # <tt>:show_group_headers</tt> True by default
29
31
  #
30
32
  class Formatter::CSV < Formatter
31
33
 
32
- renders :csv, :for => [ Renderer::Row, Renderer::Table,
33
- Renderer::Group, Renderer::Grouping ]
34
+ renders :csv, :for => [ Controller::Row, Controller::Table,
35
+ Controller::Group, Controller::Grouping ]
36
+
37
+ def initialize
38
+ require "fastercsv" unless RUBY_VERSION > "1.9"
39
+ end
40
+
41
+ attr_writer :csv_writer
34
42
 
35
43
  # Hook for setting available options using a template. See the template
36
44
  # documentation for the available options and their format.
@@ -41,37 +49,46 @@ module Ruport
41
49
  options.format_options ||= template.format_options
42
50
  end
43
51
 
52
+ # Returns the current FCSV object or creates a new one if it has not
53
+ # been set yet. Note that FCSV(sig) has a cache and returns the *same*
54
+ # FCSV object if writing to the same underlying output with the same
55
+ # options.
56
+ #
57
+ def csv_writer
58
+ @csv_writer ||= options.formatter ||
59
+ FCSV(output, options.format_options || {})
60
+ end
61
+
44
62
  # Generates table header by turning column_names into a CSV row.
45
- # Uses the row renderer to generate the actual formatted output
63
+ # Uses the row controller to generate the actual formatted output
46
64
  #
47
65
  # This method does not do anything if options.show_table_headers is false
48
66
  # or the Data::Table has no column names.
49
67
  def build_table_header
50
68
  unless data.column_names.empty? || !options.show_table_headers
51
- render_row data.column_names, :format_options => options.format_options
69
+ render_row data.column_names, :format_options => options.format_options,
70
+ :formatter => csv_writer
52
71
  end
53
72
  end
54
73
 
55
- # Calls the row renderer for each row in the Data::Table
74
+ # Calls the row controller for each row in the Data::Table
56
75
  def build_table_body
57
- render_data_by_row { |r|
58
- r.options.format_options = options.format_options
59
- }
76
+ fcsv = csv_writer
77
+ data.each { |row| fcsv << row }
60
78
  end
61
79
 
62
80
  # Produces CSV output for a data row.
63
- def build_row
64
- require "fastercsv"
65
- output << FCSV.generate_line(data,options.format_options || {})
81
+ def build_row(data = self.data)
82
+ csv_writer << data
66
83
  end
67
84
 
68
85
  # Renders the header for a group using the group name.
69
86
  #
70
87
  def build_group_header
71
- output << data.name.to_s << "\n\n"
88
+ csv_writer << [data.name.to_s] << []
72
89
  end
73
90
 
74
- # Renders the group body - uses the table renderer to generate the output.
91
+ # Renders the group body - uses the table controller to generate the output.
75
92
  #
76
93
  def build_group_body
77
94
  render_table data, options.to_hash
@@ -82,7 +99,7 @@ module Ruport
82
99
  #
83
100
  def build_grouping_header
84
101
  unless options.style == :inline
85
- output << "#{data.grouped_by}," << grouping_columns
102
+ csv_writer << [data.grouped_by] + grouping_columns
86
103
  end
87
104
  end
88
105
 
@@ -101,17 +118,17 @@ module Ruport
101
118
  private
102
119
 
103
120
  def grouping_columns
104
- require "fastercsv"
105
- data.data.to_a[0][1].column_names.to_csv
121
+ data.data.to_a[0][1].column_names
106
122
  end
107
123
 
108
124
  def render_justified_or_raw_grouping
109
125
  data.each do |_,group|
110
- output << "#{group.name}" if options.style == :justified
126
+ prefix = [group.name.to_s]
111
127
  group.each do |row|
112
- output << "#{group.name if options.style == :raw}," << row.to_csv
128
+ csv_writer << prefix + row.to_a
129
+ prefix = [nil] if options.style == :justified
113
130
  end
114
- output << "\n"
131
+ csv_writer << []
115
132
  end
116
133
  end
117
134