I will assume that the assessment is also done for articles.
models.py
from django.db import models from django.db.models import Sum, Case, When, Value # Модель для хранения голоса пользователя # за или против статьи class Vote(models.Model): voter = models.ForeignKey(User, verbose_name='Пользователь', related_name='+') article = models.ForeignKey(Article, verbose_name='Статья') positive = models.BooleanField('За') class Article(models.Model): ... # Свойство возвращающее сумму голосов 'За' # с вычетом голосов 'Против' @property def votes(self): return (self.vote_set .annotate(value=Case(When(positive=True, then=Value(1)), When(positive=False, then=Value(-1)), default=Value(0))) .aggregate(Sum('value'))
views.py
@login_required # только аутентифицированные пользователи могут голосовать def vote(request, pk, reaction): article = get_object_or_404(Article, pk=pk) vote, created = Vote.objects.get_or_create(voter=request.user, article=article, defaults={'positive': reaction}) # Если уже есть голос этого пользователя за эту # статью, то возвращаем ошибку 403 if not created: return HttpResponseForbidden('Голосовать можно только один раз') # Перенаправляем пользователя на ту страницу # с которой был отправлен голос или # на ту статью, за которую он был отправлен. next = request.META.get('HTTP_REFERER', article) return redirect(next)
urls.py
urlpatterns = [ ... re_path(r'^vote/(?P<pk>\d+)/(?P<reaction>up|down)/$', vote, name='vote'), ]
article.html
<div class="votes"> <a href="{% url 'vote' instance.pk 'down' %}"> <i class="fas fa-thumbs-down"></i> </a> <span> {{ instance.votes }} </span> <a href="{% url 'vote' instance.pk 'up' %}"> <i class="fas fa-thumbs-up"></i> </a> </div>
Only there is one nuance, since voting is not an idempotent operation, it must be carried out through the form using the POST method, and not by reference. Otherwise, the browser can vote for the user, simply by loading the page of possible transitions.