Ruport 1.2 Release Notes, 2007.08.28

This release provides a number of enhancements and updates to the ruport package, the core library for the Ruby Reports project.

Though we aimed for high backwards compatibility with the 1.0 codebase, there have been some small API changes in places. These notes will show these incompatibilities as well as a sampling of the new features we've added.

Install Instructions:

Ruport and its supporting toolset, ruport-util are packaged as gems, making installation trival:

        gem install ruport -y
        gem install ruport-util -y
      

If you're not already familiar with Ruport, you might want to have a look at some simple examples

List of changes:

API Breakage

Enhancements to existing features

Brand New Features

Notes on backwards incompatible changes:

The following features are not compatible with 1.0, and may cause problems when running older Ruport code on 1.2

- acts_as_reportable now uses real association names

In Ruport 1.0, acts_as_reportable used to do a transformation of model class names to name its associations in report tables. It now uses the proper association name to qualify attribute names. This will break any old code that relied on the earlier convention, but eliminates ambiguity issues.

For example, say you had two models, Customer and CustomerAddress defined as follows:

        class Customer
          has_many :addresses, :class_name => "CustomerAddress"
        end
      
        class CustomerAddress
          belongs_to :customer
        end
      

If you do:

        Customer.report_table(:all, :include => :addresses)
      

Previously, the columns from CustomerAddress would have been qualified as customer_address.attribute_name whereas now, they would be addresses.attribute_name

- Data::Table constructors now yield Data::Feeder objects

You may have been using block form table constructors in your report, which look like this:

        table = Table(%w[month year]) { |t| t << ["June",2006], ["July", 2007] } 
        
        table = Table("foo.csv") { |table,row| table << row }
      

These were mostly used to build up a table structure from some source data via Table#<< within a block local context.

We have now created a Data::Feeder object which allows you to do customized data transformations and filtering. These new features are described later in these notes, but what is important here is that all Table constructors now yield a Data::Feeder object instead of a Table.

A default Data::Feeder#<< will behave like Data::Table#<<. so you may not need to modify your code if this is all it needs.

However, if you need other table methods, you will need to call Data::Feeder#data to access the table object. For example:

        table = Table(%w[month year]) { |feeder|
          feeder << ["June",2007]
          feeder << ["July",2007]
          @months = feeder.data.column("month")       
        }
      

Of course, this change was meant to make life easier, not harder. See the Data::Feeder examples later in these notes for details.

- Renderer::Hooks has changed

In Ruport 1.0, you could do something like this to tie the formatting system to an arbitrary class.

       class MyClass
          include Ruport::Renderer::Hooks
          renders_as_table
          
          def renderable_data
            # return some table
          end
       end
      

We've found that sometimes you'll want to do specialized processing of your data based on the intended rendering format without creating a custom formatter object. To accommodate for this, we've changed the signature of the renderable_data hook to accept a format parameter:

        class MyClass
          include Ruport::Renderer::Hooks
          renders_as_table
          
          def renderable_data(format)
            # return some table
          end
        end
      

The format parameter is simply the format you pass to as() when you are rendering your data. There is of course, no requirement to make use of the format, it is just there for your convenience.

Because rope previously generated the 1.0 compatible code, this is the source of incompatibility between Ruport 1.2 and any ruport-util release previous to 0.8.0. If you are not using rope to generate your report classes, you may not need to upgrade ruport-util, though it is probably a good idea to do so anyway

Examples and discussion of other changes:

Nearly all of the enhancements to Ruport since 1.0 have been driven by real needs in our work. Below are some examples which use the new features in 1.2, in no particular order

- Simplifying transformations and filters with Data::Feeder

We often want to constrain our data as it is being aggregated rather than after we've collected it all. Data::Feeder provides a simple proxy object that allows us to do exactly that.

        t = Table(%w[a b c])
        feeder = Ruport::Data::Feeder.new(t)
        feeder.filter { |r| r.a < 10 }
        feeder << [1,2,3] << [9,6,1] << [11,3,2] << [2,1,7]
        t.column("a") #=> [1, 9, 2]
      

You can create multiple feeders over the same source data, and define multiple filters on a single filter. You can also set up filters to work on an initial data set via the Table constructor:

        t = Table(%w[a b c], :data => [[1,2,3],[9,6,1],[11,3,2],[2,1,7]],
                             :filters => lambda { |r| r.a < 10 })
        t.column("a") #=> [1,9,2]  
      

You can also get back a feeder object from the Table constructor and build up your result set iteratively.

        t = Table(%w[a b c]) do |feeder|
          feeder.filter { |r| r.a < 10 }
          feeder << [1,2,3] << [9,6,1] << [11,3,2] << [2,1,7]
        end
        t.column("a") #=> [1,9,2]  
      

Feeders also provide a way to do transformations on your data:

        t = Table(%w[a b c])
        feeder = Ruport::Data::Feeder.new(t)
        feeder.transform { |r| r.a = "a: #{r.a}" }
        feeder << [1,2,3] << [9,6,1] << [11,3,2] << [2,1,7]
        >> t.column("a")
        => ["a: 1", "a: 9", "a: 11", "a: 2"]
      

Like filters, these can be specified via a :transforms option to the table constructor to tranform an initial data set, or used within the block context to alter the data iteratively.

- Abstract formatting options via Formatter::Template

Templates are an entirely new feature to Ruport 1.2. They allow you to define a reusable set of formatting options. You can create multiple templates with different options and specify which one should be used when output is rendered.

