ruport 0.11.0 → 0.12.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/README CHANGED
@@ -1,10 +1,10 @@
1
1
  # ------------------------------------------------------------------------
2
2
  # Contents:
3
3
  #
4
- # - What Ruport Is
5
- # - Installation
6
- # - Resources
7
- # - Hacking
4
+ # + What Ruport Is
5
+ # + Installation
6
+ # + Resources
7
+ # + Hacking
8
8
  #
9
9
  # = What Ruport Is
10
10
  #
data/Rakefile CHANGED
@@ -3,7 +3,7 @@ require "rake/testtask"
3
3
  require "rake/gempackagetask"
4
4
 
5
5
 
6
- RUPORT_VERSION = "0.11.0"
6
+ RUPORT_VERSION = "0.12.0"
7
7
 
8
8
  begin
9
9
  require "rubygems"
data/lib/ruport.rb CHANGED
@@ -12,7 +12,7 @@
12
12
 
13
13
  module Ruport #:nodoc:#
14
14
 
15
- VERSION = "0.11.0"
15
+ VERSION = "0.12.0"
16
16
 
17
17
  class FormatterError < RuntimeError #:nodoc:
18
18
  end
@@ -103,6 +103,8 @@ module Ruport
103
103
  # table.
104
104
  # <b><tt>:include</tt></b>:: an associated model or array of associated
105
105
  # models to include in the results.
106
+ # <b><tt>:record_class</tt></b>:: specify the class of the table's
107
+ # records.
106
108
  #
107
109
  # The same set of options may be passed to the :include option in order to
108
110
  # specify the output for any associated models. In this case, the
@@ -149,6 +151,7 @@ module Ruport
149
151
  methods = options.delete(:methods)
150
152
  includes = options.delete(:include)
151
153
  preserve_namespace = options.delete(:preserve_namespace)
154
+ record_class = options.delete(:record_class) || Ruport::Data::Record
152
155
  self.aar_columns = []
153
156
 
154
157
  options[:include] = get_include_for_find(includes)
@@ -159,12 +162,41 @@ module Ruport
159
162
  :except => except,
160
163
  :methods => methods) }.flatten
161
164
 
162
- table =
163
- Ruport::Data::Table.new(:data => data, :column_names => aar_columns)
165
+ table = Ruport::Data::Table.new(:data => data,
166
+ :column_names => aar_columns,
167
+ :record_class => record_class)
164
168
  normalize_column_names(table) unless preserve_namespace
165
169
  table
166
170
  end
167
171
 
172
+ # Creates a Ruport::Data::Table from an ActiveRecord find_by_sql.
173
+ #
174
+ # Additional options include:
175
+ #
176
+ # <b><tt>:record_class</tt></b>:: specify the class of the table's
177
+ # records.
178
+ #
179
+ # Example:
180
+ #
181
+ # class Book < ActiveRecord::Base
182
+ # belongs_to :author
183
+ # acts_as_reportable
184
+ # end
185
+ #
186
+ # Book.report_table_by_sql("SELECT * FROM books")
187
+ #
188
+ def report_table_by_sql(sql, options = {})
189
+ record_class = options.delete(:record_class) || Ruport::Data::Record
190
+ self.aar_columns = []
191
+
192
+ data = find_by_sql(sql)
193
+ data = data.map {|r| r.reportable_data }.flatten
194
+
195
+ table = Ruport::Data::Table.new(:data => data,
196
+ :column_names => aar_columns,
197
+ :record_class => record_class)
198
+ end
199
+
168
200
  private
169
201
 
170
202
  def get_include_for_find(report_option)
@@ -173,10 +173,6 @@ module Ruport::Data
173
173
  # The name of the column used to group the data
174
174
  attr_reader :grouped_by
175
175
 
176
- require "forwardable"
177
- extend Forwardable
178
- def_delegator :@data, :each
179
-
180
176
  # Allows Hash-like indexing of the grouping data.
181
177
  #
182
178
  # Examples:
@@ -186,6 +182,12 @@ module Ruport::Data
186
182
  def [](name)
187
183
  @data[name] or
188
184
  raise(IndexError,"Group Not Found")
185
+ end
186
+
187
+ # Iterates through the Grouping, yielding each group name and Group object
188
+ #
189
+ def each
190
+ @data.each { |name,group| yield(name,group) }
189
191
  end
190
192
 
