Sunteți pe pagina 1din 164

The Ruport Book

Your guide to mastering Ruby Reports

Gregory Brown

Michael Milner

(and the Ruport community)


The Ruport Book

by Gregory Brown and Michael Milner with the Ruport community.

Print typesetting produced by Dinko Mehinovic.

Copyright 2008,
c Rinara Press LLC. All rights reserved.

Published by Rinara Press, LLC (http://rinarapress.com)

This book is released under the Creative Commons Attribution-Share Alike 3.0 Unported
License. For details, please see:

http://creativecommons.org/licenses/by-sa/3.0/

The latest versions of this book can always be found at: http://ruportbook.com

ii
This book is dedicated to the friends and family members of folks who spend a little too
much time on open source projects. Their tolerance and support should not be taken for
granted, as it is a key part of what makes the free software community so vibrant.
iv
Contents

Foreword xi

Preface xv

I Introducing Ruby Reports 1

1 Tutorial Introduction to Ruport 3


1.1 Getting Everything Running . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.2 The Tattle Project . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.3 Overview by Example . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.4 Ruport’s Data Structures . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
1.5 Manipulating Data . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
1.6 Grouping . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
1.7 Formatting and Rendering . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
1.8 Custom Formatting . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
1.9 Loading acts as reportable . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
1.10 Collecting Data . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
1.11 Tattle Example Report . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17

II Working With Ruport 23

2 Working With Ruport 25


2.1 Introducing PayR: A Simple Payroll System Using Ruport and Rails . . . . 26
2.2 Ruport: Silly Putty for Reporting . . . . . . . . . . . . . . . . . . . . . . . 26

v
2.3 Generate a PDF Before Your Database is Even Initialized . . . . . . . . . . 27
2.4 Using ERB for HTML Reports . . . . . . . . . . . . . . . . . . . . . . . . . 31
2.5 Time to Dig Deeper . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32

3 Report Formatting 33
3.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
3.2 Data Model . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
3.3 Ruport Controllers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
3.4 Data Collection . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
3.5 Setting Options and Data . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
3.6 Using the setup Method . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
3.7 Using the Helpers Module . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
3.8 Ruport Formatters . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
3.9 Using Templates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
3.10 Producing Output . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42

4 More Report Formatting 43


4.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
4.2 A Basic Controller and Formatter . . . . . . . . . . . . . . . . . . . . . . . . 44
4.3 Adding Text . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
4.4 Adding a Table . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
4.5 Adding a Graph . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
4.6 Adding a Border . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
4.7 Rendering the Report . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52

5 Ad-hoc Reporting with rope 55


5.1 Generating and Configuring a rope Application . . . . . . . . . . . . . . . . 55
5.2 Developing a Simple CSV Report . . . . . . . . . . . . . . . . . . . . . . . . 58
5.3 And That’s the End of That Chapter . . . . . . . . . . . . . . . . . . . . . . 64

III Cheatsheets 67

6 Data Manipulations 69
6.1 Sorting Tables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69

vi
6.2 Sorting Groupings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
6.3 Searching Rows in a Table . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
6.3.1 Custom Searches . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
6.4 Sums and Averages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
6.5 Tabular Column Operations and Calculated Fields . . . . . . . . . . . . . . 74
6.6 Filtering and Transforming Data . . . . . . . . . . . . . . . . . . . . . . . . 76
6.7 Summarizing Grouped Data . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
6.8 Multilevel Grouping . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
6.9 Related Resources / Digging Deeper . . . . . . . . . . . . . . . . . . . . . . 80

7 Using acts as reportable 81


7.1 Loading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
7.2 Basic Usage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
7.3 Filtering and Transforming Data . . . . . . . . . . . . . . . . . . . . . . . . 84
7.4 Eager Loading of Data . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
7.5 Setting Default Options . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
7.6 Find by SQL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
7.7 Related Resources / Digging Deeper . . . . . . . . . . . . . . . . . . . . . . 87

8 Using Ruport::Query 89
8.1 Configuration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
8.2 Constructing the Query . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
8.3 Using the Query . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
8.4 Related Resources / Digging Deeper . . . . . . . . . . . . . . . . . . . . . . 92

9 Ruport’s Formatting System 93


9.1 Abstracting the Rendering Process . . . . . . . . . . . . . . . . . . . . . . . 93
9.2 Using Formatters to Encapsulate Low Level code . . . . . . . . . . . . . . . 94
9.2.1 Adding Additional Formatters . . . . . . . . . . . . . . . . . . . . . 95
9.2.2 Syntactic Sugar For Single Use Formatters . . . . . . . . . . . . . . 97
9.3 Custom Formatters for Ruport’s Standard Controllers . . . . . . . . . . . . 99
9.4 Using Templates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101
9.5 Default Templates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102
9.6 Related Resources / Digging Deeper . . . . . . . . . . . . . . . . . . . . . . 104

vii
10 Building Custom Printable Documents 105
10.1 Displaying Multiple Tables in a Single PDF . . . . . . . . . . . . . . . . . . 105
10.2 Custom Headers with Logos . . . . . . . . . . . . . . . . . . . . . . . . . . . 106
10.3 Creating a Standard Report Template . . . . . . . . . . . . . . . . . . . . . 107
10.4 Generating Page Headers . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110
10.5 Making Your PDFs Display Properly in Rails . . . . . . . . . . . . . . . . . 111
10.6 Related Resources / Digging Deeper . . . . . . . . . . . . . . . . . . . . . . 112

11 Adding Logic to Custom Controllers 113


11.1 Using setup() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113
11.2 Using Controller::Hooks . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114
11.3 Using Formatter Helpers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115
11.4 Implicit Helpers for Formatter Selection . . . . . . . . . . . . . . . . . . . . 117
11.5 Related Resources / Digging Deeper . . . . . . . . . . . . . . . . . . . . . . 118

12 Integration Hacks 119


12.1 Squeezing More Out of Our Dependencies . . . . . . . . . . . . . . . . . . . 119
12.1.1 Using Ruport’s Formatters for FasterCSV Tables and Rows . . . . . 119
12.1.2 Quickly Wrapping PDF::Writer code with pdf writer proxy . . . . 121
12.2 Playing Nice with Third-Party Code . . . . . . . . . . . . . . . . . . . . . . 122
12.2.1 Wrapping Business Logic with Custom Record Classes . . . . . . . . 122
12.2.2 Reporting Against Arbitrary Data Structures . . . . . . . . . . . . . 123
12.2.3 Extending or Modifying Ruport with gem plugin . . . . . . . . . . . 124
12.3 Related Resources / Digging Deeper . . . . . . . . . . . . . . . . . . . . . . 125

13 Using Report and ReportManager 127


13.1 Dealing with Controllers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127
13.1.1 Using Custom Controllers . . . . . . . . . . . . . . . . . . . . . . . . 128
13.1.2 Details About the save as() Magic . . . . . . . . . . . . . . . . . . 128
13.2 Using query() for Raw SQL Operations . . . . . . . . . . . . . . . . . . . . 129
13.3 Mailing Reports via Report#send to() . . . . . . . . . . . . . . . . . . . . 129
13.4 Some Quick Notes for Using Report in rope . . . . . . . . . . . . . . . . . . 130
13.5 Managing Many Report Objects . . . . . . . . . . . . . . . . . . . . . . . . 131
13.6 Related Resources / Digging Deeper . . . . . . . . . . . . . . . . . . . . . . 132

viii
14 rope (A Code Generation Tool for Ruby Reports) 133
14.1 Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133
14.1.1 Starting a New rope Project . . . . . . . . . . . . . . . . . . . . . . 133
14.2 Generating a Report Definition . . . . . . . . . . . . . . . . . . . . . . . . . 134
14.3 Project Configuration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 135
14.4 Custom Rendering with rope Generators . . . . . . . . . . . . . . . . . . . 136
14.5 ActiveRecord Integration the Lazy Way . . . . . . . . . . . . . . . . . . . . 137
14.5.1 Setup Details . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137
14.5.2 Generating a Model . . . . . . . . . . . . . . . . . . . . . . . . . . . 138
14.6 Related Resources / Digging Deeper . . . . . . . . . . . . . . . . . . . . . . 138

A Ruport Hacking Guide 139


A.1 Running from Ruport’s Edge . . . . . . . . . . . . . . . . . . . . . . . . . . 139
A.1.1 Release Structure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139
A.2 Preparing A Patch . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 140
A.2.1 Choose the Right Package . . . . . . . . . . . . . . . . . . . . . . . . 140
A.2.2 Be a Good Patcher . . . . . . . . . . . . . . . . . . . . . . . . . . . . 141
A.3 Power Tools for Ruport Hackers . . . . . . . . . . . . . . . . . . . . . . . . . 142
A.3.1 Data Model . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143
A.3.2 Formatting System . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143

Afterword 145

ix
x
Foreword

Since 1998 I have been privileged to have many very young and very bright minds from
the University of New Haven’s Computer Science program work at my consulting
business, BTree Technology. Using various programming languages of the day (VB in
1988, C++ in 2000, C# in 2002 and of course Ruby since 2004), I have set many
programmers on a single simply-stated quest: to write good reports!
Oddly enough, while the focus has been reporting for dental office operations, this
seemingly simple task was enough for me to realize that what I thought should be simple
and quick - namely to write reports or small applications that manipulate data into
specific reports - actually takes a long time to do well and makes for somewhat
frustrating programming. Reporting does not have the glamour of a NASA rover project.
As with all programming, we struggle with a large disconnect between the person asking
for the report and the programmer writing the report. Actually, since the person asking
for the report has almost certainly never programmed, and almost certainly does not
know SQL, they think it is easy to do! This makes the disconnect even worse than when
writing applications, where the business logic people at least expect it to be a significant
process, and not a couple of hours’ work. Even with 15 years of CS background, I
managed to underestimate the complexity of a “simple report request.” If you asked my
programmers how many times I cheerfully wandered into their studio and asked in vain,
“Is it done yet?”, they would tell you that I used that phrase a bit too much.
When writing good reports, the ambiguities in the spec must be ironed out and the data,
its summary, and the various views of the data that comprise the report must be verified.
This involves more than making sure that the report is working bug free; it includes
validating that the programmer understood what the report was for, that the data being
presented is the data that was desired, that it is formatted correctly, that it is
summarized correctly, and that it can be easily manipulated into other views such as
graphs, charts, and other summary forms.
The frameworks available to me in 2004 were all fine choices for reporting but they also
operated more as applications than as languages. That made some seemingly easy tasks
very difficult, and as a result we had to do pre-processing of report data in VB and C#
before it could be displayed nicely by Access, or in a .Net application. This was especially
common when data was coming from two different sources, such as two different
databases, or maybe a database and a text file. Pre-processing could be done in Access or

xi
in Crystal Reports, but the code was often “this-particular-report-specific.” Every time
there was another report to do, even when it was only slightly different, we were back at
the drawing board trying to twist and shim data into a report suitable for use. With
limited time and too many reports to do, many reports ultimately went unfinished. This
made managing report development for a small business much harder than it should be.
Greg was at the University of New Haven and looking for work in 2004 when I was hiring.
We tried to continue in C#, but it was painfully slow. Then Greg wrote some quick
reports in Ruby in three hours and I was impressed. I had never seen a Windows program
do much after only three hours! With Ruby, we were able to benefit from an active open
source community. It seemed to me that since C# development was so frustrating to us
that we should pursue a new course.
Initially, we did not set out to spend two years writing a library to do reporting in Ruby.
We only set out to write a few reports using re-useable Ruby code, but that effort grew
into Ruport. At first Ruport was an application that could be called as a library, but that
design decision was limiting to us, as it wasn’t allowing us to use Ruby to its full
potential. So we dropped the application part of Ruport and it became a library. This
library forms the core of the Ruport gem that you see today.
Among the many wins we experienced along the way, I was excited by all the help we
received from the first coding contest we sponsored, where we gave awards for helping to
document the API and for writing small functions that enhanced the library. The
tremendous support we garnered from the Ruby community (especially from the mailing
list, which I read but never write on) really inspired me to believe that there was more to
this project than simply torturing a CS student with the task of coding reports. It
became very obvious to me very quickly that many programmers wish reporting chores
could be made more simple, and I felt very good about pushing a very smart person like
Greg to provide us with a small, incremental, solution towards that goal. Although it is
technically true that I funded this project by hiring Greg, he has volunteered thousands
of hours of his own time outside of the 10 hours a week he actually worked for me.
Mike Milner was really a godsend to us as he had experience with doing reporting in
Rails, the use of which truly extended Ruport to the next level. Since Mike is paid to do
reporting by the company he works for, his contributions have allowed Ruport to evolve
past what I could have afforded to invest in this project. For Mike too, this has been a
work of love, to whatever extent a reporting library can be loved. He has volunteered
many hours of his time to Ruport and to the book effort, for which I am very grateful.
Though I have more plans for the project, I have been quiet about them as Greg and
Mike added a ton of useful features and tightened integration with other important Ruby
packages, and finally got Ruport to 1.0. They also dedicated most of 2007 to writing this
book. With so much activity, the project seems to have a life of its own.
The process used to create the application you will learn in this book is what I call
“report-centric” development. We started with a bunch of manual payroll reports that
were duplicated in Rails. I have tremendous admiration for Greg Brown and Mike Milner,
and for their dedication to getting this book finished. Among many tasks, they wrote an
application (PayR) twice, first in Camping and then in Rails. This book is different

xii
because the code for a useful and real application is presented. This seems to be more
useful than presenting simplistic examples that leave off just when things start to get
tricky.
Please enjoy the book, join the Ruport mailing list, and make suggestions for improving
Ruport.

Gregory Gibson

xiii
xiv
Preface

What is Ruport Anyway?

How often have you, as a developer, needed to create a business report from some random
data, format it to look good in the process, and complete it within a tight timeline? Did
you have fun doing it? Well, we won’t claim that Ruport can make reporting “fun,” but
it can certainly make it easier.
Ruport is a collection of tools to help developers add reporting capabilities to their
applications. At its core, Ruport provides facilities for collecting data into common
structures, manipulating that data, and then outputting it in a variety of common
formats. As a developer’s toolset, it aims to ease the burden of creating reporting
applications, but it is not itself a reporting application.
Step back a moment and consider what is meant by “reporting” and what is normally
involved in adding reporting functionality to an application. This understanding will help
explain where Ruport fits into the overall development process. You might think of a
report as just formatted output that you can send to a printer, so you might think of a
reporting toolset as a collection of formatting tools. While Ruport includes such tools, it
is more than that.
In order to create a report, you first need some data. This might commonly be in a
database, but it might also be in a CSV formatted file or some other data source
altogether. Consequently, you need some way to get your data into a structure that can
be used by your application. This is the first place where Ruport can help. Ruport has
facilities for querying databases and for extracting data from CSV files. It ties into several
other Ruby libraries, including Ruby DBI, ActiveRecord, and FasterCSV to provide a
choice of data collection mechanism to fit the specific needs of a particular application.
Of course, collecting data is only the first step. What Ruport offers next is a set of
standard data structures that make working with the collected data much easier and
more consistent. Once you populate one of Ruport’s data structures, the manipulation of
that data is the same regardless of the source and method of collection. This allows you
to focus on the task instead of the differences between libraries.
Ruport offers a variety of methods to manipulate the collected data. Just some of the
capabilities it provides are: adding and deleting rows and columns, renaming columns,

xv
sorting, performing calculations, and grouping. It tries to supply you with everything
needed to process your information and get only the desired output when it comes time
to format and render the report.
Finally, Ruport includes a flexible system for formatting/rendering your reports.
Ruport’s controller component allows you to define the stages involved in constructing a
particular report. It allows you to set up expectations for data that should be available
and whether that data is required or optional. The formatting system then allows you to
define how a report should be rendered for a particular output format. All of Ruport’s
data structures have predefined controllers and formatters for a variety of formats.
As you can see, there is much more involved in the process of reporting than just
formatting the output. Ruport contains a comprehensive set of tools to make each step of
the process less painful.

About This Book

This book aims to provide a thorough understanding of all of Ruport’s capabilities. It


assumes a knowledge of the Ruby programming language and certain other libraries and
frameworks, such as Rails and its ORM component ActiveRecord. At a minimum, you
should have a working knowledge of Ruby to understand the code examples.
We have structured the book in a way that we hope provides something for everyone. We
start with a tutorial introduction which uses data from Ruby’s Tattle project to form a
simple but fully functional report. We then dive into PayR, a simple Rails payroll that
uses Ruport extensively, and provides a good example of how we actually work with
Ruport in our day to day jobs. The book is then rounded off with a series of cheatsheets
which will serve both as a reference in your daily work as well as a place to look for more
details as you read through the main content of the book. You’ll find the appropriate
sections cross-referenced as you read through the Tattle and PayR discussions.
This book has primarily been a labor of love. We’ve developed it under a fully
transparent, community oriented process. Not only do our electronic copies lack DRM,
you actually have the rights to do basically whatever you want with your copy. The
licensing terms are available at:
http://creativecommons.org/licenses/by-sa/3.0/

Even still, if you like the book, we ask that you support the project in one way or
another. The easiest way to do this is buy a PDF or printed copy from ruportbook.com.
When you do this, 25% of our revenue goes to a worthwhile charity, Engineers Without
Borders1 , while the rest goes to supporting the folks who have made this book possible.
If you’d prefer the micro-payment approach, you can also offer up donations of any
amount via ruportbook.com that will go directly to the authors. This might be a good
1 http://www.ewb-usa.org/

xvi
choice if you’ve picked up the PDF from a friend or are using just the HTML copies of
the book.
Of course, there is something much more important than financial contributions to us:
you can learn to master Ruport and its supporting packages. With this knowledge, you
can become an active contributor to our community, which is the best way to let us know
the book was worth something to you.
We hope you enjoy the book, and that it makes your reporting life suck less, through
Ruby.

Acknowledgments
Without the help of the Ruport community, this book would quite literally not exist.
As usual, Gregory Gibson of BTree Technology has supported Ruport and this book by
offering us interesting jobs to work on, including PayR. He has also helped us out by
covering some of the book expenses and generally doing things to make our lives easier.
Dinko Mehinovic did the typesetting for the print edition of the book. This amounted to
many hours of work, and the pain of dealing with rapid revisions from the authors, and is
much appreciated.
The following folks have also helped out in ways that run the gamut from keeping us
gainfully employed while still working on Ruport, to proofreading, editing, and reviewing
content:
Ian Bailey, Brad Ediger, Pat Eyler, James Edward Gray II, Stefan Mahlitz, Don Nielsen,
David Pollak, Rajesh Ramachander, Sergio da Silva, and Andrea O.K. Wright
We’d also like to tip our hats to some of the folks who have helped develop Ruport,
through code contributions large and small:
Daniel Berger, Iain Broadfoot, Chris Carter, Simon Claret, Brad Ediger, Michael
Fellinger, Dudley Flanders, James Edward Gray II, James Healy, Wes Hays, Francis
Hwang, Stefan Mahlitz, Dinko Mehinovic, Mathijs Mohlmann, Dave Nelson, Eric Pugh,
Patrice De Saint Steban, and Marshall T. Vandegrift
Finally, thank you to those who pre-ordered the book and provided feedback. The book is
better because of your efforts.

xvii
xviii
Part I

Introducing Ruby Reports

1
Chapter 1

Tutorial Introduction to Ruport

1.1 Getting Everything Running


Some folks like to take baby steps as they enter a new technology, others like to dive right
in and try to get a sense of the big picture. Though we’ll try not to leave anyone behind,
this chapter leans towards the deep dive approach. We’re going to start by building a
real, fully functional report, explaining how things work along the way.
Of course, we’ll need to get Ruport installed and running before we can do anything at
all, so let’s take care of that now.
The easiest way to install Ruport is by using RubyGems.1

gem install ruport

Associated with the Ruby Reports project, of which Ruport is the core component, is an
officially maintained set of tools called ruport-util. These are components that either
don’t fit directly into the functionality provided by the core library or are not mature
enough to be included in the core of Ruport. However, for specific needs, they can be
very useful. The package includes support for database connections using Ruby DBI,
support for the rope code generation tool, a high level report interface, graphing support,
invoicing support, and email support, among others. The ruport-util package can also be
installed via RubyGems.

gem install ruport-util

Another associated project is acts as reportable, which provides Ruport with the ability
to use ActiveRecord for data collection. You can install the acts as reportable gem as
follows:
1 http://rubygems.org

3
gem install acts_as_reportable

Once Ruport is installed, we can start hacking on a project right away to give you a feel
for how things work.

1.2 The Tattle Project


As a first step in introducing you to Ruport, we’re going to construct a simple report for
the Tattle project.2 Tattle is an application that collects statistics about hardware and
software being used by the Ruby community. It allows those involved in maintaining
RubyGems, as well as anyone else, to see collected statistics about the operating
environment in place on users’ computers; things like the RubyGems version, Ruby
version, operating system, CPU, etc.
The data collected by the Tattle project is voluntarily submitted by individual members
of the community. You can submit your own data by first installing the Tattle application.

gem install tattle

Then, to actually submit the information about your operating environment:

tattle

Collected statistics can be found on the project’s website at http://tattle.rubygarden.org.


Here’s an example of what might be submitted when you run the program:

user_key,
prefix, /usr/local
ruby_version, 1.8.5
host_vendor, apple
ruby_install_name, ruby
build, i686-apple-darwin8.8.2
target_cpu, i686
arch, i686-darwin8.8.2
rubygems_version, 0.9.2
SHELL, /bin/sh
host_os, darwin8.8.2
report_time, Tue Jun 19 22:09:54 -0400 2007
host_cpu, i686
LIBRUBY, libruby-static.a
LIBRUBY_SO, libruby.so.1.8.5
target, i686-apple-darwin8.8.2
2 http://rubyforge.org/projects/tattle

4
1.3 Overview by Example

In order to provide a general overview of Ruport’s functionality, we’re going create an


example report from the Tattle data. Ruport is perfect for this kind of task, as it involves
pulling the collected data from a database, grouping it, and then generating output in a
variety of formats, just what we described as Ruport’s strengths.

Included with the Tattle project is a Rails application3 that allows you to display
summarized statistics, optionally narrow down the data by field name, and then export
the data to XML, CSV, or YAML formats. The goal of this chapter will be to incorporate
a simple report into the application using Ruport’s acts as reportable module.

The following is a fully functional report that we’ll show you how to create in this
chapter. Some background is needed to understand the example, so we’ll begin with a
high level tour through the various aspects of Ruport. Then we’ll come back to the
example and explain it in detail.

def generate_report
table = Report.report_table(:all,
:only => %w[host_os rubygems_version user_key],
:conditions => "user_key is not null and user_key <> ’’",
:group => "host_os, rubygems_version, user_key")

grouping = Grouping(table, :by => "host_os")

rubygems_versions = Table(%w[platform rubygems_version count])

grouping.each do |name,group|
Grouping(group, :by => "rubygems_version").each do |vname,group|
rubygems_versions << { "platform" => name,
"rubygems_version" => vname,
"count" => group.length }
end
end

sorted_table = rubygems_versions.sort_rows_by("count", :order => :descending)


g = Grouping(sorted_table, :by => "platform")

send_data g.to_pdf,
:type => "application/pdf",
:disposition => "inline",
:filename => "report.pdf"
end

3 http://tattle.rubyforge.org/svn/rails app

5
1.4 Ruport’s Data Structures
Ruport uses four basic data structures: records, tables, groups, and groupings. These are
all contained in the Ruport::Data module. Thus, the fully qualified class names are
Ruport::Data::Record, Ruport::Data::Table, Ruport::Data::Group, and
Ruport::Data::Grouping. When we use the terms record, table, group, or grouping
without further qualification, we are referring to these data structures.
You may use any or all of these to construct your report, but the central data structure in
Ruport is the table. Records are used to build up a table, while groups and groupings are
used, as the names imply, to group tabular data. We will save the discussion of groups
and groupings until we finish exploring the behavior of tables.
Records are the most basic of the data structures and generally correspond to a row of
data from a database, or other base collection of data. You create them using either an
array or hash of data.

a = Ruport::Data::Record.new ["1.8.5","0.9.2","darwin8.8.2"],
:attributes => ["ruby_version", "rubygems_version", "host_os"]
b = Ruport::Data::Record.new({ :ruby_version => "1.8.5",
:rubygems_version => "0.9.2",
:host_os => "darwin8.8.2" })

In order to access the data, you can use array-like notation a[1], or (provided you supply
attribute names) you can use either hash-like or accessor notation b["ruby version"] or
b.ruby version.
Ruport tables are collections of records and as mentioned previously, they form the basis
for much of Ruport’s functionality. They also provide a variety of methods for working
with the data they contain. In the next sections of the tutorial, we will demonstrate how
to create tables and manipulate their structure and contents.

1.5 Manipulating Data


In the final version of our example, we’ll use Ruport’s acts as reportable module to collect
some data from the Tattle database, which will allow us to avoid using most of the
techniques presented in this section, since we can constrain the data as it is being
collected. However, to demonstrate the capabilities of Ruport in the context of data
manipulation, we won’t do that yet. Instead, we’ll manually create tables containing the
data of interest. The techniques we demonstrate in this section, however, are generally
applicable, regardless of how you collect the data.
First, let’s look at the methods supplied by Ruport’s Table class. We’ll need to create a
table with some of the columns expected for the Tattle data.

table = Table(%w[ruby_version host_vendor build target_cpu arch


rubygems_version host_os host_cpu])

6
Then we’ll populate it with some data.

table << { "ruby_version" => "1.8.5",


"rubygems_version" => "0.9.2",
"host_os" => "darwin8.8.2" }
table << { "ruby_version" => "1.8.6",
"rubygems_version" => "0.9.2",
"host_os" => "darwin8.8.2" }

Now let’s take a look at a text version of our table.

puts table

+----------------------------------------------------------------------------->>
| ruby_version | host_vendor | build | target_cpu | arch | rubygems_version | >>
+----------------------------------------------------------------------------->>
| 1.8.5 | | | | | 0.9.2 | >>
| 1.8.6 | | | | | 0.9.2 | >>
+----------------------------------------------------------------------------->>

We’ll use this table as the basis to explain some data manipulation techniques offered in
Ruport. First, we probably want to get rid of some of the extra columns containing no
data. You may want to start by finding out what columns exist in the table:

table.column_names

produces:

["ruby_version", "host_vendor", "build", "target_cpu", "arch",


"rubygems_version", "host_os", "host_cpu"]

As you can see, these are the column names we specified when creating the table. Now
you want to try and reduce the columns in the table to just the three we populated:
“ruby version”, “rubygems version”, and “host os”. You can delete columns one at a time
with the remove column method.

table.remove_column("arch")
table.remove_column("build")

This would obviously be tedious for this job, since we want to remove several columns.
However, it works well if you only have one or two columns to remove. An improvement
would be to use the remove columns method, which allows you to remove multiple
columns at once.

7
table.remove_columns("arch", "build")

Still, with the number of columns we need to remove, you don’t really want to list out all
the column names. Since, in this case, we’re keeping fewer columns than we’re discarding,
you can use the sub table method and specify only the columns you want to keep. The
reduce method (and its alias sub table!) does the same thing, but modifies its receiver
in-place.

table = table.sub_table(["ruby_version", "rubygems_version", "host_os"])


puts table

produces:

+-----------------------------------------------+
| ruby_version | rubygems_version | host_os |
+-----------------------------------------------+
| 1.8.5 | 0.9.2 | darwin8.8.2 |
| 1.8.6 | 0.9.2 | darwin8.8.2 |
+-----------------------------------------------+

That looks much better. The column names could be nicer, though, so let’s change those.
Ruport offers a few different methods for renaming columns in existing tables. As when
removing columns, when renaming columns Ruport gives you both rename column and
rename columns methods, that allow you to rename one or multiple columns, respectively.

table.rename_column("ruby_version", "Ruby Version")


table.rename_columns("rubygems_version" => "RubyGems Version",
"host_os" => "Host OS")
puts table

produces:

+-----------------------------------------------+
| Ruby Version | RubyGems Version | Host OS |
+-----------------------------------------------+
| 1.8.5 | 0.9.2 | darwin8.8.2 |
| 1.8.6 | 0.9.2 | darwin8.8.2 |
+-----------------------------------------------+

You can also change the order of columns. There are a few methods that can help you to
do that. You can swap the positions of two columns with the swap column method or do
a complete reorder of the column positions using the reorder method. For now, let’s just
swap the first and second columns, so that the RubyGems version is listed first.

table.swap_column("Ruby Version", "RubyGems Version")


puts table

8
produces:

+-----------------------------------------------+
| RubyGems Version | Ruby Version | Host OS |
+-----------------------------------------------+
| 0.9.2 | 1.8.5 | darwin8.8.2 |
| 0.9.2 | 1.8.6 | darwin8.8.2 |
+-----------------------------------------------+

If you have a column of data that you want to add to the table, maybe something that
doesn’t exist in your database, you can do that using Ruport as well. This is generally
useful for doing some type of calculation on the existing data, but it can be used to add
any data you want, or to just create an empty column for later use. The add column,
add columns, and replace column methods can all be helpful for doing this. As an
example, let’s just add a column of default information to the table.

table.add_column("Host Vendor", :default => "apple")


puts table

produces:

+-------------------------------------------------------------+
| RubyGems Version | Ruby Version | Host OS | Host Vendor |
+-------------------------------------------------------------+
| 0.9.2 | 1.8.5 | darwin8.8.2 | apple |
| 0.9.2 | 1.8.6 | darwin8.8.2 | apple |
+-------------------------------------------------------------+

So far, all of our data manipulations have been focused on columns of data, but you can
just as easily work with the individual rows as well. You can add and remove rows of
data based on criteria you specify. Adding data to the table is as simple as using the <<
operator. You can add arrays, hashes, and Ruport records, among others. Let’s add a few
rows of data to the existing table.

table << ["0.9.2", "1.9.0", "darwin8.8.2", "apple"]


table << { "RubyGems Version" => "0.9.0", "Ruby Version" => "1.8.5",
"Host OS" => "darwin8.8.4", "Host Vendor" => "apple" }
table << ["0.9.2", "1.8.5", "linux-gnu", "pc"]
puts table

produces:

9
+-------------------------------------------------------------+
| RubyGems Version | Ruby Version | Host OS | Host Vendor |
+-------------------------------------------------------------+
| 0.9.2 | 1.8.5 | darwin8.8.2 | apple |
| 0.9.2 | 1.8.6 | darwin8.8.2 | apple |
| 0.9.2 | 1.9.0 | darwin8.8.2 | apple |
| 0.9.0 | 1.8.5 | darwin8.8.4 | apple |
| 0.9.2 | 1.8.5 | linux-gnu | pc |
+-------------------------------------------------------------+

One thing we didn’t mention earlier when discussing the use of sub table is that you can
also supply a code block which allows you to limit the number of rows returned. The
block should implement a boolean statement which determines whether a row will be kept
in the result set. For example, you could easily remove the rows for RubyGems version
0.9.0.

table.sub_table! {|row| row["RubyGems Version"] != "0.9.0" }


puts table

produces:

+-------------------------------------------------------------+
| RubyGems Version | Ruby Version | Host OS | Host Vendor |
+-------------------------------------------------------------+
| 0.9.2 | 1.8.5 | darwin8.8.2 | apple |
| 0.9.2 | 1.8.6 | darwin8.8.2 | apple |
| 0.9.2 | 1.9.0 | darwin8.8.2 | apple |
| 0.9.2 | 1.8.5 | linux-gnu | pc |
+-------------------------------------------------------------+

You should now have a general idea of some manipulations you might do on a table using
the methods that Ruport supplies. There are many more that aren’t described here, but
these are some of the more common techniques for data manipulation. In the next section,
we’ll look at how you can group your data using more of Ruport’s data structures.

1.6 Grouping
Another thing Ruport allows you to do with your data is to group it. Ruport supplies two
data structures to assist in such operations, the group and the grouping. A group, on its
own, is marginally useful. It is basically a table with a name, and inherits from the Table
class, so it has all of the capabilities of a Ruport table. It does add a name attribute that
you can use to give the group a name when you create it.

10
Here is an example of creating a group from the table we were using in the previous
section. A table can convert itself to a group with the to group method by providing a
group name, although the name is optional.

group = table.to_group("RubyGems Versions")


puts group

produces:

RubyGems Versions:

+-------------------------------------------------------------+
| RubyGems Version | Ruby Version | Host OS | Host Vendor |
+-------------------------------------------------------------+
| 0.9.2 | 1.8.5 | darwin8.8.2 | apple |
| 0.9.2 | 1.8.6 | darwin8.8.2 | apple |
| 0.9.2 | 1.9.0 | darwin8.8.2 | apple |
| 0.9.2 | 1.8.5 | linux-gnu | pc |
+-------------------------------------------------------------+

As you can see, the output of a group is similar to the output of a table, with the
addition of the group name as a heading.
The real power of groups is seen when we use them as components of another Ruport
data structure, the grouping. A grouping is basically a collection of groups. The data for
a grouping is a hash of groups keyed on the group names. The capabilities of a grouping
go beyond what can be done with a group, however.
You can create a grouping from a table (or group). You can use the constructor or a
shortcut Kernel method (Grouping) to achieve the same thing. The following are
equivalent and will both create a grouping from the table we’ve been using. We’ll use the
“Ruby Version” column again, this time to create a grouping instead of a group.

grouping = Ruport::Data::Grouping.new(table, :by => "Ruby Version")


grouping = Grouping(table, :by => "Ruby Version")
puts grouping

produces:

11
1.8.5:

+----------------------------------------------+
| RubyGems Version | Host OS | Host Vendor |
+----------------------------------------------+
| 0.9.2 | darwin8.8.2 | apple |
| 0.9.2 | linux-gnu | pc |
+----------------------------------------------+

1.8.6:

+----------------------------------------------+
| RubyGems Version | Host OS | Host Vendor |
+----------------------------------------------+
| 0.9.2 | darwin8.8.2 | apple |
+----------------------------------------------+

1.9.0:

+----------------------------------------------+
| RubyGems Version | Host OS | Host Vendor |
+----------------------------------------------+
| 0.9.2 | darwin8.8.2 | apple |
+----------------------------------------------+

A grouping is defined by a collection of groups at the top level, so the default formatter
will output each of the groups in the grouping. You can group on more than one column
by passing an array of column names in the :by option to the constructor.4
The Grouping class offers several methods to work with a grouping, but for now, it’s
enough to recognize that creating a grouping from a table or group will create a hash of
data where each key is a unique data point from the grouping column and each value is a
group consisting of the remaining columns and all rows matching the key value in the
grouping column.
This more-or-less covers the key points about data manipulations in Ruport, so we can
move on now. In the next section, we’ll look at how you can use Ruport’s built-in
controllers and formatters to produce formatted output from existing data.

1.7 Formatting and Rendering


Now that we have some data in a particular structure, the next step is to actually
produce some output. In this section, we describe the basics of creating output from the
standard data structures using Ruport’s built-in formatters. Ruport also includes a
system for defining your own output formats, and we’ll look at that briefly.
4 See the Data Manipulations cheatsheet for more detail on multilevel groupings.

12
Whether you realize it or not, we’ve actually used Ruport’s formatting system already.
That nice-looking text output we’ve been generating with puts has been formatted and
rendered using the built-in text format tools supplied by Ruport. All the data structures
supplied with Ruport have built-in formatters that can create output in a variety of
formats (Text, CSV, HTML, and PDF).
With Ruport’s data structures, it couldn’t be easier to produce output in different
formats. We already used the text format for a Ruport table implicitly when we called
puts table, but you can achieve the same result with the to text method. Similarly, to
produce CSV output, you can call the to csv method (or to html or to pdf for the other
formats). Let’s output our table in CSV and HTML format to see what you get.

puts table.to_csv

produces:

RubyGems Version,Ruby Version,Host OS,Host Vendor


0.9.2,1.8.5,darwin8.8.2,apple
0.9.2,1.8.6,darwin8.8.2,apple
0.9.2,1.9.0,darwin8.8.2,apple
0.9.2,1.8.5,linux-gnu,pc

puts table.to_html

produces:

<table>
<tr>
<th>RubyGems Version</th>
<th>Ruby Version</th>
<th>Host OS</th>
<th>Host Vendor</th>
</tr>
<tr>
<td>0.9.2</td>
<td>1.8.5</td>
<td>darwin8.8.2</td>
<td>apple</td>
</tr>
<tr>
<td>0.9.2</td>
<td>1.8.6</td>
<td>darwin8.8.2</td>
<td>apple</td>
</tr>

13
<tr>
<td>0.9.2</td>
<td>1.9.0</td>
<td>darwin8.8.2</td>
<td>apple</td>
</tr>
<tr>
<td>0.9.2</td>
<td>1.8.5</td>
<td>linux-gnu</td>
<td>pc</td>
</tr>
</table>

When using the standard data structures, that’s all there is to it! If you don’t like the
way they look or if you have specific needs, you can define your own formats. You don’t
have to do so in order to use Ruport, but soon enough you’ll probably encounter a
situation where you need to create a customized formatter.

1.8 Custom Formatting


Ruport’s formatting system consists of two components: the controller and the formatter.
The controller defines the steps needed to build the output and the formatter defines the
implementation of those steps for one or more output formats. The following is an
example of how you can define another formatter for the table we’ve been using. It will
output the table in the same text format we’ve already seen, but add a header. It’s not of
much practical use, but will illustrate how to create a custom format.
First, we define the controller with two stages (:header and :table) that we specify with
the stage class method. This will tell the formatters that if methods named
build header and/or build table are implemented by the formatter, then the controller
will call them during the process of creating the output (although neither is required to
be present).
Ruport defines a convenient syntax for creating your formatter methods. You can use the
class method build with the name of the stage (e.g. build :header) and an associated
block that will become the body of the method. We’ll use this syntax throughout the
book; however you can get the same effect by defining your own “build ” methods (such
as “build header”).
We’ll implement the formatter for a format named :text. The name is essentially
arbitrary but it’s a good idea to use names that provide some identification of the output
being produced.

14
class TextController < Ruport::Controller
stage :header, :table

class Text < Ruport::Formatter


renders :text, :for => TextController

build :header do
output << "Example Report\n\n"
end

build :table do
output << data.to_text
end
end
end

The line in the formatter, renders :text, :for => TextController names the format
and registers itself with the controller as the formatter for that named format. To create
the output, you use the render method. In fact, all of our previous examples were really
shortcuts for the render method.

puts TextController.render(:text, :data => table)

or use the shortcut:

puts TextController.render_text(:data => table)

produces:

Example Report

+-------------------------------------------------------------+
| RubyGems Version | Ruby Version | Host OS | Host Vendor |
+-------------------------------------------------------------+
| 0.9.2 | 1.8.5 | darwin8.8.2 | apple |
| 0.9.2 | 1.8.6 | darwin8.8.2 | apple |
| 0.9.2 | 1.9.0 | darwin8.8.2 | apple |
| 0.9.2 | 1.8.5 | linux-gnu | pc |
+-------------------------------------------------------------+

The built-in formatters contain many useful helper methods and it’s frequently beneficial
to inherit from them rather than directly from Ruport::Formatter. The topic of
controllers and formatters will be covered in much more detail later in the book, but you

15
should now have a general understanding of how they work. As we mentioned, we’ll be
using the acts as reportable module to create the report, so let’s take a look briefly at
how to use it before getting back to the example.

1.9 Loading acts as reportable


Ruport has two built-in mechanisms to get the data needed for your report. First, you
can use the Query class, which relies on the Ruby DBI library. Second, you can hook into
ActiveRecord using Ruport’s acts as reportable module, and this second option is what
we’re going to use in our example. Basically, acts as reportable uses the results of an
ActiveRecord::Base.find() to prepare a Ruport data table.
Since we want to use acts as reportable to integrate Ruport into a Rails application, we
will show you how to install and use it in that context. You can, however, use
acts as reportable in any environment as long as ActiveRecord is loaded. The first thing
you need to do is ensure that both Ruport and acts as reportable are installed as
described above in the Installation section. Then, the easiest way to hook
acts as reportable into a Rails project is to load it in the environment.rb file.

require "ruport"

Loading Ruport will automatically make the acts as reportable module available if
ActiveRecord is already loaded, which will be true for a Rails application. Next, let’s see
how to use acts as reportable to collect some data.

1.10 Collecting Data


The first step in any reporting project is to collect the data needed to generate the
report. The data for the Tattle project is kept in a single database table named
“reports,” and the corresponding ActiveRecord model class is Report. The first thing we
need to do is hook the Report model up to Ruport using acts as reportable.
In the simplest form, you just need to add the acts as reportable method call to your
ActiveRecord model class definition. This provides your model class with a few
Ruport-specific methods to use, the most important being report table, which works
generally like an ActiveRecord::Base.find() with a few extra options, but returns a
Ruport table. That allows you to use all of the facilities of Ruport to manipulate and
format the generated table.

class Report < ActiveRecord::Base


acts_as_reportable
end

16
This gives the Report model the ability to collect its data into a Ruport table. If we
assume that the Tattle reports table contains the same data as we populated manually
earlier, then we can create a Ruport table using the report table method and use the
:only option to get just the columns we want.

table = Report.report_table(:all,
:only => [’ruby_version’, ’rubygems_version’, ’host_os’])
puts table

produces:

+-----------------------------------------------+
| ruby_version | rubygems_version | host_os |
+-----------------------------------------------+
| 1.8.5 | 0.9.2 | darwin8.8.2 |
| 1.8.6 | 0.9.2 | darwin8.8.2 |
+-----------------------------------------------+

There are many other options to constrain the data as you’re collecting it using
acts as reportable, but we’ll leave that for later in the book.5 Once you get your data
into one of Ruport’s data structures, the acquisition method becomes irrelevant.
Everything else you do with that object will be identical, whether you collect the data
using acts as reportable or some other method. This concludes the background
information you need to understand the real report we’re going to build using the Tattle
project data, so now let’s do some real work.

1.11 Tattle Example Report


We’re going to walk through an example of a report generated from the Tattle data. This
report is included in the examples packaged with Ruport (tattle rubygems version.rb
in the examples directory). There is also a dump of the Tattle database in the
examples/data directory. The example shows how to create the report outside of Rails,
but since we want to add it to the Tattle Rails project, we need to change it slightly.
Take a look at the full example again - then we’ll go through it with a detailed
explanation. In a Rails setting, the following method should be placed in a controller, in
this case reports controller.rb.

def generate_report
table = Report.report_table(:all,
:only => %w[host_os rubygems_version user_key],
:conditions => "user_key is not null and user_key <> ’’",
5 See the acts as reportable cheatsheet for more detail.

17
:group => "host_os, rubygems_version, user_key")

grouping = Grouping(table, :by => "host_os")

rubygems_versions = Table(%w[platform rubygems_version count])

grouping.each do |name,group|
Grouping(group, :by => "rubygems_version").each do |vname,group|
rubygems_versions << { "platform" => name,
"rubygems_version" => vname,
"count" => group.length }
end
end

sorted_table = rubygems_versions.sort_rows_by("count", :order => :descending)


g = Grouping(sorted_table, :by => "platform")

send_data g.to_pdf,
:type => "application/pdf",
:disposition => "inline",
:filename => "report.pdf"
end

Let’s look more closely at each part of the code to see how it works. First, look at the line:

table = Report.report_table(:all,
:only => %w[host_os rubygems_version user_key],
:conditions => "user_key is not null and user_key <> ’’",
:group => "host_os, rubygems_version, user_key")

As we saw earlier, the report table method uses acts as reportable to build a Ruport
table from an ActiveRecord find. It can use all of the options available to find to
customize the data set. We use the :conditions and :group options that just get passed
along to the find method, Ruport has no awareness of them. In this case, we look for a
user key that is not null or empty and we group the data on the host os,
rubygems version, and user key columns.
So that leaves the :only option, which is an option that Ruport uses. When passed a
column name or array of column names, it tells Ruport to only return those columns.
When using acts as reportable, this is a more efficient way to limit the columns in a
table, rather than the techniques we showed earlier to eliminate columns after we have
the full table. Either way would work, though.
We now have a Ruport table to work with. The table contains all of the Tattle data that
has a user key, with only the columns host os, rubygems version, and user key. Next, we
want to do some grouping of the data.

18
We create a Ruport grouping from the data table with the next line. This will group all
of the data by the host os column.

grouping = Grouping(table, :by => "host_os")

We want to do some more complicated grouping in this case, but in order to do so, we
need to take another pass through the data. We set up an empty table to hold our final
data - we’ll add rows later as we do the final grouping. We can create an empty table
using the shortcut that Ruport supplies:

rubygems_versions = Table(%w[platform rubygems_version count])

Next, we take another pass through the grouping we created earlier:

grouping.each do |name,group|
Grouping(group, :by => "rubygems_version").each do |vname,group|
rubygems_versions << { "platform" => name,
"rubygems_version" => vname,
"count" => group.length }
end
end

For each group, we create another grouping by the rubygems version column. This will
give us groups of user keys where each group is a single host OS and RubyGems version.
We want to use this information to fill in our empty table. The “platform” column is
populated with the “host os” data from the first grouping. The “rubygems version”
column gets the data from the second grouping. Finally, the “count” column is populated
with the length of the group, which is the number of rows in each group. The final table,
therefore, contains a listing of each unique combination of platform and RubyGems
version with a count of their instances in the database.
Finally, we sort the data and do one last grouping. We want to see a list of platforms
with the RubyGems versions used on that platform sorted in descending order by count.

sorted_table = rubygems_versions.sort_rows_by("count", :order => :descending)


g = Grouping(sorted_table, :by => "platform")

The first line sorts the table by count in descending order using Ruport’s sort rows by
method. This method can take a column name, array of column names, or code block to
define how to do the sort. In this case, we use the column name “count” and tell Ruport
to sort descending using the :order option.
We finally group the sorted table by platform. This gives us the final grouping that we
want to output. We can output the grouping in any of the standard formats we described
earlier, but in this case we want to provide a PDF.

19
send_data g.to_pdf,
:type => "application/pdf",
:disposition => "inline",
:filename => "report.pdf"

We generate the pdf using the to pdf method and then output the file using Rails’
send data method to send binary data to the user.

The output will look something like this:

Overall, this example shows the general steps you’ll use frequently with Ruport: collect
the data, manipulate it in some way (in this case, grouping and sorting), and then render
the data to output. This is what Ruport was designed to do. Of course, many examples
might require more complicated formatting than what we used here and the details of
data collection and manipulation will vary, but the basic flow will be the same.

20
In the sections to come, we’ll look at Ruport in more detail, digging into each of its
components to show you all of their capabilities. Don’t worry if you’re still working on
catching up on the concepts introduced in this chapter, you’ll see them all again as we go
through the main discussion of PayR.

21
22
Part II

Working With Ruport

23
Chapter 2

Working With Ruport

In the introduction, you saw that Ruport provides the raw materials to make quick work
of simple reports. This is great, but there’s probably a Perl hacker or two in the crowd
wondering what the big deal is. For ad-hoc reporting, Ruport’s strength is primarily that
it is more convenient than dealing directly with the underlying libraries it wraps. This
kind of convenience is nice, but wouldn’t be worth writing a book about if it were all we
had to offer
Ruport is really about fitting reporting functionality into wherever it is needed, not just
where it is convenient to do so. Often, this means integration with some overarching
business application. If you’ve experienced the disgusting rat’s nest that is ‘reporting
gone wrong’ first hand, you’ll know exactly what we mean when we say that some
consistency and general organization is a valuable commodity.
Still, we can’t afford to be too opinionated in the realm of reporting, so we don’t try.
Virtually all conventions you’ll find in Ruport are entirely optional, and we think you’ll
find it easy to find another path that works well for you even if it’s not the one that’s
most common.
We’re now going to embark on the discussion which forms the core of the book, and that
is a blow-by-blow commentary of Ruport in action within one of the applications which
helped shape the toolset itself. This system is a simple payroll application, which
throughout its short lifetime has migrated from the console to Camping1 and finally came
to rest as a Rails 2.0 app.
Though we won’t be covering every single step that was involved in creating this
application, we will show all the interesting bits and use them as entry points for further
investigation as to how Ruport is used by its developers. This will hopefully help you
approach many different problems in your own work.
The book is actually based off of a somewhat simplified version of our actual production
system, which has been modified a bit to make it more suitable for teaching Ruport. If
1 http://camping.rubyforge.org

25
you’d like to download it and work with it as you read along, the code is available at:
http://rubyforge.org/projects/payr

2.1 Introducing PayR: A Simple Payroll System


Using Ruport and Rails
PayR is your classic in-house application. It’s a little rough around the edges, the UI is
simple and basic, the feature set is exactly what the users need, and nothing more. If we
were to make up a requirements list for the application, it might look something like this:

• Record daily clock-in / clock-out times for employees

• Record vacation, sick, personal and holiday time

• Produce printable time sheets for each pay period

• Produce a ‘call-in sheet’ time summary report grouped by employee type

• Allow managers to see who is clocked in at a glance

• Display the current week’s time sheet when a user logs in

• Track and report annual usage of vacation and personal times

• Produce auditing reports with access times and IP addresses

Of course, this isn’t a list of every single feature we needed, but it pretty much covers the
fundamentals. Though a payroll system isn’t quite a “reporting application”, it’s easy to
see that a lot of this functionality does involve generating and producing reports.

2.2 Ruport: Silly Putty for Reporting


The great strength of Ruport is its overall flexibility and willingness to fit into pretty
much any place you want to put it. Some of the reports in PayR are absolutely trivial
and you can easily inline them in a controller method.
As a quick example, take a look at our clocked in report code for our manager panel.

def clocked_in_report
@table = RegularTime.report_table(:all,
:conditions => ["end_time is NULL"],
:only => ["employee.name", "start_time"],
:include => { :employee => { :methods => "name", :only => {}} } )

26
unless @table.empty?
@table.rename_column("employee.name", "Employee")
@table.rename_columns { |c| c.titleize }
end
end

The interesting bits of the view look something like this:

<h2>These users are currently clocked in</h2>

<%= @table.empty? ? "None Right Now" : @table.to_html %>

You end up with a simple HTML report that pulls times for employees who haven’t
clocked out yet, as well as their names.

Admittedly, the equivalent code using nothing but Rails wouldn’t be much more
complicated. However, it’s worth asking what happens when you want to also support
other formats, when you add new methods to an association, or change the associated
model entirely. Ruport allows you to easily accommodate changes as needed without
complicating your reporting process. Of course, this really starts to shine through as
things get more complex.

2.3 Generate a PDF Before Your Database is Even


Initialized
Using acts as reportable to handle your tabular data opens up a lot of doors and really
makes things easy to extend and change down the line as needed. This is principally
because it standardizes your data so that Ruport can be blissfully ignorant of where it
actually came from.
To get a sense of this, we can take a quick look at some of PayR’s timesheet generation
code, specifically the PDF formatter:

class PDF < Ruport::Formatter::PDF

renders :pdf, :for => TimeSheet

27
build :time_sheet do
pad_top(50) {
add_text "Timesheet for #{data.employee} (#{start_date} - #{end_date})"
}

pad(20) do
draw_table data.week1_data
show_overtime(data.week1_overtime)
end

draw_table data.week2_data

show_overtime(data.week2_overtime)

render_pdf
end

def show_overtime(time)
if time
pad(5) { add_text "Overtime for week: #{time}", :font_size => 10 }
end
end

end

In this case, we’re working with a somewhat more complicated data container, but in the
example above, data.week1 data and data.week2 data are Ruport::Data::Table
objects. When we use the PDF formatter’s draw table method, it generates nice PDF
tables like this:

Here’s the interesting thing about this formatting code: we wrote it long before we even
had our Rails app up and running. In Ruport, there is absolutely no difference between

28
the data structure returned by SomeActiveRecordModel.report table() and
Table("some.csv").
This is quite powerful, and we tend to use it readily in our day-to-day work. If a client has
some sense of how a report should look, we happily ask them to pull up Excel and mock
out some data for us. We can then use that to start working on formatted reports before
we even have a database populated. This leads to the great feeling of already having some
useful functionality before we even work on the basic CRUD stuff for an application.
Let’s take a quick look at a possible mock data object for this report:

class MockTimeSheet

include Ruport::Controller::Hooks
renders_with TimeSheet
def initialize(options={})
@employee = "Gregory Gibson"
@week1_data = Table(options[:week1_data])
@week2_data = Table(options[:week2_data])
@week1_overtime = nil
@week2_overtime = 10
end

attr_reader :employee, :week1_data, :week2_data,


:week1_overtime, :week2_overtime

end

Controller::Hooks allow you to tie your regular old Ruby objects to Ruport’s
formatting system. They provide the following shortcut:

MyController.render_pdf(:data => my_obj, :file => "my.pdf")

# becomes

my_obj.save_as("my.pdf")

Beyond this, they provide some functionality for transforming data and doing
format-specific adjustments, though for our needs, we don’t need that kind of magic.2
It’s also not important here to know how the real TimeSheet controller works, but you
can of course check it out in PayR’s source. Usually in the prototyping phase, the
controller might look very simple:
2 If you’re interested, consider checking out the custom controller logic cheatsheet.

29
class TimeSheet < Ruport::Controller
stage :time_sheet

module Helpers
def start_date
Date.today
end

def end_date
Date.today + 14.days
end
end
end

Believe it or not, combined with the PDF formatter we’ve shown here and the
MockTimeSheet object, you have a functional report. To test it out, try this:

@timesheet = MockTimeSheet.new(:week1_data => "week1.csv",


:week2_data => "week2.csv")
@timesheet.save_as("timesheet.pdf")

You can pretty much populate the CSV files with whatever data you’d like. If you have
some nice data from your client to work with, use that. If you don’t have data yet, just
populate it with some junk, the real goal is just to get something on the screen as fast as
possible.
When you run this mocked-out report, it doesn’t look particularly pretty, but you can see
all the key elements are there:

Rather than doing your stylistic tweaking directly in the formatter, it’s usually
recommended to use Ruport’s templates. These let you externalize the look-and-feel
components of your report and re-use them as needed. We will cover these in-depth later
on in the discussion, but for now, rest assured that you can easily make things pretty
from a slightly higher level than the raw formatter implementations.
We could stop at the PDF format and move on to wiring up the database models and the
CRUD, but we might as well try to view this mock data through the browser as well.

30
2.4 Using ERB for HTML Reports
For better or worse, most HTML generation in Rails is done through ERB. It turns out
this is a reasonably practical way to do dynamic HTML generation. A major criticism of
Ruport is that doing string concatenation in the HTML formatters seems to take people a
little outside their comfort zone, and also results in somewhat brittle report views.
Lately, we have been cheating a bit to try to make everyone happy.

class HTML < Ruport::Formatter::HTML


renders :html, :for => TimeSheet

build :time_sheet do
output << erb(RAILS_ROOT + "/app/reports/timesheet.html.erb")
end

end

This allows you to execute an ERB file in the context of a Ruport formatter, which
means you can mostly treat the HTML formatter as a shim to give you a Rails view that
has some reporting pre-processing done for you.
From here, you can reach your Ruport data, options, and helper methods as needed. The
following ERB template fleshes out the HTML format for the Timesheet controller:

<h3>Week of <%= start_date %></h3>

<table>
<tr>
<% data.week1_data.column_names.each do |c| %>
<th><%= c %></th>
<% end %>
</tr>

<% data.week1_data.each do |r| %>


<tr>
<% r.to_a[0..2].each do |c| %>
<td><%= c %></td>
<% end %>

<% r.to_a[3..-1].each do |c| %>


<% if c.blank? %>
<td style="background: #999999;">&nbsp;</td>
<% else %>
<td align="right"><%= c %></td>

<% end %>

31
<% end %>
</tr>
<% end%>

</table>

The nice thing about using an approach like this is that your HTML view code is
separate from the core formatter and can be updated as needed. Also, you can see here
that we only show one week at a time in our HTML version of the Timesheet report.
This hopefully helps illustrate the flexibility of Ruport’s formatting system.
If you want to try this code, you can use the same mock invoice object. Just tweak the
path to your ERB file as needed if you’ve not yet created your Rails app, and then run
this:

@timesheet = MockTimeSheet.new(:week1_data => "week1.csv",


:week2_data => "week2.csv")
@timesheet.save_as("timesheet.html")

When you call save as(’something.format’), Ruport just looks for the matching
formatter defined via the renders :format, :for => Something call. This means you
can easily use this shortcut with any of your custom formatters, even if they’re not one of
the four that Ruport supports by default.

2.5 Time to Dig Deeper


Through this introduction to some of the Ruport code in PayR, you may have been able
to catch a reasonable glance of how we develop reporting applications on top of Rails.
However, we certainly don’t expect you to come away from what we’ve said so far with a
comprehensive idea of how you’ll integrate Ruport into your projects. We will now take
you through two more PayR reports, this time with a whole lot more attention to detail
and comprehensive explanations of how things actually work. Though we’ll circle back
around after that to talk about some of Ruport’s bells and whistles, the extensive
discussion of the formatting system to follow will quickly bring you up to speed on one of
the most important aspects of Ruport development.

32
Chapter 3

Report Formatting

3.1 Introduction
One of the reports needed for PayR is a call-in sheet that lists employees, grouped by
type, and their hours over a two week period. Below is the formatted output from this
report:

In this chapter, we’ll look in detail at how we created this report. The first steps will be to
define a controller and one or more formatters, but since this is a Rails project, you first
need to decide where to put the files in the Rails directory structure. There’s no single
best location; some people prefer to place them in app/reports and others in lib, so
that decision is ultimately up to you for your own projects. The code for all of the reports
in PayR is located in app/reports. Each class has its own file to make browsing easy.

3.2 Data Model


Before we begin to define the report, let’s take a look at the model definitions to get an
idea of how the data is structured and how the models are associated with each other.
Since PayR is a time sheet application, one of the core models is Employee. There are
also various other models used to hold employee time data. You can find the migrations
used to create the database tables in the PayR source.

33
class Employee < ActiveRecord::Base
has_many :regular_times
has_many :lunch_times
has_many :other_times
end

class RegularTime < ActiveRecord::Base


belongs_to :employee
end

class LunchTime < ActiveRecord::Base


belongs_to :employee
end

class OtherTime < ActiveRecord::Base


belongs_to :employee
end

For the report, we need to get all of the employees and calculate some of the data to be
displayed. Then we need to group all of the employees by type and display them,
formatted as shown above.

3.3 Ruport Controllers


In Ruport, the first step to achieving formatted output is to define a controller for the
report. This is the component that establishes the steps that will be called to generate
the report’s output. It also allows you to specify options that should be populated and to
do some setup and manipulation of the data prior to formatting. A first pass at defining a
very basic controller for the report might be:

class CallInController < Ruport::Controller

stage :call_in_sheet

end

One thing to notice is that the controller class should be a subclass of


Ruport::Controller. Doing so gives your class access to the functionality of Ruport’s
rendering system. This particular class then specifies that one stage will be called during
the rendering process.
This means that Ruport will look for a method named build call in sheet (formatter
hook methods are named build plus the name of the stage) in the formatter and, if it
exists, will call it. In your formatter, you can either define the build methods directly or

34
you can use the syntax that Ruport provides for defining formatter hook methods, build
:stage name.
You can define as many stages as desired and Ruport will try to call all of them. If a
method for a particular stage isn’t found, Ruport will simply ignore it. This becomes
useful when you want to use a single controller with multiple formatters, some of which
might not implement all of the stages.

3.4 Data Collection


Now let’s look at the data we need. As shown above, the report has the following column
headings: “Employee Type”, “Employee,” “Week 1,” “Week 2,” “Regular Hours,”
“Overtime,” “Lunch,” “Personal,” “Sick,” and “Vacation.” If you look at the columns in
the employee table defined by the migration, you can see that employees have
first name, last name, and group attributes. These will be used to populate
“Employee,” and “Employee Type.”
All of the other columns contain calculated data. Since our focus is on producing the
output using Ruport, we won’t go into detail on all of the techniques used to generate the
data other than to say that the Employee model is provided with an employee record
method and a name method. The output of each might look something like this:

record.name #=> "Gregory Brown"


record.employee_record(14.days.ago) #=>
{ :week1=>"17.43", :regular_hours=>"17.43",
:week2=>"0.00", :employee=>"Gregory Brown", :lunch=>"0.00",
:employee_type=>"Dentist", :overtime=>"0.00", :personal=>"0.00",
:sick=>"8.00", :vacation=>"24.00" }

One other thing to point out with this code is that the employee record method expects
a start date as a parameter. The report outputs two weeks worth of employee time sheet
data, beginning with the week containing the start date. What this means for our report
is that we need some way of passing in the start date as an option.

3.5 Setting Options and Data


To do so, we make use of Ruport’s options object. Each controller and formatter share
an options object, implemented as a subclass of OpenStruct. This allows you to use
named options at any point in the rendering or formatting process. If you have some
required data that is to be supplied by the code that renders the report, and want to be
sure of its presence, you can use the required option class method. Ruport will raise an
exception if the data for any required options are not supplied.

35
class CallInController < Ruport::Controller

stage :call_in_sheet

required_option :start_date

end

Later, when we actually render a specific instance of the report, we need to provide a
value for the start date using this option.
Although our employee model now has the ability to provide a hash of data for the report
(to become a record in the report’s data table), we still need to create a Ruport table from
all of the employee records and then create a Grouping of the data. In order to provide
some separation in our code, we define a class named CallInAggregator to do the work.

class CallInAggregator

def initialize(options={})
@start = options[:start]
end

def to_grouping
table = Table([:employee_type, :employee, :week1, :week2, :regular_hours,
:overtime, :lunch, :personal, :sick, :vacation ]) do |t|
Employee.find(:all).each {|e| t << e.employee_record(@start) }
end
table.rename_columns(:week1 => "Week 1",
:week2 => "Week 2")
table.rename_columns {|c| c.to_s.titleize }

Grouping(table,:by => "Employee Type")


end

end

The constructor takes a hash and expects to find a member with the :start key. This is
used to populate an instance variable named start. The to grouping method does the
rest of the data manipulation.
We need to create a Ruport table from the employee data and to do so, we use one of the
shortcuts provided by Ruport, the Table method. It takes an array of column names and,
optionally, a block that can be used to populate the table. In our example, we find all of
the employees and then iterate through them, calling the employee record method on

36
each one and appending the returned data as a row in the table. The block associated
with Table is passed a Data::Feeder object which will be explained in more detail
elsewhere,1 but for now, just know that you can use the << method to add rows to the
table.
After the table is created, we use Ruport’s rename columns method to give the columns
the required names for the report. First we need to manually rename the “week1” and
“week2” columns to capitalize and add spaces, resulting in “Week 1” and “Week 2”.
Notice you can pass a hash to rename columns which maps the old column names to the
new column names. The next call to rename columns uses the block form and calls to s
and titleize for each of the column names.
The last data manipulation step is to group the data by creating a Ruport Grouping. We
use the Kernel method Grouping to create it. This method takes two paramaters; a table
or group that will be used as the source of data to create the Grouping and the name of
the column or columns to group by.

3.6 Using the setup Method


At this point we have finished populating the table that will be the basis for the final
report. Next, we set this grouping as the controller’s data source, as follows:

class CallInController < Ruport::Controller

stage :call_in_sheet

def setup
self.data ||=
CallInAggregator.new(:start => options[:start_date]).to_grouping
end

end

We create a new CallInAggregator and pass in the :start parameter, using the
:start date option that we mentioned earlier. Then we call the to grouping method on
the newly created CallInAggregator object. Finally, the resulting Grouping is set as the
controller’s data attribute. The data attribute is available in both your controller and any
associated formatters to hold the report’s data.
Note that this code is contained in a method named setup. The setup method is special
in that, if present in your controller, it will be called after all of the options have been set
(and after the data attribute has been set, if you pass in the data at rendering-time).
Consequently, you have access within setup to all of the information supplied to the
controller, allowing you to do data manipulations or anything else that you might need to
accomplish prior to actual formatting.
1 See the Data Manipulations cheatsheet for more detail.

37
3.7 Using the Helpers Module
We need to add one more thing to the controller before moving on to the formatter. Our
report is going to have a header that includes the date range. We want these dates to be
in a specific format, so we use the strftime method to output them in the proper
format. We could just do this in the formatter, but that might not be particularly DRY.
What if we want to use the dates in several different formatters (for different output
formats) or even in multiple locations within a single formatter? This is where you can
use a special module called Helpers, as follows:

class CallInController < Ruport::Controller

stage :call_in_sheet

def setup
self.data ||=
CallInAggregator.new(:start => options[:start_date]).to_grouping
end

module Helpers
def start_date
format_date(options.start_date)
end

def end_date
format_date(options.start_date + 13.days)
end

def format_date(date)
date.strftime("%m/%d/%Y")
end
end

end

We define start date and end date methods, containing the code to calculate and
format the dates, within the Helpers module. If present, Helpers will be mixed in to the
formatter (or formatters). This allows you to call any of the module’s methods in your
formatters and satisfies the DRY principle in that we only have to define them once.

3.8 Ruport Formatters


The next step in creating the output is to define one or more formatters for each of the
output formats you want to produce. Each one will register itself as the formatter for a

38
particular named format. For our report, we want to be able to produce both PDF and
HTML output, so we need two formatters.

class PDF < Ruport::Formatter::PDF

renders :pdf, :for => CallInController

end

class HTML < Ruport::Formatter::HTML

renders :html, :for => CallInController

end

Although you can define formatter classes that subclass Ruport::Formatter directly, if
your output is going to be one of the four that Ruport supports internally (PDF, HTML,
CSV, or text), you probably want to subclass the built-in formatter for that type of
output. The reason is that each of the built-in formatters contains predefined helper
methods to make the task of developing the formatting code much easier.
For our call-in report, we create formatters for PDF and HTML output by subclassing
Ruport::Formatter::PDF and Ruport::Formatter::HTML, respectively. The formatters
call the renders class method in order to register themselves with the controller. Each
supplies the name of the format and the controller for which it will supply output.
Next we add the actual formatting code. Recall that our controller defined a single stage
for the report (:call in sheet), so our formatter will need to supply an implementation for
the :call in sheet stage. For the PDF formatter, we have the following:

class PDF < Ruport::Formatter::PDF

renders :pdf, :for => CallInController

build :call_in_sheet do
pad_bottom(10) do
add_text "Call In Sheet (#{start_date} - #{end_date})"
end
render_grouping data, options.to_hash.merge(:formatter => pdf_writer)
end

end

The pad bottom method is one of the helpers provided by the built-in PDF formatter
and, as its name implies, adds the specified amount of space to the bottom of the output
created within the associated block. There are many of these types of helpers in the PDF
formatter to assist with properly positioning and drawing output on the page, suitable for

39
printing. Another is the add text method that we use to output the header. Note that
we use the start date and end date methods that we defined earlier in the Helpers
module.
Finally, we call the render grouping method. Since the data that we want to output is a
Grouping object and since groupings have their own predefined controller, we can use this
method to pass off the work to the built-in Grouping controller. We supply it with the
data attribute as the first parameter, which you will recall was set to the Grouping
created by the CallInAggregator.
The second parameter consists of the formatter options object, which we convert to a
hash. We need to pass these along because we’re asking a different controller to create
output for us, so we need to make sure it has all of the options that were sent to the main
controller, in case it needs to use some of them. In the case of a PDF, we also need to do
more - pass in the current formatter’s pdf writer object as the :formatter option.
The built-in PDF formatter contains an object called pdf writer which is an instance of
PDF::Writer. It is a representation of the PDF document we’re creating and as such, we
want all of our output to be contained in this document. If we don’t include it in the
options, the formatter for the Grouping controller will create a new pdf writer object
and we won’t get the expected results. Other output formats won’t generally have this
concern.
Below is the formatter for the HTML output:

class HTML < Ruport::Formatter::HTML

renders :html, :for => CallInController

build :call_in_sheet do
output << textile("h3. Call In Sheet (#{start_date} - #{end_date})")
output << data.to_html(:style => :justified)
end

end

For HTML, we can just append all of the formatted data to the output object as a string.
First we generate the header using the textile method. This is a helper provided by the
built-in HTML formatter and uses RedCloth to evaluate the string provided in textile
format. Next we call to html on the data. This is a shortcut method to render the
Grouping as HTML output. The built-in formatters define a number of different output
styles for groupings, here we specify the :justified style.

3.9 Using Templates


Now that the controller and formatters are defined, we’re ready to actually generate the
output, right? In fact, we could do so, but let’s do one more thing first. You may have

40
noticed, particularly with the PDF formatter, that we didn’t define any formatting
options, such as font size, alignment of data in the columns, page layout, etc. If we don’t
do anything else, Ruport will use default values to format the report. This may or may
not be what you want, so you need to experiment with the built-in default formats to see
if they fit your needs.
For the call-in report, we decided to set some formatting options to customize its look.
You can define all of these options at the time of rendering the report, but that wouldn’t
be very portable or very DRY. You would need to define the options every time you
render the report. Instead, Ruport gives you the option to define formatting templates.
Templates are useful in many different situations. They basically allow you to predefine
any number of options and have those options available to your formatter. This is helpful
if you want to render the same report in multiple locations or if you want to define a
consistent set of options to use for a number of different reports.
You define a template by using the create method of Ruport::Formatter::Template
and giving the template a name. Here’s the template we defined for the PayR reports:

Ruport::Formatter::Template.create(:default) do |format|

format.page = {
:layout => :landscape
}
format.grouping = {
:style => :separated
}
format.text = {
:font_size => 16,
:justification => :center
}
format.table = {
:font_size => 10,
:heading_font_size => 10,
:maximum_width => 720,
:width => 720
}
format.column = {
:alignment => :right
}
format.heading = {
:alignment => :right,
:bold => true
}

end

Notice that these are mostly formatting instructions that will be used by the formatters

41
(primarily by the PDF formatter). The individual formatters will simply ignore any
options that they don’t use. Once we have the template defined, we will pass its name as
the :template option when we render the report and the template will be available to the
formatter.2

3.10 Producing Output


Now we’re finally ready to actually render the output. While there are a number of
different ways you can accomplish this, let’s look at how you might typically do so in the
context of a Rails project like PayR. We add a method to one of our controllers to
generate the report.

class ManagerController < ApplicationController

def call_in_sheet
pdf = CallInController.render_pdf(
:start_date => Time.parse(params[:period]),
:template => :default
)

send_data pdf, :type => "application/pdf",


:disposition => "inline",
:filename => "call_in.pdf"
end

end

To generate a PDF report, we call render pdf on the CallInController, passing in the
start date option and the name of the template. We assign the output to a variable called
pdf and then use the send data method to stream the output to the user’s browser. We
supply the type of the output as being “application/pdf” and, although optional, we
define the disposition and filename. Rendering HTML format is similar, but you can
render the output directly rather than streaming it with send data.
This chapter provided an overview of Ruport’s formatting system through a real-world
example from the PayR project. For more on rendering and formatting, you can refer to
the cheatsheets.3 The next chapter will look at another of PayR’s reports in order to
demonstrate more of Ruport’s formatting capabilities, including graphing support.

2 See the Ruport Formatting cheatsheet for more details on template usage.
3 See the Ruport Formatting cheatsheet and the Controller Logic cheatsheet specifically.

42
Chapter 4

More Report Formatting

4.1 Introduction
Now that you’ve seen the basics of report formatting, let’s look at one of PayR’s slightly
more complicated reports. In general, by supplying an employee and range of dates, we
want to be able to generate a report of that employee’s weekly regular hours worked. The
output of the report should show a graph of the hours worked over the supplied time
period, and there should be a tabular presentation of the data used to make the graph.
Also, we want to put a border around the whole page. In the end, the report should look
like this:

43
For the examples in this chapter, we’re going to use Ruport’s graphing support, provided
in the ruport-util package. This package contains support for a few different graphing
libraries and in this case we used the Gruff library. Currently, Ruport only provides
integration with Gruff’s line graphs, but since that’s what we plan to use for this report,
it will work for us.

4.2 A Basic Controller and Formatter


Before we describe how to use the graphing library, let’s get the basics in place for the
report. We’ll develop the report iteratively, to show how you can build up a report from
different components of Ruport as well as its supported packages. As you saw in the last
chapter, we need to start by defining a controller and formatter (for this report, we only
define a PDF formatter). Start by stubbing out the different parts of the report you know
you’re going to need and then you can add the details later.

class WeeklyTimeController < Ruport::Controller


stage :week

class PDF < Ruport::Formatter::PDF


renders :pdf, :for => WeeklyTimeController

build :week do
end
end

end

This defines the structure of the report. It has a controller with a single stage of :week
and a single PDF formatter that builds the stage. You might find later that you need
more stages to separate out components of the report, that you need to predefine certain
options as being required, or that you want additional formats, but most reports you
create will have at least this basic structure.
Before you move on to writing any formatting code, you need to think about how you will
obtain the data needed for the graph and the table. Since the report’s requirements stated
that you should be able to supply the employee and range of dates, we will assume that
the code (in the controller) provides an employee id, start date, and end date as options
to the controller. From there, we should be able to obtain all the other data we need.

4.3 Adding Text


The first items on the page are the heading “Employee Times By Week” and the name of
the employee. This report also uses the Employee model as the basis for its data, which
will need to be queried for the employee name. You already saw how to add text to a

44
PDF document in the last chapter using the add text method, so let’s begin by adding
the heading and name.

class WeeklyTimeController < Ruport::Controller


stage :week

def setup
e = Employee.find(options.employee)
options.emp_name = e.name
end

class PDF < Ruport::Formatter::PDF


renders :pdf, :for => WeeklyTimeController

build :week do
padding = 40

add_text "Employee Times By Week", :font_size => 16,


:justification => :center
pad(padding) {
add_text "<b>Employee: </b>#{options.emp_name}", :font_size => 12
}
end
end

end

Once again, we use the setup method to perform manipulations on the data for our
report. We first find the employee from the id (options.employee) supplied to the
controller. Then we set another option to hold the employee’s name for our formatter to
use. As you see, you can use the options to provide outside information to the controller
as well as to communicate information between the controller and the formatter since
they share their options object.
In the formatter, we fill in some of the build :week stage. We add the title “Employee
Times By Week,” using the add text method supplied by the built-in PDF formatter and
set the font size and center-justify the text. We also add the employee name and use the
pad method to add some padding between the text.
Notice that we set the font size and justification by passing these options directly to the
add text method. There are other techniques you can use to set these types of options.
One other way is to set an option called text format. For example, we could have done
this:

45
options.text_format = { :font_size => 16, :justification => :center }

add_text "Employee Times By Week"

You can also set options globally by using a template, as shown in the last chapter. For
our report, though, since we change the font size and justification after each call to
add text, it’s just as easy to supply the options directly to the method. The other
techniques, options.text format and templates, are best saved for situations where you
have a setting that is global to the document or at least is used for a significant portion of
it.

4.4 Adding a Table


Now that all of the plain text is added to the report, we can add the graph and the table.
We will start with the table since it uses techniques that we’ve seen before and then move
on to the graph. Adding the table gives us this for our controller and formatter:

class WeeklyTimeController < Ruport::Controller


stage :week

def setup
e = Employee.find(options.employee)
options.emp_name = e.name
hours = e.regular_hours_for_range(options.start_date,options.end_date).sort

options.table = Table("Week","Time") {|table|


hours.each {|row| table << row }
}
end

class PDF < Ruport::Formatter::PDF


renders :pdf, :for => WeeklyTimeController

build :week do
padding = 40

add_text "Employee Times By Week", :font_size => 16,


:justification => :center
pad(padding) {
add_text "<b>Employee: </b>#{options.emp_name}", :font_size => 12
}

46
draw_table(options.table, :position => left_boundary, :width => 200,
:column_options => { :width => 100 })
end
end

end

There are a few things to emphasize with the changes. First, notice that the table data
comes from a method (regular hours for range) of the Employee model. This method
obtains the employee’s regular hours worked for the date range we want (supplied by the
start date and end date options). Of course, we haven’t written this method yet, but
we’ll do that next. Subsequently, we use the data to construct a Ruport::Data::Table.
We add the table to the report using the draw table method. What you should realize
here is that the options we provide to this method, such as :position and :width are being
passed along to PDF::Writer (PDF::SimpleTable to be more accurate). Since Ruport
uses PDF::SimpleTable behind the scenes to create the table, you can set any of the
attributes available to PDF::SimpleTable by supplying options with the same name. The
left boundary method is supplied by Ruport’s PDF formatter and returns the x-position
at the left margin.
Although you can set column options manually by creating PDF::SimpleTable::Column
objects, Ruport supplies an abstraction in the draw table method. You can set the
:column options to a hash containing any of the attributes available to
PDF::SimpleTable::Column (again using the same names as the attributes). This is
usually much easier than creating your own Column objects. Using this technique, we set
the width of both the table and the columns to get a nicely proportioned table.
As noted previously for text, you can also set global options for tables using the
table format option. The following is equivalent to the code shown above:

options.table_format = :position => left_boundary,


:width => 200,
:column_options => { :width => 100 }

draw_table(options.table)

Now let’s take a look at the regular hours for range method we added to the
Employee model. It expects a start date and end date for the range of weeks to include in
the results. It collects data by week from the beginning of the week containing the start
date to the end of the week containing the end date. Finally, it returns a hash of data for
the regular hours worked by the employee per week, keyed on the beginning date of each
week.

def regular_hours_for_week(date=nil)
date ||= Date.today
start = date.beginning_of_week

47
reg_times = regular_times.find(:all,
:conditions => ["start_time between ? and ?", start, start + 7.days] )
reg_times.inject(0) {|s,e| s + e.hours }
end

def regular_hours_for_range(start_date,end_date)
res = {}
date = start_date.beginning_of_week
while date <= end_date
res.merge!(date => regular_hours_for_week(date))
date += 7
end
res
end

4.5 Adding a Graph


Now that the report contains the text and table, we can add the graph to the page. This
doesn’t require anything much different than what you’ve already seen, but we will be
using the ruport-util library for its graphing support. As stated earlier, ruport-util wraps
portions of the Gruff graphing library (among others) for this purpose.
Ruport implements the concept of a graph in a data structure called, appropriately,
Ruport::Data::Graph. This is a subclass of Ruport::Data::Table with some
specialized behavior, mainly the addition of a series method that allows you to add lines
to the graph. Ruport also supplies a Kernel method called Graph as a shortcut for
creation of the graph.
The updates to the controller and formatter to include the graph are as follows:

class WeeklyTimeController < Ruport::Controller


stage :week

def setup
require ’ruport/util’

e = Employee.find(options.employee)
options.emp_name = e.name
hours = e.regular_hours_for_range(options.start_date,options.end_date).sort

options.table = Table("Week","Time") {|table|


hours.each {|row| table << row }
}

weeks = hours.map {|a| a[0].to_s }


values = hours.map {|a| a[1] }

48
g = Graph(weeks)
g.series values, e.name
options.graph = g

options.labels = weeks.inject({}) {|l,w|


l.merge!(l.size => w)
}
end

class PDF < Ruport::Formatter::PDF


renders :pdf, :for => WeeklyTimeController

build :week do
padding = 40
x = left_boundary

add_text "Employee Times By Week", :font_size => 16,


:justification => :center

pad(padding) {
add_text "<b>Employee: </b>#{options.emp_name}", :font_size => 12
}

width = 300
height = 225
y = cursor - height

draw_graph(options.graph,
:title => "Times By Week", :labels => options.labels,
:x => x, :y => y, :width => width, :height => height)

move_cursor((height + padding) * -1)


draw_table(options.table, :position => x, :width => 200,
:column_options => { :width => 100 })
end
end

end

We continue to use the setup method to perform data manipulations. The data for the
graph is the same as the data we used for the table, namely the hash of employee hours
created by the regular hours for range method. We use the Kernel method Graph to
create the graph and then use the series method to add a data series to it. Gruff uses a
hash with keys that are indexes to the series’ data to contain the labels for the x-axis, so
we create that hash and save it in options.labels to use later when we render the graph.

49
In the formatter, we need to do some work to maintain the flow of the document. Adding
an image to a PDF using the draw graph method (which we use here) places the image in
an absolute position and doesn’t move the cursor. Therefore, since we want to add it
above the table, we need to do some calculations to make sure the position of the graph is
correct and that the table follows it in the correct location.
First we define the width and height of the graph (300x225). Next we calculate the y
position for placement of the graph. PDF coordinates are calculated from the bottom
left, so to get the y position, we take the current location of the cursor and subtract the
height of the graph.
Then we use the draw graph method of ruport-util to add the graph to the page and we
supply the graph as well as a hash of options. The options allow you to specify the
position and size of the graph (with :x, :y, :width, and :height), as well as to set some
characteristics of the graph itself. We add a graph title and use the labels we saved
earlier. In addition to the options we set, you can also use the :min and :max options to
set the minimum and maximum values for the graph.
Once the graph is positioned and drawn, we need to make sure the table is positioned
below it. We need to move the cursor to where we want the table to start, so we move it
(in the negative direction) down an amount equal to the height of the graph plus the
defined padding.

4.6 Adding a Border


The final step in creating our report is to add the page border. We want this to be a
double border around the whole page. The controller doesn’t change for this step, so we’ll
only show the formatter, with the border added:

class PDF < Ruport::Formatter::PDF


renders :pdf, :for => WeeklyTimeController

build :week do
offset = 3
margin = offset * 6
padding = 40
x = left_boundary + margin

move_cursor(margin * -1)
add_text "Employee Times By Week", :font_size => 16,
:justification => :center
pad(padding) {
draw_text "<b>Employee: </b>#{options.emp_name}",
:font_size => 12, :left => x
}

50
width = 300
height = 225
y = cursor - height

draw_graph(options.graph,
:title => "Times By Week", :labels => options.labels,
:x => x, :y => y, :width => width, :height => height)

move_cursor((height + padding) * -1)


draw_table(options.table, :position => x + 105, :width => 200,
:column_options => { :width => 100 })

page_border(offset)
end

def page_border(o)
[0,o].each do |offset|
top = top_boundary - offset
bottom = bottom_boundary + offset
left = left_boundary + offset
right = right_boundary - offset
move_cursor_to(top)
horizontal_line(left,right)
move_cursor_to(bottom)
horizontal_line(left,right)
vertical_line_at(left,top,bottom)
vertical_line_at(right,top,bottom)
end
end
end

We define an offset that will be the distance between the two lines of the border. We also
use the offset to calculate a margin width to move everything already placed on the page
away from the borders. Finally, we define a page border method to draw the lines for the
border, again using PDF drawing methods supplied by Ruport.
Note that we use the margin to change the x position of the graph and the table, so that
they don’t overlap the border. We also move the cursor down below the border before
adding the title text. Finally, we change the method used to add the employee name from
add text to draw text so that we can specify its x position. We don’t need to do so with
the title text since it is centered on the page.
In this report, we used some of the drawing helpers included in Ruport’s built-in PDF
formatter, such as horizontal line and vertical line at, to show you the manual
techniques needed to draw the border. This will be of benefit if you need to draw lines on
a PDF page, but there is an easier way to accomplish border drawing by using the
FormHelpers module included in ruport-util.

51
The FormHelpers module gives you a basic set of methods that allow you to build PDF
forms. One of the included methods is draw border. Therefore, we could rewrite the
relevant sections of the report as follows:

class WeeklyTimeController < Ruport::Controller


require ’ruport/util’

#...

class PDF < Ruport::Formatter::PDF


include Ruport::Util::FormHelpers

#...

def page_border(o)
draw_border(left_boundary,
top_boundary,
right_boundary - left_boundary,
top_boundary - bottom_boundary)
draw_border(left_boundary + o,
top_boundary - o,
(right_boundary - o) - (left_boundary + o),
(top_boundary - o) - (bottom_boundary + o))
end
end

end

4.7 Rendering the Report


Now that the border is drawn, our report is complete. To finish the discussion of this
report, let’s see the controller method used to generate the report.

class ManagerController < ApplicationController

def weekly_time_report
pdf = PDF::Writer.new

ec = Employee.count - 1

52
Employee.find(:all).each_with_index do |e,i|
WeeklyTimeController.render_pdf(
:start_date => params[:start].to_date,
:end_date => params[:end].to_date,
:employee => e.id,
:formatter => pdf
)

pdf.start_new_page unless ec == i
end

send_data pdf.render, :type => "application/pdf", :filename => "graph.pdf"


end

end

When generating the report, we iterate through the employees and add a page to the PDF
for each employee. For this reason, we need to instantiate our own PDF::Writer object
and pass it to the controller. As discussed in the previous chapter, if we use the default
behavior, then the formatter will create a new PDF::Writer object each time it is called.
When we create our own object, the formatter will re-use it instead of creating a new one.
As you can see, especially when creating PDFs, formatting can involve trial and error,
adding pieces of the report individually and moving things around in the design until it
looks right. Ruport supplies a lot of tools to make these formatting tasks easier.
In the last few chapters you’ve seen most of what you’ll need for the formatting tasks
you’ll encounter in your daily work. We’ll now jump into another aspect of Ruport, which
is quick and dirty ad-hoc reporting.

53
54
Chapter 5

Ad-hoc Reporting with rope

In the last few chapters, you’ve seen how deeply Ruport can be extended. You’ve also
probably caught a glimpse of how it is reasonably easy to get everything wired up in Rails
application without acquiring a tacked-on feel.
In this chapter, we’ll turn all of those concepts on their head, and cover what Ruport was
originally designed for: quick and dirty ad-hoc reporting. Though the following example
has been simplified to make it easier on the eyes, I’m sure you can imagine plenty of
scenarios where you want a quick CSV report without much hassle, and that’s what you’ll
see how to do here.
We’re going to take a look at how to use ruport-util’s code generator, rope, to quickly
generate configuration files and reports for a standalone environment. We’ll then make
loose ties back into our Rails application so we can still make use of our models there.
Once we get things set up, we’ll create a simple report which compares our data from
PayR to a MySQL database that contains appointment data.
Our report will show us when scheduled vacation days in PayR conflict with employee
appointments. The output format will be a simple CSV of names and dates, which will be
emailed to managers on a nightly basis. Though we could have done all of this within our
Rails application, the idea here is we may end up doing a whole lot more of these ad-hoc
reports, and we want them to be largely independent of the Rails app itself for simplicity.

5.1 Generating and Configuring a rope Application


We start by running the rope command and creating a simple skeleton in the lib/
directory of PayR.

$ rope lib/nightly_reports
creating directories..
lib/nightly_reports/test

55
lib/nightly_reports/config
lib/nightly_reports/output
lib/nightly_reports/data
lib/nightly_reports/data/models
lib/nightly_reports/lib
lib/nightly_reports/lib/reports
lib/nightly_reports/lib/controllers
lib/nightly_reports/sql
lib/nightly_reports/util
creating files..
lib/nightly_reports/lib/reports.rb
lib/nightly_reports/lib/helpers.rb
lib/nightly_reports/lib/controllers.rb
lib/nightly_reports/lib/templates.rb
lib/nightly_reports/lib/init.rb
lib/nightly_reports/config/environment.rb
lib/nightly_reports/util/build
lib/nightly_reports/util/sql_exec
lib/nightly_reports/Rakefile
lib/nightly_reports/README

We now need to configure our database connections and set up a mail server. We do this
in the project’s config/environment.rb. Keep in mind that this is your rope
application’s environment file, not the Rails app.
Let’s look at the full config file first. Afterward, we’ll check it out in more detail.
lib/nightly reports/config/environment.rb

require "ruport"

RAILS_ROOT = File.dirname(__FILE__) + "/../../.."

Ruport::Query.add_source :default, :user => "root",


:dsn => "dbi:mysql:dental_db"

Ruport::Mailer.add_mailer(:default,
:host => "smtp.gmail.com",
:address => "test@gmail.com",
:user => "test@gmail.com",
:password => "alpha123",
:auth_type => :plain,
:port => 587
)

require "active_record"

56
require "ruport/acts_as_reportable"

require "#{RAILS_ROOT}/config/environment"

We first establish the location of the root of our rails app as a convenience. Since this
isn’t done for us by rope, we need to define it ourselves:

RAILS_ROOT = File.dirname(__FILE__) + "/../../.."

We then establish our connection to a MySQL database. Though configuration details


can get more interesting than this, we’re using the most simple case here:

Ruport::Query.add_source :default, :user => "root",


:dsn => "dbi:mysql:dental_db"

Since Ruport uses RubyDBI under the hood, the DSN is just a string that DBI uses to
establish a connection. Here we’re telling it to use the MySQL driver, and attach to a
database called dental db on the localhost using the MySQL root user.
From here, we establish our SMTP settings, since we’ll be mailing the report
automatically:

Ruport::Mailer.add_mailer(:default,
:host => "smtp.gmail.com",
:address => "test@gmail.com",
:user => "test@gmail.com",
:password => "alpha123",
:auth_type => :plain,
:port => 587
)

This shows pretty much every option you might need to establish an SMTP connection.
Your actual configuration may be much more simple. However, the fact that we’re using
GMail complicates things a bit, and though we won’t go into details here, you can check
lib/init.rb for details.1
Once we’ve established our email settings, we take a little shortcut to get our app
working with our Rails models:

require "active_record"
require "ruport/acts_as_reportable"

require "#{RAILS_ROOT}/config/environment"
1 Net::SMTP has some issues, which are addressed by a monkey-patch from Stephen Chu. We found this

fix at: http://www.stephenchu.com/2006/06/how-to-use-gmail-smtp-server-to-send.html

57
This establishes a connection to the Rails database and lets us skip a bunch of hard
wiring of model files and manual loading of dependencies. It’s certainly a hack, so your
mileage may vary. You’ll need to set your RAILS ENV environment variable in whatever
script ultimately runs the scheduled report, but it will default to development, which is
what we’re looking for at this point anyway.
With all of this stuff together, we can do a quick sanity check to make sure things are
working:

$ irb -Ilib -rlib/init

>> puts Ruport::Query.new("describe appointments").result


+------------------------------------------------------------+
| Field | Type | Null | Key | Default | Extra |
+------------------------------------------------------------+
| provider_id | char(20) | YES | | | |
| patient_id | int(11) | YES | | | |
| appointment_time | datetime | YES | | | |
+------------------------------------------------------------+
=> nil

>> Employee.column_names
=> ["id", "first_name", "last_name", "username", ...]

Perfect! Shows that both our raw access to the MySQL database and the hooks into our
Rails application are working. We can now move on to bigger and better things.

5.2 Developing a Simple CSV Report


Applications generated with rope have a Rakefile based interface, so we can use that to
generate our report file.

$ rake build report=vacation

reports file: lib/reports/vacation.rb


test file: test/test_vacation.rb
class name: Vacation

This simply generates a place for tests and a simple boilerplate file for a report. Actually,
rope can generate a lot more stuff,2 but this is all we’ll need for now.
The report definition is quite minimal at first, looking like this:
2 See the rope Cheatsheet for more details.

58
require "lib/init"
class Vacation < Ruport::Report

def renderable_data(format)

end

end

If you’ve worked with Ruport::Controller::Hooks3 before, this might look somewhat


familiar. However, if you haven’t, no worries.
We can start with a simple example to help clarify things, then take a look at the full
report.

require "lib/init"
class Vacation < Ruport::Report

renders_as_table

def renderable_data(format)
Table(%w[a b c]) << [1,2,3] << [4,5,6]
end

end

Vacation.generate do |report|
report.save_as "output/vacation.csv"
report.send_to("gregory.t.brown@gmail.com") do |m|
m.subject = "Vacation Conflicts Report"
m.attach "output/vacation.csv"
end
end

As you can see here, the Ruport::Report class just provides some helper methods to
make producing reports a little easier.
The first thing to notice is our renders as table call. What we are telling our report
object here is that it will pass the results of renderable data to Ruport’s table
controller. This means it expects the return value to be a Ruport::Data::Table, or at
least duck type as one.
Here we just use the defacto-standard Ruport junk data to show it’s hooked up:

def renderable_data(format)
Table(%w[a b c]) << [1,2,3] << [4,5,6]
end
3 See the Custom Controller Logic cheatsheet.

59
If we wanted to use a different controller, there are macros defined for those as well:

• renders as row
• renders as group
• renders as grouping
• renders as graph (via ruport-util)

You can also use any controller you’d like, via the renders with() command. In the
spirit of keeping things simple, we’ll stick with table rendering for now.
The final bit of this simplified report is the code that actually generates and emails the
CSV:

Vacation.generate do |report|
report.save_as "output/vacation.csv"
report.send_to("gregory.t.brown@gmail.com") do |m|
m.subject = "Vacation Conflicts Report"
m.attach "output/vacation.csv"
end
end

As you’ve seen in previous examples, Ruport is capable of inferring which formatter to


use based on the file name you give it. The code here simply tells Ruport to render a
CSV from the results of renderable data, and then email it as an attachment to the
address specified.
If we wanted to do some format-specific hackery, we could use the format argument
passed to renderable data which in this case would evaluate to :csv. Though this can
be handy, we won’t need it here, so we’ll leave that as an exercise to the reader.
If you wanted to try this report out, it’s already functional, and you can run it via rake:

$ rake run report=vacation

This will produce a file in the output folder with a rather trivial CSV:

a,b,c
1,2,3
4,5,6

This file will also be emailed to the address we specified to send to().
If you’ve understood the code so far, you’ll have no trouble understanding the real report,
which just adds some business logic and database access on top of this basic idea of
report objects. Let’s take a look at it as a whole and then break it down:

60
lib/nightly reports/lib/reports/vacation.rb

require "lib/init"
class Vacation < Ruport::Report

renders_as_table

def renderable_data(format)
Table("Employee", "Conflicted Date") do |csv_out|
payr_data.each do |employee_id, data|
times = scheduling_data_for_employee(employee_id)
conflicts = data.select { |r| times.include?(r.date) }
conflicts.each { |r| csv_out << [r["employee.name"], r["date"]] }
end
end
end

def payr_data
Grouping(
OtherTime.report_table(:all,
:conditions => ["category = ’Vacation’ and date between ? and ?",
1.day.from_now, 7.days.from_now],
:include => { :employee => { :methods => "name" } },
:transforms => lambda { |r| r["date"] = r["date"].to_date } ),
:by => "employee.employee_id")
end

def scheduling_data_for_employee(eid)
result = query "select provider_id, appointment_time from appointments where
provider_id = ? and appointment_time between ? and ?",
:params => [eid,1.day.from_now, 7.days.from_now]

result.map { |e| e["appointment_time"].to_date }


end

end

Vacation.generate do |report|
report.save_as "output/vacation_conflicts.csv"
report.send_to "gregory.t.brown@gmail.com" do |m|
m.subject = "Vacation Conflicts"
m.attach "output/vacation_conflicts.csv"
end
end

61
Working from the high level down, let’s take a look at what our renderable data code is
doing:

def renderable_data(format)
Table("Employee", "Conflicted Date") do |csv_out|
payr_data.each do |employee_id, data|
times = scheduling_data_for_employee(employee_id)
conflicts = data.select { |r| times.include?(r.date) }
conflicts.each { |r| csv_out << [r["employee.name"], r["date"]] }
end
end
end

You can see right away this is just producing a two field table and returning it:

Table("Employee", "Conflicted Date") do |csv_out|


# ...
end

This approach of building up a table within a block is mostly just syntactic sugar,
allowing us to never explicitly assign our table object to a variable, since it is immediately
passed to the controller.
When we look at the code that’s actually populating the table, we find that it’s pretty
simple:

payr_data.each do |employee_id, data|


times = scheduling_data_for_employee(employee_id)
conflicts = data.select { |r| times.include?(r.date) }
conflicts.each { |r| csv_out << [r["employee.name"], r["date"]] }
end

The payr data helper returns a grouping object, which is basically vacation time records
grouped by employee id. For each employee, we pull a list of appointment dates via the
scheduling data for employee helper.
We then select any vacation times which conflict with these appointments, and add a row
with the employee name and the vacation date to the table. If this seems somewhat
simple, you won’t find the actual database interaction code much harder.
Let’s take a look at our ActiveRecord interactions first, which we find in payr data.

62
def payr_data
Grouping(
OtherTime.report_table(:all,
:conditions => ["category = ’Vacation’ and date between ? and ?",
1.day.from_now, 7.days.from_now],
:include => { :employee => { :methods => "name" } },
:transforms => lambda { |r| r["date"] = r["date"].to_date } ),
:by => "employee.employee_id")
end

Because this is an ad-hoc report, we are coding a little quicker and dirtier than we might
if it were a full scale application. In the code above, the first thing to notice is that it is
simply doing a grouping by the employee id field.

Grouping(some_table, :by => "employee.employee_id")

In this context, the some table data is just results from a vanilla report table call via
Ruport’s acts as reportable.

OtherTime.report_table(:all,
:conditions => ["category = ’Vacation’ and date between ? and ?",
1.day.from_now, 7.days.from_now],
:include => { :employee => { :methods => "name" } },
:transforms => lambda { |r| r["date"] = r["date"].to_date }

Here we are just pulling vacation times within a certain date range, asking for it to
include the employee association information, and doing a quick transformation from
datetime objects to dates, which is necessary to make comparisons later.
From here, you end up with a simple grouping of vacation times by employee id that
include the rest of an employee’s information. We can now look at the SQL we use to
interact with our MySQL database which is not part of the PayR application.

def scheduling_data_for_employee(eid)
result = query "select appointment_time from appointments where
provider_id = ? and appointment_time between ? and ?",
:params => [eid,1.day.from_now, 7.days.from_now]

result.map { |e| e["appointment_time"].to_date }


end

This code is even easier than the PayR data code. It simply looks up appointment times
for a given time period and employee id (called provider id in our MySQL database).
It’s worth noting that if the query became larger than a couple lines, Ruport would
understand query "path/to/my file.sql", among other things. For our needs however,
this is more than enough.

63
When we run this report, we end up with a barebones CSV that shows our conflicting
Vacation dates. Output will look something like this:

Employee,Conflicted Date
Gregory Brown,2007-11-29
Gregory Gibson,2007-12-01
Gregory Gibson,2007-12-02
Joe Loop,2007-12-03

We can of course change this output to HTML, PDF, or Text by changing exactly two
lines:

Vacation.generate do |report|
report.save_as "output/vacation_conflicts.txt"
report.send_to "gregory.t.brown@gmail.com" do |m|
m.subject = "Vacation Conflicts"
m.attach "output/vacation_conflicts.txt"
end
end

This is one of the many nice things about working with Ruport as opposed to writing
one-off scripts that you soon need to throw away rather than build upon.
Now all that remains for this report is to use your favorite scheduling software, perhaps
cron, to fire this on a nightly basis. Though we won’t discuss it here, it’s usually as
simple as just running the Rakefile with any necessary environment variables.
Hopefully this gives you a taste of how rope applications work, and how you can sneak
them in to live alongside Rails apps. Some people love this kind of stuff; others prefer to
keep tight integration in their Rails app and wouldn’t want to layer something like this
into their projects. We really leave that decision up to you, by design.
When you’re using a rope-based Ruport application, it’s easy to re-use configuration
information, make reports build off of each other, and also keep your reporting code
cleanly separated from the rest of your application. If you end up with situations where
those are important, you might consider using some of the techniques shown here.

5.3 And That’s the End of That Chapter


With this, we come to the end of our discussion of PayR. We encourage you to poke
around in the source files, because we haven’t covered absolutely every use of Ruport,
probably not even most of the uses. However, we hope we’ve given you a strong sense of
not just the syntax and raw functionality pertaining to Ruport, but also how we develop
applications using it.

64
Though we’ll happily admit there are a lot of valid approaches to using Ruport, we think
that the toolset best reflects how we are using it. If you’ve gained some insight from this
discussion about that, we think you’ll find your work a whole lot easier.
The remainder of the book is a collection of more nuts-and-bolts material.
We’ve compiled several cheatsheets about common Ruport topics, which are designed to
be both a quick reference guide and a way to explore features you have not yet worked
with. We hope that combined with the detailed explainations of PayR, these cheatsheets
will provide a solid base for using Ruport in your day to day work.

65
66
Part III

Cheatsheets

67
Chapter 6

Data Manipulations

A powerful aspect of Ruport is that it allows you to work with a small set of core data
structures which can be populated from a number of different sources. It is possible to do
most common data manipulations without much effort. Here we’ll cover how to do sorting
and searching, summation, averages, and tabular column operations such as calculated
fields. We’ll also look at some of Ruport’s more advanced data summarization tools.

6.1 Sorting Tables


If you’re used to Enumerable#sort by, you’ll have no problem sorting Ruport’s Table
objects. Below is a sample of the most elementary uses of table sorting:

>> puts t
+--------------------------------------------------------------------------+
| id | name | phone | street | town | state |
+--------------------------------------------------------------------------+
| 1 | Inky | 555-000-1234 | Druary Lane | Union City | CT |
| 2 | Blinky | 525-052-9123 | Apple Street | Robot Town | NJ |
| 3 | Clyde | 247-219-4820 | Sandbox Hill | Alvin’s Landing | PA |
| 4 | Pacman | 283-102-8293 | Rat Avenue | Southford | VT |
| 5 | Mrs. Pacman | 214-892-1892 | Conch Walk | New York | NY |
+--------------------------------------------------------------------------+

69
>> puts t.sort_rows_by { |r| r.name }
+--------------------------------------------------------------------------+
| id | name | phone | street | town | state |
+--------------------------------------------------------------------------+
| 2 | Blinky | 525-052-9123 | Apple Street | Robot Town | NJ |
| 3 | Clyde | 247-219-4820 | Sandbox Hill | Alvin’s Landing | PA |
| 1 | Inky | 555-000-1234 | Druary Lane | Union City | CT |
| 5 | Mrs. Pacman | 214-892-1892 | Conch Walk | New York | NY |
| 4 | Pacman | 283-102-8293 | Rat Avenue | Southford | VT |
+--------------------------------------------------------------------------+

>> puts t.sort_rows_by("name")


+--------------------------------------------------------------------------+
| id | name | phone | street | town | state |
+--------------------------------------------------------------------------+
| 2 | Blinky | 525-0529-123 | Apple Street | Robot Town | NJ |
| 3 | Clyde | 247-219-4820 | Sandbox Hill | Alvin’s Landing | PA |
| 1 | Inky | 555-000-1234 | Druary Lane | Union City | CT |
| 5 | Mrs. Pacman | 214-892-1892 | Conch Walk | New York | NY |
| 4 | Pacman | 283-102-8293 | Rat Avenue | Southford | VT |
+--------------------------------------------------------------------------+
=> nil
>> puts t.sort_rows_by("name", :order => :descending)
+--------------------------------------------------------------------------+
| id | name | phone | street | town | state |
+--------------------------------------------------------------------------+
| 4 | Pacman | 283-102-8293 | Rat Avenue | Southford | VT |
| 5 | Mrs. Pacman | 214-892-1892 | Conch Walk | New York | NY |
| 1 | Inky | 555-000-1234 | Druary Lane | Union City | CT |
| 3 | Clyde | 247-219-4820 | Sandbox Hill | Alvin’s Landing | PA |
| 2 | Blinky | 525-0529-123 | Apple Street | Robot Town | NJ |
+--------------------------------------------------------------------------+

You can also sort by multiple columns, if needed:

>> puts a
+-----------+
| a | b | c |
+-----------+
| 1 | 2 | 3 |
| 1 | 4 | 5 |
| 1 | 3 | 9 |
| 2 | 1 | 7 |
| 0 | 1 | 9 |
+-----------+

70
>> puts a.sort_rows_by(["a","b"])
+-----------+
| a | b | c |
+-----------+
| 0 | 1 | 9 |
| 1 | 2 | 3 |
| 1 | 3 | 9 |
| 1 | 4 | 5 |
| 2 | 1 | 7 |
+-----------+

NOTE: When specifying column names as a parameter, you can be sure that the sort
will be stable, e.g. the order of the records will be preserved in the event of a sorting ‘tie’.
When you use the block form, you are responsible for ensuring that the sort has a
stabilizer.

6.2 Sorting Groupings

Ruport also includes support for sorting of groupings. In the simplest case, you can order
a Grouping by the group names by setting the :order option to a value of :name.

>> puts a
+-----------+
| a | b | c |
+-----------+
| 1 | 2 | 3 |
| 1 | 4 | 5 |
| 1 | 3 | 9 |
| 2 | 1 | 7 |
| 0 | 1 | 9 |
+-----------+

>> g = Grouping(a, :by => "a", :order => :name)


>> g.to_a.map {|name,group| name }
=> [0, 1, 2]

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

>> g = Grouping(a, :by => "a", :order => lambda {|g| g.size })
>> g.to_a.map {|name,group| name }
=> [0, 2, 1]

71
You can also sort the groupings after they have been created using the sort grouping by
(which returns a new sorted grouping) and sort grouping by! (which sorts the existing
grouping) methods.

>> g = Grouping(a, :by => "a")


>> sorted = g.sort_grouping_by {|g| g.size }
>> g.sort_grouping_by!(:name)

6.3 Searching Rows in a Table


In addition to Enumerable’s find/select, Ruport offers Table#rows with. For many
common searching operations, this comes in handy.

> puts t
+-----------+
| a | b | c |
+-----------+
| 1 | 2 | 3 |
| 7 | 3 | 1 |
| 2 | 2 | 3 |
| 7 | 6 | 9 |
+-----------+

>> t.rows_with_a(7).length
=> 2

>> t.rows_with(:c => 3, :a => 1).length


=> 1

>> t.rows_with([:a, :c]) { |a,c| a > 5 && c < 5 }.length


=> 1

>> t.rows_with([:a, :c]) { |a,c| a > 5 && c < 5 }[0].to_a


=> [7, 3, 1]

6.3.1 Custom Searches


Table#rows with also works with custom Record classes, allowing you to use methods as
searching criteria.

72
class Person < Ruport::Data::Record

def name
first_name + " " + last_name
end

end

t = Table(:column_names => %w[first_name last_name email],


:record_class => Person)
t << %w[Gregory Brown a@a.com]
t << %w[Joe Loop b@b.com]
t << %w[Alfonzo Stevens c@d.com]
t << %w[Gregory Brown f@f.com]

puts t.rows_with_name("Gregory Brown").length


=> 2
puts t.rows_with(:name => "Gregory Brown", :email => "a@a.com").length
=> 1

6.4 Sums and Averages


Basic sums are simple in Ruport via Table#sigma (aliased as sum). Both by-column and
by-block sums are supported.

>> a = Table(%w[a b c])


>> a << [1,9,1.7]
>> a << ["2","13","12.1"]
>> a << [5,1,6]

>> a.sigma("a")
=> 8

>> a.sigma("c")
=> 19.8

>> a.sigma { |r| r.a.to_i + r.c.to_f }


=> 27.8

You’ll notice that summing by column automatically coerces strings to their numeric
types, while the block form does not. If you want to avoid implicit conversion, you should
use the block form for summations.
Basic averages might be computed as follows:

73
average_cost = table.sum("cost") / table.length

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

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

6.5 Tabular Column Operations and Calculated Fields


Ruport offers a whole bunch of column operations to make life easier when manipulating
Table data.
Starting with this data:

>> puts a
+--------+
| a | b |
+--------+
| 1 | 9 |
| 2 | 13 |
| 5 | 1 |
| 5 | 1 |
+--------+

Adding a calculated column:

>> a.add_column("c") { |r| r.a + r.b }

>> puts a
+-------------+
| a | b | c |
+-------------+
| 1 | 9 | 10 |
| 2 | 13 | 15 |
| 5 | 1 | 6 |
| 5 | 1 | 6 |
+-------------+

74
Adding a fancily calculated column:

>> a.add_column("a1",:before => "b", :default => 0) do |r|


>> r.a + 1 if r.b > 10
>> end
>> puts a
+------------------+
| a | a1 | b | c |
+------------------+
| 1 | 0 | 9 | 10 |
| 2 | 3 | 13 | 15 |
| 5 | 0 | 1 | 6 |
| 5 | 0 | 1 | 6 |
+------------------+

Doing a column replacement:

>> a.replace_column("b") { |r| r.b.to_f }


>> puts a
+--------------------+
| a | a1 | b | c |
+--------------------+
| 1 | 0 | 9.0 | 10 |
| 2 | 3 | 13.0 | 15 |
| 5 | 0 | 1.0 | 6 |
| 5 | 0 | 1.0 | 6 |
+--------------------+

Renaming columns:

>> a.rename_columns { |c| "Col: #{c}" }


>> puts a
+------------------------------------+
| Col: a | Col: a1 | Col: b | Col: c |
+------------------------------------+
| 1 | 0 | 9.0 | 10 |
| 2 | 3 | 13.0 | 15 |
| 5 | 0 | 1.0 | 6 |
| 5 | 0 | 1.0 | 6 |
+------------------------------------+

75
Building a new table based on a pivot:

>> puts t.pivot("Col: a1", :group_by => "Col: a", :values => "Col: b" )
+---------------------+
| Col: a | 0 | 3 |
+---------------------+
| 1 | 9.0 | |
| 2 | | 12.0 |
| 5 | 1.0 | |
+---------------------+

Building a sub table:

>> c = a.column_names.grep(/a/)
=> ["Col: a", "Col: a1"]
>> puts a.sub_table(c) { |r| r[0] < 5 }
+------------------+
| Col: a | Col: a1 |
+------------------+
| 1 | 0 |
| 2 | 3 |
+------------------+

6.6 Filtering and Transforming Data


If you want to constrain data as it is being aggregated, rather than after it has all been
collected, Data::Feeder provides a simple proxy object that allows you 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]
>> puts t
+-----------+
| a | b | c |
+-----------+
| 1 | 2 | 3 |
| 9 | 6 | 1 |
| 2 | 1 | 7 |
+-----------+

You can set up filters to work on an initial data set via the Table constructor.

76
>> 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 })
>> puts t
+-----------+
| a | b | c |
+-----------+
| 1 | 2 | 3 |
| 9 | 6 | 1 |
| 2 | 1 | 7 |
+-----------+

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
>> puts t
+-----------+
| a | b | c |
+-----------+
| 1 | 2 | 3 |
| 9 | 6 | 1 |
| 2 | 1 | 7 |
+-----------+

