Создайте Blog API с аутентификацией JWT с использованием Django Rest Framework
Django REST framework — это мощный и гибкий инструментарий для создания веб-API. Вы можете легко создать REST API с помощью DRF и использовать конечные точки из React, Angular или другого приложения Frontend. DRF предоставляет множество готовых функций, которые упрощают и ускоряют процесс разработки. В этом руководстве мы создадим API блога со следующими функциями:
- Пользовательская модель пользователя, в которой электронная почта является уникальным идентификатором вместо электронной почты.
- Аутентификация на основе JWT.
- Возможность создавать, извлекать, обновлять и удалять сообщения.
- Нравится/не нравится функция для сообщений.
- Возможность комментировать посты.
В этом руководстве предполагается, что у вас есть знания Django и Django Rest Framework на среднем уровне.
Конфигурация проекта
Сначала создайте виртуальную среду и активируйте ее:
python3 -m venv .venv
source .venv/bin/activate
Затем установите Django и создайте новый проект Django:
pip install django==4.1.2
django-admin startproject config .
Пользовательская модель пользователя в Django для аутентификации на основе электронной почты
Одна из вещей, которая делает Django действительно замечательным, — это встроенная модель User
, которая поставляется с аутентификацией на основе имени пользователя. В мгновение ока вы можете получить аутентификацию, которая работает сразу после установки. Однако не всем проектам требуется пара имени пользователя и пароля для аутентификации, а в некоторых случаях может потребоваться включить дополнительные поля во встроенную модель User
. В любом случае, вам повезло, потому что Django также предоставляет возможности для настройки. В этом руководстве мы настроим модель User
по умолчанию, чтобы использовать электронную почту в качестве уникального первичного идентификатора пользователей вместо имени пользователя.
Другие варианты использования пользовательской модели пользователя включают:
- Включая другие уникальные поля для аутентификации, такие как номер телефона.
- Чтобы собрать всю пользовательскую информацию, будь то поля, связанные с авторизацией или не связанные с авторизацией, в
User
модель.
Давайте теперь создадим приложение с именем users
, которое будет содержать логику, о которой мы только что говорили.
py manage.py startapp users
Добавьте его в список установленных приложений в настройках:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# Local apps
'users',
]
Есть два способа настроить User
модель. Либо путем расширения из AbstractUser
, либо AbstractBaseUser
.
В чем разница и когда вы должны использовать одно вместо другого?
AbstractUser
: Вас устраивают существующие поля во встроенной модели пользователя, но хотите ли вы использовать электронную почту в качестве основного уникального идентификатора ваших пользователей или, возможно, удалить полеusername
? Или вы хотите добавить поля к существующемуUser
? Если да, тоAbstractUser
– это правильный вариант для вас.AbstractBaseUser
: Этот класс содержит функции аутентификации, но не содержит полей, поэтому вам необходимо добавить все необходимые поля при расширении из него. Вероятно, вы захотите использовать это, чтобы иметь больше гибкости в отношении того, как вы хотите обращаться с пользователями.
Если вы начинаете новый проект, настоятельно рекомендуется настроить пользовательскую модель пользователя, даже если вам достаточно модели пользователя по умолчанию. Эта модель ведет себя идентично пользовательской модели по умолчанию, но вы сможете настроить ее в будущем, если возникнет необходимость:
from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
pass
Диспетчер пользовательских моделей
Manager
– это класс, который предоставляет интерфейс, с помощью которого операции запроса к базе данных предоставляются моделям Django. У вас может быть более одного менеджера для вашей модели.
Рассмотрим эту модель:
from django.db import models
class Car(models.Model):
pass
Чтобы получить все экземпляры Car
, вы будете использовать Car.objects.all()
objects
— это имя по умолчанию, которое используют менеджеры Django. Чтобы изменить это имя, вы можете сделать следующее:
from django.db import models
class Car(models.Model):
cars = models.Manager();
Теперь, чтобы получить все экземпляры автомобиля, вы должны использовать Car.cars.all()
Для нашей пользовательской модели нам нужно определить пользовательский класс manager, потому что мы собираемся изменить Queryset
запросов, который возвращает класс Manager
по умолчанию. Мы делаем это, расширяясь из BaseUserManager и предоставляя два дополнительных метода create_user
и create_superuser
.
Создайте файл с именем managers.py
внутри приложения users
и поместите следующее:
# users/managers.py
from django.contrib.auth.base_user import BaseUserManager
from django.utils.translation import gettext_lazy as _
class CustomUserManager(BaseUserManager):
"""
Custom user model manager where email is the unique identifier
for authentication instead of usernames.
"""
def create_user(self, email, password, **extra_fields):
if not email:
raise ValueError(_("Users must have an email address"))
email = self.normalize_email(email)
user = self.model(email=email, **extra_fields)
user.set_password(password)
user.save()
return user
def create_superuser(self, email, password, **extra_fields):
extra_fields.setdefault("is_staff", True)
extra_fields.setdefault("is_superuser", True)
extra_fields.setdefault("is_active", True)
if extra_fields.get("is_staff") is not True:
raise ValueError(_("Superuser must have is_staff=True."))
if extra_fields.get("is_superuser") is not True:
raise ValueError(_("Superuser must have is_superuser=True."))
return self.create_user(email, password, **extra_fields)
Затем создайте пользовательскую модель пользователя следующим образом:
# users/models.py
from django.contrib.auth.models import AbstractUser
from django.utils.translation import gettext_lazy as _
from .managers import CustomUserManager
class CustomUser(AbstractUser):
email = models.EmailField(_("email address"), unique=True)
USERNAME_FIELD = "email"
REQUIRED_FIELDS = ["username"]
objects = CustomUserManager()
def __str__(self):
return self.email
USERNAME_FIELD
задает имя поля в пользовательской модели, которое используется в качестве уникального идентификатора. В нашем случае это электронная почта.REQUIRED_FIELDS
список имен полей, которые будут запрошены при создании суперпользователя с помощью команды управленияcreatesuperuser
. Это не имеет никакого эффекта в других частях Django, например, при создании пользователя в панели администратора.
Далее нам нужно рассказать Django о новой модели, которая должна использоваться для представления пользователя. Это делается следующим образом:
# config/settings.py
AUTH_USER_MODEL = 'users.CustomUser'
Наконец, создайте и примените миграции:
py manage.py makemigrations
py manage.py migrate
Forms
Вам необходимо расширить встроенные в Django формы UserCreationForm
и UserChangeForm
, чтобы они могли использовать новую пользовательскую модель, с которой мы работаем.
Создайте файл с именем forms.py
внутри users
приложения и добавьте следующее:
# users/forms.py
from django.contrib.auth.forms import UserChangeForm, UserCreationForm
from .models import CustomUser
class CustomUserCreationForm(UserCreationForm):
class Meta:
model = CustomUser
fields = ("email",)
class CustomUserChangeForm(UserChangeForm):
class Meta:
model = CustomUser
fields = ("email",)
Admin
Сообщите панели администратора использовать эти формы, перейдя из UserAdmin
в users/admin.py
.
# users/admin.py
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .forms import CustomUserChangeForm, CustomUserCreationForm
from .models import CustomUser
@admin.register(CustomUser)
class CustomUserAdmin(UserAdmin):
add_form = CustomUserCreationForm
form = CustomUserChangeForm
model = CustomUser
list_display = (
"username",
"email",
"is_active",
"is_staff",
"is_superuser",
"last_login",
)
list_filter = ("is_active", "is_staff", "is_superuser")
fieldsets = (
(None, {"fields": ("username", "email", "password")}),
(
"Permissions",
{
"fields": (
"is_staff",
"is_active",
"is_superuser",
"groups",
"user_permissions",
)
},
),
("Dates", {"fields": ("last_login", "date_joined")}),
)
add_fieldsets = (
(
None,
{
"classes": ("wide",),
"fields": (
"username",
"email",
"password1",
"password2",
"is_staff",
"is_active",
),
},
),
)
search_fields = ("email",)
ordering = ("email",)
add_form
иform
указывают формы для добавления и изменения пользовательских экземпляров.fieldsets
определяют поля, которые будут использоваться при редактировании пользователей, а наборыadd_fieldsets
определяют поля, которые будут использоваться при создании пользователя.
Теперь вы можете перейти в панель администратора и добавлять/редактировать пользователей.
Профиль пользователя
Давайте теперь создадим профиль пользователя. Это включает в себя поля, не связанные с аутентификацией для пользователя. На данный момент эта модель содержит аватар и биографию. Вы уже должны быть знакомы с моделированием профиля пользователя. По сути, это делается с помощью отношения «один к одному» между моделями User
и Profile
. В отношениях «один к одному» одна запись в таблице связана с одной и только одной записью в другой таблице с использованием внешнего ключа. Например, экземпляр модели пользователя связан с одним и только одним экземпляром профиля.
Перейдите к users/models.py
и добавьте следующее:
# users/models.py
import os
from django.conf import settings
from django.db import models
from django.template.defaultfilters import slugify
def get_image_filename(instance, filename):
name = instance.product.name
slug = slugify(name)
return f"products/{slug}-{filename}"
class Profile(models.Model):
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
avatar = models.ImageField(upload_to=get_image_filename, blank=True)
bio = models.CharField(max_length=200, blank=True)
def __str__(self):
return self.user.email
@property
def filename(self):
return os.path.basename(self.image.name)
Всякий раз, когда вы используете ImageField
в Django, вам необходимо установить Pillow
, которая является одной из наиболее распространенных библиотек обработки изображений в Python. Давайте установим его:
pip install pillow==9.3.0
Далее давайте зарегистрируем модель профиля в соответствии с пользовательской моделью пользователя следующим образом:
# users/admin.py
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .forms import CustomUserChangeForm, CustomUserCreationForm
from .models import CustomUser, Profile
class ProfileInline(admin.StackedInline):
model = Profile
can_delete = False
verbose_name_plural = "Profile"
@admin.register(CustomUser)
class CustomUserAdmin(UserAdmin):
add_form = CustomUserCreationForm
form = CustomUserChangeForm
model = CustomUser
list_display = (
"username",
"email",
"is_active",
"is_staff",
"is_superuser",
"last_login",
)
list_filter = ("is_active", "is_staff", "is_superuser")
fieldsets = (
(None, {"fields": ("username", "email", "password")}),
(
"Permissions",
{
"fields": (
"is_staff",
"is_active",
"is_superuser",
"groups",
"user_permissions",
)
},
),
("Dates", {"fields": ("last_login", "date_joined")}),
)
add_fieldsets = (
(
None,
{
"classes": ("wide",),
"fields": (
"username",
"email",
"password1",
"password2",
"is_staff",
"is_active",
),
},
),
)
search_fields = ("email",)
ordering = ("email",)
inlines = (ProfileInline,)
admin.site.register(Profile)
Поскольку мы работаем с загруженными пользователями изображениями, нам нужно установить MEDIA_URL и MEDIA_ROOT в настройках:
# config/settings.py
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'
Затем настройте параметры проекта urls.py
для обслуживания загруженных пользователем медиафайлов во время разработки.
# config/urls.py
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
# ...
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
Прежде чем протестировать это, давайте создадим сигнал для автоматического создания профиля пользователя при создании пользователя. Создайте файл с именем signals.py
и добавьте следующее:
# users/signals.py
from django.contrib.auth import get_user_model
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import Profile
User = get_user_model()
@receiver(post_save, sender=User)
def create_profile(sender, instance, created, **kwargs):
if created:
Profile.objects.create(user=instance)
@receiver(post_save, sender=User)
def save_profile(sender, instance, **kwargs):
instance.profile.save()
Наконец, подключите приемники в методе ready()
конфигурации приложения, импортировав модуль signals. Направляйтесь к users/apps.py
и добавьте следующее:
# users/apps.py
from django.apps import AppConfig
class UsersConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "users"
def ready(self):
import users.signals
Проверьте это:
py manage.py makemigrations
py manage.py migrate
py manage.py runserver
JWT-аутентификация
Система аутентификации по умолчанию, которую предоставляет Django, основана на сеансе. Сеансы в Django реализуются с использованием промежуточного программного обеспечения django.contrib.sessions.middleware.SessionMiddleware
. Эта аутентификация на основе сеанса хорошо работает с традиционным циклом запроса-ответа HTML. Однако, если у вас есть клиент, который ожидает, что сервер вернет ответ JSON вместо HTML, вам придется использовать аутентификацию по токену или аутентификацию JWT и позволить клиенту решить, что делать с ответом JSON. В этом руководстве мы реализуем аутентификацию JWT с использованием Django Rest Framework.
Веб-токен JSON (JWT) – это криптографически подписанный URL-safetoken для безопасной передачи информации между сторонами в виде объекта JSON.
В аутентификации на основе JWT происходит следующее:
- Клиент отправляет имя пользователя и пароль на сервер.
- Сервер проверяет учетные данные пользователя на соответствие базе данных.
- Сервер генерирует и отправляет клиенту защищенный токен JWT, который подписан с использованием секретного ключа. Этот токен имеет формат:
header.payload.signature
Декодирование токенов в вышеуказанном формате даст информацию о пользователе, такую как идентификатор, имя пользователя и т.д.
- Затем клиент включает этот токен в HTTP-заголовок для последующих запросов.
- Сервер проверяет токен с помощью секретного ключа, не обращаясь к базе данных. Если токен был подделан, запрос клиента будет отклонен.
Этот токен (также называемый токеном доступа), хотя и настраивается, обычно недолговечен. Наряду с токеном доступа сервер также генерирует и отправляет клиенту токен обновления. Токен обновления имеет более длительный срок службы, и вы можете обменять его на токен доступа.
Таким образом, JWT является масштабируемым и быстрым из-за меньшего количества обращений к базе данных.
Хорошо, давайте сначала установим Django Rest Framework:
pip install djangorestframework==3.14.0
Добавьте его в настройки установленных приложений:
# config/settings.py
INSTALLED_APPS = [
# ...
"rest_framework",
]
Чтобы реализовать JWT auth в нашем проекте, мы собираемся использовать djangorestframework_simplejwt. Установите его:
pip install djangorestframework-simplejwt==5.2.2
Затем сообщите DRF серверную часть аутентификации, которую мы хотим использовать:
# config/settings.py
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": (
"rest_framework_simplejwt.authentication.JWTAuthentication",
),
}
Вот несколько переменных настройки для простого JWT, которые можно настроить в настройках:
# config/settings.py
from datetime import timedelta
SIMPLE_JWT = {
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=15),
"REFRESH_TOKEN_LIFETIME": timedelta(days=14),
"ROTATE_REFRESH_TOKENS": True,
"BLACKLIST_AFTER_ROTATION": True,
"UPDATE_LAST_LOGIN": False,
"ALGORITHM": "HS256",
"SIGNING_KEY": SECRET_KEY,
"VERIFYING_KEY": None,
"AUDIENCE": None,
"ISSUER": None,
"JWK_URL": None,
"LEEWAY": 0,
"AUTH_HEADER_TYPES": ("Bearer",),
"AUTH_HEADER_NAME": "HTTP_AUTHORIZATION",
"USER_ID_FIELD": "id",
"USER_ID_CLAIM": "user_id",
"USER_AUTHENTICATION_RULE": "rest_framework_simplejwt.authentication.default_user_authentication_rule",
"AUTH_TOKEN_CLASSES": ("rest_framework_simplejwt.tokens.AccessToken",),
"TOKEN_TYPE_CLAIM": "token_type",
"JTI_CLAIM": "jti",
"SLIDING_TOKEN_REFRESH_EXP_CLAIM": "refresh_exp",
"SLIDING_TOKEN_LIFETIME": timedelta(minutes=5),
"SLIDING_TOKEN_REFRESH_LIFETIME": timedelta(days=1),
}
- Если для параметра
ROTATE_REFRESH_TOKENS
установлено значениеTrue
, вместе с токеном доступа будет возвращен новый токен обновления. И еслиBLACKLIST_AFTER_ROTATION
имеет значениеTrue
, токен обновления, отправленный в представление обновления, будет добавлен в черный список. Вам необходимо добавить'rest_framework_simplejwt.token_blacklist'
в список установленных приложений, чтобы параметрBLACKLIST_AFTER_ROTATION
заработал. так что давайте сделаем это:
# config/settings.py
# Third-party apps
INSTALLED_APPS = [
# ...
"rest_framework_simplejwt.token_blacklist",
]
Наконец, выполните следующую команду, чтобы применить миграции приложения:
py manage.py migrate
Теперь нам нужно создать токены доступа и обновить их, когда пользователь регистрируется/входит в систему. В следующем разделе мы добавим сериализатор и представления для выполнения этой задачи.
Регистрация пользователей и вход в систему
Создайте файл с именем serializers.py
внутри приложения users
и добавьте следующее:
# users/serializers.py
from django.contrib.auth import authenticate
from rest_framework import serializers
from .models import CustomUser, Profile
class CustomUserSerializer(serializers.ModelSerializer):
"""
Serializer class to serialize CustomUser model.
"""
class Meta:
model = CustomUser
fields = ("id", "username", "email")
class UserRegisterationSerializer(serializers.ModelSerializer):
"""
Serializer class to serialize registration requests and create a new user.
"""
class Meta:
model = CustomUser
fields = ("id", "username", "email", "password")
extra_kwargs = {"password": {"write_only": True}}
def create(self, validated_data):
return CustomUser.objects.create_user(**validated_data)
class UserLoginSerializer(serializers.Serializer):
"""
Serializer class to authenticate users with email and password.
"""
email = serializers.CharField()
password = serializers.CharField(write_only=True)
def validate(self, data):
user = authenticate(**data)
if user and user.is_active:
return user
raise serializers.ValidationError("Incorrect Credentials")
class ProfileSerializer(CustomUserSerializer):
"""
Serializer class to serialize the user Profile model
"""
class Meta:
model = Profile
fields = ("bio",)
class ProfileAvatarSerializer(serializers.ModelSerializer):
"""
Serializer class to serialize the avatar
"""
class Meta:
model = Profile
fields = ("avatar",)
- Обратите внимание, что мы также создали класс сериализатора для профиля.
Затем в views.py
# users/views.py
from django.contrib.auth import get_user_model
from rest_framework import status
from rest_framework.generics import GenericAPIView, RetrieveUpdateAPIView
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.response import Response
from rest_framework_simplejwt.tokens import RefreshToken
from . import serializers
from .models import Profile
User = get_user_model()
class UserRegisterationAPIView(GenericAPIView):
"""
An endpoint for the client to create a new User.
"""
permission_classes = (AllowAny,)
serializer_class = serializers.UserRegisterationSerializer
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = serializer.save()
token = RefreshToken.for_user(user)
data = serializer.data
data["tokens"] = {"refresh": str(token), "access": str(token.access_token)}
return Response(data, status=status.HTTP_201_CREATED)
class UserLoginAPIView(GenericAPIView):
"""
An endpoint to authenticate existing users using their email and password.
"""
permission_classes = (AllowAny,)
serializer_class = serializers.UserLoginSerializer
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = serializer.validated_data
serializer = serializers.CustomUserSerializer(user)
token = RefreshToken.for_user(user)
data = serializer.data
data["tokens"] = {"refresh": str(token), "access": str(token.access_token)}
return Response(data, status=status.HTTP_200_OK)
class UserLogoutAPIView(GenericAPIView):
"""
An endpoint to logout users.
"""
permission_classes = (IsAuthenticated,)
def post(self, request, *args, **kwargs):
try:
refresh_token = request.data["refresh"]
token = RefreshToken(refresh_token)
token.blacklist()
return Response(status=status.HTTP_205_RESET_CONTENT)
except Exception as e:
return Response(status=status.HTTP_400_BAD_REQUEST)
class UserAPIView(RetrieveUpdateAPIView):
"""
Get, Update user information
"""
permission_classes = (IsAuthenticated,)
serializer_class = serializers.CustomUserSerializer
def get_object(self):
return self.request.user
class UserProfileAPIView(RetrieveUpdateAPIView):
"""
Get, Update user profile
"""
queryset = Profile.objects.all()
serializer_class = serializers.ProfileSerializer
permission_classes = (IsAuthenticated,)
def get_object(self):
return self.request.user.profile
class UserAvatarAPIView(RetrieveUpdateAPIView):
"""
Get, Update user avatar
"""
queryset = Profile.objects.all()
serializer_class = serializers.ProfileAvatarSerializer
permission_classes = (IsAuthenticated,)
def get_object(self):
return self.request.user.profile
- Приведенные выше взгляды не требуют пояснений. По сути, представления для аутентификации пользователя используют класс
RefreshToken
simple JWT для генерации и отправки клиенту токенов обновления и доступа. Кроме того, в представлении выхода из системы маркер обновления занесен в черный список. Другие представления используются для получения или обновления пользователя и его/ее профиля.
Теперь давайте подключим наши представления к URL-адресам.
Направляйтесь к config/urls.py
и добавьте URL-адреса приложений users
:
# config/urls.py
from django.urls import include, path
urlpatterns = [
# ...
path("", include("users.urls", namespace="users")),
]
Внутри приложения users
создайте файл с именем urls.py
и добавьте конечные точки следующим образом:
# users/urls.py
from django.urls import path
from rest_framework_simplejwt.views import TokenRefreshView
from users import views
app_name = "users"
urlpatterns = [
path("register/", views.UserRegisterationAPIView.as_view(), name="create-user"),
path("login/", views.UserLoginAPIView.as_view(), name="login-user"),
path("token/refresh/", TokenRefreshView.as_view(), name="token-refresh"),
path("logout/", views.UserLogoutAPIView.as_view(), name="logout-user"),
path("", views.UserAPIView.as_view(), name="user-info"),
path("profile/", views.UserProfileAPIView.as_view(), name="user-profile"),
path("profile/avatar/", views.UserAvatarAPIView.as_view(), name="user-avatar"),
]
Обратите внимание, что конечная точка token/refresh
будет использоваться для получения нового доступа и обновления токена.
API блога
Сначала создайте приложение с именем posts
py manage.py startapp posts
и добавьте его в список установленных приложений в настройках:
# config/settings.py
INSTALLED_APPS = [
# ...
"posts",
]
Давайте подключим модели. У нас будет 3 модели: Post
, Category
и Comment
Сообщение может иметь много категорий и комментариев, поэтому мы собираемся использовать поле ManyToMany
:
# posts/models.py
from django.conf import settings
from django.db import models
from django.utils.translation import gettext_lazy as _
class Category(models.Model):
name = models.CharField(_("Category name"), max_length=100)
class Meta:
verbose_name = _("Category")
verbose_name_plural = _("Categories")
def __str__(self):
return self.name
class Post(models.Model):
title = models.CharField(_("Post title"), max_length=250)
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
related_name="posts",
null=True,
on_delete=models.SET_NULL,
)
categories = models.ManyToManyField(Category, related_name="posts_list", blank=True)
body = models.TextField(_("Post body"))
likes = models.ManyToManyField(
settings.AUTH_USER_MODEL, related_name="post_likes", blank=True
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ("-created_at",)
def __str__(self):
return f"{self.title} by {self.author.username}"
class Comment(models.Model):
post = models.ForeignKey(Post, related_name="comments", on_delete=models.CASCADE)
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
related_name="post_comments",
null=True,
on_delete=models.SET_NULL,
)
body = models.TextField(_("Comment body"))
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ("-created_at",)
def __str__(self):
return f"{self.body[:20]} by {self.author.username}"
Теперь зарегистрируйте эти модели в админ:
# posts/admin.py
from django.contrib import admin
from .models import Category, Comment, Post
admin.site.register(Category)
admin.site.register(Post)
admin.site.register(Comment)
Создание и выполнение миграций:
py manage.py makemigrations
py manage.py migrate
Давайте теперь настроим классы и представления сериализатора.
Создать serializers.py
внутри приложения posts
и добавьте следующее:
# posts/serializers.py
from rest_framework import serializers
from .models import Category, Comment, Post
class CategoryReadSerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = "__all__"
class PostReadSerializer(serializers.ModelSerializer):
author = serializers.CharField(source="author.username", read_only=True)
categories = serializers.SerializerMethodField(read_only=True)
likes = serializers.SerializerMethodField(read_only=True)
class Meta:
model = Post
fields = "__all__"
def get_categories(self, obj):
categories = list(
cat.name for cat in obj.categories.get_queryset().only("name")
)
return categories
def get_likes(self, obj):
likes = list(
like.username for like in obj.likes.get_queryset().only("username")
)
return likes
class PostWriteSerializer(serializers.ModelSerializer):
author = serializers.HiddenField(default=serializers.CurrentUserDefault())
class Meta:
model = Post
fields = "__all__"
class CommentReadSerializer(serializers.ModelSerializer):
author = serializers.CharField(source="author.username", read_only=True)
class Meta:
model = Comment
fields = "__all__"
class CommentWriteSerializer(serializers.ModelSerializer):
author = serializers.HiddenField(default=serializers.CurrentUserDefault())
class Meta:
model = Comment
fields = "__all__"
- Разделение сериализаторов для чтения и записи – это то, что может быть действительно полезно, потому что иногда вы можете захотеть включить больше деталей в ответ на чтение (список и извлечение), но ограничить количество полей при добавлении записи в базу данных. Это делает ваши классы сериализатора менее сложными.
- Также обратите внимание на использование
serializers.CurrentUserDefault
. Это действительно удобно – для автоматической установки аутентифицированного пользователя в качестве автора или владельца чего-либо.
Далее, чтобы написать наши представления, мы собираемся использовать ViewSets
. Если вы новичок в ViewSets, вот краткий обзор.
ViewSet
– это тип представления на основе классов, который объединяет логику для набора связанных представлений в один класс. 2 наиболее распространенных типа наборов представлений, которые вы, скорее всего, будете использовать, – это Modelviewset
и ReadOnlyModelViewSet
. Одним из главных преимуществ ViewSets является автоматическое определение конечных точек URL для вас через Routers
.
Наборы представлений имеют самый высокий уровень абстракции, и вы можете использовать их, чтобы избежать написания всего кода для базовых и повторяющихся вещей. Они значительно экономят время!
При этом добавьте следующий код в представления и прочитайте комментарии для получения дальнейших объяснений.
# posts/views.py
from django.shortcuts import get_object_or_404
from rest_framework import permissions, status, viewsets
from rest_framework.response import Response
from rest_framework.views import APIView
from posts.models import Category, Comment, Post
from posts.serializers import (
CategoryReadSerializer,
CommentReadSerializer,
CommentWriteSerializer,
PostReadSerializer,
PostWriteSerializer,
)
from .permissions import IsAuthorOrReadOnly
# Category is going to be read-only, so we use ReadOnlyModelViewSet
class CategoryViewSet(viewsets.ReadOnlyModelViewSet):
"""
List and Retrieve post categories
"""
queryset = Category.objects.all()
serializer_class = CategoryReadSerializer
permission_classes = (permissions.AllowAny,)
class PostViewSet(viewsets.ModelViewSet):
"""
CRUD posts
"""
queryset = Post.objects.all()
# In order to use different serializers for different
# actions, you can override the
# get_serializer_class(self) method
def get_serializer_class(self):
if self.action in ("create", "update", "partial_update", "destroy"):
return PostWriteSerializer
return PostReadSerializer
# get_permissions(self) method helps you separate
# permissions for different actions inside the same view.
def get_permissions(self):
if self.action in ("create",):
self.permission_classes = (permissions.IsAuthenticated,)
elif self.action in ("update", "partial_update", "destroy"):
self.permission_classes = (IsAuthorOrReadOnly,)
else:
self.permission_classes = (permissions.AllowAny,)
return super().get_permissions()
class CommentViewSet(viewsets.ModelViewSet):
"""
CRUD comments for a particular post
"""
queryset = Comment.objects.all()
def get_queryset(self):
res = super().get_queryset()
post_id = self.kwargs.get("post_id")
return res.filter(post__id=post_id)
def get_serializer_class(self):
if self.action in ("create", "update", "partial_update", "destroy"):
return CommentWriteSerializer
return CommentReadSerializer
def get_permissions(self):
if self.action in ("create",):
self.permission_classes = (permissions.IsAuthenticated,)
elif self.action in ("update", "partial_update", "destroy"):
self.permission_classes = (IsAuthorOrReadOnly,)
else:
self.permission_classes = (permissions.AllowAny,)
return super().get_permissions()
# Here, we are using the normal APIView class
class LikePostAPIView(APIView):
"""
Like, Dislike a post
"""
permission_classes = (permissions.IsAuthenticated,)
def get(self, request, pk):
user = request.user
post = get_object_or_404(Post, pk=pk)
if user in post.likes.all():
post.likes.remove(user)
else:
post.likes.add(user)
return Response(status=status.HTTP_200_OK)
Мы не создали пользовательское разрешение для ограничения действий по редактированию и удалению владельцем записи, так что давайте продолжим и сделаем это. Создайте файл с именем permissions.py
в приложение posts
и добавьте следующее:
# posts/permissions.py
from rest_framework import permissions
class IsAuthorOrReadOnly(permissions.BasePermission):
"""
Check if authenticated user is author of the post.
"""
def has_permission(self, request, view):
return request.user.is_authenticated is True
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return True
return obj.author == request.user
Наконец, давайте настроим URL-адреса:
# config/urls.py
from django.urls import include, path
urlpatterns = [
# ...
path("post/", include("posts.urls", namespace="posts")),
]
# posts/urls.py
from django.urls import include, path
from rest_framework.routers import DefaultRouter
from .views import CategoryViewSet, CommentViewSet, LikePostAPIView, PostViewSet
app_name = "posts"
router = DefaultRouter()
router.register(r"categories", CategoryViewSet)
router.register(r"^(?P<post_id>\d+)/comment", CommentViewSet)
router.register(r"", PostViewSet)
urlpatterns = [
path("", include(router.urls)),
path("like/<int:pk>/", LikePostAPIView.as_view(), name="like-post"),
]
Вы можете использовать Postman или встроенный доступный для просмотра API для тестирования конечных точек. Обратите внимание, что, если вы используете browsable API, вам необходимо добавить сеансовую аутентификацию в DEFAULT_AUTHENTICATION_CLASSES
, поскольку просматриваемый API использует сеансовую аутентификацию для формы входа. Для этого перейдите в настройки и обновите параметр DEFAULT_AUTHENTICATION_CLASSES
:
# config/settings.py
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": (
"rest_framework_simplejwt.authentication.JWTAuthentication",
"rest_framework.authentication.SessionAuthentication",
),
}
Затем, в проекте urls.py
файл добавьте URL API:
# config/urls.py
from django.urls import include, path
urlpatterns = [
path("api-auth/", include("rest_framework.urls")),
]
Не забудьте настроить CORS так, чтобы разрешать запросы в браузере из других источников, таких как, например, ваше приложение React.