destroy w/ params and :dependent => destroy

Hello,

I'm having trouble debugging an override of destroy. I have an object
that in some cases I need to send a parameter when I destroy it. Usually
not though. So I've defined destroy in the model class like this:

class Look < ActiveRecord::Base
  belongs_to :palette

  has_many :colorings, :dependent => :destroy
  has_many :colors, :class_name => 'SiteColor', :dependent => :destroy
  has_one :active_regatta, :class_name => 'Regatta'
  belongs_to :regatta

  def destroy(options = {})
    # special destroy so we don't accidentally default look
    protected_ids = [1]

    super unless (protected_ids.include?(self.id) || options[:override]
)
  end
end

The problem is that when I try to destroy one of these objects, I get an
error on the call to super, "wrong number of arguments (1 for 0)" :

l=Look.find(62)

=> #<Look:0x23d2a28 @attributes={"name"=>nil, "id"=>"62",
"palette_id"=>nil, "regatta_id"=>nil}>

l.destroy

ArgumentError: wrong number of arguments (1 for 0)
  from ./script/../config/../config/../app/models/look.rb:19:in
`destroy'
  from ./script/../config/../config/../app/models/look.rb:19:in
`destroy'
  from (irb):2

Note that there is a :has_many :dependent => :destroy relationship.

It seems that my call to super is getting arguments I don't know about,
but I don't understand how to dig any deeper into this. I tried stepping
into the call to super itself, and I immediately end up in the rescue
clause for ActiveRecord's transaction method:

(rdb:1) where
--> #0
/usr/local/lib/ruby/gems/1.8/gems/activerecord-1.15.3/lib/active_record/connection_adapters/abstract/database_statements.rb:62
in 'transaction'
(rdb:1) l =
[57, 66] in
/usr/local/lib/ruby/gems/1.8/gems/activerecord-1.15.3/lib/active_record/connection_adapters/abstract/database_statements.rb
   57 transaction_open = true
   58 end
   59 yield
   60 end
   61 rescue Exception => database_transaction_rollback
=> 62 if transaction_open
   63 transaction_open = false
   64 rollback_db_transaction
   65 end
   66 raise

FWIW, I considered the possibility that it was one of the dependent
relationships' objects who's destroy method was getting sent a parameter
it wasn't expecting - so I defined both of those to accept a parameter
as well, and then *their* call to super caused this problem.

Maybe you are over-complicating this.

Rails has a "before_destroy" filter.

Just use that, and if the ID of the object being destroyed is 1, abort
the process (by just returning false).

So maybe something as simple as this (just off the top of my head, so
you'll wanna test it of course)

class Look < ActiveRecord::Base
  belongs_to :palette

  has_many :colorings, :dependent => :destroy
  has_many :colors, :class_name => 'SiteColor', :dependent => :destroy
  has_one :active_regatta, :class_name => 'Regatta'
  belongs_to :regatta

  before_destroy { |record| return false if record.id == 1 }

end

Note: it's such a simple check that you don't even need to call a
method... it can be handled right in the Proc.

Also note: the filter chain halts if you return false, which is
probably what you want in this case, but again: test it to be sure.

HTH!

-Danimal

Daniel,

I appreciate the suggestion. I don't think it will cover my case, but I
must admit I had forgoten all about before_destroy. I'll have to put
some more thought into this.

The problem is that I want to be able to force the destroy if necessary.
I'm not sure how I would articulate an override into before_destroy. But
I'll look into it.

Part of the reason for my question is that this seems to be a case where
what I thought I knew doesn't seem to be correct, so I'm looking to
improving my understanding of Rails.

-Avram

Avram,

I'm confused. What do you mean by "want to be able to force the
destroy"? If you have @look as the instance of the Look object you
want to destroy, you just call @look.destroy, right? The
before_destroy hook is just to make sure that you don't destroy the
Look object with ID=1 (or whatever ID you want to protect).

Some other things to consider:

You could use before_destroy but have it call a method instead of an
inline proc. I.e.:

before_destroy :skip_delete_if_not_one

def skip_delete_if_not_one
  return false if id == 1
end

Then, you could add other logic there if need be.

Another thought that just occurred to me: If the default destroy
method takes an options hash, maybe you need to pass that to super as
well? Maybe that's the error you are getting.

Also, I vaguely remember something in the Agile book about this...
i.e. not deleting the user with ID=1. You might see how that was done.

I don't think the problem is that your thinking is off... you are
thinking about this in the right way. But like so many things in the
Rails world, there are lots of ways to do something. So, you can call
a before filter (as I suggested). Or you can override the destroy
method (as you suggested). Both are "the right way"... it's more a
matter of taste, style and what you specifically want to accomplish.

-Danimal

Danimal,

Here's what I want:

Object.find(2).destroy # object 2 is destroyed
Object.find(1).destroy # has no effect
Object.find(1).destroy :override => true # object 1 is destroyed.

I want to prevent someone from being able to delete certain Look objects
by mistake, but I don't want to completely block my ability to destroy
the special ones.

The super destroy method does not take any parameters. The error is "1
for 0" not "0 for 1". It only comes up when I add parameters to my
destroy method.

If I simply change my destroy method to
  def destroy
    super if self.id != 1
  end

I don't have any problems.

But now I can't destroy object 1 intetionally without changing the code.

I wonder if by declaring it as:

def destroy(options = {}) ...

if you are not actually overriding the default destroy method? (I've
got a reasonable amount of Ruby and Rails experience, but I'm by no
means an expert, so I may be misunderstanding something here).

What if you make a second method called: "destroy_override". So
instead of calling: Look.find(1).destroy :override => true you'd call:
Look.find(1).destroy_override

Then, you do your original code but without the options parameter.
That overrides destroy() as a method and does the ID check. Then, in
destroy_override it just calls destroy, probably through an alias for
the original destroy so it doesn't call your overridden method.

I hope that rambling made sense. Let me know if it didn't.

-Danimal

It made sense, it just doesn't prevent accidental destruction of the
object. It requires someone to be aware that they can't indiscriminately
call destroy on objects of this class. It isn't really any different
than requiring people to always say "object.destroy if
!object.protected?.