I am using Ruby on Rails 3.0.9 and I am trying to validate a nested model. Supposing that I run validation for the "main" model and that generates some errors for the nested model I get the following:
#user.valid?
#user.errors.inspect
# => {:"account.firstname"=>["is too short", "can not be blank"], :"account.lastname"=>["is too short", "can not be blank"], :account=>["is invalid"]}
How you can see the RoR framework creates an errors hash having following keys: account.firstname, account.lastname, account. Since I would like to display error messages on the front-end content by handling those error key\value pairs with JavaScript (BTW: I use jQuery) that involves CSS properties I thought to "prepare" that data and to change those keys to account_firstname, account_lastname, account (note: I substitute the . with the _ character).
How can I change key values from, for example, account.firstname to account_firstname?
And, mostly important, how I should handle this situation? Is what I am trying to do a "good" way to handle nested model errors? If no, what is the common\best approach to do that?
I've made a quick Concern which shows full error messages for nested models:
https://gist.github.com/4710856
#1.9.3-p362 :008 > s.all_full_error_messages
# => ["Purchaser can't be blank", "Consumer email can't be blank", "Consumer email is invalid", "Consumer full name can't be blank"]
Some creative patching of the Rails errors hash will let you achieve your aim. Create an initializer in config/initalizers, let call it errors_hash_patch.rb and put the following in it:
ActiveModel::Errors.class_eval do
def [](attribute)
attribute = attribute.to_sym
dotted_attribute = attribute.to_s.gsub("_", ".").to_sym
attribute_result = get(attribute)
dotted_attribute_result = get(dotted_attribute)
if attribute_result
attribute_result
elsif dotted_attribute_result
dotted_attribute_result
else
set(attribute, [])
end
end
end
All you're doing in here is simply overriding the accessor method [] to try a little harder. More specifically, if the key you're looking for has underscores, it will try to look it up as is, but if it can't find anything it will also replace all the underscores with dots and try to look that up as well. Other than that the behaviour is the same as the regular [] method. For example, let's say you have an errors hash like the one from your example:
errors = {:"account.firstname"=>["is too short", "can not be blank"], :"account.lastname"=>["is too short", "can not be blank"], :account=>["is invalid"]}
Here are some of the ways you can access it and the results that come back:
errors[:account] => ["is invalid"]
errors[:"account.lastname"] => ["is too short", "can not be blank"]
errors[:account_lastname] => ["is too short", "can not be blank"]
errors[:blah] => []
We don't change the way the keys are stored in the errors hash, so we won't accidentally break libraries and behaviours that may rely on the format of the the hash. All we're doing is being a little smarter regarding how we access the data in the hash. Of course, if you DO want to change the data in the hash, the pattern is the same you will just need to override the []= method, and every time rails tries to store keys with dots in them, just change the dots to underscores.
As to your second question, even though I have shown you how to do what you're asking, in general it is best to try and comply with the way rails tries to do things, rather than trying to bend rails to your will. In your case, if you want to display the error messages via javascript, presumably your javascript will have access to a hash of error data, so why not tweak this data with javascript to be in the format that you need it to be. Alternatively you may clone the error data inside a controller and tweak it there (before your javascript ever has access to it). It is difficult to give advice without knowing more about your situation (how are you writing your forms, what exactly is your validation JS trying to do etc.), but those are some general guidelines.
I had the same problem with AngularJs, so I decided to overwrite the as_json method for the ActiveModel::Errors class in an initializer called active_model_errors.rb so that it can replace . for _
Here is the initializer code:
module ActiveModel
class Errors
def as_json(options=nil)
hash = {}
to_hash(options && options[:full_messages]).each{ |k,v| hash[k.to_s.sub('.', '_')] = messages[k] }
hash
end
end
end
I hope it can be helpful for someone
I'm not sure but I think you can't change that behavior without pain. But you could give a try to solutions like http://bcardarella.com/post/4211204716/client-side-validations-3-0
Related
I am using a 4-year old Rails tutorial and I have Rails 4.0.2. I made a model called "Thing" and a controller called "Things". The "Thing" model has one attribute called "data". In my create action, I had this line:
#thing = Thing.new(params[:thing])
which results in this error:
ActiveModel::ForbiddenAttributesError in ThingsController#create
I found a StackOverflow thread that said I needed to require my needed parameters, and that worked just fine.
Before I looked that up I tried putting the hash from my params directly into the Thing.new() method and I didn't get an error. I started with this line:
puts params[:thing]
in my create action, typed "12345" in my text field, hit submit and got this in the console:
{"data"=>"12345"}
So I tried this in the create action:
#thing = Thing.new({"data" => "12345"})
and I didn't get the error. I even confirmed they were identical by doing this:
puts params[:thing] == {"data"=>"12345"}
and I get "true" on the console. So,
Thing.new(params[:thing])
gives me the error, but
Thing.new({"data"=>"12345"})
does not.
How can Rails tell the difference between these two arguments when they seem to be identical?
params[:thing] is not the same thing as {"data" => "12345"}, they just have the same value when inspect is called on them, and params's class overrides == to say it's equal to the hash.
Rails 4+ uses Strong Parameters, which is a security feature to make sure you know what you're putting in your models. Basically, Rails wants to you check the validity of the parameters. It lets you do Thing.new({"data" => "12345"}) because you, the developer, are creating the Hash directly, and are more trustworthy than someone on the internet calling your server.
If you need to code a considerably complex validation, the error sometimes doesnt lie in a particular attribute, but in a combination of several of them.
For example, if i want to validate that a the time period between :start_date and :end_date doesnt contain any sunday, the error doesnt belong specifically to either of those fields, but the Errors add method requires to specify it.
Try doing something like this:
# Your Model.rb
validate :my_own_validation_method
...
private
def my_own_validation_method
if there_is_no_sunday_in_the_range
self.errors[:base] << "You must have a Sunday in the time range!"
end
end
Basically, you can add your own complex validations to a model, and when you see that something erroneous has happened, you can add an error string in the array of errors.
model_instance.errors[:base] << "msg"
The answers above are outdated. For Rails 5 and higher you need to call the ActiveModel::Errors add method with :base as the first parameter. See the example below.
model_instance.errors.add(
:base,
:name_or_email_blank,
message: "either name or email must be present"
)
You can use errors[:base] to add general errors that aren't specifically tied to one attribute - rails guide link.
You can actually name the hash key whatever you'd like:
instance.errors[:case_of_the_sundays] << "Error, son."
Just a little more descriptive.
Update:
message array in order to add an error is deprecated.
Now we do something like that.
self.errors.add(:some_key, "Some Error!")
I know how to check an attribute for errors:
#post.errors[:title].any?
Is it possible to check which validation failed (for example "uniqueness")?
Recently I came across a situation where I need the same thing: The user can add/edit multiple records at once from a single form.
Since at validation time not all records have been written to the database I cannot use #David's solution. To make things even more complicated it is possible that the records already existing in the database can become duplicates, which are detected by the uniqueness validator.
TL;DR: You can't check for a specific validator, but you can check for a specific error.
I'm using this:
# The record has a duplicate value in `my_attribute`, detected by custom code.
if my_attribute_is_not_unique?
# Check if a previous uniqueness validator has already detected this:
unless #record.errors.added?(:my_attribute, :taken)
# No previous `:taken` error or at least a different text.
#record.errors.add(:my_attribute, :taken)
end
end
Some remarks:
It does work with I18n, but you have to provide the same interpolation parameters to added? as the previous validator did.
This doesn't work if the previous validator has written a custom message instead of the default one (:taken)
Regarding checking for uniqueness validation specifically, this didn't work for me:
#post.errors.added?(:title, :taken)
It seems the behaviour has changed so the value must also be passed. This works:
#post.errors.added?(:title, :taken, value: #post.title)
That's the one to use ^ but these also work:
#post.errors.details[:title].map { |e| e[:error] }.include? :taken
#post.errors.added?(:title, 'has already been taken')
Ref #34629, #34652
By "taken", I assume you mean that the title already exists in the database. I further assume that you have the following line in your Post model:
validates_uniqueness_of :title
Personally, I think that checking to see if the title is already taken by checking the validation errors is going to be fragile. #post.errors[:title] will return something like ["has already been taken"]. But what if you decide to change the error message or if you internationalize your application? I think you'd be better off writing a method to do the test:
class Post < ActiveRecord::Base
def title_unique?
Post.where(:title => self.title).count == 0
end
end
Then you can test if the title is unique with #post.title_unique?. I wouldn't be surprised if there's already a Rubygem that dynamically adds a method like this to ActiveRecord models.
If you're using Rails 5+ you can use errors.details. For earlier Rails versions, use the backport gem: https://github.com/cowbell/active_model-errors_details
is_duplicate_title = #post.errors.details[:title].any? do |detail|
detail[:error] == :uniqueness
end
Rails Guide: http://guides.rubyonrails.org/active_record_validations.html#working-with-validation-errors-errors-details
I'm building a method that ingests incoming email, and processes the email. Along the way there are a lot of things that could prevent the email from being processes successfully. The wrong reply-to address, the wrong from address, an empty message body etc..
The code is full of Switch Statements (case/when/end) and If statements. I'd like to learn a smarter, cleaner way of doing this. Additionally, a way to can track an error and at the end have one location where it emails back the user with an error. Is something like this possible with rails?
#error = []
Case XXX
when xxxx
if XXXXX
else
#error = 'You don't have permission to reply to the xxxxx'
end
else
#error = 'Unfamilar XXXX'
end
Then something at the end like...
If #errors.count > 0
Send the user an email letting them know what went wrong
else
do nothing
end
Thanks for the help here. If you know of any other tutorials that would teach me how to write logic like the above smarter, that'd be great. Right now I have case/if statements going 3 levels deeps, it's hard to keep it straight.
Thanks
First, I would just assign a symbol to each error message as a simple hash:
ErrorsDescription = {
:first => "First error",
:second => "Second error",
...
}
And use symbols instead of strings.
Then, your if and switch statements. Basicaly I can't really help you, because I don't see what kind of condition statements you have. What are you checking? Why do you have 3 level deep conditions? Probably you can write it simpler using if and switch - so this is my first answer to this issue. Another solution may be writing simple methods to improve readability, so you can write like this:
if #email.has_wrong_reply_to_address?
#errors << :wrong_reply_to_address
else
...
end
Also, as #mpapis suggested, you can use Rails build in validation system, but not as ActiveRecord but as ActiveModel. Here you have some examples how to do it and how it works (also take a look here). Of course you may need to write custom validations, but they are just simple methods. Once you do all above job, you can just use:
#email.valid?
And if it is not, you have all errors in hash:
#email.errors
Just as in ordinary ActiveRecord object.
Then you may extend your Emial class with send_error_email method which sends an email if there was an error.
EDIT:
This is about new information you attached in comment.
You don't have to use nested ifs and switch here. You can have it looking like this:
def is_this_email_valid?
if !email_from_user_in_system?
#errors << :user_not_in_system
return false
end
if comment_not_exists?
#errors << :comment_not_exists
return false
end
if user_cannot_comment_here?
#errors << :permision_error
return false
end
...
true
end
Then you can use it:
if !#email.is_this_email_valid?
#email.send_error_mail
end
I suggest using Exceptions. Start with this tutorial, then use Google, trial and error to go from there.
Edit: In more complex cases, exceptions may not be the right tool. You might want to use validator functions instead, for example (see other answers), or you could just return early instead of nesting ifs, e.g.:
unless sender_valid?
#error = "Sender invalid"
return
end
unless subject_valid?
#error = "Invalid command"
return
end
# normal no-errors flow continues here...
You could throw an error when something is not right. Then catch it at the end of your method.
http://phrogz.net/programmingruby/tut_exceptions.html
To make your code more readable and not have a lot of switch and if/then statements, you could create separate methods that validate certain aspects and call them from your main error-checking method.
Is it possible to map your message to a model ? then all the if/switch logic would be validations and automatically handled by rails. Good starting point is active record validations guide
Also worth reading is action mailer guide
What's the best practices way to test that a model is valid in rails?
For example, if I have a User model that validates the uniqueness of an email_address property, how do I check that posting the form returned an error (or better yet, specifically returned an error for that field).
I feel like this should be something obvious, but as I'm quickly finding out, I still don't quite have the vocabulary required to effectively google ruby questions.
The easiest way would probably be:
class UserEmailAddressDuplicateTest < ActiveSupport::TestCase
def setup
#email = "test#example.org"
#user1, #user2 = User.create(:email => #email), User.new(:email => #email)
end
def test_user_should_not_be_valid_given_duplicate_email_addresses
assert !#user2.valid?
end
def test_user_should_produce_error_for_duplicate_email_address
# Test for the default error message.
assert_equal "has already been taken", #user2.errors.on(:email)
end
end
Of course it's possible that you don't want to create a separate test case for this behaviour, in which case you could duplicate the logic in the setup method and include it in both tests (or put it in a private method).
Alternatively you could store the first (reference) user in a fixture such as fixtures/users.yml, and simply instantiate a new user with a duplicate address in each test.
Refactor as you see fit!
http://thoughtbot.com/projects/shoulda/
Shoulda includes macros for testing things like validators along with many other things. Worth checking out for TDD.
errors.on is what you want
http://api.rubyonrails.org/classes/ActiveRecord/Errors.html#M002496
#obj.errors.on(:email) will return nil if field is valid, and the error messages either in a String or Array of Strings if there are one or more errors.
Testing the model via unit tests is, of course, step one. However, that doesn't necessarily guarantee that the user will get the feedback they need.
Section 4 of the Rails Guide on Testing has a lot of good information on functional testing (i.e. testing controllers and views). You have a couple of basic options here: check that the flash has a message in it about the error, or use assert_select to find the actual HTML elements that should have been generated in case of an error. The latter is really the only way to test that the user will actually get the message.