Feeders also provide a way to do transformations on your data as it is being collected.

>> t = Table(%w[a b c], :data => [[1,2,3],[9,6,1],[11,3,2],[2,1,7]],


:transforms => lambda {|r| r.a = "a: #{r.a}" })
>> puts t
+---------------+
| a | b | c |
+---------------+
| a: 1 | 2 | 3 |
| a: 9 | 6 | 1 |
| a: 11 | 3 | 2 |
| a: 2 | 1 | 7 |
+---------------+

77
6.7 Summarizing Grouped Data
Grouping data by a certain criteria and then making some calculations on the grouped
data is a common operation. Often these problems take the form of “for each person, give
me the sum of all ‘a’ associated with them, and the average of all ‘b’ ”.
The following example shows how to use Grouping#summary to generate this kind of
report as a Table from a Grouping object.

t = Table(%w[name a b])
t << ["foo",10,20]
t << ["foo",15,25]
t << ["bar",5,10]
t << ["apple",3,10]
t << ["bar",19,7]

g = Grouping(t,:by => ’name’)

s = g.summary(:name, :a => lambda { |g| g.sigma("a") },


:b_avg => lambda { |g| g.sigma("b").to_f / g.length },
:order => [:name,:a,:b_avg] )
>> puts s
+--------------------+
| name | a | b_avg |
+--------------------+
| apple | 3 | 10.0 |
| foo | 25 | 22.5 |
| bar | 24 | 8.5 |
+--------------------+

