Skip to main content

Ruby on Rails - mapping a url to a resource

There are always times when you need to override the default RESTful routes in Rails. The most common case for this is mapping a /logout url to a session#destroy controller and action. However, this simple example doesn't really help when you want to achieve something a little more intricate - for example using a completely different url for a resource. Examples include a blog when you want your urls to add blog posts to be /add rather than blogs/new. Or what about a application with a Users model that has different types of users that can be added - perhaps it is a medical database and you have Doctors and Nurses - you want your urls to reflect the kind of person you are adding or editing - so doctor/new or nurse/new rather than users/new.

Mapping urls like this is easy. However there is a little gotcha that you have to be aware of. Let's walk through an example and we'll see how to make it work

New project

We'll create a simple project for this example - a hypothetical book review site. We are going to have a model called BookReview obviously, and we will create it with a couple of properties - book_name and book_review. In a Rails project, run a quick scaffold and migrate to get things up and running fast.

$ rails g scaffold BookReview book_name:string book_review:string
$ rake db:migrate

Decide on our URL structure

Since this is a trivial example and our site will have no input other than book reviews, we want to use /add as our url to add a book review. This is only slightly neater than the default routes of book_reviews/add, but it serves us well for the sample.

Edit your routes

To map a url to a resources, we have to edit config/routes.rb. Currently it looks like this:

BookReviews::Application.routes.draw do
  resources :book_reviews

We will start by making Rails aware of our /add url. Edit your routes.rb to look like this:

BookReviews::Application.routes.draw do
  resources :book_reviews
  match 'add' => 'book_reviews#new'

Now if you fire up your application with rails s, you can navigate to localhost:3000/add and you should get something similar to this:

This is great - and you might be tempted to stop here, because it all looks good, however, let's add a small constraint to our application - we want to make a field mandatory - book_name for example. Edit app/models/book_review.rb and add in a constraint to ensure book_name is filled in.

class BookReview < ActiveRecord::Base
  attr_accessible :book_name, :book_review
  validates_presence_of :book_name

Oh no, our url changes on error

When we visit /add to add a book review and we forget to enter a book_name, our model will not save. Rails neatly handles that for us and provides a flash to tell us that we need to supply a book name. Unfortunately, our url is no longer /add! Try it out - leave off the book name and you'll be redirected to /book_reviews with the flash message!

This is annoying the url that renders is not /add but /book_reviews. Why? Well, the book_review controller responds to actions at /book_reviews - the action to create a book review is a "post" - you can see this if you view source on the /add url or book_reviews/new url (they are both the same thing at the moment). We have a form that is declared like this:

<form accept-charset="UTF-8" action="/book_reviews" class="new_book_review" id="new_book_review" method="post">

When we submit the form, it posts back to /book_reviews. Rails does a little magic and sends the request to our book_review controller's create end point. The create action tries to save the model, fails and decides to render the "new" view - no redirect, no changing of the we are stuck at book_reviews.

Is that so bad?

Well, in and of itself, this isn't so bad really. And to be honest, most users probably wouldn't even notice. However, we are perfectionists and we want our urls to be consistent, if we started on /add and we get an error, we want to return to /add and report the error. And believe me, this is really useful if you've inherited an application and the last programmer jokingly named some resources with less than appropriate names!

Change the url

"Aha" I hear you say "If the form's postback url is causing us problems, let's just change that". Good thinking batman, let's try it out. Edit views/book_reviews/_form.html.erb, changing the form_for helper

<%= form_for(@book_review, :url=> "/add") do |f| %> 

Go back to your browser and try it out - submit a review without a book name. Hmmm that's not quite right is it? When we submit now, we end up at the "/add" url, but we don't get the flash message telling us that we need a book_name anymore. If we fill in book_name, it doesn't actually save it either. Well that makes sense because our route that we defined earlier instructed Rails to render our book_review#new action whenever we visit "/add"- so that's what happens - we never ever hit our create action in the controller!

More routes

Here's the real magic - we can specify our route matching to respond to different http verbs with the handy :via property. Change your config/routes.rb to look like this:

BookReviews::Application.routes.draw do
  resources :book_reviews
  match 'add' => 'book_reviews#new', :via => :get
  match 'add' => 'book_reviews#create', :via => :post

We have kept our original route that matched "/add" to our "new" action in our book_review controller, but we have instructed Rails that this should only handle the "get" http verb. We have added in a new route matching "/add" with a "post" http verb and told Rails that when this happens, we should let the book_review controller's create action handle things for us.


Now when we visit "/add" and leave out a book_name, we get sent right back without the saving, as we should, we also get our flash message *and* we remain on the "/add" url!

Your turn

This is a workable solution at the moment. There are a few things to clean up though - note the _form partial will serve up both new and edit actions for book reviews. That means on edit, you'll currently be redirected to "create" (remember, we have manually specified a url). You should tidy that up with a little logic, unless you aren't offering edit functionality. The other little loose end is the nasty somewhat magic string in your _form partial specifying the "/add" url. We can tidy that up in Rails by creating a named route, but I'll cover that in another shorter post.


Popular posts from this blog

Getting started with Ruby on Rails 3.2 and MiniTest - a Tutorial

For fun, I thought I would start a new Ruby on Rails project and use MiniTest instead of Test::Unit. Why? Well MiniTest is Ruby 1.9s testing framework dejour, and I suspect we will see more and more new projects adopt it. It has a built in mocking framework and RSpec like contextual syntax. You can probably get away with fewer gems in your Gemfile because of that.Getting started is always the hardest part - let's jump in with a new rails project rails new tddforme --skip-test-unit Standard stuff. MiniTest sits nicely next to Test::Unit, so you can leave it in if you prefer. I've left it out just to keep things neat and tidy for now. Now we update the old Gemfile: group :development, :test do gem "minitest" end and of course, bundle it all up.....from the command line: $ bundle Note that if you start experiencing strange errors when we get in to the generators later on, make sure you read about rails not finding a JavaScript runtime. Fire up your rails server…

Add css class to your label form helpers

Say, for example, you are using Twitter Bootstrap to style up a quick rails app and you want to throw in a label tag with the class set to "control-label" just like the bootstrap guys do you do it? And more importantly, since you don't want to specify the text of the label, how do you accomplish that? Fear not intrepid young but soon to be rails guru, behold the truth:

Overriding equality and Test Driven Development

Ruby has, at its root, an Object. Methods available in Object are available to every class because every class in Ruby inherits from Object somewhere in its own class hierarchy. Of course, you can override methods in subclasses, changing the functionality of a root method.You might stumble on to this idea if you work through Test Driven Development By Example by Kent Beck, translating the Java code into Ruby as you go. At some point pretty early on, he overrides the equality method on the Currency class to better test if two instances are equal. I'm going to do the same here, working with Instruments instead of Currency.EqualityEquality in Ruby can be expressed using any of the following three methods object == other equal?(other) eql?(other) These methods are defined on the base Object. The default implementation of equality will only return true if both objects are exactly the same. The interesting thing is that although these three methods start out functioning the same, the do…