Django Rest Framework: Deaktivieren Sie die Feldaktualisierung, nachdem das Objekt erstellt wurde

70

Ich versuche, mein Benutzermodell über Django Rest Framework-API-Aufrufe REST-fähig zu machen, damit ich Benutzer erstellen und deren Profile aktualisieren kann.

Da ich jedoch mit meinen Benutzern einen bestimmten Überprüfungsprozess durchlaufe, möchte ich nicht, dass die Benutzer den Benutzernamen aktualisieren können, nachdem ihr Konto erstellt wurde. Ich habe versucht, read_only_fields zu verwenden, aber das schien dieses Feld in POST-Vorgängen zu deaktivieren, sodass ich beim Erstellen des Benutzerobjekts keinen Benutzernamen angeben konnte.

Wie kann ich dies umsetzen? Relevanter Code für die API, wie sie jetzt existiert, ist unten.

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

Vielen Dank!

Brad Reardon
quelle
1
Hey, ich habe diese Frage nicht früher gefunden, also habe ich ein Duplikat gestellt. Wie auch immer, eine Antwort, die @JPG gepostet hat, scheint besser zu sein als die meisten Antworten hier. Ich weiß nicht, wie ich damit richtig umgehen soll (ich bin neu im Stapelüberlauf), daher hier meine Frage stackoverflow.com/questions/52114443/…
Jan Kaifer

Antworten:

63

Es scheint, dass Sie unterschiedliche Serializer für POST- und PUT-Methoden benötigen. In der Serializer for PUT-Methode können Sie nur das Feld Benutzername ausnehmen (oder das Feld Benutzername als schreibgeschützt festlegen).

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

Überprüfen Sie diese Frage django-rest-framework: unabhängiges GET und PUT in derselben URL, aber unterschiedlicher generischer Ansicht

Andrei Kaigorodov
quelle
Das hat wie ein Zauber gewirkt. Ich habe gerade PATCH zur Liste der Methoden hinzugefügt, die unterschiedlich serialisiert werden. Vielen Dank!
Brad Reardon
1
Dies wird aufgrund der Codeduplizierung für große Serializer schnell nicht mehr handhabbar.
BjörnW
30

Eine weitere Option (nur 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
VoSi
quelle
15
Tolle Methode. Eine kleine Empfehlung würde ich if self.instance is None:anstelle von view.action check verwenden. Auf diese Weise können Sie den Serializer außerhalb der Standard-DRF-Ansichten verwenden. Dh ich habe ein Szenario, in dem ich den Serializer in einer Sellerie-Aufgabe verwende, während ich Daten aus einer externen CSV-Datei importiere. Das view-Attribut ist in diesem Fall null, aber die self.instance-Prüfung funktioniert weiterhin.
Brocksamson
1
Kein Attribut actionin ViewSet.
PythonEnthusiast
7

Eine andere Methode wäre, eine Validierungsmethode hinzuzufügen, aber einen Validierungsfehler auszulösen, wenn die Instanz bereits vorhanden ist und sich der Wert geändert hat:

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

In meinem Fall wollte ich, dass ein Fremdschlüssel niemals aktualisiert wird:

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         

Siehe auch: Level-Field-Validierung im Django Rest Framework 3.1 - Zugriff auf den alten Wert

rrauenza
quelle
1
Ich bin damit einverstanden, dass der Benutzer wissen sollte, warum das Feld nicht aktualisiert werden kann, und dies ist die einzige Antwort, die den Job erledigt. Im Gegensatz zu Hojats Antwort. Hier können Sie immer noch dasselbe serialisierte Objekt erneut senden, ohne dass die Anforderung abgelehnt wird.
Agey
5

Mein Ansatz besteht darin, die perform_updateMethode bei Verwendung von generischen Ansichtsklassen zu ändern . Ich entferne das Feld, wenn die Aktualisierung durchgeführt wird.

class UpdateView(generics.UpdateAPIView):
    ...
    def perform_update(self, serializer):
        #remove some field
        rem_field = serializer.validated_data.pop('some_field', None)
        serializer.save()
Gooshan
quelle
Dies gibt dem API-Benutzer jedoch keinen Hinweis darauf, warum es nicht funktioniert hat. Ich denke, Sie müssen einen richtigen ValidationError für das Feld auslösen, und genau das passiert in den anderen Lösungen hier, die die Attribute read_only / write_only für die Felder mithilfe der extra kwargs-Methode dynamisch ändern.
BjörnW
(konnte meinen Kommentar oben nicht bearbeiten): Der read_only gibt eigentlich keinen Validierungsfehler. Aber ich denke immer noch, dass es in diesem Fall eine gute Praxis ist, einen Validierungsfehler zu geben, da es für den Benutzer unlogisch sein kann, ein Feld POSTEN zu können, es aber nicht zu setzen, wenn es stillschweigend entfernt wird.
BjörnW
3

AKTUALISIEREN:

Es stellt sich heraus, dass Rest Framework bereits mit dieser Funktionalität ausgestattet ist. Die richtige Art, ein "Nur erstellen" -Feld zu haben, ist die Verwendung der CreateOnlyDefault()Option.

Ich denke, das einzige, was noch zu sagen ist, ist Read the Docs !!! http://www.django-rest-framework.org/api-guide/validators/#createonlydefault

Alte Antwort:

Sieht so aus, als wäre ich ziemlich spät zur Party, aber hier sind trotzdem meine zwei Cent.

Für mich ist es nicht sinnvoll, zwei verschiedene Serializer zu haben, nur weil Sie verhindern möchten, dass ein Feld aktualisiert wird. Ich hatte genau das gleiche Problem und der Ansatz, den ich verwendete, bestand darin, meine eigene validateMethode in der Serializer-Klasse zu implementieren . In meinem Fall wird das Feld aufgerufen, das nicht aktualisiert werden soll owner. Hier ist der relevante 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

Und hier ist ein Unit-Test, um es zu validieren:

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

Ich erhebe eine, ValidationErrorweil ich nicht verbergen möchte, dass jemand versucht hat, eine ungültige Operation auszuführen. Wenn Sie dies nicht möchten und zulassen möchten, dass der Vorgang abgeschlossen wird, ohne stattdessen das Feld zu aktualisieren, gehen Sie wie folgt vor:

Entfernen Sie die Zeile:

raise ValidationError('Cannot update owner')

und ersetzen Sie es durch:

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

Hoffe das hilft!

LuisCien
quelle
22
Dies CreateOnlyDefaultgilt nicht für von der API übermittelte Daten, sondern für Dinge, die bei der ersten Erstellung auf dem Server generiert werden, z. B. das Erstellungsdatum. Für den Benutzer ist es standardmäßig schreibgeschützt.
Zequez
2

Ich habe diesen Ansatz verwendet:

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

Weitere universelle Art und Weise zu „nach Objekt - Update deaktivieren Feld erzeugt“ - einstellen read_only_fields pro View.action

1) füge dem Serializer eine Methode hinzu (besser, du verwendest deine eigenen Basis-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) Fügen Sie zu Meta des Serializer- Dikts den Namen action_readonly_fields hinzu

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

