I want to perform some action (sending email) after updating an (already existing) object.
In order to do it I need to compare values of the object before and after saving and only if something specific has changed - do that action. From reading other related question I understood that I can only do it in pre-save signal, since I can't get the old version inside 'post-save', but - what if there will be some issue with saving and the item will not be saved? I don't want to perform the action in that case. So I thought about implementing it somehow by overriding the view save, but I'm not sure that's the correct way to do it. What do you think?
This is implementing in pre-save:
#staticmethod
#receiver(pre_save, sender=Item)
# check if there is change that requires sending email notification.
def send_email_notification_if_needed(sender, instance, raw, *args, **kwargs):
try:
# if item just created - don't do anything
pre_save_item_obj = sender.objects.get(pk=instance.pk)
except sender.DoesNotExist:
pass # Object is new, so field hasn't technically changed
else:
# check if state changed to Void
if pre_save_item_obj.state_id != VOID and instance.state_id == VOID:
content = {"item_name": instance.title, "item_description": instance.description}
EmailNotificationService().send_email("item_update"
["myemail#gmail.com"], str(instance.container.id) +
str(instance.id) + " changed to Void",
content)
There is nothing wrong with overriding the model's save method. After all, this is where you have all the information that you need:
class X(models.Model):
def save(self, *args, **kwargs):
pre_obj = X.objects.filter(pk=self.pk).first()
super(X, self).save(*args, **kwargs)
# no exception from save
if pre_obj and pre_obj.state_id != VOID and self.state_id == VOID:
# send mail
Related
I am trying to implement a function which sends a notification to all employee records whenever a new document record is published. In the models, I still needed to import the receiver function because my sender model lives in a different project:
receiver function (lives in a different app in project):
def new_document_version_published(sender, instance, **kwargs):
print("New version of document published!")
print(sender)
print(instance)
# Get all employees
employees = []
# Send notifications to employees
buttons = [NotificationButton(button_text="New version of document", value="Ok", style="primary")]
notifyUsers("A new version of the document has been published", buttons, employees, [])
sender (lives in a different app in project):
from django.db.models.signals import post_save
from api.views import new_document_version_published
class DocumentVersion:
...
def save(self, *args, **kw):
if self.pk is not None:
orig = DocumentVersion.objects.get(pk=self.pk)
if orig.date_published != self.date_published:
print('date_published changed')
notify_employees()
if orig.date_approved != self.date_approved:
print('date_approved changed')
super(DocumentVersion, self).save(*args, **kw)
def notify_employees():
post_save.connect(new_document_version_published, sender=DocumentVersion)
I know there is something wrong with my implementation because I don't understand what is the difference between using the signal and just importing and calling the receiver function. All help appreciated!
What is the difference
Calling a function makes the caller dependent on (or at least aware of) the receiver function, while using Django signals makes the receiver function dependent on the signal being called by its caller(s).
From https://docs.djangoproject.com/en/3.2/topics/signals/:
... helps decoupled applications get notified when actions occur elsewhere in the framework. In a nutshell, signals allow certain senders to notify a set of receivers that some action has taken place. They’re especially useful when many pieces of code may be interested in the same events.
When to use Django signals
From https://www.django-antipatterns.com/antipattern/signals.html:
Signals have a variety of problems and unforeseen consequences.
...
Often it is better to avoid using signals. One can implement a lot of logic without signals.
...
Signals can still be a good solution if you want to handle events raised by a third party Django application.
...
What that difference looks like
Calling a function
Pre-save:
# notify_employees() # Replace this
new_document_version_published(self.__class__, self) # with this
Post-save:
class DocumentVersion(models.Model):
...
def save(self, *args, **kw):
orig = None # ......... # Add this
if self.pk is not None:
orig = DocumentVersion.objects.get(pk=self.pk)
if orig.date_published != self.date_published:
print('date_published changed')
# notify_employees() # Replace this
if orig.date_approved != self.date_approved:
print('date_approved changed')
super(DocumentVersion, self).save(*args, **kw)
if orig and orig.date_published != self.date_published: # with this
new_document_version_published(self.__class__, self) #
Using Django Signals
Since Django's post_save signal doesn't pass orig, let's use a custom signal:
Define a signal.
Send the signal.
Implement the receiver function.
Connect the receiver function, usually where the receiver function is defined.
Post-save:
post_save_published = Signal() # Add this
class DocumentVersion(models.Model):
...
def save(self, *args, **kw):
orig = None # ......... # Add this
if self.pk is not None:
orig = DocumentVersion.objects.get(pk=self.pk)
if orig.date_published != self.date_published:
print('date_published changed')
# notify_employees() # Replace this
if orig.date_approved != self.date_approved:
print('date_approved changed')
super(DocumentVersion, self).save(*args, **kw)
if orig and orig.date_published != self.date_published: # with this
post_save_published.send(sender=self.__class__, instance=self) #
def new_document_version_published(sender, instance, **kwargs):
# ...
post_save_published.connect(new_document_version_published, sender=DocumentVersion) # Add this
I'd like to make sure that nobody can't create an Invitation object with an email that is already in a database either as Invitation.email or as User.email.
To disallow creating Invitation with existing Invitation.email is easy:
class Invitation(..):
email = ...unique=True)
Is it also possible to check for the email in User table? I want to do this on a database or model level instead of checking it in serializer, forms etc..
I was thinking about UniqueConstraint but I don't know how to make the User.objects.filter(email=email).exists() lookup there.
You can override the save() method on the model, and check first in the users table. You should look that is a new model. Something like this I think:
class Invitation(..):
email = ...unique=True)
def save(self, *args, **kwargs):
if self.id is None and User.objects.filter(email=self.email).exists():
raise ValidationError('Email already used.')
else:
super().save(*args, **kwargs)
You can do it in the model.. as below. Or you can do it in the database with a Check Constraint (assuming postgres).. but you still can't avoid adding code to your view, because you'll need to catch the exception and display a message to the user.
class Invitation(models.Model):
def save(self, *args, **kwargs):
if (not self.pk) and User.objects.filter(email=self.email).exists():
raise ValueError('Cannot create invitation for existing user %s.' % self.email)
return super().save(*args, **kwargs)
PS: Some may ask why it is that I am passing *args and **kwargs to the superclass, or returning the return value.. when save has no return value. The reason for this is that I never assume that the arguments or return value for a method I am overriding won't change in the future. Passing them all through if you have no reason to intercept them, is just a good practice.
How about overriding the save method?
class Invitation(...):
...
def save(self, *args, **kwargs):
# check if an invitation email on the user table:
if User.objects.get(id=<the-id>).email:
# raise integrity error:
...
# otherwise save as normal:
else:
super().save(*args, **kwargs)
How do we enforce Django Admin to correctly call .update() instead of .save() to avoid triggering checks meant for object creation?
This is the models.py:
class BinaryChoice():
# field definitions
...
def save(self, *args, **kwargs):
# check if binary
if self.question.qtype == 2:
if self.question.choices.count() < 2:
super(BinaryChoice, self).save(*args, **kwargs)
else:
raise Exception("Binary question type can contain at most two choices.")
else:
super(BinaryChoice, self).save(*args, **kwargs)
This passes the test, no surprises:
class SurveyTest(TestCase):
def test_binary_choice_create(self):
q1 = Question.objects.create(survey=survey, title='Have you got any internship experience?', qtype=Question.BINARY)
BinaryChoice.objects.create(question=q1, choice="Yes")
BinaryChoice.objects.create(question=q1, choice="No")
with self.assertRaises(Exception):
BinaryChoice.objects.create(question=q1, choice="Unsure / Rather not say")
The .save() correctly checks that there isn't already 2 binary choices related to the same Question. However, in Django Admin, when using the interface to update the value (anything arbitrary, for example changing the value from "Yes" to "Sure") and saving it, one would expect the .update() method to be called.
It turns out, according to Django docs and also a relevant thread here, the .save() method is called instead. So now our update operation would fail when there's already 2 BinaryChoice, even if you intend to update a value in-place using the Django Admin's default interface.
For completeness sake, this is admin.py:
#admin.register(BinaryChoice)
class BinaryChoiceAdmin(admin.ModelAdmin):
pass
Instead of trying to patch the ModelAdmin why don't you simply fix your save method? Simply check if the object already has a pk or not before saving:
class BinaryChoice():
# field definitions
...
def save(self, *args, **kwargs):
# check if binary
# Here ↓
if not self.pk and self.question.qtype == 2:
if self.question.choices.count() < 2:
super(BinaryChoice, self).save(*args, **kwargs)
else:
raise Exception("Binary question type can contain at most two choices.")
else:
super(BinaryChoice, self).save(*args, **kwargs)
I need to do some actions when one field has changed.
Since this action needs to work with already saved object, I can't use pre_save signal like this:
#receiver(pre_save, sender=reservation_models.Reservation)
def generate_possible_pairs(sender, instance, **kwargs):
try:
reservation_old = sender.objects.get(pk=instance.pk)
except sender.DoesNotExist:
pass # Object is new, so field hasn't technically changed, but you may want to do something else here.
else:
if not reservation_old.datetime == instance.datetime: # Field has changed
do_something(instance) # It would be better to be sure instance has been saved
Is it possible to use post_save signal for this?
I would like to avoid adding temporary attributes to this model.
Using the post_save signal you won't be able to retrieve the previous state from db - But why use a signal at all ?
class Reservation(models.Model):
def save(self, *args, **kw):
old = type(self).objects.get(pk=self.pk) if self.pk else None
super(Reservation, self).save(*args, **kw)
if old and old.datetime != self.datetime: # Field has changed
do_something(self)
You may also want to read this : https://lincolnloop.com/blog/django-anti-patterns-signals/
Yes you can use a post_save too. You should however remember signals are synchronous
I having a weird issue with Django 1.3. When I try to add a new Activity using the ActivityForm, the Activity is often updated even if I explicitly create a new Activity object with not Id.
In addition, when the init of my ActivityForm class has an explicit trip_id parameter, the UI displays "Select a valid choice. That choice is not one of the available choices." for the field location (even though the selection shown is valid). However, if I make this an optional parameter and I pop it from kwargs, I don't see this issue.
Can someone please take a look at the code below and see what I am doing wrong?
forms.py
class DestinationMultipleChoiceField(ModelMultipleChoiceField):
def label_from_instance(self, obj):
return obj.city_name
class ActivityForm(forms.Form):
description = forms.CharField(max_length=100, required=True)
location = DestinationChoiceField(queryset = Visit.objects.none(), empty_label=None, required=True)
def __init__(self, trip_id = None, *args, **kwargs):
super(ActivityForm, self).__init__(*args, **kwargs)
if trip_id is not None:
self.fields['location'].queryset = Visit.objects.filter(trip=trip_id).all().select_related('destination')
# A new Activity() is created if nothing is provided
def save(self, activity = Activity()):
if not self.is_valid():
raise forms.ValidationError('ActivityForm was not validated first before trying to call save().')
activity.visit = self.cleaned_data['location']
activity.description = self.cleaned_data['description']
activity.added_by = User.objects.get(pk=1)
activity.save()
views.py
def add_activity(request, trip_id = None, template_name = 'edit-form.html'):
if request.POST:
form = ActivityForm(trip_id, request.POST)
if form.is_valid():
form.save()
return HttpResponseRedirect(reverse('views.list_trip'))
else:
form = ActivityForm(trip_id)
return render_to_response(template_name, {
'page_title': 'Add',
'form': form,
}, context_instance=RequestContext(request))
Look at this line:
def save(self, activity = Activity()):
In Python, default parameters are evaluated once; thus, when the save method is called the first time, a new Activity will be created, but subsequent calls will use that Activity.
Change save to something like this:
def save(self, activity=None):
if activity is None:
activity = Activity()
# Rest of the method
Then, a new Activity will be created on every call (if one is not supplied as an argument).
Model instances are mutable. As such they should never be used as default arguments to a method, as you do in save. There's a reason why you've never seen that in any documentation or examples.
As noted in very many other questions on this site, default arguments are evaluated on definition, not execution. So each call to save with no activity argument will use the same Activity that was originally defined.
However, I don't understand why you're not using a ModelForm, which does most of this for you in any case.