Продолжаем цикл статей, посвящённый основам Django Rest Framework. В предыдущей статье мы подробно рассмотрели, как сериалайзер валидирует входные данные.


В этой статье мы закрепим теорию на простом примере, а также затронем те вопросы, которые не успели рассмотреть раньше:


  • какое поле сериалайзера выбрать для ForeignKey-поля модели;
  • как сериалайзер работает с датами;
  • как устроен метод save сериалайзера.

А ещё мы напишем контроллер, который будет обрабатывать запросы к API на создание записи в БД.


image


Важное замечание: мы по-прежнему работаем с базовым классом сериалайзера, не переходя к более высокому уровню абстракции ModelSerializer. Это нужно, чтобы глубже понимать принципы работы DRF и при необходимости провести тонкую настройку сериалайзера. О ModelSerializer мы подробно поговорим в следующей статье.

Исходный код учебного проекта для этой статьи доступен в GitHub.


Объявляем класс сериалайзера


from rest_framework import serializers

class WriterSerializer(serializers.Serializer):
    pass

Смотрим, какие в модели есть поля, куда будут записываться пришедшие в запросе данные. В примере класс модели показан в сокращённом варианте.


class Writer(models.Model):
    firstname = models.CharField(max_length=100...)
    lastname = models.CharField(max_length=100...)
    patronymic = models.CharField(max_length=100...)
    birth_place = models.ForeignKey(to=Town...)
    birth_date = models.DateField(...)

Итак, нам нужно, чтобы в POST-запросе пришли данные для пяти полей.


Подбираем корреспондирующие поля сериалайзера


Каждое поле сериалайзера мы назовём так же, как поле модели, которое оно обслуживает. Это позволит не указывать дополнительный атрибут source.


Поля firstname, lastname и patronymic


Одноимённые поля модели ожидают обычные строковые значения, поэтому для корреспондирующих полей сериалайзера выберем класс serializers.Charfield.


Заглянем в метод __init__ этого же класса, чтобы определиться с аргументами при создании поля.


def __init__(self, **kwargs):
    self.allow_blank = kwargs.pop('allow_blank', False)
    self.trim_whitespace = kwargs.pop('trim_whitespace', True)
    self.max_length = kwargs.pop('max_length', None)
    self.min_length = kwargs.pop('min_length', None)
    ...

  • allow_blank трогать не будем — нам нужны данные, поэтому пусть остаётся дефолтный False.
  • trim_whitespace обрежет пробелы перед текстом и после него. Нам это подходит, поэтому атрибут тоже не трогаем и оставляем дефолтный True.
  • max_length нам нужен, потому что такой же валидатор стоит у каждого текстового поля модели Writer. Поскольку по дефолту он не задан, объявим его явно и приведём лимит по количеству символов — такой же, как и в модели.
  • min_length нам не потребуется, потому что у нас нет ограничений по минимальному количеству символов для полей модели.

Получаем следующий код:


class WriterSerializer(serializers.Serializer):
    firstname = serializers.CharField(max_length=100)
    patronymic = serializers.CharField(max_length=100)
    lastname = serializers.CharField(max_length=100)

Поле birth_place


Одноимённое поле модели относится к классу ForeignKey и связано с моделью Town.


class Writer(models.Model):
    ...
    birth_place = models.ForeignKey(to=Town...)

class Town(models.Model):
    name = models.CharField(max_length=100, unique=True, ...)

Тут нам нужно не просто передать какое-то значение (тот же текст), которое сразу запишется в базу: нам нужно, чтобы по этому значению был извлечён объект записи из модели Town.


DRF предоставляет несколько классов полей для работы с полями отношений. Нам подходит класс SlugRelatedField. Вот его описание из исходного кода: A read-write field that represents the target of the relationship by a unique 'slug' attribute. Слово slug может немного путать: под ним здесь понимается любое уникальное поле модели с любым названием. И совсем необязательно, чтобы оно называлось slug или относилось к классу SlugField.


В предыдущей статье мы разобрали, что при работе сериалайзера на запись в поле любого класса работает метод to_internal_value. Вот его исходный код для класса SlugRelatedField:


def to_internal_value(self, data):
    queryset = self.get_queryset()
    try:
        return queryset.get(**{self.slug_field: data})
    except ObjectDoesNotExist:
        self.fail('does_not_exist', slug_name=self.slug_field, value=smart_str(data))
    except (TypeError, ValueError):
        self.fail('invalid')

Код показывает, какие атрибуты нам следует передать, а именно:


  • queryset — набор записей (разумеется, из связанной модели);
  • slug_field — имя уникального поля в связанной модели, которое будет использоваться для ORM-запроса .get. Всегда можно указать pk или id, но у нас есть уникальное поле name, поэтому выберем его.

