В статье по созданию системы закладок на Django был рассмотрен пример с использованием абстрактной модели для нескольких типов закладок, а именно для статей и комментариев к статьям. Также было акцентировано внимание на том, что поля моделей, который имели внешние ключи на различные модели, должны иметь одинаковые названия, чтобы поддерживать возможность создания единого интерфейса для добавления закладок. Это становится возможным благодаря так называемой "утиной типизации" (Duck typing) , которая подразумевает, что объекты, которые не имеют единой иерархии наследования, могут использоваться в одном и том же сценарии при наличии интерфейсов (методов) имеющих одинаковую сигнатуру.
Дословно принцип утиной типизации звучит следующим образом:
Если это выглядит как утка, плавает как утка и крякает как утка, то это, возможно, и есть утка.
If it looks like a duck, swims like a duck and quacks like a duck, then it probably is a duck.
То есть имея методы с одной и той же сигнатурой, мы можем использовать объекты, не связанные иерархией наследования, в одном и том же контексте.
В данной же статье рассмотрим вариант, когда для создания системы Like Dislike используется не две различных таблицы для статей и комментариев, и даже не одна, которая будет содержать по внешнему ключу на статью или комментарий (то есть две колонки, и при этом будет заполняться только одна из колонок в зависимости от того, к какому типу контента относится активность пользователя), а одна таблица, которая будет содержать:
- content_type - тип контента, к которому относится запись
- object_id - ID записи
- content_object - генерируемый внешний ключ на запись, по сути объект контента
- прочие дополнительные поля
Модель данных LikeDislike с GenericForeignKey
Теперь подробнее рассмотрим, как может выглядеть модель данных, которая сможет работать с любым типом контента.
from django.db import models from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.fields import GenericForeignKey class LikeDislike(models.Model): LIKE = 1 DISLIKE = -1 VOTES = ( (DISLIKE, 'Не нравится'), (LIKE, 'Нравится') ) vote = models.SmallIntegerField(verbose_name=_("Голос"), choices=VOTES) user = models.ForeignKey(User, verbose_name=_("Пользователь")) content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) object_id = models.PositiveIntegerField() content_object = GenericForeignKey() objects = LikeDislikeManager()
В данном коде присутствуют как обязательные поля для использования полиморфных связей, так и дополнительные поля, которые уже характеризуют тип активности пользователя.
Система Like Dislike в данном случае строится на принципе +1/-1, что можно будет использовать для голосований с подсчётом суммарного рейтинга. Также имеется внешний ключ на пользователя, который проголосовал за статью или комментарий.
Для учёта типа контента используется модель ContentType , которая применяется в admin панели Django и формирует логи. Фактически при создании моделей данных автоматически создаётся и запись ContentType для этой модели. Таким образом все таблицы учитываются в модели ContentType . На этом основывается система логирования действий администратора в Django. Данный внешний ключ присваивается полю content_type.
object_id содержит ID первичного ключа экземпляра модели, для которой создаётся связь.
content_object содержит поле для связи с любой моделью и он является классом GenericForeignKey . Если два предыдущих поля имеют названия отличные от content_type и object_id , то их необходимо передать в качестве аргументов в GenericForeignKey . Если не отличаются, то GenericForeignKey самостоятельно их определит и будет использовать их для создания полиморфных связей.
Также в модели присутствует поле objects, которому присваивается специальный менеджер модели, который облегчит работу по получению отдельно Like и Dislike счётчиков, а также их суммарного рейтинга.
LikeDislikeManager
Данный менеджер модели позволит забирать отдельно Like и Dislike записи для текущего likedislike_set статьи или комментария.
from django.db import models from django.db.models import Sum class LikeDislikeManager(models.Manager): use_for_related_fields = True def likes(self): # Забираем queryset с записями больше 0 return self.get_queryset().filter(vote__gt=0) def dislikes(self): # Забираем queryset с записями меньше 0 return self.get_queryset().filter(vote__lt=0) def sum_rating(self): # Забираем суммарный рейтинг return self.get_queryset().aggregate(Sum('vote')).get('vote__sum') or 0
Добавление связи в модели данных статей и комментариев
Добавление связей осуществляется с помощью класса GenericRelation, который в отличие от полей content_type , object_id , GenericForeignKey, не создают дополнительных миграций базы данных.
from django.db import models from django.contrib.contenttypes.fields import GenericRelation class Article(models.Model): votes = GenericRelation(LikeDislike, related_query_name='articles') class Comment(models.Model): votes = GenericRelation(LikeDislike, related_query_name='comments')
В GenericRelation передано два аргумента:
- модель с полиморфными связями, как видите она одна и та же для обеих моделей.
- related_query_name - наименование модели, по которому можно будет делать реверсивные выборки. По умолчанию GenericForeignKey не сможет их делать.
Это означает, что при наличии related_query_name можно будет забрать голоса конкретного пользователя к статьям, или к комментариям с использованием сортироваки. А для простоты можно реализовать это в LikeDislikeManager, добавив ещё два метода.
def articles(self): return self.get_queryset().filter(content_type__model='article').order_by('-articles__pub_date') def comments(self): return self.get_queryset().filter(content_type__model='comment').order_by('-comments__pub_date')
Обратите внимание на аргумент order_by. Без указания related_query_name такой аргумент выдал бы ошибку 500. А так можно забрать все лайки с сортировкой по дате публикации статей или комментариев. Для Like Dislike системы это не очень нужно, но для закладок может быть полезно.
views.py
А теперь рассмотрим представление, которое будет добавлять или удалять голос пользователя у статьи или комментария. Реализация голосования будет сделана с использованием AJAX запросов.
Суть в том, что в файле urls.py мы будем задавать для этого View модель данных, за которую голосуем, а также тип голос Like или Dislike.
Передавая запрос мы попытаемся забрать запись и если она существует, то либо установим тип голоса на противоположный, либо удалим голос. Если запись не существует, то добавим запись голоса с текущим типом Like или Dislike.
import json from django.http import HttpResponse from django.views import View from django.contrib.contenttypes.models import ContentType from ecore.models import LikeDislike class VotesView(View): model = None # Модель данных - Статьи или Комментарии vote_type = None # Тип комментария Like/Dislike def post(self, request, pk): obj = self.model.objects.get(pk=pk) # GenericForeignKey не поддерживает метод get_or_create try: likedislike = LikeDislike.objects.get(content_type=ContentType.objects.get_for_model(obj), object_id=obj.id, user=request.user) if likedislike.vote is not self.vote_type: likedislike.vote = self.vote_type likedislike.save(update_fields=['vote']) result = True else: likedislike.delete() result = False except LikeDislike.DoesNotExist: obj.votes.create(user=request.user, vote=self.vote_type) result = True return HttpResponse( json.dumps({ "result": result, "like_count": obj.votes.likes().count(), "dislike_count": obj.votes.dislikes().count(), "sum_rating": obj.votes.sum_rating() }), content_type="application/json" )
urls.py
Запись регулярного выражения для URL определяет тип контента, за который голосуем, его первичный ключ и тип голоса. Таким образом получается 4 тип url.
from django.conf.urls import url from django.contrib.auth.decorators import login_required from . import views from .models import LikeDislike from knowledge.models import Article, Comment app_name = 'ajax' urlpatterns = [ url(r'^article/(?P<pk>\d+)/like/$', login_required(views.VotesView.as_view(model=Article, vote_type=LikeDislike.LIKE)), name='article_like'), url(r'^article/(?P<pk>\d+)/dislike/$', login_required(views.VotesView.as_view(model=Article, vote_type=LikeDislike.DISLIKE)), name='article_dislike'), url(r'^comment/(?P<pk>\d+)/like/$', login_required(views.VotesView.as_view(model=Comment, vote_type=LikeDislike.LIKE)), name='comment_like'), url(r'^comment/(?P<pk>\d+)/dislike/$', login_required(views.VotesView.as_view(model=Comment, vote_type=LikeDislike.DISLIKE)), name='comment_dislike'), ]
html
html код в моём случае будет выглядеть следующим образом:
<ul> <li data-id="{{ like_obj.id }}" data-type="article" data-action="like" title="Нравится"> <span class="glyphicon glyphicon-thumbs-up"></span> <span data-count="like">{{ like_obj.votes.likes.count }}</span> </li> <li data-id="{{ like_obj.id }}" data-type="article" data-action="dislike" title="Не нравится"> <span class="glyphicon glyphicon-thumbs-down"></span> <span data-count="dislike">{{ like_obj.votes.dislikes.count }}</span> </li> </ul>
В предыдущей статье был применён такой же принцип формирования счётчиков.
- data-id - отвечает за pk контента, который можно добавлять в закладки.
- data-type - тип контента, это же название фигурирует и в url.
- data-action - действие, которое нужно совершить, в данном случае добавление в закладки
- data-count - счётчик, показывающий сколько пользователей добавили контент в закладки
Отличие заключается в том, что для получения количества Like и Dislike используются не методы моделей данных статей и комментариев, а методы менеджера модели LikeDislikeManager , что ещё больше упрощает дальнейшую разработку, поскольку не нужно будет отслеживать, во всех ли моделях хватает методов. Достаточно будет добавить поле GenericRelation .
JavaScript
В предыдущей статье по закладкам на сайте я уже акцентировал внимание, что необходимо обрабатывать CSRF токен для AJAX-запросов. Поэтому не буду дублировать информацию.
Покажу только сами обработчики AJAX-запросов. Их будет 2. Один для Like, второй для Dislike.
function like() { var like = $(this); var type = like.data('type'); var pk = like.data('id'); var action = like.data('action'); var dislike = like.next(); $.ajax({ url : "/api/" + type +"/" + pk + "/" + action + "/", type : 'POST', data : { 'obj' : pk }, success : function (json) { like.find("[data-count='like']").text(json.like_count); dislike.find("[data-count='dislike']").text(json.dislike_count); } }); return false; } function dislike() { var dislike = $(this); var type = dislike.data('type'); var pk = dislike.data('id'); var action = dislike.data('action'); var like = dislike.prev(); $.ajax({ url : "/api/" + type +"/" + pk + "/" + action + "/", type : 'POST', data : { 'obj' : pk }, success : function (json) { dislike.find("[data-count='dislike']").text(json.dislike_count); like.find("[data-count='like']").text(json.like_count); } }); return false; } // Подключение обработчиков $(function() { $('[data-action="like"]').click(like); $('[data-action="dislike"]').click(dislike); });
Для Django рекомендую VDS-сервера хостера Timeweb .
Доброго времени суток Евгений. Не подскажете как сделать иконки (лайк-дизлайк) кликабельными?
День добрый. А в каком смысле кликабельными? Вот у меня на сайте они кликаются например. Что конкретно у вас не работает?
В прямом, иконки не кликабельные... просто как декор висят.
А что в консоли отладчика браузера? Может у вас JavaSсript с ошибками отрабатывает?
Дико извиняюсь. Действительно в консоли посмотрел, они кликабельные. Только сыпятся ошбки при клике на $.ajax
really nice work .thanks.
Hi, works perfect, thank you.
I try to retrieve list of users who like eg. Post, but it don't work. How to do this?
I know how to retrieve list of user names but not name with avatar like on evileg.
I think, you made some mistake. Do you have likes or dislikes in admin panel? Or some more information?
Because, I don`t know, what you did in your code.
I suggest you create theme on forum in django section , and we can discuss your problem.
Здравствуйте. Ваша система очень хорошо работает.Спасибо.
Но у меня есть вопрос. Допустим, проголосовал пользователь или нет, можно проверить по условию "if post.votes.user == request.user". А как опредилить какой голос пользователь поставил (лайк или дизлайк)?
Добрый день.
Если у вас уже выбран объект голоса для конкретного пользователя, то можно проверить так.
Ну или проверить, лайкнул ли пользователь из всех лайков так
Спасибо. А не подскажите как это в шаблоне проверить?
Лучше я, наверное, более подробно опишу. Есть несколько постов на странице и мне нужно проверить какой голос поставил пользователь. Если стоит лайк, то закрасить кнопку лайк, а если дизлайк, то кнопку дизлайк. Как это решить?
Я понял. Я вам позже отвечу. У меня это реализовано, но нужно посмотреть исходники сайта.
Пока что, мне пришло в голову такое решение.
Я добавил в LikeDislikeManager следующий метод:
И в шаблоне проверяю таким образом: {% if request.user in post.votes.likers %}.
я написал template tag, фильтр, через который делаю проверку прямо в шаблоне
Для этого подгружаю модуль в шаблоне и проверяю наличие пользователя в queryset
Плюс в том, что могу делать такую проверку для какой угодно модели данных, у которой поле пользователя называется user. И нет никакой зависимости на ModelManager
Спасибо большое.
Снова здравствуйте). Есть ещё вопрос. Как сохранить объект LikeDislike, при удалении статьи? Пробовал
Не помогло.
Добрый день!
А зачем его вообще сохранять, если статья удалена? Вообще объект LikeDislike как раз сохраняется... в этом-то и проблема связей GenericForeignKey, я бы как раз удалял это добро. А content_type - это внешний ключ на модель ContentType, то есть это будет влиять только при удалении самого типа контента статей, а не конкретной статьи.
Спасибо за помощь.
-"А зачем его вообще сохранять, если статья удалена?".
Мне нужно знать сколько лайков/дизлайков получал пользователь за все свои статьи, даже если он удалил какую-нибудь статью.
вообще GenericForeignKey не удаляется, если только вы там что-то не нахимичили. Проблема, как раз в обратном, чтобы его удалять и не было битых отношений.
Просто удалите статью с лайком и посмотрите, что там в админ панели, только учтите, что если имеете обращение к самой статье через лайк, то получите ошибку 500. Нужно обрабатывать эту проблему дополнительно.
Я бы ещё добавил тогда поле target_user, куда будет добавляться пользователь, который получил лайк, тогда при удалении статьи лайк будет соотноситься с сами пользователем.
-"вообще GenericForeignKey не удаляется, если только вы там что-то не нахимичили."
Если использовать ваш код, то при удалении статьи объект LikeDislike не будет удалятся?
Я правильно понял?
Просто, если это так, то очень странно. Потому что у меня LikeDislike удаляется.
да? я перепроверю. Но у меня вроде бы были проблемы с этим делом. Во всяком случае кое-какие объекты, например, теги оставляют битый мусор. Возможно в последних верссиях Джанго дело обстоит иначе уже, я начинал с Джанго 1.10.
Видимо всё из-за GenericRelation. Если его убрать со статьи, то объект LikeDislike не удаляется. Но если его убрать, то я не смогу обращаться к LikeDislike через статью. Допустим ваш template tag
не будет работать. Пока не понятно, как быть в такой ситуации.
Понятно тогда почему, вот что пишут в документации
Это означает, что если просто удалить объект, например, так
То лайк, дислайк останется. Аналогичное поведение будет в том случае, если удалить статью через админку.
Если же удалять через GenericRelation, что очевидно вы и делаете, то тогда лайк дислайк удаляется.
У меня сейчас нет возможности проверить, но попробуйте сделать удаление статьи через админ панель, а не как вы это делаете сейчас, полагаю, что у вас есть какой-то специальный функционал для этого.
Как раз таки я через админку и удалял. У меня был pre_delete signal, но я его закомментировал и попробовал снова. Результат такой же. При удалении статьи с полем GenericRelation, удаляется LikeDislike, а без этого поля не удаляется.
Видимо, придется писать какую-нибудь функцию для удаления.
Тогда я после работы посмотрю, как там обстоят дела. Дело в том, что в django-tagging как раз наоборот TaggedItem объекты остаются.
Можете пока тоже глянуть в исходники django-tagging, возможно это натолкнёт вас на идею. Я не раньше, чем часов через 8 смогу посмотреть туда.
Я перекопал все, что мог. В итоге пока пришел к тому, чтобы не использовать GenericRelation. Зато теперь все стало муторно.
Во вьюхе VoteView пришлось передалать:
И пришлось ещё создать дополнительные template tag. Типо:
Вместо вашего user_in.
И для вывода количества лайков на статье.
Естественно ещё и для дизлайка пришлось добавить.
Да. Знаете, я там ошибался немного по поводу GenericRelation. В Tagging это не используется, поэтому и TaggedItem остаются.
Вам придётся отказаться от GenericRelation, если вы хотите получить тот функционал, который вам нужен.
Большое спасибо вам за помощь в решении моих вопросов.
Hi, its possible to count likes and dislikes in this app something like reddit count?
Yes, it is possible.
For summary count you have here method sum_rating() in LikeDislikeManager
Just, need to rewrite javascript methods. Just write something like this in JS
It's possible to simply add vote option for non logged users?
Yes. You can use IP Address of user instead of Foreign Key of logged user.
In this article you will see, how to get IP Address from request. For field you can use GenericIPAddressField
тут view написан в class based view, если честно ничего не могу разобрать. Как это всё переписать в function view?
function view для модели Article и LikeDislike.LIKE будет выглядеть так
Для LikeDislike.DISLIKE соответственно заменить в том коде. Но это сплошная копипаста получается.
Приветствую вас Евгений , давно наблюда за развитием вашего замечательного портала, много полезно тут нашел , переодически зачитываюсь.
Теперь по сушеству, делаю портал и там идеально ложиться также по лайкам полиморфная модель (comments ,post,image,...)ну вот наткнулся на такую статью https://djbook.ru/examples/88/ Буду очень признателен , если скажете своё мнение.
Добрый день.
Да, я тоже читал ту статью в своё время и согласен с тем, что внешние ключи гораздо лучше, чем GenericForeignKey. Выборки в ряде случае работают быстрее.
Но лично мне проще для закладок, лайков и дислайков, уведомлений и прочего использовать GenericForeignKey в силу того, что в текущем состоянии проекта мне приходится совершать гораздо меньше телодвижений для внедрения новых связей к тем же самым лайкам и дислайкам, когда появляется новая модель данных. У меня просто есть соответствующий миксин с GenericRelation, который сразу разруливает все имена и устанавливает нужные связи. А с использованием ContentType очень большое количество кода удалось переписать в обобщённый код. Лично для меня это является более поддерживаемым решением, поскольку программным кодом портала я занимаюсь в одиночку. И так хватает мест, где есть риск что-то забыть или недописать.
В любом случае при росте проекта и добавлении новых моделей придётся добавлять новую колонку в полиморфную модель, как написано в той статье. Вот и посчитаем, например, для моего сайта, сколько колонок нужно было бы для одних только лайков и дислайков:
Получается по меньшей мере 16 колонок в полиморфной модели, в которой как минимум 15 всегда будут NULL. Я не уверен в том, как это будет влиять на размер баз данных, но мне кажется, что при большом проекте это может стать несколько избыточным, хранить большое количество NULL полей ради того, чтобы больше заботиться о целостности базы данных. На самом деле GenericRelation решает проблему целостности базы данных автоматически и при удалении контента также удаляет записи с GenericForeignKey. Поэтому н могу сказать, что я испытывал проблмы с этим.
Доброго времени суток.
Спасибо за хороший ответ, У меня ситуация така что в галлереи будет несколько миллионов фотографий с фильтрами и тегами , и я опасаюсь за производительност . Это основное что останавливает меня пере GenericForeignKey , пока остановился на первом способе в той статье(Альтернатива 1 - NULL поля в исходной таблице) , к сожалению не нашёл практического бенчмарка какого либо.
Кстати интересные темы нашёл тут https://emacsway.github.io/ru/django-framework/#django-models Может что полезного тоже Евгений найдёте
А как получить имя пользователя, который поставил лайк?
Думал так, но похоже что нет. {{ post.votes.likes.user.username }}
Это же QuerySet будет, а не отдельный единственный объект
Спасибо большое:)
Доброго времени суток!) Я случайно набрел на вашу статью, и она помогла мне решить некоторые мои трудности, я прошел за вами по шагам, в попытках адаптировать это под себя, и возник вопрос. У вас тут не рендениться шаблон, и как выводятся свежие данные по лайкам дизлайкам? в моем варианте, они начинают отображаться только после клика по кнопке. и дизлайк ведет себя, так же как лайк добавля или убирая лайк с того же юзера, при том не увеличивая дизлайки, я где-то ошибся?
как вы понимаете, я новичок и в питон и джанго и в js. не рубите с плеча
мой шаблон html
все, я со всем разобрался!) Извините!)
А так для общей суммы пойдёт?
У меня ещё вопросик, а не подскажите, как выводить окошко авторизоваться, если вы не авторизованы и хотите поставить лайк? Как его вообще подхватит?
Наверное да
У меня разный рендеринг для авторизованного и не авторизованного пользователя. Определяется при запросе страницы.
благодарю за ответ
Меня интересует только один вопрос в плате SQL запросов. Их почему то плодится очень много с системой лайков и дизлайков
Решили вопрос? Тоже интересует оптимизация запросов к БД
Нет я пока что не разбирался с этим. Решил заняться оптимизацией после того как закончу проэкт. Но мне бы всё же хотелось узнать мнение автора.
В случае Generic ключей имеется проблема в том, что ключ формируется по id и названию таблицы, в итоге у Django меньше возможностей для оптимизированного запроса. ЭТо один из недостатков GenericForeignKey.
Саму по себе проблему оптимизации можно попытаться решить следующими методами описанными в этой статье
Но всё равно могут остаться некоторые дополнительные запросы к базе данных. Поэтому можно кэшировать. Но стандартные средства кэширования не подойдут. Лично я разработал для этого специальный декоратор. Об этом можно почитать в этой статье EVILEG-CORE. Кэширование свойств объектов моделей с помощью model_cached_property
Я пытался решить всё с помощью вашей статьи про оптимизацию. Увы не работает. Всё так же на 5 записей 10 запросов к бд. Причём если пытатся оптимизировать с помощью Prefetch запросов становится ещё больше.
Недостаток GenericForeignKey в том, что они достаточно плохо оптимизируются, поэтому я решил это с помощью кэширования.
Собственно говоря, эти проблемы и послужили возникновению специального декоратора для кэширования свойств объектов моделей.
Можете почитать здесь Кэширование свойств объектов моделей с помощью model_cached_property
Спасибо. Я подумаю над таким решением. Если не ошибаюсь EVILEG_CORE на github выложен?
Да, можете выдрать сам декоратор оттуда, а то у меня руки не доходят переписать его для актуальной версии Django, там есть deprecated вещи
Хорошо. Большое спасибо. Посмотрю что из этого получится.
у меня ошибка почемуто в модели
models.py", line 70, in LikeDislike
vote = models.SmallIntegerField(verbose_name= ("Голос"), choices=VOTES)
NameError: name ' ' is not defined
Подскажите пожалуйсто как исправить?
Ошибка скорее всего на другой строке, но интерпретатор понял этого так. Может где-то лишний пробел добавили или кавычка не та. Во всяком случае из этого лога не ясно, в чём ошибка.
Ваша ошибка связана с gettext
Поле должно выглядеть так
Так как вы просто забыли _ и по этому вышла ошибка