Django Rest Framework: Disable field update after object is created

后端 未结 11 1339
独厮守ぢ
独厮守ぢ 2020-11-30 20:15

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

相关标签:
11条回答
  • 2020-11-30 20:38

    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)
    
    0 讨论(0)
  • 2020-11-30 20:43

    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()
    
    0 讨论(0)
  • 2020-11-30 20:43

    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!

    0 讨论(0)
  • 2020-11-30 20:45

    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.

    0 讨论(0)
  • 2020-11-30 20:46

    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
    
    0 讨论(0)
  • 2020-11-30 20:47

    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)

    0 讨论(0)
提交回复
热议问题