Custom validation errors on nested models - ruby-on-rails

class Parent
has_one :child
accepts_nested_attributes_for :child
end
class Child
belongs_to :parent
end
Using a nested object form, I need to add some extra validations to the child model. These are not always run on Child, so I cannot put them in a validate method in Child. It seems sensible to do the checking in a validate method in Parent, but I'm having trouble adding error messages correctly.
This does work:
class Parent
...
def validate
errors[ :"child.fieldname" ] = "Don't be blank!"
end
But we lose nice things like I18n and CSS highlighting on the error field.
This doesn't work:
def validate
errors.add :"child.fieldname", :blank
end

You should keep them in the child model since that's the one validated, however, you can set conditionals with if: and unless:
class Order < ActiveRecord::Base
validates :card_number, presence: true, if: :paid_with_card?
def paid_with_card?
payment_type == "card"
end
end
You can do several variations on this, read more in the rails documentation http://edgeguides.rubyonrails.org/active_record_validations.html#conditional-validation
I guess you could add an attribute, created_by to child, and make Child select which validations to use depending on that one. You can do that like they do in this answer: Rails how to set a temporary variable that's not a database field

Related

Why is destroying nested records happening on update_attributes and not on the parent save?

I have two associated models:
class HelpRequest < ActiveRecord::Base
has_many :donation_items, dependent: :destroy
accepts_nested_attributes_for :donation_items, allow_destroy: true
and
class DonationItem < ActiveRecord::Base
belongs_to :help_request
I have a validator that returns an error to the user if he/she tries to save a help_request with no donation_items.
validate :has_donation_items?
...
def has_donation_items?
if !self.donation_items.present?
errors.add :a_help_request, "must have at least one item."
end
end
I'm updating both the help_request and the donation_items from within a nested form in which the user can destroy single or multiple donation_items. According to this, if the user destroys any donation_items they shouldn't get destroyed in the database until the parent is saved. But I've verified that in my case, they're being destroyed immediately upon running the update_attributes method. Here's stripped down code:
#help_request.update_attributes(help_request_params) # donation items get destroyed in the database right here
# do some stuff
if #help_request.save
#do some other stuff if the save is successful
Here's the help_request_params with nested attributes:
def help_request_params
params.require(:help_request).permit(:id, :name, :description, :event_flag, :due_on_event, :date, :time, :send_notification, :event_id, :invoked, donation_items_attributes: [:id, :name, :amount, :_destroy])
end
Is there a reason why the database seems to be getting updated on update_attributes?
Think I figured it out. update_attributes saves (or attempts to) immediately. Instead, I've switched to assign_attributes. Working out some other kinks, but it seems to have resolved the main problem.

Create two models at same time with validation

