Monthly Archives: October 2010

Dynamic Lazy Loading

While reading a blog post the other day about the benefits of lazy loading, I took a look at one of my own code bases and thought about how it could benefit from lazy loading. A majority of tables in the database where had their own specific class file, some classes were group in the same file. Other classes were grouped in subfolders. All of these separate files were in one directory models, which was included on every page load. This ended up being some 150 different files and 10 megabytes of parsed code. This definitely seemed like an area for improvement.
Given the mangled naming structure using a naming schema was immediately ruled out of consideration. I next thought about creating a map, the downside to this was map maintenance. I hate having to do stuff in 3 places, add the table to the database, create the php file, and then add it to the map. I thought there must be an easier way to do this, and there was scripted model map generation.
Since my code base could was already completely able to load every model with out a problem there were three things left to do.

  1. Load the code base and figure out which classes were default to php and which were my own.
  2. Figure out what file each class was declared in
  3. Store the data to a flat file for later reading.

1. Figuring Out Whose Class It Is:

To figure out which classes were mind and which were natively part of php is a rather simple task using the get_declared_classes() function I was able to determine which classes were native to php. After completely loading the code base, I ran the function again. The difference of the initial run and the second run is my defined classes.

2. Where Was It Declared:

To figure out where each class is declared you use PHP reflection.

<?php

foreach($class_list as $classname) {
    $reflector = new ReflectionClass($classname);
    $ini_config_data[strtolower($classname)] = $reflector->getFileName();
}

?>
view raw gistfile1.php This Gist brought to you by GitHub.

Now ini_cofig_data is a hash of classname => absolute/path/to/class.php

3. How To Store It:

Incase you couldn’t guess I used an ini file. PHP has native ini parsing to array (parse_ini_file) so the read is extremely simple. The write ends up being rather simple as well.

<?php
$fp = fopen('object_map.ini', 'w');

foreach($ini_config_data as $class => $filename) {
    $write = $class . ' = ' . $filename . PHP_EOL;
    fwrite($fp, $write, strlen($write));
}

fclose($fp);
?>
view raw gistfile1.php This Gist brought to you by GitHub.

Writing __autoload:

The autoload function is very straight forward, it takes 1 parameter the name of the class to load. We must look this up from are ini array and load it. The code could look like this:

<?php
$model_object = parse_ini_file('object_map.ini');
function __autoload($class_name) {
    global $model_object;
    $class_name = strtolower($class_name);
    if (!isset($model_object[$class_name])) {
        exit('invalid class name');
    }
    
    require_once($model_object[$class_name]);
}
?>
view raw gistfile1.php This Gist brought to you by GitHub.

There is a bit of hack to get model_object global, but you can add it as an attribute to your framework that is returned via a static call? Just don’t read the file every time

Now after I create a new model, I simply rerun the script and I’m good to go.

Custom Date Formatting in Ruby on Rails

Ruby on Rails offers a couple different standardized date formats which can be really helpful. I’m personally a huge fan of :db, I hate looking it up all the time. But you may be wondering how to create a custom date format. Easy enough, open and create a new file(edit if you already have it) config/initializers/time_formats.rb.
In this file we’ll keep all of our custom date formats.
Add the following code:

Time::DATE_FORMATS[:slashy_format] = "%m/%d/%Y"
DATE::DATE_FORMATS[:slashy_format] = "%m/%d/%Y"

Restart your server and there you have it.
This format can accessed via
@something.date.to_formatted_s(:slasy_format)

There’s not really much else to it.

Setup Double Foreign Keys

This is how to create a model relationship in Ruby on Rails where one model, message, has two foreign keys to another model, user. The sql tables are defined below, and is given the solution in Ruby and how to retrieve the values.


create table `user`(
    `id` INT(10) PRIMARY KEY,
    `name` VARCHAR(25)
);
create table `message` (
    `id` INT(10) PRIMARY KEY,
    `author_user_id` INT(10),
    `recipient_user_id` INT(10),
    `subject` VARCHAR(250)
);

To create the correct ORM relations use the following ruby code:

class Message < ActiveRecord::Base
    belongs_to :author, :class_name => "User", :foreign_key => "author_user_id"
    belongs_to :recipient, :class_name => "User", :foreign_key => "recipient_user_id"
end

With the above code you can access user data via

<%= @message.author.name %>

How to Build Forums in Rails 3

This tutorial will go over the basic steps to create a forums app. The forums will extend from my previous blog post about integrating Rails 3 with AuthLogic. If you are new to rails I’d high recommend reading it, otherwise continue. First download the source code from the AuthLogic App and move the code to more logically named directory for this project.


