Перевірка власних полів Django REST Framework


75

Я намагаюся створити власну перевірку для моделі, щоб перевірити, чи start_dateвона передує її, end_dateі це виявляється майже неможливим.

Матеріали, які я пробував:

  • вбудовані валідатори Django: жодної перевірки на це немає

  • написання власного, ось так:

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

Цей біт коду я додав до класу Serializer (а потім і моделі), але, схоже, його не викликають в жодному місці.

Я також знайшов цей біт коду, який міг би бути корисним, але я не знаю, як інтегрувати його в свій метод - здається, це спрацювало б для перевірки одного атрибута моделі, але мені потрібно перевірити між двома атрибутами.

Моя модель:

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

Fyi, всі інші перевірки працюють!

Мій серіалізатор:

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

Мій погляд:

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

Відповіді:


98

Ви повинні використовувати перевірку об'єкта ( validate()), оскільки validate_dateніколи не буде викликано, оскільки dateце не поле в серіалізаторі. З документації :

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

Як запропонував Мішель Сабчук, ви можете додати помилку перевірки у end_dateполе:

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

Інша можливість - створити валідатор. Я створив його на основі коду для 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()]

До DRF 3.0 ви також можете додати його до функції очищення моделі, але це більше не називається в DRF 3.0.

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.")

Дякую, це вирішило мою проблему! Я вирішив піти на додавання його до класу моделі, оскільки додавання його в серіалізатор порушує кінцеву точку Редагувати (якщо ви хочете змінити лише одну з дат, це призведе до помилки серіалізатора). Для тих, хто знаходить цю публікацію з тим самим запитанням, обов’язково додайте `` з django.core.exceptions import ValidationError '' у файл моделі.
Габі

jadelange @ твоє рішення працювало для мене для перевірки форми, тож ти заслуговуєш голосу від мене. Я потрапив в іншу проблему, яка не працює для програми відпочинку l. Ви можете пояснити це?
Амір

о, я забув одну річ згадати. Якщо я використовую чистий метод у моделі, то це не спрацьовує для виклику відпочинку
Амір

2
Варто зазначити, що починаючи з DRF 3.0 cleanметод моделі більше не буде викликатися, як пояснюється тут ... django-rest-framework.org/topics/3.0-announcement/…
Девід М.

2
@David M. - Ця URL-адреса зараз недійсна. Ось правильний ... django-rest-framework.org/community/3.0-announcement
ExTexan

23

Відповідь jgadelange спрацювала до django rest 3, ймовірно. Якщо хтось, хто використовує версію django rest 3 *, я думаю, це було б корисно для цього. слід тримати процес перевірки на рівні моделі, і єдиним рішенням може бути чистий метод. Але в оголошенні фреймворку django rest тут сказано, що якщо хтось хоче перевірити метод rest-call у методі .clean, він / вона повинен замінити метод перевірки серіалізатора, і йому потрібно викликати чистий метод із цього класу серіалізаторів наступним чином

(оскільки doc каже: метод clean () не буде викликаний як частина перевірки серіалізатора)

class MySerializer(serializers.ModelSerializer):

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

та модель

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.")

2
Ваше посилання на документацію тепер недійсне. Не могли б ви оновити його? Дякую!
eikonomega

2
У зв’язаних документах сказано: „Можуть бути випадки, коли вам дійсно потрібно зберегти логіку перевірки в методі .clean () і не можна замість цього відокремлювати її в серіалізаторі .validate (). Ви можете це зробити, явно створивши інстанцію моделі у методі .validate (). Знову ж таки, ви дійсно повинні поглянути на правильне відокремлення логіки перевірки від методу моделі, якщо це можливо, але вищезазначене може бути корисним у деяких випадках зворотної сумісності або для простого шляху міграції. " Отже, це не говорить "Зроби це так"; його приказка, що ви можете зробити це в моделі, але ні, скоріше використовуйте серіалізатор.
MT0

22

Інша відповідь тут може бути корисною стосовно ситуації, якщо хтось вирішить замінити validate()метод серіалізатора .

Щодо відповіді на Порядок перевірки серіалізатора в Django REST Framework , я повинен сказати, що serializer.validate()метод викликається в кінці послідовності перевірки. Однак валідатори поля викликаються перед цим, в serializer.to_internal_value(), підвищення ValidationErrorв кінці.

Це означає, що власні помилки перевірки не складаються із типовими .

На мій погляд, найчистішим способом досягнення бажаної поведінки є використання перевірки методу цільового поля в класі серіалізатора:

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

У випадку, якщо вам потрібно інше значення поля з моделі, наприклад start_dateу цьому випадку, ви можете отримати їх (але недійсні, оскільки процес не завершений) за допомогою:

# `None` here can be replaced with the field's default value
start_date = self.initial_data.get('start_date')

1
Я згоден, що це найчистіший спосіб зробити це - спасибі
Аарон Вільямс

Дякую, але просто встромлення цього методу в серіалізатор не працює. Що ще потрібно зробити?
Пітікос

@Pithikos, ну, це, безумовно, робить для мене та інших. Чи є у вас якісь конкретні умови?
Пошкоджений органічний

5

Якщо хтось бореться з реалізацією цього як валідатора на основі класу на місцях ...

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)

Якщо припустити, що у вашому серіалізаторі є поля start_dateта end_dateполя, ви можете встановити end_dateполе з validators=[EndDateValidator('start_date')].


5

Розширю відповідь Конрада. Мені це подобається, тому що це досить чітко, а також ви викликаєте перевірку в інших полях, коли ми їх використовуємо. Так безпечніше, можливо, буде зайвим (деякі перевірки будуть викликані двічі)

Перше, на що слід звернути увагу, це те, що якщо ми реалізуємо так, під час запуску run_validator відображатимуться лише перевірки, встановлені у змінній валідаторів. Отже, якщо ми перевіримо поле, наприклад, методами validate_, воно не буде запущено.

Крім того, я зробив це спадкоємним, тому ми можемо реалізувати функцію перевірки та повторно використати код.

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

Тож серіалізатор буде таким: 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]},                       
         }                                                                

4

Рішення jgadelange та Damaged Organic досить цікаві, якщо ви віддаєте перевагу більш простому рішенню, особливо якщо ви не плануєте повторно використовувати валідатор більше одного разу, але я б запропонував вдосконалення: я б використовував валідатор рівня об’єкта, піднімаючи dict з помилка перевірки поля:

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

Я користуюся тим, що клас ValidationError приймає об’єкт із деталями помилок . Таким чином, я можу імітувати однакову поведінку перевірки рівня поля, пов'язуючи повідомлення про помилку із самим полем, тоді як я все ще можу порівнювати дати після кожної перевірки окремо.

Це важливо, щоб переконатись, що ви не порівнюєте з нечистою датою початку, яку вам потрібно було б надати перед порівнянням (як це було б робити, якби ви використовували self.initial_data).


Я оновлю свою відповідь, щоб у полі відображалося повідомлення про помилку end_date. Я згоден, що було б краще
jgadelange
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.