This post is part of a series of reviews on the book Agile Web Development With Rails. Check out the Introduction post for a full table of contents along with some initial notes.

I’m making this ‘extra’ post as a response to a fellow reader who suggested something nice: what if the customer could register, log in and have his own personal page that would show him his previous orders?

Suggestions like these will make the application richer and will test everything we learned so far, so if you have anything to add, feel free to comment and I’ll gladly answer, and if necessary, create an ‘extra’ post like this one.

If you followed through the whole tutorial, I hardly suggest that you try on your own before looking at what I came up with, it’s a very nice exercise. But case you have no idea of what to do and just need some pointers so you can code on your own, I’ll first explain what we’ll be doing, then show the code.

We already have a log in system on the admin controller, but that’s for admins, and we surely don’t want customers messing with our application administration, so the first thing we need to do is create a new attribute in the user model named ‘admin’ and use that to know for sure if a user is a customer (0) or an administrator (1). But we still have a problem, the authorize action in the application controller, which makes sure a user is logged in before accessing administration pages, only checks for a user ID. Thing is, a customer will also have an ID, and that’s where the ‘admin’ attribute comes in handy: we’ll edit the authorize method to also check for a true ‘admin’ attribute, so now all the administration pages are back to only allowing administrators in. We can then overwrite the authorize method on the ‘personal’ controller to the same thing it did before, so it only asks for a user ID, something both an admin and a customer will have.

Finally we can create a ‘personal’ controller, which will have an index with all the customer’s orders and a show page with information about a specific order. And how the hell do we know which orders belong to a registered user? By adding a ‘user’ attribute to the order model and filling it with the logged in user ID on the checkout action, and yes, that will force all customers who want to order something to register and/or log in. Oh, almost forgot about the register page, we’ll be needing that (on a new controller), and since we’re on to details, edit the user show/edit actions with the new ‘admin’ attribute, this way an administrator will also have the power to register a customer. You will, obviously, get a ton of errors, but errors are meant to guide you, not freak you out, so learn from them until you manage to get things right.

As said earlier, all this is just the kind of complication we needed to exercise our knowledge in Rails. Let’s get to the code.

First, let’s add the ‘admin’ attribute to the users table through a migration:

ruby script/generate migration add_admin_to_user admin:boolean
rake db:migrate

Next, we’ll need to edit our login function in the admin controller to set a session variable case the logged user is an administrator:

def login
  session[:user_id] = nil
  if request.post?
    user = User.authenticate(params[:name], params[:password])
    if user
      if user.admin?
        session[:admin] = 1
        session[:user_id] = user.id;
        redirect_to :action => "index"
      else
        session[:user_id] = user.id;
        redirect_to :controller=> :personal,:action => "index"
      end
    else
      flash.now[:notice] = "Invalid user/password combination"
    end
  end
end

We’ll also need to neutralize that new session variable in the logout method:

def logout
  session[:user_id] = nil
  session[:admin] = nil
  flash[:notice] = "Logged out"
  redirect_to(:action=> "login")
end

Let’s alter the authorize method in the application controller to also check for an existing session[:admin] value:

def authorize
  unless User.find_by_id(session[:user_id]) and !session[:admin].nil?
    flash[:notice] = "Please log in"
    redirect_to :controller => :admin, :action => :login
  end
end

Now let’s add ‘user’ attribute to the order model:

ruby script/generate migration add_user_to_order user:int

Don’t rake db:migrate yet, let’s create another migration to make some changes to the ‘orders’ table:

ruby script/generate migration remake_orders

Now open that migration file and leave it like this:

class RemakeOrders < ActiveRecord::Migration
  def self.up
	remove_column :orders,:name 
	remove_column :orders,:email
	remove_column :orders,:address
  end

  def self.down
  end
end

We’re basically removing all that personal info from the orders table, since they will now belong to a user, so we’ll need to edit the users table again and add the email and address fields, since he already has a name, so we’ll need another migration:

ruby script/generate migration add_info_to_user email:string address:string

Now we can migrate:

rake db:migrate

These changes will affect the view files related to the users and the orders model (don’t try accessing them, you’ll get a huge error), but we’ll get to them later. Now we have to generate a controller for the personal user page:

ruby script/generate controller personal index show

Not much to do, it’s basically the same as the orders controller, except that it shows only the logged customer’s orders:

class PersonalController < ApplicationController
  def index
    @orders = Order.find :all, :conditions => ["user=?",session[:user_id]]
  end

  def show
    @order = Order.find(params[:id])
    if @order.user!=session[:user_id]
      redirect_to :action => :index
      flash[:notice] = "That order is not yours. #{@order.user}"
    end
  end

  protected

  def authorize
    unless User.find_by_id(session[:user_id])
      session[:original_uri] = request.request_uri
      flash[:notice] = "Please log in"
      redirect_to :controller => :admin, :action => :login
    end
  end

end

