Реклама

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

Django, Like, Dislike, GenericForeignKey, GenericRelation

В статье по созданию системы закладок на 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);
});
Реклама

Комментарии

Комментарии

Только авторизованные пользователи могут оставлять комментарии.
Пожалуйста, Авторизуйтесь или Зарегистрируйтесь

Qt - Тест 001. Сигналы и слоты

  • Результат 5 баллов
  • Очки рейтинга -10

C++ - Тест 003. Условия и циклы

  • Результат 57 баллов
  • Очки рейтинга -2

C++ - Тест 003. Условия и циклы

  • Результат 7 баллов
  • Очки рейтинга -10
Последние комментарии
  • EVILEG
  • 7 декабря 2017 г. 9:47

Django - Урок 011. Добавление комментариев на сайт с Django

Визуальный пример чего? комментариев? При ответе на конкретный комментарий рядом с ником отвечающего будет стрелочка и указание ник другого пользователя. Который будет ссылкой на коммента...

  • Bernar
  • 7 декабря 2017 г. 9:24

Django - Урок 011. Добавление комментариев на сайт с Django

есть визуальный пример ?

  • EVILEG
  • 6 декабря 2017 г. 11:30

Django - Урок 011. Добавление комментариев на сайт с Django

Да, так будет даже лучше, я на сайте уже обновил до такого вида код Вот это уже не нужно if request.method == 'POST': Поскольку Вы и так используете метод post, то есть эта про...

  • Bernar
  • 6 декабря 2017 г. 11:19

Django - Урок 011. Добавление комментариев на сайт с Django

сделал немного по другому class EArticleView(View): template_name = 'knowledge/article.html' comment_form = CommentForm def get(self, request, *args, **kwargs): ...

Сейчас обсуждают на форуме
  • Миша
  • 15 декабря 2017 г. 11:26

Как найти в QVector макс и мин

Спасибо

  • Galant
  • 14 декабря 2017 г. 19:58

LPT

Понял! Спасибо!

  • EVILEG
  • 14 декабря 2017 г. 13:38

QCustomPlot можно ли построить прерывистую линию на одном графике?

Во-первых: В pro файле проект по идее достаточно указать следующий define для включения возможности рендеринга через OpenGL DEFINES += QCUSTOMPLOT_USE_OPENGL И во вторых:...

  • EVILEG
  • 13 декабря 2017 г. 8:05

В многопоточности выполнять действие только в одном из потоков

Статическиe методs QThread::currentThread(); и QThread::currentThreadId() могут возвращать указатель на поток и его handle id соответственно. Можете попробовать через как...

  • EVILEG
  • 13 декабря 2017 г. 7:57

А что по поводу авторизации ?

Наличие токена - это правильный подход. Например, у меня на сайте в каждой форме есть токен, чтобы не было возможности подделки запросов. Что касается SSL, то стоит поискать информацию н...