ruport book 1.1.0

164
The Ruport Book Your guide to mastering Ruby Reports Gregory Brown Michael Milner (and the Ruport community)

Upload: alex-bay

Post on 08-Apr-2015

135 views

Category:

Documents


1 download

TRANSCRIPT

Page 1: Ruport Book 1.1.0

The Ruport BookYour guide to mastering Ruby Reports

Gregory Brown

Michael Milner

(and the Ruport community)

Page 2: Ruport Book 1.1.0

The Ruport Book

by Gregory Brown and Michael Milner with the Ruport community.

Print typesetting produced by Dinko Mehinovic.

Copyright c©2008, 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 UnportedLicense. 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

Page 3: Ruport Book 1.1.0

This book is dedicated to the friends and family members of folks who spend a little toomuch 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.

Page 4: Ruport Book 1.1.0

iv

Page 5: Ruport Book 1.1.0

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

Page 6: Ruport Book 1.1.0

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

Page 7: Ruport Book 1.1.0

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

Page 8: Ruport Book 1.1.0

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

Page 9: Ruport Book 1.1.0

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

Page 10: Ruport Book 1.1.0

x

Page 11: Ruport Book 1.1.0

Foreword

Since 1998 I have been privileged to have many very young and very bright minds fromthe University of New Haven’s Computer Science program work at my consultingbusiness, BTree Technology. Using various programming languages of the day (VB in1988, C++ in 2000, C# in 2002 and of course Ruby since 2004), I have set manyprogrammers on a single simply-stated quest: to write good reports!

Oddly enough, while the focus has been reporting for dental office operations, thisseemingly simple task was enough for me to realize that what I thought should be simpleand quick - namely to write reports or small applications that manipulate data intospecific reports - actually takes a long time to do well and makes for somewhatfrustrating 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 askingfor the report and the programmer writing the report. Actually, since the person askingfor the report has almost certainly never programmed, and almost certainly does notknow SQL, they think it is easy to do! This makes the disconnect even worse than whenwriting applications, where the business logic people at least expect it to be a significantprocess, and not a couple of hours’ work. Even with 15 years of CS background, Imanaged to underestimate the complexity of a “simple report request.” If you asked myprogrammers 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 includesvalidating that the programmer understood what the report was for, that the data beingpresented is the data that was desired, that it is formatted correctly, that it issummarized correctly, and that it can be easily manipulated into other views such asgraphs, charts, and other summary forms.

The frameworks available to me in 2004 were all fine choices for reporting but they alsooperated more as applications than as languages. That made some seemingly easy tasksvery 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 especiallycommon when data was coming from two different sources, such as two differentdatabases, or maybe a database and a text file. Pre-processing could be done in Access or

xi

Page 12: Ruport Book 1.1.0

in Crystal Reports, but the code was often “this-particular-report-specific.” Every timethere was another report to do, even when it was only slightly different, we were back atthe drawing board trying to twist and shim data into a report suitable for use. Withlimited time and too many reports to do, many reports ultimately went unfinished. Thismade 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 quickreports in Ruby in three hours and I was impressed. I had never seen a Windows programdo much after only three hours! With Ruby, we were able to benefit from an active opensource community. It seemed to me that since C# development was so frustrating to usthat 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 grewinto Ruport. At first Ruport was an application that could be called as a library, but thatdesign decision was limiting to us, as it wasn’t allowing us to use Ruby to its fullpotential. So we dropped the application part of Ruport and it became a library. Thislibrary 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 wereceived from the first coding contest we sponsored, where we gave awards for helping todocument the API and for writing small functions that enhanced the library. Thetremendous support we garnered from the Ruby community (especially from the mailinglist, which I read but never write on) really inspired me to believe that there was more tothis project than simply torturing a CS student with the task of coding reports. Itbecame very obvious to me very quickly that many programmers wish reporting chorescould be made more simple, and I felt very good about pushing a very smart person likeGreg to provide us with a small, incremental, solution towards that goal. Although it istechnically true that I funded this project by hiring Greg, he has volunteered thousandsof 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 inRails, the use of which truly extended Ruport to the next level. Since Mike is paid to doreporting by the company he works for, his contributions have allowed Ruport to evolvepast what I could have afforded to invest in this project. For Mike too, this has been awork of love, to whatever extent a reporting library can be loved. He has volunteeredmany 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 andMike added a ton of useful features and tightened integration with other important Rubypackages, and finally got Ruport to 1.0. They also dedicated most of 2007 to writing thisbook. 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 thatwere 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 anapplication (PayR) twice, first in Camping and then in Rails. This book is different

xii

Page 13: Ruport Book 1.1.0

because the code for a useful and real application is presented. This seems to be moreuseful than presenting simplistic examples that leave off just when things start to gettricky.

Please enjoy the book, join the Ruport mailing list, and make suggestions for improvingRuport.

Gregory Gibson

xiii

Page 14: Ruport Book 1.1.0

xiv

Page 15: Ruport Book 1.1.0

Preface

What is Ruport Anyway?

How often have you, as a developer, needed to create a business report from some randomdata, format it to look good in the process, and complete it within a tight timeline? Didyou have fun doing it? Well, we won’t claim that Ruport can make reporting “fun,” butit can certainly make it easier.

Ruport is a collection of tools to help developers add reporting capabilities to theirapplications. At its core, Ruport provides facilities for collecting data into commonstructures, manipulating that data, and then outputting it in a variety of commonformats. As a developer’s toolset, it aims to ease the burden of creating reportingapplications, but it is not itself a reporting application.

Step back a moment and consider what is meant by “reporting” and what is normallyinvolved in adding reporting functionality to an application. This understanding will helpexplain where Ruport fits into the overall development process. You might think of areport as just formatted output that you can send to a printer, so you might think of areporting toolset as a collection of formatting tools. While Ruport includes such tools, itis more than that.

In order to create a report, you first need some data. This might commonly be in adatabase, but it might also be in a CSV formatted file or some other data sourcealtogether. Consequently, you need some way to get your data into a structure that canbe used by your application. This is the first place where Ruport can help. Ruport hasfacilities for querying databases and for extracting data from CSV files. It ties into severalother Ruby libraries, including Ruby DBI, ActiveRecord, and FasterCSV to provide achoice 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 ofstandard data structures that make working with the collected data much easier andmore consistent. Once you populate one of Ruport’s data structures, the manipulation ofthat data is the same regardless of the source and method of collection. This allows youto focus on the task instead of the differences between libraries.

Ruport offers a variety of methods to manipulate the collected data. Just some of thecapabilities it provides are: adding and deleting rows and columns, renaming columns,

xv

Page 16: Ruport Book 1.1.0

sorting, performing calculations, and grouping. It tries to supply you with everythingneeded to process your information and get only the desired output when it comes timeto 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 aparticular report. It allows you to set up expectations for data that should be availableand whether that data is required or optional. The formatting system then allows you todefine how a report should be rendered for a particular output format. All of Ruport’sdata 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 justformatting the output. Ruport contains a comprehensive set of tools to make each step ofthe process less painful.

About This Book

This book aims to provide a thorough understanding of all of Ruport’s capabilities. Itassumes a knowledge of the Ruby programming language and certain other libraries andframeworks, such as Rails and its ORM component ActiveRecord. At a minimum, youshould 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. Westart with a tutorial introduction which uses data from Ruby’s Tattle project to form asimple but fully functional report. We then dive into PayR, a simple Rails payroll thatuses Ruport extensively, and provides a good example of how we actually work withRuport in our day to day jobs. The book is then rounded off with a series of cheatsheetswhich will serve both as a reference in your daily work as well as a place to look for moredetails as you read through the main content of the book. You’ll find the appropriatesections 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 fullytransparent, 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. Thelicensing 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 oranother. 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 WithoutBorders1, 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 anyamount via ruportbook.com that will go directly to the authors. This might be a good

1http://www.ewb-usa.org/

xvi

Page 17: Ruport Book 1.1.0

choice if you’ve picked up the PDF from a friend or are using just the HTML copies ofthe 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, youcan become an active contributor to our community, which is the best way to let us knowthe book was worth something to you.

We hope you enjoy the book, and that it makes your reporting life suck less, throughRuby.

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 byoffering us interesting jobs to work on, including PayR. He has also helped us out bycovering 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 tomany hours of work, and the pain of dealing with rapid revisions from the authors, and ismuch appreciated.

The following folks have also helped out in ways that run the gamut from keeping usgainfully employed while still working on Ruport, to proofreading, editing, and reviewingcontent:

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, MichaelFellinger, Dudley Flanders, James Edward Gray II, James Healy, Wes Hays, FrancisHwang, 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 isbetter because of your efforts.

xvii

Page 18: Ruport Book 1.1.0

xviii

Page 19: Ruport Book 1.1.0

Part I

Introducing Ruby Reports

1

Page 20: Ruport Book 1.1.0
Page 21: Ruport Book 1.1.0

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 rightin 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 areal, 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 atall, 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 anofficially maintained set of tools called ruport-util. These are components that eitherdon’t fit directly into the functionality provided by the core library or are not matureenough to be included in the core of Ruport. However, for specific needs, they can bevery 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 beinstalled via RubyGems.

gem install ruport-util

Another associated project is acts as reportable, which provides Ruport with the abilityto use ActiveRecord for data collection. You can install the acts as reportable gem asfollows:

1http://rubygems.org

3

Page 22: Ruport Book 1.1.0

gem install acts_as_reportable

Once Ruport is installed, we can start hacking on a project right away to give you a feelfor 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 forthe Tattle project.2 Tattle is an application that collects statistics about hardware andsoftware being used by the Ruby community. It allows those involved in maintainingRubyGems, as well as anyone else, to see collected statistics about the operatingenvironment in place on users’ computers; things like the RubyGems version, Rubyversion, operating system, CPU, etc.

The data collected by the Tattle project is voluntarily submitted by individual membersof 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/localruby_version, 1.8.5host_vendor, appleruby_install_name, rubybuild, i686-apple-darwin8.8.2target_cpu, i686arch, i686-darwin8.8.2rubygems_version, 0.9.2SHELL, /bin/shhost_os, darwin8.8.2report_time, Tue Jun 19 22:09:54 -0400 2007host_cpu, i686LIBRUBY, libruby-static.aLIBRUBY_SO, libruby.so.1.8.5target, i686-apple-darwin8.8.2

2http://rubyforge.org/projects/tattle

4

Page 23: Ruport Book 1.1.0

1.3 Overview by Example

In order to provide a general overview of Ruport’s functionality, we’re going create anexample report from the Tattle data. Ruport is perfect for this kind of task, as it involvespulling the collected data from a database, grouping it, and then generating output in avariety of formats, just what we described as Ruport’s strengths.

Included with the Tattle project is a Rails application3 that allows you to displaysummarized statistics, optionally narrow down the data by field name, and then exportthe data to XML, CSV, or YAML formats. The goal of this chapter will be to incorporatea 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 thischapter. Some background is needed to understand the example, so we’ll begin with ahigh level tour through the various aspects of Ruport. Then we’ll come back to theexample and explain it in detail.

def generate_reporttable = 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 }

