Evgenii Legotckoi
Evgenii Legotckoi30 квітня 2017 р. 12:17

Django – Підручник 023. Система «Подобається Dislike» з використанням GenericForeignKey

У статті по створенню системи закладок на Django було розглянуто приклад з використанням абстрактної моделі для декількох типів закладок, а саме для статей та коментарів до статей. Також було наголошено на тому, що поля моделей, які мали зовнішні ключі на різні моделі, повинні мати однакові назви, щоб підтримувати можливість створення єдиного інтерфейсу для додавання закладок. Це стає можливим завдяки так званій "качиній типізації" (Duck typing)**, яка передбачає, що об'єкти, які не мають єдиної ієрархії успадкування, можуть використовуватися в тому самому сценарії за наявності інтерфейсів (методів), що мають однакову сигнатуру.

Дослівно принцип качиної типізації звучить так:

> Якщо це виглядає як качка, плаває як качка і крякає як качка, це, можливо, і є качка.
>
> Якщо вона схожа на качку, плаває як качка і крякає, як качка, то, ймовірно, це качка.
>
>

Тобто, маючи методи з тією самою сигнатурою, ми можемо використовувати об'єкти, не пов'язані ієрархією спадкування, в тому самому контексті.

У цій статті розглянемо варіант, коли для створення системи Like Dislike використовується не дві різні таблиці для статей і коментарів, і навіть не одна, яка міститиме за зовнішнім ключем на статтю або коментар (тобто дві колонки, і при цьому буде заповнюватися тільки одна з колонок залежно від того, до якого типу контенту належить активність користувача), а одна таблиця, яка міститиме:

  • content_type - тип контенту, до якого належить запис
  • object_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-запитів. Їх буде два. Один для 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 .

Рекомендуємо хостинг TIMEWEB
Рекомендуємо хостинг TIMEWEB
Стабільний хостинг, на якому розміщується соціальна мережа EVILEG. Для проектів на Django радимо VDS хостинг.

Вам це подобається? Поділіться в соціальних мережах!

ИМ
  • 03 лютого 2018 р. 20:54

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

Evgenii Legotckoi
  • 04 лютого 2018 р. 07:22

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

ИМ
  • 04 лютого 2018 р. 08:04

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

Evgenii Legotckoi
  • 04 лютого 2018 р. 08:09

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

Ещё один момент, посмотрите вот в этой статье раздел про использование CSRF токена .
Возможно, что он не используется в Cookies, тогда AJAX работать не будет.
ИМ
  • 04 лютого 2018 р. 08:55

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


ИМ
  • 04 лютого 2018 р. 08:56
Evgenii Legotckoi
  • 04 лютого 2018 р. 08:59
  1. Там ошибка 404, напрягает наличие ещё одного слеша в url вашего API
  2. Проверьте, всё ли прописали в urls.py файле. Опять же из-за ошибки 404.
l
  • 25 червня 2018 р. 04:27

really nice work .thanks.

MU
  • 25 грудня 2018 р. 12:13

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.

Evgenii Legotckoi
  • 25 грудня 2018 р. 13:59

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.

Руслан Волшебник
  • 04 січня 2019 р. 09:57

Здравствуйте. Ваша система очень хорошо работает.Спасибо.
Но у меня есть вопрос. Допустим, проголосовал пользователь или нет, можно проверить по условию "if post.votes.user == request.user". А как опредилить какой голос пользователь поставил (лайк или дизлайк)?

Evgenii Legotckoi
  • 04 січня 2019 р. 10:07

Добрый день.

Если у вас уже выбран объект голоса для конкретного пользователя, то можно проверить так.

if like_dislike_object.vote == LikeDislike.LIKE:
    # ToDo Something by Like
else:
    # ToDo Something by Dislike

Ну или проверить, лайкнул ли пользователь из всех лайков так

obj.votes.likes().filter(user=request.user)

Спасибо. А не подскажите как это в шаблоне проверить?

Лучше я, наверное, более подробно опишу. Есть несколько постов на странице и мне нужно проверить какой голос поставил пользователь. Если стоит лайк, то закрасить кнопку лайк, а если дизлайк, то кнопку дизлайк. Как это решить?

Evgenii Legotckoi
  • 04 січня 2019 р. 11:25

Я понял. Я вам позже отвечу. У меня это реализовано, но нужно посмотреть исходники сайта.

Руслан Волшебник
  • 05 січня 2019 р. 06:45
  • (відредаговано)