6.8 Multilevel Grouping


You can create multilevel groupings by providing an array of column names when the
grouping is created.

>> table = Table(%w[a b c d e], :data => [[1,2,3,4,5],[3,4,5,6,7],


[1,1,4,5,6],[3,2,4,5,6],[1,2,5,3,2],[1,2,8,9,0]])

>> grouping = Grouping(table, :by => ["a","b"])

78
>> puts grouping
1:

+---------------+
| b | c | d | e |
+---------------+
| 2 | 3 | 4 | 5 |
| 1 | 4 | 5 | 6 |
| 2 | 5 | 3 | 2 |
| 2 | 8 | 9 | 0 |
+---------------+

3:

+---------------+
| b | c | d | e |
+---------------+
| 4 | 5 | 6 | 7 |
| 2 | 4 | 5 | 6 |
+---------------+

You can see that Ruport’s built-in formatter only shows the output from the first level of
the resultant grouping. Since the output for multilevel grouping tends to be
implementation-specific, you’ll need to create your own formatter if you want to design
such a report.
However, Ruport does provide several methods to get at the underlying data in the
grouping. You can access each of the groups in the grouping using [] with the name of
the group.

>> puts grouping[1]


1:

+---------------+
| b | c | d | e |
+---------------+
| 2 | 3 | 4 | 5 |
| 1 | 4 | 5 | 6 |
| 2 | 5 | 3 | 2 |
| 2 | 8 | 9 | 0 |
+---------------+

