Django why model foreign key cascade will not trigger delete? - django

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?

Related

Using signals for two models both inheriting from User in Django

Suppose we have two models that have signal to the User model:
from django.db import models
from django.contrib.auth.models import User
from django.db.models import signals
class Company(User):
name = models.CharField(null=True, blank=True, max_length=30)
if created:
Company.objects.create(
user_ptr_id=instance.id,
username=instance.username,
password=instance.password,
email=instance.email,
first_name=instance.first_name,
last_name=instance.last_name,
is_active=instance.is_active,
is_superuser=instance.is_superuser,
is_staff=instance.is_staff,
date_joined=instance.date_joined,
)
signals.post_save.connect(
create_company, sender=User, weak=False, dispatch_uid="create_companies"
)
class Individual(User):
name = models.CharField(null=True, blank=True, max_length=30)
def create_job_seeker(sender, instance, created, **kwargs):
"""
:param instance: Current context User instance
:param created: Boolean value for User creation
:param kwargs: Any
:return: New Seeker instance
"""
if created:
'''
we should add a condition on whether the Company uses the same username
if true, then, we must not create a JobSeeker and we would disable the account using
Firebase Admin
'''
JobSeeker.objects.create(
user_ptr_id=instance.id,
username=instance.username,
password=instance.password,
email=instance.email,
first_name=instance.first_name,
last_name=instance.last_name,
is_active=instance.is_active,
is_superuser=instance.is_superuser,
is_staff=instance.is_staff,
date_joined=instance.date_joined,
)
signals.post_save.connect(
create_job_seeker, sender=User, weak=False, dispatch_uid="create_job_seekers"
)
Now, each time a User is created we should be allowed to extend it through both Individual and Company models. But, I want to prohibit the usage of both objects. User can either have a Company or an Individual object to be edited not both. Should I override the save method such as this:
def save(self, *args, **kwargs):
if not Company.objects.filter(username=self.username).exists():
super(Model, self).save(*args, **kwargs)
else:
raise 'Some error'
Or should I add a condition on the created method such as this:
...
if created and Company.objects.filter(username, self.username).exists() == False:
Company.objects.create(
...
Which approach is better? And is there another approach that you might suggest?
Signals, for most cases, I believe are the best way to handle sharing data between models assuming the CRUD for each related model isn't done together. So post_save, pre_save, post_delete, pre_delete and so are typically the best way to go about handling data that any given model instance relies on. This can be true about manipulating model data after a save. Signals were designed specifically for this reason. The other great thing about signals is you can connect them throughout your project and not necessarily just where the Model is defined. Just import the model and the signal you want to connect to it and bam!
How to use signals? follow django's documentation here. it's very simple
https://docs.djangoproject.com/en/4.0/topics/signals/

Is the post_save signal in Django atomic?

I really can't find anything solid in the docs. Lets say I'm doing something like this:
from django.db.models.signals import post_save
from django.dispatch import receiver
class Item(models.Model):
total_score = models.IntegerField()
def set_score(self):
...
class Review(models.Model):
item = models.ForeignKey(Item, on_delete=models.CASCADE)
score = models.IntegerField()
#receiver(post_save, sender=Review)
def my_handler(sender, **kwargs):
sender.item.set_score()
What I'm trying to do is call set_score() for the item object, whenever a review object is saved. Is this atomic? I definitely want the whole thing to be atomic, as a situation where a review is saved, but the item's total score is not updated is a recipe for bugs.
No, there's nothing special about signals with regard to database transactions (the only kind of atomicity handled by Django). It's up to you to ensure that the relevant commands are always part of the same database transaction.
One approach would be to simply rely on the calling code to do this by using ATOMIC_REQUESTS, using transactions in your views, etc.
Or, since post_save signals are sent as part of Model.save(), you could simply override Review.save() and make it use a transaction.
class Review(models.Model):
...
#transaction.atomic()
def save(self, *args, **kwargs):
super().save(*args, **kwargs)

Django model validate a ManyToMany field before adding

I have a model that looks somewhat like this:
class Passenger(models.Model):
name = models.CharField(max_length=50)
surname = models.CharField(max_length=50)
class Flight(models.Model):
capacity = models.IntegerField()
passengers = models.ManyToManyField(Passenger)
Before adding a new passenger to the flight I would like to validate whether the number of passengers is not going to exceed the capacity. I was wondering what would be the best way to go about this.
Obviously I could manually check the number of passengers before adding a new one, but maybe there is some support in django? I tried writing a validator, but wasn't sure how to do it.
According to Django docs you can listen to the m2m_changed signal, which will trigger pre_add and post_add actions.
Using add() with a many-to-many relationship, however, will not call
any save() methods (the bulk argument doesn’t exist), but rather
create the relationships using QuerySet.bulk_create(). If you need to
execute some custom logic when a relationship is created, listen to
the m2m_changed signal, which will trigger pre_add and post_add
actions.
According to #M.Void answer – Code Example:
from django.db import models
from django.db.models.signals import m2m_changed
from django.core.exceptions import ValidationError
class MyModel(models.Model):
m2mField = models.ManyToManyField('self')
m2mFieldLimit = 2
def m2mField_changed(sender,**kwargs):
instance = kwargs['instance']
if len(instance.m2mField.all()) >= instance.m2mFieldLimit :
raise ValidationError(f'Max number of records is {instance.m2mFieldLimi}')
m2m_changed.connect(commonobjects_changed,sender=MyModel.m2mField.through)
Override the clean method on the model to do the check you want:
class Passenger(models.Model):
name = models.CharField(max_length=50)
surname = models.CharField(max_length=50)
def clean(self, *args, **kwargs):
# clean gets called automatically by other things, so we can't always
# expect flight_id to be provided
if 'flight_id' in kwargs:
flight = Flight.objects.get(pk=kwargs['flight_id'])
if flight.passengers.all().count() >= flight.capacity:
# flight is full!
raise ValidationError
super(Passenger, self).clean()
class Flight(models.Model):
capacity = models.IntegerField()
passengers = models.ManyToManyField(Passenger)
Note that to do this, you will have to pass in the flight ID when validating the passenger:
f = Flight.objects.get(...)
p = Passenger(name='First', surname='Last')
try:
p.clean(flight_id=f.id) # full_clean calls clean, among other validations
p.save()
except ValidationError as e:
# do something to handle the error
Note that it is possible in multi-threaded applications for something to get validated successfully, but still fail to save in a race condition. You would need to add additional code to handle that.
See here for details on model validation.

Save the user from one model to the another model

What I want to do is, whenever I create a new message, I want the sender of the Message to be added to the user of that particular Thread (the Message its relating to).
How do I do that? Can it be done by overriding the save method? I can do it in the views.py, but I was hoping it would be better if I can add it in the models.py itself. Any help will be very grateful. Thank you!
class Thread(models.Model):
user = models.ManyToManyField(User)
is_hidden = models.ManyToManyField(User, related_name='hidden_thread', blank=True)
def __unicode__(self):
return unicode(self.id)
class Message(models.Model):
thread = models.ForeignKey(Thread)
sent_date = models.DateTimeField(default=datetime.now)
sender = models.ForeignKey(User)
body = models.TextField()
is_hidden = models.ManyToManyField(User, related_name='hidden_message', blank=True)
def __unicode__(self):
return "%s - %s" % (unicode(self.thread.id), self.body)
You could lookup the reverse foreign key and get all the users for a particular thread without having to manually put it in Thread
Then you can get users associated with a thread by the reverse lookup:
User.objects.filter(message__thread=thread)
If you don't want to actively pull the user set as dm03514 showed, such as if you want to add users to the thread by default but maintain the ability to remove them from the thread many-to-many later, you can indeed do this by overriding the save method or by using a post_save signal.
save is good enough for almost all cases - the advantage of post_save is that it can more reliably distinguish between saving a new message and saving edits to an existing message. But if you're not creating messages with preselected PKs or loading them from fixtures save can work fine:
class Message(models.Model):
def save(self, *args, **kwargs):
probably_new = (self.pk is None)
super(Message, self).save(*args, **kwargs)
if probably_new:
self.thread.user.add(self.sender)
A signal would look like this:
from django.db.models.signals import post_save
def update_thread_users(sender, **kwargs):
created = kwargs['created']
raw = kwargs['raw']
if created and not raw:
instance = kwargs['instance']
instance.thread.user.add(instance.sender)
post_save.connect(update_thread_users, sender=Message)
And then review the docs on preventing duplicate signals in case of multiple imports:
https://docs.djangoproject.com/en/dev/topics/signals/#preventing-duplicate-signals

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')