In the past two lessons we’ve worked beyond the confines of scaffolding to enhance our blog and to create a user login system.
We build the login system to secure the creation and editing of blog posts. Before our login system is complete, it should be enhanced so that Rails ‘remembers’ who is currently logged in.
We will need to learn about two new Rails technologies to accomplish these goals:
Upon completion of this tutorial, you should be able to:
Our current login system:
In the context of our blog, we would like our login system to:
We would like our login system to protect the following actions:
Login Controller
show_alldeletePosts Controller
newcreateeditupdatedestroyWe don’t want to re-enter our password every time we access one of these protected actions. We need some way for our application to ‘remember’ if we’ve already logged in.
HTTP, the protocol used for communication between web-servers and web-browsers is stateless. By this I mean that a web-server does not retain information about the user/browser between requests.
In the context of Rails this means that variables cannot retain their values between requests. All variables loose scope after a view is rendered.
Sessions were created to allow for data persistence across browser requests. In Rails, a session is a hash-like structure that allows a web-server to store and then restore objects across requests. Rails associates these persisted objects with specific users/browsers.
Add the following action to your login controller:
def remember_me
session[:count] = session[:count].nil? ? 0 : session[:count] + 1
render :text => "Visit Count: #{session[:count]}"
end
Watch what happens when you refresh this action a few times:
http://127.0.0.1:3000/login/remember_me
Now, load up the same URL in a different browser* and you’ll notice that each browser has an independent count.
Remember that the ternary operator in line 2 is a short way of saying:
if session[:count].nil?
# If this is our first visit initialize :count as zero.
session[:count] = 0
else
# Increment the count for subsequent visits.
session[:count] += 1
end
When we render :text no view is loaded, instead the :text String value is sent to the browser.
* By different browser I don’t mean simply a new tab or browser-window. If you normally use Firefox, try IE, or vis-versa.
We can use sessions to remember validated users.
When the validate_user action successfully validates a username/password we can save the user’s id in the session hash. We can then redirect the user to the posts controller.
session[:user_id] = @user.id
redirect_to(:controller => 'posts')
The complete validate_user action:
def validate_user
@user = User.find_encrypted(params)
if @user.nil?
flash[:error] = 'Username / Password incorrect'
redirect_to(:action => 'index')
else
flash[:notice] = 'Login Successful'
session[:user_id] = @user.id
redirect_to(:controller => 'posts')
end
end
We would also like a way for our users to logout. Let’s add a logout action to the login controller:
def logout
session[:user_id] = nil
flash[:notice] = 'You have logged out.'
redirect_to(:controller => :posts)
end
Notice that we log the user out by setting the :user_id in the session hash to nil.
The reason we integrated our login system with our blog was so that we could secure the creation and editing of posts. To do this we need to check if a user is logged in before allowing certain actions to execute.
Rails includes a filter system that allows you to write code that your controller will run before or after a specific action or group of actions. Not only can we use a filter to secure our posts in our posts controller, we can also secure the listing and deletion of user accounts in our login controller.
The bit of code that we wish to run before our protected actions should:
session hash.users table.If no matching id is found (or no id is stored in the session hash) the user should be redirected to the index action of the login controller. Before we redirect we should set a notice in the flash hash.
Because this authentication code will be protecting actions in both the login and posts controller, we will add it to the Application controller. This way our method will be inherited by both our controllers.
Added to app\controllers\application.rb:
protected
def authenticate
if session[:user_id].nil? || User.find(session[:user_id]).nil?
flash[:notice] = "Please log in"
redirect_to :controller => :login # Defaults to the index action.
end
end
We add this code to the Application controller as a protected method because it should not be a publicly available action. It should only ever be called internally be a Rails filter.
Let’s ensure that this authenticate method is only run before the show_all and delete actions of our login controller. Add the follow to the beginning of the login controller:
before_filter :authenticate, :only => [:show_all, :delete]
For our posts controller we want the authenticate method to be run before all actions except index and show. Add the follow to the beginning of the posts controller:
before_filter :authenticate, :except => [:index, :show]
The first parameter we pass to before_filter is the name of the method to be run by the filter. The second parameter specifies a key-value pair, where the value is either a single action symbol or an array of action symbols. When the key is :only we are specifying only those actions before which to run our filter. When the key is :except we have configured our filter to run before all actions except those we list.
Let’s add a link from the posts controller’s index view to the show_all action of the login controller:
<%= link_to 'Admin', :controller => 'login', :action => 'show_all' %>
We’ll also add a ‘Return to Blog’ link and a ‘Logout’ link to the login controller’s show_all view:
<%= link_to 'Return to Blog', :controller => 'posts' %><br />
<%= link_to 'Logout', :action => 'logout' %>
THE REST OF THESE NOTES HAVE YET TO BE UPDATED FOR RAILS 3.
THE MATERIAL THAT FOLLOWS IS NOT EXAMINABLE.
The last thing I’d like to add to our blog is the ability for our blog readers to leave comments for individual blog posts.
The comments will be stored in a new comments table. The posts table will have a one-to-many relationship with this comments table.
Sqlite doesn’t yet properly handle foreign key relationships, so we will be using Rails to define and maintain our table relationships.
Let’s use scaffolding to quickly setup our comment model and comments controller:
ruby script/generate scaffold comment post_id:integer commenter:string body:text
rake db:migrate
The post_id integer will be used to associate each comment with a specific post.
We can define our one-to-many relationship by calling the has_many and belongs_to methods from within our two models.
A Post may have many comments. Added to the Post model:
has_many :comments
Each comment must belong to a specific post. Added to the Comment model:
belongs_to :post
It’s also important that we edit our config/routes.db file to remove map.resource :comments and modify the posts resource mapping like so:
map.resources :posts, :has_many => :comments
This will change the structure of the URLs that map to actions in the comments controller.
For this tutorial we will only be using the create action of the comments controller. Normally, for a scaffolded resource we would execute the create action by POSTing to:
http://127.0.0.1/comments/
Now that we have nested the comments resource using :has_many in the routes.rb file, we can execute the create action by POSTing to:
http://127.0.0.1/posts/#/comments/
Where # is replaced by a post id. This post id specifies which post our newly created comment will belong to.
Let’s create a comment partial (_comment.html.erb) in the posts view folder:
<p>
<strong>Posted <%= time_ago_in_words(comment.created_at) %> ago
by <%=h comment.commenter %>.</strong>
<br/>
<%=h comment.body %>
</p>
We’ll render this partial at the bottom of the show view for posts:
<h3>Comments</h3>
<% if @post.comments.size > 0 %>
<%= render :partial => 'comment', :collection => @post.comments %>
<% else %>
<p>No comments yet.</p>
<% end %>
Notice that we didn’t need to update our show action to load the comments. The comments are directly accessible via a @posts.comments array because of the one-to-many relationship we defined in the models.
Note that we are not using any of the scaffold generated views for the comments controller. If you wish, you can delete the entire comments folder within the views folder.
We should also add a form for posting comments to the bottom of the show view:
<% form_for [@post, Comment.new] do |f| %>
<p>
<%= f.label :commenter, "Your Name" %><br />
<%= f.text_field :commenter %>
</p>
<p>
<%= f.label :body, "Your Comment" %><br />
<%= f.text_area :body %>
</p>
<p>
<%= f.submit "Add Comment" %>
</p>
<% end %>
Note that because of the nested post/comment relationship we pass an array to form_for specifying that this form will be used to create a new comment (Comment.new) which is associated with the current @post.
Finally, let’s remove all the scaffold generated actions from the comments controller and add a create action of our own:
def create
@post = Post.find(params[:post_id])
@comment = @post.comments.create!(params[:comment])
redirect_to @post
end
The params hash contains the post_id of the post we which this comment to belong to. We didn’t need to explicitly add this post id to our form, it was included because the form_for used to generate the form was passed the @post object. (See the previous slide.)
In order to create a new comment, we must first search the Post model for this particular post. We can then call the @post.comment.create! method passing in params[:comment] which contains:
params[:comment][:commenter]params[:comment][:body]In the index view for posts lets change the link to the show action so that instead of display the date/time the post was create it shows the number of comments:
<%= link_to "Comment (#{post.comments.size})", post %>
We are now done. :)