endend

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

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

5

Page 24: Ruport Book 1.1.0

1.4 Ruport’s Data Structures

Ruport uses four basic data structures: records, tables, groups, and groupings. These areall contained in the Ruport::Data module. Thus, the fully qualified class names areRuport::Data::Record, Ruport::Data::Table, Ruport::Data::Group, andRuport::Data::Grouping. When we use the terms record, table, group, or groupingwithout 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 inRuport is the table. Records are used to build up a table, while groups and groupings areused, as the names imply, to group tabular data. We will save the discussion of groupsand groupings until we finish exploring the behavior of tables.

Records are the most basic of the data structures and generally correspond to a row ofdata from a database, or other base collection of data. You create them using either anarray 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 supplyattribute names) you can use either hash-like or accessor notation b["ruby version"] orb.ruby version.

Ruport tables are collections of records and as mentioned previously, they form the basisfor much of Ruport’s functionality. They also provide a variety of methods for workingwith the data they contain. In the next sections of the tutorial, we will demonstrate howto 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 collectsome data from the Tattle database, which will allow us to avoid using most of thetechniques presented in this section, since we can constrain the data as it is beingcollected. However, to demonstrate the capabilities of Ruport in the context of datamanipulation, we won’t do that yet. Instead, we’ll manually create tables containing thedata of interest. The techniques we demonstrate in this section, however, are generallyapplicable, 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 atable with some of the columns expected for the Tattle data.

table = Table(%w[ruby_version host_vendor build target_cpu archrubygems_version host_os host_cpu])

6

Page 25: Ruport Book 1.1.0

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 inRuport. First, we probably want to get rid of some of the extra columns containing nodata. 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. Nowyou 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 timewith 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 improvementwould be to use the remove columns method, which allows you to remove multiplecolumns at once.

7

Page 26: Ruport Book 1.1.0

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

Still, with the number of columns we need to remove, you don’t really want to list out allthe 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. Thereduce method (and its alias sub table!) does the same thing, but modifies its receiverin-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 whenremoving columns, when renaming columns Ruport gives you both rename column andrename 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 todo that. You can swap the positions of two columns with the swap column method or doa complete reorder of the column positions using the reorder method. For now, let’s justswap the first and second columns, so that the RubyGems version is listed first.

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

8

Page 27: Ruport Book 1.1.0

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 thatdoesn’t exist in your database, you can do that using Ruport as well. This is generallyuseful for doing some type of calculation on the existing data, but it can be used to addany 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 anexample, 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 canjust as easily work with the individual rows as well. You can add and remove rows ofdata 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 fewrows 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

Page 28: Ruport Book 1.1.0

+-------------------------------------------------------------+| 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 canalso supply a code block which allows you to limit the number of rows returned. Theblock should implement a boolean statement which determines whether a row will be keptin the result set. For example, you could easily remove the rows for RubyGems version0.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 usingthe methods that Ruport supplies. There are many more that aren’t described here, butthese 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 twodata structures to assist in such operations, the group and the grouping. A group, on itsown, is marginally useful. It is basically a table with a name, and inherits from the Tableclass, so it has all of the capabilities of a Ruport table. It does add a name attribute thatyou can use to give the group a name when you create it.

10

Page 29: Ruport Book 1.1.0

Here is an example of creating a group from the table we were using in the previoussection. A table can convert itself to a group with the to group method by providing agroup 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 theaddition of the group name as a heading.

The real power of groups is seen when we use them as components of another Ruportdata structure, the grouping. A grouping is basically a collection of groups. The data fora grouping is a hash of groups keyed on the group names. The capabilities of a groupinggo 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 ashortcut Kernel method (Grouping) to achieve the same thing. The following areequivalent 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

Page 30: Ruport Book 1.1.0

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 formatterwill output each of the groups in the grouping. You can group on more than one columnby 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’senough to recognize that creating a grouping from a table or group will create a hash ofdata where each key is a unique data point from the grouping column and each value is agroup consisting of the remaining columns and all rows matching the key value in thegrouping column.

This more-or-less covers the key points about data manipulations in Ruport, so we canmove on now. In the next section, we’ll look at how you can use Ruport’s built-incontrollers 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 actuallyproduce some output. In this section, we describe the basics of creating output from thestandard data structures using Ruport’s built-in formatters. Ruport also includes asystem for defining your own output formats, and we’ll look at that briefly.

4See the Data Manipulations cheatsheet for more detail on multilevel groupings.

12

Page 31: Ruport Book 1.1.0

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 andrendered using the built-in text format tools supplied by Ruport. All the data structuressupplied with Ruport have built-in formatters that can create output in a variety offormats (Text, CSV, HTML, and PDF).

With Ruport’s data structures, it couldn’t be easier to produce output in differentformats. We already used the text format for a Ruport table implicitly when we calledputs table, but you can achieve the same result with the to text method. Similarly, toproduce CSV output, you can call the to csv method (or to html or to pdf for the otherformats). 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 Vendor0.9.2,1.8.5,darwin8.8.2,apple0.9.2,1.8.6,darwin8.8.2,apple0.9.2,1.9.0,darwin8.8.2,apple0.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

Page 32: Ruport Book 1.1.0

<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 theway they look or if you have specific needs, you can define your own formats. You don’thave to do so in order to use Ruport, but soon enough you’ll probably encounter asituation 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 theimplementation of those steps for one or more output formats. The following is anexample of how you can define another formatter for the table we’ve been using. It willoutput the table in the same text format we’ve already seen, but add a header. It’s not ofmuch 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 withthe stage class method. This will tell the formatters that if methods namedbuild header and/or build table are implemented by the formatter, then the controllerwill call them during the process of creating the output (although neither is required tobe present).

Ruport defines a convenient syntax for creating your formatter methods. You can use theclass method build with the name of the stage (e.g. build :header) and an associatedblock that will become the body of the method. We’ll use this syntax throughout thebook; however you can get the same effect by defining your own “build ” methods (suchas “build header”).

We’ll implement the formatter for a format named :text. The name is essentiallyarbitrary but it’s a good idea to use names that provide some identification of the outputbeing produced.

14

Page 33: Ruport Book 1.1.0

class TextController < Ruport::Controllerstage :header, :table

class Text < Ruport::Formatterrenders :text, :for => TextController

build :header dooutput << "Example Report\n\n"

end

build :table dooutput << data.to_text

endend

end

The line in the formatter, renders :text, :for => TextController names the formatand registers itself with the controller as the formatter for that named format. To createthe output, you use the render method. In fact, all of our previous examples were reallyshortcuts 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 beneficialto inherit from them rather than directly from Ruport::Formatter. The topic ofcontrollers and formatters will be covered in much more detail later in the book, but you

15

Page 34: Ruport Book 1.1.0

should now have a general understanding of how they work. As we mentioned, we’ll beusing the acts as reportable module to create the report, so let’s take a look briefly athow 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, youcan use the Query class, which relies on the Ruby DBI library. Second, you can hook intoActiveRecord using Ruport’s acts as reportable module, and this second option is whatwe’re going to use in our example. Basically, acts as reportable uses the results of anActiveRecord::Base.find() to prepare a Ruport data table.

Since we want to use acts as reportable to integrate Ruport into a Rails application, wewill show you how to install and use it in that context. You can, however, useacts as reportable in any environment as long as ActiveRecord is loaded. The first thingyou need to do is ensure that both Ruport and acts as reportable are installed asdescribed above in the Installation section. Then, the easiest way to hookacts 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 ifActiveRecord is already loaded, which will be true for a Rails application. Next, let’s seehow 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 thereport. 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 weneed 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 yourActiveRecord model class definition. This provides your model class with a fewRuport-specific methods to use, the most important being report table, which worksgenerally like an ActiveRecord::Base.find() with a few extra options, but returns aRuport table. That allows you to use all of the facilities of Ruport to manipulate andformat the generated table.

class Report < ActiveRecord::Baseacts_as_reportable

end

16

Page 35: Ruport Book 1.1.0

This gives the Report model the ability to collect its data into a Ruport table. If weassume that the Tattle reports table contains the same data as we populated manuallyearlier, 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 usingacts as reportable, but we’ll leave that for later in the book.5 Once you get your datainto 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 datausing acts as reportable or some other method. This concludes the backgroundinformation you need to understand the real report we’re going to build using the Tattleproject 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. Thisreport is included in the examples packaged with Ruport (tattle rubygems version.rbin the examples directory). There is also a dump of the Tattle database in theexamples/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 detailedexplanation. In a Rails setting, the following method should be placed in a controller, inthis case reports controller.rb.

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

5See the acts as reportable cheatsheet for more detail.

17

Page 36: Ruport Book 1.1.0

: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 }

endend

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 Ruporttable from an ActiveRecord find. It can use all of the options available to find tocustomize the data set. We use the :conditions and :group options that just get passedalong to the find method, Ruport has no awareness of them. In this case, we look for auser 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 acolumn 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 atable, rather than the techniques we showed earlier to eliminate columns after we havethe full table. Either way would work, though.

We now have a Ruport table to work with. The table contains all of the Tattle data thathas a user key, with only the columns host os, rubygems version, and user key. Next, wewant to do some grouping of the data.

18

Page 37: Ruport Book 1.1.0

We create a Ruport grouping from the data table with the next line. This will group allof 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, weneed to take another pass through the data. We set up an empty table to hold our finaldata - we’ll add rows later as we do the final grouping. We can create an empty tableusing 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 }

endend

For each group, we create another grouping by the rubygems version column. This willgive 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 ispopulated 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 populatedwith 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 RubyGemsversion 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 platformswith 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 bymethod. This method can take a column name, array of column names, or code block todefine how to do the sort. In this case, we use the column name “count” and tell Ruportto sort descending using the :order option.

We finally group the sorted table by platform. This gives us the final grouping that wewant to output. We can output the grouping in any of the standard formats we describedearlier, but in this case we want to provide a PDF.

19

Page 38: Ruport Book 1.1.0

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: collectthe data, manipulate it in some way (in this case, grouping and sorting), and then renderthe data to output. This is what Ruport was designed to do. Of course, many examplesmight require more complicated formatting than what we used here and the details ofdata collection and manipulation will vary, but the basic flow will be the same.

20

Page 39: Ruport Book 1.1.0

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

21

Page 40: Ruport Book 1.1.0

22

Page 41: Ruport Book 1.1.0

Part II

Working With Ruport

23