Что касается набора записей, мы будем искать объект Town по всем записям, поэтому передадим Town.objects.all(). Но можно сократить его до Town.objects, потому что all() будет вызван под капотом.


Итог:


class WriterSerializer(serializers.Serializer):
    ...
    birth_place = serializers.SlugRelatedField(slug_field='name', queryset=Town.objects)

Поле birth_date


В django-модели поле birth_date относится к классу DateField. Одноимённый класс предусмотрен и среди полей сериалайзера.


При объявлении поля DateField можно передать два необязательных аргумента:


  • format — формат, в котором дата будет возвращаться при работе сериалайзера на чтение;
  • input_formats — список или кортеж допустимых форматов передачи даты при работе сериалайзера на запись.

Поскольку сейчас мы акцентируемся на работе сериалайзера на запись, рассмотрим подробнее второй аргумент. Если его не передать, то применится настройка из-под капота DATE_INPUT_FORMATS. Она позволяет передать дату в формате iso-8601 — проще говоря, строкой вида YYYY-MM-DD.


Чтобы глобально переопределить это поведение, нужно прописать в настройках DRF собственные форматы. Они должны отвечать требованиям Python-модуля datetime.


Все настройки DRF прописываются в settings.py django-проекта, а именно в словаре REST_FRAMEWORK. Перенастроим формат передачи строки с датой:


# settings.py
REST_FRAMEWORK = {
    'DATE_INPUT_FORMATS': [
        '%d.%m.%Y',  # '25.10.2021'
        '%d.%m.%y',  # '25.10.21'
    ]
}

Для большей выразительности примера мы переопределим формат ввода/вывода даты не глобально, а прямо в поле сериалайзера.


birth_date = serializers.DateField(
    format='%d.%m.%Y', # из базы дата будет вытаскиваться в формате "25.10.2021"
    input_formats=['%d.%m.%Y', 'iso-8601',] # в том же виде сериалайзер ожидает дату «на вход», но можно и дефолтный формат
)

Под капотом DateField переданная строка превратится в объект класса datetime.date с помощью datetime.datetime.strptime.


Собираем все поля вместе и проверяем работу сериалайзера


Код нашего сериалайзера получился таким:


class WriterSerializer(serializers.Serializer):
    firstname = serializers.CharField(max_length=100)
    patronymic = serializers.CharField(max_length=100)
    lastname = serializers.CharField(max_length=100)
        birth_place = serializers.SlugRelatedField(
        slug_field='name',
        queryset = Town.objects
    )
    birth_date = serializers.DateField(
        format='%d.%m.%Y',
        input_formats=['%d.%m.%Y']
    )

Сериалайзер может работать и на запись, и на чтение — read only или write only полей нет. Проверим.


Сначала задействуем сериалайзер на чтение. Нам понадобится запись из базы и аргумент instance (в каких случаях нужен тот или иной аргумент при создании сериалайзера, мы подробно рассматривали в предыдущей статье).


instance = Writer.objects.first()
serializer_for_reading = WriterSerializer(instance=instance)
print(serializer_for_reading.data)

Результат:


{
    'firstname': 'Константин',
    'patronymic': 'Николаевич',
    'lastname': 'Батюшков',
    'birth_place': 'Вологда',
    'birth_date': '29.05.1787'
}

Теперь посмотрим, как наш сериалайзер работает на запись. Понадобится словарь с входными данными и аргумент data.


data = {
    'firstname': 'Иван',
    'patronymic': 'Алексеевич',
    'lastname': 'Бунин',
    'birth_place': 'Воронеж',
    'birth_date': '22.10.1870'
}
serializer_for_writing = WriterSerializer(data=data)
# валидируем входные данные
print(serializer_for_writing.is_valid())  # True
print(serializer_for_writing.errors)  # ожидаемо пустой словарь {}

Посмотрим на обработанные сериалайзером данные, готовые к записи в БД, которые находятся в validated_data:


# serializer_for_writing.validated_data
OrderedDict(
    [
        ('firstname', 'Иван'),
        ('patronymic', 'Алексеевич'),
        ('lastname', 'Бунин'),
        ('birth_place', <Town: Воронеж>),
        ('birth_date', datetime.date(1870, 10, 22))
    ]
)

В поле birth_place находится не строка «Воронеж», а объект модели Town, готовый для записи в поле с внешним ключом birth_place. А в поле birth_date — не строка с датой, а объект класса datetime.date.


Попробуем передать невалидные данные: например, для поля birth_date придёт строка в неправильном формате (допустим, 22/10/1870). В этом случае is_valid вернёт False, а словарь errors будет таким:


{
    'birth_date': [
        ErrorDetail(string='Неправильный формат date. Используйте один из этих форматов: DD.MM.YYYY.', 
                    code='invalid')
    ]
}

Все поля сериалайзера отработали штатно. Ура!


Усиливаем валидацию


