34

I have a has_many association that accepts nested attributes. I need for there to be a minimum of 1 associated object in the collection, so I wrote a custom validator:

class MinimumCollectionSizeValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    if value.size < options[:size]
      record.errors[attribute] << (options[:message] || "must have at least #{options[:size]} line.")
    end
  end
end

The model looks like:

has_many :foos, :dependent=>:destroy
accepts_nested_attributes_for :foos
validates :foos, :minimum_collection_size=>{:size=>1}

This works great on model creation, but fails miserable on update. @my_model.update_attributes(params[:my_model]) returns true even if all the foos are removed by _destroy.

How do I get update_attributes to behave the same as save?

SooDesuNe
  • 9,880
  • 10
  • 57
  • 91

3 Answers3

76

A better way to validate the minimum size of a collection:

validates :my_association, :length => { :minimum => Fixnum}
SooDesuNe
  • 9,880
  • 10
  • 57
  • 91
  • 1
    I had an experience with using this, that does fullfill exactly what I wanted. So for creating an object with only validates if the object has the minimum defined. But if lets say the minimum is 2, and after creation I delete one of those associations, it saves and validates the object with 1. I dont know why is that but I wanted the validation not work only for creation. – andre.orvalho Apr 04 '14 at 11:31
  • 1
    This validator counts object that are marked for destruction as well. Its better to use custom [validator](http://guides.rubyonrails.org/active_record_validations.html#performing-custom-validations) and count non marked for destruction with: answers.reject(&:marked_for_destruction?).count < MINIMUM_NUMBER_OF_ANSWERS – duleorlovic Sep 24 '14 at 10:49
  • 7
    This validation fails if you're using nested attributes with allow_destroy because it counts occurrences marked for destruction. – Brooks Dec 03 '14 at 18:56
  • This is solution has problem when you delete last record in collection. Below I write better solution. – Serhiy Nazarov Mar 21 '16 at 14:50
  • 1
    @duleorlovic, @Brooks it's no longer a case. In **Rails 5** new `ActiveRecord`'s [length validator](https://github.com/rails/rails/blob/master/activerecord/lib/active_record/validations/length.rb#L3-L10) was introduced which fixes the issue. – Oleg Afanasyev Jun 06 '17 at 13:36
25

SooDesuNe

Yes, validates :foos, :length => {:minimum => 1, :message=>"At least one foo" } is better than the original one, but the same issue still happens.

To fix it, we could use validate method:

validate :validate_foos

private
def validate_foos
  remaining_foos = foos.reject(&:marked_for_destruction?)
  errors.add :foos, "At least one foo" if remaining_foos.empty?

Hope it helps to who encountered the same problem.

aqingsao
  • 2,104
  • 1
  • 19
  • 18
  • You can customize the min/max messages with `too_short` and `too_long` http://api.rubyonrails.org/v4.1.1/classes/ActiveModel/Validations/HelperMethods.html#method-i-validates_length_of – Petercopter Aug 03 '14 at 01:01
13

Create validator:

class CollectionLengthValidator < ActiveModel::Validations::LengthValidator
  def validate_each(record, attribute, value)
    value = value.respond_to?(:reject) ? value.reject(&:marked_for_destruction?) : value
    super(record, attribute, value)
  end
end

This is resolve problem with delete last record in collection.

Then use in model as standard length validator:

validates :foos, :collection_length => {:minimum => 1}
Serhiy Nazarov
  • 369
  • 3
  • 8
  • 2
    This is actually the correct answer. Thank you! And it also works when making valiations in Reform forms instead of relying on accepts_nested_attributes – Freddo Apr 20 '16 at 18:38