Kittygram серьёзно подрос, обзавёлся новыми возможностями и стал удобнее. А тут поступила новая задача: новые пользователи хотят самостоятельно регистрироваться в нашем сервисе. Мало того: нужно настроить проект так, чтобы добавлять, обновлять и удалять информацию о котиках могли только их хозяева.
Судя во всему, нового релиза не избежать. Ну что ж, пусть будет Kittygram v2.0.
Релиз Kittygram v2.0
Склонируйте новую версию проекта, выполните миграции, запустите веб-сервер, создайте через API несколько пользователей и получите для них токены.
Проверьте, что пользователи появились в БД и котиков у них пока нет: сделайте несколько запросов через Postman.
Каждый релиз принято сопровождать обзором нововведений. Релиз второй версии Kittygram тоже заслуживает такого обзора.
Что новенького?
Начнём с моделей: Owner больше не понадобится, вместо неё будем использовать встроенную модель User.
#cats/models.py
from django.contrib.auth import get_user_model
from django.db import models
CHOICES = (
('Gray', 'Серый'),
('Black', 'Чёрный'),
('White', 'Белый'),
('Ginger', 'Рыжий'),
('Mixed', 'Смешанный'),
)
User = get_user_model()
class Achievement(models.Model):
name = models.CharField(max_length=64)
def __str__(self):
return self.name
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(
User, related_name='cats', on_delete=models.CASCADE)
achievements = models.ManyToManyField(Achievement, through='AchievementCat')
def __str__(self):
return self.name
class AchievementCat(models.Model):
achievement = models.ForeignKey(Achievement, on_delete=models.CASCADE)
cat = models.ForeignKey(Cat, on_delete=models.CASCADE)
def __str__(self):
return f'{self.achievement} {self.cat}'
В новой версии проекта работать будем исключительно с вьюсетами, так удобнее.
Аутентифицированные пользователи не должны иметь возможность удалять или изменять данные друг друга, поэтому вьюсет, отвечающий за работу с пользователями, унаследован от ReadOnlyModelViewSet
.
# cats/views.py
from rest_framework import viewsets
from .models import Achievement, Cat, User
from .serializers import AchievementSerializer, CatSerializer, UserSerializer
class CatViewSet(viewsets.ModelViewSet):
queryset = Cat.objects.all()
serializer_class = CatSerializer
class UserViewSet(viewsets.ReadOnlyModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
class AchievementViewSet(viewsets.ModelViewSet):
queryset = Achievement.objects.all()
serializer_class = AchievementSerializer
А чтобы дать возможность пользователям самостоятельно регистрироваться через API и обеспечить доступ по токену, воспользуемся связкой JWT+Djoser.
Добавление данных через API
Самое время наполнить базу данных котиками. Теперь это могут сделать только аутентифицированные пользователи.
Выполните POST-запрос на эндпоинт cats/
; анонимные запросы к проекту теперь запрещены, так что передайте с запросом токен одного из пользователей; в теле запроса передайте всю необходимую информацию о домашнем питомце:
{
"name": "Барсик",
"color": "White",
"birth_year": 2011
}
Запрос не обработался, ведь мы не передали в запросе id владельца котика, а это обязательное поле в сериализаторе CatSerializer
. Исправить это не трудно, можно просто добавить в тело запроса id текущего пользователя.
{
"name": "Барсик",
"color": "Black",
"birth_year": 2020,
"owner": 1
}
Если указать id другого пользователя (из списка существующих), то такой запрос тоже пройдёт. А вот это уже не очень хорошо, ведь добавлять, удалять и изменять информацию о котике может только хозяин. А пока что пользователь с id=3 может добавить котика от имени пользователя с id=1. Выглядит как подлог.
Самый простой и очевидный способ исправить ситуацию — получать владельца котика не из поля owner
в теле запроса, а из объекта request
: там доступен экземпляр пользователя, которому принадлежит токен. Нечто подобное вы делали при работе с формами. А сериализатору надо сказать, чтобы он игнорировал это поле, если оно будет указано в теле POST-запроса.
Начнём с последнего пункта: переопределим поле owner
и укажем параметр read_only
.
class CatSerializer(serializers.ModelSerializer):
achievements = AchievementSerializer(read_only=True, many=True)
color = serializers.ChoiceField(choices=CHOICES)
owner = serializers.PrimaryKeyRelatedField(read_only=True)
age = serializers.SerializerMethodField()
class Meta:
model = Cat
fields = ('id', 'name', 'color', 'birth_year','achievements', 'owner',
'age')
...
Параметр read_only_fields
можно указать не в описании поля, а во внутреннем классе Meta
: так удобнее — это позволяет перечислить все поля для чтения в одном месте, не переопределяя их.
class CatSerializer(serializers.ModelSerializer):
achievements = AchievementSerializer(read_only=True, many=True)
color = serializers.ChoiceField(choices=CHOICES)
age = serializers.SerializerMethodField()
class Meta:
model = Cat
fields = ('id', 'name', 'color', 'birth_year', 'achievements', 'owner',
'age')
read_only_fields = ('owner',)
...
Теперь сериализатор не ждёт в теле POST-запроса поле owner
(а если оно придёт, то будет проигнорировано).
Передача собственного значения в метод save()
Проблему с подделкой id пользователя решили, но легче не стало.
Теперь вьюсет не может создать новую запись в БД, ведь поле owner
в модели Cat обязательное, а данные из сериализатора в него не приходят: при POST-запросах сериализатор игнорирует поле owner
.
Задача состоит в том, чтобы при создании новой записи в БД поле owner
не осталось пустым, а в него была записана информация о пользователе, отправившем запрос. Эта информация у нас есть — она доступна в request.user
, осталось в нужный момент подсунуть её в базу.
Новая запись в БД создаётся при вызове метода save()
сериализатора, а этот метод вызывается из метода вьюсета perform_create()
.
Чтобы передать новое значение для какого-то поля в метод save()
, нужно переопределить метод perform_create()
.
В метод save()
в полe owner
передадим объект пользователя, отправившего запрос.
class CatViewSet(viewsets.ModelViewSet):
queryset = Cat.objects.all()
serializer_class = CatSerializer
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
Вот теперь всё работает как надо: сериализатор не ожидает id пользователя в POST-запросе (или игнорирует его при получении), а при создании записи о новом котике в БД информация о пользователе берётся из объекта request.user
.
При подобных операциях с PUT- и PATCH-запросами следует переопределить метод perform_update()
, а в остальном всё работает так же.
Валидаторы
По умолчанию валидация в сериализаторе происходит на базовом уровне. Если в поле ожидается целочисленное значение, а пришло число с плавающей точкой, то сериализатор вернёт ошибку.
А вот если в запросе придёт значение, которое по типу соответствует ожиданиям, но не удовлетворяет бизнес-логике или здравому смыслу (например, год рождения — в будущем), то ошибка не возникнет.
Валидация на уровне поля
Допустим, к проекту Kittygram отправлен POST-запрос, в котором переданы некорректные данные: например, год рождения котика указан в будущем или, наоборот, в далёком прошлом (а котики, к сожалению, не живут более сорока лет). Такие данные принимать нельзя, значит, надо провести валидацию полученных значений.
Проверку можно провести на уровне отдельно взятого поля. Сделаем это на примере поля birth_year
.
Метод для валидации определённого поля должен называться validate_<имя поля>
, он будет автоматически вызываться при получении данных.
Опишите в CatSerializer
новый метод validate_birth_year()
.
class CatSerializer(serializers.ModelSerializer):
achievements = AchievementSerializer(many=True, required=False)
color = serializers.ChoiceField(choices=CHOICES)
age = serializers.SerializerMethodField()
owner = serializers.PrimaryKeyRelatedField(read_only=True)
class Meta:
model = Cat
fields = ('id', 'name', 'color', 'birth_year',
'achievements', 'owner', 'age')
def validate_birth_year(self, value):
year = dt.date.today().year
if not (year - 40 < value <= year):
raise serializers.ValidationError('Проверьте год рождения!')
return value
Проверьте результат: если всё сделано верно, то добавить котика, родившегося в двадцатом году нашей эры, будет невозможно:
Если год указан в нужном диапазоне, проблем не возникнет:
Ещё один важный момент: этот метод не только проверяет данные, но и возвращает значение.
Дело здесь в том, что DRF позволяет не только проверять данные, но и нормализовать их, приводить к нужному виду — например, к верхнему или нижнему регистру. Здесь эта возможность не используется, но DRF требует вернуть значение, и поэтому функция возвращает то, что сама получила на вход, — аргумент value
.
Валидация на уровне объекта
Метод validate_birth_year
проверяет одно-единственное поле birth_year
и ничего не знает об остальных данных.
Однако значения отдельных полей могут по отдельности успешно проходить все валидации, а вот их совокупность не всегда бывает валидной.Например, если в POST-запросе значения полей name
и color
совпали — это почти наверняка ошибка: редко бывает так, что имя котика совпадает с его цветом. Чтобы выявить эту ошибку, понадобится сравнить значения этих полей.
Чтобы выполнить проверку, требующую доступа к нескольким полям, добавьте в сериализатор метод validate()
. Этот метод принимает один аргумент — словарь со значениями полей.
class CatSerializer(serializers.ModelSerializer):
achievements = AchievementSerializer(many=True, required=False)
color = serializers.ChoiceField(choices=CHOICES)
age = serializers.SerializerMethodField()
owner = serializers.PrimaryKeyRelatedField(read_only=True)
class Meta:
model = Cat
fields = ('id', 'name', 'color', 'birth_year',
'achievements', 'owner', 'age')
...
def validate(self, data):
if data['color'] == data['name']:
raise serializers.ValidationError(
'Имя не может совпадать с цветом!')
return data
Теперь ошибочные записи создаваться не будут, а владелец белого кота по кличке White получит сообщение об ошибке.
Встроенные валидаторы для сериализаторов
Для сериализаторов в DRF есть несколько встроенных классов-валидаторов, среди них есть UniqueValidator
и UniqueTogetherValidator
.
- UniqueValidator обеспечивает проверку уникальности значения поля.В моделях такое ограничение описывается параметром
unique=True
для поля. - UniqueTogetherValidator обеспечивает проверку уникальности комбинации (например, если проверка уникальности идёт по полям
name
иcolor
, в базе никогда не окажется несколько записей сname='Мурзик'
иcolor='Black'
, пусть даже остальные поля будут у них различаться).
Разработчики Kittygram 2.0 не хотят, чтобы забывчивые хозяева котиков дублировали записи о своих любимцах в БД. Поэтому реализовали проверку уникальности записей о котиках на примере двух полей: owner
и name
. В базе данных не должно быть двух или более записей, у которых имя котика и хозяин совпадают.
Эти ограничения реализуются на уровне модели: они описываются в классе Meta. Например, через атрибут unique_together:
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(
User, related_name='cats', on_delete=models.CASCADE)
achievements = models.ManyToManyField(Achievement, through='AchievementCat')
class Meta:
unique_together = ('name', 'owner')
def __str__(self):
return self.name
Но документация Django рекомендует вместо unique_together
использовать UniqueConstraint
: этот способ обеспечивает большую функциональность, а unique_together
может быть признан устаревшим в будущем.
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(
User, related_name='cats', on_delete=models.CASCADE)
achievements = models.ManyToManyField(Achievement, through='AchievementCat')
class Meta:
constraints = [
models.UniqueConstraint(
fields=['name', 'owner'],
name='unique_name_owner'
)
]
def __str__(self):
return self.name
Такую же проверку нужно реализовать и на уровне сериализатора.
В классе Meta
сериализатора нужно указать опциональное поле validators
, значением которого будет список валидаторов. Сейчас нужен только один валидатор: UniqueTogetherValidator.
У него есть два обязательных аргумента и один необязательный:
queryset
, обязательный: это queryset, для которого должна быть проверена уникальность.fields
, обязательный: список или кортеж имён полей сериализатора, которые должны составлять уникальный набор.message
: сообщение об ошибке на случай, если данные не прошли валидацию.
class CatSerializer(serializers.ModelSerializer):
achievements = AchievementSerializer(many=True, required=False)
color = serializers.ChoiceField(choices=CHOICES)
age = serializers.SerializerMethodField()
owner = serializers.PrimaryKeyRelatedField(read_only=True)
class Meta:
model = Cat
fields = ('id', 'name', 'color', 'birth_year', 'achievements', 'owner',
'age')
validators = [
UniqueTogetherValidator(
queryset=Cat.objects.all(),
fields=('name', 'owner')
)
]
...
Если при работе с ModelSerializer
в целевой модели указан параметр unique_together
, то валидатор UniqueTogetherValidator
в сериализаторе можно не прописывать — он будет применён автоматически.
Если же требуется осуществлять подобную проверку только на уровне сериализатора, не устанавливая ограничения в модели, — в сериализаторе нужно явно указывать необходимые валидаторы.
После изменений в модели создадим и применим миграции — и сделаем запрос на добавление нового котика. Начнём с такого, которого точно нет в БД.
Ошибка! Странное дело: DRF сообщает, что поле owner
— is required, обязательное. Как так, мы же только что назначили его необязательным!
Фокус ещё и в том, что если в перечне полей валидатора вместо поля owner
указать какое-либо другое поле из модели, то всё будет работать: валидатор всё проверит, записи создадутся. А вот с полем owner
— нет.
Давайте разбираться что тут происходит.
Поле owner
— необязательное, оно может отсутствовать в запросе (либо будет игнорироваться, если присутствует). Мы переопределили метод save()
, теперь туда явным образом передаются данные для поля owner
. Это происходит на этапе создания нового экземпляра класса.
Но этому этапу предшествует запуск валидаторов, и в этот момент сериализатор ещё не знает о новом содержимом поля owner
: оно там просто отсутствует. Но валидатору это поле нужно: без него он не может выполнить проверку.
Выйти из сложившейся ситуации помогут дополнительные опции сериализатора: скрытые поля и значения по умолчанию.
Скрытые поля и значения по умолчанию
Для полей сериализатора можно указать значения по умолчанию. В этом случае, если в запросе будет отсутствовать требуемое поле, будет использовано заранее указанное значение.
Для поля owner
можно указать значение по умолчанию, передав в него объект пользователя, отправившего запрос. Для определения пользователя есть встроенный класс CurrentUserDefault
— именно его и нужно указать в качестве значения по умолчанию для поля owner
.
Существует два варианта использования значений по умолчанию в сериализаторе для решения возникшей задачи.
- Можно переопределить поле
owner
, указав ему типHiddenField
(скрытое поле), и передать ему значение по умолчаниюCurrentUserDefault
.HiddenField
не принимает передаваемые в запросе данные и всегда возвращает значение по умолчанию в словарь validated_data в сериализаторе.
class CatSerializer(serializers.ModelSerializer):
...
owner = serializers.HiddenField(default=serializers.CurrentUserDefault())
...
В этом случае поле owner
всегда будет присутствовать в словаре validated_data
(и больше нет необходимости передавать его значение в метод save()
во вьюсете), но его нельзя будет использовать в ответах сериализатора, даже если это поле будет явно указано в списке необходимых полей: оно скрытое, такие поля не попадают в ответ.
- Можно переопределить поле
owner
, указав ему любой стандартный тип поля, но обязательно передать параметрыread_only=True
иdefault=<значение_по_умолчанию>
(для поляowner
это будетdefault=serializers.CurrentUserDefault()
).
class CatSerializer(serializers.ModelSerializer):
...
owner = serializers.PrimaryKeyRelatedField(
read_only=True, default=serializers.CurrentUserDefault())
...
В этом варианте поле owner
может быть использовано на выходе сериализатора, но без передачи его значения в метод save()
во вьюсете тут уже не обойтись.
В обоих случаях проблема будет решена, новые записи начнут добавляться в БД после обработки запросов и валидация UniqueTogetherValidator
будет работать корректно.
Попробуем дважды добавить одного и того же котика; при попытке добавить его второй раз получим сообщение об ошибке:
Важно: класс UniqueTogetherValidator
всегда накладывает неявное ограничение: все поля сериализатора, к которым применён этот валидатор, обрабатываются как обязательные. Поля со значением default
— исключение: они всегда предоставляют значение, даже если пользователь не передал их в запросе.