Updating `User` attributes without requiring password
Asked Answered
I

9

29

Right now, users can edit some their attributes without having to enter their password because my validations are set up like this:

validates :password, :presence =>true, :confirmation => true, :length => { :within => 6..40 }, :on => :create
validates :password, :confirmation => true, :length => { :within => 6..40 }, :on => :update, :unless => lambda{ |user| user.password.blank? } 

However, after a user does this, their password is deleted - update_attributes is updating their password to "". Here is my update definition:

def update

    if @user.update_attributes(params[:user])
        flash[:success] = "Edit Successful."
        redirect_to @user
    else
        @title = "Edit user"
        render 'edit'
    end
end

I've also tried using a different definition that uses update_attribute instead:

def save_ff
    @user = User.find(params[:id])
    @user.update_attribute(:course1, params[:user][:course1] )
    @user.update_attribute(:course2, params[:user][:course2] )
    @user.update_attribute(:course3, params[:user][:course3] )
    @user.update_attribute(:course4, params[:user][:course4] )
    redirect_to @user 
end 

But for some reason this is doing the same thing. How can I update some user attributes without changing the password? Thanks!

Illyricum answered 16/8, 2011 at 19:2 Comment(0)
L
25

I didn't realize the solution I gave you yesterday would lead to this problem. Sorry.

Well, taking inspiration from devise, you should simply update your controller this way:

def update
  params[:user].delete(:password) if params[:user][:password].blank?
  if @user.update_attributes(params[:user])
    flash[:success] = "Edit Successful."
    redirect_to @user
  else
    @title = "Edit user"
    render 'edit'
  end
end
Longley answered 17/8, 2011 at 12:23 Comment(6)
Oh now that's very clever. This way you can use the same update action and split the update forms for the user over many views, you just need to remember to delete an attribute if it's blank.Chuckchuckfull
Ah I feel bad now. But this answer didn't work for me as of Rails 4, because we now use strong params over the params hash itself, so this answer wouldn't work for a beginner who was copy-pasting. The concept is still sound though so maybe it doesn't grant a thumb-down. If you edit your answer with strong params I'll be able to upvote.Chuckchuckfull
this was posted in 2011.... lets do a null edit if you want to undo... Or feel free to blame everyone posting answers prior to strong paramsLongley
Well to be honest the internet is a fluid medium. Old questions including all of those prior to strong params should be updated. Blame is a bit of a strong word but I think they should be updated.Chuckchuckfull
sounds like a king joffreys is speaking not a stark... I leave with with your enjoyable blaming power. Keep on discouraging people to help other peopleLongley
I'm using strong params; dropping the clever line into my update function worked perfectly without any other changes needed. Nice solution, thank you.Chekiang
B
12

This blog post demonstrates the principal of what you want to do.

What is not shown, but may be helpful, is to add accessors to the model:

attr_accessor   :new_password, :new_password_confirmation
attr_accessible :email, :new_password, :new_password_confirmation

and to provide all of the desired validation under the condition that the user has provided a new password.

  validates :new_password,  :presence => true, 
                            :length   => { :within => 6..40 }, 
                            :confirmation => true, 
                            :if       => :password_changed?

Lastly, I would add a check to see if the encrypted_password has been set in order to determine if "password_changed?" in order to require a password on a new record.

  def password_changed?
    !@new_password.blank? or encrypted_password.blank?
  end
Barberabarberry answered 18/3, 2012 at 20:45 Comment(1)
Devise is using similar approachDarmstadt
H
12

I've been struggling with this and going around in circles for a while, so I thought I'd put my Rails 4 solution here.

None of the answers I've seen so far meet my use case, they all seem to involve bypassing validation in some way, but I want to be able to validate the other fields and also the password (if present). Also I'm not using devise on my project so i can't make use of anything particular to that.

Worth pointing out that it's a 2 part problem:

Step 1 - you need to remove the password and confirmation field from the strong parameters if the password is blank like so in your controller:

if myparams[:password].blank?
  myparams.delete(:password)
  myparams.delete(:password_confirmation)
end

Step 2 - you need to alter validation such that the password isn't validated if it's not entered. What we don't want is for it to be set to blank, hence why we removed it from our parameters earlier.

In my case this means having this as the validation in my model:

validates :password, :presence => true, :confirmation => true, length: {minimum: 7}, :if => :password

Note the :if => :password - skip checking if the password is not being set.

Holierthanthou answered 22/11, 2013 at 11:59 Comment(1)
I like this approach but I think it's safer restricting the password validation to just the create method like this validates :password, :presence => true, :confirmation => true, length: {minimum: 7}, on: :create so validation will only occur for the create methodHorten
C
6
# It smells

def update
  if params[:user][:password].blank?
    params[:user].delete :password
    params[:user].delete :password_confirmation
  end

  if @user.update_attributes(params[:user])
    flash[:success] = "Edit Successful."
    redirect_to @user
  else
    @title = "Edit user"
    render 'edit'
  end
end

# Refactoring

class User < ActiveRecord::Base
  ...
  def update_attributes(params)
    if params[:password].blank?
      params.delete :password
      params.delete :password_confirmation
      super params
    end
  end
  ...
end

def update
  if @user.update_attributes(params[:user])
    flash[:success] = "Edit Successful."
    redirect_to @user
  else
    @title = "Edit user"
    render 'edit'
  end
end

# And little better

class User < ActiveRecord::Base
  ...
  def custom_update_attributes(params)
    if params[:password].blank?
      params.delete :password
      params.delete :password_confirmation
      update_attributes params
    end
  end
  ...
end

def update
  if @user.custom_update_attributes(params[:user])
    flash[:success] = "Edit Successful."
    redirect_to @user
  else
    @title = "Edit user"
    render 'edit'
  end
end
Carinacarinate answered 26/6, 2012 at 21:11 Comment(1)
Definitely probably the simplest way to do it in Rails 4 and a great refactor of @Richard W 's code.Lutero
J
6

2017 answer:

In Rails 5 as also pointed out by Michael Hartl's tutorial, it's enought that you get something along these lines in your model:

validates :password, presence: true, length: { minimum: 6 }, allow_nil: true

allow_nil: true is the key here. This allows a user to edit his/her info without expecting to also change the password.

At this point people may think that this will also allow empty user signups; However this is prevented by using the has_secure_password which automatically validates password presence exclusively within the create method.

This is a demo User model for illustration purposes:

class User < ApplicationRecord
  attr_accessor :remember_token
  before_save { self.email = email.downcase }
  validates :name, presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }
  has_secure_password
  validates :password, presence: true, length: { minimum: 6 }, allow_nil: true
end

Unfortunately I've no clue how to make this work in devise. Cheers 🖖.

Janes answered 26/7, 2017 at 13:59 Comment(0)
I
4

I had the same problem, and the solutions above didn't work for me. I found the real culprit in my case: I had an encrypt_password callback in my User model, which was setting the password to blank each time.

before_save :encrypt_password

I fixed it by adding a condition at the end for this call back:

before_save :encrypt_password, :unless => Proc.new { |u| u.password.blank? }

Injury answered 9/1, 2012 at 15:35 Comment(2)
Does this create any security vulnerabilities? I guess I also have a validates :password which should handle it, but I don't know enough about rails to know whether not encrypting the password could create any problems.Deeann
This, combined with the answer below from @Chuckchuckfull did the trick for me. Thanks!Falange
C
4

The correct answer no-longer works for rails 4. I believe my answer is the cleanest and the most versatile that will work whenever you want to leave out any attributes (not just the password). This approach will be needed if you want to update the separate attributes of any model in a number of different places.

For example, if you want to do what Stack Overflow does and have the passwords updatable via a security page, the profile image updatable via the user show view and the bulk of a user's information updatable via a user edit view.

1) Extend the hash class with a class method to delete blank values. We will use this method to remove blank values that are not being updated but are still present in the params hash:

1a) Create a hash.rb file in your lib directory, under an ext directory:

command line

$ mkdir lib/ext
$ touch lib/ext/hash.rb 

1b) Inside hash.rb, 'create' a Hash class and create a .delete_blanks! method:

lib/ext/hash.rb

class Hash
    def delete_blanks!
        delete_if { |k, v| v.nil? }
    end
end

1c) Require this file (and your entire lib directory) into the rails referencing it in an initializer:

config/boot.rb