You can access the next level of each grouping with the subgrouping method (aliased as
/) and the name of the group.

>> sub = grouping.subgrouping(1)

79
>> sub = grouping / 1
>> puts sub
1:

+-----------+
| c | d | e |
+-----------+
| 4 | 5 | 6 |
+-----------+

2:

+-----------+
| c | d | e |
+-----------+
| 3 | 4 | 5 |
| 5 | 3 | 2 |
| 8 | 9 | 0 |
+-----------+

The subgrouping method returns a grouping, so you can in turn access the groups in the
newly created grouping using [] with the name of the group.

>> puts sub[2]

2:

+-----------+
| c | d | e |
+-----------+
| 3 | 4 | 5 |
| 5 | 3 | 2 |
| 8 | 9 | 0 |
+-----------+

In this manner, you should be able to traverse the levels of your grouping to get its data.
Formatting it for your report, however, is left to you.

6.9 Related Resources / Digging Deeper


All operations shown here that work on Table objects will work on Group objects (but
not necessarily on Grouping objects).

80
Chapter 7

Using acts as reportable

Ruport’s acts as reportable module provides support for using ActiveRecord for data
collection. You can use it to get a Ruport::Data::Table from an ActiveRecord model.
This cheatsheet covers the basic functionality of acts as reportable and some common use
cases.