I'm having a potluck where my friends are coming over and will be bringing one or more food items. I have a friend model and each friend has_many food_items. However I don't want any two friends to bring the same food_item so food_item has to have a validations of being unique. Also I don't want a friend to come (be created) unless they bring a food_item.
I figure the best place to conduct all of this will be in the friend model. Which looks like this:
has_many :food_items
before_create :make_food_item
def make_food_item
params = { "food_item" => food_item }
self.food_items.create(params)
end
And the only config I have in the food_item model is:
belongs_to :friend
validates_uniqueness_of :food_item
I forsee many problems with this but rails is telling me the following error: You cannot call create unless the parent is saved
So how do I create two models at the same time with validations being checked so that if the food_item isn't unique the error will report properly to the form view?
How about to use nested_attributes_for?
class Friend < ActiveRecord::Base
has_many :food_items
validates :food_items, :presence => true
accepts_nested_attributes_for :food_items, allow_destroy: true
end
You're getting the error because the Friend model hasn't been created yet since you're inside the before_create callback. Since the Friend model hasn't been created, you can't create the associated FoodItem model. So that's why you're getting the error.
Here are two suggestions of what you can do to achieve what you want:
1) Use a after_create call back (I wouldn't suggest this since you can't pass params to callbacks)
Instead of the before_create you can use the after_create callback instead. Here's an example of what you could do:
class Friend
after_create :make_food_item
def make_food_item
food_params = # callbacks can't really take parameters so you shouldn't really do this
food = FoodItem.create food_params
if food.valid?
food_items << food
else
destroy
end
end
end
2) Handle the logic creation in the controller's create route (probably best option)
In your controller's route do the same check for your food item, and if it's valid (meaning it passed the uniqueness test), then create the Friend model and associate the two. Here is what you might do:
def create
friend_params = params['friend']
food_params = params['food']
food = FoodItem.create food_params
if food.valid?
Friend.create(friend_params).food_items << food
end
end
Hope that helps.
As mentioned, you'll be be best using accepts_nested_attributes_for:
accepts_nested_attributes_for :food_items, allow_destroy: true, reject_if: reject_if: proc { |attributes| attributes['foot_item'].blank? }
This will create a friend, and not pass the foot_item unless one is defined. If you don't want a friend to be created, you should do something like this:
#app/models/food_item.rb
Class FootItem < ActiveRecord::Base
validates :[[attribute]], presence: { message: "Your Friend Needs To Bring Food Items!" }
end
On exception, this will not create the friend, and will show the error message instead

Destroy on blank nested attribute