Пока что, мне пришло в голову такое решение.
Я добавил в LikeDislikeManager следующий метод:

def likers(self):
  like_dislike = self.get_queryset()
  users = []
  for item in like_dislike:
    if item.vote == 1:
      users.append(item.user)
  return users

И в шаблоне проверяю таким образом: {% if request.user in post.votes.likers %}.

Evgenii Legotckoi
  • 05 січня 2019 р. 08:02
  • (відредаговано)

я написал template tag, фильтр, через который делаю проверку прямо в шаблоне

@register.filter
def user_in(objects, user):
    if user.is_authenticated:
        return objects.filter(user=user).exists()
    return False

Для этого подгружаю модуль в шаблоне и проверяю наличие пользователя в queryset

{% load users_extras %}
<span class="mdi mdi-star mr-1 {% if obj.likes.all|user_in:user %}text-success{% endif %}"></span>

Плюс в том, что могу делать такую проверку для какой угодно модели данных, у которой поле пользователя называется user. И нет никакой зависимости на ModelManager

Спасибо большое.

Руслан Волшебник
  • 08 січня 2019 р. 12:09

Снова здравствуйте). Есть ещё вопрос. Как сохранить объект LikeDislike, при удалении статьи? Пробовал

content_type = models.ForeignKey(ContentType, null=True, blank=True, on_delete=models.SET_NULL)

Не помогло.

Evgenii Legotckoi
  • 08 січня 2019 р. 14:52

Добрый день!

А зачем его вообще сохранять, если статья удалена? Вообще объект LikeDislike как раз сохраняется... в этом-то и проблема связей GenericForeignKey, я бы как раз удалял это добро. А content_type - это внешний ключ на модель ContentType, то есть это будет влиять только при удалении самого типа контента статей, а не конкретной статьи.

Руслан Волшебник
  • 09 січня 2019 р. 04:45

Спасибо за помощь.

Руслан Волшебник
  • 09 січня 2019 р. 04:50
  • (відредаговано)

-"А зачем его вообще сохранять, если статья удалена?".

Мне нужно знать сколько лайков/дизлайков получал пользователь за все свои статьи, даже если он удалил какую-нибудь статью.

Evgenii Legotckoi
  • 09 січня 2019 р. 04:57
  • (відредаговано)

вообще GenericForeignKey не удаляется, если только вы там что-то не нахимичили. Проблема, как раз в обратном, чтобы его удалять и не было битых отношений.

Просто удалите статью с лайком и посмотрите, что там в админ панели, только учтите, что если имеете обращение к самой статье через лайк, то получите ошибку 500. Нужно обрабатывать эту проблему дополнительно.

Evgenii Legotckoi
  • 09 січня 2019 р. 04:59

Я бы ещё добавил тогда поле target_user, куда будет добавляться пользователь, который получил лайк, тогда при удалении статьи лайк будет соотноситься с сами пользователем.

Руслан Волшебник
  • 09 січня 2019 р. 05:07

-"вообще GenericForeignKey не удаляется, если только вы там что-то не нахимичили."

Если использовать ваш код, то при удалении статьи объект LikeDislike не будет удалятся?
Я правильно понял?

Руслан Волшебник
  • 09 січня 2019 р. 05:08

Просто, если это так, то очень странно. Потому что у меня LikeDislike удаляется.

Evgenii Legotckoi
  • 09 січня 2019 р. 05:15

да? я перепроверю. Но у меня вроде бы были проблемы с этим делом. Во всяком случае кое-какие объекты, например, теги оставляют битый мусор. Возможно в последних верссиях Джанго дело обстоит иначе уже, я начинал с Джанго 1.10.

Руслан Волшебник
  • 09 січня 2019 р. 05:37
  • (відредаговано)

Видимо всё из-за GenericRelation. Если его убрать со статьи, то объект LikeDislike не удаляется. Но если его убрать, то я не смогу обращаться к LikeDislike через статью. Допустим ваш template tag

@register.filter
def user_in(objects, user):
    if user.is_authenticated:
        return objects.filter(user=user).exists()
    return False
{% load users_extras %}
<span class="mdi mdi-star mr-1 {% if obj.likes.all|user_in:user %}text-success{% endif %}"></span>

не будет работать. Пока не понятно, как быть в такой ситуации.

Evgenii Legotckoi
  • 09 січня 2019 р. 05:48

Понятно тогда почему, вот что пишут в документации

