Сериализаторы в Django: дополнительные настройки

В проекте Kittygram информация о котике содержит и год его рождения. Человеку, знакомому с календарём и арифметикой, не составит труда вычислить возраст котика. Но пользователи кошачьего API попросили разработчиков доработать сервис и явно возвращать возраст их любимцев в ответе на запрос.

Поле SerializerMethodField: обработка данных в сериализаторе

В уроке про проектирование API обсуждалось создание ответа, который содержит в числе прочего данные, которых нет в моделях. Задача с возрастом котика — тот самый случай. Давайте разберёмся, как это можно сделать, и поможет нам в этом SerializerMethodField.

SerializerMethodField — это поле для чтения, связанное с определённым методом, в котором будет вычислено значение этого поля. Метод, который будет вызван для поля <имя_поля>, по умолчанию должен называться get_<имя_поля>.

Аргументами метода get_<имя_поля> должны быть self и сериализуемый объект (он передаётся во второй аргумент):

def get_something(self, obj):
    ... 

С помощью поля SerializerMethodField можно модифицировать существующее поле или создать новое.

Добавьте в сериализатор CatSerializer поле age (его нет в модели Cat); содержимое этого поля будет вычисляться «на лету» в методе get_age:

...
import datetime as dt
...

class CatSerializer(serializers.ModelSerializer):
    achievements = AchievementSerializer(many=True)
    age = serializers.SerializerMethodField()
    
    class Meta:
        model = Cat
        fields = ('id', 'name', 'color', 'birth_year', 'owner', 'achievements',
                  'age')

    def get_age(self, obj):
        return dt.datetime.now().year - obj.birth_year 

В ответе на GET-запросы котика или котиков появится новое поле:

Хорошей практикой считается не перегружать этот метод какими-либо тяжёлыми операциями.

Пользовательский тип поля для сериализатора

Разработчики фронтенда сообщили, что по просьбе пользователей сайта они разрабатывают виджет-палитру для выбора цвета котиков.

Виджет будет передавать цвет на бэкенд через API в закодированном виде, в hex-формате, например — #ff0000.

Коллеги попросили проверить, можно ли перенастроить бэкенд так, чтобы он мог работать с цветом в hex-формате. Отличная задача, приступим.

Сейчас в БД для каждого котика хранится название его цвета; название цвета — это обычная строка. Сериализатор для модели тоже ожидает цвет в строковом формате. Если в POST-запросе в поле color вместо строки с названием цвета "красный" придёт строка с кодом цвета "#ff0000", то код будет записан в БД. А нам это не надо, это не консистентно: изначально было принято решение хранить цвета в человекочитаемом виде; не будем нарушать это правило.

Цветовая модель RGB, которую применяют фронтендеры, может отобразить более 16 млн цветов, и, разумеется, не у всех цветов есть названия. Попытки поименовать все цвета RGB, конечно, делались — так появились палитры с цветами «В меру оливково-коричневый», «Коричневый цвета кожаного седла для лошади» и, разумеется, «Цвет бедра испуганной нимфы».

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

На сегодняшний день в спецификации цветов HTML и CSS определено и поименовано более сотни цветов.

Таким образом, задача сводится к тому, что необходимо создать такое поле для сериализатора, которое

  • в режиме записи конвертирует код цвета в его название,
  • в режиме чтения вернёт название цвета из БД.

Существует простая и удобная библиотека webcolors, которая позволяет конвертировать код цвета в его название и наоборот. Она нам пригодится: установите её в виртуальное окружение согласно инструкции.

Теперь разберёмся с полем сериализатора. Кроме стандартных, вложенных или related типов полей в сериализаторах можно описывать и применять собственные типы; это как раз то, что нужно.

Для создания собственного типа поля сериализатора нужно описать класс для нового типа, который будет унаследован от serializers.Field и описать в нём два метода: def to_representation(self, value) (для чтения) и def to_internal_value(self, data) (для записи).

Опишем новый тип поля Hex2NameColor в serializers.py:

import webcolors

...

class Hex2NameColor(serializers.Field):
    # При чтении данных ничего не меняем - просто возвращаем как есть
    def to_representation(self, value):
        return value
    # При записи код цвета конвертируется в его название
    def to_internal_value(self, data):
        # Доверяй, но проверяй
        try:
            # Если имя цвета существует, то конвертируем код в название
            data = webcolors.hex_to_name(data)
        except ValueError:
            # Иначе возвращаем ошибку
            raise serializers.ValidationError('Для этого цвета нет имени')
     # Возвращаем данные в новом формате
     return data