I would like to destroy a nested model if its attributes are blanked out in the form for the parent model - however, it appears that the ActiveRecord::Callbacks are not called if the model is blank.
class Artist < ActiveRecord::Base
using_access_control
attr_accessible :bio, :name, :tour_dates_attributes
has_many :tour_dates, :dependent => :destroy
accepts_nested_attributes_for :tour_dates, :reject_if => lambda { |a| a[:when].blank? || a[:where].blank? }, :allow_destroy => true
validates :bio, :name :presence => true
def to_param
name
end
end
and
class TourDate < ActiveRecord::Base
validates :address, :when, :where, :artist_id, :presence => true
attr_accessible :address, :artist_id, :when, :where
belongs_to :artist
before_save :destroy_if_blank
private
def destroy_if_blank
logger.info "destroy_if_blank called"
end
end
I have a form for Artist which uses fields_for to show the fields for the artist's associated tour dates, which works for editing and adding new tour dates, but if I merely blank out a tour date (to delete it), destroy_if_blank is never called. Presumably the Artist controller's #artist.update_attributes(params[:artist]) line doesn't consider a blank entity worth updating.
Am I missing something? Is there a way around this?
I would keep the :reject_if block but insert :_destroy => 1 into the attributes hash if your conditions are met. (This is useful in the cases where it's not convenient to add _destroy to the form code.)
You have to do an extra check to see if the record exists in order to return the right value but the following seems to work in all cases for me.
accepts_nested_attributes_for :tour_dates, :reject_if => :reject_tour, :allow_destroy => true
def reject_tour(attributes)
exists = attributes['id'].present?
empty = attributes.slice(:when, :where).values.all?(&:blank?)
attributes.merge!({:_destroy => 1}) if exists and empty # destroy empty tour
return (!exists and empty) # reject empty attributes
end
You could apply when all attributes are blank by just changing the empty calculation to:
empty = attributes.except(:id).values.all?(&:blank?)
I managed to do something like this today. Like #shuriu says, your best option is to remove the reject_if option and handle destruction yourself. mark_for_destruction comes in handy :
class Artist < ActiveRecord::Base
accepts_nested_attributes_for :tour_dates
before_validation :mark_tour_dates_for_destruction
def mark_tour_dates_for_destruction
tour_dates.each do |tour_date|
if tour_date.when.blank? || tour_date.where.blank?
tour_date.mark_for_destruction
end
end
end
end
You have code that says the record should be ignored if the 'where' or the 'when' is blank, on the accepts_nested _attributes line, remove the reject_if and your destroy_if blank will likely be called.
Typically to destroy, you would set a _destroy attribute on the nested record, check out the docs http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html
Also, just used cocoon for some of this today, and thought it was awesome, https://github.com/nathanvda/cocoon
Similar to Steve Kenworthy's answer, no local variables.
accepts_nested_attributes_for :tour_dates, :reject_if => :reject_tour, :allow_destroy => true
def reject_tour(attributes)
if attributes[:when].blank? || attributes[:where].blank?
if attributes[:id].present?
attributes.merge!({:_destroy => 1}) && false
else
true
end
end
end
With your current code it's not possible, because of the reject_if option passed to accepts_nested_attributes_for.
As Christ Mohr said, the easiest way is to set the _destroy attribute for the nested model when updating the parent, and the nested model will be destroyed. Refer to the docs for more info on this, or this railscast.
Or you can use a gem like cocoon, or awesome_nested_fields.
To do specifically what you want, you should remove the reject_if option, and handle the logic in a callback inside the parent object. It should check for blank values in the tour_dates_attributes and destroy the nested model. But tread carefully...

Better validates_associated method for Rails 3?

Rails 3 includes the validates_associated which is automatically called when saving a nested model. The problem with the method is the message is terrible - "Model(s) is invalid"
There have been a few posts attacking this issue for Rails 2:
http://rpheath.com/posts/412-a-better-validates-associated
http://pivotallabs.com/users/nick/blog/articles/359-alias-method-chain-validates-associated-informative-error-message
and there are probably more. It would be great to have a better version as described in these posts that is Rails 3 compatible. The main improvement would be to include why the associated model fails.
On the relationship, you can use :autosave => true instead which will try to save children models when you save the parent. This will automatically run the validations of the children and they will report with proper error messages.
Moreover, if you add a presence validation on the child that the parent must be set, and you construct the child objects through the association, you don't even need the autosave flag, and you get a beautiful error message. For example:
class Trip < ActiveRecord::Base
validates :name, :presence => true
attr_accessible :name
has_many :places, dependent: :destroy, :inverse_of => :trip
end
class Place < ActiveRecord::Base
belongs_to :trip
validates :name, :trip, presence: true
attr_accessible :name
end
Then you can get an nice error message with the following usage scenario:
> trip = Trip.new(name: "California")
=> #<Trip id: nil, name: "California">
> trip.places.build
=> #<Place id: nil, name: nil, trip_id: nil>
> trip.valid?
=> false
> trip.errors
=> #<ActiveModel::Errors:0x00000004d36518 #base=#<Trip id: nil, name: "California">, #messages={:places=>["is invalid"]}>
> trip.errors[:places]
=> ["is invalid"]
I think validates_associated is a relic of the era before autosaving of children and isn't the best way to do things any more. Of course that's not necessarily documented well. I'm not 100% sure that this also applies to Rails 2.3, but I have a feeling it does. These changes came when the nested attributes feature was added (which was sometime in 2.x).
This is a simplified snippet of code from a training project I posted on github.
I was having this problem, and in the end I used the solution given here by Ben Lee:
validates associated with model's error message
Ben says:
You can write your own custom validator, based on the code for the built-in validator.
Looking up the source code for validates_associated, we see that it uses the "AssociatedValidator". The source code for that is:
module ActiveRecord
module Validations
class AssociatedValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
return if (value.is_a?(Array) ? value : [value]).collect{ |r| r.nil? || r.valid? }.all?
record.errors.add(attribute, :invalid, options.merge(:value => value))
end
end
module ClassMethods
def validates_associated(*attr_names)
validates_with AssociatedValidator, _merge_attributes(attr_names)
end
end
end
end
So you can use this as an example to create a custom validator that bubbles error messages like this:
module ActiveRecord
module Validations
class AssociatedBubblingValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
(value.is_a?(Array) ? value : [value]).each do |v|
unless v.valid?
v.errors.full_messages.each do |msg|
record.errors.add(attribute, msg, options.merge(:value => value))
end
end
end
end
end
module ClassMethods
def validates_associated_bubbling(*attr_names)
validates_with AssociatedBubblingValidator, _merge_attributes(attr_names)
end
end
end
end
You can put this code in an initializer, something like /initializers/associated_bubbling_validator.rb.
Finally, you'd validate like so:
class User < ActiveRecord::Base
validates_associated_bubbling :account
end
NOTE: the above code is completely untested, but if it doesn't work outright, it is hopefully enough to put you on the right track
validates_associated runs the validations specified in the associated object's class. Errors at the parent class level simply say 'my child is invalid'. If you want the details, expose the errors on the child object (at the level of the child's form in the view).
Most of the time validates_existence_of is all I need.

