jsanders-ruport 1.7.1

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.
Files changed (76) hide show
  1. data/AUTHORS +48 -0
  2. data/LICENSE +59 -0
  3. data/README +114 -0
  4. data/Rakefile +93 -0
  5. data/examples/RWEmerson.jpg +0 -0
  6. data/examples/anon.rb +43 -0
  7. data/examples/btree/commaleon/commaleon.rb +263 -0
  8. data/examples/btree/commaleon/sample_data/ticket_count.csv +124 -0
  9. data/examples/btree/commaleon/sample_data/ticket_count2.csv +119 -0
  10. data/examples/centered_pdf_text_box.rb +83 -0
  11. data/examples/data/tattle.dump +82 -0
  12. data/examples/example.csv +3 -0
  13. data/examples/line_plotter.rb +61 -0
  14. data/examples/pdf_report_with_common_base.rb +72 -0
  15. data/examples/png_embed.rb +54 -0
  16. data/examples/roadmap.png +0 -0
  17. data/examples/row_renderer.rb +39 -0
  18. data/examples/simple_pdf_lines.rb +25 -0
  19. data/examples/simple_templating_example.rb +34 -0
  20. data/examples/tattle_ruby_version.rb +39 -0
  21. data/examples/tattle_rubygems_version.rb +37 -0
  22. data/examples/trac_ticket_status.rb +59 -0
  23. data/lib/ruport.rb +127 -0
  24. data/lib/ruport/controller.rb +616 -0
  25. data/lib/ruport/controller/grouping.rb +71 -0
  26. data/lib/ruport/controller/table.rb +54 -0
  27. data/lib/ruport/data.rb +4 -0
  28. data/lib/ruport/data/feeder.rb +111 -0
  29. data/lib/ruport/data/grouping.rb +399 -0
  30. data/lib/ruport/data/record.rb +297 -0
  31. data/lib/ruport/data/table.rb +950 -0
  32. data/lib/ruport/extensions.rb +4 -0
  33. data/lib/ruport/formatter.rb +254 -0
  34. data/lib/ruport/formatter/csv.rb +149 -0
  35. data/lib/ruport/formatter/html.rb +161 -0
  36. data/lib/ruport/formatter/pdf.rb +591 -0
  37. data/lib/ruport/formatter/template.rb +187 -0
  38. data/lib/ruport/formatter/text.rb +231 -0
  39. data/lib/uport.rb +1 -0
  40. data/test/controller_test.rb +743 -0
  41. data/test/csv_formatter_test.rb +164 -0
  42. data/test/data_feeder_test.rb +88 -0
  43. data/test/grouping_test.rb +410 -0
  44. data/test/helpers.rb +11 -0
  45. data/test/html_formatter_test.rb +201 -0
  46. data/test/pdf_formatter_test.rb +354 -0
  47. data/test/record_test.rb +332 -0
  48. data/test/samples/addressbook.csv +6 -0
  49. data/test/samples/data.csv +3 -0
  50. data/test/samples/data.tsv +3 -0
  51. data/test/samples/dates.csv +1409 -0
  52. data/test/samples/erb_test.sql +1 -0
  53. data/test/samples/query_test.sql +1 -0
  54. data/test/samples/ruport_test.sql +8 -0
  55. data/test/samples/test.sql +2 -0
  56. data/test/samples/test.yaml +3 -0
  57. data/test/samples/ticket_count.csv +124 -0
  58. data/test/table_pivot_test.rb +134 -0
  59. data/test/table_test.rb +838 -0
  60. data/test/template_test.rb +48 -0
  61. data/test/text_formatter_test.rb +258 -0
  62. data/util/bench/data/record/bench_as_vs_to.rb +18 -0
  63. data/util/bench/data/record/bench_constructor.rb +46 -0
  64. data/util/bench/data/record/bench_indexing.rb +65 -0
  65. data/util/bench/data/record/bench_reorder.rb +35 -0
  66. data/util/bench/data/record/bench_to_a.rb +19 -0
  67. data/util/bench/data/table/bench_column_manip.rb +103 -0
  68. data/util/bench/data/table/bench_dup.rb +24 -0
  69. data/util/bench/data/table/bench_init.rb +67 -0
  70. data/util/bench/data/table/bench_manip.rb +125 -0
  71. data/util/bench/formatter/bench_csv.rb +14 -0
  72. data/util/bench/formatter/bench_html.rb +14 -0
  73. data/util/bench/formatter/bench_pdf.rb +14 -0
  74. data/util/bench/formatter/bench_text.rb +14 -0
  75. data/util/bench/samples/tattle.csv +1237 -0
  76. metadata +176 -0
