-->

Rails 4 + Rolify Gem: User roles aren't being

2019-04-26 06:58发布

问题:

I have an edit view that allows me to update roles for a particular user. I've included the code below, however, just to give you an idea, when I uncheck the boxes on the page, my puts statements in my controller correctly picks up that the role association is false and can be seen further down in the logs.

However, later in the logs, you can see that the roles are being re-associated back to 'true' and I'm not sure why that's the case at all!

If the checkboxes are unchecked, I want those roles to be removed from the user.

users/edit.html.erb:

 <fieldset>
   <%= form_for @user, :url => {action: "update"}, :html => { :class => 'user-role' } do |f| %>

     <h1 class="h1-heading">User Roles</h1>
     <p class="user-paragraph"> Check the boxes to grant different roles to <%= @user.first_name %> <%= @user.last_name %>:</p>
       <%= f.label(:admin) do %>
         <%= hidden_field_tag(:admin, 0) %>
         <%= check_box_tag(:admin, 1, @user.has_role?(:admin)) %>
         Administrator
       <% end %>

       <%= f.label(:member) do %>
         <%= hidden_field_tag(:member, 0) %>
         <%= check_box_tag(:member, 1, @user.has_role?(:member)) %>
         Member
       <% end %>
     <%= f.submit class: 'btn btn-primary-dialog pull-right' %>  
   <% end %>
 </fieldset>

users_controller.rb:

def update
    @user = User.find(params[:id])
    @customer = current_user.customer
    @logged_in_user = User.find_by_email(current_user.email)

    if params[:admin] == "1"
      @user.grant(:admin)
    elsif params[:admin] == "0"
      @user.remove_role(:admin)
    end

    if params[:member] == "1"
      @user.grant(:member)
    elsif params[:member] == "0"
      @user.remove_role(:member)
    end

    puts "NEW ROLES"
    puts @user.has_role? :member
    puts @user.has_role? :admin

    if @user.update_attributes(params[:user])
      puts "UPDATING USER"
      puts @user.has_role? :member
      puts @user.has_role? :admin
      redirect_to '/users/show', :flash => { :alert => 'User was successfully updated.' }          
      end
    end
  end

Logs:

As you can see below, it returns 'false' and then when updating the attributes, sets the roles again to 'true' and I cannot figure out where or why that's happening!

