Perspectives

Testing

What is BDD and TDD?

When it comes to testing our specs and code, Rails developer have two mainstream choices. We can use Test::Unit, which is supplied by the Ruby Standard Library and is immediately supported by Rails, or we can use the RSpec gems and plugins. Although either one can get the job done, they vary significantly in how they approach testing.

Test::Unit supports a Test Driven Development (TDD) approach, while RSpec implies a Behavior Driven Development (BDD) approach. In TDD, we are usually writing one test per function, leading to many small tests. In TDD, since there is one test per function, we end up writing tests at a core level that end up following the same structure as our code. With a BDD approach, we are using specs as a guide for testing. We are telling a story by describing how a set of functionality should behave, and focus less on the return value of a single method. Think of BDD as a humanized TDD; it is TDD written to use a more English like vocabulary for expressing functionality. BDD makes it easier for other developers to read and understand the code, becoming an additional form of documentation. BDD changes the way we think about testing, since it is more focused on implementing high level specs.

Turning our attention back to Rails, we have access to both Test::Unit and RSpec testing frameworks. Test::Unit is more focused on TDD, while RSpec is more focused on BDD. It is possible to use a BDD approach with Test::Unit, though doing so wouldn't be as clear and concise as using RSpec. Each has their own pros and cons; for example, future updates may break our RSpec tests, causing developers to go back and update their tests into a working state. Test Unit tests won't be affected since it is part of the Ruby Standard Library and built into Rails. Which ever framework you do choose to use will be sufficient for testing, so it's more a question of your preferred testing ideology.

At Killswitch, we have chosen to use the BDD testing paradigm with RSpec. The following is a brief tutorial in getting up and running with RSpec and adopting the BDD approach.

Getting Started:

1. Create a new Rails app:

rails rspec_demo -d mysql

2. Set up your test database and development databases.

3. Install the RSpec Gem:

sudo gem install rspec

4. Install Plugins (git needed):

script/plugin install git://github.com/dchelimsky/rspec.git
script/plugin install git://github.com/dchelimsky/rspec-rails.git

5. Generate RSpec directories in root of application:

script/generate rspec

Using RSpec

First, let's create some models. RSpec gives us a new rspec_model command; it will create the model similar to using the Rails model generator, except it will create RSpec files instead of Test::Unit files:

script/generate rspec_model salesman first_name:string last_name:string total_products_sold:integer
script/generate rspec_model category title:string
script/generate rspec_model product title:string quantity:integer category_id:integer
script/generate rspec_model saleman_product saleman_id:integer product_id:integer amount_sold:integer

Before migrating, add 0 as a default value in the migrations for the following columns: total_products_sold in salesman, quantity in product, amount_sold in salesman_products.

Run our migrations with:

rake db:migrate
rake db:migrate RAILS_ENV=test

Next, we need to set up relationships and validations in our models:

# app/models/salesman.rb

class Salesman < ActiveRecord::Base
  has_many :salesman_products
  has_many :products, :through => :salesman_products

  validates_presence_of :first_name
  validates_presence_of :last_name
end


# app/models/product.rb

class Product < ActiveRecord::Base
  has_many :salesman_products
  has_many :salesmen, :through => :salesman_products
  belongs_to :category
end


# app/models/category.rb

class Category < ActiveRecord::Base  
  has_many :products

  validates_presence_of :title
end


# app/models/salesman_product.rb

class SalesmanProduct < ActiveRecord::Base
  belongs_to :salesman
  belongs_to :product
end

Open up salesman_spec.rb in spec/models. It should contain the following:

require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')

describe Salesman do
  before(:each) do
    @valid_attributes = {
      :first_name => "value for first_name",
      :last_name => "value for last_name",
      :total_products_sold => "1"
    }
  end

  it "should create a new instance given valid attributes" do
    Salesman.create!(@valid_attributes)
  end
end

The first line includes the spec_helper which is inside of the spec folder. This file will hold helper methods that all specs will use, I will not be going into detail on creating spec helpers, if you would like to learn more about spec helpers check out the official rspec site.

After the spec helper we have a describe block which will describe a given situation. One can put all their tests in one description block, but I usually like to create multiple description blocks that contains logic for a new record, existing records and relationships. Inside the description block there is a before block that sets up each test with needed data.

There is also an it block inside of the describe block, which is an individual test. One describe block can contain many it blocks. Each it takes a string argument that should describe the test logic it contains. With it being Salesman, the example test would read as "Salesman should create a new instance given valid attributes"

Before moving on, lets make it clear the type of tests this describe block will contain. Each describe block can take in a optional string argument. Lets test a new Salesman object, add "that is new," as the second argument in the describe block. Now the test would read as "Salesman that is new, should create a new instance given valid attributes"

describe Salesman, "that is new," do
  before(:each) do
    @valid_attributes = {
      :first_name => "value for first_name",
      :last_name => "value for last_name",
      :total_products_sold => "1"
    }
  end

  it "should create a new instance given valid attributes" do
    Salesman.create!(@valid_attributes)
  end

end

Try running the spec with:

ruby spec/models/user_spec.rb

You should see:

1 examples, 0 failures

RSpec does not print successful tests, only ones that failed. If there was a error, for example a NoMethodError, RSpec would tell us like so:

NoMethodError in 'Salesman that is new, should create a new instance given valid attributes'

Now since we are creating a new salesman, lets remove the total_products_sold set from the hash. Total products sold is not required, so let's write a new test that will check if a new salesman starts with the value of 0 for total_products_sold. While we're at it, lets change the "value for ...." default strings in the before block to have more realistic data. Currently our spec should look similar to the following:

describe Salesman, "that is new" do
  before(:each) do
    @valid_attributes = { :first_name => "Tony", :last_name => "Little" }
  end

  it "should create a new instance given valid attributes" do
    Salesman.create!(@valid_attributes)
  end

  it "should create a new salesman with zero total products sold" do

  end

end

Usually in Ruby when we want to make sure an attribute equals a certain value we usually use:

salesman.total_products_sold == 0

Using such syntax would make our tests look similar to the actual code. However, since RSpec has its own syntax similar to English, why not use the RSpec equivalent for more readable tests:

salesman.total_products_sold.should eql(0)

Lets test for zero:

it "should create a new salesman with zero total products sold" do
  salesman = Salesman.create!(@valid_attributes)
  salesman.total_products_sold.should eql(0)
end

Now when we run our specs, both tests should be a success.

So far we have made sure when valid data is used, our tests will pass. What if we wanted to test for errors? RSpec has syntax for testing errors. In our code we would check if an object is valid with the following:

salesman.valid?

The RSpec equivalent:

salesman.should be_valid

It's pretty straight forward, we are stating the salesman should be valid. We can also test to make sure it is not valid with salesman.should_not be_valid. Lets try it out by adding the following test:

it "should not be valid without a first name" do
  @valid_attributes[:first_name] = nil
  salesman = Salesman.new(@valid_attributes)
  salesman.should_not be_valid
end

In the above example we first changed the value of first_name to nil, and then we pass those attributes into a new Salesman object. Then when it reaches salesman.should_not be_valid, it will run through the validations, check to see if it was valid, if not, it will return true and our test will pass!

We can be more detailed and thorough in the previous example by checking to make sure the first_name attribute returns a error. To do this, we simply add the following to the end of the test:

salesman.should have(1).errors_on(:first_name)

We did a few tests for a brand new Salesman, so now let's do some model testing with existing data. Before starting, we will need some data that will be used throughout all our tests. We can use fixtures to create test objects which will add data to our test database. The test database will be reset before each test so changes to data from the previous test won't break the next test. Open up salesmen.yml inside of spec/fixtures and enter the following data:

mays:
  first_name: Billie
  last_name: Mays
  total_products_sold: 356

norris:
  first_name: Chuck
  last_name: Norris
  total_products_sold: 155

simmons:
  first_name: Richard
  last_name: Simmons
  total_products_sold: 186

So far we have created a describe block for a new salesman, next we will create another describe block for existing salesmen. I always separate existing from new for not only organizational purposes, but also for speed purposes. If we were to have many tests in a single describe block and load unnecessary fixtures for every test in a spec, it would lead to really slow tests since the database is reset every time with fixture data. So let's add a new describe block that will contain only the tests requiring fixtures. I have added comments in the code:

