ruport 0.11.0 → 0.12.0

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