See object changes in post_save in django rest framework - django

I am curious if there is a way to see what has changed on an object after saving it using the Django Rest Framework. I have some special behavior I need to check if a field has been changed from its original value that I was hoping to handle using the post_save on generics.RetrieveUpdateDestroyAPIView.
My first thought was to check using pre_save but it seems that pre_save's object argument already has the changes applied to it.

OLD ANSWER for django rest framework version 2.3.12:
To check if anything has changed on update, you will have to compare the unchanged model instance which is self.object with the changed model instance which is serializer.object.
The object argument which is passed to the pre_save method is the serializer.object which is not yet saved in the database with the new changes.
The unchanged model instance is the self.object which has been fetched from the database using self.get_object_or_none(). Compare it with the obj argument in the pre_save method.
def pre_save(self,obj):
unchanged_instance = self.object
changed_instance = obj
..... # comparison code
NEW ANSWER for django rest framework 3.3:
pre_save and post_save are no longer valid.
Now you can place any pre save or post save logic in perform_update method. For example:
def perform_update(self, serializer):
# NOTE: serializer.instance gets updated after calling save
# if you want to use the old_obj after saving the serializer you should
# use self.get_object() to get the old instance.
# other wise serializer.instance would do fine
old_obj = self.get_object()
new_data_dict = serializer.validated_data
# pre save logic
if old_obj.name != new_data_dict['name']:
do_something
.....
new_obj = serializer.save()
# post save logic
......

