Destroy on blank nested attribute - ruby-on-rails

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...

Related

How to delete child during update with reject_if option for accepts_nested_attributes_for

I am on Rails 5.0. I'm not quite sure if this should work or if I need to take a different approach. I have models for Procedure and Complication where Procedure has_many Complications defined like so;
class Procedure < ActiveRecord::Base
has_many :complications, dependent: :destroy
accepts_nested_attributes_for :complications, allow_destroy: true, reject_if: proc{|attr| attr[:name] == 'None'}
end
class Complication < ActiveRecord::Base
belongs_to :procedure
validates :name, presence: true
end
The user is presented with a nested form for the procedure with multiple complications. I have used the cocoon gem to do this dynamically. On a new record the user is presented with an empty complication select box. If they leave it empty the validation fails. This is to force them to select 'None' in order to prevent them skipping the field. If they do select 'None' then no complication is added because of the reject_if option. All of this works exactly as expected.
The problem I have arises if a complication is selected (e.g. 'Failed') and the procedure record is subsequently edited. If the complication is changed to 'None' and then the record is updated, the complication will be left unchanged (i.e. still 'Failed') when the behaviour I want is for the complication to be destroyed.
Presumably the reject_if option doesn't function to delete a record on an update if it already exists. Is this correct? If so, what is the appropriate way of handling my case?
TIA.
What you want is kind of out of the scope for the reject_if option.
You should be able to get the functionality by altering the whitelisted params to add _destroy = '1' if the name is "None" (or blank or nil).
class ProceeduresController
# ...
def update
end
private
# ...
# Only allow a trusted parameter "white list" through.
def proceedure_params
params.require(:proceedure)
.permit(:name,complications_attributes: [:id, :name, :_destroy])
end
def update_params
proceedure_params.tap do |p|
p["complications_attributes"].each do |a|
if a[:id].present? && ["None", nil, ""].includes?(a[:name])
a[:_destroy] = '1'
end
end
end
end
end
class Procedure < ActiveRecord::Base
has_many :complications, dependent: :destroy
accepts_nested_attributes_for :complications,
allow_destroy: true,
reject_if: :reject_attributes_for_complication?
def reject_attributes_for_complication?(attr)
return false if attr[:_destroy]
attr[:name] == 'None'
end
end

Rails accepts_nested_attributes_for validation on transactional object