... 

Теперь новый тип поля Hex2NameColor можно присвоить полю color в CatSerializer:

...

class CatSerializer(serializers.ModelSerializer):
    achievements = AchievementSerializer(many=True, required=False)
    age = serializers.SerializerMethodField()
    color = Hex2NameColor()  # Вот он - наш собственный тип поля

    class Meta:
        model = Cat
        fields = ('id', 'name', 'color', 'birth_year', 'owner', 'achievements',
                  'age')

... 

Отправим POST-запрос на добавление нового котика, укажем в качестве цвета, например, код #228B22 и проверим результат:

Сработало! Строка с кодом конвертировалась, и теперь в БД цвет котика указан как forestgreen. При этом существующие записи при GET-запросе возвращаются без изменений.

А вот если в POST-запросе передать код цвета, для которого не существует названия — сериализатор вернёт ошибку для этого поля:

Тестирование прошло удачно.

Над Москвой встаёт #287233 восход,

По мосту идёт #ffa500 кот,

И лоточник у метро продаёт

Апельсины цвета #f5f5dc

Когда разработчики фронтенда закончат работу над виджетом — можно будет без проблем настроить API для работы с новым форматом цвета. А пока вернём всё как было.

Переименование полей: параметр source

В модели Achievement есть поле name, но и в других моделях есть поля с таким именем. Разобраться можно, но в ответах это имя лучше поменять на более информативное achievement_name.

Сериализатор, унаследованный от ModelSerializer, по умолчанию использует те же названия полей, что и в модели, с которой он работает. Эти же имена служат ключами в ответе API.

Необходимость изменить имена возникает достаточно часто. Эту задачу решают через переопределение поля и применение параметра source:

new_field_name = serializers.CharField(source='old_field_name') 

Определите новое поле achievement_name в сериализаторе, в качестве параметра передайте аргумент source=<'оригинальное имя поля в модели'>.

class AchievementSerializer(serializers.ModelSerializer):
    achievement_name = serializers.CharField(source='name')

    class Meta:
        model = Achievement
        fields = ('id', 'achievement_name') 

Теперь сериализатор знает, с каким полем из модели Achievement он должен работать, и соотнесёт всё правильно.

Этот подход работает при использовании сериализатора как для чтения, так и для записи.

Ограничение возможных значений поля: выбор из списка

Изменим возможности для выбора цвета котиков: набор кошачьих расцветок не особо велик, и можно заранее описать все доступные цвета. А потом можно будет организовать категоризацию по цветам.

Это можно сделать на уровне моделей:

from django.db import models


CHOICES = (
        ('Gray', 'Серый'),
        ('Black', 'Чёрный'),
        ('White', 'Белый'),
        ('Ginger', 'Рыжий'),
        ('Mixed', 'Смешанный'),
    )

...

class Cat(models.Model):
    name = models.CharField(max_length=16)
    color = models.CharField(max_length=16, choices=CHOICES)
    birth_year = models.IntegerField()
    owner = models.ForeignKey(
        Owner, related_name='cats', on_delete=models.CASCADE, blank=True, 
        null=True)
    achievements = models.ManyToManyField(Achievement, through='AchievementCat')

    def __str__(self):
        return self.name

... 

То же можно сделать и на уровне сериализатора, указав для поля color тип ChoiceField и передав в параметр choices список с возможными вариантами:

...

from .models import Cat, Owner, Achievement, AchievementCat, CHOICES

...

class CatSerializer(serializers.ModelSerializer):
    achievements = AchievementSerializer(many=True)
    age = serializers.SerializerMethodField()
    # Теперь поле примет только значение, упомянутое в списке CHOICES
    color = serializers.ChoiceField(choices=CHOICES)

    class Meta:
        model = Cat
        fields = ('id', 'name', 'color', 'birth_year', 'owner', 'achievements',
                  'age')
... 

Изменили модели — не забыли создать и применить миграции. После этого можно проверять.

Теперь работа с цветом стандартизована; запрос, содержащий в поле color название любого неучтённого в списке цвета, вызовет ошибку 400 Bad Request:



Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!: