Cascade delete of model with GenericForeignKey without using GenericRelation - django

I'm creating a reusable django app which includes a model with GenericForeignKey which I need to be cascade deleted.
This model may be attached to any other. I have no control over target model class as it is outside of the app. This means I can not add GenericRelation field to it and can't force user to add it as target might be in another third-party app.
Assuming we have such models (having NO control over Post and PostGroup):
class Tag(models.Model):
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
object = GenericForeignKey()
class PostGroup(models.Model):
title = models.CharField(max_length=255)
class Post(models.Model):
title = models.CharField(max_length=255)
group = models.ForeignKey(PostGroup, on_delete=models.CASCADE)
Is there a way to delete Tag in case of PostGroup queryset is being deleted?
E.g. not only post_group.delete() but also PostGroup.objects.delete().

You can use pre_delete signal to achieve this:
from django.db.models.signals import pre_delete
from django.dispatch import receiver
#receiver(pre_delete) # add relevant sender to signal (not mandatory)
def post_group_deleted(sender, instance, using, **kwargs):
# Query tags with the instance of PostGroup and delete them
if isinstance(instance, Tag):
return
Tag.objects.filter(
content_type=ContentType.objects.get_for_model(instance),
object_id=instance.pk
).delete()
See documentation here
Unlike ForeignKey, GenericForeignKey does not accept an on_delete argument to customize this behavior; if desired, you can avoid the cascade-deletion by not using GenericRelation, and alternate behavior can be provided via the pre_delete signal.

Related

Django why model foreign key cascade will not trigger delete?

there two basic ways to do something when an instance gets deleted:
Overwrite Model.delete
Signal
I used to reckon both of them serve the same purpose, just provides different ways of writing, but works exactly.
However, in this occasion, I realise I was wrong:
class Human(models.Model):
name = models.CharField(max_length=20)
class Pet(models.Model):
name = models.CharField(max_length=20)
owner = models.ForeignKey(Human, related_name="pet", on_delete=models.CASCADE)
def delete(self, *args, **kwargs):
print('------- Pet.delete is called')
return super().delete(*args, **kwargs)
h = Human(name='jason')
h.save()
p = Pet(name="dog", owner=h)
p.save()
h.delete()
# nothing is shown
Why Pet.delete Is not firing at Human.delete By the foreign cascade? Does I have to apply a signal on this? If so, would it cost more performance?
I am building something very heavy, comment system, filter decent records and delete when the commented target get deleted, the comment model has many null-able foreign key fields, with models.CASCADE Set, only one of them is assigned with value. But in product delete view, I call product.delete Then triggers cascade, but comment.delete Is not firing.
Currently, the project has delete Defined on many models, with assumption that it is always triggered when the instance get removed from database, and it is tremendous work to rewrite it in signal. Is there a way to call delete When at cascading? (I know it is likely impossible since it is a database field specification)
I implement a mix-in for Commendable models with extra methods defined, therefore, I decided to modify delete method to signal to something like this:
from django.db import models
from django.dispatch import receiver
from django.db.models.signals import pre_delete
# Create your models here.
class Base:
def __init_subclass__(cls):
#receiver(pre_delete, sender=cls)
def pet_pre_delete1(sender, instance, **kwargs):
print('pet pre delete 1 is called')
#receiver(pre_delete, sender=cls)
def pet_pre_delete2(sender, instance, **kwargs):
print('pet pre delete 2 is called')
class Human(models.Model):
name = models.CharField(max_length=20)
def __str__(self):
return f'<human>{self.name}'
class Pet(Base, models.Model):
name = models.CharField(max_length=20)
owner = models.ForeignKey(Human, related_name="pet", on_delete=models.CASCADE)
def __str__(self):
return f'<pet>{self.name}'
# ------- Pet.delete is called
# pet pre delete 1 is called
# pet pre delete 2 is called
it works fine in testing, I wonder if there is any risk using this, would it be garbage collected?

Django - Simplify Proxy Model to single class

