Well, the next solution I settled on, although not satisfying.
I have added an abstract base class for all my models:
class MyModel(models.Model): class Meta: abstract = True def pre_delete_handler(self): pass
The signal handler captures any pre_delete events for subclasses of this model:
def pre_delete_handler(sender, instance, **kwargs): if isinstance(instance, MyModel): instance.pre_delete_handler() models.signals.pre_delete.connect(pre_delete_handler)
In each of my models, I simulate any " ON DELETE RESTRICT " relationships by throwing an exception from the pre_delete_handler method if a pre_delete_handler entry exists.
class RelatedRecordsExist(Exception): pass class SomeModel(MyModel): ... def pre_delete_handler(self): if children.count(): raise RelatedRecordsExist("SomeModel has child records!")
This cancels the deletion before any data changes.
Unfortunately, it is not possible to update the data in the pre_delete signal (for example, to emulate ON DELETE SET NULL ), since the list of deleted objects was already created by Django before sending the signals. Django does this in order to avoid getting stuck in circular links and to prevent unnecessary signal transmission to the object several times.
Ensuring that deletion can be performed is now the responsibility of the calling code. To help with this, each model has a prepare_delete() method that takes care of setting the keys to NULL via self.related_set.clear() or similar:
class MyModel(models.Model): ... def prepare_delete(self): pass
To avoid too much code in my views.py and models.py , the delete() method is overridden by MyModel to call prepare_delete() :
class MyModel(models.Model): ... def delete(self): self.prepare_delete() super(MyModel, self).delete()
This means that any deletions explicitly called through obj.delete() will work as expected, but if the deletion was cascaded from a linked object or performed using queryset.delete() and the call code did not ensure that all links were damaged where necessary, then pre_delete_handler will throw an exception.
And finally, I added a similar post_delete_handler method to models that are called by the post_delete signal and allow the model to clear any other data (for example, deleting files for ImageField s.)
class MyModel(models.Model): ... def post_delete_handler(self): pass def post_delete_handler(sender, instance, **kwargs): if isinstance(instance, MyModel): instance.post_delete_handler() models.signals.post_delete.connect(post_delete_handler)
I hope this helps someone, and that the code can be re-included in something more usable without much trouble.
Any suggestions on how to improve this are more than welcome.