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