Im obigen Beispiel ist das clientFeld für Aktionen schreibgeschützt: 'update', 'teilweise_update' (dh für PUT, PATCH-Methoden)

Pymen
quelle
2

In diesem Beitrag werden vier verschiedene Möglichkeiten zur Erreichung dieses Ziels erwähnt.

Dies war der sauberste Weg, den ich denke: [Sammlung darf nicht bearbeitet werden]

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)
Hojat Modaresi
quelle
1

Eine andere Lösung (abgesehen vom Erstellen eines separaten Serializers) besteht darin, den Benutzernamen aus attrs in der restore_object-Methode einzufügen, wenn die Instanz festgelegt ist (was bedeutet, dass es sich um eine PATCH / PUT-Methode handelt):

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
Pawel Kozela
quelle
2
DRF 3.0 wurde entfernt restore_object: django-rest-framework.org/topics/3.0-announcement
Xiao Hanyu
1

Wenn Sie keinen weiteren Serializer erstellen möchten, können Sie versuchen, das get_serializer_class()Innere anzupassen MyViewSet. Dies hat mir bei einfachen Projekten geholfen.

# 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 (Objekt, Name, Wert)

Dies ist das Gegenstück zu getattr (). Die Argumente sind ein Objekt, eine Zeichenfolge und ein beliebiger Wert. Die Zeichenfolge kann ein vorhandenes oder ein neues Attribut benennen. Die Funktion weist dem Attribut den Wert zu, sofern das Objekt dies zulässt. Zum Beispiel entspricht setattr (x, 'foobar', 123) x.foobar = 123.

Nogurenn
quelle
1
Diese Funktion wird jedoch bei jedem Start der Anwendung aufgerufen. Wie kann ich sie bei jedem Aufruf der Methode 'PATCH' laden lassen? Danke
Naella
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

Gzerone
quelle
0

Ich würde vorschlagen, auch anzuschauen Django Pgtrigger zu betrachten

Auf diese Weise können Sie Trigger zur Validierung installieren. Ich habe damit angefangen und war sehr zufrieden mit seiner Einfachheit:

Hier ist eines ihrer Beispiele, das verhindert, dass ein veröffentlichter Beitrag aktualisiert wird:

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

Der Vorteil dieses Ansatzes besteht darin, .update()dass Sie auch vor umgangenen Anrufen geschützt sind.save()

rrauenza
quelle