describe Salesman, "that exists" do
  # loads spec/fixtures/salemen.yml
  fixtures :salesmen

  before(:each) do
    # Using fixtures, not needed
  end

  it "should have a collection of salesmen" do
    # !Salesman.find(:all).empty?
    Salesman.find(:all).should_not be_empty
  end

  it "should have three records" do
    # Salesman.count == 3
    Salesman.should have(3).records
  end

  it "should find an existing salesman" do
    salesman = Salesman.find(salesmen(:mays).id)
    salesman.should eql(salesmen(:mays))
  end

end

For the last test we are using salesmen(:mays), this will grab the attributes with the title 'mays' from the salesmen.yml file. Since mays has an id of 1, it will tell ActiveRecord to grab the salesman with the id of 1. Next we check to make sure that the salesman object has the same attributes found in the .yml file.

Let's quickly add one more describe block that will test relationships between salesmen and products. Once again we will need fixtures, so open up products.yml and saleman_products.yml and add the following:

# products.yml

oxiclean:
  id: 1
  title: OxiClean
  quantity: 35
  category_id: 1 # Cleaning

orangeglow:
  id: 2
  title: OrangeGlow
  quantity: 42
  category_id: 1
  
discosweat:
  id: 3
  title: Disco Sweat
  quantity: 22
  category_id: 2 # Video
  
platinumsweat:
  id: 4
  title: Platinum Sweat
  quantity: 34
  category_id: 2
    
totalgym:
  id: 5
  title: Total Gym Workout Equipment
  quantity: 11
  category_id: 3 # Exercise Equipment

handyswitch:
  id: 6
  title: Handy Switch
  quantity: 23
  category_id: 4 # Home
  
fixit:
  id: 7
  title: Fix It
  quantity: 14
  category_id: 5
  
liquid_diamond:
  id: 8
  title: Liquid Diamond
  quantity: 7
  category_id: 5 # Auto


# saleman_products.yml

one:
  saleman_id: 1
  product_id: 1
  amount_sold: 75

two:
  saleman_id: 1
  product_id: 2
  amount_sold: 89

three:
  saleman_id: 2
  product_id: 3
  amount_sold: 87
  
four:
  saleman_id: 2
  product_id: 4
  amount_sold: 99
  
five:
  saleman_id: 3
  product_id: 5
  amount_sold: 45
  
six:
  saleman_id: 1
  product_id: 6
  amount_sold: 81
  
seven:
  saleman_id: 1
  product_id: 7
  amount_sold: 76
  
eight:
  saleman_id: 1
  product_id: 8
  amount_sold: 45

Finally add in a new describe block that will load only fixtures needed for testing salesmen and products relationships.

describe Salesman, "with Products" do
  # Will load 3 fixtures need for testing Salesman Product relationships
  fixtures :salesmen, :salesman_products, :products

  before(:each) do
    # Using fixtures
  end

  it "should have products" do
    salesman = Salesman.find(salesmen(:mays).id)
    salesman.products.should_not be_empty
  end

  it "should have five products" do
    salesman = Salesman.find(salesmen(:mays).id)
    salesman.products.should have(5).records
    # should include will check if product belongs to salesman
    salesman.products.should include(products(:oxiclean))
    salesman.products.should include(products(:orange_glow))
    salesman.products.should include(products(:handy_switch))
    salesman.products.should include(products(:fix_it))
    salesman.products.should include(products(:liquid_diamond))
  end

  it "should not have products from other salesmen" do
    salesman = Salesman.find(salesmen(:norris).id)
    # should_not include is the opposite of should include
    # we are making sure this salesman, doesn't have products that
    # other salesmen are selling
    salesman.products.should_not include(products(:disco_sweat))
    salesman.products.should_not include(products(:platinum_sweat))
    salesman.products.should_not include(products(:orange_glow))
    salesman.products.should_not include(products(:liquid_diamond))
  end

end

Now with your feet soaked into the world of BDD, it is time for you to start writing tests for your own applications. If you would like to continue building over the example above, more validations can be tested and specs can to be written for the other models as well. Be sure to check out the official RSpec site for more info.



Scissors2

