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


70

我试图通过Django Rest Framework 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()]

谢谢!


1
嘿,我之前没有找到这个问题,所以我重复了。无论如何,@ JPG发布的答案似乎比这里的大多数答案要好。我不知道如何正确处理这个(我是新来的堆栈溢出),所以这里是我的问题stackoverflow.com/questions/52114443/...
扬Kaifer

Answers:


63

似乎您需要用于POST和PUT方法的不同序列化器。在用于PUT的序列化器方法中,您只能删除用户名字段(或将用户名字段设置为只读)。

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

django-rest-framework检查此问题:在相同的URL中使用独立的GET和PUT,但使用不同的泛型视图


就像魅力一样。我只是将PATCH添加到了不同序列化的方法列表中。谢谢!
布拉德·雷登

1
由于大型串行器的代码重复,这很快变得难以管理。
BjornW

30

另一种选择(仅DRF3)

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

15
很棒的方法。一个较小的建议,我将使用if self.instance is None:代替view.action检查。这使您可以在标准drf视图之外使用序列化程序。即我有一种方案,当我从外部csv文件导入数据时,我在芹菜任务中使用了序列化程序。在这种情况下,view属性将为null,但是self.instance检查仍然可以进行。
brocksamson

1
actionViewSet中没有属性。
PythonEnthusiast

7

另一种方法是添加验证方法,但是如果实例已经存在并且值已更改,则抛出验证错误:

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

就我而言,我希望永远不要更新外键:

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         

另请参见:Django Rest Framework 3.1中的级别字段验证-访问旧值


1
我同意用户应该知道为什么不能更新该字段,并且这是完成此任务的唯一答案。不同于霍贾特的答案。在这里,仍然允许您重新发送相同的序列化对象,而不会拒绝该请求。
杰里

5

我的方法是perform_update在使用泛型视图类时修改方法。我在执行更新时删除了该字段。

class UpdateView(generics.UpdateAPIView):
    ...
    def perform_update(self, serializer):
        #remove some field
        rem_field = serializer.validated_data.pop('some_field', None)
        serializer.save()

但这并没有给API用户提供任何提示,说明为什么它不起作用。我认为您需要为该字段引发一个适当的ValidationError,这就是在其他解决方案中发生的事情,这些解决方案使用额外的kwargs方法动态修改了字段上的read_only / write_only属性。
BjornW

(无法在上方编辑我的评论):read_only实际上并未给出验证错误。但是我仍然认为,在这种情况下,最好给出一个验证错误,因为如果用户将字段静默剥离,那么它能够张贴一个字段但不放入它可能对用户来说是不合逻辑的。
BjornW

3

更新:

事实证明Rest Framework已经配备了此功能。具有“仅创建”字段的正确方法是使用该CreateOnlyDefault()选项。

我想剩下唯一要说的就是阅读文档!!! http://www.django-rest-framework.org/api-guide/validators/#createonlydefault

旧答案:

看起来我参加聚会很晚,但是无论如何这是我的两分钱。

对我来说,拥有两个不同的序列化器只是因为您想防止某个字段被更新而没有意义。我遇到了完全相同的问题,而我使用的validate方法是在Serializer类中实现自己的方法。就我而言,我不想更新的字段称为owner。以下是相关代码:

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

这是一个验证它的单元测试:

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

我提出a是ValidationError因为我不想掩盖有人试图执行无效操作的事实。如果您不想执行此操作,并且希望在不更新字段的情况下完成操作,请执行以下操作:

删除行:

raise ValidationError('Cannot update owner')

并替换为:

data.update({'owner': originalOwner})

希望这可以帮助!


22
CreateOnlyDefault不是API提交的数据,它是在服务器上生成的初始创建,如创建日期,它是只读的默认情况下,用户的事情。
Zequez '16

2

我使用了这种方法:

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

2

“在创建对象后禁用字段更新”的更通用方法-根据View.action调整read_only_fields

1)向Serializer添加方法(最好使用自己的基本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)添加到名为actions_readonly_fields的序列化字典的Meta

class Meta:
    model = YourModel
    fields = '__all__'
    actions_readonly_fields = {
        ('update', 'partial_update'): ('client', )
    }

在上面的示例中,client字段对于操作:'update','partial_update'(即对于PUT,PATCH方法)将变为只读


2

这篇文章提到了实现此目标的四种不同方法。

我认为这是最干净的方法:[不得编辑集合]

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)

1

另一个解决方案(除了创建单独的序列化程序之外)是,如果实例已设置(这是PATCH / PUT方法),则从restore_object方法中的attrs中弹出用户名:

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


1

如果您不想创建另一个序列化器,则可能要尝试get_serializer_class()在内部自定义MyViewSet。对于简单项目,这对我很有用。

# 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(对象,名称,值)

这是与getattr()相对应的。参数是对象,字符串和任意值。该字符串可以命名现有属性或新属性。如果对象允许,该函数将值分配给属性。例如,setattr(x,'foobar',123)等效于x.foobar = 123。


1
但是,每次启动应用程序时都会调用此函数,如何在每次调用“ PATCH”方法时将其加载?谢谢
拉(Naella)'17

1
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


0

我建议也看看Django pgtrigger

这使您可以安装触发器进行验证。我开始使用它,并对其简单性感到非常满意:

这是阻止发布的帖子被更新的示例之一:

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

这种方法的优点是它还可以保护您免受.update()旁路呼叫的侵害.save()

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.