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.
Nov 04, 2012 @ 14:49:06
Maybe a lame question, but still – How do I now add categories to categories list?
Nov 05, 2012 @ 23:27:23
Not a lame question at all Kristaps.
After you create your categories as a resource, access the web page where you would create a new category i.e.
http://localhost:3000/categories/new
And fill out the form to create a new category.
You can add them manually like this.
Cheers – Jonathan
Aug 07, 2013 @ 15:57:50
Thank you for the amazing article, I did learn a whole bunch from it.
Any chance you can explain the best ways to make these categories clickable, in order to show contents of each category to the user?
Thank you so much