I was able to do this with help from model_utils FieldTracker. You can install a tracker on the relevant model, then in pre_save (by post_save it's too late) you can do this:
def pre_save(self, obj):
if hasattr(obj, 'tracker'):
self.changed_fields = obj.tracker.changed()
else:
self.changed_fields = None
changed_fields will look like this: {'is_public': False, 'desc': None}

Related

Django rest framework disable field update after condition

I'm using DRF and I need to disable the update of a field if a condition on the same model is respected.
example:
class Foo(models.Model):
text = models.CharField()
checkfield = models.BooleanField(default=False)
text can be modified unless checkfield is True.
So if Foo.checkfield is True Foo.text cannot be modified via DRF API.
What is the best way to do so?
I think Advanced serializers will do what you want.
Just create your custom serializer and in your view, check the value of checkfield. If it's true, pass it the text argument so it enables the field in the serializer.
Btw, since you only need one fixed extra field to be removed or added, instead of passing the fields argument as in the example, you can pass it something like enable_text=checkfield and then add the text field to the 'fields' variable in your serializer according to the value of 'checkfield'.
update to clarify:
Define your serializer without the text field. Then in your ModelViewSet, override the update method so you get the serializer this way (I think the get_serializer() method does not allow to pass extra args):
YourSerializer(object, enable_text=True)
And, inside your serializer init method, when 'enable_text' is True, you add the text field to the self.fields attribute.
I haven't tested if this works but I think it is the way to go.
Edit with snippet and modification
I've been digging a bit with what I explained and turned out it is a bit messy for the simple modification you are trying to do. What I've come up with is just to override the update method in your ViewSet. Here is the code:
from rest_framework import viewsets, status
from rest_framework.response import Response
from models import Test, TestSerializer
class TestViewSet(viewsets.ModelViewSet):
queryset = Test.objects.all()
serializer_class = TestSerializer
def update(self, request, *args, **kwargs):
partial = kwargs.pop('partial', False)
self.object = self.get_object_or_none()
if 'enable_text' in request.DATA and request.DATA['enable_text'] == True:
request.DATA['text'] = self.object.text
serializer = self.get_serializer(self.object, data=request.DATA,
files=request.FILES, partial=partial)
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
try:
self.pre_save(serializer.object)
except ValidationError as err:
# full_clean on model instance may be called in pre_save,
# so we have to handle eventual errors.
return Response(err.message_dict, status=status.HTTP_400_BAD_REQUEST)
if self.object is None:
self.object = serializer.save(force_insert=True)
self.post_save(self.object, created=True)
return Response(serializer.data, status=status.HTTP_201_CREATED)
self.object = serializer.save(force_update=True)
self.post_save(self.object, created=False)
return Response(serializer.data, status=status.HTTP_200_OK)
This code is taken from the rest_framework source code for the UpdateMixin. Take special attention at lines if 'enable_text' in request.DATA and... and request.DATA['text'] = self.object.text. Those are the ones allowing you to do the funcionality you need. Basically:
If you send the enable_text with True along with text, text will be modified.
If you send the enable_text with False along with text, it will be ignored.
Note that this code only takes into account the value of enable_text passed in the current request. You maybe want also that if enable_text is not in the current request, to check the value of enable_text in the self.object (which is the database instance itself).

Check previous model value on save

I have a model that saves an Excursion. The user can change this excursion, but I need to know what the excursion was before he change it, because I keep track of how many "bookings" are made per excursion, and if you change your excursion, I need to remove one booking from the previous excursion.
Im not entirely sure how this should be done.
Im guessing you use a signal for this?
Should I use pre_save, pre_init or what would be the best for this?
pre_save is not the correct one it seems, as it prints the new values, not the "old value" as I expected
#receiver(pre_save, sender=Delegate)
def my_callback(sender, instance, *args, **kwargs):
print instance.excursion
Do you have several options.
First one is to overwrite save method:
#Delegate
def save(self, *args, **kwargs):
if self.pk:
previous_excursion = Delegate.objects.get(self.pk).excursion
super(Model, self).save(*args, **kwargs)
if self.pk and self.excursion != previous_excursion:
#change booking
Second one is binding function to post save signal + django model utils field tracker:
#receiver(post_save, sender=Delegate)
def create_change_booking(sender,instance, signal, created, **kwargs):
if created:
previous_excursion = get it from django model utils field tracker
#change booking
And another solution is in pre_save as you are running:
#receiver(pre_save, sender=Delegate)
def my_callback(sender, instance, *args, **kwargs):
previous_excursion = Delegate.objects.get(self.pk).excursion
if instance.pk and instance.excursion != previous_excursion:
#change booking
You can use django model utils to track django model fields. check this example.
pip install django-model-utils
Then you can define your model and use fieldtracker in your model .
from django.db import models
from model_utils import FieldTracker
class Post(models.Model):
title = models.CharField(max_length=100)
body = models.TextField()
tracker = FieldTracker()
status = models.CharField(choices=STATUS, default=STATUS.draft, max_length=20)
after that in post save you can use like this :
#receiver(post_save, sender=Post)
def my_callback(sender, instance,*args, **kwargs):
print (instance.title)
print (instance.tracker.previous('title'))
print (instance.status)
print (instance.tracker.previous('status'))
This will help you a lot to do activity on status change. as because overwrite save method is not good idea.
As an alternative and if you are using Django forms:
The to-be version of your instance is stored in form.instance of the Django form of your model. On save, validations are run and this new version is applied to the model and then the model is saved.
Meaning that you can check differences between the new and the old version by comparing form.instance to the current model.
This is what happens when the Django Admin's save_model method is called. (See contrib/admin/options.py)
If you can make use of Django forms, this is the most Djangothic way to go, I'd say.
This is the essence on using the Django form for handling data changes:
form = ModelForm(request.POST, request.FILES, instance=obj)
new_object = form.instance # not saved yet
# changes are stored in form.changed_data
new_saved_object = form.save()
form.changed_data will contain the changed fields which means that it is empty if there are no changes.
There's yet another option:
Django's documentation has an example showing exactly how you could do this by overriding model methods.
In short:
override Model.from_db() to add a dynamic attribute containing the original values
override the Model.save() method to compare the new values against the originals
This has the advantage that it does not require an additional database query.

Django REST Framework ModelSerializer get_or_create functionality

When I try to deserialize some data into an object, if I include a field that is unique and give it a value that is already assigned to an object in the database, I get a key constraint error. This makes sense, as it is trying to create an object with a unique value that is already in use.
Is there a way to have a get_or_create type of functionality for a ModelSerializer? I want to be able to give the Serializer some data, and if an object exists that has the given unique field, then just return that object.
In my experience nmgeek's solution won't work in DRF 3+ as serializer.is_valid() correctly honors the model's unique_together constraint. You can work around this by removing the UniqueTogetherValidator and overriding your serializer's create method.
class MyModelSerializer(serializers.ModelSerializer):
def run_validators(self, value):
for validator in self.validators:
if isinstance(validator, validators.UniqueTogetherValidator):
self.validators.remove(validator)
super(MyModelSerializer, self).run_validators(value)
def create(self, validated_data):
instance, _ = models.MyModel.objects.get_or_create(**validated_data)
return instance
class Meta:
model = models.MyModel
The Serializer restore_object method was removed starting with the 3.0 version of REST Framework.
A straightforward way to add get_or_create functionality is as follows:
class MyObjectSerializer(serializers.ModelSerializer):
class Meta:
model = MyObject
fields = (
'unique_field',
'other_field',
)
def get_or_create(self):
defaults = self.validated_data.copy()
identifier = defaults.pop('unique_field')
return MyObject.objects.get_or_create(unique_field=identifier, defaults=defaults)
def post(self, request, format=None):
serializer = MyObjectSerializer(data=request.data)
if serializer.is_valid():
instance, created = serializer.get_or_create()
if not created:
serializer.update(instance, serializer.validated_data)
return Response(serializer.data, status=status.HTTP_202_ACCEPTED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
However, it doesn't seem to me that the resulting code is any more compact or easy to understand than if you query if the instance exists then update or save depending upon the result of the query.
#Groady's answer works, but you have now lost your ability to validate the uniqueness when creating new objects (UniqueValidator has been removed from your list of validators regardless the cicumstance). The whole idea of using a serializer is that you have a comprehensive way to create a new object that validates the integrity of the data you want to use to create the object. Removing validation isn't what you want. You DO want this validation to be present when creating new objects, you'd just like to be able to throw data at your serializer and get the right behavior under the hood (get_or_create), validation and all included.
I'd recommend overwriting your is_valid() method on the serializer instead. With the code below you first check to see if the object exists in your database, if not you proceed with full validation as usual. If it does exist you simply attach this object to your serializer and then proceed with validation as usual as if you'd instantiated the serializer with the associated object and data. Then when you hit serializer.save() you'll simply get back your already created object and you can have the same code pattern at a high level: instantiate your serializer with data, call .is_valid(), then call .save() and get returned your model instance (a la get_or_create). No need to overwrite .create() or .update().
The caveat here is that you will get an unnecessary UPDATE transaction on your database when you hit .save(), but the cost of one extra database call to have a clean developer API with full validation still in place seems worthwhile. It also allows you the extensibility of using custom models.Manager and custom models.QuerySet to uniquely identify your model from a few fields only (whatever the primary identifying fields may be) and then using the rest of the data in initial_data on the Serializer as an update to the object in question, thereby allowing you to grab unique objects from a subset of the data fields and treat the remaining fields as updates to the object (in which case the UPDATE call would not be extra).
Note that calls to super() are in Python3 syntax. If using Python 2 you'd want to use the old style: super(MyModelSerializer, self).is_valid(**kwargs)
from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned
class MyModelSerializer(serializers.ModelSerializer):
def is_valid(self, raise_exception=False):
if hasattr(self, 'initial_data'):
# If we are instantiating with data={something}
try:
# Try to get the object in question
obj = Security.objects.get(**self.initial_data)
except (ObjectDoesNotExist, MultipleObjectsReturned):
# Except not finding the object or the data being ambiguous
# for defining it. Then validate the data as usual
return super().is_valid(raise_exception)
else:
# If the object is found add it to the serializer. Then
# validate the data as usual
self.instance = obj
return super().is_valid(raise_exception)
else:
# If the Serializer was instantiated with just an object, and no
# data={something} proceed as usual
return super().is_valid(raise_exception)
class Meta:
model = models.MyModel
There are a couple of scenarios where a serializer might need to be able to get or create Objects based on data received by a view - where it's not logical for the view to do the lookup / create functionality - I ran into this this week.
Yes it is possible to have get_or_create functionality in a Serializer. There is a hint about this in the documentation here: http://www.django-rest-framework.org/api-guide/serializers#specifying-which-fields-should-be-write-only where:
restore_object method has been written to instantiate new users.
The instance attribute is fixed as None to ensure that this method is not used to update Users.
I think you can go further with this to put full get_or_create into the restore_object - in this instance loading Users from their email address which was posted to a view:
class UserFromEmailSerializer(serializers.ModelSerializer):
class Meta:
model = get_user_model()
fields = [
'email',
]
def restore_object(self, attrs, instance=None):
assert instance is None, 'Cannot update users with UserFromEmailSerializer'
(user_object, created) = get_user_model().objects.get_or_create(
email=attrs.get('email')
)
# You can extend here to work on `user_object` as required - update etc.
return user_object
Now you can use the serializer in a view's post method, for example:
def post(self, request, format=None):
# Serialize "new" member's email
serializer = UserFromEmailSerializer(data=request.DATA)
if not serializer.is_valid():
return Response(serializer.errors,
status=status.HTTP_400_BAD_REQUEST)
# Loaded or created user is now available in the serializer object:
person=serializer.object
# Save / update etc.
A better way of doing this is to use the PUT verb instead, then override the get_object() method in the ModelViewSet. I answered this here: https://stackoverflow.com/a/35024782/3025825.
A simple workaround is to use to_internal_value method:
class MyModelSerializer(serializers.ModelSerializer):
def to_internal_value(self, validated_data):
instance, _ = models.MyModel.objects.get_or_create(**validated_data)
return instance
class Meta:
model = models.MyModel
I know it's a hack, but in case if you need a quick solution
P.S. Of course, editing is not supported
class ExpoDeviceViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticated, ]
serializer_class = ExpoDeviceSerializer
def get_queryset(self):
user = self.request.user
return ExpoDevice.objects.filter(user=user)
def perform_create(self, serializer):
existing_token = self.request.user.expo_devices.filter(
token=serializer.validated_data['token']).first()
if existing_token:
return existing_token
return serializer.save(user=self.request.user)
In case anyone needs to create an object if it does not exist on GET request:
class MyModelViewSet(viewsets.ModelViewSet):
queryset = models.MyModel.objects.all()
serializer_class = serializers.MyModelSerializer
def retrieve(self, request, pk=None):
instance, _ = models.MyModel.objects.get_or_create(pk=pk)
serializer = self.serializer_class(instance)
return response.Response(serializer.data)
Another solution, as I found that UniqueValidator wasn't in the validators for the serializer, but rather in the field's validators.
def is_valid(self, raise_exception=False):
self.fields["my_field_to_fix"].validators = [
v
for v in self.fields["my_field_to_fix"].validators
if not isinstance(v, validators.UniqueValidator)
]
return super().is_valid(raise_exception)

Modify object before save and passing it to serializer

I'm trying to set some fields before saving an object that a user wants to insert. For example, if a user wants to create a new instance, before saving it, I want to set the field owner equal to request.user and then call the create method from the parent. I've achieved this with the following code:
class ClassView(viewsets.ModelViewSet):
queryset = ModelClass.objects.all()
serializer_class = ModelClassSerializer
def create(self, request, pk = None):
if ModelClass.objects.filter(pk = request.user.id):
return Response({'detail' : "This user is already inserted" }, status = status.HTTP_401_UNAUTHORIZED)
return super(ClassView, self).create(request, pk = None)
def pre_save(self, obj):
obj.user_id = ModelClass.objects.get(pk = self.request.user.id)
It could be also that I want to set an attribute of the model according to some calculation with values coming from the POST request (those values are established as fields in the serializer).
Is the pre_save solution the correct way to go or am I missing something?
Thanks in advance.
I would say this is the correct way to go but if you simply want to set the object's user to the current request user, instead of:
obj.user_id = ModelClass.objects.get(pk = self.request.user.id)
...just use:
obj.user = self.request.user
The rest framework pre_save hook is there for your exact requirement but there exists other ones you may find useful. See http://www.django-rest-framework.org/api-guide/generic-views#genericapiview - under Save / deletion hooks.
However, if you require this data to be saved on the object instance outside of the rest framework (i.e. additionally within a normal Django view) you will most probably want to use the Django pre_save signal and hook your model up to it. That way the request user will be stored each time the object is saved, not just via the rest framework: https://docs.djangoproject.com/en/dev/ref/signals/

In a django model custom save() method, how should you identify a new object?

I want to trigger a special action in the save() method of a Django Model object when I'm saving a new record (not updating an existing record.)
Is the check for (self.id != None) necessary and sufficient to guarantee the self record is new and not being updated? Any special cases this might overlook?
Alternative way to checking self.pk we can check self._state of the model
self._state.adding is True creating
self._state.adding is False updating
I got it from this page
Updated: With the clarification that self._state is not a private instance variable, but named that way to avoid conflicts, checking self._state.adding is now the preferable way to check.
self.pk is None:
returns True within a new Model object, unless the object has a UUIDField as its primary_key.
The corner case you might have to worry about is whether there are uniqueness constraints on fields other than the id (e.g., secondary unique indexes on other fields). In that case, you could still have a new record in hand, but be unable to save it.
Checking self.id assumes that id is the primary key for the model. A more generic way would be to use the pk shortcut.
is_new = self.pk is None
The check for self.pk == None is not sufficient to determine if the object is going to be inserted or updated in the database.
The Django O/RM features an especially nasty hack which is basically to check if there is something at the PK position and if so do an UPDATE, otherwise do an INSERT (this gets optimised to an INSERT if the PK is None).
The reason why it has to do this is because you are allowed to set the PK when an object is created. Although not common where you have a sequence column for the primary key, this doesn't hold for other types of primary key field.
If you really want to know you have to do what the O/RM does and look in the database.
Of course you have a specific case in your code and for that it is quite likely that self.pk == None tells you all you need to know, but it is not a general solution.
You could just connect to post_save signal which sends a "created" kwargs, if true, your object has been inserted.
http://docs.djangoproject.com/en/stable/ref/signals/#post-save
Check for self.id and the force_insert flag.
if not self.pk or kwargs.get('force_insert', False):
self.created = True
# call save method.
super(self.__class__, self).save(*args, **kwargs)
#Do all your post save actions in the if block.
if getattr(self, 'created', False):
# So something
# Do something else
This is handy because your newly created object(self) has it pk value
I'm very late to this conversation, but I ran into a problem with the self.pk being populated when it has a default value associated with it.
The way I got around this is adding a date_created field to the model
date_created = models.DateTimeField(auto_now_add=True)
From here you can go
created = self.date_created is None
For a solution that also works even when you have a UUIDField as a primary key (which as others have noted isn't None if you just override save), you can plug into Django's post_save signal. Add this to your models.py:
from django.db.models.signals import post_save
from django.dispatch import receiver
#receiver(post_save, sender=MyModel)
def mymodel_saved(sender, instance, created, **kwargs):
if created:
# do extra work on your instance, e.g.
# instance.generate_avatar()
# instance.send_email_notification()
pass
This callback will block the save method, so you can do things like trigger notifications or update the model further before your response is sent back over the wire, whether you're using forms or the Django REST framework for AJAX calls. Of course, use responsibly and offload heavy tasks to a job queue instead of keeping your users waiting :)
rather use pk instead of id:
if not self.pk:
do_something()
It is the common way to do so.
the id will be given while saved first time to the db
> def save_model(self, request, obj, form, change):
> if form.instance._state.adding:
> form.instance.author = request.user
> super().save_model(request, obj, form, change)
> else:
> obj.updated_by = request.user.username
>
> super().save_model(request, obj, form, change)
Would this work for all the above scenarios?
if self.pk is not None and <ModelName>.objects.filter(pk=self.pk).exists():
...
In python 3 and django 3 this is what's working in my project:
def save_model(self, request, obj, form, change):
if not change:
#put your code here when adding a new object.
To know whether you are updating or inserting the object (data), use self.instance.fieldname in your form. Define a clean function in your form and check whether the current value entry is same as the previous, if not then you are updating it.
self.instance and self.instance.fieldname compare with the new value