Page 42: Ruport Book 1.1.0
Page 43: Ruport Book 1.1.0

Chapter 2

Working With Ruport

In the introduction, you saw that Ruport provides the raw materials to make quick workof simple reports. This is great, but there’s probably a Perl hacker or two in the crowdwondering what the big deal is. For ad-hoc reporting, Ruport’s strength is primarily thatit is more convenient than dealing directly with the underlying libraries it wraps. Thiskind of convenience is nice, but wouldn’t be worth writing a book about if it were all wehad to offer

Ruport is really about fitting reporting functionality into wherever it is needed, not justwhere it is convenient to do so. Often, this means integration with some overarchingbusiness application. If you’ve experienced the disgusting rat’s nest that is ‘reportinggone wrong’ first hand, you’ll know exactly what we mean when we say that someconsistency 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’llfind it easy to find another path that works well for you even if it’s not the one that’smost common.

We’re now going to embark on the discussion which forms the core of the book, and thatis a blow-by-blow commentary of Ruport in action within one of the applications whichhelped shape the toolset itself. This system is a simple payroll application, whichthroughout its short lifetime has migrated from the console to Camping1 and finally cameto rest as a Rails 2.0 app.

Though we won’t be covering every single step that was involved in creating thisapplication, we will show all the interesting bits and use them as entry points for furtherinvestigation as to how Ruport is used by its developers. This will hopefully help youapproach many different problems in your own work.

The book is actually based off of a somewhat simplified version of our actual productionsystem, which has been modified a bit to make it more suitable for teaching Ruport. If

1http://camping.rubyforge.org

25

Page 44: Ruport Book 1.1.0

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 SystemUsing Ruport and Rails

PayR is your classic in-house application. It’s a little rough around the edges, the UI issimple and basic, the feature set is exactly what the users need, and nothing more. If wewere 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 thefundamentals. Though a payroll system isn’t quite a “reporting application”, it’s easy tosee 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 prettymuch any place you want to put it. Some of the reports in PayR are absolutely trivialand 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

Page 45: Ruport Book 1.1.0

unless @[email protected]_column("employee.name", "Employee")@table.rename_columns { |c| c.titleize }

endend

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’tclocked out yet, as well as their names.

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

2.3 Generate a PDF Before Your Database is EvenInitialized

Using acts as reportable to handle your tabular data opens up a lot of doors and reallymakes things easy to extend and change down the line as needed. This is principallybecause it standardizes your data so that Ruport can be blissfully ignorant of where itactually came from.

To get a sense of this, we can take a quick look at some of PayR’s timesheet generationcode, specifically the PDF formatter:

class PDF < Ruport::Formatter::PDF

renders :pdf, :for => TimeSheet

27

Page 46: Ruport Book 1.1.0

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

}

pad(20) dodraw_table data.week1_datashow_overtime(data.week1_overtime)

end

draw_table data.week2_data

show_overtime(data.week2_overtime)

render_pdfend

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

endend

end

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

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

28

Page 47: Ruport Book 1.1.0

the data structure returned by SomeActiveRecordModel.report table() andTable("some.csv").

This is quite powerful, and we tend to use it readily in our day-to-day work. If a client hassome sense of how a report should look, we happily ask them to pull up Excel and mockout some data for us. We can then use that to start working on formatted reports beforewe even have a database populated. This leads to the great feeling of already having someuseful 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::Hooksrenders_with TimeSheetdef 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’sformatting 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 doingformat-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 youcan of course check it out in PayR’s source. Usually in the prototyping phase, thecontroller might look very simple:

2If you’re interested, consider checking out the custom controller logic cheatsheet.

29

Page 48: Ruport Book 1.1.0

class TimeSheet < Ruport::Controllerstage :time_sheet

module Helpersdef start_dateDate.today

end

def end_dateDate.today + 14.days

endend

end

Believe it or not, combined with the PDF formatter we’ve shown here and theMockTimeSheet 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 havesome nice data from your client to work with, use that. If you don’t have data yet, justpopulate it with some junk, the real goal is just to get something on the screen as fast aspossible.

When you run this mocked-out report, it doesn’t look particularly pretty, but you can seeall the key elements are there:

Rather than doing your stylistic tweaking directly in the formatter, it’s usuallyrecommended to use Ruport’s templates. These let you externalize the look-and-feelcomponents of your report and re-use them as needed. We will cover these in-depth lateron in the discussion, but for now, rest assured that you can easily make things prettyfrom 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 theCRUD, but we might as well try to view this mock data through the browser as well.

30

Page 49: Ruport Book 1.1.0

2.4 Using ERB for HTML Reports

For better or worse, most HTML generation in Rails is done through ERB. It turns outthis is a reasonably practical way to do dynamic HTML generation. A major criticism ofRuport is that doing string concatenation in the HTML formatters seems to take people alittle 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::HTMLrenders :html, :for => TimeSheet

build :time_sheet dooutput << 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, whichmeans you can mostly treat the HTML formatter as a shim to give you a Rails view thathas some reporting pre-processing done for you.

From here, you can reach your Ruport data, options, and helper methods as needed. Thefollowing 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

Page 50: Ruport Book 1.1.0

<% end %></tr>

<% end%>

</table>

The nice thing about using an approach like this is that your HTML view code isseparate from the core formatter and can be updated as needed. Also, you can see herethat 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 thepath to your ERB file as needed if you’ve not yet created your Rails app, and then runthis:

@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 matchingformatter defined via the renders :format, :for => Something call. This means youcan easily use this shortcut with any of your custom formatters, even if they’re not one ofthe 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 ableto 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 acomprehensive idea of how you’ll integrate Ruport into your projects. We will now takeyou through two more PayR reports, this time with a whole lot more attention to detailand comprehensive explanations of how things actually work. Though we’ll circle backaround after that to talk about some of Ruport’s bells and whistles, the extensivediscussion of the formatting system to follow will quickly bring you up to speed on one ofthe most important aspects of Ruport development.

32

Page 51: Ruport Book 1.1.0

Chapter 3

Report Formatting

3.1 Introduction

One of the reports needed for PayR is a call-in sheet that lists employees, grouped bytype, and their hours over a two week period. Below is the formatted output from thisreport:

In this chapter, we’ll look in detail at how we created this report. The first steps will be todefine a controller and one or more formatters, but since this is a Rails project, you firstneed to decide where to put the files in the Rails directory structure. There’s no singlebest location; some people prefer to place them in app/reports and others in lib, sothat decision is ultimately up to you for your own projects. The code for all of the reportsin 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 anidea 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 arealso various other models used to hold employee time data. You can find the migrationsused to create the database tables in the PayR source.

33

Page 52: Ruport Book 1.1.0

class Employee < ActiveRecord::Basehas_many :regular_timeshas_many :lunch_timeshas_many :other_times

end

class RegularTime < ActiveRecord::Basebelongs_to :employee

end

class LunchTime < ActiveRecord::Basebelongs_to :employee

end

class OtherTime < ActiveRecord::Basebelongs_to :employee

end

For the report, we need to get all of the employees and calculate some of the data to bedisplayed. 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 thereport. This is the component that establishes the steps that will be called to generatethe report’s output. It also allows you to specify options that should be populated and todo some setup and manipulation of the data prior to formatting. A first pass at defining avery 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 ofRuport::Controller. Doing so gives your class access to the functionality of Ruport’srendering system. This particular class then specifies that one stage will be called duringthe rendering process.

This means that Ruport will look for a method named build call in sheet (formatterhook methods are named build plus the name of the stage) in the formatter and, if itexists, will call it. In your formatter, you can either define the build methods directly or

34

Page 53: Ruport Book 1.1.0

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 amethod for a particular stage isn’t found, Ruport will simply ignore it. This becomesuseful when you want to use a single controller with multiple formatters, some of whichmight 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 columnheadings: “Employee Type”, “Employee,” “Week 1,” “Week 2,” “Regular Hours,”“Overtime,” “Lunch,” “Personal,” “Sick,” and “Vacation.” If you look at the columns inthe employee table defined by the migration, you can see that employees havefirst 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 theoutput using Ruport, we won’t go into detail on all of the techniques used to generate thedata other than to say that the Employee model is provided with an employee recordmethod 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 expectsa start date as a parameter. The report outputs two weeks worth of employee time sheetdata, beginning with the week containing the start date. What this means for our reportis 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 sharean options object, implemented as a subclass of OpenStruct. This allows you to usenamed options at any point in the rendering or formatting process. If you have somerequired data that is to be supplied by the code that renders the report, and want to besure of its presence, you can use the required option class method. Ruport will raise anexception if the data for any required options are not supplied.

35

Page 54: Ruport Book 1.1.0

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 avalue 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 fromall of the employee records and then create a Grouping of the data. In order to providesome 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_groupingtable = 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) }

endtable.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 isused to populate an instance variable named start. The to grouping method does therest of the data manipulation.

We need to create a Ruport table from the employee data and to do so, we use one of theshortcuts 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 ofthe employees and then iterate through them, calling the employee record method on

36

Page 55: Ruport Book 1.1.0

each one and appending the returned data as a row in the table. The block associatedwith Table is passed a Data::Feeder object which will be explained in more detailelsewhere,1 but for now, just know that you can use the << method to add rows to thetable.

After the table is created, we use Ruport’s rename columns method to give the columnsthe 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 thenew column names. The next call to rename columns uses the block form and calls to sand titleize for each of the column names.

The last data manipulation step is to group the data by creating a Ruport Grouping. Weuse the Kernel method Grouping to create it. This method takes two paramaters; a tableor group that will be used as the source of data to create the Grouping and the name ofthe 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 finalreport. Next, we set this grouping as the controller’s data source, as follows:

class CallInController < Ruport::Controller

stage :call_in_sheet

def setupself.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 onthe newly created CallInAggregator object. Finally, the resulting Grouping is set as thecontroller’s data attribute. The data attribute is available in both your controller and anyassociated formatters to hold the report’s data.

Note that this code is contained in a method named setup. The setup method is specialin 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 thecontroller, allowing you to do data manipulations or anything else that you might need toaccomplish prior to actual formatting.

1See the Data Manipulations cheatsheet for more detail.

37

Page 56: Ruport Book 1.1.0

3.7 Using the Helpers Module

We need to add one more thing to the controller before moving on to the formatter. Ourreport is going to have a header that includes the date range. We want these dates to bein a specific format, so we use the strftime method to output them in the properformat. 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 outputformats) or even in multiple locations within a single formatter? This is where you canuse a special module called Helpers, as follows:

class CallInController < Ruport::Controller

stage :call_in_sheet

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

end

module Helpersdef start_dateformat_date(options.start_date)

end

def end_dateformat_date(options.start_date + 13.days)

end

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

endend

end