$ mkdir forums
$ git init
$ git clone git://github.com/baileylo/login_app.git forums
$ cd forums
$ rake db:migrate
$ rails -s

Feel free to verify that everything is working, you should be able to register and login at /users.

The Backend

Rails has reserved “thread” has a model name so this will create a weird model name but the other two model names are straight forward: Board, Conversation, Comment.

Board is a set of boards, groupings of conversations. Conversation, probably more commonly referred to as “Thread”, is list of Comments made by users. In our forum we’ll keep it simple, A Board will contain only a name and an id, a Conversation will belong to a board. A Conversation will have a title and a creating user. Comments will have only a body a posting user and a reference to the conversation they belong to.


$ rails g scaffold board id:integer title:string
$ rails g scaffold conversation id:integer title:string board_id:integer user_id:integer
$ rails g scaffold comment id:integer user_id:integer conversation_id:integer body:text

This created all the controllers, views, models, and migration files needed. Open up the three migration files created, and edit them to look as follows.

class CreateBoards < ActiveRecord::Migration
  def self.up
    create_table :boards do |t|
      t.integer :id
      t.string :title, :limit => 50

      t.timestamps
    end
    
  end

  def self.down
    drop_table :boards
  end
end


class CreateComments < ActiveRecord::Migration
  def self.up
    create_table :comments do |t|
      t.integer :id
      t.integer :user_id
      t.integer :conversation_id
      t.text :body

      t.timestamps
    end
    add_index :comments, :conversation_id
    add_index :comments, :user_id
  end

  def self.down
    drop_table :comments
  end
end

class CreateConversations < ActiveRecord::Migration
  def self.up
    create_table :conversations do |t|
      t.integer :id
      t.string :title, :limit => 50
      t.integer :board_id
      t.integer :user_id

      t.timestamps
    end
    add_index :conversations, :board_id
    add_index :conversations, :user_id
  end

  def self.down
    drop_table :conversations
  end
end

These changes add indexes to our Conversations and Comments tables. These will add a much needed performance boost when querying the database. They also added size limitations to board.title and conversation.title of 50 characters.

Now create the database tables by running:


$ rake db:migrate

Some Quick Backend Additions

Feel free to browse around the site, you’ll notice all the forms work correctly, but none of the objects are linked together. To tell active record that these models are linked we must edit the Model Classes.
Open up your newly created model classes and edit them to look like this:

class Board < ActiveRecord::Base
  has_many :conversations
end

view raw board.rb This Gist brought to you by GitHub.
class Comment < ActiveRecord::Base
  belongs_to :conversation
  belongs_to :user
  validates_presence_of :body
end

view raw comment.rb This Gist brought to you by GitHub.
class Conversation < ActiveRecord::Base
  has_many :comments
  belongs_to :user
  belongs_to :board
  validates_presence_of :title
end

In this we see two very important ideas. We’ve added form validation as well as ORM hooks that define relationships between our models. In board.rb and conversation.rb we added has_many :conversations and has_many :comments, respectively, this informs Rails these are 1 to many relationships. You can see the rails docs for has_many here. This makes the relationship from one conversation to many comments. In conversation.rb and comments.rb you can see the belongs_to function, this function tells rails that these objects belong to another specific object, you can see the docs for belongs_to here.
validates_presence_of is called when a save, update, or create are called on an object. This function makes sure that there is data stored in these member variables. We specified that conversation.title and comment.body must be required, we don’t want any empty posts. Board.title was purposefully skipped, it is not for general use.

If you navigate to your conversations page: http://localhost:3000/conversations and try to create a message, not including a title, you will get a nice error saying that it is a required field. You will how ever notice that you can make this field as long as you like even though we specified it should only be 50 characters long. Lets fix this, add the following line of code to your conversation.rb file:


validates_length_of :title, :maximum=>50

If you try again you will see an error message stating that the “the input must be less than 50 characters”.

If you’ve created any data, you may want to clean it up now, it may cause problems later in this demonstration.

Routes!

Now that we have the backend in order, lets create the routes needed to use this message board. Open up your config/routes.rb file and change it to look like this:

LoginApp::Application.routes.draw do

  resources :boards do
    resources :conversations
  end

  root :to => "boards#index"

  resources :users, :user_sessions
  match 'login' => 'user_sessions#new', :as => :login
  match 'logout' => 'user_sessions#destroy', :as => :logout
end