# other things such as gemfiles are required here, left out for brevity

Dir['lib/**/*.rb'].each { |f| load(f) } # requires all .rb files in the lib directory 

2) Inside the users#update action, implement our shiny new delete_blanks! class method to remove the attributes we're not updating from the params hash. Then, update the user instance via the update_attributes method, *not the update method!

2a) Firstly, let's use the delete_blanks! method to fix our user_params hash:

app/controllers/users_controller.rb

new_params = user_params.delete_blanks!

2b) And now let's update the instance using the update_attributes method, (again, not the update method):

app/controllers/users_controller.rb

@user.update_attributes(new_params)

Here's how the finished users#update action should look:

app/controllers/users_controller.rb

def update

    new_params = user_params.delete_blanks!

    if @user.update_attributes(new_params)
        redirect_to @user, notice: 'User was successfully updated.'
    else
        render action: 'edit' // or whatever you want to do
    end
end

3) In the User model, add the if: :<attribute> option to all of your validations. This is to make sure the validation is only triggered if the attribute is present in the params hash. Our delete_blanks! method will have removed the attribute from the params hash, so the validation for password, for example, won't be run. It's also worth noting that delete_blanks! only removes hash entries with a value of nil, not those with empty strings. So if someone leaves out the password on the user create form (or any form with a field for the password), a presence validation will take effect because the :password entry of the hash won't be nil, it'll be an empty string:

3a) Use the if: option on all validations:

app/models/user.rb

VALID_EMAIL_REGEX = /[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9\-.]/

validates :first_name, presence: true, if: :first_name
validates :last_name, presence: true, if: :last_name
validates :user_name, presence: true, if: :user_name

validates :email, presence: true, 
                  uniqueness: { case_sensitive: false },
                  format: { with: VALID_EMAIL_REGEX }, if: :email 

validates :password, length: { minimum: 6, maximum: 10 }, if: :password

And that's it. Now the user model can be updated over many, many different forms all over your app. Presence validations for an attribute still come into play on any form that contains a field for it, e.g. the password presence validation still would come into play in the user#create view.

This may seem more verbose than other answers, but I believe this is the most robust way. You can update in isolation an infinite number of attributes for User instances, on an infinite amount of different models. Just remember when you want to do this with a new model you need to repeat the steps 2a), 2b) and 3a)

Chuckchuckfull answered 6/4, 2014 at 20:58 Comment(2)
I like this approach, but the above didn't work for me at first. The delete_blanks! method didn't alter the hash, so I had to change that line in the update action to new_params = user_params.delete_blanks!Britannia
@ChrisHawkins Ah good save! While my bang method does alter the hash in-place, the user_params in update_attributes(user_params) isn't a variable containing the hash but a call to the user_params method, that returns a fresh, unaltered hash. I'll fix my answer :)Chuckchuckfull
C
0
@user.username=params[:username]
if @user.update_attribute(:email,params[:email])

  flash[:notice]="successful"
else
  flash[:notice]="fail"
end

above code can update username and email field. because update_attribute can update dirty fields. but it is a pity, update_attribute would skip validation.

Celestinacelestine answered 13/11, 2012 at 3:38 Comment(0)
M
-1

I was having the same problem. I wasn't able to fix it with

params[:user].delete(:password) if params[:user][:password].blank?

I have only been able to get it to work by doing "update_attribute" on each item individually, e.g.

if (  @user.update_attribute(:name, params[:user][:name])     && 
      @user.update_attribute(:email, params[:user][:email])   &&
      @user.update_attribute(:avatar, params[:user][:avatar]) &&
      @user.update_attribute(:age, params[:user][:age])       && 
      @user.update_attribute(:location, params[:user][:location]) &&
      @user.update_attribute(:gender, params[:user][:gender]) && 
      @user.update_attribute(:blurb, params[:user][:blurb])   )        
    flash[:success] = "Edit Successful."
    redirect_to @user
else
  @title = "Edit user info"
  render 'edit'
end

which is clearly a total hack but its the only way I can figure it out without messing with the validations and deleting the password!

Markos answered 17/8, 2011 at 16:59 Comment(1)
In worst case your user will be updated many times and is really a bad idea to do that.Luellaluelle

© 2022 - 2024 — McMap. All rights reserved.