Started PATCH "/users/51" for 127.0.0.1 at 2015-06-26 13:48:58 +1000
Processing by UsersController#update as HTML
  Parameters: {"utf8"=>"✓", "authenticity_token"=>"+qNti57HxMa2B/a+c6nS71Qp0p7hf+kTE4b5eiBI4No=", "admin"=>"0", "member"=>"0", "commit"=>"Update User", "id"=>"51"}
  User Load (0.5ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 ORDER BY `users`.`id` ASC LIMIT 1
  Customer Load (0.3ms)  SELECT `customers`.* FROM `customers` WHERE `customers`.`email` = 'ryan@ryandrake.com' LIMIT 1
  Company Load (0.2ms)  SELECT `companies`.* FROM `companies` WHERE `companies`.`domain` = 'ryandrake.com' LIMIT 1
  CustomerAccess Load (0.3ms)  SELECT `customer_accesses`.* FROM `customer_accesses` WHERE `customer_accesses`.`customer_id` = 1 LIMIT 1
  User Load (0.3ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` = 51 LIMIT 1
  CACHE (0.0ms)  SELECT `customers`.* FROM `customers` WHERE `customers`.`email` = 'ryan@ryandrake.com' LIMIT 1
  User Load (0.2ms)  SELECT `users`.* FROM `users` WHERE `users`.`email` = 'ryan@ryandrake.com' LIMIT 1
  Role Load (0.3ms)  SELECT `roles`.* FROM `roles` INNER JOIN `users_roles` ON `roles`.`id` = `users_roles`.`role_id` WHERE `users_roles`.`user_id` = 51 AND `roles`.`name` = 'admin'
   (0.1ms)  BEGIN
   (0.2ms)  DELETE FROM `users_roles` WHERE `users_roles`.`user_id` = 51 AND `users_roles`.`role_id` IN (9)
   (0.9ms)  COMMIT
   (0.3ms)  SELECT COUNT(count_column) FROM (SELECT 1 AS count_column FROM `users` INNER JOIN `users_roles` ON `users`.`id` = `users_roles`.`user_id` WHERE `users_roles`.`role_id` = 9 LIMIT 1) subquery_for_count
  Role Load (0.3ms)  SELECT `roles`.* FROM `roles` INNER JOIN `users_roles` ON `roles`.`id` = `users_roles`.`role_id` WHERE `users_roles`.`user_id` = 51 AND `roles`.`name` = 'member'
   (0.1ms)  BEGIN
   (0.2ms)  DELETE FROM `users_roles` WHERE `users_roles`.`user_id` = 51 AND `users_roles`.`role_id` IN (3)
   (0.4ms)  COMMIT
   (0.5ms)  SELECT COUNT(count_column) FROM (SELECT 1 AS count_column FROM `users` INNER JOIN `users_roles` ON `users`.`id` = `users_roles`.`user_id` WHERE `users_roles`.`role_id` = 3 LIMIT 1) subquery_for_count
NEW ROLES
  Role Load (0.7ms)  SELECT `roles`.* FROM `roles` INNER JOIN `users_roles` ON `roles`.`id` = `users_roles`.`role_id` WHERE `users_roles`.`user_id` = 51 AND (((roles.name = 'member') AND (roles.resource_type IS NULL) AND (roles.resource_id IS NULL)))
false
  Role Load (0.6ms)  SELECT `roles`.* FROM `roles` INNER JOIN `users_roles` ON `roles`.`id` = `users_roles`.`role_id` WHERE `users_roles`.`user_id` = 51 AND (((roles.name = 'admin') AND (roles.resource_type IS NULL) AND (roles.resource_id IS NULL)))
false
   (0.3ms)  BEGIN
  Customer Load (0.3ms)  SELECT `customers`.* FROM `customers` WHERE `customers`.`email` = 'ryan.drake2@otherlevels.com' LIMIT 1
  ExtraCustomerAccount Load (0.6ms)  SELECT `extra_customer_accounts`.* FROM `extra_customer_accounts` WHERE `extra_customer_accounts`.`email` = 'ryan.drake2@otherlevels.com' LIMIT 1
  Customer Load (0.3ms)  SELECT `customers`.* FROM `customers` WHERE `customers`.`id` = 1 LIMIT 1
  Company Load (0.2ms)  SELECT `companies`.* FROM `companies` WHERE `companies`.`domain` = 'otherlevels.com' LIMIT 1
  Company Load (0.2ms)  SELECT `companies`.* FROM `companies` WHERE `companies`.`name` = 'None' ORDER BY `companies`.`id` ASC LIMIT 1
  Role Load (0.3ms)  SELECT `roles`.* FROM `roles` WHERE `roles`.`name` = 'member' AND `roles`.`resource_type` IS NULL AND `roles`.`resource_id` IS NULL ORDER BY `roles`.`id` ASC LIMIT 1
  Role Exists (0.2ms)  SELECT 1 AS one FROM `roles` INNER JOIN `users_roles` ON `roles`.`id` = `users_roles`.`role_id` WHERE `users_roles`.`user_id` = 51 AND `roles`.`id` = 3 LIMIT 1
   (0.2ms)  SELECT `roles`.id FROM `roles` INNER JOIN `users_roles` ON `roles`.`id` = `users_roles`.`role_id` WHERE `users_roles`.`user_id` = 51
  Role Load (0.2ms)  SELECT `roles`.* FROM `roles` WHERE `roles`.`id` = 3 LIMIT 1
  Role Load (0.3ms)  SELECT `roles`.* FROM `roles` INNER JOIN `users_roles` ON `roles`.`id` = `users_roles`.`role_id` WHERE `users_roles`.`user_id` = 51
   (0.2ms)  INSERT INTO `users_roles` (`user_id`, `role_id`) VALUES (51, 3)
  Role Load (0.6ms)  SELECT `roles`.* FROM `roles` WHERE `roles`.`name` = 'admin' AND `roles`.`resource_type` IS NULL AND `roles`.`resource_id` IS NULL ORDER BY `roles`.`id` ASC LIMIT 1
  Role Load (0.3ms)  SELECT `roles`.* FROM `roles` WHERE `roles`.`id` IN (3, 9)
   (0.2ms)  INSERT INTO `users_roles` (`user_id`, `role_id`) VALUES (51, 9)
   (0.3ms)  COMMIT
UPDATING USER
  Role Load (0.4ms)  SELECT `roles`.* FROM `roles` INNER JOIN `users_roles` ON `roles`.`id` = `users_roles`.`role_id` WHERE `users_roles`.`user_id` = 51 AND (((roles.name = 'member') AND (roles.resource_type IS NULL) AND (roles.resource_id IS NULL)))
true
  Role Load (0.3ms)  SELECT `roles`.* FROM `roles` INNER JOIN `users_roles` ON `roles`.`id` = `users_roles`.`role_id` WHERE `users_roles`.`user_id` = 51 AND (((roles.name = 'admin') AND (roles.resource_type IS NULL) AND (roles.resource_id IS NULL)))
true
  Role Load (0.3ms)  SELECT `roles`.* FROM `roles` INNER JOIN `users_roles` ON `roles`.`id` = `users_roles`.`role_id` WHERE `users_roles`.`user_id` = 1 AND (((roles.name = 'otherlevels_admin') AND (roles.resource_type IS NULL) AND (roles.resource_id IS NULL)))
Redirected to http://localhost:3000/users/show
Completed 302 Found in 80ms (ActiveRecord: 15.1ms)

Would love any help with pointing out where it's not correctly updating the params!

回答1:

Your main problem is here:

@user.update_attributes(params[:user_id])

Edit - my original solution did not work at all.

Unlike my original solution this does not require simple form.

First lets setup our form:

<%= form_for(@user, url: {action: "update"}, html: { class: 'user-role' }) do |f| %>
  <h1 class="h1-heading">User Roles</h1>
  <p class="user-paragraph"> Check the boxes to grant different roles to <%= @user.first_name %> <%= @user.last_name %>:</p>
  <%= f.fields_for(:roles) do |r| %>
    <%= r.hidden_field :name unless r.object.persisted? %>
    <%= r.label :_keep do %>
      <%= r.check_box :_keep, checked: r.object.persisted? %>
      <%= r.object.name %>
    <% end %>
  <% end %>
<% end %>

We are going to pass some nested attributes for roles:

  • name (for new roles)
  • _keep a virtual attribute - do we save the Role
  • id (automatically inserted by rails if the role exists)

Then we modify the user class to accepts_nested_attributes_for :roles

class User < ActiveRecord::Base
  rolify
  accepts_nested_attributes_for :roles,
    allow_destroy: true,
    reject_if: ->(hash){ hash["_keep"] != "1" }
end

Note reject_if: ->(hash){ hash["_keep"] != "1" } which means that if the checkbox is unchecked we do not create a Role, and allow_destroy which will delete the role if we pass _delete=true.

We need to add the _keep virtual attribute to Role:

class Role < ActiveRecord::Base
  has_and_belongs_to_many :users, :join_table => :users_roles
  belongs_to :resource, :polymorphic => true
  validates :resource_type,
            :inclusion => { :in => Rolify.resource_types },
            :allow_nil => true
  scopify
  attr_accessor :_keep
  AVAILABLE_ROLES = %w{ administrator member }
end

We also add a AVAILABLE_ROLES constant so that we can get a list of the roles.

class UsersController < ApplicationController
  before_action :set_user, only: [:show, :edit, :update, :destroy]

  # ...

  # GET /users/1/edit
  def edit
    # Seed checkboxes for roles
    Role::AVAILABLE_ROLES.each do |role|
      # This adds an unsaved role to the user if it does not exist
      @user.roles.build(name: role) unless @user.has_role?(role)
    end
  end

  # PATCH/PUT /users/1
  def update
    if @user.update(update_params)
      redirect_to @user, notice: 'User was successfully updated.'
    else
      render :edit
    end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_user
      @user = User.find(params[:id])
    end

    # Only allow a trusted parameter "white list" through.
    def user_params
      params.require(:user).permit(:username, :email, roles_attributes: [:name, :id, :_keep, :_destroy])
    end

    def update_params
      user_params.tap do |o|
        # Adds the _delete attribute if the keep checkbox is unchecked
        o[:roles_attributes] = o[:roles_attributes].map do |k,h|
           attrs = h.merge(_destroy: (h[:_keep] != "1"))
           # Don't let the user update the name of an existing Role!
           # This would let a malicious user to grant any role.
           h.key?(:id) ? attrs.except(:name) : attrs 
        end
      end
    end
end