Working with Rails

Recommend Brian on Working With Rails


Powered

Building Tempo with Rails, Part IV

Posted by Brian Sam-Bodden Fri, 30 Nov 2007 05:00:00 GMT

Rails

In the last three installments of this series we concentrated our efforts on building the most important entities of the Tempo domain. If you are anything like me you need visual feedback to truly get a sense of progress, if anything for the psychological effect of seeing something working (other than on the command line)

Before we jump into building the visual aspects of the application we should finish configuring the restful_authentication system we installed in installment #1.

Configuring Restful Authentication

The first step is to protect all controllers in our application by applying a before filter that will trigger the authentication logic. In Rails the ApplicationController is the ideal place to do so since is the default parent class for all controllers in an application:

class ApplicationController < ActionController::Base
  include AuthenticatedSystem

  session :session_key => '_tempo_session_id'

  before_filter :login_required
end

In the file application.rb we include the AuthenticatedSystem and apply the before filter :login_required which will cascade down to any new controllers we create.

The restful_authentication plugin creates two controllers, the UsersController; a typical CRUD controller for User objects and the SessionsController which is the controller responsible the login/session management logic.

Taking a peek at the Session controller we see that it is set to skip the :login_required filter:

class SessionsController < ApplicationController

  skip_before_filter :login_required

  ...

end

In the Views/sessions directory we customize the new session view (a.k.a the login screen):

<div class="login">
  <div id="Dialog">
    <h1>Welcome to Tempo, please log in</h1>
    <dl>
      <% form_tag session_path do -%>
        <dt>Username:</dt>
        <dd><%= text_field_tag 'login' %></dd>

        <dt>Password:</dt>
        <dd>
          <%= password_field_tag 'password' %>
          <span>(<a href="/forgot_password">I forgot my password/username</a>)</span>
        </dd>

        <dd><%= check_box_tag 'remember_me' %>Remember me on this computer</dd>

        <dd><%= submit_tag 'Log in' %></dd>
      <% end -%>
    </dl>
  </div>
</div>

Permanent Scaffolding?

To give us a jump start in the right direction I'm going to make use of a Rails scaffolding plug-in that should allow us to get some fairly decent web pages up and running in no time. There seems to be two contenders in the area of "permanent" Rails scaffolding plugins, one is the Streamlined Framework produced by the brilliant folks at Relevance the other one is ActiveScaffold created by the same team that built the now deprecated AjaxScaffold.

Both frameworks attempt to provide you with a way to generate ActiveRecord-driven scaffolding screens that are configurable and flexible enough that you theoretically won't have to later tear them down to build the "real" pages of you application. I've briefly looked at both frameworks previously and they both seem fairly even in terms of features. So just for the sake of trying something new I'm gonna use ActiveScaffold for the Tempo time tracking application.

ActiveScaffold

The ActiveScaffold website has quite a bit of documentation that should help you get started. I in particular found their "How to Approach Active Scaffold" article useful in understanding where it would be a good idea to use the framework.

To get started with ActiveScaffold in the Tempo project you'll need to first install the plug-in using Piston:

/> piston import http://activescaffold.googlecode.com/svn/tags/active_scaffold vendor/plugins/active_scaffold
Exported r633 from 'http://activescaffold.googlecode.com/svn/tags/active_scaffold' to 'vendor/plugins/active_scaffold'

Once you have installed the plug-in all that you need to do to enable the scaffolding for one of you model objects is to add a minimal amount of code to your controllers:

class UsersController < ApplicationController 
  active_scaffold :user
end

In your application layout head section you also need to include:

<%= javascript_include_tag :defaults %>
<%= active_scaffold_includes %>

That will give you the basic ActiveScaffold CRUD functionality.

RESTful Scaffolding

To make your scaffolding "RESTful" with ActiveScaffold, you need set up the resource map entry in your routes.rb file for each of your ActiveScaffold controllers:

map.resources :users, :active_scaffold => true

User Controller

ActiveScaffold will let you configure what fields are shown for each of the CRUD screens (and their formatting ...to an extent). ActiveScaffold allows you set configuration options globally for each controller in a global config block which you will place in your ApplicationController or with per-controller config blocks as we have done in the UsersController below:

class UsersController < ApplicationController 

  #skip_before_filter :login_required

  active_scaffold :user do |conf|
    conf.actions.exclude :show
    # List
    conf.list.columns = [:person, :login, :email] 
    # Create
    conf.create.columns = [:person, :login, :email, :password, :password_confirmation]
    # Update
    conf.update.columns = [:person, :login, :email, :password, :password_confirmation]
  end