Remember that we have to overwrite the authorize method to this new one, or else a customer will never be able to log in because he will never be an administrator. Now we’re gonna create a controller that will be responsible for registering new customers:

ruby script/generate controller register index create authorize

This one will look alot like the users controller, because it does a very similar thing: creating users. This is the whole code to the ‘register’ controller:

class RegisterController < ApplicationController
  
  def index
    @user = User.new
  end

  def create
    @user = User.new(params[:user])
    respond_to do |format|
      if @user.save
        session[:user_id] = @user.id
        flash[:notice] = "Greetings #{@user.name}, your registration was successful."
        format.html { redirect_to(:controller => :personal,:action => :index) }
        format.xml  { render :xml => @user, :status => :created, :location => @user }
      else
        format.html { render :action => "index" }
        format.xml  { render :xml => @user.errors, :status => :unprocessable_entity }
      end
    end
  end

  def authorize
  end

end

All the new functionalities have been implemented, now we’ll basically need to fix the ones that got affected by what we did and fill in the new view files. Let’s start by the ‘personal’ controller’s index view file:

<h1>Hello <%= @user.name %>!</h1> These are all your orders: 
<table>
  <% for order in @orders %>
    <% user = User.find(order.user) %>
  <tr>
    <td>Order number: <%=  link_to order.id, {:action=>:show, :id=>order.id} %></td>
  </tr>
  <% end %>
</table>

Now the ‘personal’ controller’s show view file:

<p>
  <b>Payment type:</b>
  <%= h @order.pay_type %>
</p>

<p>
  <b>Line Items:</b><br/>
  <% for item in @order.line_items  %>
  <%= item.quantity %> &times; <%= link_to(item.product.title, product_url(item.product))  %> = <%= number_to_currency item.total_price %><br/>
  <% end %>
</p>

<p>
  <b>Total price:</b>
  <%= number_to_currency @order.total %>
</p>

<%= link_to 'Back', {:action=>:index} %>

Now that the personal page is fully functional, let’s fix all other things that got messed up due to our new changes. First we’ll change the checkout view file in the store controller:

<div class="depot-form">
  <%= error_messages_for 'order' %>
  <% form_for :order, :url=>{:action => :save_order} do |form| %>
  <fieldset>
    <legend>Please enter a paytment method</legend>
    <p>
      <%= label :order, :pay_type, "Pay with:" %>
      <%= form.select :pay_type, Order::PAYMENT_TYPES,
                      :prompt=>"Select a payment method" %>
    </p>
    <%= hidden_field_tag 'order[user]', session[:user_id] %>
    <%= submit_tag "Place Order", :class=>"submit" %>
  </fieldset>
  <% end %>
</div>

Notice that now all the user has to do is fill in a payment method and he is all set, the rest of the data will belong to the customer and not the order. I used a little trick here with the hidden_field_tag, since we added a ‘user’ field to the orders data, we need to insert that data in somehow, and that’s what I came up with.

This new checkout page still isn’t 100% functional because it has some old validation, so we need to alter the order model and remove that validation, leaving only the ones related to the pay_type:

class Order < ActiveRecord::Base

  PAYMENT_TYPES = [
    #Displayed        stored in DB
    ["Check",         "check"],
    ["Credit card",   "cc"],
    ["Purchase order","po"]
  ]

  validates_presence_of  :pay_type
  validates_inclusion_of :pay_type, :in => PAYMENT_TYPES.map {|disp,value| value}
  
  #rest of the code

I moved the address and email validation over to the user model, which we will get to in a bit. Let’s fix up the orders’ view files, because right now an admin will get a huge error message case he tries to access them, but first we need to add a little something to the orders controller:

def show
  @order = Order.find(params[:id])
  @user = User.find(@order.user)
  respond_to do |format|
    format.html
    format.xml {render :xml => @order}
  end
end

Now we’re set, let’s edit the index view file on the orders controller:

<h1>Listing orders</h1>
<table>
  <tr><th>Buyer Name</th></tr>
  <% for order in @orders %>
  <% user = User.find(order.user) %>
  <tr>
    <td><%=  link_to user.name, order %></td>
  </tr>
  <% end %>
</table>
<br />

And the show view file:

<p>
  <b>Customer:</b>
  <%= link_to @user.name, {:controller=>:users, :action=>:show, :id => @user.id} %>
</p>
<p>
  <b>Payment type:</b>
  <%= h @order.pay_type %>
</p>

<p>
  <b>Line Items:</b><br/>
  <% for item in @order.line_items  %>
  <%= item.quantity %> &times; <%= link_to(item.product.title, product_url(item.product))  %> = <%= number_to_currency item.total_price %><br/>
  <% end %>
</p>

<p>
  <b>Total price:</b>
  <%= number_to_currency @order.total %>
</p>

<%= link_to 'Back', orders_path %>
<%= link_to 'Destroy', @order ,:confirm => 'Are you sure?',:method => :delete %>

