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.
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
API Breakage
Enhancements to existing features
Brand New Features
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
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.
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.
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 WeberOur 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
If Ruport is helpful to you, please consider making a donation to the Ruby Reports Documentation Effort, which is responsible for Ruport Book