view raw routes.rb This Gist brought to you by GitHub.

Delete your public/index.html file, it’s no longer needed.

Now localhost:3000/ will display a list of your Boards, instead of the rails information page.

In your console type:


$ rake routes

This is a list of every url that your app can handle, the most important are the top few. We’ve now created a url hierarchy. All of a conversations will be listed by /boards/:board_id/conversations/:id.

Jumping into the view

Our site looks okay, but lets try and spiff it up a bit. Open up your views/layouts/index.html and change it to look like:

<!DOCTYPE html>
<html>
<head>
  <title>Rails Message Boards</title>
  <%= stylesheet_link_tag :all %>
  <%= javascript_include_tag :defaults %>
  <%= csrf_meta_tag %>
</head>
<body>

    <div id="wrapper">
        <div id="top_nav">
            <h1><%= image_tag("rails.png", :size => '50x50') %> Rails Boards</h1>
            
            <% if current_user %>
            <%= link_to "Edit Profile", edit_user_path(current_user.id)%> |
            <%= link_to "Logout", :logout%>
            <% else %>
            <%= link_to "Register", new_user_path%> |
            <%= link_to "Login", :login %>
            <% end %>
            <div class="bclear"></div>
        </div>
        
    
        <div id="content">
            <%= yield %>
        </div>

        <div id="footer">

        </div>
    </div>
</body>
</html>

Create a new file in public/stylesheets called style.css and copy this style sheet, http://gist.github.com/627780

.

At localhost:3000 you should be greeted by a much friendlier page:
after_style

If you haven’t created a board yet, feel free to create one now using the “Create Board” Link at the bottom of the page. You can put in you own ID if you want, if you leave it blank rails will auto increment the id.

Lets fix up that homepage a bit and make it look slightly more professional. Open up views/boards/index.html, and change it to look like this:

<h1 id="page_title">Message Boards</h1>
<p id="notice"><%= notice %></p>
<table>
  <tr>
    <th align="left">Title</th>
  </tr>

<% @boards.each do |board| %>
  <tr>
    <td><%= link_to board.title, board %></td>
  </tr>
<% end %>
</table>

Now that our landing page looks decent, lets see what happens when we view our message board:

board_view_start

Not what anybody envisions a messageboard to be looking like, lets make some quick cosmetic changes before moving into the controller. Open up app/views/boards/show.html.erb and make it the following changes:

<h1 id="page_title"><%= @board.title %></h1>
<p id="notice"><%= notice %></p>
<%= link_to 'Rails Boards', boards_path %> &gt; <%= link_to @board.title, @board %> <br />
<%= link_to 'Post New Message', new_board_conversation_url(@board) %>

As you can see here we added a “Post New Message” link, the url function did not come out of thin air, if you run rake routes again. You’ll see there is a path named “new_board_conversation”, you can use any of those as a url by appending _url or _path to them.

If you click on the link to post new message you’ll get “No Routes Matches” error. We’ll fix that in a minute, but first lets make some adjustments to our conversation controller, open up app/controllers/conversation.rb and make the follow changes:

class ConversationsController < ApplicationController
  before_filter :load_board
  
  # GET /conversations
  # GET /conversations.xml
  def index
    @conversations = Conversation.all

    respond_to do |format|
      format.html # index.html.erb
      format.xml { render :xml => @conversations }
    end
  end

  # CODE REMOVED FOR CLARITY SAKE
  
  private
  
  def load_board
    if Board.exists?(params[:board_id])
      @board = Board.find(params[:board_id]);
    end
              
    unless @board
      redirect_to(boards_path, :notice =>"Please specify a valid board")
    end
  end
end

We added a private function that tries to load a Board off a passed in parameter. This function will be called on any page that goes through this controller.

Modify AuthLogic

If you have tried to login you may have noticed that we skipped a step. The login, logout, and register functions are not exactly tied to our application. To fix this we need to correct links in the views and redirect the controllers. While we’re doing this it will be a good time to remove functions that the app won’t be using. Open up app/controllers/users_controller.rb and edit it to look like so:

