Ruby on Rails 3 Scaffolding - MVC components

Rails & Scaffolding Components

Rails Application Components

Directories created by Rails

% rails new my_app -d mysql
Rails Directory/File Description
Rakefile Configure what rake tasks are available to Rake command
config.ru Rake server configuration file
Gemfile Specify the gems needed in your Rail application
app Home directory for the Rails application code
app/controllers/application_controller.rb Application level controller
app/helpers/application_helper.rb Application helper file
app/mailers Home directory for mailers
app/models Home directory for model objects
app/views/layouts/application.html.erb Application layout
config Application configuration directory
config/routes.rb Application routing information
config/application.rb Rails application configuration
config/environment.rb Called when starting the Rails application
config/environments Directory containing environment specific initialization file
config/initializers Directory containing code to initialize Rails
config/locales Localization file directory
config/boot.rb Initialize the Rails environment when the application starts
config/database.yml DB configuration
db DB schema, migration scripts
db/seeds.rb Seed the DB with data
doc Home directory for application doc
lib Extended Rails library
log Log directory
public Home directory for static content
public/404.html ... 404 Page ...
public/index.html Default home page
public/robots.txt Crawl file
public/images image directory
public/stylesheets style sheet
public/javascripts JS directory
script Rails script to start the application and other script to deploy or run your application
test Directory holding testing fixtures, unit testing and testings on different environments
tmp tmp file location for sessions, cookies & cache etc
vendor Third party code including Ruby gmes
  • rake is a build system and constantly used in Rails to build and change DB schema
  • Rack provides an interface for developing web applications on a Rack based server

Rails Scaffolding Command

rails generate scaffold Account user_name:string description:text premium:boolean \
          income:integer ranking:float fee:decimal birthday:date login_time:time
Rails File Description
db/migrate/20110222020736_create_accounts.rb DB migration code
app/models/account.rb Model code
test/unit/account_test.rb Unit testing code
test/fixtures/accounts.yml Text fixture
app/controllers/accounts_controller.rb Controller code
app/views/accounts View directory containing views rendering the model
app/views/accounts/index.html.erb View for listing all accounts
app/views/accounts/edit.html.erb View for editing an account
app/views/accounts/show.html.erb Show account detail
app/views/accounts/new.html.erb Create a new account
app/views/accounts/_form.html.erb View partial for the account form
test/functional/accounts_controller_test.rb Functional test
app/helpers/accounts_helper.rb Helper file
test/unit/helpers/accounts_helper_test.rb Testing helper file
public/stylesheets/scaffold.css Style sheet for the scaffolding

Scaffolding also make changes to

Directory File Description
config routes.rb Adding routing information for Account (Map URL to a action/method)

DB table created by Rake & Rails Migration File

A table named "accounts" is created by

rake db:migrate
mysql> describe accounts

+-------------+---------------+------+-----+---------+----------------+
| Field       | Type          | Null | Key | Default | Extra          |
+-------------+---------------+------+-----+---------+----------------+
| id          | int(11)       | NO   | PRI | NULL    | auto_increment |
| user_name   | varchar(255)  | YES  |     | NULL    |                |
| description | text          | YES  |     | NULL    |                |
| premium     | tinyint(1)    | YES  |     | NULL    |                |
| income      | int(11)       | YES  |     | NULL    |                |
| ranking     | float         | YES  |     | NULL    |                |
| fee         | decimal(10,0) | YES  |     | NULL    |                |
| birthday    | date          | YES  |     | NULL    |                |
| login_time  | time          | YES  |     | NULL    |                |
| created_at  | datetime      | YES  |     | NULL    |                |
| updated_at  | datetime      | YES  |     | NULL    |                |
+-------------+---------------+------+-----+---------+----------------+
  • By default, a primary key (id) is created
  • created_at and updated_at are created by default in tracking when changes are made

Rails Code Walk Through Created by Scaffolding

Rails Route: URL Mapping

config/routes.rb
  resources :accounts

The scaffolding command add a routing rule (:accounts) to routes.rb automatically

"resources" maps a URL pattern to a controller's action (method) based on

  • Http Method type and
  • URL pattern

To list all the URL mappings

% rake routes
            accounts GET    /accounts(.:format)                               {:controller=>"accounts", :action=>"index"}
                     POST   /accounts(.:format)                               {:controller=>"accounts", :action=>"create"}
         new_account GET    /accounts/new(.:format)                           {:controller=>"accounts", :action=>"new"}
        edit_account GET    /accounts/:id/edit(.:format)                      {:controller=>"accounts", :action=>"edit"}
             account GET    /accounts/:id(.:format)                           {:controller=>"accounts", :action=>"show"}
                     PUT    /accounts/:id(.:format)                           {:controller=>"accounts", :action=>"update"}
                     DELETE /accounts/:id(.:format)                           {:controller=>"accounts", :action=>"destroy"}
  • Output meaning: View helper method name, HTTP method, URL, Controller/Action name
  • HTTP GET for URL /accounts is mapped to index action in the Accounts controller

