Überprüfung der benutzerdefinierten Felder des Django REST Framework

75

Ich versuche, eine benutzerdefinierte Validierung für ein Modell zu erstellen, um zu überprüfen, ob start_datees vor dem Modell liegt und ob end_datees nahezu unmöglich ist.

Sachen, die ich versucht habe:

  • eingebaute Django-Validatoren: Keine Überprüfung

  • schreibe meine eigenen, so:

    def validate_date(self):
       if self.start_date < self.end_date:
            raise serializers.ValidationError("End date must be after start date.")
    

Dieses Stück Code habe ich der Serializer-Klasse (und dann dem Modell) hinzugefügt, aber es scheint an keiner Stelle aufgerufen zu werden.

Ich habe auch diesen Code gefunden, der möglicherweise von Nutzen ist, aber ich weiß nicht, wie ich ihn in meine Methode integrieren soll. Es scheint, dass es funktionieren würde, ein Modellattribut zu validieren, aber ich muss zwischen zwei Attributen prüfen.

Mein Modell:

class MyModel(models.Model):

    created = models.DateTimeField(auto_now_add=True)
    relation_model = models.ForeignKey(RelationModel, related_name="mymodels")
    priority = models.IntegerField(
        validators = [validators.MinValueValidator(0), validators.MaxValueValidator(100)])
    start_date = models.DateField()
end_date = models.DateField()

    @property
    def is_active(self):
        today = datetime.date.today()
        return (today >= self.start_date) and (today <= self.end_date)

    def __unicode__(self):
        ...

    class Meta:
        unique_together = ('relation_model', 'priority', 'start_date', 'end_date')

Zu Ihrer Information, alle anderen Validierungen funktionieren!

Mein Serializer:

class MyModelSerializer(serializers.ModelSerializer):

    relation_model = RelationModelSerializer
    is_active = serializers.Field(source='is_active')

    def validate_date(self):
        if self.start_date > self.end_date:
            raise serializers.ValidationError("End date must be after start date.")   

    class Meta:
        model = MyModel
        fields = (
            'id', 'relation_model', 'priority', 'start_date', 'end_date', 'is_active'
        )

Meine Sicht:

class MyModelList(generics.ListCreateAPIView):
    permission_classes = (IsAdminUser,)
    queryset = MyModel.objects.all()
    serializer_class = MyModelSerializer
    ordering = ('priority')
Gabi
quelle

Antworten:

97

Sie sollten eine objektweite Validierung ( validate()) verwenden, da validate_datediese niemals aufgerufen wird, da sie datekein Feld im Serializer ist. Aus der Dokumentation :

class MySerializer(serializers.ModelSerializer):
    def validate(self, data):
        """
        Check that the start is before the stop.
        """
        if data['start_date'] > data['end_date']:
            raise serializers.ValidationError("finish must occur after start")
        return data

Wie von Michel Sabchuk vorgeschlagen, können Sie den Validierungsfehler in das end_dateFeld einfügen :

class MySerializer(serializers.ModelSerializer):
    def validate(self, data):
        """
        Check that the start is before the stop.
        """
        if data['start_date'] > data['end_date']:
            raise serializers.ValidationError({"end_date": "finish must occur after start"})
        return data

Eine andere Möglichkeit besteht darin, einen Validator zu erstellen. Ich habe einen basierend auf dem Code erstellt für UniqueTogetherValidator:

from rest_framework.utils.representation import smart_repr

class DateBeforeValidator:
    """
    Validator for checking if a start date is before an end date field.
    Implementation based on `UniqueTogetherValidator` of Django Rest Framework.
    """
    message = _('{start_date_field} should be before {end_date_field}.')

    def __init__(self, start_date_field="start_date", end_date_field="end_date", message=None):
        self.start_date_field = start_date_field
        self.end_date_field = end_date_field
        self.message = message or self.message

    def __call__(self, attrs, serializer):
        if attrs['start_date'] > attrs['end_date']:
            message = self.message.format(
                start_date_field=self.start_date_field,
                end_date_field=self.end_date_field,
            )
            # Replace the following line with
            #   raise serializers.ValidationError(
            #       {self.end_date_field: message},
            #       code='date_before',
            #   )
            # if you want to raise the error on the field level
            raise serializers.ValidationError(message, code='date_before')

    def __repr__(self):
        return '<%s(start_date_field=%s, end_date_field=%s)>' % (
            self.__class__.__name__,
            smart_repr(self.start_date_field),
            smart_repr(self.end_date_field)
        )


class MySerializer(serializers.ModelSerializer):
    class Meta:
        # If your start/end date fields have another name give them as kwargs tot the
        # validator:
        #   DateBeforeValidator(
        #       start_date_field="my_start_date", 
        #       end_date_field="my_end_date",
        #   )
        validators = [DateBeforeValidator()]

Vor DRF 3.0 können Sie es auch zur Clean-Funktion eines Modells hinzufügen, dies wird jedoch in DRF 3.0 nicht mehr aufgerufen.