class UsersController < ApplicationController

  # GET /users/1
  def show
    @user = User.find(params[:id])
  end

  # GET /users/new
  def new
    @user = User.new
    
    if current_user
      redirect_to(homepage_url, :notice => 'Already registered')
    end
  end

  # GET /users/1/edit
  def edit
    is_user
  end

  # POST /users
  # POST /users.xml
  def create
    @user = User.new(params[:user])
    @user_session = UserSession.new(params[:user])

    respond_to do |format|
      if @user.save && @user_session.save
        format.html { redirect_to(homepage_url, :notice => 'Registration successfull.') }
      else
        format.html { render :action => "new" }
      end
    end
  end

  # PUT /users/1
  def update
    is_user

    respond_to do |format|
      if @user.update_attributes(params[:user])
        format.html { redirect_to(@user, :notice => 'User was successfully updated.') }
      else
        format.html { render :action => "edit" }
      end
    end
  end
  
  private
  
  def is_user
    if User.exists?(params[:id])
      @user = User.find(params[:id]);
      if !current_user || current_user.id != @user.id
        redirect_to(homepage_url, :notice =>"You do not have access to that page")
      end
    else
      redirect_to(homepage_url, :notice =>"You do not have access to that page")
    end
  end

end

In this controller we added a function is_user. This function determines if the user is logged in and they are the correct user to be viewing specific pages. We also changed redirect urls to the site homepage and removed a lot of unused code. Now lets update our User View: We’ll have to change the links on all the pages as well as display less confidential information on the Profile page.

<h1 id="page_title">Edit Your Profile</h1>

<%= render 'form' %>

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

view raw edit.html.erb This Gist brought to you by GitHub.
<h1 id="page_title">Registration</h1>

<%= render 'form' %>

<%= link_to 'Back', homepage_url %>

view raw new.html.erb This Gist brought to you by GitHub.
<h1 id="page_title"><%= @user.username %>'s Profile</h1>
<p id="notice"><%= notice %></p>

<p>
  <b>Username:</b>
  <%= @user.username %>
</p>

<p>
  <b>Email:</b>
  <%= @user.email %>
</p>


<%= link_to 'Rails Boards', homepage_url %>

view raw show.html.erb This Gist brought to you by GitHub.

Now we just have to fix the Login view and the login/logout controller. These files need to have some cosmetic changes and fix issues with redirects open up app/views/user_sessions/new.html.erb and app/controllers/user_sessions_controller.rb and edit them to look like the following:

<h1 id="page_title">Login</h1>

<%= render 'form' %>

<%= link_to 'Back', homepage_url %>

view raw new.html.erb This Gist brought to you by GitHub.
class UserSessionsController < ApplicationController

  # GET /user_sessions/new
  def new
    @user_session = UserSession.new
  end

  # POST /user_sessions
  def create
    @user_session = UserSession.new(params[:user_session])

    if @user_session.save
      redirect_to(homepage_url, :notice => 'Login Successful')
    else
      render :action => "new"
    end
  end

  # DELETE /user_sessions/1
  def destroy
    @user_session = UserSession.find
    @user_session.destroy

    redirect_to(homepage_url, :notice => 'Goodbye!')
  end
end

Now we just changed all the automatic form rerouting to point to our default homepage, added some basic styling, and changed the links in the site to point to more logical places. Lets create some content

Creating a New Post

There are four parts to our new posts: author, board, title, body. Author will be determined by the user_session, board will be determine through the URL, what our form needs to get from the user is title and body. We must modify the Conversation controller to create a Comment object which we can use in the view.
Open up app/controllers/conversation.rb and modify create and new functions to look like this:

# GET /conversations/new
def new
  @conversation = Conversation.new
  @comment = @conversation.comments.build

  respond_to do |format|
    format.html # new.html.erb
  end
end

# POST /conversations
def create
  @conversation = Conversation.new(params[:conversation])
  @comment = @conversation.comments.build(params[:comment])
  @conversation.user_id = @comment.user_id = current_user.id
  @conversation.board = @board
  
  respond_to do |format|
    if current_user && @conversation.save
      format.html { redirect_to(board_path(@board), :notice => 'Your Post was created') }
    else
      format.html { render :action => "new" }
    end
  end
end

The build function was automatically created when we used has_many :comments in the model, it creates an unsaved object using default values. In the create function we’ve added the board and author variable assignment, we also check to see if the user is logged in before we save.

if current_user && @conversation.save

We create @comment in both places for simplicity. Both of these function use the same view, create if there is an error and new by default, thus we need the same variables in scope. In the create function we pass in params[:comment] to build, this will build a Comment object with any data passed in the comment parameter.

Now lets setup our views, change app/views/conversations/index.html.erb, app/views/conversations/_form.html.erb to look like this:

<%= form_for(@conversation, :url => board_conversations_path) do |f| %>
  <% if @conversation.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(@conversation.errors.count, "error") %> prohibited this conversation from being saved:</h2>

      <ul>
      <% @conversation.errors.full_messages.each do |msg| %>
        <li><%= msg %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= f.label :title %><br />
    <%= f.text_field :title %>
  </div>

  <%= fields_for(@comment) do |cf|%>
  <div class="field">
    <%= cf.label :body %><br />
    <%= cf.text_area(:body, :size => "60x10")%>
  </div>
  <% end %>
  

  <div class="actions">
    <%= f.submit :as => "Create Post"%>
  </div>
<% end %>

<h1 id="page_title">New conversation</h1>
<%= link_to @board.title, @board %>

<%= render 'form' %>

<%= link_to @board.title, @board %>

view raw new.html.ner This Gist brought to you by GitHub.

The changes to new.html.erb are fairy straight forward. We added an id to the h1 tag, and changed the link to display the name of the current table and link to the current table. We modified_form.html.erb pretty heavily, we removed alot of the input fields that will be assigned in the controller. We also modified the error printing at the top to include any errors from @comment. fields_for works like form_for but does not create a new form tag. This allows us to create fields that are related to other objects.

Lets create our first post. When you’re done it should look something like this:

[img new_post.jpg]

Viewing Our Post

You may be wondering where your posts are? Clearly they’re in the database but they’re not showing up in our view. Lets fix that, open up app/views/boards/show.html.erb

<h1 id="page_title"><%= @board.title %></h1>
<p id="notice"><%= notice %></p>
<%= link_to 'Rails Boards', boards_path %> &gt; <%= link_to @board.title, @board %> <br />

<table>
  <tr>
    <th>Post / Author</th>
    <th>Last Reply</th>
    <th>Number of Replies</th>
  </tr>
<% @board.conversations.each do |conversation| %>
  <tr>
    <td>
      <%= link_to conversation.title, board_conversation_path(:board_id => @board, :id => conversation) %><br />
      <%= conversation.user.username %>
    </td>
    <td>
      <%= conversation.comments.find(:last).created_at %><br />
      by <%= conversation.comments.find(:last).user.username %>
    </td>
    <td>
      <%= conversation.comments.count - 1%>
    </td>
  </tr>
<% end %>
</table>
    

<%= link_to 'Post New Message', new_board_conversation_url(@board) %>

We display all the conversations in that board along with some basic meta data. Most of this code is rather straight forward, the link to uses one of the routes that can be easily found using $ rake routes . We use the find function which was also provided when we used has_many. We subtract one for the number of replies so that we do not count the post it self. But all in all this view is as straight forward as they come.

Now lets work on the message view itself. Open up app/views/conversation/show.html.erb and edit to look like this:

<%= link_to @conversation.board.title, @conversation.board%>
<p id="notice"><%= notice %></p>



<table>
  <tr>
    <th colspan="2"><%= @conversation.title %></th>
  </tr>
  <% @conversation.comments.each do |comment| %>
  <tr>
    <td style="width:25%;">
      <%= link_to comment.user.username, comment.user %> <br />
      Posted At <br />
      <%= comment.created_at%>
    </td>
    <td>
      <p><%=comment.body%></p>
    </td>
  </tr>
  <% end %>
</table>
<%= link_to 'Reply', reply_board_conversation_url(:board_id=>@board, :id=>@conversation)%> |
<%= link_to @conversation.board.title, @conversation.board%>

view raw show.html.erb This Gist brought to you by GitHub.

Once again nothing to exciting happening here, the more astute of you may have realized we created a url function that we don’t currently have mapped. Lets create that route, add


get '/boards/:board_id/conversations/:id/reply' => "conversations#reply", :as => :reply_board_conversation
post '/boards/:board_id/conversations/:id/reply' => "conversations#save_reply", :as => :reply_board_conversation

This will create two different url matches, one for GET requests and one for POST, one when the page is called and one when the form is submitted.
Lets got and create the reply and save_reply functions in the conversation controller, they should look fairly similar to the new and create functions, or like these:

  # GET /conversations/reply
  def reply
    @conversation = Conversation.find(params[:id])
    @comment = @conversation.comments.build
    
    respond_to do |format|
      format.html #reply.html.erb
    end
  end
  
  # POST /conversations/reply
  def save_reply
    if !current_user
      redirect_to(:login, :notice =>"Please login before posting")
      return 1;
    end
    
    if Conversation.exists?(params[:id])
      @conversation = Conversation.find(params[:id])
      @comment = @conversation.comments.build(params[:comment])
      @comment.user_id = current_user.id
    else
      redirect_to(boards_path, :notice =>"Please specify a valid board")
    end
        
    respond_to do |format|
      if current_user && @comment.save
        format.html { redirect_to(board_path(@board), :notice => 'Your reply was posted') }
      else
        format.html { render :action => "new" }
      end
    end
  end