I'm struggling since some hours in order to make validations of nested attributes work in my rails app. A small caveat is that I have to validate nested attributes dynamically based off of their parent's attributes, as the amount of info required changes over time according to where in the process the parent is.
So here's my setup: I have a parent with many different associated models and I want to validate subsequently nested attributes of those every time I save the parent. Given the fact that validations change dynamically, I had to write a custom validation method in the model:
class Parent < ActiveRecord::Base
attr_accessible :children_attributes, :status
has_many :children
accepts_nested_attributes_for :children
validate :validate_nested_attributes
def validate_nested_attributes
children.each do |child|
child.descriptions.each do |description|
errors.add(:base, "Child description value cant be blank") if description.value.blank? && parent.status == 'validate_children'
end
end
end
end
class Child < ActiveRecord::Base
attr_accessible :descriptions_attributes, :status
has_many :descriptions
belongs_to :parent
accepts_nested_attributes_for :descriptions
end
In my controller I call update_attributes on the parent when I want to save. Now the problem is that, apparently, rails runs the validations against the database and not against the object that was modified by the user or the controller. So what might happen is that a child's value is erased by a user and the validations will pass, while later validations will not pass because the item in the database is not valid.
Here's a quick example of this scenario:
parent = Parent.create({:status => 'validate_children', :children_attributes => {0 => {:descriptions_attributes => { 0 => {:value => 'Not blank!'}}}})
#true
parent.update_attributes({:children_attributes => {0 => {:descriptions_attributes => { 0 => {:value => nil}}}})
#true!! / since child.value.blank? reads the database and returns false
parent.update_attributes({:children_attributes => {0 => {:descriptions_attributes => { 0 => {:value => 'Not blank!'}}}})
#false, same reason as above
The validation works for first-level associations, e.g. if a Child has a 'value' attribute, I could run a validation the way I do. The problem is with deep associations that apparently cannot be validated before saving.
Could anyone point me in the right direction of how to solve this? The only way I currently see is by saving records, validating them afterwards and deleting / reverting them if the validation fails, but I am honestly hoping for something more clean.
Thank you all in advance!
SOLUTION
So it turns out I was running validations on deep nested models by referencing those directly in the custom validation, this way:
class Parent < ActiveRecord::Base
[...]
has_many :descriptions, :through => :children
[...]
def validate_nested_attributes
descriptions.each do |description|
[...]
end
end
end
Which for some reason leads to the problems I was having above. Thanks Santosh for testing my example code and reporting it was working, this pointed me in the right direction to figure this out.
For future reference, the code in the original question works for this sort of dynamic, deeply-nested validations.
I think you should use validates_associated for this
with the following validation in Child
validates :value, :presence => true, :if => "self.parent.status == 'validate_children'"

Rails 3: validates_presence_of validation errors on default value and in associated model

I have a basic invoice setup with models: Invoice, Item, LineItems.
# invoice.rb
class Invoice < ActiveRecord::Base
has_many :line_items, :dependent => :destroy
validates_presence_of :status
before_save :default_values
def default_values
self.status = 'sent' unless self.status
end
end
# item.rb
class Item < ActiveRecord::Base
has_many :line_items
validates_presence_of :name, :price
end
# line_item.rb
class LineItem < ActiveRecord::Base
belongs_to :item
belongs_to :invoice
before_save :default_values
validates_presence_of :invoice_id
validates :item_id, :presence => true
end
There is more in the model but I only presented the above for simplicity.
I get the following errors:
2 errors prohibited this invoice from being saved:
Line items invoice can't be blank
Status can't be blank
So two problems:
If I remove validates :invoice_id, :presence => true I don't get the Line items invoice can't be blank error message anymore, but why? I do want to validate the invoice_id on line_items, ALL line_items are supposed to have an invoice_id. How can I validate the invoice_id on line_items without getting an error?
Why am I getting the Status can't be blank error if I set it as a default value? I can probably set it up on the invoices_controller but I think the default value should be set in the model, right? How can I validate the presence of status and still have a default value in the model for it?
Both of these validation errors are occurring because validations get called before save (and before the before_save callback).
I'm assuming that you're using a nested_form to create the invoice and it's line items at the same time. If this is the case, you don't want to validates :invoice_id, :presence => true on the line items - the invoice and the line items are coming in at the same time, and the invoice hasn't been saved yet, so it doesn't have an id. If you leave the validation in, you'll need to create and save an empty invoice first, and then create the line items later so the invoice_id is available. If you only want to make sure invoice_id is still set after any edits, you can enforce this via validates :invoice_id, :presence => true, :on => :update this will skip the validation when the line item is being created (and the invoice_id isn't available yet).
You're running into problems with validates :status, :presence => true for similar reasons - the values coming in via the request are being validated against, and the "status" value isn't there. The before_save callback runs after validation. You can set the default value in the before_validation or after_initialization callback and the values will be there when validations are run.
Check out the Callbacks documentation for Rails for more info.
I'll start with 2:
before save is being executed only before save, meaning, after the object passed validation and is about to be saved. If the validation fails - it won't be executed.
as for 1:
Can you give an example of how you're trying to create an invoice?
Problem 1
Try validates_associated which checks that the associated models are all valid
Problem 2
Like most of the answers say before_save gets called after validations. The magic you're looking for is after_initialize which gets run after an object's initialize method is called.
class Invoice < ActiveRecord::Base
after_initialize :default_values
validates :status, presence: true
private
def default_values
self.status ||= 'sent'
end
end

Rails: Relation with class_name does not get saved properly?

I've got a (rather simple) model representing a comment:
class Comment < ActiveRecord::Base
STATES = [:processing, :accepted, :declined]
belongs_to :note
belongs_to :author, :class_name => 'User'
validates_inclusion_of :state, :in => STATES
validates_presence_of :author
default_scope :order => 'created_at DESC'
def initialize( attributes={} )
super(attributes)
self.state ||= 'processing'
end
end
However, everytime I save a comment (with its fields set properly), the author relation always fails to save (well, actually the comment saves successfully, it just leaves out the author...). This goes as far as Comment.first.valid? returning false due to the validation on the author field (Comment.first.author is nil).
My suspicion is that I handle the default value for state-field in a wrong way? If so, how should I set the default value instead?
thx for your help in advance
About the state attribute, it would be better to use an after_initialize callback to set the default instead of overriding the initialize function :
def after_initialize
self.state ||= 'processing'
end
To properly override a function you should pass params and args this way :
def initialize(*args,&block)
super(*args,&block)
#what-you-want-to-execute
end
Notice that there is often a better way than using this !

How to create nested objects using accepts_nested_attributes_for

I've upgraded to Rails 2.3.3 (from 2.1.x) and I'm trying to figure out the accepts_nested_attributes_for method. I can use the method to update existing nested objects, but I can't use it to create new nested objects. Given the contrived example:
class Product < ActiveRecord::Base
has_many :notes
accepts_nested_attributes_for :notes
end
class Note < ActiveRecord::Base
belongs_to :product
validates_presence_of :product_id, :body
end
If I try to create a new Product, with a nested Note, as follows:
params = {:name => 'Test', :notes_attributes => {'0' => {'body' => 'Body'}}}
p = Product.new(params)
p.save!
It fails validations with the message:
ActiveRecord::RecordInvalid: Validation failed: Notes product can't be blank
I understand why this is happening -- it's because of the validates_presence_of :product_id on the Note class, and because at the time of saving the new record, the Product object doesn't have an id. However, I don't want to remove this validation; I think it would be incorrect to remove it.
I could also solve the problem by manually creating the Product first, and then adding the Note, but that defeats the simplicity of accepts_nested_attributes_for.
Is there a standard Rails way of creating nested objects on new records?
This is a common, circular dependency issue. There is an existing LightHouse ticket which is worth checking out.
I expect this to be much improved in Rails 3, but in the meantime you'll have to do a workaround. One solution is to set up a virtual attribute which you set when nesting to make the validation conditional.
class Note < ActiveRecord::Base
belongs_to :product
validates_presence_of :product_id, :unless => :nested
attr_accessor :nested
end
And then you would set this attribute as a hidden field in your form.
<%= note_form.hidden_field :nested %>
That should be enough to have the nested attribute set when creating a note through the nested form. Untested.
check this document if you use Rails3.
http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html#label-Validating+the+presence+of+a+parent+model
Ryan's solution is actually really cool.
I went and made my controller fatter so that this nesting wouldn't have to appear in the view. Mostly because my view is sometimes json, so I want to be able to get away with as little as possible in there.
class Product < ActiveRecord::Base
has_many :notes
accepts_nested_attributes_for :note
end
class Note < ActiveRecord::Base
belongs_to :product
validates_presence_of :product_id unless :nested
attr_accessor :nested
end
class ProductController < ApplicationController
def create
if params[:product][:note_attributes]
params[:product][:note_attributes].each { |attribute|
attribute.merge!({:nested => true})
}
end
# all the regular create stuff here
end
end
Best solution yet is to use parental_control plugin: http://github.com/h-lame/parental_control

Resources