Sterling Rose Design Blog

Using AJAX to Change a User's Role

2 Comments
Tags: Rails AJAX
Scenario: Client wants to display a list of users, and the role each user currently has. The user’s role should be the selected option in a select box of all roles. Administrator should be able to update the user’s role by simply selecting a new role from the select box for that user, without having to reload the page.

Approach: Each row in the user’s list should have an id that uniquely identifies it as being associated with that particular user. Each select box should also have an id field associated with the user. Choosing a new role should trigger the onchange event, which executes the update on the role and displays the new user’s row in place, without reloading the entire user list.

Code:

## app/views/users/index.html.erb

 <table>
   <% @users.each do |u| %>
     <tr id='user_row_<%= "#{u.id}" %>'>
       <td><%= link_to "#{u.first_name} #{u.last_name}", user_path(u) %></td>
       <td><%= u.username %></td>
       <td><%= u.email %></td>
       <td><%= select_tag "user#{u.id.to_s}", options_for_user_roles(u), :onchange => 
         remote_function(:url => {:controller => "permissions", :action => 
         "update_one_row"}, :with => "'user_id=' + #{u.id} + '&role_id=' + 
         $('user#{u.id}').value") %></td>
       <td><%= link_to(image_tag('/images/edit.png'), edit_user_path(u)) %></td>
     </tr>
   <% end %>
 </table>


## app/controllers/users_controller.rb
def index
  @users = User.all
end


## app/controllers/permissions_controller.rb
def update_one_row
  @user = User.find(params[:user_id])
  @company = @user.company
  @role = Role.find(params[:role_id])
  unless @user.has_role?(@role.rolename)
    @user.roles.clear
    @user.roles << @role
  end
end


## app/views/permissions/update_one_row.js.erb
$("user_row_<%=@user.id%>").replace(<%=js render(:partial => 
  "/users/update_one_row") %>);


## app/views/users/_update_one_row.html.erb
<tr <%= "id='user_row_#{@user.id}'" %>>
  <td><%= link_to "#{@user.first_name} #{@user.last_name}", user_path(@user) %></td>
  <td><%= @user.username %></td>
  <td><%= @user.email %></td>
  <td><%= select_tag "user#{@user.id.to_s}", options_for_user_roles(@user), :onchange => 
    remote_function(:url => {:controller => "permissions", :action => "update_one_row"}, 
    :with => "'user_id=' + #{@user.id} + '&role_id=' + $('user{@user.id}').value") %></td>
  <td><%= link_to(image_tag('/images/edit.png'), edit_user_path(@user)) %></td>
</tr>


## app/helpers/permissions_helper.rb
def options_for_user_roles(user)
  ## exclude "administrator" from the roles you can
  ## assign to a user
  @roles = Role.find(:all) - [Role.find_by_rolename('administrator')]
  options = ""
  @roles.each do |role|
    if user.roles.include?(role)
      options += "<option id='#{role.id}' selected='selected' 
        value='#{role.id}'>#{role.rolename}</option>"
    else
      options += "<option id='#{role.id}' value='#{role.id}'>#{role.rolename}</option>"
    end
  end
  options
end


Additional Notes: In the first code snippet above (the index view) you want a fully-closed table tag. Textile isn’t playing nice with that particular tag at the moment, so – apologies. :) Update: All fixed now. :)
Comments

Nice post Dana. Thanks for sharing.

A small refactoring I would suggest is that you move the logic for getting the roles (in permissions_helper.rb) into a named_scope on the Role model. Or you can set up a default_scope on Role to exclude the admin role so you can use Role.all and not worry about the admin.

It just occurred to me but you may have already thought of my suggestions but went for brevity in the post. In that case please feel free to ignore my comment.


Copyright 2007-2012, Sterling Rose Design. All rights reserved.