Создайте 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 модели: PostCategory и 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.

#Python #Django

+1
1
+1
0
+1
0
+1
0
+1
0

Ответить

Ваш адрес email не будет опубликован. Обязательные поля помечены *