I\'m trying to make my User model RESTful via Django Rest Framework API calls, so that I can create users as well as update their profiles.
However, as I go through
This post mentions four different ways to achieve this goal.
This was the cleanest way I think: [collection must not be edited]
class DocumentSerializer(serializers.ModelSerializer):
def update(self, instance, validated_data):
if 'collection' in validated_data:
raise serializers.ValidationError({
'collection': 'You must not change this field.',
})
return super().update(instance, validated_data)
My approach is to modify the perform_update
method when using generics view classes. I remove the field when update is performed.
class UpdateView(generics.UpdateAPIView):
...
def perform_update(self, serializer):
#remove some field
rem_field = serializer.validated_data.pop('some_field', None)
serializer.save()
UPDATE:
Turns out Rest Framework already comes equipped with this functionality. The correct way of having a "create-only" field is by using the CreateOnlyDefault()
option.
I guess the only thing left to say is Read the Docs!!! http://www.django-rest-framework.org/api-guide/validators/#createonlydefault
Old Answer:
Looks I'm quite late to the party but here are my two cents anyway.
To me it doesn't make sense to have two different serializers just because you want to prevent a field from being updated. I had this exact same issue and the approach I used was to implement my own validate
method in the Serializer class. In my case, the field I don't want updated is called owner
. Here is the relevant code:
class BusinessSerializer(serializers.ModelSerializer):
class Meta:
model = Business
pass
def validate(self, data):
instance = self.instance
# this means it's an update
# see also: http://www.django-rest-framework.org/api-guide/serializers/#accessing-the-initial-data-and-instance
if instance is not None:
originalOwner = instance.owner
# if 'dataOwner' is not None it means they're trying to update the owner field
dataOwner = data.get('owner')
if dataOwner is not None and (originalOwner != dataOwner):
raise ValidationError('Cannot update owner')
return data
pass
pass
And here is a unit test to validate it:
def test_owner_cant_be_updated(self):
harry = User.objects.get(username='harry')
jack = User.objects.get(username='jack')
# create object
serializer = BusinessSerializer(data={'name': 'My Company', 'owner': harry.id})
self.assertTrue(serializer.is_valid())
serializer.save()
# retrieve object
business = Business.objects.get(name='My Company')
self.assertIsNotNone(business)
# update object
serializer = BusinessSerializer(business, data={'owner': jack.id}, partial=True)
# this will be False! owners cannot be updated!
self.assertFalse(serializer.is_valid())
pass
I raise a ValidationError
because I don't want to hide the fact that someone tried to perform an invalid operation. If you don't want to do this and you want to allow the operation to be completed without updating the field instead, do the following:
remove the line:
raise ValidationError('Cannot update owner')
and replace it with:
data.update({'owner': originalOwner})
Hope this helps!
If you don't want to create another serializer, you may want to try customizing get_serializer_class()
inside MyViewSet
. This has been useful to me for simple projects.
# Your clean serializer
class MySerializer(serializers.ModelSerializer):
class Meta:
model = MyModel
fields = '__all__'
# Your hardworking viewset
class MyViewSet(MyParentViewSet):
serializer_class = MySerializer
model = MyModel
def get_serializer_class(self):
serializer_class = self.serializer_class
if self.request.method in ['PUT', 'PATCH']:
# setting `exclude` while having `fields` raises an error
# so set `read_only_fields` if request is PUT/PATCH
setattr(serializer_class.Meta, 'read_only_fields', ('non_updatable_field',))
# set serializer_class here instead if you have another serializer for finer control
return serializer_class
setattr(object, name, value)
This is the counterpart of getattr(). The arguments are an object, a string and an arbitrary value. The string may name an existing attribute or a new attribute. The function assigns the value to the attribute, provided the object allows it. For example, setattr(x, 'foobar', 123) is equivalent to x.foobar = 123.
Another solution (apart from creating a separate serializer) would be to pop the username from attrs in the restore_object method if the instance is set (which means it's a PATCH / PUT method):
def restore_object(self, attrs, instance=None):
if instance is not None:
attrs.pop('username', None)
user = super(UserSerializer, self).restore_object(attrs, instance)
user.set_password(attrs['password'])
return user
More universal way to "Disable field update after object is created" - adjust read_only_fields per View.action
1) add method to Serializer (better to use your own base cls)
def get_extra_kwargs(self):
extra_kwargs = super(BasePerTeamSerializer, self).get_extra_kwargs()
action = self.context['view'].action
actions_readonly_fields = getattr(self.Meta, 'actions_readonly_fields', None)
if actions_readonly_fields:
for actions, fields in actions_readonly_fields.items():
if action in actions:
for field in fields:
if extra_kwargs.get(field):
extra_kwargs[field]['read_only'] = True
else:
extra_kwargs[field] = {'read_only': True}
return extra_kwargs
2) Add to Meta of serializer dict named actions_readonly_fields
class Meta:
model = YourModel
fields = '__all__'
actions_readonly_fields = {
('update', 'partial_update'): ('client', )
}
In the example above client
field will become read-only for actions: 'update', 'partial_update' (ie for PUT, PATCH methods)