Is the post_save signal in Django atomic? - django

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)

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?

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/

Add one object to M2M fields of many objects in a bulk transaction

I've got the following post_save signal.
#receiver(post_save, sender=Questionnaire)
def add_new_eligible_users_to_questionnaire(sender, instance, created, **kwargs):
if created:
if instance.open_to_all:
users = Respondent.objects.filter(organization=instance.organization)
users.update(eligible_for=instance)
The idea is that once the survey is created, if it is open to everyone, it will automatically be added to their eligible_for row.
Unfortunately, update() doesn't work on M2M relationships. Is there any way I can do this in one fell swoop?
Given the following model:
class Respondent(models.Model):
user = models.OneToOneField('Home.ListenUser', on_delete=models.CASCADE)
eligible_for = models.ManyToManyField('Questionnaire', related_name='eligible_users', null=True)
I used this to accomplish it:
#receiver(post_save, sender=Questionnaire)
def add_new_eligible_users_to_questionnaire(sender, instance, created, **kwargs):
if created:
if instance.open_to_all:
users = Respondent.objects.filter(organization=instance.organization)
instance.eligible_users.set(users)

Limit the number of rows in a Django table

I have a table in my models file and I want to design it such that there is a limit to ten rows in the table. When the limit is exceeded the oldest row will be deleted. For some context this is for a display on the front end that shows a user the ten most recent links they have accessed. I am new to Django so if anyone had a suggestion on how to do this, it would be greatly appreciated!
You could write a custom save method that checks the length of YourObject.objects.all(), and then deletes the oldest one when that length is equal to 10.
Something along the line of:
def save(self, *args, **kwargs):
if YourModel.objects.count() == 10:
objects[0].delete()
super(YourModel, self).save(*args, **kwargs)
In my opinion, you can use Signals. A post_save in this case. This way, you keep the object creation and deletion logic separate.
Since you want the oldest to be deleted, I am assuming you have a created field in the model.
Once you save,
def my_handler(sender, instance, **kwargs):
qs = MyModel.objects.order_by('created') #ensure ordering.
if qs.count() > 10:
qs[0].delete() #remove the oldest element
class MyModel(models.Model):
title = models.CharField('title', max_length=200)
created = models.DateTimeField(auto_add_now=True, editable=False)
post_save.connect(my_handler, sender=MyModel)
Of course, nothing stops you from using the pre_save signal, but use it only if you are absolutely sure the save method wont fail.
Improving a bit on #karthikr answer.
I also believe a post-save signal handler is the best way, but isolating the function in a class method and checking for the created flag. If for example you want to keep a log of the last 1000 emails your system has sent:
from django.db import models
from django.db.models.signals import post_save
LOG_SIZE=1000
class EmailLog(models.Model):
"""Keeps a log of the 1000 most recent sent emails."""
sent_at = models.DateTimeField(auto_add_now=True)
message = models.TextField()
to_addr = models.EmailField()
#classmethod
def post_create(
cls,
sender,
instance: "EmailLog",
created: bool,
*args,
**kwargs
):
if created: # Indicates if it's a new object
qset = EmailLog.objects.order_by("sent_at") # Force ordering
if qset.count() > LOG_SIZE:
qset[0].delete()
post_save.connect(EmailLog.post_create, sender=EmailLog)

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