We define start date and end date methods, containing the code to calculate andformat the dates, within the Helpers module. If present, Helpers will be mixed in to theformatter (or formatters). This allows you to call any of the module’s methods in yourformatters 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 theoutput formats you want to produce. Each one will register itself as the formatter for a

38

Page 57: Ruport Book 1.1.0

particular named format. For our report, we want to be able to produce both PDF andHTML 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, ifyour 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 ofoutput. The reason is that each of the built-in formatters contains predefined helpermethods 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 subclassingRuport::Formatter::PDF and Ruport::Formatter::HTML, respectively. The formatterscall the renders class method in order to register themselves with the controller. Eachsupplies 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 stagefor the report (:call in sheet), so our formatter will need to supply an implementation forthe :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 dopad_bottom(10) doadd_text "Call In Sheet (#{start_date} - #{end_date})"

endrender_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 formatterand, as its name implies, adds the specified amount of space to the bottom of the outputcreated within the associated block. There are many of these types of helpers in the PDFformatter to assist with properly positioning and drawing output on the page, suitable for

39

Page 58: Ruport Book 1.1.0

printing. Another is the add text method that we use to output the header. Note thatwe use the start date and end date methods that we defined earlier in the Helpersmodule.

Finally, we call the render grouping method. Since the data that we want to output is aGrouping object and since groupings have their own predefined controller, we can use thismethod to pass off the work to the built-in Grouping controller. We supply it with thedata attribute as the first parameter, which you will recall was set to the Groupingcreated by the CallInAggregator.

The second parameter consists of the formatter options object, which we convert to ahash. We need to pass these along because we’re asking a different controller to createoutput for us, so we need to make sure it has all of the options that were sent to the maincontroller, in case it needs to use some of them. In the case of a PDF, we also need to domore - 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 ofPDF::Writer. It is a representation of the PDF document we’re creating and as such, wewant all of our output to be contained in this document. If we don’t include it in theoptions, the formatter for the Grouping controller will create a new pdf writer objectand we won’t get the expected results. Other output formats won’t generally have thisconcern.

Below is the formatter for the HTML output:

class HTML < Ruport::Formatter::HTML

renders :html, :for => CallInController

build :call_in_sheet dooutput << 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 thebuilt-in HTML formatter and uses RedCloth to evaluate the string provided in textileformat. Next we call to html on the data. This is a shortcut method to render theGrouping as HTML output. The built-in formatters define a number of different outputstyles 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 theoutput, right? In fact, we could do so, but let’s do one more thing first. You may have

40

Page 59: Ruport Book 1.1.0

noticed, particularly with the PDF formatter, that we didn’t define any formattingoptions, such as font size, alignment of data in the columns, page layout, etc. If we don’tdo anything else, Ruport will use default values to format the report. This may or maynot be what you want, so you need to experiment with the built-in default formats to seeif 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’tbe very portable or very DRY. You would need to define the options every time yourender the report. Instead, Ruport gives you the option to define formatting templates.

Templates are useful in many different situations. They basically allow you to predefineany number of options and have those options available to your formatter. This is helpfulif you want to render the same report in multiple locations or if you want to define aconsistent set of options to use for a number of different reports.

You define a template by using the create method of Ruport::Formatter::Templateand 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

Page 60: Ruport Book 1.1.0

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

3.10 Producing Output

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

class ManagerController < ApplicationController

def call_in_sheetpdf = 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 thestart date option and the name of the template. We assign the output to a variable calledpdf and then use the send data method to stream the output to the user’s browser. Wesupply the type of the output as being “application/pdf” and, although optional, wedefine the disposition and filename. Rendering HTML format is similar, but you canrender the output directly rather than streaming it with send data.

This chapter provided an overview of Ruport’s formatting system through a real-worldexample from the PayR project. For more on rendering and formatting, you can refer tothe cheatsheets.3 The next chapter will look at another of PayR’s reports in order todemonstrate more of Ruport’s formatting capabilities, including graphing support.

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

42

Page 61: Ruport Book 1.1.0

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 slightlymore complicated reports. In general, by supplying an employee and range of dates, wewant to be able to generate a report of that employee’s weekly regular hours worked. Theoutput of the report should show a graph of the hours worked over the supplied timeperiod, 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 looklike this:

43

Page 62: Ruport Book 1.1.0

For the examples in this chapter, we’re going to use Ruport’s graphing support, providedin the ruport-util package. This package contains support for a few different graphinglibraries and in this case we used the Gruff library. Currently, Ruport only providesintegration 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 thereport. We’ll develop the report iteratively, to show how you can build up a report fromdifferent components of Ruport as well as its supported packages. As you saw in the lastchapter, we need to start by defining a controller and formatter (for this report, we onlydefine a PDF formatter). Start by stubbing out the different parts of the report you knowyou’re going to need and then you can add the details later.

class WeeklyTimeController < Ruport::Controllerstage :week

class PDF < Ruport::Formatter::PDFrenders :pdf, :for => WeeklyTimeController

build :week doend

end

end

This defines the structure of the report. It has a controller with a single stage of :weekand a single PDF formatter that builds the stage. You might find later that you needmore stages to separate out components of the report, that you need to predefine certainoptions as being required, or that you want additional formats, but most reports youcreate will have at least this basic structure.

Before you move on to writing any formatting code, you need to think about how you willobtain the data needed for the graph and the table. Since the report’s requirements statedthat you should be able to supply the employee and range of dates, we will assume thatthe code (in the controller) provides an employee id, start date, and end date as optionsto 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 ofthe employee. This report also uses the Employee model as the basis for its data, whichwill need to be queried for the employee name. You already saw how to add text to a

44

Page 63: Ruport Book 1.1.0

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

class WeeklyTimeController < Ruport::Controllerstage :week

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

end

class PDF < Ruport::Formatter::PDFrenders :pdf, :for => WeeklyTimeController

build :week dopadding = 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 ourreport. We first find the employee from the id (options.employee) supplied to thecontroller. Then we set another option to hold the employee’s name for our formatter touse. As you see, you can use the options to provide outside information to the controlleras well as to communicate information between the controller and the formatter sincethey share their options object.

In the formatter, we fill in some of the build :week stage. We add the title “EmployeeTimes By Week,” using the add text method supplied by the built-in PDF formatter andset the font size and center-justify the text. We also add the employee name and use thepad method to add some padding between the text.

Notice that we set the font size and justification by passing these options directly to theadd 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 donethis:

45

Page 64: Ruport Book 1.1.0

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. Forour report, though, since we change the font size and justification after each call toadd text, it’s just as easy to supply the options directly to the method. The othertechniques, options.text format and templates, are best saved for situations where youhave a setting that is global to the document or at least is used for a significant portion ofit.

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 moveon to the graph. Adding the table gives us this for our controller and formatter:

class WeeklyTimeController < Ruport::Controllerstage :week

def setupe = Employee.find(options.employee)options.emp_name = e.namehours = 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::PDFrenders :pdf, :for => WeeklyTimeController

build :week dopadding = 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

Page 65: Ruport Book 1.1.0

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

endend

end

There are a few things to emphasize with the changes. First, notice that the table datacomes from a method (regular hours for range) of the Employee model. This methodobtains the employee’s regular hours worked for the date range we want (supplied by thestart date and end date options). Of course, we haven’t written this method yet, butwe’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 realizehere is that the options we provide to this method, such as :position and :width are beingpassed along to PDF::Writer (PDF::SimpleTable to be more accurate). Since Ruportuses PDF::SimpleTable behind the scenes to create the table, you can set any of theattributes available to PDF::SimpleTable by supplying options with the same name. Theleft boundary method is supplied by Ruport’s PDF formatter and returns the x-positionat the left margin.

Although you can set column options manually by creating PDF::SimpleTable::Columnobjects, Ruport supplies an abstraction in the draw table method. You can set the:column options to a hash containing any of the attributes available toPDF::SimpleTable::Column (again using the same names as the attributes). This isusually much easier than creating your own Column objects. Using this technique, we setthe 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 thetable 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 theEmployee model. It expects a start date and end date for the range of weeks to include inthe results. It collects data by week from the beginning of the week containing the startdate to the end of the week containing the end date. Finally, it returns a hash of data forthe regular hours worked by the employee per week, keyed on the beginning date of eachweek.

def regular_hours_for_week(date=nil)date ||= Date.todaystart = date.beginning_of_week

47

Page 66: Ruport Book 1.1.0

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_weekwhile date <= end_dateres.merge!(date => regular_hours_for_week(date))date += 7

endres

end

4.5 Adding a Graph

Now that the report contains the text and table, we can add the graph to the page. Thisdoesn’t require anything much different than what you’ve already seen, but we will beusing the ruport-util library for its graphing support. As stated earlier, ruport-util wrapsportions 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 somespecialized behavior, mainly the addition of a series method that allows you to add linesto the graph. Ruport also supplies a Kernel method called Graph as a shortcut forcreation of the graph.

The updates to the controller and formatter to include the graph are as follows:

class WeeklyTimeController < Ruport::Controllerstage :week

def setuprequire ’ruport/util’

e = Employee.find(options.employee)options.emp_name = e.namehours = 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

Page 67: Ruport Book 1.1.0

g = Graph(weeks)g.series values, e.nameoptions.graph = g

options.labels = weeks.inject({}) {|l,w|l.merge!(l.size => w)

}end

class PDF < Ruport::Formatter::PDFrenders :pdf, :for => WeeklyTimeController

build :week dopadding = 40x = 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 = 300height = 225y = 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 })

endend

end

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

49

Page 68: Ruport Book 1.1.0

In the formatter, we need to do some work to maintain the flow of the document. Addingan image to a PDF using the draw graph method (which we use here) places the image inan absolute position and doesn’t move the cursor. Therefore, since we want to add itabove the table, we need to do some calculations to make sure the position of the graph iscorrect 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 yposition for placement of the graph. PDF coordinates are calculated from the bottomleft, so to get the y position, we take the current location of the cursor and subtract theheight of the graph.

Then we use the draw graph method of ruport-util to add the graph to the page and wesupply the graph as well as a hash of options. The options allow you to specify theposition and size of the graph (with :x, :y, :width, and :height), as well as to set somecharacteristics of the graph itself. We add a graph title and use the labels we savedearlier. In addition to the options we set, you can also use the :min and :max options toset the minimum and maximum values for the graph.

Once the graph is positioned and drawn, we need to make sure the table is positionedbelow 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 thedefined padding.

4.6 Adding a Border

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

class PDF < Ruport::Formatter::PDFrenders :pdf, :for => WeeklyTimeController

build :week dooffset = 3margin = offset * 6padding = 40x = 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

Page 69: Ruport Book 1.1.0

width = 300height = 225y = 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 - offsetbottom = bottom_boundary + offsetleft = left_boundary + offsetright = right_boundary - offsetmove_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)

