I'm using django's post_save signal to execute some statements after saving the model.
class Mode(models.Model):
name = models.CharField(max_length=5)
mode = models.BooleanField()
from django.db.models.signals import post_save
from django.dispatch import receiver
#receiver(post_save, sender=Mode)
def post_save(sender, instance, created, **kwargs):
# do some stuff
pass
Now I want to execute a statement based on whether the value of the mode field has changed or not.
#receiver(post_save, sender=Mode)
def post_save(sender, instance, created, **kwargs):
# if value of `mode` has changed:
# then do this
# else:
# do that
pass
I looked at a few SOF threads and a blog but couldn't find a solution to this. All of them were trying to use the pre_save method or form which are not my use case. https://docs.djangoproject.com/es/1.9/ref/signals/#post-save in the django docs doesn't mention a direct way to do this.
An answer in the link below looks promising but I don't know how to use it. I'm not sure if the latest django version supports it or not, because I used ipdb to debug this and found that the instance variable has no attribute has_changed as mentioned in the below answer.
Django: When saving, how can you check if a field has changed?
If you want to compare state before and after save action, you can use pre_save signal which provide you instance as it should become after database update and in pre_save you can read current state of instance in database and perform some actions based on difference.
from django.db.models.signals import pre_save
from django.dispatch import receiver
#receiver(pre_save, sender=MyModel)
def on_change(sender, instance: MyModel, **kwargs):
if instance.id is None: # new object will be created
pass # write your code here
else:
previous = MyModel.objects.get(id=instance.id)
if previous.field_a != instance.field_a: # field will be updated
pass # write your code here
Ussually it's better to override the save method than using signals.
From Two scoops of django:
"Use signals as a last resort."
I agree with #scoopseven answer about caching the original value on the init, but overriding the save method if it's possible.
class Mode(models.Model):
name = models.CharField(max_length=5)
mode = models.BooleanField()
__original_mode = None
def __init__(self, *args, **kwargs):
super(Mode, self).__init__(*args, **kwargs)
self.__original_mode = self.mode
def save(self, force_insert=False, force_update=False, *args, **kwargs):
if self.mode != self.__original_mode:
# then do this
else:
# do that
super(Mode, self).save(force_insert, force_update, *args, **kwargs)
self.__original_mode = self.mode
UPDATE IF YOU NEED SIGNALS
Just in case you really need signals because you need a decoupled app or you can't simply override the save() method, you can use pre_save signal to 'watch' previous fields
#receiver(pre_save, sender=Mode)
def check_previous_mode(sender, instance, *args, **kwargs):
original_mode = None
if instance.id:
original_mode = Mode.objects.get(pk=instance.id).mode
if instance.mode != original_mode:
# then do this
else:
# do that
The problem with this is that you make changes before, so if save() has a problem you could have some issues later.
So to fix that issue, you can store the original value on the pre_save and use on post_save.
#receiver(pre_save, sender=Mode)
def cache_previous_mode(sender, instance, *args, **kwargs):
original_mode = None
if instance.id:
original_mode = Mode.objects.get(pk=instance.id).mode
instance.__original_mode = original_mode:
#receiver(post_save, sender=Mode)
def post_save_mode_handler(sender, instance, created, **kwargs):
if instance.__original_mode != instance.original_mode:
# then do this
else:
# do that
The problem with signals and this approach also is that you need one more query to check previous values.
Set it up on the __init__ of your model so you'll have access to it.
def __init__(self, *args, **kwargs):
super(YourModel, self).__init__(*args, **kwargs)
self.__original_mode = self.mode
Now you can perform something like:
if instance.mode != instance.__original_mode:
# do something useful
This is an old question but I've come across this situation recently and I accomplished it by doing the following:
class Mode(models.Model):
def save(self, *args, **kwargs):
if self.pk:
# If self.pk is not None then it's an update.
cls = self.__class__
old = cls.objects.get(pk=self.pk)
# This will get the current model state since super().save() isn't called yet.
new = self # This gets the newly instantiated Mode object with the new values.
changed_fields = []
for field in cls._meta.get_fields():
field_name = field.name
try:
if getattr(old, field_name) != getattr(new, field_name):
changed_fields.append(field_name)
except Exception as ex: # Catch field does not exist exception
pass
kwargs['update_fields'] = changed_fields
super().save(*args, **kwargs)
This is more effective since it catches all updates/saves from apps and django-admin.
in post_save method you have kwargs argument that is a dictionary and hold some information. You have update_fields in kwargs that tell you what fields changed. This fields stored as forzenset object. You can check what fields changed like this:
#receiver(post_save, sender=Mode)
def post_save(sender, instance, created, **kwargs):
if not created:
for item in iter(kwargs.get('update_fields')):
if item == 'field_name' and instance.field_name == "some_value":
# do something here
But there is an issue in this solution. If your field value for example was 10, and you update this field with 10 again, this field will be in update_fields again.
I'm late but it can be helpful for others.
We can make custom signal for this.
Using custom signal we can easily do these kind of things:
Post is created or not
Post is modified or not
Post is saved but any field does not changed
class Post(models.Model):
# some fields
Custom signals
**Make signal with arguments **
from django.dispatch import Signal, receiver
# provide arguments for your call back function
post_signal = Signal(providing_args=['sender','instance','change','updatedfields'])
Register signal with call back function
# register your signal with receiver decorator
#receiver(post_signal)
def post_signalReciever(sender,**kwargs):
print(kwargs['updatedfields'])
print(kwargs['change'])
Sending the signal from post-admin
We sending the signals from Post admin and also save object when it actually modified
#sending the signals
class PostAdmin(admin.ModelAdmin):
# filters or fields goes here
#save method
def save_model(self, request, obj, form, change):
if not change and form.has_changed(): # new post created
super(PostAdmin, self).save_model(request, obj, form, change)
post_signal.send(self.__class__,instance=obj,change=change,updatedfields=form.changed_data)
print('Post created')
elif change and form.has_changed(): # post is actually modified )
super(PostAdmin, self).save_model(request, obj, form, change)
post_signal.send(self.__class__,instance=obj,change=change,updatedfields=form.changed_data)
print('Post modified')
elif change and not form.has_changed() :
print('Post not created or not updated only saved ')
See also:
Django Signals official doc
This can be identified using instance._state.adding
if not instance._state.adding:
# update to existing record
do smthng
else:
# new object insert operation
do smthng
You can use update_fields in django signals.
#receiver(post_save, sender=Mode)
def post_save(sender, instance, created, **kwargs):
# only update instance
if not created:
update_fields = kwargs.get('update_fields') or set()
# value of `mode` has changed:
if 'mode' in update_fields:
# then do this
pass
else:
# do that
pass
Related
So I have two forms, Sale and SaleItems
SaleForm = SaleForm(request.POST or None, auto_id=False, prefix = 'SaleForm')
SaleItemsForm = modelformset_factory(
Sale, form = SaleItemsForm, formset = ItemsFormSet, extra=1, can_delete=True
)
once they're both given POST data and valid, they're in the usual statement:
if SaleForm.is_valid() and SaleItemsForm.is_valid():
pass
When it comes time to do validation I've superseded the basemodelformset and want to write my own custom clean method for the modelformset. I want to use cleaned data from SaleForm inside the clean method for the ItemsFormSet:
from django import forms
class ItemsFormSet(forms.models.BaseModelFormSet):
def __init__(self, *args, **kwargs):
super(ItemsFormSet, self).__init__(*args, **kwargs)
def clean(self):
super().clean()
print(TheSaleForm.cleaned_data['Value'])
This doesn't work, and I've tried a few dumb things:
try to make SaleForm a global and access it between the views.py and forms.py modules. That was a bad idea and now I understand more about the module scope
try to import the actual object, again not smart
I'm assuming there has to be a way to do this without saving the cleaned data off somewhere to the database and retrieving it again in the clean method of the formset. I'm not sure if overwriting ItemsFormSet.is_valid() and trying to allow a kwarg dictionary item to be passed through would be the right way to go.... but I'm hoping someone has an idea of what the "correct" way to approach this is.
You should allow the data to be passed into the init of ItemsFormSet and keep it as an instance attribute which you can reference later.
class ItemsFormSet(forms.models.BaseModelFormSet):
def __init__(self, *args, **kwargs):
self.sale_form = kwargs.pop('sale_form', None)
super(ItemsFormSet, self).__init__(*args, **kwargs)
def clean(self):
super().clean()
print(self.sale_form.cleaned_data['Value'])
and now in your view:
if request.method == 'POST':
form = SaleForm(request.POST)
formset = SaleItemsForm(request.POST, sale_form=form)
I've got a Django model like so...
class Example(models.Model):
title = models.CharField(...)
...
I'm trying to compare two values - the title field before the user changes it, and the title after. I don't want to save both values in the database at one time (only need one title field), so I'd like to use pre_save and post_save methods to do this. Is it possible to get the title before the save, then hold this value to be passed into the post_save method?
The pre_save and post_save methods look like so...
#receiver(pre_save, sender=Example, uid='...')
def compare_title_changes(sender, instance, **kwargs):
# get the current title name here
x = instance.title
#receiver(post_save, sender=Example, uid='...')
def compare_title_changes(sender, instance, **kwargs):
# get the new title name here and compare the difference
x = instance.title # <- new title
if x == old_title_name: # <- this is currently undefined, but should be retrieved from the pre_save method somehow
...do some logic here...
Any ideas would be greatly appreciated!
Edit
As was pointed out to me, pre_save and post_save both occur after save() is called. What I was looking for is something like pre_save() but before the actual save method is called. I set this on the model so that the logic to be performed will be accessible wherever the instance is saved from (either admin or from a user view)
Use Example.objects.get(pk=instance.id) to get the old title from the database in the pre_save handler function:
#receiver(pre_save, sender=Example, uid='...')
def compare_title_changes(sender, instance, **kwargs):
new_title = instance.title # this is the updated value
old_title = Example.objects.get(pk=instance.id)
# Compare the old and new titles here ...
This trick was proposed here a long time ago. I've not tested it with the recent Django version. Please let me know whether it's still working.
We can only say object has changed if "save" method passes successfully, so post_save is good to be sure that model object has updated.
Setting on the fly attribute on model class instance can do the task, as the same instance is passed from pre_save to post_save.
def set_flag_on_pre_save(sender, instance, *args, **kwargs):
# Check here if flag setting condition satisfies
set_the_flag = true
if set_the_flag:
instance.my_flag=0
def check_flag_on_post_save(sender, instance, *args, **kwargs):
try:
print(instance.my_flag)
print('Flag set')
except AttributeError:
print('Flag not set')
pre_save.connect(set_flag_on_pre_save, sender=ModelClass)
post_save.connect(check_flag_on_post_save, sender=ModelClass)
How can use the signal pre_save in django for remove the old image and save the new image.
The code in models.py is:
#receiver(pre_save, sender = Treballador)
def treballador_removing(sender, instance, **kwargs):
this = Video.objects.get(pk=self.pk)
if this.photo == self.photo:
instance.photo.delete(False)`
What do bad?
I have Chart and Module models (see code below). Each chart belongs to a module (via a ForeignKey). A chart may have another chart as its parent (another ForeignKey). What I would like in the admin is that the dropdown for parent on a particular chart only includes charts in the same module.
I'm looking for a Python solution, not AJAX. That means that on creating a new chart the dropdown will have to be empty (without a model instance there's no module selected) and that changing the module in the admin won't update the parent options to match until the model is saved. I'm ok with that.
There are plenty of similar-sounding questions (and answers) that turn out to filter the options according to the user; ModelAdmin.formfield_for_foreignkey gets the request as an argument so you can use request.user, but it doesn't get given a model instance to play with. (I'm using Django 1.3.)
My models (highly simplified of course):
class Module(models.Model):
pass
class Chart(models.Model):
module = models.ForeignKey(Module)
parent = models.ForeignKey(Chart, blank=True, null=True)
class ChartAdmin(admin.ModelAdmin):
def formfield_for_foreignkey(self, db_field, request, **kwargs):
# I would like to do this, but have no "instance":
kwargs['queryset'] = Chart.objects.filter(module=instance.module)
return super(ChartAdmin, self).formfield_for_foreignkey(self, db_field, request, **kwargs)
admin.site.register(Chart, ChartAdmin)
Just override the ModelForm being used:
class ChartAdminForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(ChartAdminForm, self).__init__(*args, **kwargs)
if self.instance.module_id:
self.fields['parent'].queryset = self.fields['parent'].queryset.filter(module=self.instance.module)
class ChartAdmin(admin.ModelAdmin):
form = ChartAdminForm
...
From what i've seen in the source code, kwargs should contain only the widget (not helping !).
One possible hack, would be to override the get_form() modeladmin method, just to set request.current_object. Then, you could use request.current_object in formfield_callback:
def get_form(self, request, obj=None, **kwargs):
request.current_object = obj
return super(ChartAdmin, self).get_form(request, obj, **kwargs)
def formfield_for_foreignkey(self, db_field, request, **kwargs):
instance = request.current_object
kwargs['queryset'] = Chart.objects.filter(module=instance.module)
return super(ChartAdmin, self).formfield_for_foreignkey(self, db_field, request, **kwargs)
I want use a model to save the system setting for a django app, So I want to limit the model can only have one record, how to do the limit?
Try this:
class MyModel(models.Model):
onefield = models.CharField('The field', max_length=100)
class MyModelAdmin(admin.ModelAdmin):
def has_add_permission(self, request):
# if there's already an entry, do not allow adding
count = MyModel.objects.all().count()
if count == 0:
return True
return False
An easy way is to use the setting's name as the primary key in the settings table. There can't be more than one record with the same primary key, so that will allow both Django and the database to guarantee integrity.
William is right, but I guess this is the best practice
def has_add_permission(self, *args, **kwargs):
return not MyModel.objects.exists()
As reported in the official Django Documentation:
Note: Don’t use this if all you want to do is determine if at least one result exists.
It’s more efficient to use exists().
https://docs.djangoproject.com/en/dev/ref/models/querysets/#when-querysets-are-evaluated
Overwriting has_add_permission works, but in the given examples it breaks the permissions system in Django(staff without necessary permissions can add settings). Here's a one that doesn't break it:
class SettingAdmin(admin.ModelAdmin):
def has_add_permission(self, request):
base_add_permission = super(SettingAdmin, self).has_add_permission(request)
if base_add_permission:
# if there's already an entry, do not allow adding
count = Setting.objects.all().count()
if count == 0:
return True
return False
A model with a single allowed row is nothing more than a perverted form of a "persisted object" -- maybe even a "persisted singleton"? Don't do that, that's not how models work.
Check out https://github.com/danielroseman/django-dbsettings
It looks like Ali Reza's answer but you can update the existed records and return the error message to any form that uses this model. I believe it is reliable and much easy to control.
class MyModel(models.Model):
...
def clean(self):
super().clean()
if not self.id and MyModel.objects.exists():
raise ValidationError('You cannot add more somethings.')
The following is a class I have created which can be used as a singleton class.
from django.db import models
class SingletonModel(models.Model):
class Meta:
abstract = True
def save(self, *args, **kwargs):
self.__class__.objects.exclude(id=self.id).delete()
super(SingletonModel, self).save(*args, **kwargs)
#classmethod
def load(cls):
try:
return cls.objects.get()
except cls.DoesNotExist:
return cls()
From the above SingletonModel we can create multiple models, all of which will be having only one record
class ProjectSettings(SingletonModel):
max_tickets = models.IntegerField(default=15)
min_tickets = models.IntegerField(default=2)
...
We can access the only object of the settings model as follows
ProjectSettings.load().max_tickets
It is also possible to register ProjectSettings to django admin
#admin.register(ProjectSettings)
class ProjectSettingsAdmin(admin.ModelAdmin):
list_display = [field.name for field in ProjectSettings._meta.get_fields()]
def has_delete_permission(self, request, obj=None):
# Nobody is allowed to delete
return False
You can rewrite the save method on your model. Whenever the user wants to register a new record, you can check the existence then, either save the record or, reject the request.
class MyModel(models.Model):
title = models.CharField(...)
body = models.TextField(...)
def save(self, *args, **kwargs):
if MyModel.objects.exists():
raise ValueError("This model has already its record.")
else:
super().save(*args, **kwargs)
You can also use validators. I prefer to use this method.
Hope you find it useful.