@@ -0,0 +1,616 @@
1
+ # controller.rb : General purpose control of formatted data for Ruby Reports
2
+ #
3
+ # Copyright December 2006, Gregory Brown. All Rights Reserved.
4
+ #
5
+ # This is free software. Please see the LICENSE and COPYING files for details.
6
+
7
+
8
+ # This class implements the core controller for Ruport's formatting system.
9
+ # It is designed to implement the low level tools necessary to build report
10
+ # controllers for different kinds of tasks. See Controller::Table for a
11
+ # tabular data controller.
12
+ #
13
+ class Ruport::Controller
14
+
15
+ class RequiredOptionNotSet < RuntimeError #:nodoc:
16
+ end
17
+ class UnknownFormatError < RuntimeError #:nodoc:
18
+ end
19
+ class StageAlreadyDefinedError < RuntimeError #:nodoc:
20
+ end
21
+ class ControllerNotSetError < RuntimeError #:nodoc:
22
+ end
23
+
24
+ require "ostruct"
25
+
26
+ # Structure for holding controller options.
27
+ # Simplified version of HashWithIndifferentAccess
28
+ class Options < OpenStruct
29
+
30
+ if RUBY_VERSION < "1.9"
31
+ private :id
32
+ end
33
+
34
+ # Returns a Hash object. Use this if you need methods other than []
35
+ def to_hash
36
+ @table
37
+ end
38
+ # Indifferent lookup of an attribute, e.g.
39
+ #
40
+ # options[:foo] == options["foo"]
41
+ def [](key)
42
+ send(key)
43
+ end
44
+
45
+ # Sets an attribute, with indifferent access.
46
+ #
47
+ # options[:foo] = "bar"
48
+ #
49
+ # options[:foo] == options["foo"] #=> true
50
+ # options["foo"] == options.foo #=> true
51
+ # options.foo #=> "bar"
52
+ def []=(key,value)
53
+ send("#{key}=",value)
54
+ end
55
+ end
56
+
57
+ # This module provides hooks into Ruport's formatting system.
58
+ # It is used to implement the as() method for all of Ruport's data
59
+ # structures, as well as the renders_with and renders_as_* helpers.
60
+ #
61
+ # You can actually use this with any data structure, it will look for a
62
+ # renderable_data(format) method to pass to the <tt>controller</tt> you
63
+ # specify, but if that is not defined, it will pass <tt>self</tt>.
64
+ #
65
+ # Examples:
66
+ #
67
+ # # Render Arrays with Ruport's Row Controller
68
+ # class Array
69
+ # include Ruport::Controller::Hooks
70
+ # renders_as_row
71
+ # end
72
+ #
73
+ # # >> [1,2,3].as(:csv)
74
+ # # => "1,2,3\n"
75
+ #
76
+ # # Render Hashes with Ruport's Row Controller
77
+ # class Hash
78
+ # include Ruport::Controller::Hooks
79
+ # renders_as_row
80
+ # attr_accessor :column_order
81
+ # def renderable_data(format)
82
+ # column_order.map { |c| self[c] }
83
+ # end
84
+ # end
85
+ #
86
+ # # >> a = { :a => 1, :b => 2, :c => 3 }
87
+ # # >> a.column_order = [:b,:a,:c]
88
+ # # >> a.as(:csv)
89
+ # # => "2,1,3\n"
90
+ module Hooks
91
+ module ClassMethods
92
+
93
+ # Tells the class which controller as() will forward to.
94
+ #
95
+ # Usage:
96
+ #
97
+ # class MyStructure
98
+ # include Controller::Hooks
99
+ # renders_with CustomController
100
+ # end
101
+ #
102
+ # You can also specify default rendering options, which will be used
103
+ # if they are not overriden by the options passed to as().
104
+ #
105
+ # class MyStructure
106
+ # include Controller::Hooks
107
+ # renders_with CustomController, :font_size => 14
108
+ # end
109
+ def renders_with(controller,opts={})
110
+ @controller = controller
111
+ @rendering_options=opts
112
+ end
113
+
114
+ # The default rendering options for a class, stored as a hash.
115
+ def rendering_options
116
+ @rendering_options
117
+ end
118
+
119
+ # Shortcut for renders_with(Ruport::Controller::Table), you
120
+ # may wish to override this if you build a custom table controller.
121
+ def renders_as_table(options={})
122
+ renders_with Ruport::Controller::Table,options
123
+ end
124
+
125
+ # Shortcut for renders_with(Ruport::Controller::Row), you
126
+ # may wish to override this if you build a custom row controller.
127
+ def renders_as_row(options={})
128
+ renders_with Ruport::Controller::Row, options
129
+ end
130
+
131
+ # Shortcut for renders_with(Ruport::Controller::Group), you
132
+ # may wish to override this if you build a custom group controller.
133
+ def renders_as_group(options={})
134
+ renders_with Ruport::Controller::Group,options
135
+ end
136
+
137
+ # Shortcut for renders_with(Ruport::Controller::Grouping), you
138
+ # may wish to override this if you build a custom grouping controller.
139
+ def renders_as_grouping(options={})
140
+ renders_with Ruport::Controller::Grouping,options
141
+ end
142
+
143
+ # The class of the controller object for the base class.
144
+ #
145
+ # Example:
146
+ #
147
+ # >> Ruport::Data::Table.controller
148
+ # => Ruport::Controller::Table
149
+ def controller
150
+ @controller
151
+ end
152
+ end
153
+
154
+ def self.included(base) #:nodoc:
155
+ base.extend(ClassMethods)
156
+ end
157
+
158
+ # Uses the Controller specified by renders_with to generate formatted
159
+ # output. Passes the return value of the <tt>renderable_data(format)</tt>
160
+ # method if the method is defined, otherwise passes <tt>self</tt> as :data
161
+ #
162
+ # The remaining options are converted to a Controller::Options object and
163
+ # are accessible in both the controller and formatter.
164
+ #
165
+ # Example:
166
+ #
167
+ # table.as(:csv, :show_table_headers => false)
168
+ def as(format,options={})
169
+ raise ControllerNotSetError unless self.class.controller
170
+ unless self.class.controller.formats.include?(format)
171
+ raise UnknownFormatError
172
+ end
173
+ self.class.controller.render(format,
174
+ self.class.rendering_options.merge(options)) do |rend|
175
+ rend.data =
176
+ respond_to?(:renderable_data) ? renderable_data(format) : self
177
+ yield(rend) if block_given?
178
+ end
179
+ end
180
+
181
+ def save_as(file,options={})
182
+ file =~ /.*\.(.*)/
183
+ format = $1
184
+ as(format.to_sym, options.merge(:file => file))
185
+ end
186
+ end
187
+
188
+
189
+
190
+ class << self
191
+
192
+ # Returns a hash that maps format names to their formatter classes, for use
193
+ # with the formatter shortcut. Supported formats are :html, :csv, :pdf, and
194
+ # :text by default.
195
+ #
196
+ #
197
+ # Sample override:
198
+ #
199
+ # class MyController < Ruport::Controller
200
+ #
201
+ # def built_in_formats
202
+ # super.extend(:xml => MyXMLFormatter,
203
+ # :json => MyJSONFormatter)
204
+ # end
205
+ # end
206
+ #
207
+ # This would allow for:
208
+ #
209
+ # class ChildController < MyController
210
+ #
211
+ # formatter :xml do
212
+ # # ...
213
+ # end
214
+ #
215
+ # formatter :json do
216
+ # # ...
217
+ # end
218
+ # end
219
+ #
220
+ def built_in_formats
221
+ { :html => Ruport::Formatter::HTML,
222
+ :csv => Ruport::Formatter::CSV,
223
+ :pdf => Ruport::Formatter::PDF,
224
+ :text => Ruport::Formatter::Text }
225
+ end
226
+
227
+
228
+ # Generates an anonymous formatter class and ties it to the Controller.
229
+ # This method looks up the built in formats in the hash returned by
230
+ # built_in_formats, but also explicitly specify a custom Formatter class to
231
+ # subclass from.
232
+ #
233
+ # Sample usage:
234
+ #
235
+ # class ControllerWithAnonymousFormatters < Ruport::Controller
236
+ #
237
+ # stage :report
238
+ #
239
+ # formatter :html do
240
+ # build :report do
241
+ # output << textile("h1. Hi there")
242
+ # end
243
+ # end
244
+ #
245
+ # formatter :csv do
246
+ # build :report do
247
+ # build_row([1,2,3])
248
+ # end
249
+ # end
250
+ #
251
+ # formatter :pdf do
252
+ # build :report do
253
+ # add_text "hello world"
254
+ # end
255
+ # end
256
+ #
257
+ # formatter :text do
258
+ # build :report do
259
+ # output << "Hello world"
260
+ # end
261
+ # end
262
+ #
263
+ # formatter :custom => CustomFormatter do
264
+ #
265
+ # build :report do
266
+ # output << "This is "
267
+ # custom_helper
268
+ # end
269
+ #
270
+ # end
271
+ #
272
+ # end
273
+ #
274
+ def formatter(*a,&b)
275
+ case a[0]
276
+ when Symbol
277
+ klass = Class.new(built_in_formats[a[0]])
278
+ klass.renders a[0], :for => self
279
+ when Hash
280
+ k,v = a[0].to_a[0]
281
+ klass = Class.new(v)
282
+ klass.renders k, :for => self
283
+ end
284
+ klass.class_eval(&b)
285
+ end
286
+
287
+ attr_accessor :first_stage,:final_stage,:required_options,:stages #:nodoc:
288
+
289
+ # Registers a hook to look for in the Formatter object when the render()
290
+ # method is called.
291
+ #
292
+ # Usage:
293
+ #
294
+ # class MyController < Ruport::Controller
295
+ # # other details omitted...
296
+ # finalize :apple
297
+ # end
298
+ #
299
+ # class MyFormatter < Ruport::Formatter
300
+ # renders :example, :for => MyController
301
+ #
302
+ # # other details omitted...
303
+ #
304
+ # def finalize_apple
305
+ # # this method will be called when MyController tries to render
306
+ # # the :example format
307
+ # end
308
+ # end
309
+ #
310
+ # If a formatter does not implement this hook, it is simply ignored.
311
+ def finalize(stage)
312
+ if final_stage
313
+ raise StageAlreadyDefinedError, 'final stage already defined'
314
+ end
315
+ self.final_stage = stage
316
+ end
317
+
318
+ # Registers a hook to look for in the Formatter object when the render()
319
+ # method is called.
320
+ #
321
+ # Usage:
322
+ #
323
+ # class MyController < Ruport::Controller
324
+ # # other details omitted...
325
+ # prepare :apple
326
+ # end
327
+ #
328
+ # class MyFormatter < Ruport::Formatter
329
+ # renders :example, :for => MyController
330
+ #
331
+ # def prepare_apple
332
+ # # this method will be called when MyController tries to render
333
+ # # the :example format
334
+ # end
335
+ #
336
+ # # other details omitted...
337
+ # end
338
+ #
339
+ # If a formatter does not implement this hook, it is simply ignored.
340
+ def prepare(stage)
341
+ if first_stage
342
+ raise StageAlreadyDefinedError, "prepare stage already defined"
343
+ end
344
+ self.first_stage = stage
345
+ end
346
+
347
+ # Registers hooks to look for in the Formatter object when the render()
348
+ # method is called.
349
+ #
350
+ # Usage:
351
+ #
352
+ # class MyController < Ruport::Controller
353
+ # # other details omitted...
354
+ # stage :apple,:banana
355
+ # end
356
+ #
357
+ # class MyFormatter < Ruport::Formatter
358
+ # renders :example, :for => MyController
359
+ #
360
+ # def build_apple
361
+ # # this method will be called when MyController tries to render
362
+ # # the :example format
363
+ # end
364
+ #
365
+ # def build_banana
366
+ # # this method will be called when MyController tries to render
367
+ # # the :example format
368
+ # end
369
+ #
370
+ # # other details omitted...
371
+ # end
372
+ #
373
+ # If a formatter does not implement these hooks, they are simply ignored.
374
+ def stage(*stage_list)
375
+ self.stages ||= []
376
+ stage_list.each { |stage|
377
+ self.stages << stage.to_s
378
+ }
379
+ end
380
+
381
+ # Defines attribute writers for the Controller::Options object shared
382
+ # between Controller and Formatter. Will throw an error if the user does
383
+ # not provide values for these options upon rendering.
384
+ #
385
+ # usage:
386
+ #
387
+ # class MyController < Ruport::Controller
388
+ # required_option :employee_name, :address
389
+ # # other details omitted
390
+ # end
391
+ def required_option(*opts)
392
+ self.required_options ||= []
393
+ opts.each do |opt|
394
+ self.required_options << opt
395
+
396
+ o = opt
397
+ unless instance_methods(false).include?(o.to_s)
398
+ define_method(o) { options.send(o.to_s) }
399
+ end
400
+ opt = "#{opt}="
401
+ define_method(opt) {|t| options.send(opt, t) }
402
+ end
403
+ end
404
+
405
+ # Lists the formatters that are currently registered on a controller,
406
+ # as a hash keyed by format name.
407
+ #
408
+ # Example:
409
+ #
410
+ # >> Ruport::Controller::Table.formats
411
+ # => {:html=>Ruport::Formatter::HTML,
412
+ # ?> :csv=>Ruport::Formatter::CSV,
413
+ # ?> :text=>Ruport::Formatter::Text,
414
+ # ?> :pdf=>Ruport::Formatter::PDF}
415
+ def formats
416
+ @formats ||= {}
417
+ end
418
+
419
+ # Builds up a controller object, looks up the appropriate formatter,
420
+ # sets the data and options, and then does the following process:
421
+ #
422
+ # * If the controller contains a module Helpers, mix it in to the instance.
423
+ # * If a block is given, yield the Controller instance.
424
+ # * If a setup() method is defined on the Controller, call it.
425
+ # * Call the run() method.
426
+ # * If the :file option is set to a file name, appends output to the file.
427
+ # * Return the results of formatter.output
428
+ #
429
+ # Please see the examples/ directory for custom controller examples, because
430
+ # this is not nearly as complicated as it sounds in most cases.
431
+ def render(format, add_options=nil)
432
+ rend = build(format, add_options) { |r|
433
+ yield(r) if block_given?
434
+ r.setup if r.respond_to? :setup
435
+ }
436
+ rend.run
437
+ rend.formatter.save_output(rend.options.file) if rend.options.file
438
+ return rend.formatter.output
439
+ end
440
+
441
+ # Allows you to set class-wide default options.
442
+ #
443
+ # Example:
444
+ #
445
+ # options { |o| o.style = :justified }
446
+ #
447
+ def options
448
+ @options ||= Ruport::Controller::Options.new
449
+ yield(@options) if block_given?
450
+
451
+ return @options
452
+ end
453
+
454
+ private
455
+
456
+ # Creates a new instance of the controller and sets it to use the specified
457
+ # formatter (by name). If a block is given, the controller instance is
458
+ # yielded.
459
+ #
460
+ # Returns the controller instance.
461
+ #
462
+ def build(format, add_options=nil)
463
+ rend = self.new
464
+
465
+ rend.send(:use_formatter, format)
466
+ rend.send(:options=, options.dup)
467
+ if rend.class.const_defined? :Helpers
468
+ rend.formatter.extend(rend.class.const_get(:Helpers))
469
+ end
470
+ if add_options.kind_of?(Hash)
471
+ d = add_options.delete(:data)
472
+ rend.data = d if d
473
+ add_options.each {|k,v| rend.options.send("#{k}=",v) }
474
+ end
475
+
476
+ yield(rend) if block_given?
477
+ return rend
478
+ end
479
+
480
+ # Allows you to register a format with the controller.
481
+ #
482
+ # Example:
483
+ #
484
+ # class MyFormatter < Ruport::Formatter
485
+ # # formatter code ...
486
+ # SomeController.add_format self, :my_formatter
487
+ # end
488
+ #
489
+ def add_format(format,name=nil)
490
+ formats[name] = format
491
+ end
492
+
493
+ end
494
+
495
+ # The name of format being used.
496
+ attr_accessor :format
497
+
498
+ # The formatter object being used.
499
+ attr_writer :formatter
500
+
501
+ # The +data+ that has been passed to the active formatter.
502
+ def data
503
+ formatter.data
504
+ end
505
+
506
+ # Sets +data+ attribute on the active formatter.
507
+ def data=(val)
508
+ formatter.data = val
509
+ end
510
+
511
+ # Controller::Options object which is shared with the current formatter.
512
+ def options
513
+ yield(formatter.options) if block_given?
514
+ formatter.options
515
+ end
516
+
517
+ # Call the _run_ method. You can override this method in your custom
518
+ # controller if you need to define other actions.
519
+ def run
520
+ _run_
521
+ end
522
+
523
+ # If an IO object is given, Formatter#output will use it instead of
524
+ # the default String. For Ruport's core controllers, we technically
525
+ # can use any object that supports the << method, but it's meant
526
+ # for IO objects such as File or STDOUT
527
+ #
528
+ def io=(obj)
529
+ options.io=obj
530
+ end
531
+
532
+ # Returns the active formatter.
533
+ #
534
+ # If a block is given, it is evaluated in the context of the formatter.
535
+ def formatter(&block)
536
+ @formatter.instance_eval(&block) if block
537
+ return @formatter
538
+ end
539
+
540
+ # Provides a shortcut to render() to allow
541
+ # render(:csv) to become render_csv
542
+ #
543
+ def self.method_missing(id,*args,&block)
544
+ id.to_s =~ /^render_(.*)/
545
+ unless args[0].kind_of? Hash
546
+ args = [ (args[1] || {}).merge(:data => args[0]) ]
547
+ end
548
+ $1 ? render($1.to_sym,*args,&block) : super
549
+ end
550
+
551
+ private
552
+
553
+ # Called automatically when the report is rendered. Uses the
554
+ # data collected from the earlier methods.
555
+ def _run_
556
+ unless self.class.required_options.nil?
557
+ self.class.required_options.each do |opt|
558
+ if options.__send__(opt).nil?
559
+ raise RequiredOptionNotSet, "Required option #{opt} not set"
560
+ end
561
+ end
562
+ end
563
+
564
+ if formatter.respond_to?(:apply_template) && options.template != false
565
+ formatter.apply_template if options.template ||
566
+ Ruport::Formatter::Template.default
567
+ end
568
+
569
+ prepare self.class.first_stage if self.class.first_stage
570
+
571
+ if formatter.respond_to?(:layout) && options.layout != false
572
+ formatter.layout do execute_stages end
573
+ else
574
+ execute_stages
575
+ end
576
+
577
+ finalize self.class.final_stage if self.class.final_stage
578
+ maybe :finalize
579
+ end
580
+
581
+ def execute_stages
582
+ unless self.class.stages.nil?
583
+ self.class.stages.each do |stage|
584
+ maybe("build_#{stage}")
585
+ end
586
+ end
587
+ end
588
+
589
+ def prepare(name)
590
+ maybe "prepare_#{name}"
591
+ end
592
+
593
+ def finalize(name)
594
+ maybe "finalize_#{name}"
595
+ end
596
+
597
+ def maybe(something)
598
+ formatter.send something if formatter.respond_to? something
599
+ end
600
+
601
+ def options=(o)
602
+ formatter.options = o
603
+ end
604
+
605
+ # Selects a formatter for use by format name
606
+ def use_formatter(format)
607
+ self.formatter = self.class.formats[format].new
608
+ self.formatter.format = format
609
+ rescue NoMethodError
610
+ raise UnknownFormatError
611
+ end
612
+
613
+ end
614
+
615
+ require "ruport/controller/table"
616
+ require "ruport/controller/grouping"