has_many and validates_associated

Given the following code:

class MyClass < ActiveRecord::Base

  has_many :technical_contacts
  has_many :schedule_contacts

  validates_associated :technical_contacts, :schedule_contacts

  protected

  def validate
    validate_contacts(technical_contacts)
    validate_contacts(schedule_contacts)
  end

  private

  def validate_contacts(contacts)
    contacts.each_with_index{|contact, i|
      (i+1).upto(contacts.length-1) do |j|
        if contacts[j].email == contact.email
          contacts[j].errors.add(:email,
                                 'You have already provided this email
address')
        end
      end
    }
  end

end

If my validate_contacts method adds errors to some contact objects, I
would expect @myclass.save to fail but it does not. I have tested that
the objects are invalid using the "valid?" method and they are so I
don't understand what is going wrong. Can anyone please help?

Craig Nicoll wrote:

Given the following code:

class MyClass < ActiveRecord::Base

  has_many :technical_contacts
  has_many :schedule_contacts

  validates_associated :technical_contacts, :schedule_contacts

  protected

  def validate
    validate_contacts(technical_contacts)
    validate_contacts(schedule_contacts)
  end

  private

  def validate_contacts(contacts)
    contacts.each_with_index{|contact, i|
      (i+1).upto(contacts.length-1) do |j|
        if contacts[j].email == contact.email
          contacts[j].errors.add(:email,
                                 'You have already provided this email
address')
        end
      end
    }
  end

end

If my validate_contacts method adds errors to some contact objects, I
would expect @myclass.save to fail but it does not. I have tested that
the objects are invalid using the "valid?" method and they are so I
don't understand what is going wrong. Can anyone please help?

Custom "validate" methods run after all other validations, so
validates_associated will run before the contact errors are added.

You can either get rid of the validates_associated call and add
the base errors in the validate_contacts method, or move the
validate_contacts calls to a :validates_each call that is
placed before the validates_associated call.

Mark Reginald James wrote:
You can either get rid of the validates_associated call and add
the base errors in the validate_contacts method, or move the
validate_contacts calls to a :validates_each call that is
placed before the validates_associated call.>

Hi Mark,

I tried your suggestion but the save method is still working when it
shouldn't. Here is my code:

class MyClass < ActiveRecord::Base

  has_many :technical_contacts
  has_many :schedule_contacts

  validates_each :technical_contacts do |model, attr, value|
    value.each_with_index{|contact, i|
      (i+1).upto(value.length-1) do |j|
        if value[j].email == contact.email
          value[j].errors.add(:email,
                                 'You have already provided this email
address')
        end
      end
    }
  end

  validates_associated :technical_contacts, :schedule_contacts

end

Any help would be most appreciated.

Thanks,

Craig

Craig Nicoll wrote:

I tried your suggestion but the save method is still working when it shouldn't. Here is my code:

class MyClass < ActiveRecord::Base

  has_many :technical_contacts
  has_many :schedule_contacts

  validates_each :technical_contacts do |model, attr, value|
    value.each_with_index{|contact, i|
      (i+1).upto(value.length-1) do |j|
        if value[j].email == contact.email
          value[j].errors.add(:email,
                                 'You have already provided this email
address')
        end
      end
    }
  end

  validates_associated :technical_contacts, :schedule_contacts

end

With what you have the error messages you add to a contact in the
validates_each will be cleared before valid? is called on each
of them during the validates_associated.

Is something like this possible?

class Contact < ActiveRecord::Base
   validates_uniqueness_of :email, :scope => :my_class_id,
     :message => '^You have already provided this email address'
end

class TechnicalContact < Contact end
class ScheduleContact < Contact end

class MyClass < ActiveRecord::Base
   has_many :technical_contacts
   has_many :schedule_contacts
   validates_associated :technical_contacts, :schedule_contacts
end

Otherwise, drop the validates_associated:

validates_each :technical_contacts, :schedule_contacts do |m, a, v|
   v.each_with_index{ |contact, i|
     (i+1).upto(v.length-1) do |j|
       if value[j].email == contact.email
         value[j].errors.add(:email, 'Email already provided')
         m.errors.add_to_base(a, 'Bad contact email address')
       end
     end
   }
end