191
193
  # Used to add extra data to the Grouping. <tt>group</tt> should be a Group.
@@ -39,7 +39,45 @@ module Ruport
39
39
  # Grouping:
40
40
  # * style (:inline,:justified,:separated,:offset)
41
41
  #
42
- class Formatter::PDF < Formatter
42
+ class Formatter::PDF < Formatter
43
+
44
+ ## THESE ARE WHY YOU SHOULD NEVER USE PDF::Writer
45
+
46
+ module PDFWriterMemoryPatch #:nodoc:
47
+ unless self.class.instance_methods.include?("_post_transaction_rewind")
48
+ def _post_transaction_rewind
49
+ @objects.each { |e| e.instance_variable_set(:@parent,self) }
50
+ end
51
+ end
52
+ end
53
+
54
+ module PDFSimpleTableOrderingPatch #:nodoc:
55
+ def __find_table_max_width__(pdf)
56
+ #p "this actually gets called"
57
+ max_width = PDF::Writer::OHash.new(-1)
58
+
59
+ # Find the maximum cell widths based on the data and the headings.
60
+ # Passing through the data multiple times is unavoidable as we must do
61
+ # some analysis first.
62
+ @data.each do |row|
63
+ @cols.each do |name, column|
64
+ w = pdf.text_width(row[name].to_s, @font_size)
65
+ w *= PDF::SimpleTable::WIDTH_FACTOR
66
+
67
+ max_width[name] = w if w > max_width[name]
68
+ end
69
+ end
70
+
71
+ @cols.each do |name, column|
72
+ title = column.heading.title if column.heading
73
+ title ||= column.name
74
+ w = pdf.text_width(title, @heading_font_size)
75
+ w *= PDF::SimpleTable::WIDTH_FACTOR
76
+ max_width[name] = w if w > max_width[name]
77
+ end
78
+ max_width
79
+ end
80
+ end
43
81
 
44
82
  renders :pdf, :for => [ Renderer::Row, Renderer::Table,
45
83
  Renderer::Group, Renderer::Grouping ]
@@ -54,7 +92,7 @@ module Ruport
54
92
  :paper_orientation
55
93
 
56
94
  def initialize
57
- quiet do
95
+ quiet do
58
96
  require "pdf/writer"
59
97
  require "pdf/simpletable"
60
98
  end
@@ -274,15 +312,15 @@ module Ruport
274
312
 
275
313
  format_opts = table_format.merge(format_opts) if table_format
276
314
 
277
- ::PDF::SimpleTable.new do |table|
278
- table.data = table_data
315
+ ::PDF::SimpleTable.new do |table|
316
+ table.extend(PDFSimpleTableOrderingPatch)
279
317
  table.maximum_width = 500
280
- table.column_order = table_data.column_names
281
-
318
+ table.column_order = table_data.column_names
319
+ table.data = table_data
320
+ table.data = [{}] if table.data.empty?
282
321
  apply_pdf_table_column_opts(table,table_data,format_opts)
283
322
 
284
- format_opts.each {|k,v| table.send("#{k}=", v) }
285
-
323
+ format_opts.each {|k,v| table.send("#{k}=", v) }
286
324
  table.render_on(pdf_writer)
287
325
  end
288
326
  end
@@ -296,7 +334,14 @@ module Ruport
296
334
  def horizontal_line(x1,x2)
297
335
  pdf_writer.line(x1,cursor,x2,cursor)
298
336
  pdf_writer.stroke
299
- end
337
+ end
338
+
339
+ # draws a horizontal line from left_boundary to right_boundary
340
+ def horizontal_rule
341
+ horizontal_line(left_boundary,right_boundary)
342
+ end
343
+
344
+ alias_method :hr, :horizontal_rule
300
345
 
301
346
  # draws a vertical line at x from y1 to y2
302
347
  def vertical_line_at(x,y1,y2)
@@ -343,14 +388,6 @@ module Ruport
343
388
  end
344
389
 
345
390
  include DrawingHelpers
346
-
347
- module PDFWriterMemoryPatch #:nodoc:
348
- unless self.class.instance_methods.include?("_post_transaction_rewind")
349
- def _post_transaction_rewind
350
- @objects.each { |e| e.instance_variable_set(:@parent,self) }
351
- end
352
- end
353
- end
354
391
 
355
392
  private
356
393
 
@@ -55,7 +55,8 @@ module Ruport
55
55
  # calculate_max_col_widths
