Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rails: Prevent parent model from being created and surfacing child model validation errors

I have been stuck with this problem for a while and have thoroughly confused myself as to how nested models and validations work together.

In the code below, my aim is to have the creation of the parent model (Image or Video) fail if the validation of the child model(Content) fails. Currently, the parent model is being saved while the child model is not, and the validation errors are going unheard. If there are no validation errors, then everything works as expected.

#Image.rb
has_one     :content,
as:         :contentable,
inverse_of: :contentable,
dependent:  :destroy

#Video.rb
has_one     :content,
as:         :contentable,
inverse_of: :contentable,
dependent:  :destroy

#Content.rb
belongs_to   :contentable,
inverse_of:  :content,
polymorphic: true

validate     :all_good?

def all_good?
  errors.add(:base, "Nope!")
  return false
end

Any leads or insights are much appreciated!

like image 305
geoboy Avatar asked Sep 21 '16 02:09

geoboy


2 Answers

Rails has a special validation called validates_associated that ensures that the associated record(s) is valid. If the associated record is invalid then the parent record will also be invalid and an error for the association will be added to it's list of errors.

In both your Image and Video classes add the following:

validates_associated :content

Now if the content association is invalid the video or image will not be saved.

video = Video.new
video.content = Content.new
video.save #=> false
video.valid? #=> false
video.errors #=> [:content => "is invalid"]
like image 144
infused Avatar answered Nov 19 '22 10:11

infused


Short Answer

Add to image and video model:

accepts_nested_attributes_for :content

The Proof

I was quite sure I knew the answer to this but wasn't sure if it worked with polymorphic associations (which I haven't used before) so I set up a small test.

Created the models the same way as you have yours setup but with a name attribute and with a validation that I can use to test for failure.

class Image < ActiveRecord::Base
  has_one     :content,
              as:         :contentable,
              inverse_of: :contentable,
              dependent:  :destroy

  validates_length_of :name, maximum: 10
end

class Content < ActiveRecord::Base
  belongs_to   :contentable,
               inverse_of:  :content,
               polymorphic: true

  validates_length_of :name, maximum: 10
end

Next setup the migrations as so:

class CreateImages < ActiveRecord::Migration
  def change
    create_table :images do |t|
      t.string :name

      t.timestamps null: false
    end
  end
end


class CreateContents < ActiveRecord::Migration
  def change
    create_table :contents do |t|
      t.string :name
      t.references :contentable, polymorphic: true, index: true

      t.timestamps null: false
    end
  end
end

Next write an RSpec to test that the parent isn't saved if child can't be saved and that validation errors perculate up.

  it 'should not save image if content is invalid' do
    image = Image.new()
    image.name = 'this is ok'
    expect(image).to be_valid

    content = Content.new()
    content.name = 'a string that should fail validation'
    image.content = content

    expect(image).to_not be_valid
    image.save

    expect(image).to_not be_persisted
    expect(content).to_not be_persisted

    expect(image.errors.count).to eq(1)
    expect(image.content.errors[:name][0]).to include('is too long')
  end

Ran the test and sure enough it fails.

Next add the following line to image (and video)

  accepts_nested_attributes_for :content

The tests now pass - i.e., if the child fails validation the parent will also fail validation and will not save.

like image 40
David Avatar answered Nov 19 '22 10:11

David