В статье мы реализуем функционал типичной кнопки “Мне нравится”. В этот функционал входит возможность:
- Добавлять лайк;
- Удалять свой лайк;
- Посмотреть общее количество лайков у объекта;
- Проверить, лайкнул ли пользователь объект или нет;
- Показать пользователей, которые лайкнули объект.
Исходный код урока: https://github.com/apirobot/django-likes-app
Первоначальные настройки
Создаем и активируем виртуальное окружение:
$ virtualenv -p python3 venv $ source venv/bin/activate
Устанавливаем django:
$ pip install django
Создаем проект:
$ django-admin startproject django_likes $ cd django_likes
Объект, который мы будем лайкать в нашем тестовом проекте будет Твит. Этим объектом может быть все, что угодно: запись из блога, комментарий и т.д. Если вы уже работаете над каким-то своим проектом, то вы сможете легко адаптироваться.
Создаем приложения:
$ django-admin startapp likes $ django-admin startapp tweets
Добавляем приложения в список установленных приложений (INSTALLED_APPS):
# django_likes/settings.py
INSTALLED_APPS = [
...
'likes.apps.LikesConfig',
'tweets.apps.TweetsConfig',
]
Модели (models.py)
Начнем с реализации модели Like:
# likes/models.py
from django.conf import settings
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db import models
class Like(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL,
related_name='likes',
on_delete=models.CASCADE)
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')
Модель Like основана на встроенном в Django фреймворке ContentType. Фреймворк ContentType предоставляет отношение GenericForeignKey, которое создает обобщенные (generic) отношениямежду моделями. Для сравнения, обычный ForeignKey создает отношение только с какой-то конкретной моделью.
Процесс создания GenericForeignKey:
- Создаем поле с внешним ключом (ForeignKey) на модель ContentType.
- Создаем поле для хранения первичного ключа (primary key) объекта, который вы хотите связать с моделью Like. В этом поле мы будем хранить ID экземпляра модели Tweet. Но хранить можно ID любой модели (моделей), поэтому отношение и называется обобщенным.
- Создаем поле типа GenericForeignKey, передав в нее имена полей, которые мы создали в предыдущих двух пунктах.
Создаем модель Tweet и связываем ее с моделью Like через GenericRelation:
# tweets/models.py
from django.contrib.contenttypes.fields import GenericRelation
from django.db import models
from likes.models import Like
class Tweet(models.Model):
body = models.CharField(max_length=140)
likes = GenericRelation(Like)
def __str__(self):
return self.body
@property
def total_likes(self):
return self.likes.count()
После выполнения миграций проверяем работу моделей:
>>> from django.contrib.contenttypes.models import ContentType >>> from likes.models import Like >>> from tweets.models import Tweet >>> from django.contrib.auth import get_user_model >>> User = get_user_model() >>> user = User.objects.create_user(username='testuser', password='testuser') >>> tweet = Tweet.objects.create(body='People are space puppets') >>> tweet_model_type = ContentType.objects.get_for_model(tweet) >>> Like.objects.create(content_type=tweet_model_type, object_id=tweet.id, user=user) >>> Like.objects.count() 1 >>> Like.objects.first().content_object <Tweet: People are space puppets> >>> tweet.total_likes 1
Функционал (services.py)
Реализуем функционал, о котором я говорил в начале статьи (добавление лайка, удаление лайка и т.д). Этот функционал будет представлен в виде обычных функций. Конечного пользователя этих функций (вас или другого программиста) не должно колыхать, как там эти лайки добавляются. Такой пользователь просто хочет вызвать функцию в контроллере (view) или в другом каком-то месте, и получить нужный ему результат.
После реализации (см. ниже) получим функционал:
# `user` лайкает `tweet` >>> add_like(tweet, user) >>> tweet.total_likes 1 # `user` удаляет свой лайк >>> remove_like(tweet, user) >>> tweet.total_likes 0 # Проверяем, лайкнул ли `user` `tweet` >>> is_fan(tweet, user) False >>> add_like(tweet, user) >>> is_fan(tweet, user) True # Получаем всех пользователей, которые лайкнули `tweet` >>> get_fans(tweet) <QuerySet [<User: user>]>
Сама реализация:
# likes/services.py
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from .models import Like
User = get_user_model()
def add_like(obj, user):
"""Лайкает `obj`.
"""
obj_type = ContentType.objects.get_for_model(obj)
like, is_created = Like.objects.get_or_create(
content_type=obj_type, object_id=obj.id, user=user)
return like
def remove_like(obj, user):
"""Удаляет лайк с `obj`.
"""
obj_type = ContentType.objects.get_for_model(obj)
Like.objects.filter(
content_type=obj_type, object_id=obj.id, user=user
).delete()
def is_fan(obj, user) -> bool:
"""Проверяет, лайкнул ли `user` `obj`.
"""
if not user.is_authenticated:
return False
obj_type = ContentType.objects.get_for_model(obj)
likes = Like.objects.filter(
content_type=obj_type, object_id=obj.id, user=user)
return likes.exists()
def get_fans(obj):
"""Получает всех пользователей, которые лайкнули `obj`.
"""
obj_type = ContentType.objects.get_for_model(obj)
return User.objects.filter(
likes__content_type=obj_type, likes__object_id=obj.id)
Что дальше?
Функционал, о котором я говорил в начале урока – готов. Теперь вы можете пойти по одному из путей:
- Реализовать приложение используя один Django.
- Написать API (Django Rest Framework, Django Tastypie, …) и затем обработать его (React, Vue.js, Elm, …)
Сегодня модно использовать Javascript на фронте, поэтому я пойду по второму пути и напишу API с помощью Django Rest Framework (frontend за вами).
Пишем API
Устанавливаем django rest framework:
$ pip install djangorestframework
Обновляем список установленных приложений:
# django_likes/settings.py
INSTALLED_APPS = [
...
'rest_framework',
]
Сериализуем Tweet:
# tweets/api/serializers.py
from rest_framework import serializers
from ..models import Tweet
class TweetSerializer(serializers.ModelSerializer):
class Meta:
model = Tweet
fields = (
'id',
'body',
'total_likes'
)
Создаем ViewSet используя ModelViewSet, который снабжает нас create, update, list, retrieve, delete методами:
# tweets/api/viewsets.py
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticatedOrReadOnly
from ..models import Tweet
from .serializers import TweetSerializer
class TweetViewSet(viewsets.ModelViewSet):
queryset = Tweet.objects.all()
serializer_class = TweetSerializer
permission_classes = (IsAuthenticatedOrReadOnly, )
Так как мы используем ViewSet, то нам нет необходимости самим настраивать URLs. Можно использовать готовый класс Router, который предоставляет django rest framework:
# tweets/api/urls.py from rest_framework.routers import DefaultRouter from .viewsets import TweetViewSet # Создаем router и регистрируем ViewSet router = DefaultRouter() router.register(r'tweets', TweetViewSet) # URLs настраиваются автоматически роутером urlpatterns = router.urls
Теперь эти urls нужно включить в django_likes/urls.py:
# django_likes/urls.py
from django.conf.urls import include, url
from django.contrib import admin
# Регистрируем API
apipatterns = [
url(r'^', include('tweets.api.urls')),
]
urlpatterns = [
url(r'^api/v1/', include(apipatterns, namespace='api')),
url(r'^admin/', admin.site.urls),
]
На данный момет нам доступны только стандартные CRUD операции над моделью Tweet.
Когда текущий пользователь (request.user) получает информацию о Твите, мы должны знать, лайкнул он уже этот твит или нет. Таким образом мы будем знать, нужно ли подсвечивать кнопку “Мне нравится” на фронте или нет. Для этого добавляем в TweetSerializer поле is_fan:
# tweets/api/serializers.py
from rest_framework import serializers
from likes import services as likes_services
from ..models import Tweet
class TweetSerializer(serializers.ModelSerializer):
is_fan = serializers.SerializerMethodField()
class Meta:
model = Tweet
fields = (
'id',
'body',
'is_fan',
'total_likes',
)
def get_is_fan(self, obj) -> bool:
"""Проверяет, лайкнул ли `request.user` твит (`obj`).
"""
user = self.context.get('request').user
return likes_services.is_fan(obj, user)
Для реализации оставшегося API создадим viewset mixin используя декоратор detail_route:
# likes/api/mixins.py
from rest_framework.decorators import detail_route
from rest_framework.response import Response
from .. import services
from .serializers import FanSerializer
class LikedMixin:
@detail_route(methods=['POST'])
def like(self, request, pk=None):
"""Лайкает `obj`.
"""
obj = self.get_object()
services.add_like(obj, request.user)
return Response()
@detail_route(methods=['POST'])
def unlike(self, request, pk=None):
"""Удаляет лайк с `obj`.
"""
obj = self.get_object()
services.remove_like(obj, request.user)
return Response()
@detail_route(methods=['GET'])
def fans(self, request, pk=None):
"""Получает всех пользователей, которые лайкнули `obj`.
"""
obj = self.get_object()
fans = services.get_fans(obj)
serializer = FanSerializer(fans, many=True)
return Response(serializer.data)
Сериализуем пользователя:
# likes/api/serializers.py
from django.contrib.auth import get_user_model
from rest_framework import serializers
User = get_user_model()
class FanSerializer(serializers.ModelSerializer):
full_name = serializers.SerializerMethodField()
class Meta:
model = User
fields = (
'username',
'full_name',
)
def get_full_name(self, obj):
return obj.get_full_name()
Последний штрих. Наследуемся от миксина:
# tweets/api/viewsets.py
...
from likes.api.mixins import LikedMixin
class TweetViewSet(LikedMixin,
viewsets.ModelViewSet):
...
Тестим API
Для тестирования API будем использовать библиотеку HTTPie.
Так как для большинства запросов необходимо быть авторизованным пользователем, то вам нужно создать пользователя (если вы этого еще не сделали):
>>> from django.contrib.auth import get_user_model >>> get_user_model().objects.create_user(username='testuser', password='testuser')
Создаем Твит:
$ http -a testuser:testuser POST "http://localhost:8000/api/v1/tweets/" body='People are space puppets'

