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

Допустим у нас есть зарегистрированные пользователи и какая-то модель, например «Компании», которую пользователь может добавлять в избранное. Обычно такая задача решается путем создания третьей таблицы 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, позволяющие создавать многополюсные отношения и уменьшающие количество кода, который необходимо написать для работы с различными типами моделей.


ссылка на оригинал статьи https://habr.com/ru/post/723300/

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

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