Many Web Applications these days consist of two main parts: the public facing front-end, and the private, secure administration area. When developing with Ruby on Rails, the convention is to try to write code as DRY (don't repeat yourself) as possible and use one single controller for both the public side and the administration side of the site. For certain circumstances this is fine, but I think there is some flexibility here, and recently I've noticed that many times the Admin logic is quite a bit different that the public facing functionality — enough so to warrant its own set of admin controllers and views.

With Rails 2.0+, you can use namespacing to keep the admin controller logic and views separate from the public controller logic and views. I've come to really like the way this helps to keep both sections of the site separate and organized.

I'll demonstrate a very simple example of a web app that provides a way for an Administrator to create, edit, and delete articles with one controller, and for a visitor to view those articles with a separate controller.

Create Your Controllers

After you've created your new rails app, we can proceed by generating our two controllers to access our articles:

# our private admin controller uses namespacing. Notice the Admin:: prefix
./script/generate controller Admin::Articles
# our public controller is created normally
./script/generate controller Articles

We now have two controllers named articles_controller.rb. The public controller lives in app/controllers/ and the private controller lives in app/controllers/admin/ with its respective views in app/views/admin/articles. Once you've created the necessary views, you should have a file structure similar to the image below. Our public controller only needs two methods at this point: 'index' and 'show', while our admin controller will require all 7 standard REST methods.

Create Your Routes

Before either of these controllers can function, we need to setup our routes in the config/routes.rb file.

map.namespace :admin do |admin|
  admin.resources :articles
end

map.resources :articles
  

Take a second and run rake routes on the command line to view all of your current routes. You'll notice you now have 2 sets of articles routes, one of which targets your admin controller, and one which targets your public controller.

Handling Links and Forms

So how does this change how links are handled in the views? It turns out it's not all that complicated thanks to Rails creating a set of admin specific URL generation methods:

# Create a link to the public article page '/articles/:id'
<%= link_to 'View Article', article_url(@article) %>

# Create a link to view the article in the Admin area '/admin/articles/:id'
<%= link_to 'View Article', admin_article_url(@article) %>

# Create a link to edit the article in the Admin area '/admin/articles/:id/edit'
<%= link_to 'Edit Article', edit_admin_article_url(@article) %>

The same goes for using forms in the Admin area. Instead of using <% form_for(@article) do |f| %> you simply add in the admin namespace, and voila, your forms submit to the /admin/articles controller:

# Create a form that submits to the /admin/articles controller
<% form_for([:admin, @article]) do |f| %>
....
<% end %>

Securing and Styling Your Admin Controllers

You will no doubt want to secure all of your Admin controllers and require an Admin user to be logged in. If you use a plugin like the popular RESTUL Authentication, you can simply apply the before_filter :login_required to the top of each Admin controller.

Most of the time the Administration area is also styled differently than the public facing area of the site. Here we can simply put something like layout 'admin' at the top of our Admin controllers to use a separate admin.html.erb layout file instead of the public application.html.erb layout file.

class Admin::ArticlesController < ApplicationController
  before_filter :login_required
  layout 'admin'
  
  ... REST methods here ...
end

Wrapping Things Up...

You should now have the beginnings of a web app with a set of self-contained, password-protected Admin CMS controllers prefixed with /admin, and a set publicly viewable front-end controllers. If you're feeling adventurous, you could even extract the controllers, views, routes, and stylesheets into a plugin or a generator if many of the CMS features will be used throughout a variety of different applications.



1

A story about building an application

I was recently working on a Ruby on Rails application that had a section for sending messages. This sounds pretty easy, right? I started with a User model and a Message model and some basic associations:

 class User < ActiveRecord::Base
    has_many :messages
 end

 class Message < ActiveRecord::Base
    belongs_to :user
 end  

Fig.1 - initial User and Message models

But when it came time to actually start building the application, I found this simple model code was not enough. The devil is in the details, as they say. There was a lot of functionality I needed to add beyond just a list of messages connected to a User.

1) Filtered views

I needed different views of the messages such as sent messages, drafts, and deleted messages.

How do I determine 'draft' status? Well one way is to fill in a delivered_at date whenever a message is sent. Then a draft is just a Message with no delivered_at date.