I have User table in my DB, they can be active or inactive. If I only want to query on active user, I define a Proxy Model like following.
class User(models.Model):
name = models.CharField(max_length=50)
location = models.CharField(max_length=50)
active = models.BooleanField()
class UserActive(models.Manager):
def get_queryset(self):
return super(UserActive, self).get_queryset().filter(active=True)
class ActiveUser(User):
objects = UserActive()
class Meta:
proxy = True
Then by working with ActiveUser, I can do my calculation/statistic with only active user.
The problem is, I need to define both UserActive and ActiveUser class, it seems awkward to me. Because with each main class (in this case is User), we need to define two other classes. Imaging we have several other model need to implement Proxy, the code would look messy. May I know if we can have more elegant way ?
Thanks
Alex
I would really avoid overwriting the .objects manager, and use this as some sort of implicit filtering. The Zen of Python is explicit is better than implicit, by using ActiveUser, you basically implement a filtering manager, but propose it like the entire set.
Perhaps a more elegant solution is to define multiple managers. So we can construct a filtering manager decorator:
def filter_manager(**kwargs):
def decorator(klass):
def get_queryset(self):
return super(klass, self).get_queryset().filter(**kwargs)
klass.get_queryset = get_queryset
return klass
return decorator
This decorator will however throw away a get_queryset that is defined on the manager itself, so you can not perform an extra patch with this.
Now we can define some managers in a rather elegant way:
#filter_manager(active=True)
class ActiveManager(models.Manager):
pass
#filter_manager(active=False)
class InactiveManager(models.Manager):
pass
Finally we can add these managers to the User model, and use explicit names:
class User(models.Model):
name = models.CharField(max_length=50)
location = models.CharField(max_length=50)
active = models.BooleanField()
objects = models.Manager()
active_users = ActiveManager()
inactive_users = InactiveManager()
So now we can use User.active_users to query for the active users. We thus have no proxy models, and can query with User.active_users.count() for example (well we can perform all operations like with .objects but then for .active_users.
I created a new Django project, with only User model. My models.py look like this
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models
# Create your models here.
def filter_manager(**kwargs):
def decorator(klass):
def get_queryset(self):
return super(klass, self).get_queryset().filter(**kwargs)
klass.get_queryset = get_queryset
return klass
return decorator
#filter_manager(active=True)
class ActiveManager(models.Manager):
pass
#filter_manager(active=False)
class InactiveManager(models.Manager):
pass
class User(models.Model):
name = models.CharField(max_length=50)
location = models.CharField(max_length=50)
active = models.BooleanField()
active_user = ActiveManager()
When I tried User.objects.all().
Error: type object 'User' has no attribute 'objects'

get_profile() fails randomly in Django project

In my Django project I have a user authentication system. Each user has a userprofile:
# Extending main user profile
class UserProfile(models.Model):
# Required
user = models.OneToOneField(User)
# Added fields to main user model
position = models.CharField(max_length=20, null=True, blank=True)
avatar = models.ImageField(upload_to=upload_path_handler, blank=True, default='images/avatar.png')
class Meta:
app_label = 'auth'
# handler -- Create automatically UserProfile foreign key when
# a new user is registered.
def create_user_profile(sender, instance, created, **kwargs):
if created:
# Creating UserProfile
UserProfile.objects.create(user=instance)
post_save.connect(create_user_profile, sender=User)
Well, I have a context_processor.py which its main function returns something like this:
return {'USER_MEDIA_URL': settings.USER_MEDIA_URL,
'DEBUG': settings.DEBUG,
'SITE_URL': settings.SITE_URL,
'keys_left': keys_left,
'ownRoom': c,
'userThumbnail': request.user.get_profile().avatar,}
All my templates use "userThumbnail" and "request.user.get_profile().avatar" fails randomly without any kind of explication.
The returned error in template is:
Unable to load the profile model, check AUTH_PROFILE_MODULE in your project settings
Sometimes I get this error and sometimes not. It's very annoying.
Any hint?
It's very annoying.
:)
If you don't need to perform other extra tasks on post_save except of creating a profile, maybe you could use AutoOneToOneField from django-annoying instead?
The method get_profile() does not create the profile, if it does not exist.
(django docs)
AutoOneToOneField does.
It's not really an answer, but it may turn out to be a solution for you.
from annoying.fields import AutoOneToOneField
class UserProfile(models.Model):
user = AutoOneToOneField(User, verbose_name=_(u"user"),
on_delete=models.CASCADE,
related_name="profile")
Then you use it with something like that:
return { 'userThumbnail': request.user.profile.avatar, }
Add the following to your settings.py
AUTH_PROFILE_MODULE = 'appname.UserProfile'
That should solve the issue.

ForeignKey to multiple Models or Queryset