В предыдущей статье мы говорили о многоступенчатой системе валидации в DRF-сериалайзере. Попрактикуемся в создании валидаторов на разных этапах проверки входных данных.


Валидатор для конкретного поля. Допустим, нас интересуют только писатели, которые родились не позднее XX века. Для проверки этого условия добавим аргумент validators в поле birthdate.


from datetime import date
from django.core.validators import MaxValueValidator
...
class WriterSerializer(serializers.Serializer):
    ...
    birth_date = serializers.DateField(..., validators=[MaxValueValidator(date(1999, 12, 31))])

Метавалидатор. Мы хотим, чтобы сочетание «имя – отчество – фамилия» было уникальным. Здесь пригодится UniqueTogetherValidator, который следует объявить в классе Meta сериалайзера, а до этого — импортировать из rest_framework.validators.


class WriterSerializer(serializers.Serializer):
    ...
    class Meta:
        validators = [
            UniqueTogetherValidator(
                queryset=Writer.objects,
                fields=['firstname', 'patronymic', 'lastname']
            ) 
        ]  

Заключительная валидация методом validate. Напоследок мы хотим проверить, что имя, фамилия и отчество не повторяются между собой.


Напомню, что в attrs находится упорядоченный словарь с прошедшими все предыдущие проверки данными.


class WriterSerializer(serializers.Serializer):
    ...
    class Meta:
        ...
    def validate(self, attrs):
        set_attrs = set(
            [attrs['firstname'], attrs['patronymic'], attrs['lastname']]
        )
        if len(set_attrs) != 3:
            raise ValidationError(
                'Имя, отчество и фамилия не могут совпадать между собой',
                code='duplicate values'
            )
        return attrs 

Добавляем сериалайзеру возможность записывать валидированные данные в БД


DRF-класс Serializer наследует от класса BaseSerializer, у которого есть метод `save`. Но вызвать его напрямую мы пока не можем. Чтобы метод заработал, внутри класса нашего сериалайзера нужно описать два метода:


  • create с логикой сохранения в БД новой записи;
  • update с логикой обновления в БД существующей записи.

Примеры этих методов есть в документации.


Сейчас нам достаточно записывать в БД лишь новые данные, поэтому определим только метод create:


class WriterSerializer(serializers.Serializer):
    # тут следуют определения полей сериалайзера, класс Meta, метод validate — пропускаем их для краткости

    def create(self, validated_data):
       return Writer.objects.create(**validated_data)

Теперь при вызове у экземпляра сериалайзера метода .save() без аргументов он вернёт новую запись. Вызывать метод save можно только после получения валидированных данных — другими словами, после вызова is_valid.


Разработчики DRF отмечают: логика save абсолютно не исчерпывается созданием или обновлением записи в БД. Можно вообще не описывать методы create и update, а целиком переопределить сам save. Например, чтобы при его вызове валидированные данные отправлялись по электронной почте.


Контроллер и Browsable API


В первой статье, где демонстрировался пример работы DRF на чтение, мы использовали самый простой контроллер на основе класса APIView. Задействуем его и в этот раз — понадобится лишь дописать логику метода post.


Для работы с POST-запросами в Browsable API есть удобная вкладка HTML form, которая предоставляет отдельное поле в форме для каждого поля сериалайзера. Чтобы эта вкладка отрендерилась в шаблоне, в контроллере должен присутствовать атрибут get_serializer или serializer_class. Для высокоуровневых контроллеров, начиная с GenericAPIView, эти атрибуты есть под капотом.


Поскольку мы используем «голый» APIView, то допишем необходимый атрибут самостоятельно.


from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import APIView

from .models import Writer
from .serializers import WriterSerializer

class WriterInfoView(APIView):
    serializer_class = WriterSerializer
    model = Writer

    def get(self, request):
        ... # полный код не приводим — он аналогичен коду из первой статьи о работе API на чтение

    def post(self, request):
        serializer_for_writing = self.serializer_class(data=request.data)
        serializer_for_writing.is_valid(raise_exception=True)
        serializer_for_writing.save()
        return Response(data=serializer_for_writing.data, status=status.HTTP_201_CREATED)

Посмотрим на представление нашего API в браузере:



Сверху видим ответ на GET-запрос: в базе пока нет записи ни об одном писателе. Ниже есть удобная форма для отправки POST-запроса. Заполним и отправим её. Результат:





Итак, мы разобрались, как создать API для валидации входных данных с последующей их записью в базу данных. В следующей статье мы поднимемся на один уровень абстракции вверх и рассмотрим, как устроен класс ModelSerializer.

Комментарии (1)


  1. foxairman
    10.12.2021 18:45

    Спасибо, интересно! А расскажите как отлавливать баги в проекте Django? Иногда print не спасает, а нужно понять что возвращает сериалайзер или другой объект. Думаю это тема для отдельной статьи :)