class MyModel(models.Model):
    start_date = models.DateField()
    end_date = models.DateField()
    def clean(self):
        if self.end_date < self.start_date:
            raise ValidationError("End date must be after start date.")
jgadelange
quelle
Danke, das hat mein Problem gelöst! Ich habe beschlossen, es der Modellklasse hinzuzufügen, da das Hinzufügen zum Serializer den Edit-Endpunkt beschädigt (wenn Sie nur eines der Daten ändern möchten, führt dies zu einem Serializer-Fehler). Wenn Sie diesen Beitrag mit derselben Frage finden, stellen Sie sicher, dass Sie `` `aus django.core.exceptions import ValidationError` `` zu Ihrer Modelldatei hinzufügen.
Gabi
jadelange @ Ihre Lösung hat für mich bei der Formularvalidierung funktioniert, daher verdienen Sie eine positive Bewertung von mir. Ich bin in ein anderes Problem geraten und das funktioniert nicht für den Ruhezustand l. Kannst du es bitte erklären?
Amir
Oh, ich habe eine Sache vergessen zu erwähnen. Wenn ich eine saubere Methode im Modell verwende, funktioniert sie nicht für einen Restaufruf
Amir,
2
Es ist erwähnenswert, dass die cleanMethode des Modells seit DRF 3.0 nicht mehr aufgerufen wird, wie hier erläutert ... django-rest-framework.org/topics/3.0-announcement/…
David M.
2
@ David M. - Diese URL ist jetzt ungültig. Hier ist die richtige ... django-rest-framework.org/community/3.0-announcement
ExTexan
23

Jgadelanges Antwort funktionierte wahrscheinlich vor Django Rest 3. Wenn jemand die Django Rest Framework 3 * -Version verwendet, denke ich, dass dies für diese Leute hilfreich wäre. Man sollte den Validierungsprozess auf Modellebene halten und eine saubere Methode könnte die einzige Lösung sein. Die Ankündigung des Django-Rest-Frameworks besagt hier jedoch , dass jemand, der den Restaufruf in der Modell-Clean-Methode validieren möchte, die Serializer-Validierungsmethode überschreiben sollte und die Clean-Methode aus dieser Serializer-Klasse auf folgende Weise aufrufen muss

(weil doc sagt: Die Methode clean () wird im Rahmen der Serializer-Validierung nicht aufgerufen.)

class MySerializer(serializers.ModelSerializer):

   def validate(self, attrs):
     instance = MyModel(**attrs)
     instance.clean()
     return attrs

und Modell

class MyModel(models.Model):
    start_date = models.DateField()
    end_date = models.DateField()

    def clean(self):
        if self.end_date < self.start_date:
            raise ValidationError("End date must be after start date.")
Amir
quelle
2
Ihr Dokumentationslink ist jetzt ungültig. Könnten Sie es aktualisieren? Vielen Dank!
Eikonomega
2
In den verknüpften Dokumenten heißt es: "In einigen Fällen müssen Sie die Validierungslogik tatsächlich in der .clean () -Methode des Modells beibehalten und können sie stattdessen nicht in den Serializer .validate () trennen. Sie können dies tun, indem Sie ein Modell explizit instanziieren." Beispiel in der .validate () -Methode. Auch hier sollten Sie nach Möglichkeit darauf achten, die Validierungslogik ordnungsgemäß von der Modellmethode zu trennen. In einigen Fällen der Abwärtskompatibilität oder für einen einfachen Migrationspfad kann dies jedoch hilfreich sein. " Es heißt also nicht "Mach es so"; Es heißt, Sie können es im Modell tun, aber nicht, verwenden Sie stattdessen den Serializer.
MT0
22

Eine andere Antwort hier könnte nützlich sein, wenn man die validate()Methode des Serializers überschreibt .

In Bezug auf die Antwort auf die Reihenfolge der Serializer-Validierung im Django REST Framework muss ich sagen, dass die serializer.validate()Methode am Ende der Validierungssequenz aufgerufen wird. Die Validatoren des Feldes werden jedoch vorher aufgerufen serializer.to_internal_value(), um ValidationErroram Ende zu erhöhen .

Dies bedeutet, dass benutzerdefinierte Validierungsfehler nicht mit Standardfehlern gestapelt werden .

Meiner Meinung nach ist der sauberste Weg, um das gewünschte Verhalten zu erreichen , die Validierung der Zielfeldmethode in der Serializer-Klasse:

def validate_end_date(self, value):
    # validation process...
    return value

Wenn Sie einen anderen start_dateFeldwert aus dem Modell benötigen, wie in diesem Fall, können Sie diese abrufen (jedoch nicht validiert, da ein Prozess nicht abgeschlossen ist) mit:

# `None` here can be replaced with the field's default value
start_date = self.initial_data.get('start_date')
Beschädigtes Bio
quelle
1
Ich bin damit einverstanden, dass dies der sauberste Weg ist - danke
Aaron Williams
Danke, aber nur diese Methode in den Serializer zu stecken, funktioniert nicht. Was muss noch getan werden?
Pithikos
@Pithikos, na ja, das tut es sicherlich für mich und andere. Haben Sie spezielle Bedingungen?
Damaged Organic
4

