Using a Non-Default Manager with GenericForeignKey() - django

I have altered the default manager on some of the objects which a GenericForeignKey() can reference such that those objects may no longer appear within that default manager.
I have other managers which will be able to find these deleted objects, but I see no way to tell the content types framework about them. Is this possible?
I am implementing 'soft deletion' with some models which involves the following managers:
from django.db import models
SDManager(models.Manager):
def get_query_set(self):
return super(SDManager, self).get_query_set().filter(is_deleted=False)
SDDeletedManager(models.Manager):
def get_query_set(self):
return super(SDDeletedManager, self).get_query_set().filter(is_deleted=True)
This allows me to do the following:
SDModel(models.Model):
# ...
objects = SDManager() # Only non (soft) deleted objects
all_objects = models.Manager() # The default manager
deleted_objects = SDDeletedManager() # Only (soft) deleted objects
When using a GenericForeignKey() field in a model to reference an object defined such as SDModel, it uses the _default_manager attribute which evaluates to the objects manager, to get the reference. This means it looses references when objects are soft deleted.
This was one of the main reasons I was using GenericForeignKey() fields. A solution I have been milling over is implementing a lesser version of the content types framework, so that I can define my own get_object() which uses the all_objects manager to access the references object.
So my question really is:
Is it possible to use a non-default manager with the existing content types framework so that it finds the soft deleted objects, or will I have to re implement all the parts I need from scratch?

I have the exact same issue as you, and after diving into the documentation/source it looks like Django does not provide an out of the box way to do this. The simplest method I found was to subclass GenericForeignKey and then override the __get__ method.
The troublesome line is where it calls:
rel_obj = ct.get_object_for_this_type(pk=getattr(instance, self.fk_field))
So this line needs to be rewritten as:
rel_obj = ct.model_class().all_objects.get(pk=getattr(instance, self.fk_field))
It's a little bit hackish but it works, and you then get to use the full power of GenericForeignKey like you usually would.

Related

What is the difference between creating a model instance using the default Manager and Customer Manager

While going through the official Django documentation I came across the Model Instance reference section in which it is mentioned that you can create an instance of the model using the custom Model using self.create. I wanted to know what's the difference between using the create method and the custom create_method when both are using the same fields and in both the cases the data is being saved in the DB.
Documentation:
https://docs.djangoproject.com/en/2.2/ref/models/instances/#creating-objects
class BookManager(models.Manager):
def create_book(self, title):
book = self.create(title=title)
return book
class Book(models.Model):
title = models.CharField(max_length=100)
objects = BookManager()
book = Book.objects.create_book("Pride and Prejudice")
Difference between these two
book2 = Book.objects.create(title="Pride and Prejudice")
Well in this simplest case, there is no difference. The reason of describing this technique in docs is obvious there
You may be tempted to customize the model by overriding the __init__
method. If you do so, however, take care not to change the calling
signature as any change may prevent the model instance from being
saved. Rather than overriding __init__, try using one of these
approaches:
It means you may be want to set some extra/default values to model instance. If you override constructor for this purpose, it is a little unsafe (and not usually a practice in django). That's why two other techniques for doing this are described. You are mentioning one of them. You can do some extra stuff in custom manager method if you want
class BookManager(models.Manager):
def create_book(self, title):
# you can do some extra stuff here for instance creation
book = self.create(title=title)
# or here when it is saved to db
return book
Otherwise there is no difference.

How to modify Django's Manager QuerySet create() method?

I have two models, Invoice and InvoiceItems, which have a one-to-many relationship.
Throughout the code base we're creating InvoiceItems for a given Invoice using the Manager object as:
invoice.invoice_items.create(...)
The thing is, now we have a validation that has to take place before trying to create an InvoiceItem, and going through the codebase, refactoring all the creation pieces would be a headache.
I wonder if there's a way to override the create method itself or should we go for the model's save()?
To modify a Manager's method you need to create your own. Given the following case:
# models
class MyModel(models.Model):
# ... fields
objects = MyManager()
class MyManager(models.Manager):
def create(self):
# write your own code here
pass
Do not worry about the others methods (filter, delete, etc.) all of them will work as usual.
You can find more about custom managers here

Django - call Model save() without using "objects" Manager get_queryset

