In the article on creating a system of bookmarks on Django, an example was considered with the use of an abstract model for several types of bookmarks, namely for articles and comments on articles. Attention was also drawn to the fact that the model fields that had foreign keys to different models should have the same names to support the possibility of creating a single interface for adding bookmarks. This is made possible by the so-called "duck typing", which implies that objects that do not have a single inheritance hierarchy can be used in the same scenario if there are interfaces (methods) that have the same signature.

Literally the principle of duck typing sounds like this:

If it looks like a duck, swims like a duck and quacks like a duck, then it probably is a duck.

That is, having methods with the same signature, we can use objects that are not associated with an inheritance hierarchy, in the same context.

In the same article, let's consider the variant when using the Like Dislike system, not two different tables for articles and comments are used, and not even one that will contain a foreign key for an article or comment (that is, two columns, One of the columns, depending on what type of content the user activity is related to), and one table that will contain:

content_type - The content type to which the record belongs

- The content type to which the record belongs object_id - Record ID content_object - The generated foreign key to write, in fact the content object

- Record ID Other additional fields

The LikeDislike Data Model with GenericForeignKey

Now let's take a closer look at what a data model can look like that can work with any type of content.

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, 'Dislike'), (LIKE, '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()

In this code there are both mandatory fields for using polymorphic links, and additional fields that already characterize the type of user activity.

The Like Dislike system in this case is based on the principle +1/-1, which can be used for voting with the calculation of the total rating. There is also a foreign key to the user who voted for the article or comment.

To account for the content type, the ContentType model is used, which is used in the Django admin panel and generates logs. In fact, when creating data models, the ContentType entry for this model is automatically created. Thus, all tables are taken into account in the ContentType model. This is the basis for the system for logging administrator actions in Django . This foreign key is assigned to the content_type field.

object_id contains the primary key ID of the model instance for which the relationship is created.

content_object contains a field for communication with any model and it is the GenericForeignKey class. If the two previous fields have names other than content_type and object_id , then they must be passed as arguments to GenericForeignKey . If not different, then GenericForeignKey will independently define them and will use them to create polymorphic relationships.

Also in the model there is a field of objects, which is assigned a special model manager, which will facilitate the work of getting separately Like and Dislike counters, as well as their total rating.

LikeDislikeManager

This model manager will allow you to take separately Like and Dislike entries for the current likedislike_set article or comment.

from django.db import models from django.db.models import Sum class LikeDislikeManager(models.Manager): use_for_related_fields = True def likes(self): # We take the queryset with records greater than 0 return self.get_queryset().filter(vote__gt=0) def dislikes(self): # We take the queryset with records less than 0 return self.get_queryset().filter(vote__lt=0) def sum_rating(self): # We take the total rating return self.get_queryset().aggregate(Sum('vote')).get('vote__sum') or 0

Adding a link to the article and comment model

Adding links is done using the GenericRelation class, which, unlike the fields content_type , object_id , GenericForeignKey , does not create additional database migrations.

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')

Two arguments are passed to GenericRelation:

Model with polymorphic connections, as you can see it is the same for both models. related_query_name - The name of the model, according to which it will be possible to do reverse sampling. By default, GenericForeignKey can not do them.

This means that if you have the related_query_name , you can pick up the voices of a particular user to articles, or to comments using sorting. And for simplicity, you can implement it in LikeDislikeManager , adding two more methods.

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')

Pay attention to the argument order_by . Without specifying the related_query_name , such an argument would give a 500 error. And so you can pick up all the likes with sorting by the date of publication of articles or comments. For a Like Dislike system, this is not very necessary, but for bookmarks it can be useful.

views.py

And now consider a view that will add or remove a user's voice from an article or comment. The implementation of the vote will be made using AJAX requests.

The main point is that in the file urls.py we will set the data model for this View, for which we vote, and also the type of the Like or Dislike voice.

Sending the request, we try to take the record and if it exists, then we will set the voice type to the opposite one, or we will delete the voice. If the entry does not exist, then add a voice record with the current Like or Dislike type.

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 # Data Model - Articles or Comments vote_type = None # Vote type Like/Dislike def post(self, request, pk): obj = self.model.objects.get(pk=pk) # GenericForeignKey does not support 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

A regular expression record for a URL determines the type of content for which we vote, its primary key and the type of voice. This produces 4 types of 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

The html code in my case will look like this:

<ul> <li data-id="{{ like_obj.id }}" data-type="article" data-action="like" title="Like"> <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="Dislike"> <span class="glyphicon glyphicon-thumbs-down"></span> <span data-count="dislike">{{ like_obj.votes.dislikes.count }}</span> </li> </ul>

In the previous article , the same principle of forming counters was applied.

data-id - Responsible for pk content, which can be added to the bookmarks.

- Responsible for pk content, which can be added to the bookmarks. data-type - Type of content, the same name appears in the url.

- Type of content, the same name appears in the url. data-action - The action to be taken, in this case adding to the bookmarks

- The action to be taken, in this case adding to the bookmarks data-count - Counter that shows how many users have added content to bookmarks

The difference is that to get the amount of Like and Dislike , not the methods of the models of these articles and comments, but the methods of the LikeDislikeManager , which further simplifies further development, since it will not be necessary to keep track of whether all the models have enough methods. It is enough to add the GenericRelation field.

JavaScript

In the previous article on bookmarks on the site, I already told that it is necessary to handle the CSRF token for AJAX-requests. Therefore, I will not duplicate the information.

I will show only the handlers of AJAX-requests. There will be two. One for Like, the second for 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; } // Connecting Handlers $(function() { $('[data-action="like"]').click(like); $('[data-action="dislike"]').click(dislike); });

For Django I recommend VDS-server of Timeweb hoster .