56
56
  #
57
57
  def prepare_table
58
- raise "Can't output empty table" if data.empty?
58
+ raise Ruport::FormatterError, "Can't output table without " +
59
+ "data or column names." if data.empty? && data.column_names.empty?
59
60
  calculate_max_col_widths
60
61
  end
61
62
 
@@ -84,6 +85,7 @@ module Ruport
84
85
  #
85
86
  def build_table_body
86
87
  output << fit_to_width(hr)
88
+ return if data.empty?
87
89
 
88
90
  calculate_max_col_widths unless max_col_width
89
91
 
@@ -154,8 +156,9 @@ module Ruport
154
156
  #
155
157
  # "+------------------+"
156
158
  def hr
157
- len = max_col_width.inject(data[0].to_a.length * 3) {|s,e|s+e}+1
158
- "+" + "-"*(len-2) + "+\n"
159
+ ref = data.column_names.empty? ? data[0].to_a : data.column_names
160
+ len = max_col_width.inject(ref.length * 3) {|s,e|s+e}
161
+ "+" + "-"*(len-1) + "+\n"
159
162
  end
160
163
 
161
164
  # Returns options.table_width if specified.
@@ -1,24 +1,19 @@
1
- require "ruport"
2
- require "test/unit"
3
-
4
- begin
5
- require "rubygems"
6
- rescue LoadError
7
- nil
8
- end
9
-
1
+ require "test/unit"
2
+ require "ruport"
10
3
 
11
4
  begin
12
- require 'mocha'
13
- require 'stubba'
14
- require 'active_record'
5
+ require "rubygems"
6
+ gem "mocha", ">=0.4.0"
7
+ require "mocha"
8
+ require "stubba"
9
+ require "active_record"
15
10
  rescue LoadError
16
11
  nil
17
12
  end
18
13
 
19
- if Object.const_defined?(:ActiveRecord) && Object.const_defined?(:Mocha)
14
+ require "ruport/acts_as_reportable"
20
15
 
21
- require "ruport/acts_as_reportable"
16
+ if Object.const_defined?(:ActiveRecord) && Object.const_defined?(:Mocha)
22
17
 
23
18
  class Team < ActiveRecord::Base
24
19
  acts_as_reportable :except => 'id', :include => :players
@@ -36,20 +31,18 @@ if Object.const_defined?(:ActiveRecord) && Object.const_defined?(:Mocha)
36
31
  end
37
32
 
38
33
  module SomeModule
39
-
40
34
  class PersonalTrainer < ActiveRecord::Base
41
35
  acts_as_reportable
42
36
  has_one :team
43
37
  has_many :players
44
38
  end
45
-
46
39
  end
47
40
 
48
- class TestActsAsReportable < Test::Unit::TestCase
41
+ module ModelStubsSetup
49
42
  Column = ActiveRecord::ConnectionAdapters::Column
50
43
  PersonalTrainer = SomeModule::PersonalTrainer
51
-
52
- def setup
44
+
45
+ def setup
53
46
  setup_column_stubs
54
47
 
55
48
  @trainers = []
@@ -70,11 +63,49 @@ if Object.const_defined?(:ActiveRecord) && Object.const_defined?(:Mocha)
70
63
 
71
64
  setup_find_stubs
72
65
  end
66
+
67
+ private
68
+
69
+ def setup_column_stubs
70
+ PersonalTrainer.stubs(:columns).returns([
71
+ Column.new("id", nil, "integer", false),
72
+ Column.new("name", nil, "string", false)])
73
+ Team.stubs(:columns).returns([Column.new("id", nil, "integer", false),
74
+ Column.new("name", nil, "string", false),
75
+ Column.new("league", nil, "string", true)])
76
+ Player.stubs(:columns).returns([Column.new("id", nil, "integer", false),
77
+ Column.new("team_id", nil, "integer", true),
78
+ Column.new("name", nil, "string", false),
79
+ Column.new("personal_trainer_id", nil, "integer", true)])
80
+ end
81
+
82
+ def setup_find_stubs
83
+ PersonalTrainer.stubs(:find).returns(@trainers)
84
+ @trainers[0].stubs(:players).returns([@players[0]])
85
+ @trainers[1].stubs(:players).returns([@players[1]])
86
+ Team.stubs(:find).returns(@teams)
87
+ @teams[0].stubs(:players).returns(@players)
88
+ @teams[1].stubs(:players).returns([])
89
+ Player.stubs(:find).returns(@players)
90
+ Player.stubs(:find_by_sql).returns(@players)
91
+ @players[0].stubs(:team).returns(@teams[0])
92
+ @players[1].stubs(:team).returns(@teams[0])
93
+ @players[0].stubs(:personal_trainer).returns(@trainers[0])
94
+ @players[1].stubs(:personal_trainer).returns(@trainers[1])
95
+ end
96
+ end
97
+
73
98
 
