Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

attr_accessor and password validation on update

I have this code in my user model:

class User < ActiveRecord::Base
  attr_accessible :email, :password, :password_confirmation
  attr_accessor :password

  before_save :encrypt_password

  validates :email, :presence => true,
                    :uniqueness => { :case_sensitive => false },
                    :format => { :with => /\A[^@]+@[^@]+\z/ },
                    :length => 7..128
  validates :password, :presence => true,
                       :confirmation => true,
                       :length => 6..128

  private
    def encrypt_password
      return unless password
      self.encrypted_password = BCrypt::Password.create(password)
    end
end

Now in my controller when I'm updating some user fields with

@user.update_attributes(params[:user])

the password field is always validated, even when it is not set in the params hash. I figured that this is happening because of the attr_accesor :password which always sets password = "" on update_attributes.

Now I could simply skip the validation of password if it is an empty string:

validates :password, :presence => true,
                     :confirmation => true,
                     :length => 6..128,
                     :if => "password.present?"

But this doesn't work because it allows a user to set an empty password.

Using update_attribute on the field I'd like to change is not a solution because i need validation on that attribute. If I pass in the exact parameter with

@user.update_attributes(params[:user][:fieldname])

it doesn't solve the problem because it also triggers password validation.

Isn't there a way to prevent attr_accesor :password from always setting password = "" on update?

like image 642
mjaros Avatar asked Nov 17 '11 16:11

mjaros


2 Answers

New answer

This works for me:

validates :password, :presence     => true,
                     :confirmation => true,
                     :length       => { :minimum => 6 },
                     :if           => :password # only validate if password changed!

If I remember correctly it also took me some time to get this right (a lot of trial and error). I never had the time to find out exactly why this works (in contrast to :if => "password.present?").

Old answer - not really useful for your purpose (see comments) I get around this problem by using a completely different action for password update (user#update_password). Now it is sufficient to only validate the password field

:on => [:create, :update_password]

(and also only make it accessible to those actions).

Here some more details:

in your routes:

resources :users do
  member do
    GET :edit_password # for the user#edit_password action
    PUT :update_password # for the user#update_passwor action
  end
end

in your UsersController:

def edit_password
  # could be same content as #edit action, e.g.
  @user = User.find(params[:id])
end
def update_password
  # code to update password (and only password) here
end

In your edit_password view, you now have a form for only updating the password, very similar to your form in the edit view, but with :method => :put and :url => edit_password_user_path(@user)

like image 125
emrass Avatar answered Nov 18 '22 18:11

emrass


The solution I have started using to get round this problem is this:

Start using ActiveModel's built in has_secure_password method.

At console

rails g migration add_password_digest_to_users password_digest:string
rake db:migrate

In your model:

class User < ActiveRecord::Base
  has_secure_password

  attr_accessible :login_name, :password, :password_confirmation

  # secure_password.rb already checks for presence of :password_digest
  # so we can assume that a password is present if that validation passes
  # and thus, we don't need to explicitly check for presence of password
  validates :password, 
    :length => { :minimum => 6 }, :if => :password_digest_changed?

  # secure_password.rb also checks for confirmation of :password 
  # but we also have to check for presence of :password_confirmation
  validates :password_confirmation, 
    :presence=>true, :if => :password_digest_changed?
end

And finally,

# In `config/locales/en.yml` make sure that errors on
# the password_digest field refer to "Password" as it's more human friendly 

en:
  hello: "Hello world"

  activerecord:
    attributes:
      user:
        password_digest: "Password"  

Oh, one more thing: watch the railscast

like image 43
stephenmurdoch Avatar answered Nov 18 '22 17:11

stephenmurdoch