django post_save signal sends outdated inline formsets - django

Consider the following:
class OrderForm(models.Model):
title = models.CharField(max_length=100)
desc = models.TextField()
class OrderFormLine(models.Model):
order = models.ForeignKey(OrderForm)
lagel = models.CharField(max_length=100)
qty = models.IntegerField(...)
price = models.FloatField(...)
Now I want to send an email with the orderform details whenever someone creates one or modify one.
No rocket science so far .. let's just use a post_save signal;
post_save.connect(email_notify, sender=OrderForm)
But there's one tiny problem, the OrderForm object passed to email_notify is updated with the new data as expected, but not the related OrderFormLine items.
I've tried to override the save methods in the admin AND in the model, I've tried to save the object, the form and its relation before passing it to my notification handler, nothing works.
I'm aware that I could attach the post_save signal to the OrderItem model, but then the email would be sent for each items.
Help I'm on the brink of madness.
UPDATE:
Found a simple and reliable solution
Short story:
def email_notify_orderform(sender, **kwargs):
instance = kwargs['instance']
ct = ContentType.objects.get_for_model(OrderForm)
if ct.id == instance.content_type.id:
print instance.is_addition()
print instance.is_change()
print instance.is_deletion()
print instance.change_message
print instance.action_time
print instance.get_edited_object().total() # BINGO !
post_save.connect(email_notify_orderform, sender=LogEntry)

The basic problem is that when the main objects post_save signal is sent, the inlines have not been saved yet: the parent model always gets saved first. So, it's not that it's sending old data; in fact it's the current state of the data.
The simplest solution is to create a custom signal and have that signal sent at a place where the inlines have been saved. The save_formset method on ModelAdmin is your hook.

Related

Django : Recalculating mean value in the database after creating a new instance