Once again here nothing is too revolutionary. reply handles the page load. It creates a @conversation and @comment for use in the view to generate forms. The @conversation is loaded from the url, as is a @board. @comment is empty. save_reply handles POST requests, so form submissions. It verifies that the user is logged in and that the parent conversation exists. It then creates the comment from the conversation, assigns the user, and assigns the values passed in from the form. It saves the data and redirects the user to the board overview with a friendly reminder.

Reply function uses reply.html.erb as it’s view, since that hasn’t been created yet lets do that now. Create another file as well _reply_form.html.erb. They should look like this:

<%= form_for(@comment, :url => reply_board_conversation_url(:board_id=>@board, :id=>@conversation)) do |f| %>
  <% if @comment.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(@comment.errors.count, "error") %> prohibited this reply from being saved:</h2>

      <ul>
      <% @comment.errors.full_messages.each do |msg| %>
        <li><%= msg %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= f.label :body %><br />
    <%= f.text_area(:body, :size => "60x10")%>
  </div>
  

  <div class="actions">
    <%= f.submit(:value => "Create Reply")%>
  </div>
<% end %>

<h1 id="page_title">Reply To <%= @conversation.title %></h1>
<%= link_to @board.title, @board %>

<%= render 'reply_form' %>

<%= link_to @board.title, @board %>

There is nothing we haven’t seen before in these files by themselves. Reply.html.erb includes _reply_form.html.erb you can tell this by the render statement, ‘reply_form’ is prepended with ‘_’ so you know it’s not a controller view. _reply_form.html.erb uses the same action as we saw to generate the url to this page, and only has a text area input field.

Feel free to try it now out now. The complete forum/message board should be working. If you have any questions feel free to leave a comment or view the working code base on my GitHub For Rails boards

Running Rails 3 on Different Port

To run the rails webbrick server on a different por use the follow command:
rails s --port=3030
This will run the server on port 3030. To find a full list of options use the follow command
rails s --help

How to Setup AuthLogic In Rails 3

This will go over, from start to finish, how to implement session handling using AuthLogic in Ruby On Rails 3.

Up And Running

To get up and running we’ll need our new application, user model, user controller, and some basic styling so that nobody is too confused with what they’re doing.

Run the following in your terminal:

$ rails new login_app
$ cd login_app
$ vim Gemfile
/* Add line "gem 'authlogic'" somewhere in the gem file */
$ bundle install

This will create an application called “login_app”, add the authlogic gem, and install the gem if it is not already installed.

Next we’ll create our user model, controllers, and model classes.


$ rails generate scaffold user username:string email:string crypted_password:string password_salt:string persistence_token:string
$ rake db:migrate

Crypted_password persistence_token are both expected attributes from authlogic. Now we’ll need to setup our User Model to work as the authentication class.

class User < ActiveRecord::Base
  acts_as_authentic
end

view raw user.rb This Gist brought to you by GitHub.

Now that our Model is setup, lets work on the UI. Lets add a global register link so that anybody logged out can register easily. Open your /app/views/layouts/application.html.erb file to look like this:

<!DOCTYPE html>
<html>
<head>
  <title>LoginApp</title>
  <%= stylesheet_link_tag :all %>
  <%= javascript_include_tag :defaults %>
  <%= csrf_meta_tag %>
</head>
<body>

    <div id="nav">
        <%= link_to "Register", new_user_path%>
    </div>


<%= yield %>

</body>
</html>

If you haven’t started your server yet feel free to start it now.


$ rails s

As you can see we’re using the built in rails scaffolding. Since this is a basic application we’ll have the user overview list act as our main page. Lets clean it up a bit, it currently shows encrypted passwords, salts, and tokens. Open your /app/views/users/index.html.erb and edit it to look like this:

<h1>Listing users</h1>

<table>
  <tr>
    <th>Username</th>
    <th>Email</th>
    <th></th>
    <th></th>
    <th></th>
  </tr>

<% @users.each do |user| %>
  <tr>
    <td><%= user.username %></td>
    <td><%= user.email %></td>
    <td><%= link_to 'Show', user %></td>
    <td><%= link_to 'Edit', edit_user_path(user) %></td>
    <td><%= link_to 'Destroy', user, :confirm => 'Are you sure?', :method => :delete %></td>
  </tr>