endend

end

We define an offset that will be the distance between the two lines of the border. We alsouse the offset to calculate a margin width to move everything already placed on the pageaway from the borders. Finally, we define a page border method to draw the lines for theborder, 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 thatthey don’t overlap the border. We also move the cursor down below the border beforeadding the title text. Finally, we change the method used to add the employee name fromadd text to draw text so that we can specify its x position. We don’t need to do so withthe 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 PDFformatter, such as horizontal line and vertical line at, to show you the manualtechniques needed to draw the border. This will be of benefit if you need to draw lines ona PDF page, but there is an easier way to accomplish border drawing by using theFormHelpers module included in ruport-util.

51

Page 70: Ruport Book 1.1.0

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

class WeeklyTimeController < Ruport::Controllerrequire ’ruport/util’

#...

class PDF < Ruport::Formatter::PDFinclude 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))

endend

end

4.7 Rendering the Report

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

class ManagerController < ApplicationController

def weekly_time_reportpdf = PDF::Writer.new

ec = Employee.count - 1

52

Page 71: Ruport Book 1.1.0

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 == iend

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 PDFfor each employee. For this reason, we need to instantiate our own PDF::Writer objectand pass it to the controller. As discussed in the previous chapter, if we use the defaultbehavior, 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 itlooks 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 tasksyou’ll encounter in your daily work. We’ll now jump into another aspect of Ruport, whichis quick and dirty ad-hoc reporting.

53

Page 72: Ruport Book 1.1.0

54

Page 73: Ruport Book 1.1.0

Chapter 5

Ad-hoc Reporting with rope

In the last few chapters, you’ve seen how deeply Ruport can be extended. You’ve alsoprobably caught a glimpse of how it is reasonably easy to get everything wired up in Railsapplication without acquiring a tacked-on feel.

In this chapter, we’ll turn all of those concepts on their head, and cover what Ruport wasoriginally designed for: quick and dirty ad-hoc reporting. Though the following examplehas been simplified to make it easier on the eyes, I’m sure you can imagine plenty ofscenarios where you want a quick CSV report without much hassle, and that’s what you’llsee how to do here.

We’re going to take a look at how to use ruport-util’s code generator, rope, to quicklygenerate configuration files and reports for a standalone environment. We’ll then makeloose 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 fromPayR to a MySQL database that contains appointment data.

Our report will show us when scheduled vacation days in PayR conflict with employeeappointments. The output format will be a simple CSV of names and dates, which will beemailed to managers on a nightly basis. Though we could have done all of this within ourRails application, the idea here is we may end up doing a whole lot more of these ad-hocreports, 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_reportscreating directories..lib/nightly_reports/test

55

Page 74: Ruport Book 1.1.0

lib/nightly_reports/configlib/nightly_reports/outputlib/nightly_reports/datalib/nightly_reports/data/modelslib/nightly_reports/liblib/nightly_reports/lib/reportslib/nightly_reports/lib/controllerslib/nightly_reports/sqllib/nightly_reports/util

creating files..lib/nightly_reports/lib/reports.rblib/nightly_reports/lib/helpers.rblib/nightly_reports/lib/controllers.rblib/nightly_reports/lib/templates.rblib/nightly_reports/lib/init.rblib/nightly_reports/config/environment.rblib/nightly_reports/util/buildlib/nightly_reports/util/sql_execlib/nightly_reports/Rakefilelib/nightly_reports/README

We now need to configure our database connections and set up a mail server. We do thisin the project’s config/environment.rb. Keep in mind that this is your ropeapplication’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 => "[email protected]",:user => "[email protected]",:password => "alpha123",:auth_type => :plain,:port => 587

)

require "active_record"

56

Page 75: Ruport Book 1.1.0

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 thisisn’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 detailscan 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 toestablish a connection. Here we’re telling it to use the MySQL driver, and attach to adatabase called dental db on the localhost using the MySQL root user.

From here, we establish our SMTP settings, since we’ll be mailing the reportautomatically:

Ruport::Mailer.add_mailer(:default,:host => "smtp.gmail.com",:address => "[email protected]",:user => "[email protected]",: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 usingGMail complicates things a bit, and though we won’t go into details here, you can checklib/init.rb for details.1

Once we’ve established our email settings, we take a little shortcut to get our appworking with our Rails models:

require "active_record"require "ruport/acts_as_reportable"

require "#{RAILS_ROOT}/config/environment"

1Net::SMTP has some issues, which are addressed by a monkey-patch from Stephen Chu. We found thisfix at: http://www.stephenchu.com/2006/06/how-to-use-gmail-smtp-server-to-send.html

57

Page 76: Ruport Book 1.1.0

This establishes a connection to the Rails database and lets us skip a bunch of hardwiring of model files and manual loading of dependencies. It’s certainly a hack, so yourmileage may vary. You’ll need to set your RAILS ENV environment variable in whateverscript ultimately runs the scheduled report, but it will default to development, which iswhat 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 areworking:

$ 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 ourRails 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 togenerate our report file.

$ rake build report=vacation

reports file: lib/reports/vacation.rbtest file: test/test_vacation.rbclass 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:

2See the rope Cheatsheet for more details.

58

Page 77: Ruport Book 1.1.0

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 somewhatfamiliar. However, if you haven’t, no worries.

We can start with a simple example to help clarify things, then take a look at the fullreport.

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("[email protected]") do |m|m.subject = "Vacation Conflicts Report"m.attach "output/vacation.csv"

endend

As you can see here, the Ruport::Report class just provides some helper methods tomake producing reports a little easier.

The first thing to notice is our renders as table call. What we are telling our reportobject here is that it will pass the results of renderable data to Ruport’s tablecontroller. This means it expects the return value to be a Ruport::Data::Table, or atleast 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]

end3See the Custom Controller Logic cheatsheet.

59

Page 78: Ruport Book 1.1.0

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 thespirit 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 theCSV:

Vacation.generate do |report|report.save_as "output/vacation.csv"report.send_to("[email protected]") do |m|m.subject = "Vacation Conflicts Report"m.attach "output/vacation.csv"

endend

As you’ve seen in previous examples, Ruport is capable of inferring which formatter touse based on the file name you give it. The code here simply tells Ruport to render aCSV from the results of renderable data, and then email it as an attachment to theaddress specified.

If we wanted to do some format-specific hackery, we could use the format argumentpassed to renderable data which in this case would evaluate to :csv. Though this canbe 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,c1,2,34,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 ofreport objects. Let’s take a look at it as a whole and then break it down:

60

Page 79: Ruport Book 1.1.0

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"]] }

endend

end

