For my pet rails project I need to add a category (drop down) to a listings page.

Step 1: Create the category resource.

Using the rails scaffolding, I create a brand new resources and it’s respectful Restful services like this:

$ rails generate scaffold Category name:string

This gives me a simple categories table with an id and name lookup.

Step 2: Create the migration

I now want to connect the listing to the category through the category_id.

$ rails generate migration AddCategoryToListings category_id:int
class AddCategoryToListings < ActiveRecord::Migration
  def change
    add_column :listings, :category_id, :int
  end
end

> rake db:migrate
> rake db:test:prepare

Note: this is not a database enforced foreign key constraint. Just a rails active record connection.

Step 3: Update the models

Now I need to make it clear that a listing belongs_to a category, and that a category has_many listings a the model level.

I also need to make the new category_id variable available for mass assignment by adding it to the attr_accessible.

/models/listing.rb

class Listing < ActiveRecord::Base
  attr_accessible :title, :description, :category_id

  belongs_to  :category

/models/category.rb

class Category < ActiveRecord::Base
  has_many :listings
end

Step 4: Setup some test factories.

To make sure things are working at database level, I need some factories to give me some test data.

Here I define a factory for a user, listing, and category.

/spec/factories.rb

Factory.define :user do |user|
  user.name                  "Michael Hartl"
  user.email                 "mhartl@example.com"
  user.password              "foobar"
end

Factory.define :listing do |listing|
  listing.title "Title"
  listing.description "Description"
  listing.association :category
  listing.association :user
end

Factory.sequence :category_name do |n|
  "#{n}"
end

Factory.define :category do |c|
  c.name  Factory.next :category_name
end

Step 5: Test the model.

/spec/models/listing_spec.rb

require 'spec_helper'

describe Listing do

  before(:each) do
    @user = Factory(:user)
    @category = Factory(:category)
    @attr = { :title => "some title",
              :description => "some description",
              :category_id => @category.id }
  end

  it "should create a new instance given valid attributes" do
    @user.listings.create!(@attr)
  end

  describe "associations" do
    
    it "should have a category attribute" do
      @category =  @listing.should respond_to(:category)
    end

  end

  describe "validations" do

    before(:each) do
      @attr = { :title => "some title",
                :description => "some description",
                :category_id => @category.id }
    end

    it "should build a listing" do
      @user.listings.build(@attr).should be_valid
    end

    it "should require a category" do
      @user.listings.build(@attr.merge(:category_id => nil)).should_not be_valid
    end

  end

end

A few words about the controller and the view

Before we can test the controller and the model, it’s important to understand what’s going on.

The controller set’s things up for the view. That is, it sets up the the @listing attributes, so the view knows how to render the html necessary to create the view. And it populates the @categories object so the collection_select has all the category information it needs to populate a fully loaded category drop down.

/controllers/listings_controller.rb

class ListingsController < ApplicationController

  before_filter :authenticate, :only => [:create, :destroy, :new]
  before_filter :prepare_categories

  def new
    @listing = Listing.new
    @user = current_user
  end

  def create
    #raise params.inspect

    @listing  = current_user.listings.build(params[:listing])

    begin
      @listing.save!
      flash[:success] = "Listing created!"
      redirect_to root_path
    rescue Exception => e
      render :action => 'new'
    end
  end

  def destroy
  end

  # add the @categories = Category.All to the before action so avail for all actions

  private
    def prepare_categories
      @categories = Category.all
    end

end

Once the controller does this, the view is good to go.

/view/listings/new.html.erb

<%= form_for @listing do |f| %>
  <%= render 'shared/error_messages', :object => f.object %>
  <div class="field">
    <%= f.label :title %><br />
    <%= f.text_field :title, :size => 57 %>
  </div>
  <div class="field">
    <%= f.label :description %><br />
    <%= f.text_area :description, :class => 'description' %>
  </div>
  <div class="field">
    <%= f.label :category %><br />
    <%= f.collection_select(:category_id, @categories, :id, :name, :include_blank => "Please select") %>
  </div>
  <div class="actions">
    <%= f.submit "Submit" %>
  </div>
<% end %>

Step 6: Test the controller

/spec/controllers/listings_controller_spec

require 'spec_helper'

describe ListingsController do
  render_views
  
  describe "GET 'new'" do

    it "should be successful" do
      get :new
      response.should be_success
    end

    it "should prepare categories" do
      get :new
      assigns(:categories).should_not be_nil
    end
  end


  describe "POST 'create'" do

     describe "failure" do

       before(:each) do
         @attr = { :title => "", :description => "", :category_id => "" }
       end

       it "should not create a listing" do
         lambda do
           post :create, :listing => @attr
         end.should_not change(Listing, :count)
       end

       it "should render the home page" do
         post :create, :listing => @attr
         response.should render_template('listings/new')
       end
     end

     describe "success" do

       before(:each) do
         @attr = { :title => "Lorem ipsum",
                   :description => "Lorem ipsum",
                   :category_id => '1' }
       end

       it "should create a listing" do
         lambda do
           post :create, :listing => @attr
         end.should change(Listing, :count).by(1)
       end

       it "should redirect to the home page" do
         post :create, :listing => @attr
         response.should redirect_to(root_path)
       end

       it "should have a flash message" do
         post :create, :listing => @attr
         flash[:success].should =~ /listing created/i
       end
     end
  end
end

Step 7: Test the view.

/spec/requests/listing_spec.rb

require 'spec_helper'

describe "When creating a new listing" do

  it "should create" do

    # signin
    user = Factory(:user)
    visit signin_path
    fill_in "Email",    :with => user.email
    fill_in "Password", :with => user.password
    click_button

    lambda do
      visit '/listings/new'
      fill_in "Title",          :with => "SomeTitle"
      fill_in "Description",    :with => "SomeDescription"
      select "books",            :from => "listing_category_id"
      click_button
    end.should change(Listing, :count).by(1)
  end

end

Note: This test used webrat, and had some test fixture data loaded into the test database via

test/fixtures/categories.yml

one:
  name: books

two:
  name: computers
$ rake db:fixtures:load RAILS_ENV=test

Just a heads up – this example may not work out of the box. It’s just something I through up to remind myself what the steps were for the next time I did this.

To learn more about what’s going on behind the scenes I recommend Michael Hartl’s Ruby Tutorial.

Also apologies for any spelling/typos.

Advertisements