I have a problem with subclassing Rail’s models. Suppose you have a User model and several subclasses of it (user types) that store specific methods and associations, for example: Director, Admin, Trainee, Instructor, etc. This is just simple “single-table inheritance”. The problem is 2-fold.
-
paths/urls often crash or do weird things when you pass a subclass rather than the base class. Here’s an example:
<% if user.enabled? %> <%= link_to 'Disable', disable_user_path(user) %> <% else %> <%= link_to 'Enable', enable_user_path(user) %> <% end %>If you pass in a
Usermodel, it works just fine. But if you pass a subclass, likeAdmin, it throws this exception:No route matches {:controller=>"users", :action=>"disable", :id=>#<Admin id: 1, first_name: "Ken", ..., created_at: "2011-05-23 21:01:35", updated_at: "2011-05-23 21:04:28">}Clearly, this is not behaving correctly. How can we get rails to use the base class all the time?
-
Even more disturbing is forms (I am using
simple_form). Let’s say you have a/profileform. You want allUsersubclasses to access it equally, and you don’t want to deal with their subclasses on a special-case basis; it should be 100% generic.For some reason, if the user is an
Admin, it will post the params hash asparams[:admin]Even worse, if you view the source of the form, it actually says
user[first_name]instead ofadmin[first_name], so something is definitely screwy! The instance variable is@usertoo, so I don’t see why it should be posting toparams[:admin].
Here is the form view code for (2):
<%= simple_form_for(@user, :url => profiles_path, :method => :put, :html => {:multipart => true}) do |f| %>
<%= f.error_notification %>
<div class="form">
<fieldset>
<legend>Personal Information</legend>
<%= f.input :first_name %>
<%= f.input :last_name %>
</fieldset>
<fieldset>
<legend>Credentials</legend>
<%= f.input :email %>
</fieldset>
<fieldset>
<legend>Preferences</legend>
<%= f.input :receive_email_notifications %>
<%= f.input :receive_newsletters %>
<%= f.input :allow_private_messages %>
</fieldset>
<fieldset>
<legend>Avatar</legend>
<p>
Select an image from your computer to use as your avatar. You will be given the oppurtunity
to crop this image further after your image has been uploaded.
</p>
<%= f.input :avatar, :as => :file, :label => "Select File" %>
<%= f.hidden_field :avatar_cache %>
</fieldset>
<div class="actions">
<%= f.button :submit, :value => "Update Profile" %>
<%= link_to 'Cancel', profiles_path %>
</div>
</div>
<% end %>
Here are the controller actions to first render the view and when I submit the form:
def edit
@user = User.find(current_user.id)
end
def update
@user = User.find(current_user.id)
if @user.update_attributes(params[:user]) # <-- this bombs for Admin subclass
if params[:user][:avatar] # <-- this would bomb also.
redirect_to(crop_avatar_profiles_path)
else
redirect_to(profiles_path, :notice => 'Your profile was successfully updated.')
end
else
render :action => "edit"
end
end
def crop_avatar
@user = User.find(current_user.id)
end
def update_avatar
@user = User.find(current_user.id)
@user.crop(params[:x].to_i, params[:y].to_i, params[:h].to_i, params[:w].to_i)
redirect_to(profiles_path, :notice => 'Your profile and avatar was successfully updated.')
end
Besides thinking up some pretty inelegant solutions (especially to the forms problem), I am at a loss as to how I can fix them. There has to be a better way to deal with these situations.
I had the same problem with STI due to the dynamic casting rails did for me. I solved it using the becomes method.
In your User model:
Now whenever you don’t want a user instance to be of an specific type (Admin, Director..) but just User, you can do:
And your @user object will now be treated as a User, not Admin or Director or whatever, same goes for all_without_typecast, like this:
Maybe this methods could be run before each scope so you don’t have to rewrite every one of them.
Hope it helps!