user3009646
user3009646

Reputation: 51

Rolify remove_role deletes from role table?

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

Upvotes: 5

Views: 3251

Answers (4)

Srikanth Jeeva
Srikanth Jeeva

Reputation: 3011

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.

Upvotes: -1

Thadeu Esteves Jr.
Thadeu Esteves Jr.

Reputation: 403

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.

Upvotes: 1

Eiji
Eiji

Reputation: 440

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

Upvotes: 2

semiomant
semiomant

Reputation: 584

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.

Upvotes: 4

Related Questions