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.
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:stringThe 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 endFor 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:migrateLet's also generate a corresponding Hobby controller, with two actions:
ruby script/generate controller Hobby add editTo 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 endTying 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 endWe 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
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 ... endListing 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.
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 ... endProblemWe 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?
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 ... endThe solution here is very easy: besides the usual stuff there is only one new and quite obvious line of code: @hobby.destroy.
Copyright (C) 2009-2011 by Jaroslaw Francik