I'm developing a legacy Django 1.7 system for a client. The programmers before me overrode the Member model (basically the User model) "objects" property with a filter query that removes anything with "is_deleted" set to "True". I've listed the snippets below:
Member class snippet:
class Member(AbstractUser):
objects = MemberManager()
all_objects = models.Manager()
MemberManager class snippet:
class MemberManager(BaseUserManager):
def get_queryset(self):
return super(MemberManager, self).get_queryset().filter(is_deleted=False)
Now when I try to update a user that has the is_deleted flag set to "True" it fails. Below is an example code snippet. Notice how I use "all_objects" which is the default models.Manager() that returns all records.
user = Member.all_objects.get(pk=id) # id of an is_deleted = True record
user.is_deleted = False
user.save()
This code causes this Django query to run which unfortunately has "is_deleted = 0" included in the WHERE clause, which causes it to not find the record. Below is what shows up in the logs:
UPDATE Member [[snip...]] WHERE (Member.is_deleted = 0 AND Member.id = 6)
Is there any way to call "save()" that will not use the MemberManager.objects get_queryset filter?
I think the problem stems from having MemberManager listed first. As the documentation says:
Take note that the first Manager Django encounters (in the order in which they’re defined in the model) has a special status. Django interprets the first Manager defined in a class as the "default" Manager, and several parts of Django will use that Manager exclusively for that model. As a result, it’s a good idea to be careful in your choice of default manager in order to avoid a situation where overriding get_queryset() results in an inability to retrieve objects you’d like to work with.
Reversing the order of objects and all_objects should fix the problem.
I tried Kevin Christopher Henry's answer and unfortunately changing the default manager messed up the authentication code, which needed to inherit from the "BaseUserManager" class. The solution was to create an "undelete" function in the Member model class that uses the "all_objects" property to undelete the user before it's saved.
class Member(AbstractUser):
objects = MemberManager() # default manager
all_objects = models.Manager()
def undelete(self):
if self.is_deleted:
Member.all_objects.filter(id=self.id).update(is_deleted=False)
Then in my code I did this:
user = Member.all_objects.get(pk=id)
user.undelete()

Django - Cascade deletion in ManyToManyRelation

Using the following related models (one blog entry can have multiple revisions):
class BlogEntryRevision(models.Model):
revisionNumber = models.IntegerField()
title = models.CharField(max_length = 120)
text = models.TextField()
[...]
class BlogEntry(models.Model):
revisions = models.ManyToManyField(BlogEntryRevision)
[...]
How can I tell Django to delete all related BlogEntryRevisions when the corresponding BlogEntry is deleted? The default seems to be to keep objects in a many-to-many relation if an object of the "other" side is deleted. Any way to do this - preferably without overriding BlogEntry.delete?
I think you are misunderstanding the nature of a ManyToMany relationship. You talk about "the corresponding BlogEntry" being deleted. But the whole point of a ManyToMany is that each BlogEntryRevision has multiple BlogEntries related to it. (And, of course, each BlogEntry has multiple BlogEntryRevisions, but you know that already.)
From the names you have used, and the fact that you want this deletion cascade functionality, I think you would be better off with a standard ForeignKey from BlogEntryRevision to BlogEntry. As long as you don't set null=True on that ForeignKey, deletions will cascade - when the BlogEntry is deleted, all Revisions will be too.
As Of Django 2.0
The ForeignKey initializer now requires you to specify the on_delete parameter:
from django.db import models
from .models import MyRelatedModel
class model(models.Model):
related_model = models.ForeignKey(MyRelatedModel, on_delete=models.CASCADE)
I had this exact use-case today:
Model Author: can have several entries
Model Entry: can have several authors
For this, I'm using a ManyToManyRelationship.
My use-case was: if I delete the last entry of a particular author, then this author should be deleted as well.
The solution can be achieved using the pre_delete signal:
#receiver(pre_delete, sender=Entry)
def pre_delete_story(sender, instance, **kwargs):
for author in instance.authors.all():
if author.entries.count() == 1 and instance in author.entries.all():
# instance is the only Entry authored by this Author, so delete it
author.delete()
Simply use the clear() method to remove related objects since Django uses a through model to specify the relationships the clear method removes all related BlogEntryRevision
be = BlogEntry.objects.get(id=1)
be.blogentryrevision_set.clear()
You can use a custom model manager, but the documentation seems to indicate that it does do something like this already and I can't recall exactly what this means:
The delete method, conveniently, is
named delete(). This method
immediately deletes the object and has
no return value. Example:
e.delete()
You can also delete objects
in bulk. Every QuerySet has a delete()
method, which deletes all members of
that QuerySet.
For example, this deletes all Entry
objects with a pub_date year of 2005:
Entry.objects.filter(pub_date__year=2005).delete()
Keep in mind that this will, whenever
possible, be executed purely in SQL,
and so the delete() methods of
individual object instances will not
necessarily be called during the
process. If you've provided a custom
delete() method on a model class and
want to ensure that it is called, you
will need to "manually" delete
instances of that model (e.g., by
iterating over a QuerySet and calling
delete() on each object individually)
rather than using the bulk delete()
method of a QuerySet.
When Django deletes an object, it
emulates the behavior of the SQL
constraint ON DELETE CASCADE -- in
other words, any objects which had
foreign keys pointing at the object to
be deleted will be deleted along with
it. For example:
b = Blog.objects.get(pk=1)
# This will delete the Blog and all of its Entry objects.
b.delete()