Note also, that if you delete an object that has a GenericRelation, any objects which have a GenericForeignKey pointing at it will be deleted as well. In the example above, this means that if a Bookmark object were deleted, any TaggedItem objects pointing at it would be deleted at the same time.
Unlike ForeignKey, GenericForeignKey does not accept an on_delete argument to customize this behavior; if desired, you can avoid the cascade-deletion simply by not using GenericRelation, and alternate behavior can be provided via the pre_delete signal.

Это означает, что если просто удалить объект, например, так

Article.objects.filter(id=tearget_id).delete()

То лайк, дислайк останется. Аналогичное поведение будет в том случае, если удалить статью через админку.

Если же удалять через GenericRelation, что очевидно вы и делаете, то тогда лайк дислайк удаляется.

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

Руслан Волшебник
  • 09 січня 2019 р. 06:01

Как раз таки я через админку и удалял. У меня был pre_delete signal, но я его закомментировал и попробовал снова. Результат такой же. При удалении статьи с полем GenericRelation, удаляется LikeDislike, а без этого поля не удаляется.

Руслан Волшебник
  • 09 січня 2019 р. 06:02

Видимо, придется писать какую-нибудь функцию для удаления.

Evgenii Legotckoi
  • 09 січня 2019 р. 06:05
  • (відредаговано)

Тогда я после работы посмотрю, как там обстоят дела. Дело в том, что в django-tagging как раз наоборот TaggedItem объекты остаются.

Можете пока тоже глянуть в исходники django-tagging, возможно это натолкнёт вас на идею. Я не раньше, чем часов через 8 смогу посмотреть туда.

Руслан Волшебник
  • 09 січня 2019 р. 11:28

Я перекопал все, что мог. В итоге пока пришел к тому, чтобы не использовать GenericRelation. Зато теперь все стало муторно.

Во вьюхе VoteView пришлось передалать:

LikeDislike.objects.create(content_type=content_type, vote=self.vote_type, object_id=obj.id, user=request.user, target_user=obj.user)
like_count = LikeDislike.objects.filter(content_type=content_type, object_id=obj.id, vote=LikeDislike.LIKE).count()
dislike_count = LikeDislike.objects.filter(content_type=content_type, object_id=obj.id, vote=LikeDislike.DISLIKE).count()

И пришлось ещё создать дополнительные template tag. Типо:

@register.filter
def like_in(obj, user):
    if user.is_authenticated:
        try:
            likedislike = LikeDislike.objects.get(content_type=ContentType.objects.get_for_model(obj), object_id=obj.id, user=user)
            if likedislike.vote == 1:
                return True
            return False
        except LikeDislike.DoesNotExist:
            return False
    return False

Вместо вашего user_in.

И для вывода количества лайков на статье.

@register.simple_tag
def like_count(obj):
    likedislike = LikeDislike.objects.filter(content_type=ContentType.objects.get_for_model(obj), object_id=obj.id, vote=LikeDislike.LIKE)
    return likedislike.count()

Естественно ещё и для дизлайка пришлось добавить.

Evgenii Legotckoi
  • 09 січня 2019 р. 14:35

Да. Знаете, я там ошибался немного по поводу GenericRelation. В Tagging это не используется, поэтому и TaggedItem остаются.

Вам придётся отказаться от GenericRelation, если вы хотите получить тот функционал, который вам нужен.

Руслан Волшебник
  • 10 січня 2019 р. 03:16

Большое спасибо вам за помощь в решении моих вопросов.

MU
  • 07 березня 2019 р. 13:58

Hi, its possible to count likes and dislikes in this app something like reddit count?

Evgenii Legotckoi
  • 08 березня 2019 р. 03:35
  • (відредаговано)

Yes, it is possible.

For summary count you have here method sum_rating() in LikeDislikeManager

obj.votes.sum_rating()

Just, need to rewrite javascript methods. Just write something like this in JS

success : function (json) {
    summary_counter.find("[data-count='counter']").text(json.sum_rating);
}
MU
  • 20 березня 2019 р. 11:43

It's possible to simply add vote option for non logged users?

Evgenii Legotckoi
  • 21 березня 2019 р. 04:12

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

OK
  • 10 вересня 2019 р. 16:10

тут view написан в class based view, если честно ничего не могу разобрать. Как это всё переписать в function view?

Evgenii Legotckoi
  • 10 вересня 2019 р. 16:38

function view для модели Article и LikeDislike.LIKE будет выглядеть так

