Ruby on Rails, Part 1
LUG Programming Course, 10th March 2008
This week's lesson starts us on a two part adventure building a web application using Ruby and the Ruby on Rails framework. You'll need to
install Ruby on Rails – we're using version 2.0.2. Our application also uses RMagick and SQLite. Installing RMagick is explained in this
FAQ, we're using version 2.0.0. Installing SQLite is explained in this
How-to, we're using the Ruby SQLite3 wrapper version 1.2.1. Windows users will need both the
.exe and
.dll versions of SQLite. The Ruby on Rails
Wiki page also gives helpful information for SQLite.
The lesson went quite well, and although there was a lot to cover, we overran by only 15 minutes.
The WebAlbum Application
We'll build a web application for multiple users which allows then to upload images into albums. What? It's already been done? Well I'll admit this isn't going to get
Flickr worried, but it will demonstrate many aspects of the Ruby on Rails framework.
The application will allow any visitor to register themselves as a user. Then they can create unlimited numbers of albums, each with unlimited numbers of pictures. Users can edit and modify their own albums and pictures. Logged in users see only their own albums and pictures. Anonymous visitors can see all the users, their albums and pictures.
The Real World
I admit that these specifications are not very realistic. You'd probably also want private albums and pictures, which you'd then allow a few chosen family members and friends to see. We simply won't have the time in two lessons to do that, but you're welcome to extend the application yourself.
Reality Check
Have you noticed that we always manage to specify the program in just a couple of paragraphs? You've probably already developed some mental picture of how it's going to work, you can see the list of albums, you're already clicking on them to see the list of thumbnail pictures, choosing one, and then seeing it full screen.
All that in just 30 seconds. Unfortunately, we need to convert that mental image into a program, and even Ruby on Rails can't read minds. Those 30 seconds are going to translate into at least 4 hours of work.
This is the classic client / developer impedance mismatch. You, the client, are thinking “instant”, whereas we, the developers, are thinking “half a day's work”. The other problem is that the client's mental image may also be very different from the developer's. That can lead to nasty surprises.
Since this application is going to take more than one lesson to explain, we'll use the “release early and often” open source solution. We won't be able to finish the application this week, but we'll leave the fictitious client something to “play with” at the end of the lesson. It may not look marvellous, but it'll work.
Using Rails Generators
Rails comes with a suite of code generators which prepare the way for our own efforts. The first we'll look at is the basic application generator called
rails. It creates much of the
boilerplate code, including the three 'classic' environments
test,
development and
production, and prepares the database.
As always, there is an archived file at the end of this discussion, containing all the code for the lesson. What follows here is an explanation of how we get to produce that code. Do try to build the application yourself, you'll find it helps in the learning process.
So, let's get going. Create a folder for the project (mine is /Courses/LUGPC9, but you can choose any name you like). Open a console in that folder and type:
rails WebAlbum --database=sqlite3
What you should see in the console is:
C:\Courses\LUGPC9>rails WebAlbum -–database=sqlite3
create
create app/controllers
create app/helpers
create app/models
create app/views/layouts
create config/environments
create config/initializers
create db
create doc
create lib
create lib/tasks
create log
create public/images
create public/javascripts
create public/stylesheets
create script/performance
create script/process
create test/fixtures
create test/functional
create test/integration
create test/mocks/development
create test/mocks/test
create test/unit
create vendor
create vendor/plugins
create tmp/sessions
create tmp/sockets
create tmp/cache
create tmp/pids
create Rakefile
create README
create app/controllers/application.rb
create app/helpers/application_helper.rb
create test/test_helper.rb
create config/database.yml
create config/routes.rb
create public/.htaccess
create config/initializers/inflections.rb
create config/initializers/mime_types.rb
create config/boot.rb
create config/environment.rb
create config/environments/production.rb
create config/environments/development.rb
create config/environments/test.rb
create script/about
create script/console
create script/destroy
create script/generate
create script/performance/benchmarker
create script/performance/profiler
create script/performance/request
create script/process/reaper
create script/process/spawner
create script/process/inspector
create script/runner
create script/server
create script/plugin
create public/dispatch.rb
create public/dispatch.cgi
create public/dispatch.fcgi
create public/404.html
create public/422.html
create public/500.html
create public/index.html
create public/favicon.ico
create public/robots.txt
create public/images/rails.png
create public/javascripts/prototype.js
create public/javascripts/effects.js
create public/javascripts/dragdrop.js
create public/javascripts/controls.js
create public/javascripts/application.js
create doc/README_FOR_APP
create log/server.log
create log/production.log
create log/development.log
create log/test.log
Keep calm. All those files might look frightening, but for the time being we're only interested in three of the folders; app - where we put our web application code, public – where we put our static files, and db – where the database lives.
Now make WebAlbum the current folder in your console. All further console work is going to take place exclusively inside WebAlbum.
Although we're using the latest version of Rails, it is a fast moving target. Newer versions may break our code, so we want to keep a local copy of Rails right in our application. We do that with the freeze command:
rake rails:freeze:gems
The output will look something like:
C:\Courses\LUGPC9\WebAlbum>rake rails:freeze:gems
(in C:/Courses/LUGPC9/WebAlbum)
Freezing to the gems for Rails 2.0.2
Unpacked gem: 'activesupport-2.0.2'
Unpacked gem: 'activerecord-2.0.2'
Unpacked gem: 'actionpack-2.0.2'
Unpacked gem: 'actionmailer-2.0.2'
Unpacked gem: 'activeresource-2.0.2'
Unpacked gem: 'rails-2.0.2'
Now our application will use the Rails code (in the vendor folder), and not the Rails code in our Ruby distribution. Next we'll check that the database settings are working. Type:
rake db:migrate
The output should be:
C:\Courses\LUGPC9\WebAlbum>rake db:migrate
(in C:/Courses/LUGPC9/WebAlbum)
The db folder will now contain two files:
db/
development.sqlite3
schema.rb
So far, so good. The next step is to generate the
scaffolding code for our users. We'll start off with just a couple of fields, we'll add additional fields in a moment. Type:
ruby script/generate scaffold user name:string motto:string
The output will look something like:
C:\Courses\LUGPC9\WebAlbum>ruby script/generate scaffold user name:string motto:string
exists app/models/
exists app/controllers/
exists app/helpers/
create app/views/users
exists app/views/layouts/
exists test/functional/
exists test/unit/
create app/views/users/index.html.erb
create app/views/users/show.html.erb
create app/views/users/new.html.erb
create app/views/users/edit.html.erb
create app/views/layouts/users.html.erb
create public/stylesheets/scaffold.css
dependency model
exists app/models/
exists test/unit/
exists test/fixtures/
create app/models/user.rb
create test/unit/user_test.rb
create test/fixtures/users.yml
create db/migrate
create db/migrate/001_create_users.rb
create app/controllers/users_controller.rb
create test/functional/users_controller_test.rb
create app/helpers/users_helper.rb
route map.resources :users
Again, please stay seated, the Captain has everything under control – I hope. Forget the
exists messages and look at the
create messages. Rails created a controller -
app/controllers/users_controller.rb, a model
app/models/user.rb, and four views for our user -
app/views/users/*.html.erb. The
model-view-controller pattern is used by Rails, and most other web application frameworks, to distribute the work. The model takes care of the data, the view handles displaying information, and the controller coordinates the whole thing.
Rails also created a database migrate class -
db/migrate/001_create_users.rb. This class inherits from
ActiveRecord::Migration which provides methods to change the database structure. We need a few more fields than we defined in the scaffold script, so open this file, and modify it to look like:
class CreateUsers < ActiveRecord::Migration
def self.up
create_table :users do |t|
t.string :name, :limit => 40, :null => false
t.string :motto, :limit => 80
t.string :salted_password, :limit => 40, :null => false
t.string :salt, :limit => 40, :null => false
t.timestamps
end
add_index :users, [:name], :name => :users_name_index, :unique => true
end
def self.down
remove_index :users, :name => :users_name_index
drop_table :users
end
end
I've highlighted the modifications so that you can see the changes from the original file.
ActiveRecord::Migration is Rails' way of version controlling the database. We can keep modifying the database, adding new migration files, and Rails will keep track of it all. If we make a mistake, we can usually retrace our steps by reverting to a previous version, and carry on from there. Our
CreateUsers class has two class methods (that's why they start with
self),
up adds the modifications to the database from the previous version, and
down reverts the database to the previous version. - it undoes whatever
up did, usually in reverse order.
Our
up method creates the
users database table, and a database index for the user
name. The down method removes the index and then the table, in that order. Our
users can have a
name and a
motto (something like “I am the greatest”, if you're
Mohammed Ali). I'll talk about the
salt stuff a little later.
Right now we're at version 0, so let's update the database. Type:
rake db:migrate
The output will be similar to the following:
C:\Courses\LUGPC9\WebAlbum>rake db:migrate
(in C:/Courses/LUGPC9/WebAlbum)
== 1 CreateUsers: migrating ===================================================
-- create_table(:users)
-> 0.0940s
-- add_index("users", [:name], {:name=>:users_name_index, :unique=>true})
-> 0.2500s
== 1 CreateUsers: migrated (0.3440s) ==========================================
If you want to check that you can revert to the previous version, then add the version number to the rake command:
rake db:migrate VERSION=0
The output will be similar to the following:
C:\Courses\LUGPC9\WebAlbum>rake db:migrate VERSION=0
(in C:/Courses/LUGPC9/WebAlbum)
== 1 CreateUsers: reverting ===================================================
-- remove_index(:users, {:name=>:users_name_index})
-> 0.0620s
-- drop_table(:users)
-> 0.0780s
== 1 CreateUsers: reverted (0.1400s) ==========================================
Remember to run rake again without the version number afterwards.
Now let's take a look at what we've got. Start the web application, by typing:
ruby script/server
The output will be as follows:
C:\Courses\LUGPC9\WebAlbum>ruby script/server
=> Booting WEBrick...
=> Rails application started on http://0.0.0.0:3000
=> Ctrl-C to shutdown server; call with --help for options
[2008-02-22 10:11:21] INFO WEBrick 1.3.1
[2008-02-22 10:11:21] INFO ruby 1.8.6 (2007-09-24) [i386-mswin32]
[2008-02-22 10:11:21] INFO WEBrick::HTTPServer#start: pid=3124 port=3000
Start up your favourite browser, and type http://localhost:3000/ in the location bar. When the Rails home page appears, click on the “About your application's environment” link, see below.
Now lets look at what Rails prepared for us as our users views. Add users to the location, and load the page.
Not too exciting, because we haven't actually got any users yet, however Rails provides a lot of boilerplate code for us. The next step will be to allow us to create a new user. What? You want to click on the “New user” link? Alright, but don't try to create a new user - we aren't quite ready for that.
Now shut down the server (Ctrl+C), we've got some more coding to do.
Our First User
Each of our users will have a password. We don't want to keep that password as plain text, because, well, someone might steal it. I'm not talking about stealing
a single user's password, I'm talking about the
whole database. The '
salt and shaker' process used here is probably a good enough solution to a potentially
big problem. Ranting aside, let's look at the code. This is the modified
app/models/user.rb file:
require 'digest/sha1'
class User < ActiveRecord::Base
attr_readonly :name
attr_reader :password
attr_accessor :password_confirmation
validates_presence_of :name
validates_length_of :name, :within => 1..40
validates_uniqueness_of :name
validates_length_of :motto, :within => 0..80
validates_confirmation_of :password
def password=(pword)
@password = pword.strip
return if @password.length < 6 || @password.length > 40
self.salt = Digest::SHA1.hexdigest("--#{Time.now.to_s}--#{rand.to_s}--")
self.salted_password = User.encrypted_password(self.salt, @password)
end
def validate_on_create
if @password.length < 6 || @password.length > 40
errors.add(:password, "must have between 6 and 40 characters")
end
end
def self.authenticate(name, password)
user = find(:first, :conditions => [ "name = ?", name ])
if user
if user.salted_password != encrypted_password(user.salt, password)
user = nil
end
end
user
end
private
def self.encrypted_password(salt, password)
Digest::SHA1.hexdigest("--#{salt}--#{password}--")
end
end
ActiveRecord::Base is just bristling with methods, including those found in
ActiveRecord::Validations. Errors are handled by an instance of the
ActiveRecord::Errors class, called
errors. We use
attr_readonly to make the name field a constant, because we don't want the user changing his user name.
The validates_* methods will do all the checking for us, including confirming that password is the same as password_confirmation (we'll ask for the password twice to ensure that the user doesn't make a typing mishtake). In the form we'll ask for a field called password, which we'll actually store in the database field salted_password, using the password= method. The validate_on_create method makes sure that a password is given when we attempt to create a new user. The class method authenticate is used when the user attempts to log in to our application. The private class method encrypted_password is used to calculate the, uhm, encrypted password.
Now we'll need to make some modifications to the ERB template for a new user, in the file app/views/user/new.html.erb:
<h1>New user</h1>
<%= error_messages_for :user %>
<% form_for(@user) do |f| %>
<div>
<%= f.label :name, 'Name:' %>
<%= f.text_field :name, :size =>40, :maxlength => 40 %>
</div>
<div>
<%= f.label :motto, 'Motto:' %>
<%= f.text_field :motto, :size => 40, :maxlength => 80 %>
</div>
<div>
<%= f.label :password, 'Password:' %>
<%= f.password_field :password, :size => 40, :maxlength => 40 %>
</div>
<div>
<%= f.label :password_confirmation, 'Confirm password:' %>
<%= f.password_field :password_confirmation, :size => 40, :maxlength => 40 %>
</div>
<div>
<%= f.submit "Create" %>
</div>
<% end %>
<%= link_to 'Back', users_path %>
Here we've just added the two fields for password, and password_confirmation, and added a few attributes. We've also used <div> elements instead of <p>, and replaced the <b>...</b> elements with f.label helper method calls.
Now we're ready to add a user. Fire up the server again:
ruby script/server
Open the browser and go to the location http://localhost:3000/users/new. You should now see something like this:
Now I know you're just dying to create a user, but try submitting the empty form first. You should see something like this:
Seems like most of the validation is working. Hmm, pity that Rails modified our formatting though. Now fill in the name, the motto (if you want to), the password, but not the password_confirmation:
Well, confirmation validation works as well. At this point, let's really create the user (add the password in the confirm password field):
Now ain't that grand? We've created a user, and we also get a nice green message telling us so. Now we can go back to the index page and see how that looks:
What if we want to edit the user properties? That page now looks like:
Hmm, there's work to be done here as well. Certainly the Rails generators have done much of the groundwork for us, but there is still much that can be done to improve things. Scaffolding code should be considered just that - an initial platform on which we construct our own application.
The app/views/users/show.html.erb template needs modifying so that we can have better CSS control over the positioning of the elements:
<h1>Showing user</h1>
<div class='show'>
<p class='label'>Name:</p>
<p class='value'><%=h @user.name %></p>
</div>
<div class='show'>
<p class='label'>Motto:</p>
<p class='value'><%=h @user.motto %></p>
</div>
<div class='show'>
<% if same_user? @user %>
<%= link_to 'Edit', edit_user_path(@user) %> |
<% end %>
<%= link_to 'Back', users_path %>
</div>
As a security measure, we've wrapped the link to the edit page with a test that the logged in user is the user being displayed, same_user?. This is explained in a moment.
The same applies for the app/views/users/edit.html.erb template, which needs similar modifications to those we made for the new user form:
<h1>Editing user</h1>
<% if same_user? @user %>
<%= error_messages_for :user %>
<% form_for(@user) do |f| %>
<div>
<%= f.label :name, 'Name:' %>
<%= f.text_field :name, :size => 40, :maxlength => 40,
:disabled => 'disabled', :class => 'disabled' %>
</div>
<div>
<%= f.label :motto, 'Motto:' %>
<%= f.text_field :motto, :size => 40, :maxlength => 80 %>
</div>
<div>
<%= f.submit "Update" %>
</div>
<% end %>
<% else %>
<p class='advise'>
You must be logged in to change this information.
</p>
<% end %>
<%= link_to 'Show', @user %> |
<%= link_to 'Back', users_path %>
This time we skip the entire form if it's not the same_user?, and display an advisory message. Although we display the user name as a text_field, we've also set the disabled flag, so that it can't be modified.
In the user list page, app/views/users/index.html.erb, the New user link is at the bottom of the page. As the user list grows, we'll have to scroll down to the bottom to create a new user. We can remedy this by repeating the link at the top of the page:
<h1>Listing users</h1>
<%= link_to 'New user', new_user_path %>
<p> </p>
<table>
<tr>
<th>Name</th>
<th>Motto</th>
</tr>
<% for user in @users %>
<tr>
<td><%=h user.name %></td>
<td><%=h user.motto %></td>
<td><%= link_to 'Show', user %></td>
<% if same_user? user %>
<td><%= link_to 'Edit', edit_user_path(user) %></td>
<% end %>
</tr>
<% end %>
</table>
<p> </p>
<%= link_to 'New user', new_user_path %>
We also removed the Destroy link, and wrapped the Edit link as for previous pages. Now shut down the server again (Ctrl+C).
You might be interested in seeing what's inside the database, so at the console, type:
sqlite3 db/development.sqlite3
You should see something like:
C:\Courses\LUGPC9\WebAlbum>sqlite3 db/development.sqlite3
SQLite version 3.5.6
Enter “.help” for instructions
sqlite>
First, let's see what tables we have in the database:
sqlite> .tables
which gives:
schema_info users
The schema_info table is created by Rails. It contains the current migration version number. The second table contains our users, all one of them. Let's have a look at the table contents, type:
sqlite> .header ON
sqlite> select * from schema_info;
which gives:
version
1
The
select... line is an
SQL query – actually probably one of the simplest there is. It translates to “select and get all the columns in the
schema_info table for all the rows”.
Now let's look at the users table:
sqlite> select * from users;
which gives:
id|name|motto|salted_password|salt|created_at|updated_at
1|jhl|I'm part of the Villafranca LUG team|
56352147b463894ee939eb92c5842f5cbc223a9f|df6480b74d7814c2a51375883f544ef7044d3bd4|
2008-02-29 18:39:05|2008-02-29 18:39:05
I tidied the last output up a bit because of the 80 column line wrapping. I'm not too worried that you'll be able to understand my password. Now type:
sqlite> .exit
to leave the SQLite console.
You can achieve the same results using the Rails console. Type
ruby script/console
Then when the console prompt appears, type User.find :all, the results will look like:
C:\Courses\LUGPC9\WebAlbum>ruby script/console
Loading development environment (Rails 2.0.2)
>> User.find :all
=> [#<User id: 1, name: "jhl", motto: "I'm part of the Villafranca LUG team", sa
lted_password: "56352147b463894ee939eb92c5842f5cbc223a9f", salt: "df6480b74d7814
c2a51375883f544ef7044d3bd4", created_at: "2008-02-22 18:39:05", updated_at: "200
8-02-22 18:39:05">]
>> quit
Remember to type quit to exit the console.
Pause for Thought
Before we can consider the application ready for an 'early' release, we still have the following tasks to complete:
-
We need to make the pages look better.
-
The error highlighting in the form changes the layout of the form itself, which needs correcting.
-
The user must be able to log in and out.
-
Only the specific user can edit their own information.
You've probably noticed by now that the templates we've looked at are actually only a fragment of the entire page. In app/views/layouts you'll find a template called users.html.erb. It contains the body of the HTML page, and a yield statement, which passes control to one of our templates. Rails makes great use of such 'partial' templates to reduce the amount of code, and avoid repeated code – which would be worse. We can use this knowledge to make our own 'application wide' layout, complete with header and footer, which well see in a moment.
We'd like to change the Rails standard behaviour when highlighting fields with errors, because it currently breaks the layout of our forms. Rails wraps a <div class='fieldWithErrors'> around the offending <label> and <input> elements – I checked using Firebug in Firefox. How do we find the code that does this work? Well, I didn't know either, so I did a file search for the class name fieldWithErrors, which gave back this code snippet from vendor/rails/actionpack/lib/action_view/helpers/active_record_helper.rb:
module ActionView
class Base
@@field_error_proc = Proc.new{ |html_tag, instance|
"<div class=\"fieldWithErrors\">#{html_tag}</div>" }
cattr_accessor :field_error_proc
end
...
Hmm, so if we give a different Proc object to the class variable ActionView::Base.field_error_proc then we can change the behaviour. Good.
Since we can now create users, we need to be able to log them in and out. As you may already know,
HTTP is a stateless protocol, which means that once the server has responded to a browser request, it forgets the whole deal. But now we need to keep the information about the logged in user between multiple requests and responses. Well, you'll be glad to know that it can be done – using
cookies on the browser and
sessions on the server. All clever stuff which Rails handles transparently for us. Here is the cookie for our little application:
It uses the application name, and an encrypted value, which Rails will associate on the server side with the
session object. So all we need to do is store the user (or better, the user's identifier) in the session object.
Now that we've got the groundwork out of the way, we can get back to coding.
The Master Layout Template
As a starting point for improving the web pages, we'll create a master page template. In app/views/layouts rename user.html.erb to web_album.html.erb. Now we'll modify our layout template:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="content-type" content="text/html;charset=UTF-8" />
<% title = 'WebAlbum: ' +
controller.controller_name.capitalize + ': ' +
controller.action_name.capitalize %>
<title><%= title %></title>
<%= stylesheet_link_tag 'web_album' %>
</head>
<body>
<%= render 'layouts/partials/header' %>
<div class='content'>
<p style="color: green"><%= flash[:notice] %></p>
<%= yield %>
</div>
<%= render 'layouts/partials/footer' %>
</body>
</html>
I've highlighted the changes. We calculate the title, using Rails methods, based on the controller name and action. We use the render method to, uhm, render the two partial templates, header and footer, which we'll look at next.
In app/views/layouts we'll create a /partials folder for our header and footer templates. Here is header.html.erb:
<div class='header'>
<div class='title'>WebAlbum</div>
<div class='login_form'>
<% logged_user = current_user
form_tag "/#{controller.controller_name}/#{logged_user ? 'logout' : 'login'}" do %>
<p class='login'>
<% if logged_user %>
User: <span class='user'><%= link_to logged_user.name, logged_user %></span>
<%= submit_tag 'Log out' %>
<% else %>
Name: <%= text_field_tag :name, params[:name], :size => 15, :maxlength => 40 %>
Password: <%= password_field_tag :password, params[:password], :size => 15,
:maxlength => 40 %>
<%= hidden_field_tag :url, url_for %>
<%= submit_tag 'Log in' %>
<% end %>
</p>
<p class='login_notice'><%= flash[:login_notice] %></p>
<% end %>
</div>
</div>
The lion's share of the code here handles the log in (and out) form. It uses the current_user method, explained in a moment, to check if a user is already logged in. If so, the user's name (linked to their user edit page) and the log out button is shown. If not, then the usual name/password form is shown. This form is available on every page, so we use the controller's name, and two actions; login and logout.
This is the brief footer.html.erb:
<div class='footer'>
<p>WebAlbum. Brought to you by the Villafranca LUG team.</p>
</div>
We've changed the CSS file name in the template, so we'll have to do the same in public/stylesheets. Rename scaffold.css to web_album.css. The modified CSS file contents can be found in the downloadable archive as it is a little too large to list here.
Controllers and Helpers
We still have a few minor modifications to make to our Ruby code, before we've completed the tasks for this lesson. We need to write the login and logout action methods, we need to tell the users_controller.rb which layout template to use, and we need to change the error procedure we talked about before.
Since all our controllers inherit from ApplicationController in app/controllers/application.rb, this is were we'll put our action methods:
# Filters added to this controller apply to all controllers in the application.
# Likewise, all the methods added will be available for all controllers.
class ApplicationController < ActionController::Base
helper :all # include all helpers, all the time
# See ActionController::RequestForgeryProtection for details
# Uncomment the :secret if you're not using the cookie session store
protect_from_forgery # :secret => 'de30b92a0dfd2a772c000dee4ea91d09'
def login
session[:user_id] = nil
if request.post?
user = User.authenticate(params[:name], params[:password])
if user
session[:user_id] = user.id
else
flash[:login_notice] = "Invalid name/password combination"
end
redirect_to params[:url]
else
redirect_to '/users'
end
end
def logout
session[:user_id] = nil
redirect_to '/users'
end
helper_method :find_user
def find_user(id)
User.find(:first, :conditions => [ "id = ?", id ])
end
helper_method :current_user
def current_user
@current_user ||= (session[:user_id] ? find_user(session[:user_id]) : nil)
end
helper_method :same_user?
def same_user?(user)
logged_user = current_user
return true if logged_user && logged_user.id == user.id
false
end
end
When a user attempts to log in, we check the authenticity, and if successful set the session user_id. If unsuccessful, we show an error message, and reload the current page (which was stored in the form as a hidden field). The worst case situation is if no form was posted at all, in this case we redirect to the users list page.
We've also put the current_user and same_user methods here, and marked them both as a helper_method for the view templates, rather than being an action method.
In app/controllers/users_controller.rb we'll add layout 'web_album' to the start of the class definition, so that it uses our master template, instead of the default. The file now begins like this:
class UsersController < ApplicationController
layout 'web_album'
...
One further small modification to this file is to automatically login a newly created user. This means adding a single line of code to the create method:
def create
@user = User.new(params[:user])
respond_to do |format|
if @user.save
session[:user_id] = @user.id
flash[:notice] = 'User was successfully created.'
format.html { redirect_to(@user) }
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
We want to change the error procedure for all our templates. Rails has a concept of helpers, which are loaded into all of the templates, so this is obviously the place to put our code. We also uses a same_user method to protect certain links and forms, this can also be placed in the view helpers file. So in app/helpers/application_helper.rb, we'll add the following lines:
# Methods added to this helper will be available to all templates in the application.
module ApplicationHelper
::ActionView::Base.field_error_proc = Proc.new { |html_tag, instance|
"<span class='fieldWithErrors'>#{html_tag}</span>"
}
end
Because we're inside a module, we have to reference ActionView by preceding it with the scope expression (::).
First Release
Now we're ready for our first release. Start up the server again:
ruby script/server
Open the browser and go to the location http://localhost:3000/users. You should now see something like this:
Clicking on Log in, without specifying the name and password, causes the error message to be displayed. Next we'll look at the show page (click on Show):
Now let's see what the edit page looks like. Log in as jhl with password villafranca, then click on Edit:
Next we'll check out the new user page, (click Log out, then New user):
Let's see what the error highlighting looks like now (click on Create):
Finally, creating a new user, also automatically logs in that user:
We seem to have resolved all the problems. That's as far as we're going in this lesson, so we'll give release 0.1 to the fictitious client, while we carry on coding – next week.
Source Files
All the source files for this lesson can be found in the
LUGPC9.zip archived file. Be warned, though – this is a 2 MB file. The password for both users (
jhl and
lug) is
villafranca. The source code is licensed identically to Ruby on Rails, using the
MIT license.
What's Next?
Next we'll complete our Ruby on Rails web application, by adding albums and pictures. Meanwhile, why not try out the current version of the application for yourself?
Addendum
Wrapping the
edit.html.erb template using the
same_user? method is not by any means the best way to provide security in Rails. At the time I thought it would have taken too long to explain the intricacies of the
filtering mechanism. I have since taken the
WebAlbum application one step forward in an article I wrote on my company site, which provides such security. Feel free to
take a look.