Как стать автором
Обновить

Django REST. Реализация функционала добавления в «избранное» с возможностью расширения типов добавляемого контента

Уровень сложностиПростой
Время на прочтение5 мин
Количество просмотров6.6K

Допустим у нас есть зарегистрированные пользователи и какая-то модель, например "Компании", которую пользователь может добавлять в избранное. Обычно такая задача решается путем создания третьей таблицы Favorite, являющейся связующим звеном, для реализации ManyToManyField связи между пользователем и компанией

from django.db import models
from django.contrib.auth.models import User

class Company(models.Model):
    name = models.CharField(max_length=100)

class Favorite(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    company = models.ForeignKey(Company, on_delete=models.CASCADE)

    class Meta:
        unique_together = ('user', 'company')

Однако, если мы захотим в будущем добавлять в "избранное" другие модели, например "Заявки", то придется писать много лишнего кода.

С помощью инструментов Django можно реализовать функционал добавления в "избранное" с возможностью расширения типов добавляемого контента.

Django ContentType и GenericForeignKey - это мощные инструменты, которые позволяют создавать универсальные и многополюсные модели в Django.

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

GenericForeignKey - это свойство модели, которое позволяет создавать отношения между моделями, не зная точного типа связываемой модели на момент создания. Вместо того, чтобы ссылаться на связываемую модель напрямую, вы ссылаетесь на ContentType и ID модели, которую вы хотите связать.

Для того чтобы использовать GenericForeignKey, вы должны определить два поля в вашей модели - поле ContentType и поле Object ID. Поле ContentType хранит тип связываемой модели, а поле Object ID хранит ID этой модели. Вы также должны определить GenericForeignKey свойство, указывающее на поля ContentType и Object ID.

Описание моделей

Добавляем универсальную модель Favorite.

from django.db import models
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType


class Favorite(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey('content_type', 'object_id')

    class Meta:
        verbose_name = 'Избранное'
        verbose_name_plural = 'Избранные'
        ordering = ['-id']
        constraints = [
            models.UniqueConstraint(
                fields=['user', 'object_id', 'content_type'],
                name='unique_user_content_type_object_id'
            )
        ]

При необходимости в модели, которые будут добавляться пользователем в избранное,
можно добавить поле favorites = GenericRelation('Favorite'). По обращению к полю favorites есть возможность получать все связанные объекты модели Favorite.

from django.contrib.contenttypes.fields import GenericRelation

class Application(models.Model):
    name = models.CharField(max_length=100)
    favorites = GenericRelation('Favorite')


class Company(models.Model):
    name = models.CharField(max_length=100)
    favorites = GenericRelation('Favorite')

Добавление/Удаление в избранное

Каждому Вью-сету контента, который можно добавлять в избранное, будем добавлять путь .../<id>/favorite/. Путь url только для авторизированных, принимает GET-запрос. В методе мы получаем объект контента, который пользователь хочет добавить/удалить из избранных. Если объект уже добавлен этим пользователем в избранное, то мы удаляем из избранных, если его еще нет в избранных, до добавляем в избранное. Для удобства масштабирования пишем класс ManageFavorite, в котором описываем метод favorite, добавляющий данный функционал.

from django.contrib.contenttypes.models import ContentType

class ManageFavorite:
    @action(
      detail=True, 
      methods=['get'], 
      url_path='favorite', 
      permission_classes=[IsAuthenticated, ]
    )
    def favorite(self, request, pk):
        instance = self.get_object()
        content_type = ContentType.objects.get_for_model(instance)
        favorite_obj, created = Favorite.objects.get_or_create(
            user=request.user, content_type=content_type, object_id=instance.id
        )

        if created:
            return Response(
                {'message': 'Контент добавлен в избранное'},
                status=status.HTTP_201_CREATED
            )
        else:
            favorite_obj.delete()
            return Response(
                {'message': 'Контент удален из избранного'},
                status=status.HTTP_200_OK
            )

Добавляем этот класс ManageFavorite в родители Вью-класса контента, который
хотим добавлять в избранное

class ApplicationViewSet(viewsets.ModelViewSet, ManageFavorite):
    ...


class CompanyViewSet(viewsets.ModelViewSet, ManageFavorite):
    ...

Например, мы хотим добавить Заявку с id=2 в избранное.

  • GET-запрос api/applications/2/favorite/

  • status 201

{
    "message": "Контент добавлен в избранное"
}

Удалить Заявку с id=2 из избранного.

  • GET-запрос api/applications/2/favorite/

  • status 200

{
    "message": "Контент удален из избранного"
}

Просмотр контента, с включением поля is_favorite для текущего пользователя

В нашем классе ManageFavorite добавляем метод annotate_qs_is_favorite_field, который принимает queryset и добавляет к каждому объекту модели контента булево поле is_favorite, отражающее добавлял ли текущий юзер данный экземпляр контента в избранное.

from django.contrib.contenttypes.models import ContentType
from django.db.models import Exists, OuterRef

class ManageFavorite:
    ...
    
    def annotate_qs_is_favorite_field(self, queryset):
        if self.request.user.is_authenticated:
            is_favorite_subquery = Favorite.objects.filter(
                object_id=OuterRef('pk'), 
                user=self.request.user, 
                content_type=ContentType.objects.get_for_model(queryset.model)
            )
            queryset = queryset.annotate(is_favorite=Exists(is_favorite_subquery))
        return queryset

Во Вью-сете контента описываем метод get_queryset, в котором применяем данную
аннотацию для queryset.

class ApplicationViewSet(viewsets.ModelViewSet, ManageFavorite):
    serializer_class = ApplicationSerializer

    def get_queryset(self):
        queryset = Application.objects.all()
        queryset = self.annotate_qs_is_favorite_field(queryset)
        return queryset

В сериалайзер добавляем одноименное аннотированное поле только для чтения.

class ApplicationSerializer(serializers.ModelSerializer):
    is_favorite = serializers.BooleanField(read_only=True)

    class Meta:
        model = Application
        fields = '__all__'

Например, запрашиваем список Заявок.

  • GET-запрос api/applications/

  • status 200

[
    {
        "id": 5,
        "is_favorite": false,
        "name": "Заявка 5"
    },
    {
        "id": 4,
        "is_favorite": true,
        "name": "Заявка 4"
    },
    {
        "id": 3,
        "is_favorite": true,
        "name": "Заявка 3"
    },
    {
        "id": 2,
        "is_favorite": false,
        "name": "Заявка 2"
    }
]

Просмотр только избранного

В нашем классе ManageFavorite добавляем метод favorites, который формирует путь .../favorites/. Путь url только для авторизированных, принимает GET-запрос. Фильтрует queryset контента, аннотированный полем is_favorite, выбираем только значения True.

class ManageFavorite:
    ...

    @action(
        detail=False,
        methods=['get'],
        url_path='favorites',
        permission_classes=[IsAuthenticated, ]
    )
    def favorites(self, request):
        queryset = self.get_queryset().filter(is_favorite=True)
        serializer_class = self.get_serializer_class()
        serializer = serializer_class(queryset, many=True)
        return Response(serializer.data, status=status.HTTP_200_OK)

Например, запрашиваем список Заявок добавленных в избранное.

  • GET-запрос api/applications/favorites/

  • status 200

[
    {
        "id": 4,
        "is_favorite": true,
        "name": "Заявка 4"
    },
    {
        "id": 3,
        "is_favorite": true,
        "name": "Заявка 3"
    }
]

Заключение

В целом, использование ContentType и GenericForeignKey может помочь вам создавать более универсальные и гибкие модели в Django, позволяющие создавать многополюсные отношения и уменьшающие количество кода, который необходимо написать для работы с различными типами моделей.

Теги:
Хабы:
Всего голосов 4: ↑4 и ↓0+4
Комментарии1

Публикации

Истории

Работа

Data Scientist
69 вакансий
Python разработчик
111 вакансий

Ближайшие события

Антиконференция X5 Future Night
Дата30 мая
Время11:00 – 23:00
Место
Онлайн
OTUS CONF: GameDev
Дата30 мая
Время19:00 – 20:30
Место
Онлайн
Конференция «IT IS CONF 2024»
Дата20 июня
Время09:00 – 19:00
Место
Екатеринбург
Summer Merge
Дата28 – 30 июня
Время11:00
Место
Ульяновская область