Simplifying and sharing code with Rails conventions
January 7th, 2008 by TrevorOne of the benefits of convention over configuration in Rails is that you can make assumptions about a lot of stuff. A good example of this is the user_id column found in most Rails database tables.
If you see a user_id column in a schema, you can typically infer that it represents the unique identifier (id) of the user that created that record. This leads to all kinds of interesting opportunities for code simplification.
I'll take you through a basic example found in my open-source app, El Dorado to demonstrate. This example will show you how to share code related to authentication throughout your app. We'll start by looking at the user, topic, and post database objects defined in schema.rb:
create_table "users", :force => true do |t| t.string "login" t.boolean "admin", :default => false # ... end create_table "topics", :force => true do |t| t.integer "user_id" t.string "title" # ... end create_table "posts", :force => true do |t| t.integer "user_id" t.text "body" # ... end
According to Rails conventions, the user_id field in the topics and posts tables should reference the id field in the users table. (The id field is assumed and not mentioned in the schema.rb file.) This convention is what allows you to do things like:
@user = User.find(1) @user.topics # the topics the user has created @user.posts # the posts the user has created
This also lets you share code amongst all of the controllers and views in an application. Let's use a basic concept for authentication: checking for permission to edit an item. Using some of the conventions introduced in the restful_authentication plugin, we'll be able to add some basic permission checking that will work for any model that has a user_id field, and for users that have an id field. We'll assume that a user has permission to edit an item if (a) they're an administrator, or (b) they've created the item they're trying to edit.
We'll start by adding the following to application.rb:
class ApplicationController < ActionController::Base helper_method :current_user, :logged_in?, :admin?, :can_edit? def current_user @current_user ||= ((session[:user_id] && User.find_by_id(session[:user_id])) || 0) end def logged_in?() current_user != 0 end def admin?() logged_in? && (current_user.admin == true) end def can_edit?(current_item) return false unless logged_in? if request.path_parameters['controller'] == "users" return current_user.admin? || (current_user == current_item) else return current_user.admin? || (current_user.id == current_item.user_id) end end def can_edit redirect_to root_path and return false unless logged_in? klass = request.path_parameters['controller'].singularize.classify.constantize @item = klass.find(params[:id]) if request.path_parameters['controller'] == "users" redirect_to root_path and return false unless admin? || (current_user == @item) else redirect_to root_path and return false unless admin? || (current_user == @item.user) end end # ... end
The current_user bit assumes that you're setting the session[:user_id] value to the currently logged-in user's id. (If you're unsure how to accomplish this and/or aren't doing it already, you should give the restful_authentication plugin a gander.) The logged_in? helper will return true if the user is logged in, and the admin? helper will return true if the logged-in user has their admin attribute set to true.
The can_edit? helper and the can_edit action are where the real action is. We'll start with the can_edit action, which allows you to do the following in any controller in your application:
class TopicsController < ApplicationController before_filter :can_edit, :only => [:edit, :update, :destroy] end # ... class PostsController < ApplicationController before_filter :can_edit, :only => [:edit, :update, :destroy] end # ... class UsersController < ApplicationController before_filter :can_edit, :only => [:edit, :update, :destroy] end
The can_edit action is accessible to any controller, and it can be used to check the editing permissions for any item without any additional code overhead. If the user is logged in and is an administrator, they'll be allowed to proceed. Otherwise, the action will check to see if the user is trying to edit an item that they created, or their own user account. If so, they'll be allowed to proceed; if not, they'll be redirected to the root_path. This, of course, requires the defining of a root_path in your routes.rb file:
map.root :controller => 'home'
That's pretty nice, and there's also the can_edit? helper that can also be shared across all views. Here's a couple of examples. The first would be useful in, say, the user/show view. The second, perhaps in posts/show...?
<%= link_to 'Edit User', edit_user_path(@user) if can_edit?(@user) %> <%= link_to 'Edit Post', edit_post_path(@post) if can_edit?(@post) %>
And there you have a great example of the benefits of convention in Rails app. You can do a lot of cool stuff with very little code. Plus, DRY code is always nice. Of course, you can dig further into the whole kit and kaboodle behind this by checking out the El Dorado source...

Comments
No comments yet.