Rails Custom Validations on Associations - ruby-on-rails

I have a user. A user can have many tables. Actual, only 5.
In my table model I have
validate :max_tables
def max_tables
if user.tables.count > 5
errors[:base] << "You already have 5 tables."
end
end
This works all good and if I try to create a table and my user already has 5, I get a page that says
ActiveRecord::RecordInvalid in TablesController#create
Validation failed: You already have 5 tables.
But I don't get redirected back to the new table page with errors displaying nicely like I do if other validations aren't met. For some reason I'm getting stuck on this hard error page.
Any ideas?
EDIT: SOLVED
I was generating a short url in an after_create callback, and in there I was calling save!
Once I fixed that, All was good. So thanks #house9!

Use errors.add_to_base "You already have 5 tables." - in my oldest and biggest app I use errors.add_to_base and return false - it could be that the return is not required, but I haven't tested it.

EDIT: SOLVED
I was generating a short url in an after_create callback, and in there I was calling save!
Once I fixed that, All was good. So thanks #house9!

Related

Rails .update_attribute not updating

I notice this question pops up a lot, but after trying several recommended solutions I found I still can't figure out what is wrong. I have a model called sample and a user model as well. When a sample is approved the hours on the sample are supposed to be added to the users total hours, but the users value is never updated. Each user has a unique email which is stored in the sample when it is submitted for approval. I checked in the database to make sure it wasn't an issue with accessing the value, and no error is being thrown so I am not really sure what is happening. I'm pretty new to ruby and rails so any help is appreciated. My samples_controller.rb contains the function:
def approve
#sample = Sample.find(params[:id])
#sample.update(sample_status:1)
#user = User.find(Sample.email)
hours_update = #user.hours + #sample.volunteer_hours
#user.update_attributes(:hours, hours_update)
redirect_to samples_adminsamples_path
end
Edit: thanks for the help everyone, turns out I needed to use the command
#user = User.find_by(email: #sample.email)
in order to get the proper user.
Can you please give some more data like db structure of Sample and User tables.
From the limited information, I think the line number 4 (#user = User.find(Sample.email)) is the problem.
.find() tries to query the DB on id and Sample.email would be giving user's email and not the id of the corresponding user in db.
I am also guessing that in your controller, you are suppressing the thrown exception some where using begin-rescue block because .find() throws ActiveRecord::RecordNotFound exception if it fails to find the resource.
Alternatively, if it is fetching the user correctly, you can also try update_column to update the values.
You are using incorrect format for update_attributes
It should be
#user.update_attributes(hours: hours_update)
or
#user.update_attribute(:hours, hours_update)
NOTE: update_attribute doesn't triggers the callbacks

Rails Active Record - How to do validation which calls method rather than throws error

Active Record validations throw an error when they fail. What I have in a model is
validate_format_of :field_which_cannot_have_spaces, :with => /^[^\s]+$/, :message => "Some error message"
What I want instead, is for a string replacement to substitute spaces for underscores (snake_case).
The advantages of using validation for me, are that it runs every time the field is changed unless save(validate: false), and that I don't need to repeat the replacement in the create and update controller methods.
Front end javascript solutions won't help if the user hacks the form... a rails solution is needed!
It sounds like you want a callback rather than a validation. This can run each time your object is modified.
So, to remove spaces from your field before the object is saved you can do:
before_save :remove_spaces_from_x
def remove_spaces_from_x
self.field_which_cannot_have_spaces.gsub!("\s","_")
end
Note also that validation do not always raise an error when they fail. If you use save! or create! then an error is raised but if you use the equivalent save or create then no error is raised, false is returned and the object's errors are populated with details of the validation failure.
Co-worker just told me to do the following in the model:
def field_which_cannot_have_spaces=(input_from_form)
super(input_from_form.gsub("\s","_"))
end
This will change the value as it is set.
"Validations are for informing the client there is a problem, and shouldn't be doing something other than throwing an error."
Hope this helps someone else...

Is it possible to add errors to an ActiveRecord object without associating them with a particular attribute?

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!")

Why does this unused self.hash method cause a "can't convert String into Integer" error?

I am running through the Lynda Rails 3 tutorial. At one point, in a controller called access_controller, we call a method from a model called AdminUser. The original call was:
authorized_user = AdminUser.authenticate(params[:username], params[:password])
When I run rails server, open up the browser, and access the appropriate view, I get the error: TypeError, can't convert String into Integer
This same question has been asked twice before. The first time, the asker says the problem resolved itself the next day. (I first ran into this 3 days ago, so that has not happened.) The second question has not been answered. I will try to provide much more detail:
The method in the model was:
def self.authenticate(username="", password="")
user = AdminUser.find_by_username(username)
if user && user.password_match?(password)
return user
else
return false
end
end
When I call this method from the rails console, it works totally fine. Something about calling it from a controller, or trying to get at via the browser, seems to be going wrong (I am relative beginner, so I apologize that I cannot express this thought better). I have since replicated this error with a more simple method in the same AdminUser model:
def self.nothing
true
end
This still gives me the same error. I then tried calling the self.nothing method from a different controller and action (called pages_controller#show). When I tried to open that up in the browser, I once again got the same error: "can't convert String into Integer"
I then created an identical self.nothing method in my Subject model. When I try to run that method from the show action in pages_controller, it works totally fine. No errors.
So, the same method runs totally fine in rails console, totally fine when I place it in my Subject model, but produces an error when I place it in my AdminUser model.
I then tried to comment out basically everything in sight in my AdminUser model to see if I can make the error go away. I finally was able to. The error was apparently caused by another method:
def self.hash(password="")
Digest::SHA1.hexdigest(password)
end
I was supposed to have deleted this method a few video lessons ago when we added these other methods:
def self.make_salt(username="")
Digest::SHA1.hexdigest("Use #{username} with #{Time.now} to make salt")
end
def self.hash_with_salt(password="", salt="")
Digest::SHA1.hexdigest("Put #{salt} on the #{password}")
end
I never deleted the initial one, but for some reason, it was the one causing the error.
So, my question now is: Why did leaving in that method (which was not being used anywhere) cause this "can't convert String into Integer" error?
The reason is that User.hash overrides Object.hash that should return a Fixnum.
You should change it's name for something like User.make_hash

Validation: how to check for specific 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

Resources