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