7.1 Loading
When Ruport is loaded, it tries to automatically load acts as reportable as well. In order
for that to happen, however, there are two prerequisites: acts as reportable must be
installed and ActiveRecord must be installed and loaded. Otherwise, you can load
acts as reportable manually when you need it by calling require
’ruport/acts as reportable’.

7.2 Basic Usage


You hook up your ActiveRecord model to acts as reportable using the
acts as reportable class method.

class Book < ActiveRecord::Base


acts_as_reportable
belongs_to :author

def author_name
author.name
end
end

81
class Author < ActiveRecord::Base
acts_as_reportable
has_many :books
end

You can get a Ruport table using the report table method.

>> puts Book.report_table


+----------------------------------------------------+
| title | id | author_id |
+----------------------------------------------------+
| Why’s (Poignant) Guide to Ruby | 1 | 1 |
| Flow My Tears, The Policeman Said | 2 | 2 |
| Farenheit 451 | 3 | 3 |
| Gravity’s Rainbow | 4 | 4 |
+----------------------------------------------------+

You can use the :only option to specify only certain columns be returned. Note that
although the columns in the tables returned by acts as reportable are generally
unordered, use of the :only option will set the columns to be returned in the order they
are listed in the option’s array. Therefore, the :only option has a secondary function: in
addition to reducing the population of columns returned, it will also order the columns.