I have informations about companies presented in a table. One of the field of this table is the mean value of each note the company received ('note_moyenne' in models.FicheIdentification).
By clicking on a button, people are able to submit a new note for the company ('note' in models.EvaluationGenerale). I want the mean value of the notes to update in the database each time someone submit a new note.
Here is my models.py :
class FicheIdentification(models.Model):
entreprise=models.ForeignKey(Entreprise, on_delete=models.CASCADE)
note_moyenne=models.IntegerField()
def __str__(self):
return self.entreprise.nom_entreprise
class EvaluationGenerale(models.Model):
entreprise=models.ForeignKey(Entreprise, on_delete=models.CASCADE)
note=models.IntegerField()
commentaires=models.CharField(max_length=1000)
date_evaluation=models.DateField(auto_now_add=True)
def __str__(self):
return self.commentaires
views.py :
class CreerEvaluationGenerale(CreateView):
form_class = FormulaireEvaluationGenerale
model = EvaluationGenerale
def form_valid(self, form):
form.instance.entreprise=Entreprise.objects.filter(siret=self.kwargs['siret']).first()
return super(CreerEvaluationGenerale, self).form_valid(form)
def get_success_url(self):
return reverse('details-evaluations')
Currently I just display the mean value in my table using this
def render_evaluation(self, record):
return (EvaluationGenerale.objects.filter(entreprise=record.entreprise.siret).aggregate(Avg('note'))['note__avg'])
but I really don't like this solution as I want the value to be stored in the database, in FicheIdentification.note_moyenne.
I thought about creating a UpdateView class but couldn't manage to link it with my CreateView.
Any help or documentation would be really appreciated, I'm a bit lost right know...
Typically, you would not store calculated fields. The usual way is not to store the average, but to use an annotation/aggregation in your query.
To centralize this to your model, you would want to write a custom model manager to implement this, so it can be reused anywhere you use your model without rewriting the logic.
class MyModelManager(models.Manager):
def note_average(self, **filter_kwargs):
qs = self.get_queryset()
# replace `...` with your aggregation as needed
return qs.filter(**filter_kwargs).aggregate(...)
class EvaluationGenerale(models.Model):
objects = MyModelManager() # override the default manager
# ... the rest of the model as-is
Then you can use something like the following in your view(s):
EvaluationGenerale.objects.note_average(entreprise=record.entreprise.siret)
See for additional reference: How to add a calculated field to a Django model
I see two ways of doing it.
Either a listener post_save on EvaluationGenerale (doc). You'll be able to compute the new average each time a new EvaluationGenerale is entered in DB.
#receiver(post_save, sender=EvaluationGenerale)
def evaluation_general_note_moyenne_computer_post_save_listener(sender, instance, **kwargs):
entreprise = instance.entreprise
entreprise.note_moyenne = entreprise.evaluationgeneral_set.aggregate(Avg('note')).values()[0])
entreprise.save()
post save listener will only trigger on instance.save() and models.objects.create() not on queryset.update() or model.objects.bulk_create().
Either overriding the save (doc) function of your form to compute the average after the creation of the new EvaluationGenerale
def save(self):
instance = super.save()
entreprise = instance.entreprise
entreprise.note_moyenne = entreprise.evaluationgeneral_set.aggregate(Avg('note')).values()[0]
entreprise.save()
return instance
Assuming there is as single FicheIdentification object per enterprise, you could update the note_moyenne field when you save the EvaluationGenerale object, like:
obj = FicheIdentification(...)
FicheIdentification.objects.filter(entreprise=record.entreprise.siret).update(note_moyenne=obj.aggregate(Avg('note'))['note__avg']
obj.save()
Please let me know if it works.

Access Django through model extra fields in m2m_changed signal handler

Let's say I have the following Django models, which represents a sorted relationship between a Parent and Child:
class Parent(models.Model):
name = models.CharField(max_length=50)
children = models.ManyToManyField("Child", through="ParentChild")
class Child(models.Model):
name = models.CharField(max_length=50)
class ParentChild(models.Model):
class Meta:
constraints = [
models.UniqueConstraint(fields=["parent", "child"], name="uc_parent_child"),
models.UniqueConstraint(fields=["parent", "sort_number"], name="uc_parent_child"),
]
parent = models.ForeignKey(Parent, on_delete=models.CASCADE)
child = models.ForeignKey(Child, on_delete=models.CASCADE)
sort_number = models.IntegerField()
def save(self, *args, **kwargs):
exising_sort_numbers = self.parent.parentchild_set.values_list(
"sort_number", flat=True
)
if self.sort_number in exising_sort_numbers:
raise Exception(f"Duplicate sort number: {self.sort_number}")
super().save(*args, **kwargs)
Now if I create the relationships using the through model, I get the exception for a duplicate sort_number:
ParentChild.objects.create(parent=parent, child=child1, sort_number=0)
ParentChild.objects.create(parent=parent, child=child2, sort_number=0) # raises Exception
However, if I create the relationships using the .add method, I don't get the exception:
parent.children.add(child1, through_defaults={"sort_number": 0})
parent.children.add(child2, through_defaults={"sort_number": 0}) # does NOT raise Exception
I know using the .add method doesn't call the .save method on the through model so I need to use the m2m_change signal to run this logic. But I'm not sure how to get the sort_number within this signal. Here's the code I have for the signal so far:
#receiver(m2m_changed, sender=Parent.children.through)
def validate_something(sender, instance, action, reverse, model, pk_set, **kwargs):
if action == "pre_add":
for pk in pk_set:
child = model.objects.get(pk=pk)
exising_sort_numbers = instance.parentchild_set.values_list(
"sort_number", flat=True
)
# where's sort_number specified in through_defaults ???
Any idea how I can get this value and perform the "pre_add" validation or is this not possible?
You have this constraint - models.UniqueConstraint(fields=["parent", "sort_number"], name="uc_parent_child"), which means that you can't have more than one relation with the same parent and sort_number. There's even an extra check in ParentChild's save method to further enforce this. Makes sense to have an exception thrown when you try to create such a relation.
Also, the constraint name needs to be unique. I tried the code and couldn't make migrations as is.
If you do what you are trying to, you'll get that exception again when saving.
Instead of trying to hack around the constraint, you should either change/remove it or adapt your code to work with it - don't try to create instance which will violate it.
As to your specific question,the instance you get in validate_something is Parent and there's no direct access to the intermediary instance or it's defaults. You also can't query the intermediary instance, because it does not exist yet.
For any googlers that might be looking for a way to handle through model fields, you can't get it in pre_add as #4140tm said, since the record doesn't exist yet. But you can work around it on post_add with some effort:
#receiver(m2m_changed, sender=Parent.children.through)
def validate_something(sender, instance, action, reverse, model, pk_set, **kwargs):
# notice this is not 'pre_add':
if action == "post_add":
# just for clarity:
parent = instance
child_model = model
through_model = sender
# > OP: where's sort_number specified in through_defaults ???
# here it is:
existing_sort_numbers = [pc.sort_number \
for pc in through_model.objects.filter(parent=parent.id) \
if pc.child not in pk_set]
# now just work around (rollback, raise exception, etc):
for pk in pk_set:
added_child = child_model.objects.get(id=pk)
# goes on dirty work...

Child model updates Parent model in Django ForeignKey relationship

Assuming the following models schema,
Parent model:
class Batch(models.Model):
start = models.DateTimeField()
end = models.DateTimeField()
One of many child models:
class Data(models.Model):
batch = models.ForeignKey(Batch, on_delete=models.ON_CASCADE)
timestamp = models.DateTimeField()
My goals is the following: to have a start field of parent model that is always updated when any child model is modified.
Basically, if the timestamp of a newly data instance is older than the start field I want the start field to be updated to that instance timestamp value. In the case of deletion of the data instance which is the oldest time reference point I want batch start field to be updated to the second oldest. Vice-versa for the end field.
One of the possible way to do this is to add post or pre-save signal of relative models and Update your necessary fields according to this. Django official documentation for signal, link. I want to add another link, one of the best blog post i have seen regarding django signal.
Edit for André Guerra response
One of easiest way to do a get call and bring Batch instance. What i want to say
#receiver(post_save,sender=Data)
def on_batch_child_saving(sender,instance,**kwargs):
batch_instance = Batch.objects.get(pk=instance.batch)
if (instance.timestamp < batch_instance.start):
batch_instance.start = instance.timestamp
batch_instance.save()
elif (instance.timestamp > batch_instance.end):
batch_instance.end = instance.timestamp
batch_instance.save()
Based on Shakil suggestion, I come up with this: (my doubt here was on how to save the parent model)
#receiver(post_save,sender=Data)
def on_batch_child_saving(sender,instance,**kwargs):
if (instance.timestamp < instance.batch.start):
instance.batch.start = instance.timestamp
instance.batch.save()
elif (instance.timestamp > instance.batch.end):
instance.batch.end = instance.timestamp
instance.batch.save()

Django admin save not sending post_remove action with m2m_changed signal

I'm trying to get a many to many model to update when I save a related model. This should be possible using the m2m_changed signal (and it works! but not in the admin?) e.g.
# i want the references field to update when related model is saved.
# so just call count_references
class Tag(models.Model):
"""Group everything into categories"""
# stuff stuff stuff
references = models.IntegerField(default=0, editable=False)
def count_references(self):
# just add up references each time to save headaches
self.references = 0
# search for reverse managers
sets = re.compile('^\w+_set$')
for rel_set in [method for method in dir(self) if sets.match(method)]:
self.references += getattr(self, rel_set).count()
self.save()
class Entry(models.Model):
"""Blog entry"""
# stuff stuff stuff
tags = models.ManyToManyField('Tag', blank=True)
# this will call count_references when entry adds or removes tags
#receiver(m2m_changed, sender=Entry.tags.through)
def update_tag_ref_count(sender, instance, action, reverse, model, pk_set, **kwargs):
print action
if not reverse and action == 'post_add' or action == 'post_remove':
for tag_pk in pk_set:
print tag_pk
Tag.objects.get(pk=tag_pk).count_references()
print Tag.objects.get(pk=tag_pk).references
Everything works perfectly when run in the shell. e.g. with a tests.py like so:
t = Tag.objects.all()[0]
s = Snippet.objects.all()[0]
s.tags.remove(t)
s.save()
s.tags.add(t)
s.save()
I get the following (where 'test' is the tag name being printed):
pre_remove
post_remove
test
0
pre_add
post_add
test
1
perfect! And when I add a tag to an entry in the admin I get the following (between HTTP stuff):
pre_clear
post_clear
pre_add
post_add
test
1
still good! not sure what pre/post_clear was called for... and when I remove:
pre_clear
post_clear
argh! pre/post_remove is not called! pre/post_clear is useless as well as it doesn't provide any primary keys. this feels like a bug in the admin implementation. any suggestions?
Update: Bug #16073 filed and accepted.
(Creating this as a community wiki to close out this as an "unanswered" question.)
This is a bug in Django. OP filed a ticket at https://code.djangoproject.com/ticket/16073.

Django: save multiple object signal once

I need some help with sending email when an order is placed. To illustrate the problem, following is the abstract code:
class Order(models.Model):
user = models.ForeignKey(User)
class OrderItem(modes.Model):
order = models.ForeignKey(Order, related_name='items')
item = models.CharField(max_length=255)
unit_price = models.DecimalField()
qty = models.IntegerField()
item_amount = models.DecimalField()
def email_order_on_save(sender, instance, **kwargs):
# Need order.items.all() here
pass
post_save.connect(email_order_on_save, sender=Order)
Most of the problems on SO and google seem to deal with one child object at a time; such as this.
Listening to OrderItem would release 5 signals if 5 orders items saved from admin inlines. I can't seem to get my head around this problem. One way, I think (not sure if possible), could be listening to last of all(5) OrderItem's post_save signals.
Any help appreciated.
I'm guessing you're trying to solve this in the wrong place. Sending an email when the order is completed and saving the Order model are at different levels of abstraction.
I think sending the email should be triggered by some condition in the view that has more information about whether the order is completely saved or not. Think for example of what will happen if an order needs updating (say it's status changes)? Should the email be sent then too?
Create your own custom signal and send it at the point when you have the data you need saved. Pass in as parameters whatever data structures you need.
Listen for your custom signal in your callback function email_order_on_save and make appropriate decisions based on the parameters about sending or not the e-mail.
You could create your model as follows
ORDER_STATE = (
(1, 'Completed'),
(2, 'Processing'),
)
class Order(models.Model):
user = models.ForeignKey(User)
state = models.IntegerField(choices = ORDER_STATE)
You could have many states for the order. The state "Completed" could represent that the order processing is complete. You could change the state of your order in your views.
In the signal handler, you could check for the state of the order and then send mail, if the order is in completed state.
I think you can have a problem with signals, OrderItem with inlines will not send save signal, read this