We’re almost done. The application has changed alot and the main menu should go along with that change, so edit the store layout file with this:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" >
<html>
<head>
<title>Pragprog Books Online Store</title>
<%= stylesheet_link_tag "depot" , :media => "all" %>
<%= javascript_include_tag :defaults %>
</head>
<body id="store">
<div id="banner">
<%= image_tag("logo.png" ) %>
<%= @page_title || "Pragmatic Bookshelf" %>
</div>
<div id="columns"> </div>
<div id="side">
  <div id="cart">
    <% if @cart %>
      <% hidden_div_if(@cart.items.empty?, :id => "cart" ) do %>
      <%= render(:partial => "cart", :object => @cart) %>
      <% end %>
    <% end %>
  </div>
  <%= link_to 'Store',  :controller => 'store' %><br />
  <%= link_to 'Personal Page',  :controller => 'personal' %><br />
  <% if session[:admin] %>
    <br/>
    <%= link_to 'Orders',  :controller => 'orders' %><br/>
    <%= link_to 'Products',:controller => 'products' %><br/>
    <%= link_to 'Users',   :controller => 'users' %><br/>
    <% end %>
  <% if session[:user_id] %>
    <br/>
    <%=link_to 'Logout',   :controller => :admin, :action => 'logout'%>
  <% end %>
    <br/><br/>
</div>
<div id="main">
  <% if flash[:notice] -%>
  <div id="notice"> <%=flash[:notice]%> </div>
  <%  end -%>
<%= yield :layout %>
</div>
</body>
</html>

Remember I said we’d go back to the users? We’ll do it now. I know the focus of this is to create a customer functionality but we can’t forget that everything needs to be administrated, so to finalize what we did, let’s edit some users files.

First, remember we took our the address and email validations from the order model? Let’s insert them in the user model now:

require 'digest/sha1'

class User < ActiveRecord::Base

  validates_presence_of :name , :address, :email

  #rest of the code

To the view files. The index page doesn’t need to be changed, so let’s move on to the ‘show’ page:

<p>
  <b>Name:</b>
  <%=h @user.name %>
</p>

<p>
  <b>Address:</b>
  <%=h @user.address %>
</p>

<p>
  <b>E-mail:</b>
  <%=h @user.name %>
</p>

<% if @user.admin?  %>
  This user is an administrator. <br/><br/>
<% end %>

<%= link_to 'Edit', edit_user_path(@user) %> |
<%= link_to 'Back', users_path %>

Now, the ‘edit’ page:

<h1>Editing user</h1>

<% form_for(@user) do |f| %>
  <%= f.error_messages %>

  <p>
    <%= f.label :name %><br />
    <%= f.text_field :name %>
  </p>
  <p>
    <%= f.label :email %><br />
    <%= f.text_field :email %>
  </p>
  <p>
    <%= f.label :address %><br />
    <%= f.text_field :address %>
  </p>
  <p>
    <%= f.label 'Administrator?' %><br />
    <%= f.select :admin, [['Yes', true],['No',false]] %>
  </p>
  <p>
    <%= f.submit 'Update' %>
  </p>
<% end %>

<%= link_to 'Show', @user %> |
<%= link_to 'Back', users_path %>

And to end it all, the ‘new’ page:

<div class="depot-form">
  <%= error_messages_for :user%>
  <% form_for(@user) do |f| %>
  <fieldset>
    <legend>Enter User Details</legend>
    <p>
      <label for="user_name">Name:</label>
      <%=  f.text_field :name, :size=>40 %>
    </p>
    <p>
      <label for="user_email">E-Mail:</label>
      <%=  f.text_field :email, :size=>40 %>
    </p>
    <p>
      <label for="user_address">Address:</label>
      <%=  f.text_field :address, :size=>40 %>
    </p>
    <p>
      <label for="user_password">Password:</label>
      <%=  f.password_field :password, :size=>40%>
    </p>
    <p>
      <label for="user_password_confirmation">Confirm:</label>
      <%=  f.password_field :password_confirmation, :size=>40 %>
    </p>
    <p>
      <label for="user_admin">Administrator?</label>
      <%= f.select :admin, [['Yes', true],['No',false]] %>
    </p>
    <p>
      <%=f.submit "Add User", :class => "submit" %>
    </p>
  </fieldset>
  <% end %>
</div>

<%= link_to 'Back', users_path %>

That’s it, we integrated a new functionality that changed the flow of most part of the application. But this was just my way of doing it, and it was all based on what was learned in the previous tasks. Of course that, to finish perfectly, we’d have to run some tests on this, but that’s something I’ll leave for you guys to do as practice :P (lousy excuse for lazyness, haha)

Just wanted to thank Millisami again for suggesting this, thanks to you I have greatly increased my knowledge in Rails by doing this all by myself without the guidance of any book, which is why I insist that more suggestions should be made.

If anyone did this a different way, do comment about it. Feel free to ask anything too. Cheers.