Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to make a django model "commentable", "likeable" and "rateable"

I am using Django 2.0.8 and Python 3.5 for a project. I have different models in my project, some of which, I want to allow commenting on - with both the object (e.g. a blogpost) and comments to the blogpost being likeable.

I am using the threaded comments django app to provide commenting functionality.

Assuming I have a model Foo (see below):

from django.db import models
from django.conf import settings

class Foo(models.Model):
    owner = models.ForeignKey(settings.AUTH_USER_MODEL, blank=False, null=False, default=1, on_delete = models.PROTECT)
    # ...


class Likeable():
    pass

class Rateable():
    pass

How could I use mixins (or any other mechanism for that matter), to make the object Foo "commentable" (i.e. an object which can be commented upon), "likeable" (i.e. an object which can be commented upon) and "rateable" (i.e. an object which can be rated?)- bearing in mind that comments on an objects may be BOTH liked and rated.

like image 818
Homunculus Reticulli Avatar asked Jan 02 '23 00:01

Homunculus Reticulli


2 Answers

According to django documentation , you can achieve this using the Content types Framework. ContentType is a generic model that permits you to track all the models included in INSTALLED_APPS using for that their app_label, model_name and pk. The way it works is easy:

Your generic Comment model

from django.db import models
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.conf import settings


class Comment(models.Model):
    # Generic relation fields
    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey('content_type', 'object_id')
    # Model specific fields
    author = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE
    )
    comment = models.TextField()
    created = models.DatetimeField(auto_now_add=True)
    # ...

Your reusable generic relation model. The best way is using abstract model classes or mixins. For example, using abstract models:

from django.db import models
from django.contrib.contenttypes.fields import GenericRelation


class Commentable(models.Model):
    comments = GenericRelation(Comment)

    class Meta:
        abstract = True

Your Commentable model:

from django.db import models


class Foo(Commentable, Likeable, ...):
    # your stuff

How to use it:

# Add a new comment to Foo
foo = new Foo()
foo.save()
foo.comments.create(author=author, comment="Your comment")

# Retrieve all comments from an specific user no matter the base model
comments = Comment.objects.filter(author=author)

EDIT As @ozren1983 said, each approach has its own downsides, but this is the standard way to do it.

The main advantages are:

  • You can retrieve all the comments (for example) made in all your commentable models in just one query. Using the approach of having a comment, like, etc table per model, you would need to concatenate a query per model. This could be problematic and a bit challenging if you have a lot of models or if you want to merge the results and order them, for example.
  • Just one table per functionality (comments, likes) implies just one database migration in case of change. This could be key if your database is huge.

The main disadvantage is the lack of integrity checks of this generic relationship in database. But if you plan to use the django ORM strictly, nothing should be broken.


BONUS: Another approach that many projects use is inheriting the models (one to one relationship) from an specific one called Item or Thread. Then, you can add all the comments, likes, etc functionalities to this model. This is called multi-table inheritance. An example:

from django.db import models


class Thread(models.Model):
    pass

class Comment(models.Model):
    # Relation with thread
    thread = models.ForeignKey(
        Thread,
        on_delete=models.CASCADE,
        related_name="comments"
    )
    # Model specific fields
    author = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE
    )
    comment = models.TextField()
    created = models.DatetimeField(auto_now_add=True)
    # ...

class Foo(Thread):
    pass

Unlike using the generic relationships, the main advantage of this method is that, this way, you have database integrity checks.

The main disadvantage is that your database structure could become complex.

like image 101
ddiazp Avatar answered Jan 05 '23 14:01

ddiazp


Based on my experience and recommendations in Two scoops of Django, I would advise against using GenericForeignKey and GenericRelation. Two big downsides of that approach are:

  • slow queries
  • danger of data corruption

Instead, I would use following approach. Let's say you have 3 models:

class User(models.Model):
    username = models.CharField(max_length=255)

class Author(models.Model):
    name = models.CharField(max_length=255)

class Post(models.Model):
    title = models.CharField(max_length=255)
    author = models.ForeignKey(Author)

Add abstract Like model, and use it as base class for other models that will implement liking functionality.

class Like(models.Model):
    user = models.ForeignKey(User)
    date_created = models.DateTimeField(auto_now_add=True)

    class Meta:
        abstract = True

class AuthorLike(Like):
    author = models.ForeignKey(Author)

class PostLike(Like):
    post = models.ForeignKey(Post)

Similarly, add abstract Rating model and use it as a base class:

class Rating(models.Model):
    user = models.ForeignKey(User)
    rate = models.PositiveSmallIntegerField()
    date_created = models.DateTimeField(auto_now_add=True)

    class Meta:
        abstract = True

class AuthorRating(Rating):
    author = models.ForeignKey(Author)

class PostRating(Rating):
    post = models.ForeignKey(Post)

You can use same approach to enable liking and rating to the Comments model you are using:

from threadedcomments.models import ThreadedComment

class ThreadedCommentRating(Rating):
    threadedcomment = models.ForeignKey(ThreadedComment)

class ThreadedCommentLike(Like):
    threadedcomment = models.ForeignKey(ThreadedComment)
like image 24
ozren1983 Avatar answered Jan 05 '23 15:01

ozren1983