Search code examples
ruby-on-railsrubyvalidationrails-activerecordvalidates-uniqueness-of

rails validation of nested attributes for uniqueness when some may be marked for destroy


I've got the following (sanitized) models:

 class Person < ActiveRecord::Base

      attr_accessible :name, :first_name, :last_name, :age, :job_title, :salary, :ssn, :prison_convictions, :addresses_attributes

      has_many :addresses, inverse_of: :person

      accepts_nested_attributes_for :addresses, allow_destroy: true

 end



 class Address < ActiveRecord::Base

      attr_accessible :zip_code, :street,:house_number,
 :unique_per_person_government_id

      belongs_to :person, inverse_of: :addresses

      validates_uniqueness_of :unique_per_person_government_id, scope: :person_id
 end

The problem is as follows,

Lets say Person Joe Shmoe has two addresses currently attached to himself

666 Foo Street, 12345 with unique id: “ABCDEFG” and 777 Lucky Avenue, 54321 with unique id: “GFEDCBA”

And lets say that the following post comes through from a form:

 {:addresses_attributes =>
 { [0] => {:unique_per_person_government_id => “ABCDEFG”, :street=> “Foo Street”, :house_number => 666, :zip_code=>12345, _destroy => 1}
 [1] => {:unique_per_person_government_id => “ABCDEFG”, :street=>”Bar Street”, :house_number => 888, :zip_code => 12345, _destroy => 0}
 }

The behavior appears to be that the second (ie the new) record is validated for uniqueness first, failing validation.

The behavior I would like is to quite simply remove all elements that are marked_for_destruction? before conducting any further validation.

How can I rewire the lifecycle in this way? Is there a better way to achieve this?

Thanks!


Solution

  • I've solved this as follows:

    class Person < ActiveRecord::Base
      attr_accessible :name, :first_name, :last_name, :age, :job_title, :salary, :ssn, :prison_convictions, :addresses_attributes
    
      has_many :addresses, inverse_of: :person
    
      accepts_nested_attributes_for :addresses, allow_destroy: true
    
      def validate_addresses
        codes = {}
        addresses.each do |a|
          if codes.key?(a.unique_per_person_government_id) and not a.marked_for_destruction?
            codes[a.unique_per_person_government_id] = codes[a.unique_per_person_government_id]+1
            if codes[a.unique_per_person_government_id] > 1
              return false
            end
          elsif not codes.key?(a.unique_per_person_government_id) and a.marked_for_destruction?
            codes[a.code] = 0
          elsif not codes.key?(a.unique_per_person_government_id) and not a.marked_for_destruction?
            codes[dacode] = 1
          end
        end
        return true
      end
    end
    
    
    class Address < ActiveRecord::Base
      before_save :validate_addresses
    
      attr_accessible :zip_code, :street,:house_number, :unique_per_person_government_id
    
      belongs_to :person, inverse_of: :addresses
    
      validates_uniqueness_of :unique_per_person_government_id, scope: :person_id, unless: :skip_validation?
    
      def skip_validation?
        person.addresses.each do |a|
          if unique_per_person_government_id == a.code and id != a.id
            return false unless a.marked_for_destruction?
          elsif id == a.id and a.marked_for_destruction?
            return false
          end
        end
        return true
      end
    end
    

    Thus enforcing uniqueness, and preventing a person with invalid addresses from saving, but ignoring items that are marked for destruction. If anybody has had a similiar problem and has a better/algorithmically simpler/easier to read solution, and would like to share it, that would be awesome :D