How can I disable a validation and callbacks in a rails STI derived model?

Given a model
class BaseModel < ActiveRecord::Base
validates_presence_of :parent_id
before_save :frobnicate_widgets
end
and a derived model (the underlying database table has a type field - this is simple rails STI)
class DerivedModel < BaseModel
end
DerivedModel will in good OO fashion inherit all the behaviour from BaseModel, including the validates_presence_of :parent_id. I would like to turn the validation off for DerivedModel, and prevent the callback methods from firing, preferably without modifying or otherwise breaking BaseModel
What's the easiest and most robust way to do this?
I like to use the following pattern:
class Parent < ActiveRecord::Base
validate_uniqueness_of :column_name, :if => :validate_uniqueness_of_column_name?
def validate_uniqueness_of_column_name?
true
end
end
class Child < Parent
def validate_uniqueness_of_column_name?
false
end
end
It would be nice if rails provided a skip_validation method to get around this, but this pattern works and handles complex interactions well.
As a variation of the answer by #Jacob Rothstein, you can create a method in parent:
class Parent < ActiveRecord::Base
validate_uniqueness_of :column_name, :unless => :child?
def child?
is_a? Child
end
end
class Child < Parent
end
The benefit of this approach is you not need to create multiple methods for each column name you need to disable validation for in Child class.
From poking around in the source (I'm currently on rails 1.2.6), the callbacks are relatively straightforward.
It turns out that the before_validation_on_create, before_save etc methods, if not invoked with any arguments, will return the array which holds all the current callbacks assigned to that 'callback site'
To clear the before_save ones, you can simply do
before_save.clear
and it seems to work
A cleaner way is this one:
class Parent < ActiveRecord::Base
validate :column_name, uniqueness: true, if: 'self.class == Parent'
end
class Child < Parent
end
Or you can use it also in this way:
class Parent < ActiveRecord::Base
validate :column_name, uniqueness: true, if: :check_base
private
def check_base
self.class == Parent
end
end
class Child < Parent
end
So, uniqueness validation is done if the instance class of model is Parent.
Instance class of Child is Child and differs from Parent.
Instance class of Parent is Parent and is the same as Parent.
Since rails 3.0 you can also access the validators class method to manipulate get a list of all validations. However, you can not manipulate the set of validations via this Array.
At least as of rails 5.0 you however seem to be able to manipulate the _validators (undocumented) method.
Using this method you can modify the validations in the subclass like e.g.:
class Child < Parent
# add additional conditions if necessary
_validators.reject! { |attribute, _| attribute == :parent_id }
end
While this uses an undocumented method, is has the benefit of not requiring the superclass to know anything about the child's implementation.
Again poking around in the source, it seems that validations can be run either on every save, or updates/creates only. This maps to
:validate => all saves
:validate_on_create => creations only
:validate_on_update => updates only
To clear them, you can use write_inheritable_attribute, like this:
write_inheritable_attribute :validate, nil
Here is a slight variation of RubyDev's that I've been using in mongoid 3.
class Parent
include Mongoid::Document
validates :column_name , uniqueness: true, unless: Proc.new {|r| r._type == "Child"}
end
class Child < Parent
end
It has been working pretty good for me so far.

Resources