It is possible to make a ForeignKey to more than one model. I want to choose from different models like Parts and Machines Model.
I read this to combine multiple models into one list: How to combine 2 or more querysets in a Django view?
How can I get foreign key to that list somehow?
I know that you asked this over year ago, but I had a similar problem and I want to share a link to the solution for future readers.
Generally the contenttypes framework solves this problem, and I guess this is what Daniel Roseman was talking about.
How to use dynamic foreignkey in Django?
You need generic relations.
A generic relation allows you to dynamically the target model of the foreign key.
I'll provide a comprehensive answer for this question, I know its quite old, but it's still relevant.
We're gonna be using Generic Relations.
First, in settings.py make sure that django.contrib.contenttypes is included in the INSTALLED_APPS array.
Let's create a new model in models.py:
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
With content_type we can associate Image with any other model class, while object_id will hold the other model instance.
class Image(models.Model):
image = models.ImageField(
upload_to="imgs/products", blank=True)
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey()
To refer back to the Image model from a Company instance we need to make a reverse generic relation
class Company(models.Model):
name = models.CharField(max_length=100)
images = GenericRelation(Image)
In schema.py, we can create Images in a Company instance like:
company_instance = Company(name="Apple")
company_instance.save()
for img in imgs:
#Image(image=img, content_object=company_instance)
company_instance.images.create(image=img)
company_instance.images.all() # fetch all images
the company_instance.images field is just a GenericRelatedObjectManager (docs)
This is how the final Image table looks in the database:
The Django-polymorphic library provides a simple solution that should work well with the admin and forms too using formsets.
For example:
from polymorphic.models import PolymorphicModel
class BookOwner(PolymorphicModel):
book = models.ForeignKey(Book, on_delete=models.CASCADE)
class StaffBookOwner(BookOwner):
owner = models.ForeignKey(Staff, on_delete=models.CASCADE)
class StudentBookOwner(BookOwner):
owner = models.ForeignKey(Student, on_delete=models.CASCADE)
With this, you can use the parent model to set the owner to either a Staff or Student instance or use the child models directly.

Define an attribute in a model, like an object of the other model in Django

Is posible to define an attribute in a data model like an object of other data model in Django?
This is the scenary:
models.py
class Inmueble(models.Model):
calle = models.CharField(max_length=20, verbose_name="Calle")
numero = models.CharField(max_length=6, verbose_name="Numero")
piso = models.IntegerField(verbose_name="Piso", blank=True, null=True)
galeria_id = models.OneToOneField(Galeria, verbose_name="Galería del Inmueble")
class Galeria(Gallery):
nombre = models.CharField(max_length=30, verbose_name="Nombre")
The point is: I need to create a new Galeria object automatically every time an Inmueble object is created. Thanks in advance!
Analía.
There are two ways to handle this:
Override the save() method for the Inmueble model.
Create a signal handler on Galeria that receives signals emitted by Inmueble
Both methods would work and are acceptable, however I recommend using a signal for a couple reasons:
It's a bit more de-coupled. If later you change or remove Galeria, your code doesn't break
The signal handler for postsave includes a boolean value to indicate whether the model is being created or not. You could technically implement the same functionality in model save() by checking if the model has a .id set or not, but IMO the signal is a cleaner solution.
Here's an idea of the code for both of these...
Using a Signal (recommended)
from django.db.models.signals import post_save
from wherever.models import Inmueble
class Galeria(Gallery):
# ...
def inmueble_postsave(sender, instance, created, **kwargs):
if created:
instance.galeria_id = Galeria.objects.create()
instance.save()
post_save.connect(inmueble_postsave, sender=Inmueble, dispatch_uid='galeria.inmueble_postsave')
Overriding Model save() Method
from wherever.models import Galeria
class Inmueble(models.Model):
# ...
def save(self, force_insert=False, force_update=False):
# No Id = newly created model
if not self.id:
self.galeria_id = Galeria.objects.create()
super(Inmueble, self).save()
Maybe
AutoOneToOneField is the answer.
Finally, I did:
from django.db.models.signals import post_save
class Galeria(Gallery):
inmueble_id = models.ForeignKey(Inmueble)
def inmueble_postsave(sender, instance, created, **kwargs):
if created:
instance = Galeria.objects.create(inmueble_id=instance, title=instance.calle+' '+instance.numero, title_slug=instance.calle+' '+instance.numero)
instance.save()
post_save.connect(inmueble_postsave, sender=Inmueble, dispatch_uid='galeria.inmueble_postsave')