def payr_dataGrouping(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 "[email protected]" do |m|m.subject = "Vacation Conflicts"m.attach "output/vacation_conflicts.csv"

endend

61

Page 80: Ruport Book 1.1.0

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

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"]] }

endend

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 immediatelypassed to the controller.

When we look at the code that’s actually populating the table, we find that it’s prettysimple:

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 recordsgrouped by employee id. For each employee, we pull a list of appointment dates via thescheduling data for employee helper.

We then select any vacation times which conflict with these appointments, and add a rowwith the employee name and the vacation date to the table. If this seems somewhatsimple, 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

Page 81: Ruport Book 1.1.0

def payr_dataGrouping(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 mightif it were a full scale application. In the code above, the first thing to notice is that it issimply 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 viaRuport’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 toinclude the employee association information, and doing a quick transformation fromdatetime 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 thatinclude the rest of an employee’s information. We can now look at the SQL we use tointeract 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 timesfor 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 wouldunderstand query "path/to/my file.sql", among other things. For our needs however,this is more than enough.

63

Page 82: Ruport Book 1.1.0

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

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

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

Vacation.generate do |report|report.save_as "output/vacation_conflicts.txt"report.send_to "[email protected]" do |m|m.subject = "Vacation Conflicts"m.attach "output/vacation_conflicts.txt"

endend

This is one of the many nice things about working with Ruport as opposed to writingone-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, perhapscron, to fire this on a nightly basis. Though we won’t discuss it here, it’s usually assimple 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 sneakthem in to live alongside Rails apps. Some people love this kind of stuff; others prefer tokeep tight integration in their Rails app and wouldn’t want to layer something like thisinto 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 configurationinformation, make reports build off of each other, and also keep your reporting codecleanly separated from the rest of your application. If you end up with situations wherethose 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 pokearound 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 ofnot just the syntax and raw functionality pertaining to Ruport, but also how we developapplications using it.

64

Page 83: Ruport Book 1.1.0

Though we’ll happily admit there are a lot of valid approaches to using Ruport, we thinkthat the toolset best reflects how we are using it. If you’ve gained some insight from thisdiscussion 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 tobe both a quick reference guide and a way to explore features you have not yet workedwith. We hope that combined with the detailed explainations of PayR, these cheatsheetswill provide a solid base for using Ruport in your day to day work.

65

Page 84: Ruport Book 1.1.0

66

Page 85: Ruport Book 1.1.0

Part III

Cheatsheets

67

Page 86: Ruport Book 1.1.0
Page 87: Ruport Book 1.1.0

Chapter 6

Data Manipulations

A powerful aspect of Ruport is that it allows you to work with a small set of core datastructures which can be populated from a number of different sources. It is possible to domost common data manipulations without much effort. Here we’ll cover how to do sortingand searching, summation, averages, and tabular column operations such as calculatedfields. 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 Tableobjects. 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

Page 88: Ruport Book 1.1.0

>> 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

Page 89: Ruport Book 1.1.0

>> 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 sortwill 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 astabilizer.

6.2 Sorting Groupings

Ruport also includes support for sorting of groupings. In the simplest case, you can ordera 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, wesort 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

Page 90: Ruport Book 1.1.0

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 existinggrouping) 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 manycommon 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 assearching criteria.

72

Page 91: Ruport Book 1.1.0

class Person < Ruport::Data::Record

def namefirst_name + " " + last_name

end

end

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

t << %w[Gregory Brown [email protected]]t << %w[Joe Loop [email protected]]t << %w[Alfonzo Stevens [email protected]]t << %w[Gregory Brown [email protected]]

puts t.rows_with_name("Gregory Brown").length=> 2puts t.rows_with(:name => "Gregory Brown", :email => "[email protected]").length=> 1

6.4 Sums and Averages

Basic sums are simple in Ruport via Table#sigma (aliased as sum). Both by-column andby-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 numerictypes, while the block form does not. If you want to avoid implicit conversion, you shoulduse the block form for summations.

Basic averages might be computed as follows:

73

Page 92: Ruport Book 1.1.0

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 manipulatingTable 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

Page 93: Ruport Book 1.1.0

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

Page 94: Ruport Book 1.1.0

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 beencollected, Data::Feeder provides a simple proxy object that allows you to do exactlythat.

>> 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

Page 95: Ruport Book 1.1.0

>> 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 resultset 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

Page 96: Ruport Book 1.1.0

6.7 Summarizing Grouped Data

Grouping data by a certain criteria and then making some calculations on the groupeddata is a common operation. Often these problems take the form of “for each person, giveme 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 ofreport 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 thegrouping 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

Page 97: Ruport Book 1.1.0

>> puts grouping1:

+---------------+| 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 ofthe resultant grouping. Since the output for multilevel grouping tends to beimplementation-specific, you’ll need to create your own formatter if you want to designsuch a report.

However, Ruport does provide several methods to get at the underlying data in thegrouping. You can access each of the groups in the grouping using [] with the name ofthe 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

Page 98: Ruport Book 1.1.0

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

+-----------+| 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 thenewly 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 (butnot necessarily on Grouping objects).

80

Page 99: Ruport Book 1.1.0

Chapter 7

Using acts as reportable

Ruport’s acts as reportable module provides support for using ActiveRecord for datacollection. 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 usecases.

7.1 Loading

When Ruport is loaded, it tries to automatically load acts as reportable as well. In orderfor that to happen, however, there are two prerequisites: acts as reportable must beinstalled and ActiveRecord must be installed and loaded. Otherwise, you can loadacts 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 theacts as reportable class method.

class Book < ActiveRecord::Baseacts_as_reportablebelongs_to :author

def author_nameauthor.name

endend

81

Page 100: Ruport Book 1.1.0

class Author < ActiveRecord::Baseacts_as_reportablehas_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 thatalthough the columns in the tables returned by acts as reportable are generallyunordered, use of the :only option will set the columns to be returned in the order theyare listed in the option’s array. Therefore, the :only option has a secondary function: inaddition 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

Page 101: Ruport Book 1.1.0

>> 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 includedassociations, 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

Page 102: Ruport Book 1.1.0

Any options that acts as reportable doesn’t recognize will be passed along to theActiveRecord 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 toqualify any attributes returned from those models, but when you include SQL in yourquery, 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 datathat is returned in the table. You can pass a Proc or array of Procs to each of theseoptions.

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

84

Page 103: Ruport Book 1.1.0

>> 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 beingsupplied 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 findmethod that is used behind the scenes to collect your data. However, if you want to turnoff eager loading, you can do so with the :eager loading option by setting it to a valueof 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 thereport table method, allowing you to set default options for your reports.

class Book < ActiveRecord::Baseacts_as_reportable :except => [:id, :author_id]belongs_to :author

end

85

Page 104: Ruport Book 1.1.0

>> 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 reportablerecognizes. Any of the four options (:only, :except, :methods, and :include) passed tothe 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 theaar 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

Page 105: Ruport Book 1.1.0

7.6 Find by SQL

Analogous to the ActiveRecord find by sql method, acts as reportable provides areport 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::Tableobjects, so you can use the returned tables just as you would if you created them by anyother method.

Ruport::Query1 provides a raw SQL mechanism if you need legacy integration or wish touse existing queries without mapping to ActiveRecord.

1See the Ruport::Query cheatsheet.

87

Page 106: Ruport Book 1.1.0

88

Page 107: Ruport Book 1.1.0

Chapter 8

Using Ruport::Query

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

8.1 Configuration

Ruport::Query provides methods for storing configuration information for later use. Youcan add a data source by using the add source method. The first parameter names thedata 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 eitherspecify data source parameters or reference another named data source. You can retrieveall of the available named sources using the sources method, which returns them in ahash keyed by the source names.

89

Page 108: Ruport Book 1.1.0

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 mentionedearlier, if you don’t specify any connection parameters, Ruport will attempt to use thedefault data source (or error if there is no default defined). You can get a Ruport tablecontaining 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 :sourceoption 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 specifythe 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 usethe :file option with the filename.

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

90

Page 109: Ruport Book 1.1.0

If you don’t want your data to be packaged into a Ruport::Data::Table, but wouldrather have raw DBI::Row objects, then you can use the :row type option to theconstructor 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 demonstratedpreviously, you can use the result method to get a Ruport::Data::Table containing theresults of executing the query.

>> query = Ruport::Query.new("SELECT authors.name, books.title FROMauthors 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 theexecute 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 FROMauthors JOIN books ON authors.id = books.author_id")

>> puts query.to_csv

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

91

Page 110: Ruport Book 1.1.0

You can iterate through the result set, returning the rows one by one, using the eachmethod.

>> query = Ruport::Query.new("SELECT * FROM books WHEREtitle = ’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 WHEREtitle = ’Farenheit 451’")

>> g = query.generator>> while g.next?; puts g.next.class; endRuport::Data::Record

8.4 Related Resources / Digging Deeper

For a complete list of DSN configurations as well as installation instructions for workingwith various databases, you’ll want to consult the RubyDBI documentation.

Ruport’s acts as reportable module provides similar data collection functionality usingActiveRecord.1

The generator method returns a Generator object - you might want to read the RubyStandard Library API docs to see what you can do with it.

1See the acts as reportable cheatsheet.

92

Page 111: Ruport Book 1.1.0

Chapter 9

Ruport’s Formatting System

A common need in reporting is to be able to display the same data in a number ofdifferent formats. Ruport’s Formatting System provides a highly flexible way to separateyour rendering process from your specific formatting code. This cheatsheet shows how theparts 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 toproduce 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 knowthat 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 alsoworks as expected:

93

Page 112: Ruport Book 1.1.0

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

endend

The power here of course is that even as new formats are added, our external interfacestays the same. We’ll now show how formatters are implemented, to give you an idea ofhow 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 moreController objects. Here we show a simple text formatter for the InvoiceController.

class Text < Ruport::Formatter::Text

renders :text, :for => InvoiceController

build :company_header dooutput << "My Corp. Standard Report\n\n"

end

build :invoice_header dooutput << "Invoice for #{options.employee_name} " <<

"(#{options.employee_id}), generated #{Date.today}\n\n"end

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

endend

build :invoice_footer dooutput << "\n#{options.note}\n\n" if options.note

endend

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

94

Page 113: Ruport Book 1.1.0

attempt to call all of these hooks, it will happily pass over any that are missing. Thismeans that if you did not want to include the company header in a given format, youcould just leave build :company header out of your formatter.

You’ll also notice that the options passed into the Controller can be accessed via theoptions 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.00Sew Curtains | 120.00Fly 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

Page 114: Ruport Book 1.1.0

def layoutoutput << "<invoice>\n"yieldoutput << "</invoice>"

end

build :invoice_body doadd_employee_info

generate_data_rows

add_meta_dataend

def add_employee_infooutput << "<employee name=’#{options.employee_name}’

id=’#{options.employee_id}’/>\n"end

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

endend

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

endend

There are a few things which make this different from our text formatter, but at the heartit is the same general idea.

You’ll notice that we use a layout for this code. This allows us to have some additionalcontrol over the rendering process, allowing us to run some code before and after thestages 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 isbecause we’re generating output for serialization, so we have no need for headers andfooters.

To generate XML instead of Text, you’ll notice it’s only a couple characters that needchanging:

puts InvoiceController.render_xml( :data => data,:employee_name => "Samuel L. Jackson",:employee_id => "e1337",:note => "END OF INVOICE")

96

Page 115: Ruport Book 1.1.0

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 ashortcut 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 dobuild :company_header dooutput << "My Corp. Standard Report\n\n"

end

build :invoice_header dooutput << "Invoice for #{options.employee_name} " <<

"(#{options.employee_id}), generated #{Date.today}\n\n"end

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

endend

build :invoice_footer dooutput << "\n#{options.note}\n\n" if options.note

endend

97

Page 116: Ruport Book 1.1.0

formatter :xml => XML dodef layoutoutput << "<invoice>\n"yieldoutput << "</invoice>"

end

build :invoice_body doadd_employee_info

generate_data_rows

add_meta_dataend

def add_employee_infooutput << "<employee name=’#{options.employee_name}’ "+

"id=’#{options.employee_id}’/>\n"end

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

endend

def add_meta_dataoutput << "<note>#{options.note}</note>\n"+

"<created>#{Date.today}</created>\n"end

endend

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 ofRuport::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 giveit a little more help.

98

Page 117: Ruport Book 1.1.0

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 bebased on the XML class we’ve stubbed out here. Keep in mind that you could use any classconstant here that points to a Ruport::Formatter subclass, it does not necessarily needto be defined within the same namespace.

Though we won’t cover it here, it is possible to alter which formatter classes Ruport willuse as base classes for this anonymous formatter shortcut. Please see theController.built in formats API documentation for details.

9.3 Custom Formatters for Ruport’s StandardControllers

Using the techniques from above, you can easily build an extension to our standardcontrollers.

The following example adds XML support for our Table controller:

class XML < Ruport::Formatter

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

def layoutoutput << "<table>\n"yieldoutput << "</table>\n"

end

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

end

99

Page 118: Ruport Book 1.1.0

build :table_body dooutput << "<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::Tablestructure.

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 moreinteresting than this. Ruport happily wraps any third party library you’d like to use.

100

Page 119: Ruport Book 1.1.0

9.4 Using Templates

Templates allow you to define a reusable set of formatting options. You can createmultiple templates with different options and specify which one should be used at thetime 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 thetemplate.

class Ruport::Formatter::Text

def apply_templateoptions.note = template.note

end

end

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

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

Page 120: Ruport Book 1.1.0

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

To derive one template from another, use the :base option toRuport::Formatter::Template.create.

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

Ruport has a standard template interface to each of the built-in formatters. This exampleshows 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 defaulttemplate that will be available to all of your formatters. You create one like any othertemplate, but give it the name :default.

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

}

102

Page 121: Ruport Book 1.1.0

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 specifyinga template or that you render with : template => false. For example, assume we havedefined the default template shown above, as well as the :simple template defined in theprevious 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 theRuport::Formatter::Template.default method. You might use this to mix some ofthe default and specific templates in your apply template method.

103

Page 122: Ruport Book 1.1.0

class Ruport::Formatter::Text

def apply_templateoptions.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 awhole 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 sharelogic between formatters, see the custom controller logic cheatsheet.

If you’re looking to build printable reports in PDF format and would like to takeadvantage of some of Ruport’s helpers, see the printable documents cheatsheet.

There are also some helpful examples in the integration hacks cheatsheet that show howto quickly wrap existing code with Ruport’s formatting system.

The API docs for the controllers all list the hooks they implement and the options theyreceive. 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 availablein the template interface to the built-in formatters.

104

Page 123: Ruport Book 1.1.0

Chapter 10

Building Custom PrintableDocuments

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

10.1 Displaying Multiple Tables in a Single PDF

