February 15, 2011
Hacking a Rails controller and template inheritance strategy
Developing with Ruby on Rails can be fun, but sometimes, especially for newbies like me, we can get stuck somewhere. I’ll document my strategy for single model inheritance and how I did with the controllers and templates.
I have two types of users in my app, User (a common user) and ThingsAdmin (a user, but can have and administer Things). The two type of users basically share the same attributes and the same behavior, though the ThingsAdmin user can have and administer many Things.
For more about Single table inheritance, check the docs http://api.rubyonrails.org/classes/ActiveRecord/Base.html
Let me show you my code for the models:
class User < ActiveRecord::Base #attributes, validations, filters, methods and other stuff for User model end
And the ThingsAdmin is something like this:
class ThingsAdmin < User has_many :things #Specific attributes, validations, filters, methods and other stuff for ThingsAdmin model end
In the migration, I just needed to add a new field “type”:
def self.up change_table :users do |t| t.string :type end end
It’s not needed to create a new table for ThingsAdmin.
Having these two models defined like this, when I create a new User, the field type is set to nil but If I create a new ThingsAdmin, the field “type” is automatically set to “ThingsAdmin”, i.e., the name of the model.
I can test this in the rails console:
irb(main):008:0> user = User.new irb(main):009:0> user.type => nil irb(main):008:0> admin = ThingsAdmin.new irb(main):009:0> admin.type => ThingsAdmin
Now, I can have two different classes of users and it’s time to move to the controllers. I decided to start by mapping all things_admin resources to the controller User, something like this in my routes.rb:
resources :things_admins, :controller=>"users"
However, this does not works as expected because in my UserController the new action is something like:
class UsersController < ApplicationController def new @user = User.new end ##rest of the controller end
So, how can I distinguish between the request for a new User or request for a new ThingsAdmin? My first though was to have a parameter moving around so I can differentiate the type of user I want to create, something like:
class UsersController < ApplicationController def new @user = User.new if params[:role]=='user' @user = ThingsAdmin.new if params[:role]=='admin' end ##rest of the controller end
But this will bring some ugly things into my code, for example in the create action I would need to do something like:
class UsersController < ApplicationController def create @user = User.new(params[:user]) if params[:role]=='user' @user = ThingsAdmin.new(params[:things_admin] if params[:role]=='admin' end ##rest of the controller end
I cannot forget about moving the param “role” when doing these operations…
So, I decided to try other solution, which I thing is cleaner. I created a controller for ThingsAdmin that only contains the actions “new” and “create”:
class ThingsAdminsController < UsersController def new @user = ThingsAdmin.new render "users/new" end def create @user = ThingsAdmin.new(params[:things_admin] if params[:role]=='admin' create_user(@user) end ##rest of the controller end
And modified the UsersController to looks something like:
class UsersController < ApplicationController def new @user = User.new end def create @user = User.new(params[:things_admin] if params[:role]=='admin' create_user(@user) end protected def create_user(user) #Code to create users (User or ThingsAdmin) end ##rest of the controller end
Now, I have to tell the rails routing engine to redirect every action for business_admins to the UsersController except for “new” and “create” actions. My routes.rb look something like:
resources :users match "/things_admins/new(.:format)" => "things_admins#new", :via=>[:get], :as=>:new_things_admin match "/things_admins(.:format)" => "things_admins#create", :via=>[:post], :as=>:things_admins resources :things_admins, :controller=>"users", :except=>[:new, :create]
Now, every action concerned to a User or ThingsAdmin will be handled by the controller UsersController, except the “new” and “create” action.
There is something more I have to do, now concerning the update action. To have only one action handling the update of both User and ThingsAdmin, I just need to modify one line in the UsersController:
class UsersController < ApplicationController def update @user = User.find[params[:id]) ##This method is handling both the update of BusinessAdmins and Users.... if @user.update_attributes(params[@user.class.name.underscore]) flash[:success = "User updated" redirect_to user_path(@user) else flash[:error] = "Could not update user" render :edit end end ##rest of the controller end
Now, everything works as expected. Really, I don’t know if this is the best strategy because it’s the first one I tried. If you have a better solution, please share because I would like to know.