I want to override related manager for a class.
I have a Company model. It has state column which can be in ACTIVE, INACTIVE, SUSPENDED. I am adding 2 new states called SALES, CLOSED.
Since its legacy model, just adding state can be devastating(there are very many places in code which doesn't filter by state).
So, to avoid inadvertent changes elsewhere, I decided to hide our new states for all other apps/elsewhere unless required otherwise (I'll whitelist only for our app/models)
I've overridden object manager in company.
class CompanyManager(models.Manager):
def get_queryset(self):
return super(CompanyRelatedManager, self).get_queryset().exclude(state__in=['SALES', 'CLOSED'])
class Company(models.Model):
_default_manager = models.Manager()
objects = CompanyManager()
allObjects = models.Manager()
name = models.TextField()
...
salesContact = models.ForeignKey(Contact)
The problem is that Company.objects.filter(blah=blah) filters out new states. But something like salesContact.companies.all() doesn't.
In [9]: salesContact.companies
Out[9]: <django.db.models.fields.related.RelatedManager at 0x12157a990>
My question is how to override the related manager of the salesContact = models.ForeignKey(Contact) and the likes so that I can modify the default queryset to exclude my new state.
And, I can't override the default manager, since overriding default manager also implies that I am overriding the db_manager which results in un-intended consequence (db tries to insert instead of update, whole other story).
Related
I have a custom model defined as following, with get() method override
class CustomQuerySetManager(models.QuerySet):
def get(self, *args, **kwargs):
print('Using custom manager')
# some other logics here...
return super(CustomQuerySetManager, self).get(*args, **kwargs)
class CustomModel(models.Model):
objects = CustomQuerySetManager.as_manager()
class Meta:
abstract = True
Then I have two models defined as
class Company(CustomModel):
name = models.CharField(max_length=40)
class People(CustomModel):
company = models.ForeignKey(Company, on_delete=models.CASCADE)
first_name = models.CharField(max_length=20)
last_name = models.CharField(max_length=20)
If I use get() directly like People.objects.get(pk=1) then it works, "Using custom manager" gets printed, but if I try to get the foreign key info, django still uses the get() method from the default manager, nothing gets printed and the rest of logic defined won't get executed, for example
someone = People.objects.get(id=1) # prints Using custom manager, custom logic executed
company_name = someone.company.name # nothing gets printed, custom logic does not execute
Is the foreign key field in a model using a different manager even though the foreign key model is also using my custom model class? Is there a way to make my custom get() method work for all fields?
As django doc says
By default, Django uses an instance of the Model._base_manager manager
class when accessing related objects (i.e. choice.question), not the
_default_manager on the related object
See more here.
So you have to tell django model which manager to use as base manager, like this:
class CustomModel(models.Model):
objects = CustomQuerySetManager.as_manager()
class Meta:
#django will use your custom "objects" manager as base_manager
#or you may have different managers for base and default managers
#if you define two managers with different names
base_manager_name = 'objects'
abstract = True
But, please, pay attention that you do not filter away any results from base manager. Django doc says:
This manager is used to access objects that are related to from some
other model. In those situations, Django has to be able to see all the
objects for the model it is fetching, so that anything which is
referred to can be retrieved.
Therefore, do not override get_queryset() for this kind of managers.
See more here.
In django, I have the following models:
class System(models.Model):
name = models.CharField(max_length=200)
""" ... many other fields, not useful for here ..."""
# Would it make more sense to have the primary instance here ?
class Instance(models.Model):
name = models.CharField(max_length=200)
url = models.UrlField(max_length=200)
system = models.ForeignKey(System, on_delete=models.PROTECT)
is_production = models.BooleanField()
This data is managed using the admin. What I want is that when an instance of the system is marked as is_production, all other instances, for that system have their is_production field updated to False.
Also, I am interested in how to best setup the admin for this case. I, will be using inlines for the edition/creation of instances.
However, I am not sure about how to make sure each system can only have one instance in production.
Should I use a dropdown on the System to select the production instance and filter using formfield_for_foreignkey?
Use an admin action, something like: Mark as production ?
Use signals after a save ?
is there any other way I have not thought about ?
You asked multiple questions but I'll focus on what I interpreted as the main one:
What I want is that when an instance of the system is marked as is_production, all other instances, for that system have their is_production field updated to False.
How about overriding the Instance model's save method?
class Instance(models.Model):
name = models.CharField(max_length=200)
url = models.URLField(max_length=200)
system = models.ForeignKey(System, on_delete=models.PROTECT)
is_production = models.BooleanField()
def save(self, *args, **kwargs):
if self.is_production:
self.system.instance_set.exclude(id=self.id).update(is_production=False)
super().save(*args, **kwargs)
This ensures that whenever an Instance instance with is_production=True is saved, all other Instance instances that are linked to the related System object will have their is_production values updated to False.
Depending on how you go about changing the Instance instances' is_production values, this might or might not be suitable for what you want to do. See e. g. this thread discussing how using the .update() method doesn't lead to the save() method being called: Django .update doesn't call override save? (also described in the Django docs, referred to in the linked thread)
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()
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()
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.