<% end %>
</table>

<br />

<%= link_to 'New User', new_user_path %>

Now fire up your browser and check your nifty site. If you goto localhost:3000 you should see the default rails homepage, navigate to localhost:3000/users. It should now look something like this

user_overview

There are currently not users listed because nobody has registered. If you open up the registration page you’ll see that it doesn’t look much like anything, lets add some quick formatting there as well. Open up app/views/users/_form.html.erb and change it to look like this:

<%= form_for(@user) do |f| %>
  <% if @user.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(@user.errors.count, "error") %> prohibited this user from being saved:</h2>

      <ul>
      <% @user.errors.full_messages.each do |msg| %>
        <li><%= msg %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= f.label :username %><br />
    <%= f.text_field :username %>
  </div>
  <div class="field">
    <%= f.label :email %><br />
    <%= f.text_field :email %>
  </div>
  <div class="field">
    <%= f.label :password %><br />
    <%= f.password_field :password %>
  </div>
  <div class="field">
      <%= f.label :password_confirmation %><br />
      <%= f.password_field :password_confirmation %>
    </div>
  <div class="actions">
    <%= f.submit %>
  </div>
<% end %>

The main changes here are rather simple. We changed the field name from :crypted_password to :password. Authlogic will map the :password field to :crypted_password after hashing it. We also changed the field type from f.text_field to f.password_field, this will create your standard password input field instead of a plain text input field. We have also added a :password_confirmation field. All of the logic to support these fields is built into authlogic. Try registering with an invalid e-mail address. You should see a page like this:

invalid_email

Authlogic adds in auto e-mail validation on email forms, as well as some other basic settings that you can change yourself.

Now lets create our first account. As you can see it has displayed our hashed passwords and some other data that no user really needs to be bothered with. Also the flash notice isn’t exactly user friendly. To fix these open up app/controllers/user_controllers.rb and edit the create function to make it look like this.

  def create
    @user = User.new(params[:user])

    respond_to do |format|
      if @user.save
        format.html { redirect_to(:users, :notice => 'Registration successfull.') }
        format.xml { render :xml => @user, :status => :created, :location => @user }
      else
        format.html { render :action => "new" }
        format.xml { render :xml => @user.errors, :status => :unprocessable_entity }
      end
    end
  end

In rails 3 the :flash has been replaced with :notice. As you can see this syntax is a little clearer. We’ll change this message to say “Registration Successful”, a little more user friendly. Also we change the redirect to the users list page instead of the view user page. If you register another account you’ll see a much cleaner UI interaction and interface.

Where’s my session?

The Model

You may be saying, “That’s neat where does the session handling come in”. We’re going to create a separate controller and model to handle our session handling logic. Since the user model contains all the information about our tracked object, we can just create an empty model called user_session, run the following command


$ rails g model user_session

You can delete the migration that was generated, it is not needed and only creates an empty table. Now lets setup our user_session model, edit your app/models/user_session.rb to look like this:

class UserSession < Authlogic::Session::Base
  def to_key
     new_record? ? nil : [ self.send(self.class.primary_key) ]
  end
  
  def persisted?
    false
  end
end

In rails 2 all that was required was changing the inherited class to AuthLogic::Session::Base, but with rails 3 there are some errors in the form handler. AuthLogic users a function called to_key which is clearly not defined. The function we have created does some ruby magic, if this is a new record it returns nil, as there is no key otherwise it calls send, basically saying self.primary_key. There is none on this object and we could most likely return nil at all times here, but this is nice to have anyways. Rails will also want to know if this object is persisted, it is not so we’ll just return false.

The Controller

Well now that we have a way to track sessions, we do authlogic. Lets create a way to login and log out. We’ll create a user_session controller to specifically handle this, once again we’ll use ruby generate with some scaffolding.


$ rails g scaffold_controller user_session username:string password:string

This command creates a controller that expects its respective object to have two attributes: a password, and a username. As you can probably tell from now our UserSession class does not have either of these, but our User model has a username as well as a crypted_password that is mapped to password by authlogic. It also has already been setup to expect authlogic with the act_as_authentic function.
If you open up your app/controllers/user_sessions_controller.rb you’ll see it has tons of generated code in there, we’re going to trim this down to a much nicer and more manageable file, that should look like this:

