Django RestFramework: 创建对象后禁用字段更新

我试图通过 Django RestFramework API 调用使我的用户模型 RESTful,这样我就可以创建用户并更新他们的配置文件。

但是,当我和我的用户一起经历一个特定的验证过程时,我不希望用户在创建帐户后能够更新用户名。我尝试使用 read _ only _ fields,但这似乎在 POST 操作中禁用了该字段,因此在创建用户对象时无法指定用户名。

我如何实现这一点呢? 现有 API 的相关代码如下。

class UserSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = User
fields = ('url', 'username', 'password', 'email')
write_only_fields = ('password',)


def restore_object(self, attrs, instance=None):
user = super(UserSerializer, self).restore_object(attrs, instance)
user.set_password(attrs['password'])
return user




class UserViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows users to be viewed or edited.
"""
serializer_class = UserSerializer
model = User


def get_permissions(self):
if self.request.method == 'DELETE':
return [IsAdminUser()]
elif self.request.method == 'POST':
return [AllowAny()]
else:
return [IsStaffOrTargetUser()]

谢谢!

33119 次浏览

It seems that you need different serializers for POST and PUT methods. In the serializer for PUT method you are able to just except the username field (or set the username field as read only).

class UserViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows users to be viewed or edited.
"""
serializer_class = UserSerializer
model = User


def get_serializer_class(self):
serializer_class = self.serializer_class


if self.request.method == 'PUT':
serializer_class = SerializerWithoutUsernameField


return serializer_class


def get_permissions(self):
if self.request.method == 'DELETE':
return [IsAdminUser()]
elif self.request.method == 'POST':
return [AllowAny()]
else:
return [IsStaffOrTargetUser()]

Check this question django-rest-framework: independent GET and PUT in same URL but different generics view

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

I used this approach:

def get_serializer_class(self):
if getattr(self, 'object', None) is None:
return super(UserViewSet, self).get_serializer_class()
else:
return SerializerWithoutUsernameField

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

老答案:

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!

Another option (DRF3 only)

class MySerializer(serializers.ModelSerializer):
...
def get_extra_kwargs(self):
extra_kwargs = super(MySerializer, self).get_extra_kwargs()
action = self.context['view'].action


if action in ['create']:
kwargs = extra_kwargs.get('ro_oncreate_field', {})
kwargs['read_only'] = True
extra_kwargs['ro_oncreate_field'] = kwargs


elif action in ['update', 'partial_update']:
kwargs = extra_kwargs.get('ro_onupdate_field', {})
kwargs['read_only'] = True
extra_kwargs['ro_onupdate_field'] = kwargs


return extra_kwargs

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()

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.

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)

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)

Another method would be to add a validation method, but throw a validation error if the instance already exists and the value has changed:

def validate_foo(self, value):
if self.instance and value != self.instance.foo:
raise serializers.ValidationError("foo is immutable once set.")
return value

In my case, I wanted a foreign key to never be updated:

def validate_foo_id(self, value):
if self.instance and value.id != self.instance.foo_id:
raise serializers.ValidationError("foo_id is immutable once set.")
return value

See also: Level-field validation in django rest framework 3.1 - access to the old value

class UserUpdateSerializer(UserSerializer):
class Meta(UserSerializer.Meta):
fields = ('username', 'email')


class UserViewSet(viewsets.ModelViewSet):
def get_serializer_class(self):
return UserUpdateSerializer if self.action == 'update' else super().get_serializer_class()

djangorestframework==3.8.2

I would suggest also looking at Django pgtrigger

This allows you to install triggers for validation. I started using it and was very pleased with its simplicity:

Here's one of their examples that prevents a published post from being updated:

import pgtrigger
from django.db import models




@pgtrigger.register(
pgtrigger.Protect(
operation=pgtrigger.Update,
condition=pgtrigger.Q(old__status='published')
)
)
class Post(models.Model):
status = models.CharField(default='unpublished')
content = models.TextField()

The advantage of this approach is it also protects you from .update() calls that bypass .save()