def like(request, pk):
    obj = Article.objects.get(pk=pk)
    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 LikeDislike.LIKE:
            likedislike.vote = LikeDislike.LIKE
            likedislike.save(update_fields=['vote'])
            result = True
        else:
            likedislike.delete()
            result = False
    except LikeDislike.DoesNotExist:
        obj.votes.create(user=request.user, vote=LikeDislike.LIKE)
        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"
    )

Для LikeDislike.DISLIKE соответственно заменить в том коде. Но это сплошная копипаста получается.

Misha Lebedev
  • 14 вересня 2019 р. 17:08

Приветствую вас Евгений , давно наблюда за развитием вашего замечательного портала, много полезно тут нашел , переодически зачитываюсь.
Теперь по сушеству, делаю портал и там идеально ложиться также по лайкам полиморфная модель (comments ,post,image,...)ну вот наткнулся на такую статью https://djbook.ru/examples/88/ Буду очень признателен , если скажете своё мнение.

Evgenii Legotckoi
  • 17 вересня 2019 р. 03:23
  • (відредаговано)

Добрый день.

Да, я тоже читал ту статью в своё время и согласен с тем, что внешние ключи гораздо лучше, чем GenericForeignKey. Выборки в ряде случае работают быстрее.
Но лично мне проще для закладок, лайков и дислайков, уведомлений и прочего использовать GenericForeignKey в силу того, что в текущем состоянии проекта мне приходится совершать гораздо меньше телодвижений для внедрения новых связей к тем же самым лайкам и дислайкам, когда появляется новая модель данных. У меня просто есть соответствующий миксин с GenericRelation, который сразу разруливает все имена и устанавливает нужные связи. А с использованием ContentType очень большое количество кода удалось переписать в обобщённый код. Лично для меня это является более поддерживаемым решением, поскольку программным кодом портала я занимаюсь в одиночку. И так хватает мест, где есть риск что-то забыть или недописать.

В любом случае при росте проекта и добавлении новых моделей придётся добавлять новую колонку в полиморфную модель, как написано в той статье. Вот и посчитаем, например, для моего сайта, сколько колонок нужно было бы для одних только лайков и дислайков:

  • Section, Article, Comments
  • ForumSection, ForumTopic, ForumComment
  • SocialBookmarkSection, SocialBookmark, SocialBookmarkComment
  • TestSection, Test
  • IdeaSection, Idea, IdeaComment
  • Vacancy
  • Company

Получается по меньшей мере 16 колонок в полиморфной модели, в которой как минимум 15 всегда будут NULL. Я не уверен в том, как это будет влиять на размер баз данных, но мне кажется, что при большом проекте это может стать несколько избыточным, хранить большое количество NULL полей ради того, чтобы больше заботиться о целостности базы данных. На самом деле GenericRelation решает проблему целостности базы данных автоматически и при удалении контента также удаляет записи с GenericForeignKey. Поэтому н могу сказать, что я испытывал проблмы с этим.

Misha Lebedev
  • 17 вересня 2019 р. 04:50

Доброго времени суток.

Спасибо за хороший ответ, У меня ситуация така что в галлереи будет несколько миллионов фотографий с фильтрами и тегами , и я опасаюсь за производительност . Это основное что останавливает меня пере GenericForeignKey , пока остановился на первом способе в той статье(Альтернатива 1 - NULL поля в исходной таблице) , к сожалению не нашёл практического бенчмарка какого либо.

Misha Lebedev
  • 17 вересня 2019 р. 06:07

Кстати интересные темы нашёл тут https://emacsway.github.io/ru/django-framework/#django-models Может что полезного тоже Евгений найдёте

Владислав Меленчук
  • 29 травня 2020 р. 12:43

А как получить имя пользователя, который поставил лайк?
Думал так, но похоже что нет. {{ post.votes.likes.user.username }}

Думал так, но похоже что нет. {{ post.votes.likes.user.username }}

Это же QuerySet будет, а не отдельный единственный объект

{% for vote in post.votes %}
  {{ vote.user.username }}
{% endfor %}

Спасибо большое:)

s
  • 04 серпня 2020 р. 07:09
  • (відредаговано)

Доброго времени суток!) Я случайно набрел на вашу статью, и она помогла мне решить некоторые мои трудности, я прошел за вами по шагам, в попытках адаптировать это под себя, и возник вопрос. У вас тут не рендениться шаблон, и как выводятся свежие данные по лайкам дизлайкам? в моем варианте, они начинают отображаться только после клика по кнопке. и дизлайк ведет себя, так же как лайк добавля или убирая лайк с того же юзера, при том не увеличивая дизлайки, я где-то ошибся?

