Some things are stronger together than apart.
So far you’ve had a taste of some of the key Rails ingredients. You’ve created entire web applications and taken what Rails generates and customized it for your needs. But out in the real world, life can be more complex. Read on... it’s time to build some multi-functional web pages. Not only that, it’s time to deal with difficult data relationships and take control of your data by writing your own custom validators.
There’s no better way of traveling between islands than by seaplane, and Coconut Airways has an entire fleet. They offer scenic tours, excursions, and a handy shuttle service between all the local islands. Their service is proving popular with tourists and locals alike.
Demand for their flights is sky-high, and they need an online reservation system to help them. The system needs to manage flight and seat bookings. Here’s the data they need to store:
If we simply create scaffolding and don’t customize the app, it will be hard to use. In order to book a seat on a flight, the user will have to look up the id of the flight from its URL:
We need to display a flight together with its seat bookings.
We need the flight page to look something like this:
Let’s see how this compares with the seat pages generated by the scaffolding:
Can any of these help us generate the flight page?
Two of the generated pages look pretty similar to what we need on the flight page, the seat list and the booking form. The middle section of the flight page looks like the seat list, and the booking form looks like the end section:
So we need the flight “show” page to include view code like the seat “index” list and the new seat booking form.
So should we just copy the code from each form into the flight page?
Mark: Woah - wait a minute. How much code is that?
Laura: I dunno. We need the code in the page, though. It’s in the design.
Mark: I know we need the seat list and the booking form to appear in the page. But does that mean we have to have the code in there?
Laura: Why - what’s the problem with that code?
Mark: The seat list and the booking form are doing significantly different things. Can’t we break them apart somehow?
Bob: Break them apart? You mean into separate files?
Mark: Yes. That way we could have one file that displays a list of seats, one that displays the booking, and then include or call each page from the main page.
Laura: Oh - like separation of concerns.
Bob: What’s that?
Laura: Separation of concerns. It means you get one piece of code to do just one thing. Makes it easier to track down bugs.
Bob: Sure, sounds great... but how do you actually do that?
If we can split a page into separate files, it will make things more manageable. But how do we do that?
Rails lets us store fragments of pages into separate files called partial page templates or—more simply—partials. A partial is like a sub-routine that outputs a small part of a page. In our case we can use two partials: one for the seat list and another to add a new seat booking.
Partials are simply embedded Ruby files, just like templates. The only difference is that, unlike templates, partials have names that begin with an underscore (_
).
We need to create partials for the booking form and the seat list, and then Embedded Ruby can process the flight page and call the partials each time the render expression is reached.
This allows a separation of concerns: we have separate components dealing with booking and seats, and those components are combined for the user when needed.
When Rails gets a request for flight information, it will use the partials, templates, and layout with Embedded Ruby to generate a single HTML response.
Let’s start by looking at the first thing on the list—the booking form.
Partials are just another kind of ERb file, so they contain the same kinds of tags that templates contain. Here’s the content of our _new_seat.html.erb
partial. It contains exactly the same code as the new seat page, which means that all we have to do is copy app/views/seats/new.html.erb
and save it as app/views/flights/_new_seat.html.erb
:
We could have left the partial in the “seats” folder, but we move it into the “flights” folder to make it slightly easier to call. It’s also really important that the partial begins with the _
character. The _
character is used by Rails to distinguish partials from page templates.
Creating the partial is only half the job. We now need to modify the flight show.html.erb page template to include the partial in its output. Partials, like templates, are really just pieces of Ruby code disguised to look like HTML. And in the same way that one piece of Ruby code can call another, the template can easily call the partial.
So how do you call a partial? By adding a render
command to the flight page:
The render call tell Embedded Ruby to process the partial and include its output at that point in the file.
The partial should now appear in the flight page.
The problem is caused because the ERb code contains a reference to the @seat
variable. So why is this a problem?
This file used to be a page template associated with the SeatsController. The SeatsController initialized the @seat
instance variable like this:
@seat = Seat.new
But now the file has become a partial that is going to be used by the FlightsController, and that controller has no @seat
instance variable. So we need to change @seat
into a local variable called seat
:
seat
is called a local variable because nothing outside the partial can read or write to it. But if that’s the case, then how do we pass the partial a value for the seat
variable?
Partials and templates work a lot like Ruby methods or functions. When a template renders a partial, it’s a little like one function calling another function.
And since a partial’s like a function, you can pass in parameters like this:
The render method can accept a hash called locals
. Within the hash, you can include a set of values indexed by a variable name. Like pretty much everywhere in Rails, names are expressed as symbols.
But what value should we pass in for seat
? Let’s look at what value the original SeatsController used:
def new @seat = Seat.new
Because the form is being used to initialize a seat, we just need to pass the form a freshly created Seat
object:
So has this fixed the problem with the flight page?
We can pass in the flight id number to the booking form partial.
But how will the form use the id? And how can a form provide a default value for a field, without asking the user?
We can convert the seat “index” list in more or less the same way that we converted the booking form—by copying the original seat template file to a partial file. Let’s call this new partial _seat_list.html
:
The seats “index” page displayed the contents of a SeatsController instance variable called @seats
. The SeatsController created the instance variable just prior to index.html.erb
was displayed. But what about now? We copied the index.html.erb
template to a partial that will be displayed after running the FlightsController... so there’s no @seats
instance variable containing an array of seats.
That means we need to provide the new _seat_list.html.erb
partial with an array of seats. So what value should we provide for the array of seats? This is how the SeatsController initialized @seats
:
def index @seats = Seat.find(:all)
So, for now, let’s call the seat list like this and see how it works:
Everyone thinks the system looks great, so the system goes live. Unfortunately, it doesn’t takes long before someone spots a problem...
So what happened?
The flight page is displaying all the seat bookings for all the flights!
So what’s going on? The problem is caused by the render
command, which calls the seat list partial. Remember, we called the partial like this:
<%= render :partial=>"seat_list",
:locals=>{:seats=>Seat.find(:all)} %>
This displays the list of all seats in the database. That was fine when the seat list was the index page for the seat data... but now that we’re displaying the data against the flight, we need to restrict the seats so that only seats belonging to the current flight are displayed.
We could fix the finder... but it would be better to create a relationship.
You’ll often find that certain model objects are often used together, like flights and seat bookings. You may need to use data from one type—like the flight id—to find the related objects in the other type, like the seats booked on the flight.
You could just use finders to read the related objects. For example, if you had a flight object called @flight
, you could find the related seat objects like this:
But it’s actually easier to connect the two models together with a relationship:
A relationship makes objects of one type of object appear to be attributes of another type of object. For example, if we create a relationship on the flight model that connects to the seat model, we can refer to the seats associated with a flight like this:
@flight.seats
This will return the exact same thing as the finder above, but defining a relationship between two models will simplify your code and reduce the chances that you will make a mistake by repeatedly defining finders to jump from one model to another. It will also make your code a lot easier to read.
Sounds good. So how do relationships work?
We are going to give the Flight model an extra attribute called seats
, so it makes sense that the Flight model code is the place where we define the relationship:
The has_many
command accepts the name of a related model and, because it will be used to find arrays of related seats, the name of the model is plural. So the parameter for has_many
is :seats
and not :seat
(without the “s” at the end). Once the relationship is in place, you can use your new attribute like this:
The seats
attribute returns an array of seat objects associated with the flight:
Now there’s a problem with the baggage on the flights. Some people are arriving at the airport carrying too much stuff—way more than the allowance for their flight. The flight data records the maximum baggage allowance, but a lot of the passengers are unhappy because they told the airline how much baggage they were bringing with them when they entered the seat booking, and the system didn’t complain. The system needs to be modified to prevent people reserving seats with too much baggage... before they show up with a booked seat.
Rails comes with a set of built-in validators that can perform a lot of basic tests, like whether data is entered or if it is correctly formatted. But sometimes you will need to check something that isn’t covered by the basic validators.
In the case of baggage, Rails doesn’t come with a validates_too_much_baggage
validator. There’s not a maximum value validator, either. So we need to write out own validator.
If you create a method in the Seat code called validate
, that method will always be called by the model object just before things get saved or updated to the database:
The errors.add_to_base(...)
command inserts a message into the list of errors. If there’s an error message created, the save or update operation is aborted and the user should be sent back to the form to correct the problem.
Prefer relationships over manual finders.
Instead of using finders to look up the related flight object, you can define a relationship between seats and flights. But the question is, what sort of relationship do we need?
When we created a relationship before, we gave the Flight model a new attribute called seats
:
@flight.seats
But what do we need this time? Before, we had a Flight object and we wanted to know what the related seats were. The difference is that now we’re checking a seat object, and to do that we need to know about the related flight. So what sort of relationship do we need this time around?
This time we need a relationship that’s the opposite way around to the one we had before. Given a particular seat object, we need to get the related flight:
We want to have an attribute on seats like this:
@seat.flight
We want to know which flight a seat belongs to. And each seat will have only one flight. How do you think that will be coded?
Life’s pretty good at the airline. Tourists and locals find it a breeze to use the system. The planes don’t get overloaded with baggage or get overbooked. In fact, the staff are using the time they saved a little more productively...
You’ve got Chapter 6 under your belt, and now you’ve added the ability to make the most of your connections.
render :partial=>“name” displays _name.html.erb
Pass a variable to a partial with
render :partial=>“name”, :locals=>{:var1=>“val1”}
Custom validation code is in a model method called validate
errors.add_to_base(...) creates an error message
belongs_to defines a relationship from an object to its parent
has_many is the reverse relationship