27

I have a Django model that holds settings core to the function of an app. You should never delete this model. I'm trying to enforce this application-wide. I've disabled the delete function in the admin, and also disabled the delete method on the model, but QuerySet has it's own delete method. Example:

MyModel.objects.all()[0].delete() # Overridden, does nothing

MyModel.objects.all().delete() # POOF!

Ironically, the Django docs say has this to say about why delete() is a method on QuerySet and not Manager:

This is a safety mechanism to prevent you from accidentally requesting Entry.objects.delete(), and deleting all the entries.

How having to include .all() is a "safety mechanism" is questionable to say the least. Instead, this effectively creates a backdoor that can't be closed by conventional means (overriding the manager).

Anyone have a clue how to override this method on something as core as QuerySet without monkey-patching the source?

Chris Pratt
  • 232,153
  • 36
  • 385
  • 444

2 Answers2

59

You can override a Manager's default QuerySet by overriding the Manager.get_query_set() method.

Example:

class MyQuerySet(models.query.QuerySet):

    def delete(self):
        pass  # you can throw an exception


class NoDeleteManager(models.Manager):
    def get_query_set(self):
        return MyQuerySet(self.model, using=self._db)

class MyModel(models.Model)
    field1 = ..
    field2 = ..


    objects = NoDeleteManager()

Now, MyModel.objects.all().delete() will do nothing.

For more informations: Modifying initial Manager QuerySets

Community
  • 1
  • 1
manji
  • 47,442
  • 5
  • 96
  • 103
  • Well, that's a big duh on my part. Should have thought of that myself, but thanks for supplementing my aging brain's failings ;). – Chris Pratt Jun 23 '11 at 21:04
  • 9
    Method from django 1.6+ is called `get_queryset(self)`. – Andrei-Niculae Petre Jul 13 '15 at 21:03
  • 3
    Unless I'm mistaken, you can do this a bit cleaner with [QuerySet.as_manager](https://docs.djangoproject.com/en/1.10/topics/db/managers/#creating-a-manager-with-queryset-methods). You could remove the above manager class and just do: `objects = MyQuerySet.as_manager()` – mgalgs Nov 10 '16 at 19:30
  • 1
    As of now (Django 1.11) the "get_queryset" method looks like `def get_queryset(self): return MyQuerySet(model=self.model, using=self._db, hints=self._hints)` – Mark Chackerian Oct 20 '17 at 19:02
11

mixin approach

https://gist.github.com/dnozay/373571d8a276e6b2af1a

use a similar recipe as @manji posted,

class DeactivateQuerySet(models.query.QuerySet):
    '''
    QuerySet whose delete() does not delete items, but instead marks the
    rows as not active, and updates the timestamps
    '''
    def delete(self):
        self.deactivate()

    def deactivate(self):
        deleted = now()
        self.update(active=False, deleted=deleted)

    def active(self):
        return self.filter(active=True)


class DeactivateManager(models.Manager):
    '''
    Manager that returns a DeactivateQuerySet,
    to prevent object deletion.
    '''
    def get_query_set(self):
        return DeactivateQuerySet(self.model, using=self._db)

    def active(self):
        return self.get_query_set().active()

and create a mixin:

class DeactivateMixin(models.Model):
    '''
    abstract class for models whose rows should not be deleted but
    items should be 'deactivated' instead.

    note: needs to be the first abstract class for the default objects
    manager to be replaced on the subclass.
    '''
    active = models.BooleanField(default=True, editable=False, db_index=True)
    deleted = models.DateTimeField(default=None, editable=False, null=True)
    objects = DeactivateManager()

    class Meta:
        abstract = True

other interesting stuff

dnozay
  • 23,846
  • 6
  • 82
  • 104