Допустим у нас есть зарегистрированные пользователи и какая-то модель, например "Компании", которую пользователь может добавлять в избранное. Обычно такая задача решается путем создания третьей таблицы 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, позволяющие создавать многополюсные отношения и уменьшающие количество кода, который необходимо написать для работы с различными типами моделей.
werevolff
Довольно трудно представить ситуацию, при которой потребовалось бы возвращать избранное без группировки по типу. А Django ContentType - это такой костыль, который, насколько я помню, не создаëт индексов между идентификаторами связываемых объектов. Поэтому, здесь напрашивается либо Union, либо, представление в БД, чтобы выбирать список разных таблиц <modelname>_favorite_user.