end

In the code snippet above we turn off the "show" action and configure which objects fields we want to be shown in the different screens. For the "list" page we only want the to_s of Person, the login and email. In the create or update screens we also want to show the password and the password confirmation.

To test this screen you can point your browser to /users/new and create a new user. Make sure to uncomment the skip_before_filter line above since we don't have an official "registration" page yet. Once you create at least one user that you can log in with you can enable the filter again.

Other controllers will be similarly configured, but before we get there let's set up the look of the application with a set of CSS stylesheets and configuring some simple application navigation logic.

Main View Layout

To configure the main view layout for our application, under your Rails application Views/layouts directory customize the application.rhtml as shown below and pair it with a "good enough" stylesheet:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<head>
   <title>Tempo</title>
   <%= javascript_include_tag :defaults %>
   <%= active_scaffold_includes %>
   <%= stylesheet_link_tag 'tempo' %>
</head>
<body>
  <!-- ====== -->
  <!-- Header -->
  <!-- ====== -->    
  <div id="Header">
    <h3 class="global">
      <% if logged_in? -%>  
      Logged in as <%= current_user %> (<a href="/logout" title="Log-out">Log-out</a>)&nbsp;
      <% else -%>
      <a href="/login" title="Login">Log-in</a>&nbsp;
      <% end -%>
      <a href="/help" class="image"><img alt="Help" src="/images/help.png" align="absmiddle" height="16" width="32"></a>
    </h3>
    <h1>
      <img src="/images/tempo.png" title="Tracking time with Ease" alt="Tempo" border="0">
    </h1>
    <% if logged_in? -%>
    <!-- =============== -->
    <!-- Navigation Tabs -->
    <!-- =============== -->
    <div id="Tabs">
      <%= links_for_navigation %>
    </div>
    <% end -%>
  </div>
  <!-- ============== -->
  <!-- Beta Indicator -->
  <!-- ============== --> 
  <img id="beta" src="/images/beta.png" alt="">
  <div id="Wrapper">
    <div class="Container">

      <div id="DashContentFrame">
        <!-- =========== -->  
        <!-- Left Column -->
        <!-- =========== -->      
        <div class="Left">
          <div class="col">
            <% if flash[:notice] -%>
            <div class="good" id="Flash">
            <p><%= flash[:notice] %></p>
            </div>
            <% end -%>
            <% if flash[:error] -%>
            <div class="bad" id="Flash">
            <p><%= flash[:error] %></p>
            </div>
            <% end -%>  
            <%= yield %>
          </div>
          <div class="bottom">&nbsp;</div>
        </div>
        <!-- ============ --> 
        <!-- Right Column -->
        <!-- ============ --> 
        <div class="Right">
          <div class="col">            
            <h1>[[Help Title]]</h1>
            <p>[[Help Body]]</p>
          </div>
        </div>  
      </div>
    </div>
  </div>

  <!-- ====== --> 
  <!-- footer -->
  <!-- ====== -->
  <div id="footer">
    <a id="powered" href="http://integrallis.com/" title="Integrallis Software, LLC.">
      <img src="/images/powered-by-integrallis.png" title="Integrallis Software, LLC." alt="Integrallis logo" border="0">
    </a>
  </div>
</body>
</html>

Let's examine what the template provides. At the top of the page I plan to have a simple header that shows the currently logged in user and a logout link. If there is no user logged in then a login link is shown.

Further down the page (after the application logo) we will have a set of application tabs that will be generated by the utility method links_for_navigation which we will create later in the ApplicationHelper.

The body of the page will be divided into two columns, the largest of the two on the left at about 75% of the page that will contain the main elements of the application and the right column for application controls and (if I ever get to it) some interactive help.

Navigation

The navigation code is fairly simple, I'm doing this old school (read: I'm sure there are cleaner ways to do this) but we'll get to refactor this code later on. In this helper I have a method that builds a link for a given page (tagging the "current" page with a specific CSS class) and a method that returns all the links for the application.