$(function () {
$('.dislike').click(function(){
 const csrftoken = jQuery("[name=csrfmiddlewaretoken]").val();
$.ajaxSetup({
    beforeSend: function(xhr, settings) {
            xhr.setRequestHeader("X-CSRFToken", csrftoken);
    }
});
    var dislike = $(this);
    var type = dislike.data('type');
    var pk = dislike.data('id');
    var action = dislike.data('action');
    var like = dislike.prev();

    $.ajax({
        url : "/votes/" + $(this).data('id') + "/dislike/",
        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;
});
});

как вы понимаете, я новичок и в питон и джанго и в js. не рубите с плеча

<div class="col-md-3">
        <div class="card" style="width: 18rem; text-align: center;">
            <img src="{% static 'Thonks.png' %}" class="card-img-top">
            <div class="card-body">
                <h5 class="card-title">{{obj.name}}</h5>
            </div>
            <div class="btn-group col-md-12" role="group" aria-label="Basic example">
                <button type="button" class="btn btn-secondary like col-md-6"
                        data-id="{{ obj.id }}" data-type="article" data-action="like" title="Нравится">Like
                    <span data-count="like">{{ like_obj.votes.likes.count }}</span>
                </button>
                <button type="button" class="btn btn-secondary dislike col-md-6"
                        data-id="{{ obj.id }}" data-type="article" data-action="dislike" title="Не нравится">Dislike
                 <span data-count="dislike">{{ like_obj.votes.dislikes.count }}</span>
                </button>
            </div>
        </div>
    </div>

мой шаблон html

s
  • 04 серпня 2020 р. 07:40

все, я со всем разобрался!) Извините!)

Владислав Меленчук
  • 17 травня 2021 р. 11:30
  • (відредаговано)

А так для общей суммы пойдёт?

    @property
    def total_rate(self):
        return sum([rating.vote for rating in self.rating.all()])
Владислав Меленчук
  • 24 травня 2021 р. 11:16

У меня ещё вопросик, а не подскажите, как выводить окошко авторизоваться, если вы не авторизованы и хотите поставить лайк? Как его вообще подхватит?

Наверное да

У меня разный рендеринг для авторизованного и не авторизованного пользователя. Определяется при запросе страницы.

благодарю за ответ

NSProject
  • 02 квітня 2022 р. 14:06
  • (відредаговано)

Меня интересует только один вопрос в плате SQL запросов. Их почему то плодится очень много с системой лайков и дизлайков

СС
  • 12 квітня 2022 р. 07:36

Решили вопрос? Тоже интересует оптимизация запросов к БД

NSProject
  • 13 квітня 2022 р. 05:21

Нет я пока что не разбирался с этим. Решил заняться оптимизацией после того как закончу проэкт. Но мне бы всё же хотелось узнать мнение автора.

Evgenii Legotckoi
  • 17 квітня 2022 р. 17:34
  • (відредаговано)

В случае Generic ключей имеется проблема в том, что ключ формируется по id и названию таблицы, в итоге у Django меньше возможностей для оптимизированного запроса. ЭТо один из недостатков GenericForeignKey.

Саму по себе проблему оптимизации можно попытаться решить следующими методами описанными в этой статье

Но всё равно могут остаться некоторые дополнительные запросы к базе данных. Поэтому можно кэшировать. Но стандартные средства кэширования не подойдут. Лично я разработал для этого специальный декоратор. Об этом можно почитать в этой статье EVILEG-CORE. Кэширование свойств объектов моделей с помощью model_cached_property

NSProject
  • 17 квітня 2022 р. 21:54

Я пытался решить всё с помощью вашей статьи про оптимизацию. Увы не работает. Всё так же на 5 записей 10 запросов к бд. Причём если пытатся оптимизировать с помощью Prefetch запросов становится ещё больше.

Evgenii Legotckoi
  • 17 травня 2022 р. 05:48

Недостаток GenericForeignKey в том, что они достаточно плохо оптимизируются, поэтому я решил это с помощью кэширования.
Собственно говоря, эти проблемы и послужили возникновению специального декоратора для кэширования свойств объектов моделей.
Можете почитать здесь Кэширование свойств объектов моделей с помощью model_cached_property

NSProject
  • 27 травня 2022 р. 06:29

Спасибо. Я подумаю над таким решением. Если не ошибаюсь EVILEG_CORE на github выложен?

Evgenii Legotckoi
  • 27 травня 2022 р. 08:37

