Validate Before Destroy

Ruby on-RailsValidation

Ruby on-Rails Problem Overview


I have three classes: School, Account, and Administratorship.

School

has_many :administatorships
has_many :administrators, :through => :administratorships

Account

has_many :administratorships

Administratorship

belongs_to :account
belongs_to :school

before_destroy :confirm_presence_of_alternate_administratorship_in_school

protected

def confirm_presence_of_alternate_administratorship_in_school
	unless school.administrators.count(["administratorships.account_id != #{id}"]) > 0
		errors.add_to_base "The school must have at least one administrator"
	end
end

Now, what I would like to happen is when I call destroy on an instance of Administratorship, for it to add an error to the model and prevent the destruction of the model. I have removed the unless statement to see if that was preventing the error from being added, but it wasn't the case. It seems that having errors on the model does not prevent the destroy from occurring.

So my question is, is there any way I can prevent the destroy from occurring using validations? I realize I could define a method that destroys only if the above condition is met, but it seems that a validation approach is a more elegant solution.

Ruby on-Rails Solutions


Solution 1 - Ruby on-Rails

If you return false from that before_destroy method, it will prevent the destruction.

Solution 2 - Ruby on-Rails

This is a Rails 5 answer, if you return false it will give a deprecation warning: "Returning false in Active Record and Active Model callbacks will not implicitly halt a callback chain in Rails 5.1".

def confirm_presence_of_alternate_administratorship_in_school
  return if school.administrators.count(["administratorships.account_id != #{id}"]) > 0
  errors[:base] << 'The school must have at least one administrator'
  throw :abort
end

Solution 3 - Ruby on-Rails

Returning false from your validation method will prevent the record from getting destroyed.

Example:

def confirm_presence_of_alternate_administratorship_in_school
  unless school.administrators.count(["administratorships.account_id != #{id}"]) > 0
    # errors.add_to_base() is deprecated in Rails 3. Instead do...
    errors.add(:base, "The school must have at least one administrator")

    # this will prevent the object from getting destroyed
    return false
  end
end

Side note: I was having trouble with this error message not being displayed. The validation would work and the object would not be deleted, but there would be no message letting me know what happened. The reason for this was that the controller was redirecting to the index view instead of rendering the delete view (if there is an error while creating a new user for example, it will render :action => 'new'. In this case there is no delete view). When this happened, the instance variable on which the error message was set (in errors.add(:base,"message")) is actually being reset, which destroys the error in the process.

Solution 4 - Ruby on-Rails

For Rails 5, returningfalse won't halt the callback chain. You need to use throw(:abort)

belongs_to :account belongs_to :school

before_destroy :confirm_presence_of_alternate_administratorship_in_school

protected

def confirm_presence_of_alternate_administratorship_in_school
    unless school.administrators.count(["administratorships.account_id != #{id}"]) > 0
        errors.add_to_base "The school must have at least one administrator"
        throw(:abort)
    end
end

Solution 5 - Ruby on-Rails

I ended up using code from here to create a can_destroy override on activerecord: https://gist.github.com/andhapp/1761098

class ActiveRecord::Base
  def can_destroy?
    self.class.reflect_on_all_associations.all? do |assoc|
      assoc.options[:dependent] != :restrict || (assoc.macro == :has_one && self.send(assoc.name).nil?) || (assoc.macro == :has_many && self.send(assoc.name).empty?)
    end
  end
end

This has the added benefit of making it trivial to hide/show a delete button on the ui

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestiontanmanView Question on Stackoverflow
Solution 1 - Ruby on-Railsuser688461View Answer on Stackoverflow
Solution 2 - Ruby on-RailsMartin Cabrera DiaubalickView Answer on Stackoverflow
Solution 3 - Ruby on-RailsRyanView Answer on Stackoverflow
Solution 4 - Ruby on-RailsNdetoView Answer on Stackoverflow
Solution 5 - Ruby on-RailsHugo ForteView Answer on Stackoverflow