Chapter 8: Refining Your Code

  1. Refining Your Model

    We refine the User model just to make other files, like controllers and views, simpler, easier to write and read. We will just exclude some of the commonly used stuff and put it into the model, in form of functions. Look at the following code:

    app/models/user.rb
    class User < ActiveRecord::Base
      attr_accessor :current_password
    
      validates_presence_of :screen_name, :e_mail, :password
      validates_uniqueness_of :screen_name
      validates_uniqueness_of :e_mail, :case_sensitive => false
      validates_length_of :screen_name, :within => 6..30
      validates_length_of :e_mail, :within => 6..50
      validates_length_of :password, :within => 6..30
      validates_confirmation_of :password
      validates_format_of     :e_mail,     :with => /^[A-Z0-9_.%-]+@([A-Z0-9_]+\.)+[A-Z]{2,4}$/i,
                                           :message => "must be a valid e-mail address"
      validates_presence_of :forename, :surname
      
      def full_name
        [forename, surname].join(' ')
      end
      
      def login!(session)
        session[:user_id] = id
      end
      
      def self.logout!(session)
        session[:user_id] = nil
      end
      
      def self.logged_in?(session)
        session[:user_id] != nil
      end
      
      def self.logged_in(session)
        return nil if not logged_in?(session)
        User.find(session[:user_id])
      end
      
      def update_attributes(attributes)
        if attributes[:password].empty?
          # the user doesn't want to change the password
          attributes[:password] = password
          attributes[:password_confirmation] = password
        elsif attributes[:current_password] != password
          # the user provided incorrect current_password
          errors.add(:current_password, "is incorrect")
          return false
        end
        super(attributes)
      end
    end
    

    We have added the following functions:

    full_name
    Displaying the full name (forename + surname) is quite common, especially in the views. The full_name is not present in the database and therefore is not available as the user member variable, but thanks to the definition of this function you can use expressions like @user.full_name as it were.
    login!
    This function replaces the session[:user_id] = id that appeared in several places in the user controller. This just slightly influenced the length of the code but, by adding the meaning to the snippets like @user.login!(session) it's contributed the text clarity and understandability. Notice the exclamation mark, used to point out that this function changes the state of the application (from not logged to logged).
    logout!
    Similar to the previous one. Notice that to logout you do not need any particular instance of user, therefore this function should be used for the whole class, ie. User.logout!(session) rather than @user.logout!(session). Notice the word self just before the name of the function: this tells the Ruby interpreter that this is a class function, not an instance function, and that it should be called for the entire class, not just an individual object.
    logged_in?
    Another example of a class function. Returns a logical expression which is true if someone is logged in and false if there is no currently logged user.
    logged_in
    Yet another class function, returns the is of the currently logged user (or nil if none).
    update_attributes
    This is an overriden function. This means that it was already defined in our class, and we have even used this function from within the user controller. The new, updated (or: overwritten) version of this function additionally pre-processes the password (for the discussion of the password update check Chapter 7), and, after that, it calls it standard (also known as: super) version to update all the attributes.

    Look below for some samples of the possible modifications in the code. The first file shows how a sample view template can benefit from the full_name function.

    app/views/user/find_by_town.html.erb
    <h1>All People in <%= @town %></h1>
    <ul>
    <% @users.each do |user| %>
      <li><%= link_to user.full_name, 
                    :controller => :user,
                    :action => :profile,
                    :id => user.screen_name %></li>
    <% end %>
    </ul>
    

    Here is the complete source code of the User Controller with modifications made possible with the new functions defined in the User Model:.

    app/controllers/user_controller.rb
    class UserController < ApplicationController
    
      before_filter :protect, :except => [:login, :register, :tech]
    
      def index
        @title = "User Index"
        @user = User.logged_in(session)
      end
    
      def register
        @title = "Register"
    
        if request.post?
          @user = User.new(params[:user])
          if @user.save
            @user.login!(session)
            flash[:notice] = "User with login #{@user.screen_name} created!"
            redirect_to :action => :index
          end
        end
      end
    
      def login
        @title = "Log in to Fakebook"
      
        if request.post?
          user = User.find_by_screen_name_and_password(params[:user][:screen_name], params[:user][:password])
          if user
            user.login!(session)
            flash[:notice] = "User #{user.screen_name} logged in"
            redirect_to :action => "index"
          else
            flash[:notice] = "Invalid email/password combination"
          end
        end
      end
    
      def logout
        flash[:notice] = "User " + User.logged_in(session).screen_name + " logged out"
        User.logout!(session)
        redirect_to :controller => :site, :action => :index
      end
      
      def edit
        @title = "Edit Your Details"
        @user = User.logged_in(session)
        if request.post?
          if @user.update_attributes(params[:user])
            flash[:notice] = "Your details have been updated"
            redirect_to :action => :index
          end
        end
        @user.password = nil
        @user.password_confirmation = nil
      end
    
      def profile
        @screen_name = params[:id]
        @title = "User Profile for #{@screen_name}"
        @user = User.find_by_screen_name(@screen_name)
      end
    
      def find_by_town
        @town = params[:id]
        @title = "All people from #{@town}"
        @users = User.find(:all, :conditions => "town = '#{@town}'")
      end
    
    private
      # Protect a page against not logged users
      def protect
     	  unless User.logged_in?(session)
          flash[:notice] = "Please login first"
          redirect_to :action => :login
    	    return false
    	  end
      end
    end
    
  2. Refining User Hub & Profile

    To go on smoothly, we can also slightly modify the User Hub and Profile pages. These modifications include a better use of HTML headings and lists. They are quite straightforward and do not require any comment:

    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>
    
    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 %>
    

Valid XHTML 1.0 Strict Valid CSS!