ruport 1.4.0 → 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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