So after adding that field (and a sender_id and receiver_id) to the database I went to my messages_controller.rb file and added a few methods that looked sort of like this:

 def index
   @messages = user.messages.find(:all, 
     :conditions => ['delivered_at is not NULL and recipient_id = ?', 
       user.id])
 end
 
 def sent_mail
   @messages = user.messages.find(:all, 
     :conditions => ['delivered_at is not NULL and sender_id = ?', 
       user.id])
 end
 
 def drafts 
   @messages = user.messages.find(:all, 
     :conditions => ['delivered_at is NULL and sender_id = ?', 
       user.id])
 end

Fig.2 - initial fragment from messages_controller.rb

2) Pagination

Nobody wants to load a page of 1000 messages at a time, so I needed to be able to break up that list into limited sized chunks. I used the excellent plugin will_paginate for that purpose. Then my controllers methods got a little more verbose:

 
 def index
   @messages = user.messages.find(:all, 
     :conditions => ['delivered_at is not NULL and recipient_id = ?', 
       user.id]
    ).paginate(:page => (params[:page] == "" ? 1 : params[:page]))
 end

 def sent_mail
   @messages = user.messages.find(:all, 
     :conditions => ['delivered_at is not NULL and sender_id = ?',
       user.id]
    ).paginate(:page => (params[:page] == "" ? 1 : params[:page]))
 end
 
 def drafts 
   @messages = user.messages.find(:all, 
     :conditions => ['delivered_at is NULL and sender_id = ?', 
       user.id]
    ).paginate(:page => (params[:page] == "" ? 1 : params[:page]))
 end

Fig.3 - fragment from messages_controller.rb with pagination

3) The ability to flag content (i.e. spam, objectionable content etc...)

What if someone gets spam in the message system - or something objectionable in some other way. Well I need to filter that stuff out. I added a Flag model and connected that to messages like so:

  class Message < ActiveRecord::Base
    has_many :flags
    belongs_to :user
  end

Fig.4 - Message model with flags added

