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.
Like this:
Like Loading...