>> puts Book.report_table(:all, :only => [:id, :title])


+----------------------------------------+
| id | title |
+----------------------------------------+
| 1 | Why’s (Poignant) Guide to Ruby |
| 2 | Flow My Tears, The Policeman Said |
| 3 | Farenheit 451 |
| 4 | Gravity’s Rainbow |
+----------------------------------------+

You can use the :except option to specify certain columns to not be returned.

>> puts Book.report_table(:all, :except => :author_id)


+----------------------------------------+
| title | id |
+----------------------------------------+
| Why’s (Poignant) Guide to Ruby | 1 |
| Flow My Tears, The Policeman Said | 2 |
| Farenheit 451 | 3 |
| Gravity’s Rainbow | 4 |
+----------------------------------------+

You can use the :methods option to return the result of calling a method for each row.

82
>> puts Book.report_table(:all, :methods => :author_name)
+---------------------------------------------------------------------+
| author_name | title | id | author_id |
+---------------------------------------------------------------------+
| _why | Why’s (Poignant) Guide to Ruby | 1 | 1 |
| Philip K. Dick | Flow My Tears, The Policeman Said | 2 | 2 |
| Ray Bradbury | Farenheit 451 | 3 | 3 |
| Thomas Pynchon | Gravity’s Rainbow | 4 | 4 |
+---------------------------------------------------------------------+

You can use the :include option to include associated models.

>> puts Book.report_table(:all, :include => :author)


+----------------------------------------------------------------------------->>
| title | id | author_id | author.id | author.nam>>
+----------------------------------------------------------------------------->>
| Why’s (Poignant) Guide to Ruby | 1 | 1 | 1 | _why >>
| Flow My Tears, The Policeman Said | 2 | 2 | 2 | Philip K. D>>
| Farenheit 451 | 3 | 3 | 3 | Ray Bradbur>>
| Gravity’s Rainbow | 4 | 4 | 4 | Thomas Pync>>
+----------------------------------------------------------------------------->>

The options can be combined and all of the same options can be passed to any included
associations, using a hash.

>> puts Book.report_table(:all, :only => :title,


:include => { :author => { :only => :name } })
+----------------------------------------------------+
| title | author.name |
+----------------------------------------------------+
| Why’s (Poignant) Guide to Ruby | _why |
| Flow My Tears, The Policeman Said | Philip K. Dick |
| Farenheit 451 | Ray Bradbury |
| Gravity’s Rainbow | Thomas Pynchon |
+----------------------------------------------------+

>> puts Book.report_table(:all, :only => [:title], :methods => [:author_name])


+----------------------------------------------------+
| title | author_name |
+----------------------------------------------------+
| Why’s (Poignant) Guide to Ruby | _why |
| Flow My Tears, The Policeman Said | Philip K. Dick |
| Farenheit 451 | Ray Bradbury |
| Gravity’s Rainbow | Thomas Pynchon |
+----------------------------------------------------+

83
Any options that acts as reportable doesn’t recognize will be passed along to the
ActiveRecord find method, so you can use all of the options allowed by find.

>> puts Book.report_table(:all, :only => :title,


:include => { :author => { :only => :name } },
:order => "authors.name")
+----------------------------------------------------+
| title | author.name |
+----------------------------------------------------+
| Flow My Tears, The Policeman Said | Philip K. Dick |
| Farenheit 451 | Ray Bradbury |
| Gravity’s Rainbow | Thomas Pynchon |
| Why’s (Poignant) Guide to Ruby | _why |
+----------------------------------------------------+

>> puts Book.report_table(:all, :only => :title,


:include => { :author => { :only => :name } },
:conditions => "authors.name like ’_why’")
+----------------------------------------------+
| title | author.name |
+----------------------------------------------+
| Why’s (Poignant) Guide to Ruby | _why |
+----------------------------------------------+

Note that acts as reportable uses the association name to specify included models and to
qualify any attributes returned from those models, but when you include SQL in your
query, you need to use the table names. Thus, the above example uses :include =>
:author but :conditions => "authors.name...".

7.3 Filtering and Transforming Data


You can use the :filters and :transforms methods to limit and/or modify the data
that is returned in the table. You can pass a Proc or array of Procs to each of these
options.

The :filters option, as its name implies, allows you to filter the data that will make up
the table.

84
>> puts Book.report_table(:all, :only => [:id, :title],
:filters => lambda {|r| r["id"] > 1 })
+----------------------------------------+
| id | title |
+----------------------------------------+
| 2 | Flow My Tears, The Policeman Said |
| 3 | Farenheit 451 |
| 4 | Gravity’s Rainbow |
+----------------------------------------+

The :transforms option can be used to perform transformations on the data being
supplied to the table.

>> puts Book.report_table(:all, :only => [:id, :title],


:transforms => lambda {|r| r["id"] = "#{Author.find(r["id"]).name}" })
+----------------------------------------------------+
| id | title |
+----------------------------------------------------+
| _why | Why’s (Poignant) Guide to Ruby |
| Philip K. Dick | Flow My Tears, The Policeman Said |
| Ray Bradbury | Farenheit 451 |
| Thomas Pynchon | Gravity’s Rainbow |
+----------------------------------------------------+

7.4 Eager Loading of Data


By default, acts as reportable will pass any :include options to the ActiveRecord find
method that is used behind the scenes to collect your data. However, if you want to turn
off eager loading, you can do so with the :eager loading option by setting it to a value
of false.

>> puts Book.report_table(:all, :include => :author, :eager_loading => false)

7.5 Setting Default Options


The acts as reportable class method also takes all of the same options as the
report table method, allowing you to set default options for your reports.

class Book < ActiveRecord::Base


acts_as_reportable :except => [:id, :author_id]
belongs_to :author
end

85
>> puts Book.report_table
+-----------------------------------+
| title |
+-----------------------------------+
| Why’s (Poignant) Guide to Ruby |
| Flow My Tears, The Policeman Said |
| Farenheit 451 |
| Gravity’s Rainbow |
+-----------------------------------+

However, this only works if you don’t use any of the options that acts as reportable
recognizes. Any of the four options (:only, :except, :methods, and :include) passed to
the report table method will disable the default options passed to the class method.

>> puts Book.report_table(:all, :methods => :author_name)


+---------------------------------------------------------------------+
| author_name | title | id | author_id |
+---------------------------------------------------------------------+
| _why | Why’s (Poignant) Guide to Ruby | 1 | 1 |
| Philip K. Dick | Flow My Tears, The Policeman Said | 2 | 2 |
| Ray Bradbury | Farenheit 451 | 3 | 3 |
| Thomas Pynchon | Gravity’s Rainbow | 4 | 4 |
+---------------------------------------------------------------------+

You can still use the ActiveRecord options.

>> puts Book.report_table(:all, :conditions => "title like ’%Poignant%’")


+--------------------------------+
| title |
+--------------------------------+
| Why’s (Poignant) Guide to Ruby |
+--------------------------------+

You can access the options you passed to the acts as reportable class method with the
aar options attribute.

>> puts Book.report_table(:all, Book.aar_options.merge(:methods => :author_name))


+----------------------------------------------------+
| author_name | title |
+----------------------------------------------------+
| _why | Why’s (Poignant) Guide to Ruby |
| Philip K. Dick | Flow My Tears, The Policeman Said |
| Ray Bradbury | Farenheit 451 |
| Thomas Pynchon | Gravity’s Rainbow |
+----------------------------------------------------+

86
7.6 Find by SQL
Analogous to the ActiveRecord find by sql method, acts as reportable provides a
report table by sql method.

>> puts Book.report_table_by_sql("SELECT * FROM books")


+-----------------------------------+
| title |
+-----------------------------------+
| Why’s (Poignant) Guide to Ruby |
| Flow My Tears, The Policeman Said |
| Farenheit 451 |
| Gravity’s Rainbow |
+-----------------------------------+

7.7 Related Resources / Digging Deeper


The report table and report table by sql methods return Ruport::Data::Table
objects, so you can use the returned tables just as you would if you created them by any
other method.
Ruport::Query1 provides a raw SQL mechanism if you need legacy integration or wish to
use existing queries without mapping to ActiveRecord.

1 See the Ruport::Query cheatsheet.

87
88
Chapter 8

Using Ruport::Query

Ruport’s Query module provides support for using Ruby DBI for data collection. It
allows you to connect to any of the databases supported by DBI and execute arbitrary
SQL queries. You can then package the results into a Ruport Data::Table or obtain raw
DBI::Row data. In addition to providing the interface to DBI, Ruport::Query also
provides assistance with configuration of data sources as well as some methods for
traversing the data. This cheatsheet covers the basic functionality of Query and some
common use cases.

8.1 Configuration
Ruport::Query provides methods for storing configuration information for later use. You
can add a data source by using the add source method. The first parameter names the
data source and the second parameter is a hash that defines the connection using the
:dsn, :user, and :password options.

Ruport::Query.add_source :default,
:dsn => "dbi:mysql:my_db",
:user => "mike",
:password => "chunkybacon"

Ruport::Query.add_source :test,
:dsn => "dbi:mysql:other_db",
:user => "tester",
:password => "blinky"

The default data source (named :default) will be used by any queries that don’t either
specify data source parameters or reference another named data source. You can retrieve
all of the available named sources using the sources method, which returns them in a
hash keyed by the source names.

89
Ruport::Query.sources

You can retrieve the default data source if it’s defined, using the default source method.

Ruport::Query.default_source

8.2 Constructing the Query


You construct a query object using the Ruport::Query constructor. As mentioned
earlier, if you don’t specify any connection parameters, Ruport will attempt to use the
default data source (or error if there is no default defined). You can get a Ruport table
containing the results of running the query using the result method.

>> query = Ruport::Query.new("SELECT * FROM books")


>> puts query.result
+----------------------------------------------------+
| id | title | author_id |
+----------------------------------------------------+
| 1 | Why’s (Poignant) Guide to Ruby | 1 |
| 2 | Flow My Tears, The Policeman Said | 2 |
| 3 | Farenheit 451 | 3 |
| 4 | Gravity’s Rainbow | 4 |
+----------------------------------------------------+

If you want to use a different named data source, you can specify it using the :source
option to the constructor.

>> query = Ruport::Query.new("SELECT * FROM books", :source => :my_source)

If you haven’t set up your data source parameters as a named source, you can also specify
the parameters directly to the constructor.

>> query = Ruport::Query.new("SELECT * FROM books", :dsn => "dbi:mysql:my_db",


:user => "mike", :password => "chunkybacon")

You can also construct your query using SQL stored in a file. If the filename ends with
“.sql”, you can simply pass the filename to the constructor. Otherwise, you need to use
the :file option with the filename.

>> query1 = Ruport::Query.new("my_query.sql")


>> query2 = Ruport::Query.new(:file => "other_query")

90
If you don’t want your data to be packaged into a Ruport::Data::Table, but would
rather have raw DBI::Row objects, then you can use the :row type option to the
constructor with the value of :raw.

>> query = Ruport::Query.new("SELECT * FROM books", :row_type => :raw)

8.3 Using the Query


Once you have a query object, you can begin to use it to obtain results. As demonstrated
previously, you can use the result method to get a Ruport::Data::Table containing the
results of executing the query.