# Methods added to this helper will be available to all templates in the application.
module ApplicationHelper
  # http://dev.rubyonrails.org/ticket/2382
  # current_page? does not respect alternative routes
  def nav_link_to(name, options, html_options)
    if current_page?(options)
      html_options.store(:class, 'current')
    end
    link_to name, options, html_options
  end

  def links_for_navigation
    tabs = []
    tabs << nav_link_to('Users', {:controller => 'users', :action => 'list'}, {:title => 'Manage Users', :style => 'float: right; margin-right: 30px;'})
    tabs << nav_link_to('Projects', {:controller => 'projects', :action => 'list'}, {:title => 'Manage Projects', :style => 'float: right;'})
    tabs << nav_link_to('Activities', {:controller => 'activities', :action => 'list'}, {:title => 'Manage Activities', :style => 'float: right;'})
    tabs << nav_link_to('Time Entries', {:controller => 'time_entries', :action => 'list'}, {:title => 'Manage Time Entries', :style => 'float: right;'})
    tabs << nav_link_to('Dashboard', {:controller => 'dashboard', :action => 'index'}, {:title => 'Dashboard', :style => 'float: left;'})

    html = '<ul id="MainTabs">'
    tabs.each do |tab|
      html += '<li>'
      html += tab
      html += '</li>'
    end
    html += '</ul>'
  end

end

In the links_for_navigation method I construct an HTML unordered list and add the links for the application, notice that I'm styling the links/tabs to show right justified except for the tab that will become the entry point of the application. The "dashboard" which will be left justified.

Stylesheet

Instead of boring you with the CSS, here is a screenshot of the original HTML template styled by my CSS and a link to download the CSS and graphics if you are interested.

tempo website template

I've zipped up the tempo specific files in the public directory which you can download here

Finishing the controllers with ActiveScaffold

Now that we have all the elements in place let's tackle the configuration of each of the controllers. Along the way we'll make enhancements to the domain objects.

If you haven't done so already, navigate to /users/new on you application and create a new user (remember to disable the login filter)

creating a new user

Activities Controller

The Activity model object is extremely simple, having only two string fields; name and description. To correctly display the object instances in other ActiveScaffold screens (in drop-downs) we can provide an implementation of the to_s method as shown below:

class Activity < ActiveRecord::Base
  def to_s
    self.name
  end
end

The controller code is minimal:

class ActivitiesController < ApplicationController
  active_scaffold :activity do |conf|
    conf.actions.exclude :show
    # List
    conf.list.columns = [:name, :description]
    # Create
    conf.create.columns = [:name, :description]
    # Update
    conf.update.columns = [:name, :description]
  end
end

activities list

Project Controller

As we did with Activities add the following to_s method to the Project model:

  def to_s
    self.name
  end

The controller again is very simple to configure:

class ProjectsController < ApplicationController
  active_scaffold :project do |conf|
    conf.actions.exclude :show

    # List
    conf.list.columns = [:name, :description]
    # Create
    conf.create.columns = [:name, :description, :projects_activity, :projects_users]
    # Update
    conf.update.columns = [:name, :description, :projects_activity, :projects_users]
  end
end

projects list

TimeEntries

The TimeEntries controller requires the most configuration. For the TimeEntry create and update screens we want to have the user, project, and project_activity appear as drop-down select elements. To accomplish this ActiveScaffold provides the form_ui element of the column API. The :select option renders a select box or a collection of checkboxes. There are also options for :calendar, :checkbox, :country, :password, :textarea and :usa_state

class TimeEntriesController < ApplicationController
  active_scaffold :time_entry do |conf|
    conf.actions.exclude :show 
    # UI Type
    conf.columns[:project].form_ui = :select
    conf.columns[:user].form_ui = :select
    conf.columns[:projects_activity].form_ui = :select
    conf.columns[:comment].form_ui = :textarea
    # List
    conf.list.columns = [:project, :user, :date, :total_hours, :projects_activity]    
    # Create
    conf.create.columns = [:project, :user, :projects_activity, :start, :end, :hours, :comment]
    # Update
    conf.update.columns = [:project, :user, :projects_activity, :start, :end, :hours, :comment]
  end
end

projects list

In the next installment we will build the DashboardController that will provide a more stylish and user friendly interface for users to enter time into Tempo.

Posted in ,  | Tags , ,  | 3 comments | no trackbacks

Comments

  1. Avatar pablo said 3 months later:

    hi.

    thanks for this great tutorial, i finnaly managed to understand and see how to use RSPEC !

    seems that the link do public/ files is bronken, could you provide this ?

    thanks in advance.

  2. Avatar Victor Hugo said 5 months later:

    Hm... how could I configure the :password and :passwordconfirmation fields to be shown as password field, cause config.columns[:password].formui = :password doesn't work.

  3. Avatar ukesh said 7 months later:

    In the above, how to relate the models.i mean in the project, its have projectactivity and projectusers.

Trackbacks

Use the following link to trackback from your own site:
http://www.integrallis.com/ourblogs/trackbacks?article_id=building-tempo-with-rails-part-iv&day=30&month=11&year=2007

(leave url/email »)

   Comment Markup Help Preview comment