class UserSessionsController < ApplicationController

  # GET /user_sessions/new
  # GET /user_sessions/new.xml
  def new
    @user_session = UserSession.new

    respond_to do |format|
      format.html # new.html.erb
      format.xml { render :xml => @user_session }
    end
  end

  # POST /user_sessions
  # POST /user_sessions.xml
  def create
    @user_session = UserSession.new(params[:user_session])

    respond_to do |format|
      if @user_session.save
        format.html { redirect_to(:users, :notice => 'Login Successful') }
        format.xml { render :xml => @user_session, :status => :created, :location => @user_session }
      else
        format.html { render :action => "new" }
        format.xml { render :xml => @user_session.errors, :status => :unprocessable_entity }
      end
    end
  end

  # DELETE /user_sessions/1
  # DELETE /user_sessions/1.xml
  def destroy
    @user_session = UserSession.find
    @user_session.destroy

    respond_to do |format|
      format.html { redirect_to(:users, :notice => 'Goodbye!') }
      format.xml { head :ok }
    end
  end
end

The biggest changes here are in the destroy function. UserSession does not have a id, so one does not need to be passed in. Other than that, we just did some cosmetic changes, redirecting the create and destroy functions to to :users and changing their :flash text.

Logging In and Logging Out

Currently the login and logout functions are stored at user_session/create and user_session/destroy, not very pretty. Lets open up config/routes.rb and set up some cleaner urls. Your file should look like this:

LoginApp::Application.routes.draw do
  resources :users, :user_sessions
  match 'login' => 'user_sessions#new', :as => :login
  match 'logout' => 'user_sessions#destroy', :as => :logout
end

view raw routes.rb This Gist brought to you by GitHub.

We added :user_sessions to the resources, this is required for the login form submission. We then wrote to name match rules to change create routes for /login and /logout, this is a little more user friendly.

Now lets create the login link. We’ll want this link available everywhere, right next to our Register link preferably. Open up app/views/layouts/application.html.erb, and change the body to look like this

<body>

    <div id="nav">
        <%= link_to "Register", new_user_path%> |
        <%= link_to "Login", :login %>
    </div>


<%= yield %>

</body>

Since our route defines the login link as :login we can just refer to it as such, very easy and clean.
There are couple more quick UI changes, in app/views/user_sessions/new.html.erb change the h1 tag to say “Login”. In app/views/user_sessions/_form.html.erb change the password field to f.password_field just like we did in the register form.

Feel free to login now.

I Logged In But Nothing Happened?!?

It may seem that way, but we weren’t displaying the notice. Change app/views/users/index.html.erb to look like this:

<h1>Listing users</h1>
<p id="notice"><%= notice %></p>
<table>
  <tr>
    <th>Username</th>
    <th>Email</th>
    <th></th>
    <th></th>
    <th></th>
  </tr>

<% @users.each do |user| %>
  <tr>
    <td><%= user.username %></td>
    <td><%= user.email %></td>
    <td><%= link_to 'Show', user %></td>
    <td><%= link_to 'Edit', edit_user_path(user) %></td>
    <td><%= link_to 'Destroy', user, :confirm => 'Are you sure?', :method => :delete %></td>
  </tr>
<% end %>
</table>

<br />

<%= link_to 'New User', new_user_path %>

Now it will display what ever is in the notice variable. Lets also add a logout link, this should only be visible when the user is logged in.
Change app/views/layouts/application.html.erb nav div to look like this:

    <div id="nav">
        <% if current_user %>
        <%= link_to "Edit Profile", edit_user_path(current_user.id)%>
        <%= link_to "Logout", :logout%>
        <% else %>
        <%= link_to "Register", new_user_path%> |
        <%= link_to "Login", :login %>
        <% end %>
    </div>

current_user is a helper method that will return the current views user model or nil. If there is a current user, we’ll display an edit profile and a log out links, otherwise the reigster and login link. Now lets create that helper method, edit app/controllers/application_controller.rb so it looks like this:

class ApplicationController < ActionController::Base
  protect_from_forgery
  
  helper_method :current_user
  
  private
  
  def current_user_session
    return @current_user_session if defined?(@current_user_session)
    @current_user_session = UserSession.find
  end
  
  def current_user
    return @current_user if defined?(@current_user)
    @current_user = current_user_session && current_user_session.record
  end
end

current_user function is pretty much as we said earlier, it returns the current user object if viewer is logged in. It has global scope because of the helper_method :current user function call. current_user_session returns the current_session if the user is logged in.

Now refresh your browser page, you should see the Edit Profile, and Logout links. Feel free to play around with this, you can see the complete working code for this sample at it’s github repository:
http://github.com/baileylo/login_app