However, at this point my controller methods are starting to look like this:

 def sent_mail
   @messages = user.messages.find(:all, 
     :conditions => ['delivered_at is not NULL and 
         flags.flagged_item_id is NULL and 
         recipient_id = ?', user.id], 
       :include => :flags
    ).paginate(:page => (params[:page] == "" ? 1 : params[:page]))
 end

 def sent_mail
   @messages = user.messages.find(:all, 
     :conditions => ['delivered_at is not NULL and 
         flags.flagged_item_id is NULL and 
         sender_id = ?', user.id], 
       :include => :flags
    ).paginate(:page => (params[:page] == "" ? 1 : params[:page]))
 end
 
 def drafts 
   @messages = user.messages.find(:all, 
     :conditions => ['delivered_at is NULL and
         flags.flagged_item_id is NULL and 
         sender_id = ?', user.id], 
       :include => :flags
    ).paginate(:page => (params[:page] == "" ? 1 : params[:page]))
 end

Fig.5 - fragment from messages_controller.rb with flags

I'm looking at some ugly code - with a lot of repetition. How do I pare this down?

Begin Pruning

My first thought is that if anything in my application can be flagged, I should be able to do a little meta-programming to create a find method that will give me only un-flagged items. Ideally I could even send in all the rest of the find arguments exactly the same.

There is the named_scope addition to Rails 2.x that does just that - but I also want something I can add to any class as a Mixin. That way I can write code like this:

  Message.unflagged_items.find(:all, 
    :conditions => ['delivered_at is not NULL'])
  SomeOtherThing.unflagged_items.find(:all, :conditions => ...)

Fig.6 - call to imagined method unflagged_items

The method with_scope is a good candidate for sending in some pre-determined find conditions - but leaving it open to add more later. I'm wanting to add the following method to all my classes that need to be flagged:

  def unflagged_items(*args)  
    self.with_scope(:find => {
        :conditions => 'flags.flagged_item_id is NULL', 
        :include => :flags}) do  
      self.find(*args)
    end  
  end

Fig.7 - code for imaginary unflagged_items method

How do I do that? Well, I can turn that code into a Module and add it to any class automatically using a little metaprogramming:

  module Flaggable

    def self.included(base)
      base.class_eval do
        has_many :flags, :as => :flagged_item, :dependent => :destroy
      end
      base.extend(ClassMethods)
    end

    module ClassMethods

      def unflagged_items(*args)  
        self.with_scope(:find => {
            :conditions => 'flags.flagged_item_id is NULL', 
            :include => :flags}) do  
          self.find(*args)
        end  
      end  
    end

  end

Fig.8 - Flaggable module

Any model I put the line include Flaggable in will have that method available. So if I include it in the User class I've added a method user.messages.unflagged_items which returns a sort of incomplete version of the find function - with all the necessary logic to limit the list to unflagged items already filled in. I still have to fill in the :all or :first or any other :conditions I want. But the function is sort of half-called. This is a useful thing - getting half-called functions. In functional programming it's called currying. I'll come back to that in a moment.

Anyway, So now my controller methods now look like this:

 def index
    @messages = user.messages.unflagged_items(:all,
      :conditions => ['delivered_at is not NULL and recipient_id = ?', 
        user.id]
    ).paginate(:page => (params[:page] == "" ? 1 : params[:page]))
 end

 def sent_mail
    @messages = user.messages.unflagged_items(:all, 
      :conditions => ['delivered_at is not NULL and sender_id = ?', 
        user.id]
    ).paginate(:page => (params[:page] == "" ? 1 : params[:page]))
 end
 
 def drafts 
   @messages = user.messages.unflagged_items(:all, 
     :conditions => ['delivered_at is NULL and sender_id = ?', 
       user.id]
    ).paginate(:page => (params[:page] == "" ? 1 : params[:page]))
 end

Fig.9 - fragment from new messages_controller.rb

Continue Pruning

It's getting better, but isn't there some way I can pare it down even more? Now I'll go to the User model. Instead of simply using has_many :messages - since has_many supports blocks - I can add some more convenience methods to the User class:

  class User < ActiveRecord::Base

    has_many :received_messages, :foreign_key => 'recipient_id', 
          :class_name => 'Message' do
      def delivered_and_unflagged(page=1)
          unflagged_items(:all, :conditions => 'delivered_at IS NOT NULL'
        ).paginate(:page => page, :per_page => @messages_per_page)
      end
    end

    has_many :sent_messages, :foreign_key => 'sender_id', 
          :class_name => 'Message' do
      def delivered_and_unflagged(page=1)
          unflagged_items(:all, :conditions => 'delivered_at IS NOT NULL'
        ).paginate(:page => page, :per_page => @messages_per_page) 
      end
    end

    has_many :draft_messages, :foreign_key => 'sender_id', 
          :class_name => 'Message', 
          :conditions => 'delivered_at IS NULL' do
      def paginated(page=1)
        paginate(:page => page, :per_page => @messages_per_page)
      end
    end

    def inbox(page=1)
      self.received_messages.delivered_and_unflagged(page)
    end

    def sent_mail(page=1)
      self.sent_messages.delivered_and_unflagged(page)
    end

    def drafts(page=1)
      self.draft_messages.paginated(page)
    end
  end

Fig.10 - more developed User model

I'm doing pretty well with reduction of code in my controller now. The only ugly bit of code leftover is the params[:page]... bit - but I can make that slightly better too by factoring it out. I would like to use params[:page] || 1 but params[:page] returns an empty string if there is no matching parameter and will_paginate interprets an empty string as a request for page 0 and returns an error. So I have to use the longer statement with the ternary operator. Now my controller code looks like this:

 def index
   @messages = user.inbox(figure_page)
 end
 
 def sent_mail
   @messages = user.sent_messages(figure_page)
 end
 
 def drafts 
   @messages = user.drafts(figure_page)
 end

 def figure_page
   params[:page] == "" ? 1 : params[:page]
 end

Fig.11 - pruned fragment from messages_controller.rb

I'm happy enough with that. I've made different lists of messages for the currently logged on User that automatically paginate and filter out flagged items with just one line of code per method.

3) Next and Previous Message

I'm not done yet though - because the view page of a message needs a next and previous link. So if the user is looking at a draft - next should be the next draft - not the next sent message - and previous should be the previous draft - not the previous sent message. Make sense?

One way I could do this is to have a show_draft method, a show_sent_item method etc... and just call the correct link from the correct listing page (i.e. the list of all drafts page has links to show_draft, the sent items page has links to show_sent_item etc...).

There are 2 problems with this though. 1) That is creating several methods for basically one 'show' action. So they will all be virtually the same code over and over again. 2) I'm using a partial to render the list of messages - so I'd have to send in some way to create a different link based on the type of filter ('drafts', 'sent mail' etc...) but I'd rather just call render :partial => "message", :collection => @messages. I don't want the partial to have to worry about what particular filtered list of messages it happens to be rendering.

