Reputation: 19032
I'm working in Rails 3.2.16. In my app, an account has_many users. An account must always have admin users, so you can't destroy them. This takes care of that:
class Account < ActiveRecord::Base
has_many :users, :dependent => :destroy
end
class User < ActiveRecord::Base
before_destroy :check_if_admin
def check_if_admin
false if self.is_admin
end
end
However, when you destroy the whole account, the admins should be destroyed as well. Instead, when I call @account.destroy
from the controller, the User#before_delete
callback prevents the admin users from being destroyed.
I know I could call @account.delete
to skip callbacks, but my understanding is that the :dependent => :destroy
is itself a callback, so that would only delete the account, not the users.
Is there a way inside the callback to know where I came from, e.g.
def check_if_admin
return if [I'm doing an Account dependent delete]
false if self.is_admin
end
or do I have to manually delete
the users before destroy
ing the account?
Upvotes: 2
Views: 1849
Reputation: 19032
With the help of this answer, I found this solution. The idea is to temporarily turn off the specific child callback while destroying the parent. Note that I had to add the :prepend => :true
option to get my custom callback re-added to the front of the chain.
class Account < ActiveRecord::Base
before_destroy :disable_user_check_if_admin
before_destroy :enable_user_check_if_admin
has_many :users, :dependent => :destroy
def disable_user_check_if_admin
User.skip_callback(:destroy, :before, :check_if_admin)
end
def enable_user_check_if_admin
User.set_callback(:destroy, :before, :check_if_admin), :prepend => :true
end
end
class User < ActiveRecord::Base
before_destroy :check_if_admin
has_many :contacts, :dependent => :restrict
def check_if_admin
false if self.is_admin
end
end
Without :prepend => :true
, I ran into trouble because my User model also has_many :contacts, :dependent => :restrict
. The problem is that, skip_callback
actually deletes the callback and set_callback
re-adds the callback at the end of the callback chain. Using :prepend => :true
, I was able to insert my custom before_destroy :check_if_admin
callback at the front of the chain. See docs and source code here.
Alternative solution (not in use)
While I was fighting the callback sequence, I tried a different solution that leaves the callbacks intact. Borrowing from this answer, I used an accessor on Account to let me check when it is being deleted:
class Account < ActiveRecord::Base
before_destroy :disable_user_check_if_admin
before_destroy :enable_user_check_if_admin
has_many :users, :dependent => :destroy
attr_accessor :destroying
def disable_user_check_if_admin
self.destroying = true
end
def enable_user_check_if_admin
self.destroying = false
end
end
class User < ActiveRecord::Base
before_destroy :check_if_admin
has_many :contacts, :dependent => :restrict
def check_if_admin
return if self.account.destroying
false if self.is_admin
end
end
This did work but it doesn't "smell" right to be setting and checking a flag like this, so I went back to the skip/set callback using :prepend => :true
.
Upvotes: 3