Für den Fall, dass jemand Schwierigkeiten hat, dies als klassenbasierten Validator auf dem Feld zu implementieren ...

from rest_framework.serializers import ValidationError

class EndDateValidator:
    def __init__(self, start_date_field):
        self.start_date_field = start_date_field

    def set_context(self, serializer_field):
        self.serializer_field = serializer_field

    def __call__(self, value):
        end_date = value
        serializer = self.serializer_field.parent
        raw_start_date = serializer.initial_data[self.start_date_field]

        try:
            start_date = serializer.fields[self.start_date_field].run_validation(raw_start_date)
        except ValidationError:
            return  # if start_date is incorrect we will omit validating range

        if start_date and end_date and end_date < start_date:
            raise ValidationError('{} cannot be less than {}'.format(self.serializer_field.field_name, self.start_date_field)

Angenommen, Sie haben start_dateund end_dateFelder in Ihrem Serializer, können Sie dann ein end_dateFeld mit festlegen validators=[EndDateValidator('start_date')].

Konrad Perzyna
quelle
4

Ich werde Konrad Antwort erweitern. Ich mag es, weil es ziemlich explizit ist und Sie auch die Validierung für andere Felder aufrufen, wenn wir sie verwenden. Es ist also sicherer und wird wahrscheinlich redundant sein (einige Validierungen werden zweimal aufgerufen).

Als erstes ist zu beachten, dass bei einer solchen Implementierung beim Ausführen von run_validator nur die in der Variablen validators festgelegten Validierungen angezeigt werden. Wenn wir also ein Feld beispielsweise mit den Methoden validate_ validieren, wird es nicht ausgeführt.

Außerdem habe ich es vererbbar gemacht, damit wir die Validierungsfunktion erneut implementieren und den Code wiederverwenden können.

validators.py

from rest_framework.serializers import ValidationError

class OtherFieldValidator:

    #### This part is the same for all validators ####

    def __init__(self, other_field):
        self.other_field = other_field # name of parameter

    def set_context(self, serializer_field):
        self.serializer_field = serializer_field # name of field where validator is defined

    def make_validation(self,field, other_field):
        pass

    def __call__(self, value):
        field = value
        serializer = self.serializer_field.parent # serializer of model
        raw_other_field = serializer.initial_data[self.other_field] # data del otro campo

        try:
            other_field = serializer.fields[self.other_field].run_validation(raw_other_field)
        except ValidationError:
            return # if date_start is incorrect we will omit validating range

    #### Here is the only part that changes ####

        self.make_validation(field,other_field)

class EndDateValidator(OtherFieldValidator):

    def make_validation(self,field, other_field):
        date_end = field
        date_start = other_field
        if date_start and date_end and date_end < date_start:
            raise ValidationError('date cannot be')

Der Serializer sieht also folgendermaßen aus: serializers.py

# Other imports
from .validators import EndDateValidator

 def myfoo(value):                                                        
     raise ValidationError("start date error")                             

 class MyModelSerializer(serializers.ModelSerializer):                                        
     class Meta:                                                          
         model = MyModel                                                      
         fields = '__all__'                                                                                       
         extra_kwargs = {                                                 
             'date_end': {'validators': [EndDateValidator('date_start')]},
             'date_start': {'validators': [myfoo]},                       
         }                                                                
Gonzalo
quelle
4

Die Lösungen von jgadelange und Damaged Organic sind ziemlich interessant, wenn Sie eine einfachere Lösung bevorzugen, insbesondere wenn Sie den Validator nicht mehr als einmal wiederverwenden möchten, aber ich würde eine Verbesserung vorschlagen: Ich würde den Validator auf Objektebene verwenden und ein Diktat mit auslösen der Validierungsfehler des Feldes:

def validate(self, data):
    ...
    if data["start_date"] > data["end_date"]:
        raise serializers.ValidationError(
            {"end_date": "End date must be after start date."}
        )
    ...

Ich nutze den Vorteil, dass die ValidationError-Klasse ein Objekt mit den Fehlerdetails akzeptiert . Auf diese Weise kann ich das gleiche Verhalten einer Validierung auf Feldebene emulieren und die Fehlermeldung mit dem Feld selbst verknüpfen, während ich die Daten nach jeder einzelnen Validierung einzeln vergleichen kann.

Dies ist wichtig, um sicherzustellen, dass Sie nicht mit einem unreinen Startdatum vergleichen, das Sie vor dem Vergleich umsetzen müssten (wie Sie es tun würden, wenn Sie self.initial_data verwenden würden).

Michel Sabchuk
quelle
Ich werde meine Antwort aktualisieren, damit der Fehler auf dem end_dateFeld angezeigt wird. Ich bin damit einverstanden, dass das besser wäre
jgadelange