Да, можете выдрать сам декоратор оттуда, а то у меня руки не доходят переписать его для актуальной версии Django, там есть deprecated вещи

NSProject
  • 27 травня 2022 р. 10:58

Хорошо. Большое спасибо. Посмотрю что из этого получится.

Y
  • 21 серпня 2023 р. 18:38

у меня ошибка почемуто в модели

models.py", line 70, in LikeDislike
vote = models.SmallIntegerField(verbose_name= ("Голос"), choices=VOTES)
NameError: name '
' is not defined
Подскажите пожалуйсто как исправить?

Evgenii Legotckoi
  • 22 серпня 2023 р. 03:03

Ошибка скорее всего на другой строке, но интерпретатор понял этого так. Может где-то лишний пробел добавили или кавычка не та. Во всяком случае из этого лога не ясно, в чём ошибка.

NSProject
  • 24 серпня 2023 р. 13:40
  • (відредаговано)

Ваша ошибка связана с gettext

from django.utils.translation import gettext_lazy as _

Поле должно выглядеть так

vote = models.SmallIntegerField(verbose_name=_("Голос"), choices=VOTES) 

Так как вы просто забыли _ и по этому вышла ошибка

Коментарі

Only authorized users can post comments.
Please, Log in or Sign up
sf

C++ - Тест 004. Указатели, Массивы и Циклы

  • Результат:90бали,
  • Рейтинг балів8
МВ

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

  • Результат:68бали,
  • Рейтинг балів-1
ЛС

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

  • Результат:53бали,
  • Рейтинг балів-4
Останні коментарі
A
ALO1ZE19 жовтня 2024 р. 08:19
Читалка файлів fb3 на Qt Creator Подскажите как это запустить? Я не шарю в программировании и кодинге. Скачал и установаил Qt, но куча ошибок выдается и не запустить. А очень надо fb3 переконвертировать в html
ИМ
Игорь Максимов05 жовтня 2024 р. 07:51
Django - Урок 064. Як написати розширення для Python Markdown Приветствую Евгений! У меня вопрос. Можно ли вставлять свои классы в разметку редактора markdown? Допустим имея стандартную разметку: <ul> <li></li> <li></l…
d
dblas505 липня 2024 р. 11:02
QML - Урок 016. База даних SQLite та робота з нею в QML Qt Здравствуйте, возникает такая проблема (я новичок): ApplicationWindow неизвестный элемент. (М300) для TextField и Button аналогично. Могу предположить, что из-за более новой верси…
k
kmssr08 лютого 2024 р. 18:43
Qt Linux - Урок 001. Автозапуск програми Qt під Linux как сделать автозапуск для флэтпака, который не даёт создавать файлы в ~/.config - вот это вопрос ))
АК
Анатолий Кононенко05 лютого 2024 р. 01:50
Qt WinAPI - Урок 007. Робота з ICMP Ping в Qt Без строки #include <QRegularExpressionValidator> в заголовочном файле не работает валидатор.
Тепер обговоріть на форумі
J
JacobFib17 жовтня 2024 р. 03:27
добавить qlineseries в функции Пользователь может получить любые разъяснения по интересующим вопросам, касающимся обработки его персональных данных, обратившись к Оператору с помощью электронной почты https://topdecorpro.ru…
ИМ
Игорь Максимов03 жовтня 2024 р. 04:05
Реализация навигации по разделам Спасибо Евгений!
JW
Jhon Wick01 жовтня 2024 р. 15:52
Indian Food Restaurant In Columbus OH| Layla’s Kitchen Indian Restaurant If you're looking for a truly authentic https://www.laylaskitchenrestaurantohio.com/ , Layla’s Kitchen Indian Restaurant is your go-to destination. Located at 6152 Cleveland Ave, Colu…
КГ
Кирилл Гусарев27 вересня 2024 р. 09:09
Не запускается программа на Qt: точка входа в процедуру не найдена в библиотеке DLL Написал программу на C++ Qt в Qt Creator, сбилдил Release с помощью MinGW 64-bit, бинарнику напихал dll-ки с помощью windeployqt.exe. При попытке запуска моей сбилженной программы выдаёт три оши…
F
Fynjy22 липня 2024 р. 04:15
при создании qml проекта Kits есть но недоступны для выбора Поставил Qt Creator 11.0.2. Qt 6.4.3 При создании проекта Qml не могу выбрать Kits, они все недоступны, хотя настроены и при создании обычного Qt Widget приложения их можно выбрать. В чем может …

Слідкуйте за нами в соціальних мережах