By adding an extension to a URL, Rails invokes different view handler to output data in different format

For example, to request XML output instead of HTML

http://localhost:3000/accounts.xml
http://localhost:3000/accounts/1.xml

Without any extension, Rails output HTML data

Rails Controller Class (Accounts)

Controller class contains actions (Ruby's methods) to handle a Http request

apps/controllers/accounts_controller.rb
class AccountsController < ApplicationController
...
end

To list all Records (DB rows) in "accounts"

Use the method "index"

  # Handling the following HTTP request
  # GET /accounts
  # GET /accounts.xml
  def index
    # Account.all retrieve all the rows of the DB table "accounts"
    # Assign it to @accounts to be displayed in a view
    @accounts = Account.all

    # "respond_to" handles what output format will be generated
    respond_to do |format|
      # Generate HTML code if HTML output is requested (default)
      format.html # index.html.erb

      # Generate XML data if the URL ends with .xml: /accounts.xml)
      # If HTML is requested, it will not generate any data
      format.xml  { render :xml => @accounts }
    end
  end

For XML response

  • format.xml call Rails' built-in "render"
  • "render :xml" renders the "@accounts" into XML data
    <accounts type="array">
      <account>
        <birthday type="date">2004-01-02</birthday>
        <created-at type="datetime">2009-10-02T18:56:57Z</created-at>
        <description>Study in SF High</description>
        <fee type="integer" nil="true"></fee>
        <id type="integer">2</id>
        <income type="integer">500</income>
        <login-time type="datetime">2000-01-01T18:55:00Z</login-time>
        <premium type="boolean">true</premium>
        <ranking type="float">3.67</ranking>
        <updated-at type="datetime">2009-10-02T18:56:57Z</updated-at>
        <user-name>John</user-name>
      </account>
      <account>
        <birthday type="date" nil="true"></birthday>
        ...
        <user-name>john</user-name>
      </account>
    </accounts>
    

For HTML response

By default, format.html invokes a view with the same name as action: action.html.erb (index.html.erb)

format.html # index.html.erb

"# index.html.erb" comment generated by Rails is only for developer reference and not used by Rails

app/views/accounts/index.html.erb
<h1>Listing accounts</h1>

<table>
   ...

<!-- Read @accounts stored by the Controller's action -->
<% @accounts.each do |account| %>
  <tr>
    <td><%= account.user_name %></td>
    <td><%= account.description %></td>
    ...
<%= account.user_name %></td>

To View a Single Record (single DB row)

The action "show"

  # GET /accounts/1
  # GET /accounts/1.xml
  def show
    # Find the Account with the ID (:id) retrieve from the URL /accounts/1
    @account = Account.find(params[:id])

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

The view

app/views/accounts/show.html.erb
<p>
  <b>User name:</b>
  <%= @account.user_name %>
</p>

<p>
  <b>Description:</b>
  <%= @account.description %>
</p>

...

Allow User to Create New Data

The action "new"

  # GET /accounts/new
  # GET /accounts/new.xml
  def new
    @account = Account.new

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

The view

app/views/accounts/new.html.erb
<h1>New account</h1>

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

  <p>
    <%= f.label :user_name %><br />
    <%= f.text_field :user_name %>
  </p>
  <p>
    <%= f.label :description %><br />
    <%= f.text_area :description %>
  </p>
  <p>
    <%= f.label :premium %><br />
    <%= f.check_box :premium %>
  </p>
  ...
  <p>
    <%= f.label :birthday %><br />
    <%= f.date_select :birthday %>
  </p>
  <p>
    <%= f.label :login_time %><br />
    <%= f.datetime_select :login_time %>
  </p>
  <p>
    <%= f.submit 'Create' %>
  </p>
<% end %>

View code

<% form_for(@account) do |f| %>

Generate

<form action="/accounts" class="new_account" id="new_account" method="post">

  <!-- A Rails security feature to prevent cross-site request forgery ->
  <input name="authenticity_token" type="hidden" value="rsvBgPtLxkjIS9lzm4sSxUnNPCTY+Nkw/b4o/J/Mr64=" />

  ...

  <input id="account_submit" name="commit" type="submit" value="Create" />

</form>

When the form is submitted, it sends a HTTP Post to "/accounts"



View Code

    <%= f.label :user_name %><br />
    <%= f.text_field :user_name %>

Generate

    <label for="account_user_name">User name</label><br />
    <input id="account_user_name" name="account[user_name]" size="30" type="text" />

Create a New Record (New DB row) by Rails

The action "create" handles HTTP Post to "/accounts"

  # POST /accounts
  # POST /accounts.xml
  def create
    # Populate account with the form post data
    @account = Account.new(params[:account])

    respond_to do |format|
      if @account.save
        # Account is save successfully
        # Save a message to be displayed by the redirect
        flash[:notice] = 'Account was successfully created.'

        # Redirect the browser to /accounts/4/
        format.html { redirect_to(@account) }

        format.xml  { render :xml => @account, :status => :created, :location => @account }
      else
        # Fail to insert the data to the DB
        # Use the view "new.html.erb" instead to generate the HTML data
        format.html { render "new" }

        format.xml  { render :xml => @account.errors, :status => :unprocessable_entity }
      end
    end
  end

Action "create" redirect the browser to another URL if data is saved or use the view of another action if failed. Hence "create" does not need a view file

Allow a User to Edit Data

The action "edit"

  # GET /accounts/1/edit
  def edit
    # Find Account with ID retrieved from the URL /accounts/1/edit
    @account = Account.find(params[:id])
  end

The view

app/views/accounts/edit.html.erb
<h1>Editing account</h1>

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

  <p>
    <%= f.label :user_name %><br />
    <%= f.text_field :user_name %>
  </p>
  ...
<% end %>

View code

<% form_for(@account) do |f| %>

Generate

<form action="/accounts/1" class="edit_account" id="edit_account_1" method="post">

  <!-- Since browser may not support HTTP PUT -->
  <!-- Rails reads the hidden field "_method" (if presence) to override what is actually used -->
  <input name="_method" type="hidden" value="put" />
  <input name="authenticity_token" type="hidden" value="rsvBgPtLxkjIS9lzm4sSxUnNPCTY+Nkw/b4o/J/Mr64=" />
  ...
  <input id="account_submit" name="commit" type="submit" value="Update" />
</form>

When the form is submitted, Rails treats it as a HTTP PUT to "/accounts/1"

Update a Record (DB row) by Rails

The action "update" handles HTTP PUT to "/accounts/1"

  # PUT /accounts/1
  # PUT /accounts/1.xml
  def update
    # Find a Account by ID retrieved from the URL /accounts/1
    @account = Account.find(params[:id])

    respond_to do |format|
      if @account.update_attributes(params[:account])
        # Update the row successfully

        # Save a message to be displayed in the layout file
        flash[:notice] = 'Account was successfully updated.'

        # redirect the browser to /accounts/1
        format.html { redirect_to(@account) }
        format.xml  { head :ok }
      else
        # Fail to update
        # Use the view "edit.html.erb" to render the HTML data
        format.html { render "edit" }
        format.xml  { render :xml => @account.errors, :status => :unprocessable_entity }
      end
    end
  end

Delete a Row

The action "delete"

  # DELETE /accounts/1
  # DELETE /accounts/1.xml
  def destroy
    @account = Account.find(params[:id])
    # Delete the row
    @account.destroy

    respond_to do |format|
      # redirect the browser to /accounts
      format.html { redirect_to(accounts_url) }
      format.xml  { head :ok }
    end
  end

Rails View Rendering

By default,

format.html
  • It renders a view with the same name as action: action.html.erb (index.html.erb)
  • If an action returns without calling "format.html", "format.format" or rendering any output, action.html.erb will be rendered implicitly by Rails

To use another view to render an action

format.html { render "edit" } # Use the "edit" view "edit.html.erb" to render the page instead

A common mis-understanding on "render" in particular with the older style of "render":

format.html { render "new" }

# Same as above (older version)
format.html { render :action=>"new" }

Both statements only invoke the view file action.html.erb, the action "new" IS NOT called

URL Links & Redirect in Ruby on Rails

Redirect in a controller

Redirect the browser to a different URL

Method Target Description
redirect_to(@account) /accounts/1 Account detail page
redirect_to(accounts_url) /accounts List all accounts

Generate URL Links with "link_to"

View code

<%= link_to 'Edit', edit_account_path(@account) %>

Generate

<a href="/accounts/1/edit">Edit</a>
link_to Map to
<%= link_to 'Back', accounts_path %> /accounts
<%= link_to 'Show', @account %> /accounts/1 (with ID stored in @account.id)
<%= link_to 'Edit', edit_account_path(@account) %> /accounts/1/edit
<%= link_to 'Edit New account', new_account_path %> /accounts/new
<%= link_to 'Destroy', @account, :confirm => 'Are you sure?', :method => :delete %> HTTP Delete on /accounts/1

Layout File

"scaffold" automatically creats a application level controller layout file

  1. Which invokes the view action.html.erb
    app/views/layouts/application.html.erb
    <!DOCTYPE html>
    <html>
    <head>
      <title>My App</title>
      <%= stylesheet_link_tag :all %>
      <%= javascript_include_tag :defaults %>
      <%= csrf_meta_tag %>
    </head>
    <body>
    
    <%= yield %>
    
    </body>
    </html
    
Controller
    respond_to do |format|
      if @account.save
        # Account is save successfully
        flash[:notice] = 'Account was successfully created.'

        ...
      else
        # Fail to insert the data to the DB
        flash[:error] = 'Fail to insert account data'

        ...
      end

Adding Business logic

Put Rails code other than controller, model, view & helper codes under

libs

By default, any code changes under libs will not be automatic refresh even in the development mode. To turn code refresh in the development mode

require_dependency 'business/campaign'