Making a multi-table PDF is easy, you just need to build your own controller andformatter. Every time we say that, people cringe with fear, but have a look at thisexample 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 dodata.each { |table| pad(10) { draw_table(table) } }render_pdf

end

endend

105

Page 124: Ruport Book 1.1.0

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 fullrange 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 otherdecorations 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_headerpdf_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

Page 125: Ruport Book 1.1.0

move_cursor_to top_boundary - 80

pad_bottom(20) { hr }

options.text_format[:justification] = :leftoptions.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 thisis 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 sameheader code and finalization hook.

module StandardPDFReport

def build_standard_report_headerpdf_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

Page 126: Ruport Book 1.1.0

options.text_format[:justification] = :leftoptions.text_format[:font_size] = 12

end

def finalize_standard_reportrender_pdfpdf_writer.save_as(options.file)

end

end

class DocumentController < Ruport::Controller

stage :standard_report_header, :document_bodyfinalize :standard_report

end

class TableController < Ruport::Controller

stage :standard_report_header, :table_bodyfinalize :standard_report

end

class FormatterForPDF < Ruport::Formatter::PDF

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

include StandardPDFReport

build :document_body doadd_text "Lorum, Ipsum Blah Blah...\n" * 25

end

build :table_body dodraw_table(data)

end

end

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

DocumentController.render_pdf( :file => "foo.pdf",:report_title => "Sample Document" )

108

Page 127: Ruport Book 1.1.0

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 noneed to do that. Including the StandardPDFReport module would work for any PDFformatter subclass.

109

Page 128: Ruport Book 1.1.0

10.4 Generating Page Headers

If you are generating a report which has tables that do not exceed one page, or data thatcan be generated on a page by page basis, page headers can simply be drawn usingadd text / draw text much like the way the document headers were drawn in theprevious example.

However, if you have information which must be repeated on each page, and you don’thave 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-helpersto draw in page headers and footers. You’ll notice a few less than pleasant things aboutthis:

• 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_reportrequired_option :file

class PDF < Ruport::Formatter::PDF

renders :pdf, :for => AllPagesController

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

end

110

Page 129: Ruport Book 1.1.0

def prepare_long_reportapply_long_report_page_headerapply_long_report_page_footer

end

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

}end

def apply_long_report_page_footerpdf_writer.bottom_margin = 75all_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 ispainful, 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. Ifyou want to have your controller properly hand back a PDF for download, you should usesend 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

Page 130: Ruport Book 1.1.0

10.6 Related Resources / Digging Deeper

There are a wide range of PDF helpers provided by Formatter::PDF, so taking a quicklook at the API documentation should be helpful. Many functions, such as add text anddraw table, expose a large number of features by forwarding options directly toPDF::Writer.

Also, in ruport-util, you’ll find PDF Invoice support and some drawing helpers forgenerating printable forms. More tools are constantly being added, so keep an eye out fornew stuff in the future.

112

Page 131: Ruport Book 1.1.0

Chapter 11

Adding Logic to CustomControllers

Custom controllers in Ruport are often used to define the process your Formatters shouldimplement and the options they should handle. This allows you to gain a consistentinterface 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 normalizedform 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 scenariosyou’ll run into.

Here we’ll discuss the Controller#setup hook, Controller::Hooks and Formatterhelpers.

11.1 Using setup()

The setup hook is called after options are processed from the hash arguments and blockgiven to your render call. This means that you can easily manipulate the data attributeas 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::Controllerstage :documentrequired_option :text

def setuptext << " for #{username}"

end

113

Page 132: Ruport Book 1.1.0

def usernamedata[:name].capitalize

end

end

class DocumentFormatter < Ruport::Formatter

renders :example, :for => DocumentController

build :document dooutput << "||" << 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 tohook up the formatting system to your custom, non-ruport classes. In the previousexample, 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 thatcontroller.

class Person

include Ruport::Controller::Hooks

renders_with DocumentController

def initialize(name)@name = name

end

114

Page 133: Ruport Book 1.1.0

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 onmany 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 youwant to leave it up to the individual formatters to decide how to make use of them. Youcould go about this by explicitly including a module in your different formatters. Becausethis task is relatively common, Ruport provides a shortcut.

If you define a module called Helpers in your controller, it will be mixed into theformatter instance at run time. This allows for controllers to specify different ways ofhandling specific tasks without clashing.

The simple example below adds a time() helper that gives formatted output of thecurrent time and shows it used in multiple formatters.

class DocumentController < Ruport::Controllerstage :documentrequired_option :text

def setuptext << " for #{username}"

end

def usernamedata[:name].capitalize

end

115

Page 134: Ruport Book 1.1.0

module Helpersdef timeTime.now.strftime("%H:%m:%S")

endend

end

class DocumentFormatter < Ruport::Formatter

renders :example, :for => DocumentController

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

end

end

class DocumentFormatter2 < Ruport::Formatter

renders :example2, :for => DocumentController

build :document dooutput << "||" << 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

Page 135: Ruport Book 1.1.0

11.4 Implicit Helpers for Formatter Selection

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

class DocumentFormatter < Ruport::Formatter

renders :example, :for => DocumentController

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

end

end

class DocumentFormatter2 < Ruport::Formatter

renders :example2, :for => DocumentController

build :document dooutput << "||" << 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 beingrendered.

The example below is essentially identical to the code above.

class DocumentFormatter < Ruport::Formatter

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

