In this second part of a two-part series, we'll finish creating reports that can be accessed via the Internet. This article is excerpted from chapter 5 of the book Practical Reporting with Ruby and Rails, written by David Berube (Apress; ISBN: 1590599330).
Building Reports Accessible from the Internet - Examining the Graphical Reporting Application (Page 5 of 5 )
You can run the example by using the following command:
ruby script/server
Now open a web browser and browse to http://localhost:3000/. You'll see a screen showing the available players and games, just as in the example in Chapter 4. If you select Michael Southwick and Tech Website Baron from the drop-down lists, you will see a screen similar to Figure 5-3.
Figure 5-3.Flash chart showing player statistics
As you move your mouse over the various elements, you will see a tool tip with the details of each item; it should also highlight slightly.
Let's take a look at the code line by line.
Dissecting the Code
The models and application layout are fairly straightforward. The models are the same ones you used in Chapters 3 and 4. The application layout (Listing 5-20) is just a short wrapper for the application, which includes a bit of CSS to make the application more attractive. It does contain one important line:
<%=javascript_include_tag :defaults %>
This includes the default list of JavaScript files, which includes Prototype by default. Because the Flash Object plug-in adds itself to the list of defaults, this line also includes the Flash Object plug-in for you.
Note You need Prototype, as it's used later in this example. However, if you need to include Flash Object by itself, you can use this line of code: <% = javascript_include_tag "flashobject" %>.
Next, let's examine the config/routes.rb file (Listing 5-10), which controls the URLs for the entire application:
ActionController::Routing::Routes.draw do |map| map.connect 'performance/:game_id/:player_id', :controller=>'performance', :action=>'show'
map.connect ':controller/:action/:id' map.connect ':controller/:action/:id.:format' end
The first route is a route that defines URLs of the form performance/game_id/player_id. The second route defines URLs that specifically set an output type, like performance/game_id/player_id.xml and performance/game_id/player_id.html. The next route specifies that / should map to the Home controller. The remaining routes are catchall routes. Although they are not used in the current version of this application, it's wise to include them so that you can easily add new controllers.
Let's examine the Home controller (Listing 5-11) next:
class HomeController < ApplicationController def index @available_players =Player.find(:all) @available_games = Game.find(:all) end end
This code is pretty straightforward. It populates a list of available players and games, which is passed to your view. Let's take a look at that view next.
The first part of the view for the application (Listing 5-17) uses the @available_players and @available_games variables, and constructs two select boxes, which let users choose a player and game combination to view:
<h1>Team Performance Reporting</h1>
<div id="top"> <%=select 'player', 'id', [['Click here to select a player',""]] + @available_players.map { |p| [p.name, p.id] }, {:include_blank=>false} %> <%=select 'game', 'id', [['Click here to select a game',""]] + @available_games.map { |g| [g.name, g.id] }, {:include_blank=>false} %></div>
<div id="chart"> </div>
This code creates two drop-down lists from the data passed from the controller. You use the map method to turn each array of Active Record objects into the type of array that the select tag expects: an array of arrays, with the first element as the label and the second element as the value. This means that for the player Matthew Gifford, for example, the player's name will be displayed in the drop-down list, but the control will actually have the value 1 (the player's ID), which you'll use to display the appropriate chart. This code also puts a blank "Click here to select . . ." entry at the top of each drop-down list. This entry has a label but no value, and it serves to tell the user what to do.
The second div, which has the ID chart, will be used to store the chart. The following JavaScript makes that happen:
<script>
function show_report(){
$('chart').hide();
var player_id = $('player_id').value;
var game_id = $('game_id').value
if( player_id && game_id ) {
new Ajax.Updater("chart",
'/performance'+
'/' + $('game_id').value +
'/' + $('player_id').value,
This JavaScript code defines a new function, show_report, and then uses Prototype's Event.observe function to run the show_report function whenever either of the drop-down lists changes. The show_report button hides the existing chart, and then checks if both a player and game were selected. If neither or just one of them was selected, then the routine does nothing. If both are selected, then it uses Ajax.Updater to call the show method of the Performance controller, passing it both the ID of the selected game and the ID of
the selected player. (Note that you don't need to specify explicitly that it's the show action, because you defined an appropriate route in your routes.rb file.)
The Ajax.Updater call has three important optional parameters passed to it:
The first is evalScripts, which ensures that JavaScript code passed by the Performance controller is executed. By default, code retrieved by Ajax.Updater is not executed.
The second is the method parameter. By default, Ajax.Updater uses a POST request, and since this is a read-only request that does not affect the state of the database, it should be a GET request.
The third is a callback, onComplete, which will run when Ajax.Updater has finished updating the control. This is a small fix for a bug in the Flash Object plug-in, which results in a "You do not have Flash installed" message appearing while the page is loading. To avoid the problem, you wait 400 milliseconds to redisplay the chart component.
Next, let's take a look at the Performance controller (Listing 5-12):
class PerformanceController < ApplicationController
def show
@player = Player.find_by_id(params[:player_id])
@game = Game.find_by_id(params[:game_id])
@events = Event.find(:all,
:select=>'event, ' <<
'AVG(time)/1000 as average_time',
:group=>'events.event ASC',
:joins=>' INNER JOIN plays ON events.play_id=plays.id',
:conditions=>["plays.game_id = ? AND plays.player_id= ?",
@game.id, @player.id]
).map { |event|
{:event=>event.event,
:average_time=>event.average_time.to_i}
}
respond_to do |format|
format.html { render :layout=>false if request.xhr? }
format.text { render :layout=>false }
format.xml { render :xml=>{'player'=>@player,
'game'=>@game,
'events'=>@events
}.to_xml(:root=>'player_performance_report',
:skip_types=>true) }
end
end
end
This code sets the @player and @game variables, which allow the view to know which player and game were selected and display the information, and then it prepares the data. It retrieves the performance data using SQL that is similar to the example at the end of Chapter 4, but it uses find to retrieve the values instead of find_by_sql. The routine then maps it into an array of hashes, with each hash having an event value and an average_time value.
Note The reason the raw array of Event values isn't passed directly is that you need to call the to_i method on average_time, since Rails doesn't do that for you. This would require knowledge of the controller's internal structure to be embedded in the view, which violates MVC separation. By remapping it into a new data structure and calling to_i on the average_time method, you can have a controller-agnostic view. This has the benefit of letting you change the way this data is produced without affecting the view. As long as the data passed to the view is an array of hashes with the appropriate values, it should work.
Finally, a respond_to block is used to provide varying results depending on which format is called. For example, the URL http://localhost:3000/performance/5/1 will use the HTML format, since that's the default format specified in routes.rb.
The first format is HTML. This is the code that is called by the show_report JavaScript function in the Home controller. Note that it disables the layout if it's being called by an Ajax call. The request.xhr? method will return true during an XmlHttpRequest (XHR ) request, and in that case, the layout is disabled.
The second format is text. This is the format that Open Flash Chart uses to store its data. The first format, HTML, calls this format to retrieve the data. This does mean that the SQL is executed twice. It is necessary because the chart component should not be rendered if there is no data for the player/game combination; instead, a message should be displayed. You can detect that by running the computation for the HTML format as well as the other formats.
The last format isn't used in the example, but it demonstrates how easy it is to add machine-readable formats to Rails 2.0 applications. The XML format can be read by an application written in almost any language, as well as a desktop application such as Microsoft Access. For example, the XML generated by this code for player Matthew Gifford and game Tech Website Baron can be seen at the URL http://localhost:3000/ performance/5/1.xml and looks like this:
The exact appearance of the XML is controlled by two optional parameters passed to the to_xml method: :root=>'player_performance_report' and :skip_types=>true. The first, :root, sets the name of the root node to be easier to read; otherwise, it would simply be "hash," which isn't very descriptive. The second removes the type attributes, such as type="array" for the <events> element; those attributes clutter up the XML
without adding much information.
<p> <%=@player.name%> has no recorded data for <%=@game.name%>.</p>
<%end%>
If the user/game combination has no data, the view displays a message to that effect. If the user has selected a player and game, the view will use flashobject_tag--provided by the Flash Object plug-in--to include a graph. The data for the graph comes from the path /performance/game_id/player_id.text.
Although it's possible to include Flash objects directly in your HTML views using EMBED tags , that's not a good idea. If the user doesn't have Flash installed, you should display a message stating that Flash is required to see the content. Additionally, if you use EMBED tags, Internet Explorer requires users to click Flash objects to activate them and display their content, which is annoying. The Flash Object plug-in will take care of both problems. It will check if Flash is installed, and if not, it will display the message. The plug-in also inserts the objects dynamically, which avoids the Internet Explorer click-to-activate issue.
Next, let's take a look at the last view, app/views/performance/show.text.erb (Listing 5-19):
First, you loop through the @events array and pull out the event and average_time from each element. The events are used as labels; the average time is used as values. Note that this code is in here and not in the controller to keep their concerns separate. You could pass the labels and values directly to the view, but that would require the controller code to embody knowledge of how Open Flash Chart works, which would violate MVC separation. It would also add code that is irrelevant to the three other output formats to the controller.
Many of these options are fairly self-explanatory. For example, x_axis_colour controls the color of the x axis lines. The y_ticks parameter is a comma-delimited list of thre e parameters, which control the ticks (the small lines that point to numeric labels) on the left side of the graph. The first y_ticks parameter is the distance from the ticks to the labels, the second is the distance from the labels to the chart itself, and the third is the total number of ticks. You can get a full list of parameters from http://teethgrinder.co.uk/open-flash-chart/.
The final line of the code converts the hash into key=value pairs surrounded by ampersands, which is the data format required by Open Flash Chart.
Summary
This chapter demonstrated how you can easily use Rails, along with the techniques you've already learned, to quickly create web applications that serve reports as textual HTML or as Flash charts. Rails is a fast and easy way to create reporting software. The Web is ubiquitous, which gives it implicit deployment advantages, and Rails is a great way to create web applications.
However, while Rails takes care of many of the problems inherent in web applications, it cannot address all software issues. You're still likely to encounter various application-specific problems. The rest of this book is dedicated to examples of how you can solve specific reporting problems with Ruby.
DISCLAIMER:
The content provided in this article is not warranted or guaranteed by Developer Shed, Inc.
The content provided is intended for entertainment and/or educational purposes in order to
introduce to the reader key ideas, concepts, and/or product reviews. As such it is incumbent
upon the reader to employ real-world tactics for security and implementation of best practices.
We are not liable for any negative consequences that may result from implementing any information
covered in our articles or tutorials. If this is a hardware review, it is not recommended to open
and/or modify your hardware.