5

This is strange. I'm using Rolify + CanCan + Devise in my rails 3.2 app. My use case is simple. I want a user to have only one role at a time, thus to change a role, I do something like this:

user.remove_role "admin"
user.add_role "associate"

The strange thing to me is that when I do this, the role "admin" gets deleted from the Roles table. Why would this be? I don't want to eliminate the role entirely, just a given role from the user. What am I doing wrong?

Here's the SQL. Notice the last delete from roles statement:

3] pry(main)> u.remove_role "sub_admin"
  Role Load (0.1ms)  SELECT "roles".* FROM "roles" INNER JOIN "users_roles" ON "roles"."id" = "users_roles"."role_id" WHERE "users_roles"."user_id" = 2 AND "roles"."name" = 'sub_admin'
   (0.0ms)  begin transaction
   (0.3ms)  DELETE FROM "users_roles" WHERE "users_roles"."user_id" = 2 AND "users_roles"."role_id" IN (2)
   (1.9ms)  commit transaction
  User Load (0.1ms)  SELECT "users".* FROM "users" INNER JOIN "users_roles" ON "users"."id" = "users_roles"."user_id" WHERE "users_roles"."role_id" = 2
   (0.0ms)  begin transaction
  SQL (2.1ms)  DELETE FROM "roles" WHERE "roles"."id" = ?  [["id", 2]]
   (0.6ms)  commit transaction
Monolo
  • 18,205
  • 17
  • 69
  • 103
user3009646
  • 51
  • 1
  • 3

4 Answers4

4

The basic problem is that each combination if role-name, resource_type, and resource_id is stored only once in the roles table. If you delete this row, it is deleted for everyone.

The solution then is to delete only the rows from the join table of rolify connecting the User and the Role models. For ease of access i will make the join table a model to use some rails magic to generate the SQL. Since this is really a kind of service object, i will make it a class singleton. Here is my hack:

class UsersRoles < ActiveRecord::Base

    def self.delete_role(subject,role_symbol, obj=nil)
        res_name = obj.nil? ? nil : obj.class.name
        res_id   = obj.id rescue nil
        role_row = subject.roles.where(name: role_symbol.to_s, resource_type: res_name , resource_id: res_id).first
        if  role_row.nil?
            raise "cannot delete nonexisting role on subject"
        end
        role_id = role_row.id
        self.delete_all(user_id: subject.id,role_id: role_id)
    end

    private_class_method :new
end

This code is not optimized, but should give you idea what to do: for example you can now add a convenience method to the User model:

def delete_role(role_symbol,target=nil)
    UsersRoles.delete_role self,role_symbol,target
end

then you can say:

user.delete_role :admin

and it will only remove what you want.

Note that this will not remove the table row with the role, which I would retain for future use.

semiomant
  • 584
  • 6
  • 12
2

I know this question is more than two years old, but I found better and more "rolify like" answer.
You can do it really easy in User model:

def remove_only_role_relation(role_name)
    roles.delete(roles.where(:name => role_name))
end

and use it like:

@user = User.find_by_id(params[:id])
role_name = params[:role_name]
# or:
# role_name = Role.find_by_id(params[:role_id]).name rescue nil
if @user and role_name
    @user.remove_only_role_relation(role_name)
end

I found similar code in their sources:
rolyfi: role.rb - here method remove_role cals method "remove" in adapter file: rolyfi: role_adapter.rb

Eiji
  • 340
  • 5
  • 14
1

This is how I resolved this issue:

models/user.rb

def delete_roles
  roles.delete(roles.where(:id => self.roles.ids))
end

user_controller.rb

def add_role
  @user.delete_roles
  role = params[:user][:roles]
  @user.add_role Role.find(role).name if not role.blank?
  @user.role_id = Role.find(role).id if not role.blank?
end

Every time a user is created or updated, delete the user's roles.

Community
  • 1
  • 1
0

Go to config/initializers/rolify.rb

uncomment this line

  config.remove_role_if_empty = false 

so the file will look like this now:

Rolify.configure do |config|
  
  # Configuration to remove roles from database once the last resource is removed. Default is: true
  config.remove_role_if_empty = false
end

By default, remove_role_if_empty is set to true and rolify will delete role if it's not assigned to any other user.

Srikanth Jeeva
  • 3,005
  • 4
  • 38
  • 58