Validating associated objects

R. Elliott Mason wrote:

I am having a weird problem

class Post
  has_many :uploads
end

class Upload
  belongs_to :post
end

In the form in my view the user can upload multiple files at once. In
my model
there are multiple validations for an Upload, such as being under a
certain filesize or not having absurd dimensions if it's an image, etc.

The problem is that if one of the uploads fails validation, the Post
object fails validation as well. Previously, I thought I had to specify
validates_associated :uploads in the Post model to get this behavior,
but seems like that is not the case. If one of the uploads fails I get
the error message "Uploads is invalid" in the Post object's errors,
which prevents the Post and all other Uploads from saving whether or not
they are valid; so basically one bad upload ruins the whole thing. This
is kind of bad since it can take a user a while to upload multiple
files, and I think it's silly to say "one upload out of ten failed,
start all over" when some of the uploads might be good.

What I would -like- to do is accept as many uploads as possible, having
the Post object fail to save only if zero uploads were valid. Any
invalid uploads get thrown away (although ideally I could somehow keep
their errors stored somewhere [in the Post object?] so I can show the
user what uploads failed and why).

The file_column plugin, and perhaps other file upload plugins,
will automatically store the valid uploads in temporary files
so that they do not have to be uploaded again when the form
is re-presented.

But if you want the partial save to proceed you'll have to
loop through the upload params, creating upload objects,
calling valid? on them, and adding them to the post's uploads
collection if valid and to a reject array if not, then
saving the post (along with the valid uploads) if at least one
upload is valid. Re-present the invalid uploads to the user.

If you still need to get rid of the automatic associate validity
check that is causing your "Uploads is invalid" message, just add:

      class Post
        validate_associated_records_for_uploads() end
      end

R. Elliott Mason wrote:

Hm, the problem I'm finding in this approach is that validation is being called twice; once when I call valid? for the object and a second time before the Post is saved. This wouldn't be too much of an issue if my validations didn't require database queries to be run, but unfortunately because they are run it causes an unnecessary about of redundancy.

save(false) saves without validation; and the automatic validation
of has_many associates can be turned off in the way described in
my last post.

R. Elliott Mason wrote:

It seems like the uploads, associated with the post, are being validated as the post is saved. Both the post and its associated objects having not been saved, all the objects go through their validations, which makes sense I guess. Overriding the validate_associated_records_for_uploads method prevents the validation from being run twice, but as each upload object is saved, it is validated.

I tried to instead use your save(false) method and save the uploads -before- I saved the post object, after calling valid? on those objects. I separated the good uploads from the bad and left the bad in the Post object as post.bad_uploads. However, saving the uploads ahead of time didn't work out well either. Validation was still run as the post object was saved.

I could try to save the post and upload completely separate from each other but the problem is their validations are related. A post shouldn't be saved if its uploads are bad, and an upload shouldn't be saved if the post is bad, so it's turning out to be somewhat of a mess.

So overriding validate_associated_records_for_uploads() just really prevents validation from being run THREE times in my case (once with that method, once with valid? and a third time when the post is saved.)

Yes, Rails 1.2 incorrectly runs validation twice on cascaded
saves, even if the parent object is saved with save(false)
or save_without_validation. Blocking the validate_associated_records...
method gets rid of one, but each new child will still be validated
before it's inserted into the DB.

To avoid this you have to do your own save cascade: save the
parent using save(false)/save_without_validation without any
new children added to its collections. Then separately build
and save each child the same way.

To save only if everything's valid you have to first run valid?
on each record.

   if post.valid? && uploads.inject(true) { |v, u| u.valid? && v }
     post.save(false)
     uploads.each { |u| u.post = post; u.save(false) }
   end

This can be simplified if post is not a new record.