>> query = Ruport::Query.new("SELECT authors.name, books.title FROM


authors JOIN books ON authors.id = books.author_id")
>> puts query.result

+----------------------------------------------------+
| name | title |
+----------------------------------------------------+
| _why | Why’s (Poignant) Guide to Ruby |
| Philip K. Dick | Flow My Tears, The Policeman Said |
| Ray Bradbury | Farenheit 451 |
| Thomas Pynchon | Gravity’s Rainbow |
+----------------------------------------------------+

If you only want to execute a query and not return any results, then you can use the
execute method.

>> query = Ruport::Query.new("INSERT INTO authors (name)


VALUES (’James Joyce’)")
>> query.execute

You can obtain a CSV dump of the data returned by the query using the to csv method.

>> query = Ruport::Query.new("SELECT authors.name, books.title FROM


authors JOIN books ON authors.id = books.author_id")
>> puts query.to_csv

name,title
_why,Why’s (Poignant) Guide to Ruby
Philip K. Dick,"Flow My Tears, The Policeman Said"
Ray Bradbury,Farenheit 451
Thomas Pynchon,Gravity’s Rainbow

91
You can iterate through the result set, returning the rows one by one, using the each
method.

>> query = Ruport::Query.new("SELECT * FROM books WHERE


title = ’Farenheit 451’")
>> query.each {|row| puts row.class }
Ruport::Data::Record

You can also obtain a Generator object with the result set, using the generator method.

>> query = Ruport::Query.new("SELECT * FROM books WHERE


title = ’Farenheit 451’")
>> g = query.generator
>> while g.next?; puts g.next.class; end
Ruport::Data::Record

8.4 Related Resources / Digging Deeper


For a complete list of DSN configurations as well as installation instructions for working
with various databases, you’ll want to consult the RubyDBI documentation.
Ruport’s acts as reportable module provides similar data collection functionality using
ActiveRecord.1
The generator method returns a Generator object - you might want to read the Ruby
Standard Library API docs to see what you can do with it.

1 See the acts as reportable cheatsheet.

92
Chapter 9

Ruport’s Formatting System

A common need in reporting is to be able to display the same data in a number of


different formats. Ruport’s Formatting System provides a highly flexible way to separate
your rendering process from your specific formatting code. This cheatsheet shows how the
parts come together and the tools that are available to you.

9.1 Abstracting the Rendering Process


Building a custom controller allows you to define the steps which should be taken to
produce your reports, as well as the options that will be available to them.
The following simple example shows a fully functional controller:

class InvoiceController < Ruport::Controller

stage :company_header, :invoice_header, :invoice_body, :invoice_footer

required_option :employee_name, :employee_id

end

Without any more code, the above defines what our interface will look like. We know
that in order to render a given format, we’ll be writing something like this:

InvoiceController.render_some_format(:data => my_data,


:employee_name => "Samuel L. Jackson",
:employee_id => "e1337")

The block form that you may be familiar with from the standard Ruport controllers also
works as expected:

93
InvoiceController.render(:some_format) do |r|
r.data = my_data
r.options do |o|
o.employee_name = "Samuel L. Jackson"
o.employee_id = "e1337"
end
end

The power here of course is that even as new formats are added, our external interface
stays the same. We’ll now show how formatters are implemented, to give you an idea of
how stages work.

9.2 Using Formatters to Encapsulate Low Level code


A Formatter implements one or more labeled formats which are registered on one or more
Controller objects. Here we show a simple text formatter for the InvoiceController.

class Text < Ruport::Formatter::Text

renders :text, :for => InvoiceController

build :company_header do
output << "My Corp. Standard Report\n\n"
end

build :invoice_header do
output << "Invoice for #{options.employee_name} " <<
"(#{options.employee_id}), generated #{Date.today}\n\n"
end

build :invoice_body do
data.each do |r|
output << "#{r[:service].ljust(40)} | #{r[:rate].rjust(10)}\n"
end
end

build :invoice_footer do
output << "\n#{options.note}\n\n" if options.note
end
end

Looking back at the controller, you can see that stage :some stage gets translated to
build :some stage in the formatter. This is a convenience syntax for defining methods
with the name build some stage for each of the stages. Although the controller will

94
attempt to call all of these hooks, it will happily pass over any that are missing. This
means that if you did not want to include the company header in a given format, you
could just leave build :company header out of your formatter.
You’ll also notice that the options passed into the Controller can be accessed via the
options collection in the formatter.
You can also see that the formatter registers itself with the InvoiceController, via:

renders :text, :for => InvoiceController

The following chunk of code shows our Controller/Formatter pair in action.

data = Table(:service,:rate)
data << ["Mow The Lawn", "50.00"]
data << ["Sew Curtains", "120.00"]
data << ["Fly To Mars", "10000.00"]

puts InvoiceController.render_text(:data => data,


:employee_name => "Samuel L. Jackson",
:employee_id => "e1337",
:note => "END OF INVOICE")

Output:

My Corp. Standard Report

Invoice for Samuel L. Jackson (e1337), generated 2007-08-01

Mow The Lawn | 50.00


Sew Curtains | 120.00
Fly To Mars | 10000.00

END OF INVOICE

9.2.1 Adding Additional Formatters


The following definition adds XML format to our report.

class XML < Ruport::Formatter

renders :xml, :for => InvoiceController

95
def layout
output << "<invoice>\n"
yield
output << "</invoice>"
end

build :invoice_body do
add_employee_info

generate_data_rows

add_meta_data
end

def add_employee_info
output << "<employee name=’#{options.employee_name}’
id=’#{options.employee_id}’/>\n"
end

def generate_data_rows
data.each do |r|
output << "<item charge=’#{r[:rate]}’>#{r[:service]}</item>\n"
end
end

def add_meta_data
output << "<note>#{options.note}</note>\n<created>#{Date.today}</created>\n"
end
end

There are a few things which make this different from our text formatter, but at the heart
it is the same general idea.
You’ll notice that we use a layout for this code. This allows us to have some additional
control over the rendering process, allowing us to run some code before and after the
stages are executed. In this case, we’re simply wrapping the output in an < invoice > tag.
We’ve also only implemented one of the many stages the controller tries to call. This is
because we’re generating output for serialization, so we have no need for headers and
footers.
To generate XML instead of Text, you’ll notice it’s only a couple characters that need
changing:

puts InvoiceController.render_xml( :data => data,


:employee_name => "Samuel L. Jackson",
:employee_id => "e1337",
:note => "END OF INVOICE")

96
Output:

<invoice>
<employee name=’Samuel L. Jackson’ id=’e1337’/>
<item charge=’50.00’>Mow The Lawn</item>
<item charge=’120.00’>Sew Curtains</item>
<item charge=’10000.00’>Fly To Mars</item>
<note>END OF INVOICE</note>
<created>2007-08-01</created>
</invoice>

9.2.2 Syntactic Sugar For Single Use Formatters


If the formatters you have built will only be used by a single controller, Ruport has a
shortcut interface you can use.
Based on the examples above, we can build our simplified controller as such:

class InvoiceController < Ruport::Controller

class XML < Ruport::Formatter; end

stage :company_header, :invoice_header, :invoice_body, :invoice_footer

required_option :employee_name, :employee_id

formatter :text do
build :company_header do
output << "My Corp. Standard Report\n\n"
end

build :invoice_header do
output << "Invoice for #{options.employee_name} " <<
"(#{options.employee_id}), generated #{Date.today}\n\n"
end

build :invoice_body do
data.each do |r|
output << "#{r[:service].ljust(40)} | #{r[:rate].rjust(10)}\n"
end
end

build :invoice_footer do
output << "\n#{options.note}\n\n" if options.note
end
end

97
formatter :xml => XML do
def layout
output << "<invoice>\n"
yield
output << "</invoice>"
end

build :invoice_body do
add_employee_info

generate_data_rows

add_meta_data
end

def add_employee_info
output << "<employee name=’#{options.employee_name}’ "+
"id=’#{options.employee_id}’/>\n"
end

def generate_data_rows
data.each do |r|
output << "<item charge=’#{r[:rate]}’>#{r[:service]}</item>\n"
end
end

def add_meta_data
output << "<note>#{options.note}</note>\n"+
"<created>#{Date.today}</created>\n"
end
end
end

Let’s take a look at the form of the first formatter definition.

formatter :text do
# ...
end

In this simple form, Ruport knows to create an anonymous subclass of


Ruport::Formatter::Text and then register it with the current controller.
However, when we add a class that isn’t part of Ruport’s built in system, we need to give
it a little more help.

98
class XML < Ruport::Formatter; end

# ...

formatter :xml => XML do


# ...
end

Here we are telling the controller that when render xml is called, our subclass will be
based on the XML class we’ve stubbed out here. Keep in mind that you could use any class
constant here that points to a Ruport::Formatter subclass, it does not necessarily need
to be defined within the same namespace.
Though we won’t cover it here, it is possible to alter which formatter classes Ruport will
use as base classes for this anonymous formatter shortcut. Please see the
Controller.built in formats API documentation for details.

9.3 Custom Formatters for Ruport’s Standard


Controllers
Using the techniques from above, you can easily build an extension to our standard
controllers.
The following example adds XML support for our Table controller:

class XML < Ruport::Formatter

renders :xml, :for => Ruport::Controller::Table

def layout
output << "<table>\n"
yield
output << "</table>\n"
end

build :table_header do
output << "<header>\n"
output << build_row(data.column_names)
output << "</header>\n"
end

99
build :table_body do
output << "<body>\n"
data.each { |r| output << build_row(r) }
output << "</body>"
end

def build_row(row)
"<row>\n <cell>" <<
row.to_a.join("</cell><cell>") <<
"</cell>\n</row>\n"
end

end

This makes the formatter immediately available for use with Ruport’s Data::Table
structure.

data = Table(:service,:rate)
data << ["Mow The Lawn", "50.00"]
data << ["Sew Curtains", "120.00"]
data << ["Fly To Mars", "10000.00"]
puts data.to_xml

Output:

<table>
<header>
<row>
<cell>service</cell><cell>rate</cell>
</row>
</header>
<body>
<row>
<cell>Mow The Lawn</cell><cell>50.00</cell>
</row>
<row>
<cell>Sew Curtains</cell><cell>120.00</cell>
</row>
<row>
<cell>Fly To Mars</cell><cell>10000.00</cell>
</row>
</body>
</table>

Of course, you can and should use your favorite Ruby XML builder for any task more
interesting than this. Ruport happily wraps any third party library you’d like to use.

100
9.4 Using Templates
Templates 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 at the
time of rendering.
Define a template using the create method of Ruport::Formatter::Template.

Ruport::Formatter::Template.create(:simple) do |t|
t.note = "END OF INVOICE"
end

Ruport::Formatter::Template.create(:other) do |t|
t.note = "END"
end

Then define an apply template method in your formatter to tell it how to process the
template.

class Ruport::Formatter::Text

def apply_template
options.note = template.note
end

end

To use a particular template, specify it using the :template option when you render your
output.

data = Table(:service,:rate)
data << ["Mow The Lawn", "50.00"]
data << ["Sew Curtains", "120.00"]
data << ["Fly To Mars", "10000.00"]

puts InvoiceController.render_text(:data => data,


:employee_name => "Samuel L. Jackson",
:employee_id => "e1337",
:template => :simple)

puts InvoiceController.render_text(:data => data,


:employee_name => "Samuel L. Jackson",
:employee_id => "e1337",
:template => :other)

101
puts InvoiceController.render_text(:data => data,
:employee_name => "Samuel L. Jackson",
:employee_id => "e1337")

To derive one template from another, use the :base option to


Ruport::Formatter::Template.create.

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

Ruport has a standard template interface to each of the built-in formatters. This example
shows a number of the options being used.

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

format.text = {
:font_size => 16
}
format.table = {
:font_size => 16,
:show_headings => false
}
format.column = {
:alignment => :center,
:heading => { :justification => :right }
}
format.grouping = {
:style => :separated
}
end

9.5 Default Templates


Ruport takes the idea of templates one step further by allowing you to define a default
template that will be available to all of your formatters. You create one like any other
template, but give it the name :default.

Ruport::Formatter::Template.create(:default) do |format|
format.page = {
:size => "LETTER",
}

102
format.text = {
:font_size => 16
}
format.table = {
:font_size => 16,
:show_headings => true,
:width => 40
}
format.grouping = {
:style => :separated
}
end

The default template will then be used for any reports that you render without specifying
a template or that you render with : template => false. For example, assume we have
defined the default template shown above, as well as the :simple template defined in the
previous section.
This will render the report using the :default template.

puts InvoiceController.render_text(:data => data,


:employee_name => "Samuel L. Jackson",
:employee_id => "e1337")

This will render the report using the :simple template.

puts InvoiceController.render_text(:data => data,


:employee_name => "Samuel L. Jackson",
:employee_id => "e1337",
:template => :simple)

This will render the report without using a template.

puts InvoiceController.render_text(:data => data,


:employee_name => "Samuel L. Jackson",
:employee_id => "e1337",
:template => false)

You can retrieve the default template, if it exists, using the


Ruport::Formatter::Template.default method. You might use this to mix some of
the default and specific templates in your apply template method.

103
class Ruport::Formatter::Text

def apply_template
options.show_table_headers = template.table[:show_headings]
options.table_width = Ruport::Formatter::Template.default.table[:width]
end

end

9.6 Related Resources / Digging Deeper


What was shown here are simply the formatting system basics. You can actually do a
whole lot more as your tasks become more complicated.
If you’re looking for an easy way to normalize your data before it is formatted, or share
logic between formatters, see the custom controller logic cheatsheet.
If you’re looking to build printable reports in PDF format and would like to take
advantage of some of Ruport’s helpers, see the printable documents cheatsheet.
There are also some helpful examples in the integration hacks cheatsheet that show how
to quickly wrap existing code with Ruport’s formatting system.
The API docs for the controllers all list the hooks they implement and the options they
receive. This will be helpful if you are looking to extend our built-in controllers.
The API docs for Ruport::Formatter::Template list all of the options/values available
in the template interface to the built-in formatters.

104
Chapter 10

Building Custom Printable


Documents

The built in support for PDF output may get you where you need for simple reports, but
most of the time, your printable documents will need some customizations. This
cheatsheet is based on the most common questions our users have when building PDF
reports.

10.1 Displaying Multiple Tables in a Single PDF


Making a multi-table PDF is easy, you just need to build your own controller and
formatter. Every time we say that, people cringe with fear, but have a look at this
example to see that there is nothing to be afraid of:

class MultiTableController < Ruport::Controller

stage :multi_table_report

class PDF < Ruport::Formatter::PDF

renders :pdf, :for => MultiTableController

build :multi_table_report do
data.each { |table| pad(10) { draw_table(table) } }
render_pdf
end

end
end

105
t1 = Table(%w[a b c]) << [1,2,3] << [4,5,6]
t2 = Table(%w[a b c]) << [7,8,9] << [10,11,12]

pdf = MultiTableController.render_pdf(:data => [t1,t2])

The resulting PDF looks like this:

Using custom formatters opens up a whole lot of doors, as you’ll have access to the full
range of formatting helper functions Ruport offers.

10.2 Custom Headers with Logos


Often you’ll need to include a company logo, some formatted text, and possibly other
decorations in your reports. The following chunk of code shows how to do exactly that,
making use of a number of Ruport’s features.

def build_standard_report_header
pdf_writer.select_font("Times-Roman")

options.text_format = { :font_size => 14, :justification => :right }

add_text "<i>Rinara Productions</i>, <i>#{options.report_title}</i>"


add_text "Generated at #{Time.now.strftime(’%H:%M on %Y-%m-%d’)}"

center_image_in_box "ruport.png", :x => left_boundary,


:y => top_boundary - 50,
:width => 275,
:height => 70

106
move_cursor_to top_boundary - 80

pad_bottom(20) { hr }

options.text_format[:justification] = :left
options.text_format[:font_size] = 12
end

This outputs a header which looks like this:

10.3 Creating a Standard Report Template


Often, you’ll want to reuse these standard report elements, and the easiest way to do this
is to encapsulate these stages in a module, and then include them into your formatter.
The follow extended example shows how two different controllers can reuse the same
header code and finalization hook.

module StandardPDFReport

def build_standard_report_header
pdf_writer.select_font("Times-Roman")

options.text_format = { :font_size => 14, :justification => :right }

add_text "<i>Rinara Productions</i>, <i>#{options.report_title}</i>"


add_text "Generated at #{Time.now.strftime(’%H:%M on %Y-%m-%d’)}"

center_image_in_box "ruport.png", :x => left_boundary,


:y => top_boundary - 50,
:width => 275,
:height => 70

move_cursor_to top_boundary - 80

pad_bottom(20) { hr }

107
options.text_format[:justification] = :left
options.text_format[:font_size] = 12
end

def finalize_standard_report
render_pdf
pdf_writer.save_as(options.file)
end

end

class DocumentController < Ruport::Controller

stage :standard_report_header, :document_body


finalize :standard_report

end

class TableController < Ruport::Controller

stage :standard_report_header, :table_body


finalize :standard_report

end

class FormatterForPDF < Ruport::Formatter::PDF

renders :pdf, :for => [DocumentController, TableController]

include StandardPDFReport

build :document_body do
add_text "Lorum, Ipsum Blah Blah...\n" * 25
end

build :table_body do
draw_table(data)
end

end

Using the above definitions, the following sample usages generate PDFs which look like
the following images:

DocumentController.render_pdf( :file => "foo.pdf",


:report_title => "Sample Document" )

108
t = Table(:column_names => %w[col1 col2 col3 col4],
:data => [%w[lorum ipsum blah blah]] * 20)

TableController.render_pdf( :file => "bar.pdf",


:report_title => "Sample Table Report",
:data => t )

Although this implementation uses the same formatter for both controllers, there is no
need to do that. Including the StandardPDFReport module would work for any PDF
formatter subclass.

109
10.4 Generating Page Headers
If you are generating a report which has tables that do not exceed one page, or data that
can be generated on a page by page basis, page headers can simply be drawn using
add text / draw text much like the way the document headers were drawn in the
previous example.
However, if you have information which must be repeated on each page, and you don’t
have control over the document flow, things are somewhat more complicated.
PDF::Writer does not have any real support for drawing content on all pages. However,
Ruport does have limited support for this, via the pdf-helpers plugin.
To install the plugin, do:

gem install pdf-helpers --source http://gems.rubyreports.org

The following example shows how to use the all pages callback added by pdf-helpers
to draw in page headers and footers. You’ll notice a few less than pleasant things about
this:

• You need to adjust the margins and then draw headers / footers in them

• You need to use Ruport’s draw text! method

• You need to register these callbacks before rendering your table

require "ruport"
require "ruport/extensions"

class AllPagesController < Ruport::Controller

stage :long_report
required_option :file

class PDF < Ruport::Formatter::PDF

renders :pdf, :for => AllPagesController

build :long_report do
prepare_long_report
draw_table Table(%w[a b c], :data => [[1,2,3]]*100)
render_pdf
end

110
def prepare_long_report
apply_long_report_page_header
apply_long_report_page_footer
end

def apply_long_report_page_header
pdf_writer.top_margin = 50
all_pages {
draw_text! "This is my header text", :x1 => 100, :y => top_boundary + 15
}
end

def apply_long_report_page_footer
pdf_writer.bottom_margin = 75
all_pages {
draw_text! "This is my footer text", :x1 => 500, :y => 15
}
end

end

end

AllPagesController.render_pdf(:file => "long.pdf")

Hopefully, nicer support for this will some day be available. For now, even though it is
painful, it is at least possible to accomplish this.

10.5 Making Your PDFs Display Properly in Rails

Though this is really a Rails matter and not a Ruport matter, it comes up very often. If
you want to have your controller properly hand back a PDF for download, you should use
send data. In its most simple form, your controller code might look something like this:

pdf = LabelGenerator.render_pdf(:data => user_addresses)

send_data pdf, :type => "application/pdf",


:filename => "mailing_labels.pdf"

Consult your favorite Rails documentation resource for further information.

111
10.6 Related Resources / Digging Deeper
There are a wide range of PDF helpers provided by Formatter::PDF, so taking a quick
look at the API documentation should be helpful. Many functions, such as add text and
draw table, expose a large number of features by forwarding options directly to
PDF::Writer.
Also, in ruport-util, you’ll find PDF Invoice support and some drawing helpers for
generating printable forms. More tools are constantly being added, so keep an eye out for
new stuff in the future.

112
Chapter 11

Adding Logic to Custom


Controllers

Custom controllers in Ruport are often used to define the process your Formatters should
implement and the options they should handle. This allows you to gain a consistent
interface across your formatters, and for many uses this is good enough.
When reports are more complex, there is often a need to massage data into a normalized
form or make some decisions about how the data should be represented at run time.
Ruport’s Controller class offers a number of facilities for the most common scenarios
you’ll run into.
Here we’ll discuss the Controller#setup hook, Controller::Hooks and Formatter
helpers.

11.1 Using setup()


The setup hook is called after options are processed from the hash arguments and block
given to your render call. This means that you can easily manipulate the data attribute
as well as any of the options passed to your controller.
You can also call any methods within your controller subclass as needed.

class DocumentController < Ruport::Controller


stage :document
required_option :text

def setup
text << " for #{username}"
end

113
def username
data[:name].capitalize
end

end

class DocumentFormatter < Ruport::Formatter

renders :example, :for => DocumentController

build :document do
output << "||" << options.text << "||"
end

end

puts DocumentController.render_example(
:data => { :name => "gregory" },
:text => "Sample Text"
)

Output:

||Sample Text for Gregory||

11.2 Using Controller::Hooks


Ruport tries to be as flexible as possible, and part of that task is making it very easy to
hook up the formatting system to your custom, non-ruport classes. In the previous
example, you can see that DocumentController expects hash-like data.
Below is a simple example of how to make an unlike class still play nice with that
controller.

class Person

include Ruport::Controller::Hooks

renders_with DocumentController

def initialize(name)
@name = name
end

114
def renderable_data(format)
{ :name => @name }
end

end

me = Person.new("gregory")
puts me.as(:example, :text => "Hooks example")

Output:

||Hooks example for Gregory||

This can be very powerful, as it allows you to do whatever transformations you need on
many diverse data structures and use a few standard controllers.

11.3 Using Formatter Helpers


Sometimes you will have some methods you want available to all your formatters, but you
want to leave it up to the individual formatters to decide how to make use of them. You
could go about this by explicitly including a module in your different formatters. Because
this task is relatively common, Ruport provides a shortcut.
If you define a module called Helpers in your controller, it will be mixed into the
formatter instance at run time. This allows for controllers to specify different ways of
handling specific tasks without clashing.
The simple example below adds a time() helper that gives formatted output of the
current time and shows it used in multiple formatters.

class DocumentController < Ruport::Controller


stage :document
required_option :text

def setup
text << " for #{username}"
end

def username
data[:name].capitalize
end

115
module Helpers
def time
Time.now.strftime("%H:%m:%S")
end
end
end

class DocumentFormatter < Ruport::Formatter

renders :example, :for => DocumentController

build :document do
output << "At #{time}, I render ||" << options.text << "||"
end

end

class DocumentFormatter2 < Ruport::Formatter

renders :example2, :for => DocumentController

build :document do
output << "||" << options.text << "||, I rendered this at #{time}"
end

end

Using our Person object from before:

puts me.as(:example, :text => "Hooks example")

Outputs:

At 22:05:56, I render ||Hooks example for Gregory||

Using the other formatter for the same data:

puts me.as(:example2, :text => "Hooks example")

Outputs:

||Hooks example for Gregory||, I rendered this at 22:05:14

116
11.4 Implicit Helpers for Formatter Selection

If you think that this chunk of code seems verbose, you’re probably looking for our
format selection helpers.

class DocumentFormatter < Ruport::Formatter

renders :example, :for => DocumentController

build :document do
output << "At #{time}, I render ||" << options.text << "||"
end

end

class DocumentFormatter2 < Ruport::Formatter

renders :example2, :for => DocumentController

build :document do
output << "||" << options.text << "||, I rendered this at #{time}"
end

end

Ruport automatically creates helper methods for each format registered on the controller.
These helpers accept a block that will only be called when the particular format is being
rendered.
The example below is essentially identical to the code above.

class DocumentFormatter < Ruport::Formatter

renders [:example, :example2], :for => DocumentController

build :document do
example { output << "At #{time}, I render ||" << options.text << "||" }
example2 { output << "||" << options.text << "||, I rendered this at #{time}" }
end

end

This can come in handy when you have lots of slightly different formats to deal with.

117
11.5 Related Resources / Digging Deeper
• Controller::Hooks has shortcuts for the standard controllers.
• Controller::Hooks also has a rendering options class method for setting
defaults.
• Controller has a run() hook that can be used to override the way stages are
executed.

118
Chapter 12

Integration Hacks

Ruport itself is a very simple core system. A lot of its power comes from how easy it is to
integrate with other software. Sometimes you may just want to access a couple advanced
features from one of our dependencies. Other times, you may be wrapping a complex
legacy reporting system. This cheatsheet shows a few ways to get Ruport working
alongside other code without pain.

12.1 Squeezing More Out of Our Dependencies


Ruport tries to make its dependencies easily accessible, especially FasterCSV and
PDF::Writer. Much of what you can do with these libraries, you can access through
Ruport’s API.
Still, if you have pre-written FasterCSV or PDF::Writer code, you’re probably looking
for some shortcuts to avoid rewriting that code. We’ll look at two plugins that help you
do exactly that.

12.1.1 Using Ruport’s Formatters for FasterCSV Tables and


Rows
If you’ve already been using FasterCSV::Table for your data manipulations, and you
just want to make use of Ruport for your formatted output, it’s a piece of cake with
fcsv formatter.
You’ll need to grab the plugin first:

gem install fcsv_formatter --source http://gems.rubyreports.org

Below is a slightly modified example from the FasterCSV 1.2.0 source that shows that
formatting tables ‘just works’.

119
require "ruport"
require "ruport/extensions"

table = FCSV.parse(DATA, :headers => true, :header_converters => :symbol)

table << %w[james gray 30]


table[-1].fields #=> ["james", "gray", "30"]

table[:type] = "name"

table[:ssn] = %w[123-456-7890 098-765-4321]


table[:ssn] #=> ["123-456-7890", "098-765-4321", nil]

puts table.as(:text)

__END__
first_name,last_name,age
zaphod,beeblebrox,42
ara,howard,34

Outputs:

+-----------------------------------------------------+
| first_name | last_name | age | type | ssn |
+-----------------------------------------------------+
| zaphod | beeblebrox | 42 | name | 123-456-7890 |
| ara | howard | 34 | name | 098-765-4321 |
| james | gray | 30 | name | |
+-----------------------------------------------------+

This is just using Ruport’s table controller, so you can also use the built in PDF, CSV,
and HTML formats. Any additional formatters you attach to the table controller will also
be detected.
FasterCSV::Row objects can be formatted as well:

puts table[0].as(:text)
=> "| zaphod | beeblebrox | 42 | name | 123-456-7890 |"

If you need grouping support, you can explicitly transform a FasterCSV::Table to a


Ruport::Data::Table by calling table.renderable data.

120
Bonus Side Effect: Good Performance
It turns out that FasterCSV is quite a bit faster for loading CSV files than Ruport is.
This is mainly because Ruport offers some different behaviors at load time, and also uses
FCSV under the hood. If you’ve got straightforward needs, you might use this plugin to
speed up your report generation with large files.

12.1.2 Quickly Wrapping PDF::Writer code with pdf writer proxy


If you’re needing to do a lot of custom PDF work or you’re wrapping some existing
PDF::Writer code with Ruport, the pdf writer proxy will surely be helpful.

# Only necessary for Ruport <= 1.2.3


gem install pdf_writer_proxy --source http://gems.rubyreports.org

Here is a basic example from the PDF::Writer sources:

pdf = PDF::Writer.new
pdf.select_font "Times-Roman"
pdf.text "Chunky Bacon!!", :font_size => 72, :justification => :center

i0 = pdf.image "../images/chunkybacon.jpg", :resize => 0.75


i1 = pdf.image "../images/chunkybacon.png", :justification => :center, :resize => 0.75
pdf.image i0, :justification => :right, :resize => 0.75

pdf.text "Chunky Bacon!!", :font_size => 72, :justification => :center

pdf.save_as("chunkybacon.pdf")

Wrapping it with Ruport and changing it a tiny bit, you get this:

require "ruport"
require "ruport/extensions"

class ChunkyController < Ruport::Controller

stage :bacon
required_option :file

class PDF < Ruport::Formatter::PDF

renders :pdf, :for => ChunkyController

proxy_to_pdf_writer

121
build :bacon do
options.text_format = { :font_size => 72, :justification => :center }

select_font "Times-Roman"
add_text "Chunky Bacon"

i0 = image "chunkybacon.jpg", :resize => 0.75


image "chunkybacon.png", :justification => :center, :resize => 0.75
image i0, :justification => :right, :resize => 0.75

add_text "ChunkyBacon"

save_as(options.file)
end

end
end

ChunkyController.render_pdf(:file => "chunkybacon.pdf")

You’ll notice that the core of the report is quite similar, and is directly using some
PDF::Writer calls. Most PDF::Writer methods will “just work” as the plugin just
forwards all requests it doesn’t understand to the underlying pdf writer object in the
formatter.
In certain cases, Ruport’s method names conflict with the PDF::Writer names. For
example, if you are looking to use PDF::Writer#add text rather than Ruport’s you’d
need to type pdf writer.add text explicitly.
Otherwise, this is typically a very fast way to extend your formatter so it can use the full
PDF::Writer tool belt.

12.2 Playing Nice with Third-Party Code


A lot of times, you’ll want to bend Ruport for your own needs, or mallet it into some
other system. We try to make it as easy as possible for you to do this.

12.2.1 Wrapping Business Logic with Custom Record Classes

Usually when you’re dealing with data processing, you’ll need to apply some custom
calculations or manipulations.
The following example generates total sale prices for a series of items with different prices
and quantities:

122
require "rubygems"
require "ruport"

class Sale < Ruport::Data::Record

TAX = 0.06

def total_sale
price.to_f * quantity.to_f * (1 + TAX)
end

end

puts Table(:string => DATA, :record_class => Sale).sum(:total_sale) #=> 1288.589

__END__
item,price,quantity
apple,1.05,3
banana,1.25,10
kitten,12,100

The :record class does not technically need to be a child of Ruport::Data::Record,


though that’s the general assumption. You may find that a suitably duck typed object
will work just as well.

12.2.2 Reporting Against Arbitrary Data Structures


You might find yourself dealing with a number of structures that are fairly easy to
convert to Ruport’s data model but would like to avoid copious calls that look like
my data.to table.as(:html)
To remedy, you can use Controller::Hooks.
The following example shows how to use our table controller for Ruby’s Matrix class:

require "ruport"
require "matrix"

class Matrix

include Ruport::Controller::Hooks

renders_as_table

123
def renderable_data(format)
to_a.to_table
end

def to_s
as(:text)
end

end

puts Matrix[[1,2,3],[4,5,600]]

Outputs:

+-------------+
| 1 | 2 | 3 |
| 4 | 5 | 600 |
+-------------+

Shortcuts are also defined for the other controllers: renders as group,
renders as grouping, and renders as row are all available.
If you’re using ruport-util, you also have renders as graph.
The way these hooks work is actually very simple, the as() call just sets your :data
attribute to the result of renderable data(format), and passes along any options to the
controller.
You can use your own custom controllers, too, via the renders with command.

12.2.3 Extending or Modifying Ruport with gem plugin


If you’re using some of the techniques listed in this cheatsheet, you may want to make
your modifications just snap into Ruport whenever they’re installed without having to
explicitly require them. This is how the plugins we showed work, and is extremely easy to
do.
In order for require ruport/extensions to discover your plugin, you simply need to
have your gem depend on both ruport and gem plugin.
Then, at lib/my app/init.rb, you should have an initialization similar to this code:

class RuportInvoiceLoader < GemPlugin::Plugin "ruport/invoice"


require ’invoice’
# or other setup stuff here.
end

124
# here you can define more classes, reopen stuff,
# do normal ruby, whatever, or stick things in other files.

The string passed to GemPlugin::Plugin is just a unique identifier for your plugin, which
you probably will not need.
That’s really it! Any code you include in that file or require from within it will be
auto-detected and loaded alongside any other plugins installed when a user includes
ruport/extensions in their code.
Because this involves autoloading, we recommend against breaking behavior in the core
library via plugins. Extensions are welcome though!

12.3 Related Resources / Digging Deeper


Controller::Hooks can be incredibly handy when used alongside custom controllers, as
they allow you to keep your transformations close to your actual data structures and
quickly convert various objects into standard forms so controllers can be reused readily.
You’ll want to review the Controller Logic cheatsheet for some ideas.
A comprehensive set of plugin instructions is available at:
http://stonecode.svnrepository.com/ruport/trac.cgi/wiki/RuportPluginSystem

125
126
Chapter 13

Using Report and


ReportManager

Ruport’s Report class is meant to provide a high level interface to some of Ruport’s core
libraries and utilities.
Report provides easy ways to work with Ruport::Query and Ruport::Mailer, and also
provides rendering shortcuts. These features make it easier to tie together some of
Ruport’s lower level bits into a single object.

13.1 Dealing with Controllers


Report uses simple hooks to make it act like a Controller object. The following code is
an example of rendering tables:

class MyReport < Ruport::Report

renders_as_table

attr_accessor :file

def renderable_data(format)
t = Table(file)
t.sub_table { |r| r.a == "foo" }
end

end

127
MyReport.generate do |report|
report.file = "in.csv"
report.save_as "out.csv" # renders to csv
report.save_as "out.pdf" # renders to pdf
puts report.to_text # renders to screen as text
end

The other default controllers are also supported, named renders as row,
renders as group, and renders as grouping. The data returned by
renderable data(format) will be passed to the controller you specify.

You can also pass default options to your controller if needed, e.g.

renders_as_table :show_table_headers => false

13.1.1 Using Custom Controllers


Most interesting Ruport applications make use of custom controllers. It is trivial to make
use of them with your reports:

class MyReport < Ruport::Report


renders_with MyCustomController

def renderable_data(format)
# the return of this will be passed as :data to MyCustomController
end
end

You can then continue to use as(), save as(), and the to format shortcuts.

13.1.2 Details About the save as() Magic


For the save as() function, the follow mappings are used:

save_as("foo.txt") forwards to as(:text)


save_as("foo.pdf") forwards to as(:pdf) and writes as a binary
save_as("foo.csv") forwards to as(:csv)
save_as("foo.html") forwards to as(:html)

For other extensions, if your controller handles them you can still use save as(), it will
just convert the extension to a symbol and pass it to as().
Example: save as("foo.xml") forwards to as(:xml)

128
These formats will be written using text mode by default, rather than binary. You can
specify file access flags if needed, e.g.

save_as("foo.jpg", :flags => "wb")

13.2 Using query() for Raw SQL Operations


The following example shows a report which sets up a number of database sources and
then generates grouped output. query() is a shortcut interface to Ruport::Query,
covering the most common needs.

class MyReport < Ruport::Report

renders_as_grouping

attr_accessor :source

def prepare
add_source :legacy, :dsn => "dbi:mysql:old_db", :user => :root
add_source :default, :dsn => "dbi:mysql:new_db", :user => :root
end

def renderable_data(format)
results = query "select name, email from foo where num < 10",
:source => source

Grouping(results,:by => "name")


end
end

legacy_report = MyReport.new
legacy_report.source = :legacy
legacy_report.save_as "foo.csv"

new_report = MyReport.new # will use :default source


new_report.save_as "bar.csv"

13.3 Mailing Reports via Report#send to()


Report offers an alternate interface to Ruport::Mailer. This allows you to easily attach
reports to an email and send them via SMTP.
The following example is for using the Report class directly, but would work for
subclasses as well.

129
r = Ruport::Report.new

r.add_mailer :default,
:host => "mail.adelphia.net",
:address => "gregory.t.brown@gmail.com"

r.send_to("gregory.t.brown@gmail.com") do |mail|
mail.subject = "Hello"
mail.attach "foo.csv"
mail.text = "This is an email with attached csv"
end

13.4 Some Quick Notes for Using Report in rope

This information is covered in more detail in the rope cheatsheet, but the following notes
are provided as a quick reference.

Generating a Report

rake build report=my_report

Running a Report

rake run report=my_report

Setting up Sources for query()

If you’d like to share sources between reports, define them in config/environment.rb.

Using Autogenerated Controllers

To generate the controller definition:

rake build controller=my_controller

In your report, add require "lib/controllers" and the call:

renders_with MyController

130
13.5 Managing Many Report Objects
In the ruport-util package there is a tool called ReportManager that allows you to select
different reports programatically. This might be useful in web applications, where you’d
like to determine which class should generate a report via a drop down menu.
The following simple example shows how this might be used:

class Mars < Ruport::Report

acts_as_managed_report
renders_as_table

def renderable_data(format)
Table("mars.csv")
end
end

class Venus < Ruport::Report

acts_as_managed_report
renders_as_table

def renderable_data(format)
Table("venus.csv")
end
end

["Mars","Venus"].each do |n|
Ruport::ReportManager[n].generate { |r| r.save_as "#{n}.pdf" }
end

Typically, it might be easier to just store report instances in a hash lookup, but there are
certain cases where you’ll want to be able to dynamically register and run reports, which
is easy to do with ReportManager.
If you need to override the name of a report, add a name method to it which returns a
string to identify the report with.

131
13.6 Related Resources / Digging Deeper
Report is really most useful when used in conjunction with rope, so you’ll want to review
that cheatsheet.
Also, much of the heavy lifting can and should be done by your controllers, especially
using the various hooks available in them. This class is among the feature sets in the
ruport-util package that is likely to change over time to adapt to people’s needs.

132
Chapter 14

rope (A Code Generation Tool


for Ruby Reports)

14.1 Overview
The rope tool comprises a number of simple utilities that script away much of your
boilerplate code, and also provide useful tools for development.
If you’re looking for something to help keep your project organized and help cut down on
typing, you may find this tool helpful.

14.1.1 Starting a New rope Project


$ rope labyrinth
creating directories..
labyrinth/test
labyrinth/config
labyrinth/output
labyrinth/data
labyrinth/data/models
labyrinth/lib
labyrinth/lib/reports
labyrinth/lib/controllers
labyrinth/sql
labyrinth/util
creating files..
labyrinth/lib/reports.rb
labyrinth/lib/helpers.rb
labyrinth/lib/controllers.rb
labyrinth/lib/templates.rb

133
labyrinth/lib/init.rb
labyrinth/config/environment.rb
labyrinth/util/build
labyrinth/util/sql_exec
labyrinth/Rakefile
labyrinth/README

Successfully generated project: labyrinth

Once this is complete, you’ll have a large number of mostly empty folders laying around,
along with some helpful tools at your disposal.

Utilities

• build : A tool for generating reports and formatting system extensions


• sql exec: A simple tool for getting a result set from an SQL file (possibly with ERB)
• Rakefile: Script for project automation tasks.

Directories

• test : unit tests stored here can be auto-run


• config : holds a configuration file which is shared across your applications
• reports : when reports are autogenerated, they are stored here
• controllers : autogenerated formatting system extensions are stored here
• models : stores autogenerated activerecord models
• sql : SQL files can be stored here (they are pre-processed by ERB)
• util : contains rope-related tools

14.2 Generating a Report Definition


$ rake build report=ghosts
report file: lib/reports/ghosts.rb
test file: test/test_ghosts.rb
class name: Ghosts

$ rake
(in /home/sandal/labyrinth)
/usr/bin/ruby -Ilib:test
"/usr/lib/ruby/gems/1.8/gems/rake-0.7.1/lib/rake/rake_test_loader.rb"

134
"test/test_ghosts.rb"
Loaded suite /usr/lib/ruby/gems/1.8/gems/rake-0.7.1/lib/rake/rake_test_loader
Started
F
Finished in 0.001119 seconds.

1) Failure:
test_flunk(TestGhosts) [./test/test_ghosts.rb:6]:
Write your real tests here or in any test/test_* file.

1 tests, 1 assertions, 1 failures, 0 errors


rake aborted!
Command failed with status (1): [/usr/bin/ruby -Ilib:test
"/usr/lib/ruby/ge...]

(See full trace by running task with --trace)

You can now edit lib/reports/ghosts.rb as needed and write tests for it in
test/test ghosts.rb without having to hook up any underplumbing.

14.3 Project Configuration


Projects generated with rope will automatically make use of the configuration details in
config/environment.rb, which can be used to set up database connections, Ruport’s
mailer, and other project information.
The default file is shown below.

require "ruport"

# Uncomment and modify the lines below if you want to use query.rb
#
# Ruport::Query.add_source :default, :user => "root",
# :dsn => "dbi:mysql:mydb"

# Uncomment and modify the lines below if you want to use AAR
#
# require "active_record"
# require "ruport/acts_as_reportable"
# ActiveRecord::Base.establish_connection(
# :adapter => ’mysql’,
# :host => ’localhost’,

135
# :username => ’name’,
# :password => ’password’,
# :database => ’mydb’
# )

You’ll need to tweak this as needed to fit your database configuration needs. If you need
to require any third party libraries which are shared across your project, you should do it
in this file.

14.4 Custom Rendering with rope Generators


$ rope my_reverser
$ cd my_reverser
$ rake build controller=reverser

Edit test/test reverser.rb to look like the code below:

require "test/unit"
require "lib/controllers/reverser"

class TestReverser < Test::Unit::TestCase


def test_reverser
assert_equal "baz", Reverser.render_text("zab")
end
end

Now edit lib/controllers/reverser.rb to look like this:

require "lib/init"

class Reverser < Ruport::Controller


stage :reverser
end

class ReverserFormatter < Ruport::Formatter

renders :text, :for => Reverser

build :reverser do
output << data.reverse
end
end

The tests should pass. You can now generate a quick report using this controller.

136
$ rake build report=reversed_report

Edit test/test reversed report.rb as such:

require "test/unit"
require "lib/reports/reversed_report"

class TestReversedReport < Test::Unit::TestCase


def test_reversed_report
report = ReversedReport.new
report.message = "hello"
assert_equal "olleh", report.to_text
end
end

Edit lib/reports/reversed report.rb as below and run the tests.

require "lib/init"
require "lib/controllers/reverser"
class ReversedReport < Ruport::Report

renders_with Reverser
attr_accessor :message

def renderable_data(format)
message
end
end

14.5 ActiveRecord Integration the Lazy Way


Ruport has built in support for acts as reportable, which provides ActiveRecord
integration with Ruport.

14.5.1 Setup Details


Edit the following code in config/environment.rb (change as needed to match your
config information).

137
ActiveRecord::Base.establish_connection(
:adapter => ’mysql’,
:host => ’localhost’,
:username => ’name’,
:password => ’password’,
:database => ’mydb’
)

14.5.2 Generating a Model


Here is an example of generating the model file:

$ util/build model my_model


model file: data/models/my_model.rb
class name: MyModel

This will create a barebones model that looks like this:

class MyModel < ActiveRecord::Base

acts_as_reportable

end

The data/models.rb file will require all generated models, but you can of course require
specific models in your reports.

14.6 Related Resources / Digging Deeper


You’ll want to look at the documentation for Ruport::Report1 and possibly
acts as reportable2 to make the most out of rope. Also, if you want to build your own
custom rope-based generator, look into Ruport::Generator.

1 See the Report cheatsheet.


2 See the acts as reportable cheatsheet.

138
Appendix A

Ruport Hacking Guide

If you’ve read your way through this book, you probably have a decent grasp on how to
use Ruport. However, there is power in knowing what lies under the hood. This appendix
will help show you how to work against Ruport’s bleeding edge, and how to hack on the
internals as needed. If you’re interested in patching Ruport to meet some particular need,
or you’d like to make a contribution to our community, this is a must read.

A.1 Running from Ruport’s Edge


The first step to hacking on Ruport is of course to get yourself the latest sources. Things
tend to move reasonably fast in Ruport, so working against Subversion is a must. You can
always find repository URL listings at code.rubyreports.org, but at the time of writing,
the locations of trunk for the various components of Ruby Reports were as follows:
Ruport
http://stonecode.svnrepository.com/svn/ruport/ruport/trunk

acts as reportable
http://stonecode.svnrepository.com/svn/ruport/acts as reportable

ruport-util
http://stonecode.svnrepository.com/svn/ruport-util/trunk

Ruport/Rails
http://stonecode.svnrepository.com/svn/ruport rails/

A.1.1 Release Structure


All of our packages follow a common release numbering scheme which is meant to easily
let our users know what kind of release they are using at a glance.

139
Stable Releases:

a.b.c -> a.b.(c+1) : Bug Fixes. No API Incompatibility (minor)


a.b.c -> a.(b+1).c : Feature Enhancements and possible API breakage (major)
a.b.c -> (a+1).b.c : Huge Milestone. All bets are off.

We always support the latest stable release branch. As of December 2007 this was Ruport
1.4, and it resides in /branches/1.4. Once a new major stable release comes out, the
branch effectively becomes obsolete.
We try not to use odd numbers for the middle number in our public releases, meaning the
next stable major release after 1.4.x will likely be 1.6.x

Beta Releases:
We will occasionally publish beta gems. They follow the form a.b.rev, where rev is the
SVN revision the gem is built from. For example, a beta build of the 1.4 codebase
generated from r1224 would become Ruport 1.3.1224
This numbering scheme makes it very easy to tell both which branch the code is
ultimately going to become a part of, and also approximately what it is in. Combined
with an svn log of trunk, it shouldn’t be too hard to keep track of what is in the beta
builds.

A.2 Preparing A Patch


If you’d like to extend Ruport either for your own use or to contribute back to the
community, you’ll find it’s easier than you think.

A.2.1 Choose the Right Package


Ruport’s distribution is split across several packages, and picking the right one to patch
for your new features will help both make your work easier and increase the likelihood
that your patch be accepted if you submit it to us.
What follows is a brief description of each package, so that you have a sense of where
your feature might belong.

Ruport
The core Ruport package has become increasingly lean from version to version. We now
basically think of it as a super lightweight base for building actual tools on top of. With
this in mind, we basically have our core data structures, some simple controllers, and a
few formatters. As of Ruport 1.4, it should be easy for anyone to keep most of Ruport’s

140
core in their head, which we think is a good thing. Basically, the only features which
belong in Ruport’s core are the ones so common that most everyone will use them (CSV
I/O, Tabular Data Structure) and also the ones that provide a solid base for extension
(PDF Formatter). Most other features belong in the other packages.
The Ruport package is currently under a release cycle that supports a single stable
branch and a main development branch. Stable branches typically last a couple months
before being replaced by newer versions from development. What this basically means is
that it sometimes takes a while for changes in the core package to make it out to the
general public.

ruport-util
The ruport-util package is Ruport’s fat cousin, chock full of pretty much anything that
supports Ruport development. This means that it is an ideal home for somewhat
specialized tools or for formatters and data modeling libraries that aren’t particularly
extensible. This is also the place for any command line utilities. Two such examples
currently in ruport-util are rope and csv2ods.
The ruport-util package is under a much less structured release cycle, in which releases
typically take place a few days after new functionality is added. Given the nature of the
package as sort of a collection of different tools, this approach seems to work pretty well.
The only restrictions are that ruport-util’s trunk should run against the latest stable
version of Ruport, not a development version. When work needs to be done against the
bleeding edge, we’ll usually raise branches.

acts as reportable
Formerly part of core Ruport, acts as reportable is our ActiveRecord integration.
Essentially, anything specific to AR belongs in this package.
Though you can likely expect more stability than ruport-util, acts as reportable is also on
a rolling release schedule and may change over time.

Ruport/Rails
Of the four packages mentioned here, Ruport/Rails is the only ‘unofficial’ package. This
is a Rails plugin and is meant for any non-AR Rails helpers. If you have an extension
that is rather specific to Rails that needs a home, this may be the right place.
This package is SVN-only, and does not have a formal release schedule. It is also still
considered to be experimental, but Mike uses it in his day to day work.

A.2.2 Be a Good Patcher

Once you’ve decided which package you’d like to patch, it’s time to dig in.

141
Get up to date
A first step to take before working on a patch is to check the development version of the
package you’re working on to see if it doesn’t already have the feature you need. We
don’t announce every feature while we’re working on it, so glancing at the svn log is
always a good idea.

Get in contact
Once you’ve verified that your problem still needs solving, please contact us on the
ruport-dev mailing list: http://groups.google.com/group/ruport-dev
This list is mostly for contributors and developers, and it is where the implementation
details of new features can be discussed. We will also happily review any patches you post
to this list. Usually, the best way to get a feature into any of our packages is to send a
short email describing the problem, maybe with some code, or if not, just example usage.
We encourage folks not to worry about perfecting their code before starting up a
discussion with us. You’ll find it a whole lot easier to get a patch together with a couple
Ruport developers helping you along.

Get your patch applied


If you’re just working on an extension for personal use and don’t want to worry about
refining it for use by the general community, that’s fine. However, if you want your patch
to get integrated into the project, there are a few simple guidelines.

• Always send unified diff, as output by svn diff.


• Try to keep your patches small and atomic.
• Try to follow good Ruby coding practices, and stick to two space indent.
• Don’t let your patch break our test suite.
• Make sure your patch has tests of its own, especially if it’s a bug fix.

Once you’ve prepared a diff file, you’re welcome to open up a ticket in Trac and attach
the file there. You can also send it to the ruport-dev mailing list. However, if you’ve put
it in the tracker, please mail a link to the mailing list so we can discuss the patch there.
We have an open commit policy, so all it takes is one accepted patch to get you full access
to all four packages. So far, this has worked out great with our contributors, and
hopefully lets people know that anyone is welcome to help make Ruport better.

A.3 Power Tools for Ruport Hackers


Though this gives you a basic overview of how the project is laid out development-wise,
it’s worth mentioning some interesting bits that are in the core Ruport gem and are

142
worth investigating if you’re thinking of extending things. Listed below are several of the
developer conveniences that might be useful in your work:

A.3.1 Data Model


• Data::Feeder can wrap any object that implements << and feed element.
• Table accepts :record class, which allows you to use Record subclasses or duck
typed objects. A subclass of Table could set these automatically. For reference see
the way graphs are implemented in ruport-util.
• Records can be reindexed to point at a new set of attribute names, and attributes
can be deleted via private functions.

A.3.2 Formatting System


• Formatter::RenderingTools contains the definitions for the render * helpers. You
can easily use render helper() in this module to create your own wrappers.

• You can override Controller#run() to completely change the way the rendering
process works.
• You can use Controller’s build method to create a controller instance and tie it to a
formatter. This allows you to persist a controller instance rather than generating
them on the fly, among other things.

• The PDF formatter has a number of private methods that do primitive


manipulations on the document, some of these may come in handy for designing
advanced components.
• Controller::Hooks lets you tie any data structure to the formatting system.

Though these features are quite advanced, they make it possible to significantly extend
and even modify the way core Ruport works without having to uproot the underlying
codebase. We’d be quite excited to see these used to solve interesting problems.

143
144
Afterword

This book has provided a tour through the major components of Ruport. At the level of
the software, you’ve seen how to collect data and process that data into a form that
Ruport can use. You’ve also seen how to use Ruport’s formatting system to output that
data into your report. Along the way, we’ve shown you how Ruport fits into a real
application and demonstrated some of the libraries associated with the core project.
With that background, you should be able to produce some pretty fancy reports, but
there are a number of avenues we haven’t explored here. We covered all of the major
functionality of Ruport, but this book isn’t intended as a comprehensive reference
manual. To get all of the details on what Ruport can do, the API docs are the best
resource and are located at http://api.rubyreports.org.
Although this book mainly covered the Ruport core, the overall Ruby Reports project
includes several other sub-projects. You’ve seen ruport-util and acts as reportable in
action throughout the book, but especially with ruport-util, there is a lot we didn’t cover.
For instance, ruport-util contains interfaces to several other graphing libraries, support
for producing Excel and OpenDocument spreadsheet output, a simple invoicing module
and more.
Some of what we did show you, such as the PDF form helpers and the mail support, have
many features that we didn’t cover. In addition, ruport-util is the home of those parts of
the project that we feel are either are not general or not mature enough to be included in
Ruport core, and as such it is constantly evolving. You definitely want to keep an eye on
what’s happening in ruport-util.
One independent project that falls under the umbrella of Ruby Reports is Documatic.
Documatic is an OpenDocument extension for Ruport. It is a template-driven formatter
that can be used to produce attractive printable documents such as database reports,
invoices, letters, faxes and more.
Another project falling under the Ruby Reports scope is Ruport/Rails. It is a plugin that
integrates Ruport with the Rails web development framework, allowing you to use
Ruport’s formatting system within your views. If you’re using Ruport within Rails, it’s
worth a look.
The main Ruport web site is located at http://rubyreports.org. It contains many useful
resources to get you more acquainted with the overall project and links to other locations
where you can find Ruport-related material. The web site for this book is located at

145
http://ruportbook.com. It contains HTML-formatted versions of the book content, as well as
up-to-date news and errata. We maintain a blog about Ruby Reports at
http://blog.rubyreports.org for general project news. The official developer resource listing
for Ruby Reports is located at code.rubyreports.org. There you can find repository locations,
wikis, bug trackers, and other essential details for Ruport and its related projects.
There is an active community surrounding the Ruby Reports project. Once you get your
feet wet with some reporting, you may want to get involved. We maintain an active
mailing list at http://list.rubyreports.org so please sign up there to keep up to date with the
project. On the mailing list, you can get announcements, ask questions, or participate in
discussions about Ruport. You can also catch up with the developers and community
members on the #ruport channel on Freenode.
Though we often have discussions about feature requests, bugs, and other development
related things on the main mailing list, we also maintain a developers’ mailing list to
announce developer meetings or discuss changes that affect the internals of Ruport. It’s
by invite only, but we approve any requests to join that aren’t from spammers. You can
find the dev-list at http://groups.google.com/group/ruport-dev.
Thanks very much for reading and happy reporting!

146

S-ar putea să vă placă și