Tuesday, 16 October 2012

Rails Callbacks and Boolean Values

Rails has a lovely feature as part of ActiveRecord - Callbacks; to wit:

Callbacks are hooks into the life cycle of an Active Record object that allow you to trigger logic before or after an alteration of the object state

Basically, that means that you can instruct Rails to execute your own code throughout the process of creating, updating and destroying objects/records. You can, for example, use them to set some default values on create, or perhaps do some last minute validation, or even fire an event just after you've saved a record. The available callbacks are:

  • after_initialize
  • after_find
  • after_touch
  • before_validation
  • after_validation
  • before_save
  • around_save
  • after_save
  • before_create
  • around_create
  • after_create
  • before_update
  • around_update
  • after_update
  • :before_destroy
  • around_destroy
  • after_destroy
  • after_commit
  • after_rollback

At the same time, Ruby methods have the concept of implicit return values. That is to say, any method will return the value of the last evaluated statement. This is neat - you don't need to write an explicit "return" at the end of your methods.

When we mangle these two features together, we can create subtle bugs in our code. Well not so subtle, because your code will just stop working the way you expect, and it can be fairly difficult to debug because no error is thrown.

Consider the Rails Callbacks again:

If a callback returns false, all the later callbacks and the associated action are cancelled

And I'm sure you'll see where I'm going with this now.....Take an example where you have a boolean value as one of your fields in your model. You want to set this boolean in a callback, either as a default value or because it represents some state based on some other logic.

Let's contrive an example - a blog application which allows comments. We want comments to be moderated of course, because spam is bad. We create a quick boolean on our comment model called "isinmoderation". Every time a new comment is submitted, we want to set this variable to true. We have an added piece of logic to trust any comments from readers who have submitted five good (non-spammy) comments.

Given the above, we might end up with something like the following:

class Comment < ActiveRecord::Base
  before_create :moderate

    def moderate
      unless is_from_trusted_reader
        this.isinmoderation = false

We run some tests and find that new comments from untrusted readers are not being saved to the database. We notice that our transaction is being rolled back, but there's no error. For a while, we are confused. And then we remember this blog post and realise that we are returning a falsey value from one of our callbacks....and, as the Rails docs say,

all the later callbacks and the associated action are cancelled

And the mystery is solved. So is there an elegant way to ensure we don't hit this condition? Well one way out is to explicitly declare a return of true from our moderate method. It is a little less elegant than the normal implicit return, but c'est la vie

Monday, 8 October 2012

Install or update gems behind corporate firewall or proxy server

If you work with Ruby on Windows in a corporate environment, sooner or later, you will hit an error when you are trying to install or update Gems. In my case, I simply had to set a few environment variables and I was good to go. RubyGems uses the HTTP_PROXY, HTTP_PROXY_USER and HTTP_PASS by default so if you open up a command prompt and set these as follows, it will use them to find the proxy and authenticate you to your proxy server:

set HTTP_PROXY=http://pr.oxy.ip:port
set HTTP_PROXY_USER=domain\user.name
set HTTP_PASS=sUp3rS3curepAs$w0rd

Extra points: A reader sent me a comment that this will only work when the proxy is using basic authentication. If you're having trouble using this method then you can give ntlmaps a shot. It's basically a little bit of python magic that sets up a proxy on your localhost through which you can route traffic and it will handle the authentication with your proxy server for you.

Thursday, 4 October 2012

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 url....so 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.