74
- def test_options_set
99
+ class TestActsAsReportableClassMethods < Test::Unit::TestCase
100
+
101
+ def test_aar_options_set
75
102
  assert_equal({:except => 'id', :include => :players}, Team.aar_options)
76
103
  end
77
-
104
+ end
105
+
106
+ class TestActsAsReportableSingletonMethods < Test::Unit::TestCase
107
+ include ModelStubsSetup
108
+
78
109
  def test_basic_report_table
79
110
  actual = Player.report_table
80
111
  expected = [[1, "Player 1", 1],
@@ -82,26 +113,33 @@ if Object.const_defined?(:ActiveRecord) && Object.const_defined?(:Mocha)
82
113
  assert_equal expected, actual
83
114
  end
84
115
 
85
- def test_only
116
+ def test_report_table_by_sql
117
+ actual = Player.report_table_by_sql("SELECT * FROM players")
118
+ expected = [[1, "Player 1", 1],
119
+ [1, "Player 2", 2]].to_table(%w[team_id name personal_trainer_id])
120
+ assert_equal expected, actual
121
+ end
122
+
123
+ def test_only_option
86
124
  actual = Player.report_table(:all, :only => 'name')
87
125
  expected = [["Player 1"],["Player 2"]].to_table(%w[name])
88
126
  assert_equal expected, actual
89
127
  end
90
128
 
91
- def test_except
129
+ def test_except_option
92
130
  actual = Player.report_table(:all, :except => 'personal_trainer_id')
93
131
  expected = [[1, "Player 1"],[1, "Player 2"]].to_table(%w[team_id name])
94
132
  assert_equal expected, actual
95
133
  end
96
134
 
97
- def test_methods
135
+ def test_methods_option
98
136
  actual = Player.report_table(:all, :only => 'name', :methods => :stats)
99
137
  expected = [["Player 1", "Player 1 stats"],
100
138
  ["Player 2", "Player 2 stats"]].to_table(%w[name stats])
101
139
  assert_equal expected, actual
102
140
  end
103
141
 
104
- def test_include
142
+ def test_include_option
105
143
  actual = Player.report_table(:all, :only => 'name',
106
144
  :include => :personal_trainer)
107
145
  expected = [["Player 1", "Trainer 1"],
@@ -109,7 +147,7 @@ if Object.const_defined?(:ActiveRecord) && Object.const_defined?(:Mocha)
109
147
  assert_equal expected, actual
110
148
  end
111
149
 
112
- def test_include_with_options
150
+ def test_include_has_options
113
151
  actual = Team.report_table(:all, :only => 'name',
114
152
  :include => { :players => { :only => 'name' } })
115
153
  expected = [["Testers", "Player 1"],
@@ -117,6 +155,26 @@ if Object.const_defined?(:ActiveRecord) && Object.const_defined?(:Mocha)
117
155
  ["Others", nil]].to_table(%w[name player.name])
118
156
  assert_equal expected, actual
119
157
  end
158
+
159
+ def test_preserve_namespace_option
160
+ actual = Player.report_table(:all, :only => 'name',
161
+ :include => :personal_trainer, :preserve_namespace => true)
162
+ expected = [["Player 1", "Trainer 1"],
163
+ ["Player 2", "Trainer 2"]].to_table(%w[name
164
+ some_module/personal_trainer.name])
165
+ assert_equal expected, actual
166
+ end
167
+
168
+ class CustomRecord < Ruport::Data::Record; end
169
+
170
+ def test_record_class_option
171
+ actual = Player.report_table(:all, :record_class => CustomRecord)
172
+ actual.each { |r| assert_equal CustomRecord, r.class }
173
+
174
+ actual = Player.report_table_by_sql("SELECT * FROM players",
175
+ :record_class => CustomRecord)
176
+ actual.each { |r| assert_equal CustomRecord, r.class }
177
+ end
120
178
 
121
179
  def test_get_include_for_find
122
180
  assert_equal :players, Team.send(:get_include_for_find, nil)
@@ -124,15 +182,19 @@ if Object.const_defined?(:ActiveRecord) && Object.const_defined?(:Mocha)
124
182
  assert_equal :team, Player.send(:get_include_for_find, :team)
125
183
  assert_equal [:team],
126
184
  Player.send(:get_include_for_find, {:team => {:except => 'id'}})
127
- end
128
-
185
+ end
186
+ end
187
+
188
+ class TestActsAsReportableInstanceMethods < Test::Unit::TestCase
189
+ include ModelStubsSetup
190
+
129
191
  def test_reportable_data
130
192
  actual = @players[0].reportable_data
131
193
  expected = [{ 'team_id' => 1,
132
194
  'name' => "Player 1",
133
195
  'personal_trainer_id' => 1 }]
134
196
  assert_equal expected, actual
135
-
197
+
136
198
  actual = @teams[0].reportable_data(:include =>
137
199
  { :players => { :only => 'name' } })
138
200
  expected = [{ 'name' => "Testers",
@@ -159,69 +221,30 @@ if Object.const_defined?(:ActiveRecord) && Object.const_defined?(:Mocha)
159
221
  assert @teams[0].send(:has_report_options?, { :include => 'name' })
160
222
  assert !@teams[0].send(:has_report_options?, { :foo => 'name' })
161
223
  end
162
-
163
- def test_preserve_namespace_option
164
- actual = Player.report_table(:all, :only => 'name',
165
- :include => :personal_trainer, :preserve_namespace => true)
166
- expected = [["Player 1", "Trainer 1"],
167
- ["Player 2", "Trainer 2"]].to_table(%w[name
168
- some_module/personal_trainer.name])
169
- assert_equal expected, actual
170
- end
171
-
224
+
172
225
  def test_get_attributes_with_options
173
226
  actual = @players[0].send(:get_attributes_with_options)
174
227
  expected = { 'team_id' => 1,
175
228
  'name' => "Player 1",
176
229
  'personal_trainer_id' => 1 }
177
230
  assert_equal expected, actual
178
-
231
+
179
232
  actual = @players[0].send(:get_attributes_with_options,
180
233
  { :only => 'name' })
181
234
  expected = { 'name' => "Player 1" }
182
235
  assert_equal expected, actual
183
-
236
+
184
237
  actual = @players[0].send(:get_attributes_with_options,
185
238
  { :except => 'personal_trainer_id' })
186
239
  expected = { 'team_id' => 1,
187
240
  'name' => "Player 1" }
188
241
  assert_equal expected, actual
189
-
242
+
190
243
  actual = @players[0].send(:get_attributes_with_options,
191
244
  { :only => 'name', :qualify_attribute_names => true })
192
245
  expected = { 'player.name' => "Player 1" }
193
246
  assert_equal expected, actual
194
247
  end
195
-
196
- private
197
-
198
- def setup_column_stubs
199
- PersonalTrainer.stubs(:columns).returns([
200
- Column.new("id", nil, "integer", false),
201
- Column.new("name", nil, "string", false)])
202
- Team.stubs(:columns).returns([Column.new("id", nil, "integer", false),
203
- Column.new("name", nil, "string", false),
204
- Column.new("league", nil, "string", true)])
205
- Player.stubs(:columns).returns([Column.new("id", nil, "integer", false),
206
- Column.new("team_id", nil, "integer", true),
207
- Column.new("name", nil, "string", false),
208
- Column.new("personal_trainer_id", nil, "integer", true)])
209
- end
210
-
211
- def setup_find_stubs
212
- PersonalTrainer.stubs(:find).returns(@trainers)
213
- @trainers[0].stubs(:players).returns([@players[0]])
214
- @trainers[1].stubs(:players).returns([@players[1]])
215
- Team.stubs(:find).returns(@teams)
216
- @teams[0].stubs(:players).returns(@players)
217
- @teams[1].stubs(:players).returns([])
218
- Player.stubs(:find).returns(@players)
219
- @players[0].stubs(:team).returns(@teams[0])
220
- @players[1].stubs(:team).returns(@teams[0])
221
- @players[0].stubs(:personal_trainer).returns(@trainers[0])
222
- @players[1].stubs(:personal_trainer).returns(@trainers[1])
223
- end
224
-
225
248
  end
226
249
 
227
250
  else