My User model contains the following:
validates :password_digest, :presence => true, :message => "The password has to be 6 or more characters long"
def password=(password)
self.password_digest = BCrypt::Password.create(password) if password.length >= 6
end
The issue is that the message in the validates isn't working. I get a Unknown validator: 'MessageValidator' error. I assumed the way the presence validation worked was that it would just check if the password_digest was nil, which it would be had the password had a length less than 6. I want a solution that is elegant, like what I attempted. I have solved this one way, but I would really appreciate an understanding as to why what I'm trying isn't working, and is there a way to make it work.
What I got to work was:
validate do |user|
user.errors['password'] = "can't be less than 6 characters" if user.password_digest.nil?
end
This is due to how the validates method works. It assumes that you're looking for the MessageValidator when you specify :message as a key in the hash passed to validates.
This can be solved by reconstructing the query as follows:
validates :password_digest, :presence => { :message => "The password has to be 6 or more characters long" }
Related
Faced a problem not clear to me.
I used password validation during registration.
def pass_val
if password_digest.count("a-z") <= 0 || password_digest.count("A-Z") <= 0
errors.add(:password, "must contain 1 small letter, 1 capital letter and minimum 8 symbols")
end
end
validates :password_digest, presence: true,
length: { minimum: 8 }
I made it possible to recover the password for the user, and validation just stopped working.
I changed all password_digest in validation to just password and validation worked again.
BUT, after that, password recovery disappeared, which began to curse for validation.
NoMethodError in PasswordResetsController#create
undefined method `count' for nil:NilClass
If I change all password again to password_digest, validation will disappear, but the recovery password will work again.
UPDATE
Using password - validation in the console works (it also works on the site. Passwords recovery does not work).
Using password_digest - validation in the console also works (it doesn’t work on the site, the recovery password works).
UPDATE 2
Moreover, an error occurs during password recovery when a letter is sent to the mail (before sending), and not exactly during the password change.
UPDATE 3
create method in PasswordResetsController:
def create
author = Author.find_by_email(params[:email])
author.send_password_reset if author
flash[:success] = "Email sent with password reset instructions."
redirect_to root_path
end
UPDATE 4
send password_reset:
def send_password_reset
confirmation_token
self.password_reset_sent_at = Time.zone.now
save!
AuthorMailer.password_reset(self).deliver!
end
But the letter is not sent, crashes up to this point.
I want to emphasize that I either use password_digest in validation and validation does not work, and password recovery works. Or I use password in validation and validation works, but password recovery does not.
Also, it’s worth noting that the create method in PasswordResetsController is not used to change the password, but to send an email to the email. I can’t understand why validation swears at him.
Seems like you already found a solution yourself. However I'm not sure you understand why this is happening.
Let's start of with the important fact that password_digest doesn't contain the password but rather a hashed version of the password. (The result of a hash function is also called a digest.) You'll want to add the validations to the password attribute, since that contains the actual password.
When you assign a new password to an author both the password and password digest will be assigned a new value. You could imagine the password setter to look something like this:
def password=(password)
#password = password
#password_digest = do_some_hash_magic(password)
end
Because password is set when you assign it to the author you're able to validate the contents of it.
Another important thing to know is that the actual password is never saved into the database, only the password digest is (for security reasons). For this reason when you (re)load the instance from the database password is set to nil, because the application no longer knows what it is.
Instead of skipping the validation with save(validate: false), I suggest using the :allow_nil option or making the validation conditional to only trigger the validation when the password is not nil.
validates :password, length: { in: 8..255 }, allow_nil: true
validates :password, format: { with: /\p{Lower}/, message: :no_lower }, allow_nil: true
validates :password, format: { with: /\p{Upper}/, message: :no_upper }, allow_nil: true
validates :password, format: { with: /\p{Digit}/, message: :no_digit }, allow_nil: true
validates :password, format: { with: /[\p{S}\p{P}]/, message: :no_special }, allow_nil: true
Alternatively you can be more explicit and use the :on option with a custom context to only trigger trigger the validations when you want them to:
validates :password, length: { in: 8..255 }, on: :password
validates :password, format: { with: /\p{Lower}/, message: :no_lower }, on: :password
validates :password, format: { with: /\p{Upper}/, message: :no_upper }, on: :password
validates :password, format: { with: /\p{Digit}/, message: :no_digit }, on: :password
validates :password, format: { with: /[\p{S}\p{P}]/, message: :no_special }, on: :password
The context doesn't have to be named after an attribute, using :foo instead of :password would also be just fine. You can then run these validations by passing a context when calling valid? or save.
author.valid?(:password)
author.save(context: :password)
If you use Rails 5 or higher you can pass multiple contexts as an array if you want to run a mix of validations. author.valid?([:create, :password])
For additional info about the regexes used check out the Ruby regexp documentation.
The error was in the send_password_reset method.
Added (: validate => false) to this method and it worked.
def send_password_reset
confirmation_token
self.password_reset_sent_at = Time.zone.now
save!(:validate => false)
AuthorMailer.password_reset(self).deliver!
end
In vallidation i use password.
I have two validations:
validates :email, format: { with: /\A(.+)#(aol|gmail|office365|outlook|verizon|yahoo)\.com\Z/i }, if: Proc.new { |user| user.imap_server.blank? }
validates :email, presence: true
validates :imap_server, presence: true, if: Proc.new { |user| user.email.present? && user.email_invalid? }
def email_invalid?
self.email =~ /\A(.+)#(aol|gmail|office365|outlook|verizon|yahoo)\.com\Z/i
end
I show a user a form. It displays an email field but not imap_server field. If the value in email field does not match the specific regex, then I want to show them form again with the imap_server field present as well. If they enter a value for the imap_server field, then I no longer want to validate the regex of email field (although it must still be present).
The problem it feels like I am duplicating a validation. Both email_invalid? and the validates :email, format: ... do the same thing. How can I clean this up?
You could replace the validates :email, format: ... with
validate :email_format
def email_format
errors.add(:email, 'format invalid') if imap_server.blank? && email_invalid?
end
which is slightly more lines but lets you define the format validation in one place.
I suspect that the issue is you're trying to check the result of a validation (email_invalid?) while you're * still doing * the validations... you don't know what order the validations are going to be run (the order on the page is not something I'd trust)... so the best way to solve it is to just write all these things into a single validates method eg a quick-and-dirty way:
validates :email_or_imap_server
def email_or_imap_server
email_valid = false # for scoping
if email.present?
# note: email validation via regex is harder than you think...
# google it...
email_valid = email.match(/#{VALID_EMAIL_FORMATS}/)
if email_invalid
errors.add(:email, "email invalid format should be...")
errors.add(:imap_server, "email or imap-server must be present") unless imap_server.present?
end
else
errors.add(:imap_server, "either email or imap-server must be present") unless imap_server.present?
end
end
etc.
Note: the code above is almost certainly full of bugs and typos... don't copy/paste it almost certainly won't work and the logic doesn't exactly match that of your validations... but do something like this.
In an multilingual application, a user can input their Chinese and English names. The user can input either or both, but must input at least one name.
class Person < ActiveRecord::Base
validates :zh_name, :presence => true
validates :en_name, :presence => true
validates :fr_name, :presence => true
end
Since the built-in :validates_presence_of method can only validate both attributes at once, is there a way to validate the presence of at least one of many attributes in rails?
Like a magical, validates_one_of :zh_name, :en_name, :fr_name
Thank you in advance,
validate :at_least_one_name
def at_least_one_name
if [self.zh_name, self.en_name, self.fr_name].reject(&:blank?).size == 0
errors[:base] << ("Please choose at least one name - any language will do.")
end
end
Taking #micapam's answer a step futher, may I suggest:
validate :has_a_name
def has_a_name
unless [zh_name?, en_name?, fr_name?].include?(true)
errors.add :base, 'You need at least one name in some language!'
end
end
just a quick shot out, you can pass a "if" or "unless" to the validator, maybe you can get it working this way. i have something like this in mind
validates :zh_name, :presence => { :if => (fr_name.blank? && en_name.blank?) }
validate :has_a_name
def has_a_name
unless [zh_name, en_name, fr_name].any?{|val| val.present? }
errors.add :base, 'You need at least one name in some language!'
end
end
Max Williams' answer is fine, but I didn't see the need to count hits when any? returns a boolean.
I am having an issue while saving a user object in the database. I have two fields in the model (email and password) which are not allowed to be null in the database itself. Plus I have added validation in the model like
validates_presence_of :email, :message => "must be provided"
validates_presence_of :password, :message => "must be provided"
Now when I try to save the model from the create method of the controller, it invalidates the data and renders the new action again. However I have multiple error messages for each field
Email can't be blank
Email must be provided
Password can't be blank
Password must be provided
I don't need multiple error messages for the same one. How can I eliminate this?
Looks like you are validating in two different places. You have to figure it out the places...
If you are doing two different validation for a field and want to display one error message on a field at a time, you can do the following,
validates_presence_of :email, :message => "must be provided"
validates_uniqueness_of :email, :message => "must be unique",
:if => lambda { |a| a.errors.on(:email).blank? }
Looks like you are rendering errors twice. Check all your views, they can also be inherited.
I'm trying to capture the value that's throwing a uniqueness error (or for that matter, any other type of built-in validation) to display it in the :message option. Here's what I tried (didn't work)
# inside the model
validate_uniqueness_of :name, :message => "#{name} has already been taken" # also tried using #{:name}
I could use a custom validation, but this beats the point of using something that's already integrated into AR. Any thoughts? thanks.
Try this interpolation technique:
validate_uniqueness_of :name, :message => "%{value} has already been taken"
The RailsGuide for Active Record Validations and Callbacks shows an example where %{value} is interpolated in a custom error message:
:message => "%{value} is not a valid size"
I looked at the validates_each documentation and can see the validate block is passed three properties: |record, attr, value|. All three can be accessed with %{model}, %{attribute} and %{value}.
While this is limited, since it only gives you access to three properties, thankfully that is all you need.
Try self.name
validates_uniqueness_of :name, :message => "#{self.name} has already been taken" # also tried using #{:name}
Also validate_uniqueness_of is wrong it should be validates_uniqueness_of
If this not works use validate method and comment line validates_uniqueness_of
def validate
name= User.find_by_name(self.name) #Assuming User is your Model Name
unless name.blank?
self.errors.add :base, "#{self.name} has already been taken"
end
end