Лайкаем этот твит:
$ http -a testuser:testuser POST "http://localhost:8000/api/v1/tweets/4/like/"

Проверяем информацию о твите (без авторизации):
$ http GET "http://localhost:8000/api/v1/tweets/4/"

Количество лайков (total_likes) увеличилось, а is_fan остался false, потому что запрос сделан без авторизации. Повторим запрос, только с авторизацией:
$ http -a testuser:testuser GET "http://localhost:8000/api/v1/tweets/4/"

Получаем пользователей, которые лайкнули твит:
$ http -a testuser:testuser GET "http://localhost:8000/api/v1/tweets/4/fans/"

Удаляем лайк:
$ http -a testuser:testuser POST "http://localhost:8000/api/v1/tweets/4/unlike/"

Как в html имплементировать ? Сложна сложна. Буду рад ответу
Это нужно отдельную статью писать, либо эту дополнять 🙂
Как успехи с этим?
У меня такой вопрос, можете подсказать
Я, когда создаю твит, хочу еще и добавлять в таблицу Юзера, который его создал
то-есть в модель Твита добавляю еще ForeignKey на User
но когда хочу создать запись, запрашивает объект Юзера
Так такой вопрос, как переделать TweetSetView, чтоб он под автора поста подставлял текущего авторизованного юзера ?
Надеюсь хорошо изложил мысль, надеюсь поможете
спасибо
Привет. Почитай тут https://www.django-rest-framework.org/tutorial/4-authentication-and-permissions/#associating-snippets-with-users
Здравствуйте. После того, как лайкаешь от двух разных пользователей и более один твит, создается два экземпляра в выводе. То есть по факту в БД один экземпляр, но в выводе json их два с одинаковым увеличенным кол-вом лайков. подскажите, в чем может быть загвоздка. Как будто создаются два экземпляра лайков на одну книгу и поэтому они дублируют и саму книгу