build :document doexample { 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

Page 136: Ruport Book 1.1.0

11.5 Related Resources / Digging Deeper

• Controller::Hooks has shortcuts for the standard controllers.

• Controller::Hooks also has a rendering options class method for settingdefaults.

• Controller has a run() hook that can be used to override the way stages areexecuted.

118

Page 137: Ruport Book 1.1.0

Chapter 12

Integration Hacks

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

12.1 Squeezing More Out of Our Dependencies

Ruport tries to make its dependencies easily accessible, especially FasterCSV andPDF::Writer. Much of what you can do with these libraries, you can access throughRuport’s API.

Still, if you have pre-written FasterCSV or PDF::Writer code, you’re probably lookingfor some shortcuts to avoid rewriting that code. We’ll look at two plugins that help youdo exactly that.

12.1.1 Using Ruport’s Formatters for FasterCSV Tables andRows

If you’ve already been using FasterCSV::Table for your data manipulations, and youjust want to make use of Ruport for your formatted output, it’s a piece of cake withfcsv 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 thatformatting tables ‘just works’.

119

Page 138: Ruport Book 1.1.0

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,agezaphod,beeblebrox,42ara,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 alsobe 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 aRuport::Data::Table by calling table.renderable data.

120

Page 139: Ruport Book 1.1.0

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 usesFCSV under the hood. If you’ve got straightforward needs, you might use this plugin tospeed 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 existingPDF::Writer code with Ruport, the pdf writer proxy will surely be helpful.

# Only necessary for Ruport <= 1.2.3gem install pdf_writer_proxy --source http://gems.rubyreports.org

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

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

i0 = pdf.image "../images/chunkybacon.jpg", :resize => 0.75i1 = pdf.image "../images/chunkybacon.png", :justification => :center, :resize => 0.75pdf.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 :baconrequired_option :file

class PDF < Ruport::Formatter::PDF

renders :pdf, :for => ChunkyController

proxy_to_pdf_writer

121

Page 140: Ruport Book 1.1.0

build :bacon dooptions.text_format = { :font_size => 72, :justification => :center }

select_font "Times-Roman"add_text "Chunky Bacon"

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

add_text "ChunkyBacon"

save_as(options.file)end

endend

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

You’ll notice that the core of the report is quite similar, and is directly using somePDF::Writer calls. Most PDF::Writer methods will “just work” as the plugin justforwards all requests it doesn’t understand to the underlying pdf writer object in theformatter.

In certain cases, Ruport’s method names conflict with the PDF::Writer names. Forexample, if you are looking to use PDF::Writer#add text rather than Ruport’s you’dneed to type pdf writer.add text explicitly.

Otherwise, this is typically a very fast way to extend your formatter so it can use the fullPDF::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 someother 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 customcalculations or manipulations.

The following example generates total sale prices for a series of items with different pricesand quantities:

122

Page 141: Ruport Book 1.1.0

require "rubygems"require "ruport"

class Sale < Ruport::Data::Record

TAX = 0.06

def total_saleprice.to_f * quantity.to_f * (1 + TAX)

end

end

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

__END__item,price,quantityapple,1.05,3banana,1.25,10kitten,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 objectwill 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 toconvert to Ruport’s data model but would like to avoid copious calls that look likemy 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

Page 142: Ruport Book 1.1.0

def renderable_data(format)to_a.to_table

end

def to_sas(: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 :dataattribute to the result of renderable data(format), and passes along any options to thecontroller.

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 makeyour modifications just snap into Ruport whenever they’re installed without having toexplicitly require them. This is how the plugins we showed work, and is extremely easy todo.

In order for require ruport/extensions to discover your plugin, you simply need tohave 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

Page 143: Ruport Book 1.1.0

# 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, whichyou probably will not need.

That’s really it! Any code you include in that file or require from within it will beauto-detected and loaded alongside any other plugins installed when a user includesruport/extensions in their code.

Because this involves autoloading, we recommend against breaking behavior in the corelibrary via plugins. Extensions are welcome though!

12.3 Related Resources / Digging Deeper

Controller::Hooks can be incredibly handy when used alongside custom controllers, asthey allow you to keep your transformations close to your actual data structures andquickly 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

Page 144: Ruport Book 1.1.0

126

Page 145: Ruport Book 1.1.0

Chapter 13

Using Report andReportManager

Ruport’s Report class is meant to provide a high level interface to some of Ruport’s corelibraries and utilities.

Report provides easy ways to work with Ruport::Query and Ruport::Mailer, and alsoprovides rendering shortcuts. These features make it easier to tie together some ofRuport’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 isan 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

Page 146: Ruport Book 1.1.0

MyReport.generate do |report|report.file = "in.csv"report.save_as "out.csv" # renders to csvreport.save_as "out.pdf" # renders to pdfputs 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 byrenderable 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 makeuse of them with your reports:

class MyReport < Ruport::Reportrenders_with MyCustomController

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

endend

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 binarysave_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 willjust convert the extension to a symbol and pass it to as().

Example: save as("foo.xml") forwards to as(:xml)

128

Page 147: Ruport Book 1.1.0

These formats will be written using text mode by default, rather than binary. You canspecify 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 andthen 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 prepareadd_source :legacy, :dsn => "dbi:mysql:old_db", :user => :rootadd_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.newlegacy_report.source = :legacylegacy_report.save_as "foo.csv"

new_report = MyReport.new # will use :default sourcenew_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 attachreports to an email and send them via SMTP.

The following example is for using the Report class directly, but would work forsubclasses as well.

129

Page 148: Ruport Book 1.1.0

r = Ruport::Report.new

r.add_mailer :default,:host => "mail.adelphia.net",:address => "[email protected]"

r.send_to("[email protected]") 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 notesare 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

Page 149: Ruport Book 1.1.0

13.5 Managing Many Report Objects

In the ruport-util package there is a tool called ReportManager that allows you to selectdifferent reports programatically. This might be useful in web applications, where you’dlike 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_reportrenders_as_table

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

endend

class Venus < Ruport::Report

acts_as_managed_reportrenders_as_table

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

endend

["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 arecertain cases where you’ll want to be able to dynamically register and run reports, whichis easy to do with ReportManager.

If you need to override the name of a report, add a name method to it which returns astring to identify the report with.

131

Page 150: Ruport Book 1.1.0

13.6 Related Resources / Digging Deeper

Report is really most useful when used in conjunction with rope, so you’ll want to reviewthat cheatsheet.

Also, much of the heavy lifting can and should be done by your controllers, especiallyusing the various hooks available in them. This class is among the feature sets in theruport-util package that is likely to change over time to adapt to people’s needs.

132

Page 151: Ruport Book 1.1.0

Chapter 14

rope (A Code Generation Toolfor Ruby Reports)

14.1 Overview

The rope tool comprises a number of simple utilities that script away much of yourboilerplate code, and also provide useful tools for development.

If you’re looking for something to help keep your project organized and help cut down ontyping, you may find this tool helpful.

14.1.1 Starting a New rope Project

$ rope labyrinthcreating directories..labyrinth/testlabyrinth/configlabyrinth/outputlabyrinth/datalabyrinth/data/modelslabyrinth/liblabyrinth/lib/reportslabyrinth/lib/controllerslabyrinth/sqllabyrinth/util

creating files..labyrinth/lib/reports.rblabyrinth/lib/helpers.rblabyrinth/lib/controllers.rblabyrinth/lib/templates.rb

133

Page 152: Ruport Book 1.1.0

labyrinth/lib/init.rblabyrinth/config/environment.rblabyrinth/util/buildlabyrinth/util/sql_execlabyrinth/Rakefilelabyrinth/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=ghostsreport file: lib/reports/ghosts.rbtest file: test/test_ghosts.rbclass 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

Page 153: Ruport Book 1.1.0

"test/test_ghosts.rb"Loaded suite /usr/lib/ruby/gems/1.8/gems/rake-0.7.1/lib/rake/rake_test_loaderStartedFFinished 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 errorsrake 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 intest/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 inconfig/environment.rb, which can be used to set up database connections, Ruport’smailer, 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

Page 154: Ruport Book 1.1.0

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

You’ll need to tweak this as needed to fit your database configuration needs. If you needto require any third party libraries which are shared across your project, you should do itin 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::TestCasedef test_reverserassert_equal "baz", Reverser.render_text("zab")

endend

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

require "lib/init"

class Reverser < Ruport::Controllerstage :reverser

end

class ReverserFormatter < Ruport::Formatter

renders :text, :for => Reverser

build :reverser dooutput << data.reverse

endend

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

136

Page 155: Ruport Book 1.1.0

$ 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::TestCasedef test_reversed_reportreport = ReversedReport.newreport.message = "hello"assert_equal "olleh", report.to_text

endend

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 Reverserattr_accessor :message

def renderable_data(format)message

endend

14.5 ActiveRecord Integration the Lazy Way

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

14.5.1 Setup Details

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

137

Page 156: Ruport Book 1.1.0

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_modelmodel file: data/models/my_model.rbclass 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 requirespecific models in your reports.

14.6 Related Resources / Digging Deeper

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

1See the Report cheatsheet.2See the acts as reportable cheatsheet.

138

Page 157: Ruport Book 1.1.0

Appendix A

Ruport Hacking Guide

If you’ve read your way through this book, you probably have a decent grasp on how touse Ruport. However, there is power in knowing what lies under the hood. This appendixwill help show you how to work against Ruport’s bleeding edge, and how to hack on theinternals 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. Thingstend to move reasonably fast in Ruport, so working against Subversion is a must. You canalways 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 easilylet our users know what kind of release they are using at a glance.

139

Page 158: Ruport Book 1.1.0

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 Ruport1.4, and it resides in /branches/1.4. Once a new major stable release comes out, thebranch effectively becomes obsolete.

We try not to use odd numbers for the middle number in our public releases, meaning thenext 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 theSVN revision the gem is built from. For example, a beta build of the 1.4 codebasegenerated from r1224 would become Ruport 1.3.1224

This numbering scheme makes it very easy to tell both which branch the code isultimately going to become a part of, and also approximately what it is in. Combinedwith an svn log of trunk, it shouldn’t be too hard to keep track of what is in the betabuilds.

A.2 Preparing A Patch

If you’d like to extend Ruport either for your own use or to contribute back to thecommunity, 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 patchfor your new features will help both make your work easier and increase the likelihoodthat 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 whereyour feature might belong.

Ruport

The core Ruport package has become increasingly lean from version to version. We nowbasically think of it as a super lightweight base for building actual tools on top of. Withthis in mind, we basically have our core data structures, some simple controllers, and afew formatters. As of Ruport 1.4, it should be easy for anyone to keep most of Ruport’s

140

Page 159: Ruport Book 1.1.0

core in their head, which we think is a good thing. Basically, the only features whichbelong in Ruport’s core are the ones so common that most everyone will use them (CSVI/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 stablebranch and a main development branch. Stable branches typically last a couple monthsbefore being replaced by newer versions from development. What this basically means isthat it sometimes takes a while for changes in the core package to make it out to thegeneral public.

ruport-util

The ruport-util package is Ruport’s fat cousin, chock full of pretty much anything thatsupports Ruport development. This means that it is an ideal home for somewhatspecialized tools or for formatters and data modeling libraries that aren’t particularlyextensible. This is also the place for any command line utilities. Two such examplescurrently in ruport-util are rope and csv2ods.

The ruport-util package is under a much less structured release cycle, in which releasestypically take place a few days after new functionality is added. Given the nature of thepackage 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 stableversion of Ruport, not a development version. When work needs to be done against thebleeding 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 ona rolling release schedule and may change over time.

Ruport/Rails

Of the four packages mentioned here, Ruport/Rails is the only ‘unofficial’ package. Thisis a Rails plugin and is meant for any non-AR Rails helpers. If you have an extensionthat 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 stillconsidered 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

Page 160: Ruport Book 1.1.0

Get up to date

A first step to take before working on a patch is to check the development version of thepackage you’re working on to see if it doesn’t already have the feature you need. Wedon’t announce every feature while we’re working on it, so glancing at the svn log isalways a good idea.

Get in contact

Once you’ve verified that your problem still needs solving, please contact us on theruport-dev mailing list: http://groups.google.com/group/ruport-dev

This list is mostly for contributors and developers, and it is where the implementationdetails of new features can be discussed. We will also happily review any patches you postto this list. Usually, the best way to get a feature into any of our packages is to send ashort 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 adiscussion with us. You’ll find it a whole lot easier to get a patch together with a coupleRuport 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 aboutrefining it for use by the general community, that’s fine. However, if you want your patchto 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 attachthe file there. You can also send it to the ruport-dev mailing list. However, if you’ve putit 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 accessto all four packages. So far, this has worked out great with our contributors, andhopefully 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

Page 161: Ruport Book 1.1.0

worth investigating if you’re thinking of extending things. Listed below are several of thedeveloper 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 ducktyped objects. A subclass of Table could set these automatically. For reference seethe way graphs are implemented in ruport-util.

• Records can be reindexed to point at a new set of attribute names, and attributescan be deleted via private functions.

A.3.2 Formatting System

• Formatter::RenderingTools contains the definitions for the render * helpers. Youcan easily use render helper() in this module to create your own wrappers.

• You can override Controller#run() to completely change the way the renderingprocess works.

• You can use Controller’s build method to create a controller instance and tie it to aformatter. This allows you to persist a controller instance rather than generatingthem on the fly, among other things.

• The PDF formatter has a number of private methods that do primitivemanipulations on the document, some of these may come in handy for designingadvanced 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 extendand even modify the way core Ruport works without having to uproot the underlyingcodebase. We’d be quite excited to see these used to solve interesting problems.

143

Page 162: Ruport Book 1.1.0

144

Page 163: Ruport Book 1.1.0

Afterword

This book has provided a tour through the major components of Ruport. At the level ofthe software, you’ve seen how to collect data and process that data into a form thatRuport can use. You’ve also seen how to use Ruport’s formatting system to output thatdata into your report. Along the way, we’ve shown you how Ruport fits into a realapplication and demonstrated some of the libraries associated with the core project.

With that background, you should be able to produce some pretty fancy reports, butthere are a number of avenues we haven’t explored here. We covered all of the majorfunctionality of Ruport, but this book isn’t intended as a comprehensive referencemanual. To get all of the details on what Ruport can do, the API docs are the bestresource and are located at http://api.rubyreports.org.

Although this book mainly covered the Ruport core, the overall Ruby Reports projectincludes several other sub-projects. You’ve seen ruport-util and acts as reportable inaction 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, supportfor producing Excel and OpenDocument spreadsheet output, a simple invoicing moduleand more.

Some of what we did show you, such as the PDF form helpers and the mail support, havemany features that we didn’t cover. In addition, ruport-util is the home of those parts ofthe project that we feel are either are not general or not mature enough to be included inRuport core, and as such it is constantly evolving. You definitely want to keep an eye onwhat’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 formatterthat 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 thatintegrates Ruport with the Rails web development framework, allowing you to useRuport’s formatting system within your views. If you’re using Ruport within Rails, it’sworth a look.

The main Ruport web site is located at http://rubyreports.org. It contains many usefulresources to get you more acquainted with the overall project and links to other locationswhere you can find Ruport-related material. The web site for this book is located at

145

Page 164: Ruport Book 1.1.0

http://ruportbook.com. It contains HTML-formatted versions of the book content, as well asup-to-date news and errata. We maintain a blog about Ruby Reports athttp://blog.rubyreports.org for general project news. The official developer resource listingfor 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 yourfeet wet with some reporting, you may want to get involved. We maintain an activemailing list at http://list.rubyreports.org so please sign up there to keep up to date with theproject. On the mailing list, you can get announcements, ask questions, or participate indiscussions about Ruport. You can also catch up with the developers and communitymembers on the #ruport channel on Freenode.

Though we often have discussions about feature requests, bugs, and other developmentrelated things on the main mailing list, we also maintain a developers’ mailing list toannounce developer meetings or discuss changes that affect the internals of Ruport. It’sby invite only, but we approve any requests to join that aren’t from spammers. You canfind the dev-list at http://groups.google.com/group/ruport-dev.

Thanks very much for reading and happy reporting!

146