You define a template by using the create method of Ruport::Formatter::Template.

        Ruport::Formatter::Template.create(:simple) do |t|
          t.page_format = {
            :size   => "LETTER",
            :layout => :landscape
          }
        end
      

When creating the template, you can specify whatever options you want, but your formatter needs to know what to do with them or else they will just be ignored. You define an apply_template method in your formatter to tell it how to process the template.

        class Ruport::Formatter::PDF

          def apply_template
            options.paper_size = template.page_format[:size]
            options.paper_orientation = template.page_format[:layout]
          end

        end 
      

To use a template, just specify it using the :template option when you render your output. Note that even if you've defined templates and set up the formatter to use them, they are still optional. If you don't specify a :template option, apply_template simply won't be called.

        t = Table(%w[a b c]) << [1,2,3] << [1,4,6] << [2,3,4] 
        puts t.to_pdf(:template => :simple)  #=> uses the :simple template
        puts t.to_pdf                        #=> doesn't use a template
      

You can also derive a template from another, pre-existing template, using the :base option to Ruport::Formatter::Template.create.

        Ruport::Formatter::Template.create(:derived, :base => :simple)
      

One thing that the templates have allowed us to do is to standardize the interface to the different built-in formatters. Each formatter has an apply_template method predefined that will accept a standard set of options. If, however, you don't like the predefined set up, it's easy to specify your own interface by overriding the existing apply_template methods. This example shows a number of the options being used.

        Ruport::Formatter::Template.create(:simple) do |t|
          t.page_format = {
            :size   => "LETTER",
            :layout => :landscape
          }
          t.text_format = {
            :font_size => 16
          }
          t.table_format = {
            :font_size      => 16,
            :show_headings  => false
          }
          t.column_format = {
            :alignment => :center,
            :heading => { :justification => :right }
          }
          t.grouping_format = {
            :style => :separated
          }
        end
      

- Sorting your Grouping objects

In Ruport 1.0, Grouping objects were unordered. Though isn't so much an issue for working with the objects, order often becomes significant in output. We've provided a few feature enhancements to make this easier.

The most simple case is when you simply want to order your Grouping by the group names.

        t = Table(%w[email id])
        t << ["aaa@aaa.com",1] << ["bbb@bbb.com",4] << ["aaa@aaa.com",3]
        g = Grouping(t, :by => "email", :order => :name) 
        g.to_a.map { |name,group| name } #=> ["aaa.aaa.com", "bbb@bbb.com"]
      

For more complex situations, you can order by an arbitrary block. In this example, we sort the groups by their size:

        group_size = lambda { |g| g.size }
        g = Grouping(t, :by => "email", :order => group_size)
        g.to_a.map { |name,group| name } #=> ["bbb@bbb.com", "aaa@aaa.com"]
      

You can also sort the groupings after they have been created, see the sort_grouping_by and sort_grouping_by! methods for details.

- Table sorting gets smarter

In Ruport 1.0, Table#sort_rows_by(col) would throw an error if any nils were encountered. They are now simply tacked on to the end of a result set by default.

You can also now easily specify the sort order. If you want to reverse the order of sorting, just use something like this:

      table.sort_rows_by(:some_column, :order => :descending)
    

- Grouping#sigma

You can now do sums across Grouping objects in the same manner as you do with Tables:

      table = [[1,2,3],[3,4,5],[5,6,7]].to_table(%w[col1 col2 col3])
      grouping = Grouping(table, :by => "col1")
      grouping.sigma("col2") #=> 12
      grouping.sigma(0)      #=> 12
      grouping.sigma { |r| r.col2 + r.col3 } #=> 27
      grouping.sigma { |r| r.col2 + 1 } #=> 15  
    

You can also use sum() instead of sigma(), they do the same thing.

Easy File Output for Renderers

We got tired of writing File.open(...), so our formatters now know how to write to file:

     Table(%w[a b c], :data => [[1,2,3],[4,5,6]]).to_pdf(:file => "foo.pdf")
   

This doesn't cover absolutely all the changes made in Ruport 1.2, but hits most of the interesting ones.

Project news:

Over the last few months, we've been busy working on a number of community resources.

We've set up github.com/ruport as a portal for Ruport development, linking all of our projects and related projects from our contributors. If you're working on a project that is complimentary to Ruport and want to share some of our resources, let us know.

The real core of development over the last few months however has been The Ruport Book. With a large chunk of its content already available online at ruportbook.com, this work represents the most comprehensive and up to date documentation for Ruport, and will give new users a much easier entry point than those who were with us before 1.0

We are self-publishing this work under a Creative Commons license, with printed copies available some time before the end of the year. If you like Ruport and find it helpful, the best way to thank us is to pick up a copy when it is available for sale, or donate to the documentation effort. We will have more details soon, but expect to be taking pre-orders for the printed book in the near future.

Acknowledgements

In addition to Ruport's core team of contributors, the following folks helped make this release possible:

Brad Ediger,Gregory Gibson, Imobach González Sosa, Stefan Mahlitz, Jeremy McAnally, Dave Nelson, Emmanuel Oga, Jason Roelofs, Greg Weber

Want To Get Involved?

Our community is friendly and helpful, and happy to walk through whatever problems you might be having on our mailing list.

If you're more interested in getting involved in development, the resources at github.com/ruport will get you started

Donations Welcome!

If Ruport is helpful to you, please consider making a donation to the Ruby Reports Documentation Effort, which is responsible for Ruport Book

Click here to lend your support to: Ruby Reports Documentation Effort and make a donation at www.pledgie.com !