web-dev-qa-db-fra.com

Django Rest Framework: désactiver la mise à jour des champs après la création de l'objet

J'essaie de rendre mon modèle utilisateur RESTful via Django Appels API Rest Framework, afin de pouvoir créer des utilisateurs et mettre à jour leurs profils.

Cependant, comme je passe par un processus de vérification particulier avec mes utilisateurs, je ne veux pas que les utilisateurs aient la possibilité de mettre à jour le nom d'utilisateur après la création de leur compte. J'ai essayé d'utiliser read_only_fields, mais cela semblait désactiver ce champ dans les opérations POST, donc je n'ai pas pu spécifier de nom d'utilisateur lors de la création de l'objet utilisateur.

Comment puis-je procéder pour l'implémenter? Le code pertinent pour l'API tel qu'il existe maintenant est ci-dessous.

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

Merci!

56
Brad Reardon

Il semble que vous ayez besoin de différents sérialiseurs pour les méthodes POST et PUT. Dans le sérialiseur pour la méthode PUT, vous pouvez simplement exclure le champ du nom d'utilisateur (ou définir le champ du nom d'utilisateur en lecture seule).

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

Cochez cette question Django-rest-framework: GET et PUT indépendants dans la même URL mais vue générique différente

52

Une autre option (DRF3 uniquement)

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
22
VoSi

Mon approche est de modifier le perform_update méthode lors de l'utilisation de classes de vue génériques. Je supprime le champ lorsque la mise à jour est effectuée.

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

MISE À JOUR:

Il s'avère que Rest Framework est déjà équipé de cette fonctionnalité. La façon correcte d'avoir un champ "créer uniquement" consiste à utiliser l'option CreateOnlyDefault().

Je suppose que la seule chose qui reste à dire est de lire les documents !!! http://www.Django-rest-framework.org/api-guide/validators/#createonlydefault

Ancienne réponse:

On dirait que je suis assez tard pour la fête mais voici mes deux cents quand même.

Pour moi, cela n'a pas de sens d'avoir deux sérialiseurs différents simplement parce que vous voulez empêcher la mise à jour d'un champ. J'ai eu exactement le même problème et l'approche que j'ai utilisée était d'implémenter ma propre méthode validate dans la classe Serializer. Dans mon cas, le champ que je ne souhaite pas mettre à jour s'appelle owner. Voici le code pertinent:

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

Et voici un test unitaire pour le valider:

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

Je lève un ValidationError parce que je ne veux pas cacher le fait que quelqu'un a essayé d'effectuer une opération invalide. Si vous ne souhaitez pas le faire et que vous souhaitez autoriser l'opération à se terminer sans mettre à jour le champ à la place, procédez comme suit:

supprimer la ligne:

raise ValidationError('Cannot update owner')

et remplacez-le par:

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

J'espère que cela t'aides!

3
LuisCien

J'ai utilisé cette approche:

def get_serializer_class(self):
    if getattr(self, 'object', None) is None:
        return super(UserViewSet, self).get_serializer_class()
    else:
        return SerializerWithoutUsernameField
2
Alex Rothberg
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

1
gzerone

Une autre méthode serait d'ajouter une méthode de validation, mais de renvoyer une erreur de validation si l'instance existe déjà et que la valeur a changé:

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

Dans mon cas, je voulais qu'une clé étrangère ne soit jamais mise à jour:

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         

Voir aussi: Validation de champ de niveau dans Django rest framework 3.1 - accès à l'ancienne valeur

1
rrauenza

Une autre solution (en dehors de la création d'un sérialiseur séparé) serait de faire apparaître le nom d'utilisateur dans attrs dans la méthode restore_object si l'instance est définie (ce qui signifie qu'il s'agit d'une méthode PATCH/PUT):

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
Pawel Kozela

Si vous ne voulez pas créer un autre sérialiseur, vous pouvez essayer de personnaliser get_serializer_class() à l'intérieur MyViewSet. Cela m'a été utile pour des projets simples.

# 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 (objet, nom, valeur)

C'est l'équivalent de getattr (). Les arguments sont un objet, une chaîne et une valeur arbitraire. La chaîne peut nommer un attribut existant ou un nouvel attribut. La fonction attribue la valeur à l'attribut, à condition que l'objet le permette. Par exemple, setattr (x, 'foobar', 123) est équivalent à x.foobar = 123.

1
Nogurenn

This post mentionne quatre façons différentes d'atteindre cet objectif.

C'était la façon la plus propre, je pense: [la collection ne doit pas être modifiée]

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
Hojat Modaresi

Plus manière universelle à "Désactiver la mise à jour des champs après la création de l'objet" - ajustez read_only_fields per View.action

1) ajouter une méthode à Serializer (mieux utiliser vos propres cls de base)

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) Ajouter à la méta du dict du sérialiseur nommé actions_readonly_fields

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

Dans l'exemple ci-dessus, le champ client deviendra en lecture seule pour les actions: 'update', 'partial_update' (c'est-à-dire pour les méthodes PUT, PATCH)

1
pymen