Same Table Django ORM Soft Delete Method Okay?

I'm using the following setup to implement soft deletes in Django. I'm not very familiar with Django under the hood so I'd appreciate any feedback on gotchas I might encounter. I'm particular uncomfortable subclassing a QuerySet.
The basic idea is that the first call to delete on a MyModel changes MyModel's date_deleted to the current datetime. A second delete will actually delete the object. (Catching a delete requires two overrides, one on the object and one on the QuerySet, which can bypass an object's delete method.) Since the default manager will hide deleted objects, deleted objects disappear and must be explicitly requested via the deleted_objects manager.
Using this setup requires defining DeletionQuerySet and DeletionManager and adding date_deleted, objects, and deleted_objects to your model(s).
Thanks,
P.S., forgot to mention that this method of filtering objects out of the default manager is strongly discouraged!
class DeletionQuerySet(models.query.QuerySet):
def delete(self):
prev_deleted = self.filter(date_deleted__isnull=False)
prev_deleted.actual_delete()
prev_undeleted = self.filter(date_deleted__isnull=True)
prev_undeleted.update(date_deleted=datetime.datetime.now())
def actual_delete(self):
super(DeletionQuerySet, self).delete()
class DeletionManager(models.manager.Manager):
# setting use_for_related_fields to True for a default manager ensures
# that this manager will be used for chained lookups, a la double underscore,
# and therefore that deleted Entities won't popup unexpectedly.
use_for_related_fields = True
def __init__(self, hide_deleted=False, hide_undeleted=False):
super(DeletionManager, self).__init__()
self.hide_deleted = hide_deleted
self.hide_undeleted = hide_undeleted
def get_query_set(self):
qs = DeletionQuerySet(self.model)
if self.hide_deleted:
qs = qs.filter(date_deleted__isnull=True)
if self.hide_undeleted:
qs = qs.filter(date_deleted__isnull=False)
return qs
class MyModel(models.Model):
# Your fields here...
date_deleted = models.DateTimeField(null=True)
#the first manager defined in a Model will be the Model's default manager
objects = DeletionManager(hide_deleted=True)
deleted_objects = DeletionManager(hide_undeleted=True)
def delete(self):
if self.date_deleted is None:
self.date_deleted = datetime.datetime.now()
self.save()
else:
super(Agreement, self).delete()
I think anything with current in use, popular, technologies, there is no way to have problem domain agnostic, generic soft deletes.
I think it is more linked to historical/history oriented database systems than to what we are used.
I recommend you to not circumvent django's delete (which is a hard delete). Keep as is.
The "delete" that you most likely will have in our system, is in 90% of the case, a visual delete ...
In this regard, try to find synonyms with delete for your specific domain problem and do this from the start of the project.
Because complain that a IsVisible, IsUnpublished (even IsDeleted) mess up your queries, they complain that you must always be careful to include them...
But this is obviously ignorance of the domain problem, if the domain has objects that can be made invisible, or become unpublished - of course when you query the list of all the objects you want to display, you should FROM THE START, QUERY ALL THE OBJECTS that are not visible and unpublished because this is how your domain problem is solved in a complete form.
Cheers.