I have a setup like this (simplified for this question):
class Employee(models.Model):
name = models.CharField(name, unique=True)
class Project(models.Model):
name = models.CharField(name, unique=True)
employees = models.ManyToManyField(Employee)
When an Employee
is about to get deleted, I want to check whether or not he is connected to any projects. If so, deletion should be impossible.
I know about signals and how to work them. I can connect to the pre_delete
signal, and make it throw an exception like ValidationError
. This prevents deletion but it is not handled gracefully by forms and such.
This seems like a situation that other will have run into. I'm hoping someone can point out a more elegant solution.
I was looking for an answer to this problem, was not able to find a good one, which would work for both models.Model.delete() and QuerySet.delete(). I went along and, sort of, implementing Steve K's solution. I used this solution to make sure an object (Employee in this example) can't be deleted from the database, in either way, but is set to inactive.
It's a late answer.. just for the sake of other people looking I'm putting my solution here.
Here is the code:
class CustomQuerySet(QuerySet):
def delete(self):
self.update(active=False)
class ActiveManager(models.Manager):
def active(self):
return self.model.objects.filter(active=True)
def get_queryset(self):
return CustomQuerySet(self.model, using=self._db)
class Employee(models.Model):
name = models.CharField(name, unique=True)
active = models.BooleanField(default=True, editable=False)
objects = ActiveManager()
def delete(self):
self.active = False
self.save()
Usage:
Employee.objects.active() # use it just like you would .all()
or in the admin:
class Employee(admin.ModelAdmin):
def queryset(self, request):
return super(Employee, self).queryset(request).filter(active=True)
For those referencing this questions with the same issue with a ForeignKey
relationship the correct answer would be to use Djago's on_delete=models.PROTECT
field on the ForeignKey
relationship. This will prevent deletion of any object that has foreign key links to it. This will NOT work for for ManyToManyField
relationships (as discussed in this question), but will work great for ForeignKey
fields.
So if the models were like this, this would work to prevent the deletion of
any Employee
object that has one or more Project
object(s) associated with it:
class Employee(models.Model):
name = models.CharField(name, unique=True)
class Project(models.Model):
name = models.CharField(name, unique=True)
employees = models.ForeignKey(Employee, on_delete=models.PROTECT)
Documentation can be found HERE.
This would wrap up solution from the implementation in my app. Some code is form LWN's answer.
There are 4 situations that your data get deleted:
delete()
on Model instance: project.delete()
delete()
on QuerySet innstance: Project.objects.all().delete()
While there is nothing much you can do with the first case, the other three can be fine grained controlled.
One advise is that, in most case, you should never delete the data itself, because those data reflect the history and usage of our application. Setting on active
Boolean field is prefered instead.
To prevent delete()
on Model instance, subclass delete()
in your Model declaration:
def delete(self):
self.active = False
self.save(update_fields=('active',))
While delete()
on QuerySet instance needs a little setup with a custom object manager as in LWN's answer.
Wrap this up to a reusable implementation:
class ActiveQuerySet(models.QuerySet):
def delete(self):
self.save(update_fields=('active',))
class ActiveManager(models.Manager):
def active(self):
return self.model.objects.filter(active=True)
def get_queryset(self):
return ActiveQuerySet(self.model, using=self._db)
class ActiveModel(models.Model):
""" Use `active` state of model instead of delete it
"""
active = models.BooleanField(default=True, editable=False)
class Meta:
abstract = True
def delete(self):
self.active = False
self.save()
objects = ActiveManager()
Usage, just subclass ActiveModel
class:
class Project(ActiveModel):
...
Still our object can still be deleted if any one of its ForeignKey fields get deleted:
class Employee(models.Model):
name = models.CharField(name, unique=True)
class Project(models.Model):
name = models.CharField(name, unique=True)
manager = purchaser = models.ForeignKey(
Employee, related_name='project_as_manager')
>>> manager.delete() # this would cause `project` deleted as well
This can be prevented by adding on_delete argument of Model field:
class Project(models.Model):
name = models.CharField(name, unique=True)
manager = purchaser = models.ForeignKey(
Employee, related_name='project_as_manager',
on_delete=models.PROTECT)
Default of on_delete
is CASCADE
which will cause your instance deleted, by using PROTECT
instead which will raise a ProtectedError
(a subclass of IntegrityError
). Another purpose of this is that the ForeignKey of data should be kept as a reference.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With