Chapter 9: Modelling User's Hobbies

The material provided below relies heavily on the stuff introduced in previous chapters. Where applicable, links to previous sections are added.

  1. Creating the Hobby Model & Controller

    The user can have many hobbies. This means that there is no space in the User model to include the information about hobbies: models can directly include only single pieces of information, no lists or collections.

    So far we did quite well with just a single model (User), but now it's the time to overcome the problem with multiple hobbies with a new Hobby model. We will use the standard generator:

    ruby script/generate model Hobby user_id:integer name:string description:string

    The Hobby model should include its name, description and the id of the user to which this hobby belongs. You can check if it is so in the migration file. Notice that your file may be named slightly different depending on how active you were with the activities so far.

    db/migrate/003_create_hobbies.rb
    class CreateHobbies < ActiveRecord::Migration
      def self.up
        create_table :hobbies do |t|
          t.integer :user_id
          t.string :name
          t.string :description
    
          t.timestamps
        end
      end
    
      def self.down
        drop_table :hobbies
      end
    end
    
    

    For this to work you have to "rake" your application. It is a common mistake to miss your rake call after you created a model.

    rake db:migrate

    Let's also generate a corresponding Hobby controller, with two actions:

    ruby script/generate controller Hobby add edit

    To start, we can apply the same security measures we used for the User controller (see Chapter 5.5) to protect the new actions againt unauthorised users.

    app/controllers/hobby_controller.rb
    class HobbyController < ApplicationController
    
      before_filter :protect
    
      def add
      end
    
      def edit
      end
      
    private
      # Protect a page against not logged users
      def protect
        unless User.logged_in?(session)
          flash[:notice] = "Please login first"
          redirect_to :controller => :user, :action => :login
          return false
        end
      end
    
    end
    
  2. Tying User and Hobby Together

    In the first sentence of this chapter we have stated that:

    • User has many hobbies.

    Let's add to that:

    • Hobby belongs to user.

    Quite surprisingly, these two sentences may be used almost intact to define the relationship between the User and Hobby models in a Rails application:

    app/models/user.rb
    class User < ActiveRecord::Base
      has_many :hobby
      ...
    
    app/models/hobby.rb
    class Hobby < ActiveRecord::Base
      belongs_to :user
      validates_presence_of :name, :description
    end
    

    We have also added a very simple validation for the Hobby model.

    Done this, you can:

    • access a collection of the hobbies of a given user with @user.hobby
    • access the user to which a given hobby belongs with @hobby.user
  3. Adding a New Hobby

    The reader should by now gain enough experience in Ruby on Rails to understand the code below without much commentary. The view template for the add action is similar to, say, user registration form (notice that in the error_messages_for and form_for statements the :user symbol has been replaced by :hobby):

    app/views/hobby/add.html.erb
    <h1>Add a New Hobby</h1>
    <%= error_messages_for :hobby %>
    <% form_for :hobby do |f| %>
      <p>
        <%= f.label :name %>:
        <%= f.text_field :name %>
      </p>
      <p>
        <%= f.label :description %>:
        <%= f.text_area :description %>
      </p>
      <p>
        <%= f.submit "Add" %>
      </p>
    <% end %>
    
    

    The corresponding action in the Hobby controller is again quite similar to the user registration action (see Chapter 4.6). The only new addition is that we add a line of code to link the newly created hobby with the user who is currently logged in:

    app/controllers/hobby_controller.rb
    class HobbyController < ApplicationController
    
      ...
    
      def add
        @title = "Add a New Hobby"
    	
        if request.post?
          @hobby = Hobby.new(params[:hobby])
          @hobby.user_id = User.logged_in(session).id
          if @hobby.save
            flash[:notice] = "Hobby #{@hobby.name} added!"
            redirect_to :controller => :user, :action => :index
          end
        end
      end
    
      ...
      
    end
    
    
  4. Listing Hobbies

    In the next step we will add code to User Hub and User Profile view templates so to render the user's hobbies:

    app/views/user/index.html.erb
    <h1>Hello, <%= @user.full_name %>!</h1>
    
    <h2>Your Details:</h2>
    <ul>
    <li>Screen name: <%= @user.screen_name %></li>
    <li>E-mail address: <%= @user.e_mail %></li>
    <li>Occupation: <%= @user.occupation %></li>
    <li>
      Town/City: <%= @user.town %>
      (<%= link_to "see all people in your town",
                    :controller => :user,
                    :action => :find_by_town,
                    :id => @user.town %>)
    </li>
    </ul>
    <p><%= link_to "Edit Your Details", :action => :edit %></p>
    
    <h2>Your Hobbies:</h2>
    <dl>
    <% @user.hobby.each do |hobby| %>
      <dt><%= hobby.name %></dt>
      <dd>
        <%= hobby.description %><br />
        <%= link_to "Edit", 
              :controller => :hobby, 
              :action => :edit,
              :id => hobby.id %> |
        <%= link_to "Delete", 
              :controller => :hobby, 
              :action => :delete,
              :id => hobby.id %>
      </dd>
    <% end %>
    </dl>
    
    <p><%= link_to "Add a Hobby", :controller => :hobby, :action => :add %></p>
    
    app/views/user/profile.html.erb
    <% if @user %>
      <h1><%= @user.full_name %>: User Profile</h1>
    
      <h2>Personal Details:</h2>
      <ul>
        <li>Screen name: <%= @user.screen_name %></li>
        <li>Occupation: <%= @user.occupation %></li>
        <li>Town/City: <%= @user.town %></li>
      </ul>
    <% else %>
      <h1>No User Profile for: <%= @screen_name %></h1>
    <% end %>
    
    <h2>Hobbies</h2>
    <dl>
    <% @user.hobby.each do |hobby| %>
      <dt><%= hobby.name %></dt>
      <dd>
        <%= hobby.description %><br />
      </dd>
    <% end %>
    </dl>
    

    Both files are pretty much similar. Notice the following:

    • Thanks to the magic of Rails we can access the collection of all the user's hobbies by typing simply @user.hobby and then apply the each function in the way you should already be familiar with. This will iterate through each hobby the given user has.
    • The definition list (<dl> ) is used to render hobby.name and hobby.description
    • Link to edit action is added. This action has been created when we generated the controller and will be implemented in the next section.
    • Link to delete action is also added. We will create this action soon.
    • The latter two links do not appear in the User Profile: this is a public page and we do not want members of the public to change or delete the private hobbies.
  5. Editing Hobbies

    Editing hobbies do not differ much from editing the user profiles, as described in Chapter 7.3 and will not be discussed in much detail here. The only important modification is that we use params[:id] to find the hobby to be updated. This is the last section of the URL address and the method has already been introduced in Chapter 7.6.

    app/views/hobby/edit.html.erb
    <h1>Edit a Hobby</h1>
    <%= error_messages_for :hobby %>
    <% form_for :hobby do |f| %>
      <p>
        <%= f.label :name %>:
        <%= f.text_field :name %>
      </p>
      <p>
        <%= f.label :description %>:
        <%= f.text_area :description %>
      </p>
      <p>
        <%= f.submit "Update" %>
      </p>
    <% end %>
    
    
    app/controllers/hobby_controller.rb
    class HobbyController < ApplicationController
    
      ...
    
      def edit
        @title = "Edit a Hobby"
        @hobby = Hobby.find(params[:id])
        if request.post?
          if @hobby.update_attributes(params[:hobby])
            flash[:notice] = "Hobby #{@hobby.name} updated!"
            redirect_to :controller => :user, :action => :index
          end
        end
      end
      
      ...
      
    end
    
    
    Problem

    We leave for a more ambitious reader the following problem: if you know the hobby id, for example 14, just navigating to http://localhost:3000/hobby/edit/14 you can simply edit the hobby even if it is not your hobby. The only condition that the application checks is if you are logged in: if you are, it will not check who you are and therefore you will be able to edit other people's hobbies. How to protect the application against this kind of abuse?

  6. Deleting Hobbies

    Finally, we will add an action to delete hobbies. A link to it is already available at the User Hub page (index page), now we will add the appropriate (and so far missing) action function to the Hobby controller. We will not need to provide a view template as the action immediately redirects to the User Index.

    app/controllers/hobby_controller.rb
    class HobbyController < ApplicationController
    
      ...
    
      def delete
        @hobby = Hobby.find(params[:id])
        flash[:notice] = "Hobby #{@hobby.name} deleted!"
        @hobby.destroy
        redirect_to :controller => :user, :action => :index
      end
      
      ...
      
    end
    
    

    The solution here is very easy: besides the usual stuff there is only one new and quite obvious line of code: @hobby.destroy.

Valid XHTML 1.0 Strict Valid CSS!