【问题标题】:Django Rest Framework: Disable field update after object is createdDjango Rest Framework:创建对象后禁用字段更新
【发布时间】:2014-04-03 04:33:58
【问题描述】:

我正在尝试通过 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()]

谢谢!

【问题讨论】:

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

标签: django rest django-rest-framework


【解决方案1】:

对于 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: independent GET and PUT in same URL but different generics view

【讨论】:

  • 这就像一个魅力。我刚刚将 PATCH 添加到以不同方式序列化的方法列表中。谢谢!
  • 由于大型序列化程序的代码重复,这很快变得难以管理。
  • 最初是从那个开始的,它工作得很好。但是,如果您想使用 swagger-codegen 生成前端代码,并且由于不同的序列化程序架构而最终拥有多个不同的前端类,那么您需要走不同的道路。
【解决方案2】:

另一个选项(仅限 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

【讨论】:

  • 好方法。一个小建议,我会使用if self.instance is None: 而不是 view.action 检查。这允许您在标准 drf 视图之外使用序列化程序。 IE。我有一个场景,我在 celery 任务中使用序列化程序,同时从外部 csv 文件导入数据。在这种情况下 view 属性将为 null,但 self.instance 检查仍然有效。
  • ViewSet 中没有属性action
【解决方案3】:

另一种方法是添加验证方法,但如果实例已存在且值已更改,则会引发验证错误:

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         

另请参阅:Level-field validation in django rest framework 3.1 - access to the old value

【讨论】:

  • 我同意用户应该知道为什么该字段无法更新,这是完成这项工作的唯一答案。与霍贾特的回答不同。在这里,您仍然可以重新发送相同的序列化对象,而不会拒绝请求。
【解决方案4】:

我的方法是在使用泛型视图类时修改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 属性的其他解决方案中发生的情况。
  • (无法编辑我上面的评论):read_only 实际上并没有给出验证错误。但我仍然认为在这种情况下给出一个验证错误是一种很好的做法,因为如果用户能够 POST 一个字段但如果它被静默剥离却不能 PUT 它可能是不合逻辑的。
【解决方案5】:

我使用了这种方法:

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

【讨论】:

    【解决方案6】:

    更新:

    原来 Rest Framework 已经配备了这个功能。拥有“仅创建”字段的正确方法是使用 CreateOnlyDefault() 选项。

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

    旧答案:

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

    对我来说,仅仅因为你想阻止一个字段被更新就拥有两个不同的序列化器是没有意义的。我遇到了同样的问题,我使用的方法是在 Serializer 类中实现我自己的 validate 方法。就我而言,我不想更新的字段称为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
    

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

    删除线:

    raise ValidationError('Cannot update owner')
    

    并将其替换为:

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

    希望这会有所帮助!

    【讨论】:

    • CreateOnlyDefault 不是API提交的数据,它是用于初始创建时在服务器上生成的东西,例如创建日期,默认情况下对用户是只读的。跨度>
    【解决方案7】:

    更多通用方式到“创建对象后禁用字段更新” - 根据 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 方法)

    【讨论】:

      【解决方案8】:

      This 的帖子提到了实现这一目标的四种不同方法。

      这是我认为最干净的方式:[不得编辑收藏]

      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)
      

      【讨论】:

        【解决方案9】:

        另一个解决方案(除了创建一个单独的序列化程序)是如果设置了实例(这意味着它是一个 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
        

        【讨论】:

        【解决方案10】:

        如果您不想创建另一个序列化程序,您可能想尝试在MyViewSet 中自定义get_serializer_class()。这对我来说对于简单的项目很有用。

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

        这是 getattr() 的对应物。这 参数是一个对象、一个字符串和一个任意值。字符串 可以命名现有属性或新属性。功能 如果对象允许,则将值分配给属性。为了 例如,setattr(x, 'foobar', 123) 等价于 x.foobar = 123。

        【讨论】:

        • 但是每次启动应用程序时都会调用此函数,如何在每次调用方法“PATCH”时加载它?谢谢
        【解决方案11】:
        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

        【讨论】:

          【解决方案12】:

          我建议也看看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()
          

          这种方法的优点是它还可以保护您免受绕过 .save().update() 调用

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2015-09-19
            • 2018-01-08
            • 1970-01-01
            • 2014-07-07
            • 1970-01-01
            • 1970-01-01
            • 2020-11-11
            • 1970-01-01
            相关资源
            最近更新 更多