© EVILEG 2015-2018
Рекомендует хостинг
TIMEWEB

Django - Урок 023. Like Dislike система с помощью GenericForeignKey

Like, Django, GenericRelation, Dislike, GenericForeignKey

В статье по созданию системы закладок на 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 передано два аргумента:

  1. модель с полиморфными связями, как видите она одна и та же для обеих моделей.
  2. 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 .

Комментарии

4 февраля 2018 г. 1:54

Доброго времени суток Евгений. Не подскажете как сделать иконки (лайк-дизлайк) кликабельными?

4 февраля 2018 г. 12:22

День добрый. А в каком смысле кликабельными? Вот у меня на сайте они кликаются например. Что конкретно у вас не работает?

Для Django рекомендую VDS-хостинг TIMEWEB

4 февраля 2018 г. 13:04

В прямом, иконки не кликабельные... просто как декор висят.

4 февраля 2018 г. 13:09

А что в консоли отладчика браузера? Может у вас JavaSсript с ошибками отрабатывает?

Ещё один момент, посмотрите вот в этой статье раздел про использование CSRF токена .
Возможно, что он не используется в Cookies, тогда AJAX работать не будет.

Для Django рекомендую VDS-хостинг TIMEWEB

4 февраля 2018 г. 13:55

Дико извиняюсь. Действительно в консоли посмотрел, они кликабельные. Только сыпятся ошбки при клике на $.ajax


4 февраля 2018 г. 13:56
4 февраля 2018 г. 13:59
  1. Там ошибка 404, напрягает наличие ещё одного слеша в url вашего API
  2. Проверьте, всё ли прописали в urls.py файле. Опять же из-за ошибки 404.

Для Django рекомендую VDS-хостинг TIMEWEB

25 июня 2018 г. 8:27

really nice work .thanks.

Комментарии

Только авторизованные пользователи могут оставлять комментарии.
Пожалуйста, Авторизуйтесь или Зарегистрируйтесь
19 октября 2018 г. 8:05
Василий Ильченко

C++ - Тест 001. Первая программа и типы данных

  • Результат:66баллов,
  • Очки рейтинга-1
18 октября 2018 г. 16:14
druidx

C++ - Тест 002. Константы

  • Результат:25баллов,
  • Очки рейтинга-10
18 октября 2018 г. 15:58
druidx

C++ - Тест 001. Первая программа и типы данных

  • Результат:80баллов,
  • Очки рейтинга4
Последние комментарии
19 октября 2018 г. 7:59
Евгений Легоцкой

Python 3 - Урок 001. Введение

Добрый день! Вот это линуксовая директива #!/usr/bin/python3 Указывает, где находится python пакет в системе. А вы видимо под windows разрабатываете. Попробуйте просто у...
19 октября 2018 г. 7:54
Abdurashid Parpieff

Python 3 - Урок 001. Введение

Python 3.7.0 (v3.7.0:1bf9cc5093, Jun 27 2018, 04:06:47) [MSC v.1914 32 bit (Intel)] on win32 Type "copyright", "credits" or "license()" for more information. >>> #!/usr/bi...
18 октября 2018 г. 8:31
Евгений Легоцкой

Qt/C++ - Урок 006. QSqlQueryModel - Таблицы в Qt с помощью SQL-запросов

Как-то даже странно, а вы что ли в отдельный поток убрали базу данных? То есть изначально инстанс создаётся в одном потоке, а все QSqlQuery в другом потоке? Они должны находиться в одном поток...
17 октября 2018 г. 8:43
pasagir

Qt/C++ - Урок 006. QSqlQueryModel - Таблицы в Qt с помощью SQL-запросов

Не получается bool DataBase::insertDataIntoDB(QVariantList data){ QSqlQuery query(db); QString str; qDebug()<<"InsertInsertInsertInsertInsert"<<QThread::curre...
17 октября 2018 г. 7:09
Евгений Легоцкой

Qt/C++ - Урок 006. QSqlQueryModel - Таблицы в Qt с помощью SQL-запросов

Попробуйте передать инстанс базы данных в конструктор QSqlQuery QSqlQuery q(db);
Сейчас обсуждают на форуме
19 октября 2018 г. 11:52
Евгений Легоцкой

Уведомления о лайках и дислайках

Вообще планируется система ачивок. А рейтинг есть в зачаточном состоянии. Но для системы ачивок нужно собраться с духом и выделить приличную кучку времени. Помимо того, что сами ачивки п...
18 октября 2018 г. 12:15
Михаиллл

Настройка Qt Creator для Android

все галочки включены.
18 октября 2018 г. 8:16
Allyans .

Работа с WinAPI в QT(изменение title bar)

Хорошо, я попробуй по вашей статье.
15 октября 2018 г. 12:45
Allyans .

QGraphicsItem change color

Хорошо)
11 октября 2018 г. 10:13
Arrow

Работа с WebView в QML

Нашел в чем проблема. Пишу на случай если кому-то попадется такое же счастье с WebView как и мне. Проблема как оказалась с Debug версией, так как в Release и Profile все работает (...
Присоединяйтесь к нам в социальных сетях