I'm sure there are a lot of ways to solve this. What I came up with was to add a 'from' value as a parameter for each link_to :action => 'show' in the partial. That way I could just append params[:action] to every url and by the time the controller gets the request, it knows where the request is coming from. This gives me the information I need to respond differently to the show action depending on that parameter. And leaves that logic out of the view.

In order to get the next and previous messages though, I needed to be able to identify and generate a list of messages based on the value of a string (i.e. value of params[:from]).

The code I wrote at first looked something like this and was in the controller:

  def show
    @message = Message.find(params[:id])
    # need @messages for previous, next
    case params[:from]
    when 'sent_mail'
      @messages = user.sent_messages(figure_page)
    when 'drafts'
      @messages = user.drafts(figure_page)
    #...
  end

  def bulk_action
    # ... do bulk action

    # need @messages for previous, next
    case params[:from]
    when 'sent_mail'
      @messages = user.sent_messages(figure_page)
    when 'drafts'
      @messages = user.drafts(figure_page)
    #...
  end

Fig.12 - fragment of messages_controller.rb with new code

So I've lost some of my simplicity, I'm repeating myself again and my code is in need of pruning.

What I need is a function that returns a function waiting to receive arguments. This is similar to the with_scope method I mentioned earlier, and the idea of function currying. I need a function that's partially filled out - but not called yet - waiting for some parameters. This is a good place to use the the fact that a Method is just another object in Ruby - and create a method to return whichever User method I want.

A method that returns a method

  def get_messages_function(param)
    # special case of 'index' action
    if param == 'index'
       self.method('inbox')
    else
       self.method(param)
    end
  end

Fig.13 - fragment from User model

returns a method as an object waiting for arguments. So I can put that code in my User class and I can call it like this in my controller:

 def index
  @messages = user.get_messages_function(params[:action]).call(figure_page)
 end

 def sent_mail
  @messages = user.get_messages_function(params[:action]).call(figure_page)
 end
 
 def drafts 
  @messages = user.get_messages_function(params[:action]).call(figure_page)
 end

 def show
  @message = Message.find(params[:id])
  @messages = user.get_messages_function(params[:from]).call(figure_page)
  # ...
 end

Fig.14 - fragment from new messages_controller.rb

One last trick

I'm almost done. But I can go one step further in minimization of code. Taking advantages of the fact that a method can be converted to a block by putting an & in front of it. In the controller, since all the returned methods are taking that same figure_page parameter - I can factor that out as a method accepting a block and do something like this:

  def index
    @messages = find_messages(&user.get_messages_function(:inbox))
  end

  def sent_mail
    @messages = find_messages(&user.get_messages_function(:sent_mail))
  end

  def show
    @message = Message.find(params[:id])
    @messages = find_messages(&user.get_messages_function(params[:from])) 
    # ...
  end

  def figure_page
    params[:page] == "" ? 1 : params[:page]
  end

  private
  def find_messages(&func)
    yield(figure_page)
  end

Fig.15 - fragment from another revision to messages_controller.rb

It's odd looking, I admit. I've lost a little readability for the sake of density. But I've left myself very little code in the controller and nothing specific about controllers in the model. That much I like.

Conclusion

So if you ever writing a Ruby on Rails application that has messages that need to be filtered, paginated and include a detail view with a previous, next link - you might be able to glean some code from the article to help get started. Also, today's lesson is that it's sometimes handy to pass around functions as objects.

NOTE: I've included a zip file of various items related to this article. It includes some Ruby code as a demonstration which requires a sqlite3 installation. Also, I used Python to generate this document with all the color-coded sections. I've included that in case it is of interest to anyone. It requires the Mako and Pygments packages.




RSS Feed


CATEGORIES


ARCHIVES


BOOKMARKED


Add to Technorati Favorites