Chapter 7: User Profile
Extending Your Details
We will now extend the information kept by the system about each user with the following items:
- Forename
- Surname
- Occupation
- Town
First, we modify the database so that to include new information. In Rails it is called a migration - we migrate to a new version of the database. And, yes, there is a generator to go on with that:
ruby script/generate migration add_profileOnce the new migration is created, edit it and insert statements for:
- adding the database columns when we migrate up, upgrading to a newer version of the database), or
- removing the database columns when we migrate down, rolling back to an older version of the database):
db/migrate/002_add_profile.rb
class AddProfile < ActiveRecord::Migration def self.up add_column :users, :forename, :string add_column :users, :surname, :string add_column :users, :occupation, :string add_column :users, :town, :string end def self.down remove_column :users, :forename remove_column :users, :surname remove_column :users, :occupation remove_column :users, :town end endFinally, don't forget to rake the application:
rake db:migrateExtending the Registration
It's pretty simple to add the new information to the Registration form. These lines will include the forename and the surname in the registration process. Notice that the occupation and the town are left behind at this stage:
app/views/user/register.html.erb
... <p> <%= f.label :forename %>: <%= f.text_field :forename %> </p> <p> <%= f.label :surname %>: <%= f.text_field :surname %> </p> ...To make it really working just update the validation in the User model, so that the forename and surname will now be obligatory:
app/models/user.rb
... validates_presence_of :forename, :surname ...Editing the Details
Now let's create a new action, edit, in the User controller, which will display a special form to edit all the user details:
app/controllers/user_controller.rb
... def edit @title = "Edit Your Details" @user = User.find(session[:user_id]) end ...The view template for this action is built around the same scheme as the Register and Login forms:
app/views/user/edit.html.erb
<h1>Edit Your Details</h1> <%= error_messages_for :user %> <% form_for :user do |f| %> <p> <%= f.label :forename %>: <%= f.text_field :forename %> </p> <p> <%= f.label :surname %>: <%= f.text_field :surname %> </p> <p> <%= f.label :e_mail, "E-Mail" %>: <%= f.text_field :e_mail %> </p> <p> <%= f.label :occupation %>: <%= f.text_field :occupation %> </p> <p> <%= f.label :town %>: <%= f.text_field :town %> </p> <p> <%= f.submit "Update" %> </p> <% end %>In fact, the similarities in the three forms (Register, Login and Edit) should be exploited to minimize repeatitions of the code (or: to keep the code DRY). This is normally done with so called partials. To keep this tutorial as clear as possible we will not introduce them here, leaving this topic to more ambitious readers.
Notice that we do not allow for editing the screen name. A facility for changing the password will be added in the next section.
The time is now to process the data posted in the form (submitted after pushing the button). The general scheme is again similar to what we have already done in other forms. The only new thing here is the function update_attributes which updates an existing object rather than creates a new one.
app/controllers/user_controller.rb
... def edit @title = "Edit Your Details" @user = User.find(session[:user_id]) if request.post? if @user.update_attributes(params[:user]) flash[:notice] = "Your details have been updated" redirect_to :action => :index end end end ...Here is what exactly happens:
- The title is set (it's really simple!)
- The currently logged user is found by its id. This user (@user variable) will also be used by the form.
- Under the condition that this is a post request:
- If the @user is successfully updated using the post params:
- The flash notice is set
- and the browser is redirected to the user hub page.
- If the @user is successfully updated using the post params:
To get more from this application you should now improve the User Hub page (the user/index action) so that to reflect new pieces of information and new functionality. Here is a useful one, however not of very good quality:
app/views/user/index.html.erb
<h1>Welcome!</h1> <p>Hello, <%= [@user.forename, @user.surname].join(" ") %>!</p> <p>You are logged as: <%= @user.screen_name %></p> <p>Your e-mail address is: <%= @user.e_mail %></p> <p>Your occupation is: <%= @user.occupation %></p> <p>Your town/city is: <%= @user.town %></p> <p></p> <p><%= link_to "Edit Your Details", :action => :edit %></p>Changing the Password
Before you go on with this section, make sure you have done all the validation code from the Chapter 4. Your User model file should look like this:
app/models/user.rb
class User < ActiveRecord::Base validates_presence_of :screen_name, :e_mail, :password validates_uniqueness_of :screen_name validates_uniqueness_of :e_mail, :case_sensitive => false validates_length_of :screen_name, :within => 6..30 validates_length_of :e_mail, :within => 6..50 validates_length_of :password, :within => 6..30 validates_confirmation_of :password validates_format_of :e_mail, :with => /^[A-Z0-9_.%-]+@([A-Z0-9_]+\.)+[A-Z]{2,4}$/i, :message => "must be a valid e-mail address" validates_presence_of :forename, :surname endIn this section a functionality of changing the password will be added. Compared with what we have done in the previous section, it is slightly more complex, as we will require two extra security measures:
- The user must provide the current password, along with the new one,
- The user must provide confirmation of the password.
Password conformation is an easy part - we have already done this. In fact, the validates_confirmation_of validator does it automatically. Let's add the following code to the edit action view template:
app/views/user/edit.html.erb
... <p> <%= f.label :password %>: <%= f.password_field :password %> </p> <p> <%= f.label :password_confirmation %>: <%= f.password_field :password_confirmation %> </p> ...It is actually working - you can modify your password under the condition you provide the correct password confirmation. This page is however highly inpractical in all these cases you do not intend to change the password. There are two problems with it:
- The way in which password is shown in the dialog is highly insecure: it may be easily revealed by simply copying and pasting it somewhere,
- The user must provide a valid password (and confirm it) - otherwise the form will not validate. The user should be allowed to leave the password box blank, and expect that this will keep the password unchanged.
We need further modifications to be done with the User controller.
Firstly (this is an easy part) we can reset both the password and its conformation before the form is displayed. We add two simple lines at the end of the action.
Secondly, we add a conditional statement that is only executed if the user left the password empty. Notice that all the data that comes with this post request is known as params. To get the data associated with the form that was used to edit the user we should use params[:user]. To get the password from that, we neeed params[:user][:password] and the method empty? will tell you if it is empty. In case it is - or, the user does not want to edit the password - we will do it instead of him. We'll just copy the real password, known as @user.password, to the params[:user][:password] (and also params[:user][:password_confirmation]).
app/controllers/user_controller.rb
... def edit @title = "Edit Your Details" @user = User.find(session[:user_id]) if request.post? if params[:user][:password].empty? # password not edited params[:user][:password] = @user.password params[:user][:password_confirmation] = @user.password end if @user.update_attributes(params[:user]) flash[:notice] = "Your details have been updated" redirect_to :action => :index end end @user.password = nil @user.password_confirmation = nil endIf that was an easy part, so what is the hard part going to be?
Let's start with adding some essential stuff to the edit view template:
app/views/user/edit.html.erb
... <p> <%= f.label :current_password %>: <%= f.password_field :current_password %> </p> <p> <%= f.label :password %>: <%= f.password_field :password %> </p> <p> <%= f.label :password_confirmation %>: <%= f.password_field :password_confirmation %> </p> ...So far so good.
The problem is that when you try to edit the data what you get is an error: undefined method `current_password'.
The problem that we encounter is that every piece of the form should be reflected by the model. We cannot just add a new field and expect it is ok, while Rails does not know what is it for!
Actually, all the time we are using a field that is not backed in the database. This is the password_conformation. However, this field (or: attribute) is automatically processed by one of our model validators.
In a more general way, it is pretty simple to add new model attributes that do not necessarily require any modification to the database structure. We only need to acknowledge the model about the presence of such an additional attribute. Add the following line to your User model:
app/models/user.rb
class User < ActiveRecord::Base attr_accessor :current_password ...Now, we can freely use the new current_password attribute to provide the additional check. Now someone who does not know the current password will not be able to change our password:
app/controllers/user_controller.rb
... def edit @title = "Edit Your Details" @user = User.find(session[:user_id]) if request.post? if params[:user][:password].empty? # password not edited params[:user][:password] = @user.password params[:user][:password_confirmation] = @user.password else # password edited if params[:user][:current_password] != @user.password @user.errors.add(:current_password, "is incorrect") return end end if @user.update_attributes(params[:user]) flash[:notice] = "Your details have been updated" redirect_to :action => :index end end @user.password = nil @user.password_confirmation = nil endThe new piece of code is conditional. The @user.errors structure is normally used by the User model validators to report errors. We do here a very similar thing.
The question why we need a return statement we'll leave to the reader.
User Profile
FakeBook is a social networking website. No such application could exist without a public User Profile page, which would show publically available information about every user of the system. This will allow people to know each other, find profiles similar to their own, make friendships, send messages etc.
We will call the new action profile. This is not very typical - usually actions are verbs - but this seems to be a good choice for this case. Assuming we can reveal everything besides the e-mail and the password, we can easily create a view template for this new action using the existing index template. As usually, let's assume that the @user variable contains all the information about the user whose profile we watch:
app/views/user/profile.html.erb
<h1>User Profile for <%= [@user.forename, @user.surname].join(" ") %></h1> <p>screen name: <%= @user.screen_name %></p> <p>e-mail: <%= @user.e_mail %></p> <p>occupation: <%= @user.occupation %></p> <p>town/city: <%= @user.town %></p>That was easy. The difficult part, now, is how to setup the @user variable?
So far we usually got something like this: @user = User.find(session[:user_id]). The problem is that this provided the information about the user who is currently logged in, and now we need any user... Any, means which? How can we specify which user profile are we going to watch?
In web applications such things are usually specified in the URL, and we will also do something like this. Let's assume that we want to view the profile of a user whose screen name is gordon. Rails offers a clear convention of naming URL's (nicer than that of PHP), which allows us to access gordon's profile at:
http://localhost:3000/user/profile/gordon
We by now know that user is the name of a controller and that profile identifies the action. But what is gordon in this example?
Let's have a quick look at the config/routes.rb file which contains all the definitions of Rails routing used by our application. We will only look at this file, not read. It contains something like this:
config/routes.rb
... map.connect ':controller/:action/:id' ...This line of code tells what the third slashed component of the URL is, or, rather, how it's called. The id has no particular meaning but we still can use it.
To intercept information sent through HTTP requests, we used a special variable called param. So far we used param[:user] to access whatever the user sent in a web form called :user. But also we can use this variable to read the data from the URL.
Now, with the URL like above, param[:controller] will simply return 'user', which is not very insightful; param[:action] will return 'profile', again, not very useful, but param[:id] will return 'gordon', which is exactly what we need.
Before trying out this code with http://localhost:3000/user/profile/gordon don't forget to register a user with 'gordon' as his screen name.
app/controllers/user_controller.rb
... def profile @screen_name = params[:id] @title = "User Profile for #{@screen_name}" @user = User.find_by_screen_name(@screen_name) end ...Finally, a better version of the profile view template - error tolerant:
app/views/user/profile.html.erb
<% if @user %> <h1>User Profile for <%= [@user.forename, @user.surname].join(" ") %></h1> <p>screen name: <%= @user.screen_name %></p> <p>occupation: <%= @user.occupation %></p> <p>town/city: <%= @user.town %></p> <% else %> <h1>No User Profile for: <%= @screen_name %></h1> <% end %>Find all people in your town
User Profiles are find, but their usability is just limited unless we have an efficient system of finding those profiles that we are interested in. In this section we will create a simple function that finds profiles of 'all people in my town', and, in fact, in any town at all. To find all the registered users who provided 'Kingston' as their town, we should use the following URL:
http://localhost:3000/user/find_by_town/Kingston
The find_by_town action will use params[:id] to identify the town. We cannot use a handy find_by_town function, because it returns just a single return, and what we need is a collection of all users living in town. In Chapter 4 we have used User.find(:all) to find all the users in the database. A quick check at the Rails Framework Reference can reveal how we can narrow the search by providing a condition.
app/controllers/user_controller.rb
... def find_by_town @town = params[:id] @title = "All people from #{@town}" @users = User.find(:all, :conditions => "town = '#{@town}'") end ...The view template is similar to the code we used in Chapter 4 to test after user registration. It simply iterates each user from the collection of found users, providing a link to his or her profile page.
app/views/user/find_by_town.html.erb
<h1>All People in <%= @town %></h1> <ul> <% @users.each do |user| %> <li><%= link_to [user.forename, user.surname].join(" "), :controller => :user, :action => :profile, :id => user.screen_name %></li> <% end %> </ul>The last thing that must be done is to provide a link from the User Home Page (or User Hub):
app/views/user/index.html.erb
<p> <%= link_to "See all people in your town", :controller => :user, :action => :find_by_town, :id => @user.town %> </p>
Copyright (C) 2009-2011 by Jaroslaw Francik

