Search code examples
pythondjangodjango-modelsdjango-mptt

How to create nested comment system with django-mptt


I'm working on simple blog and I'm trying to implement a nested comment system for each post.

I created model for comments and it's works fine via Django Admin Page.

I don't know how to create form for posting new comment and replying.

Here is what I have so far:

models.py

(...)
class Post(models.Model):
    author = models.ForeignKey('Author', on_delete=models.CASCADE)
    title = models.CharField(max_length=250)
    slug = models.SlugField(unique=True, blank=True, max_length=250)
    created = models.DateTimeField(auto_now=False, auto_now_add=True)
    modified = models.DateTimeField(auto_now=True, auto_now_add=False)
    tags = TaggableManager(blank=True)
    image = models.ImageField(upload_to="images/%Y/%m/", blank=True, null=True)
    content = models.TextField()

    def get_absolute_url(self):
        return reverse('post_detail', kwargs={'slug': self.slug, })

    # create slug
    def save(self, *args, **kwargs):
        if not self.id:
            self.slug = slugify(unidecode(self.title))
        super(Post, self).save(*args, **kwargs)

    def __str__(self):
        return str(self.title)

class Comment(MPTTModel):
    post = models.ForeignKey("Post", on_delete=models.CASCADE, null=True, related_name='comments')
    parent = TreeForeignKey('self', null=True, blank=True, related_name='replies', db_index=True)

    name = models.CharField(max_length=250)
    email = models.EmailField(max_length=250, blank=True, null=True)
    website = models.CharField(max_length=250, blank=True, null=True)
    created = models.DateTimeField(auto_now=False, auto_now_add=True)
    content = models.TextField()

    def __str__(self):
        return str("{}: {}...".format(self.name, self.content[:50]))

forms.py

class CommentForm(forms.ModelForm):

    class Meta:
        model = Comment
        fields = [ "name", 'email', 'website', 'content']

views.py

class PostDetailView(DetailView):
    model = Post

    def get_context_data(self, **kwargs):
        context = super(PostDetailView, self).get_context_data(**kwargs)
        context['form'] = CommentForm()
        return context


class PostCommentView(SingleObjectMixin, FormView):
    template_name = 'blog/post_detail.html'
    form_class = CommentForm
    model = Post

    def post(self, request, *args, **kwargs):
        self.object = self.get_object()
        return super(PostCommentView, self).post(request, *args, **kwargs)

    def get_success_url(self):
        return reverse('post_detail', kwargs={'slug': self.object.slug})

class PostDetail(View):

    def get(self, request, *args, **kwargs):
        view = PostDetailView.as_view()
        return view(request, *args, **kwargs)

    def post(self, request, *args, **kwargs):
        view = PostCommentView.as_view()
        return view(request, *args, **kwargs)

comment_form.html

<div id="respond">
    <h2 class="respond-title">Leave a comment</h2>
    <span class="form-caution">Make sure you enter the (*) required information where indicated. HTML code is not allowed.</span>
    <form action="" method="post">
        {{ form.non_field_errors }}
        {% csrf_token %}
        {{  form }}
         <input name="submit" type="submit" id="submit" class="btn btn-primary" value="Submit Comment">
    </form>
</div>

Solution

  • In general

    Modified Preorder Tree Traversal is not necessary the best solution for building nested comments. Take into consideration that with each new node whole tree has to be rebuild. For example, lets imagine comments under post F:

    //             F
    //           / | \
    //          B  X  G
    //         / \
    //        Z   Y
    

    Notice that comment X has left value of 8 and right value of 9. But, with every new subcomment to B or X these values will change. E.g. with a new comment under comment Z values of X will change to lft = 10 and rht = 11. This could lead to awful performance and, as I imagine, loss of data in case of messenger means where information is sent only once (e.g. Django channels).


    Customizing ready solutions

    This post presents solution for achieving nested commend system by connecting inbuilt comment app and the mptt. It's an old post and the django.contrib.comments has been separated to a separate project since Django 1.6. Recently Tim Graham added changes to support Django 2.0, so it seems up-to-date.

    The idea is to create a custom model and form that will add to the title field of Django_comments. The post suggest wiring it like that:

    Models.py

    from django_comments.models import Comment
    from mptt.models import MPTTModel, TreeForeignKey
    
    class MPTTComment(MPTTModel, Comment):
        """ Threaded comments - Add support for the parent comment store and MPTT traversal"""
    
        # a link to comment that is being replied, if one exists
        parent = TreeForeignKey('self', null=True, blank=True, related_name='children')
    
        class MPTTMeta:
            # comments on one level will be ordered by date of creation
            order_insertion_by=['submit_date']
    
        class Meta:
            ordering=['tree_id','lft']
    

    Forms.py

    from django import forms
    from django.forms.widgets import widgets        
    from django_comments.forms import CommentForm                            
    from models import MPTTComment
    
    class MPTTCommentForm(CommentForm):
        parent = forms.ModelChoiceField(queryset=MPTTComment.objects.all(),
            required=False, widget=forms.HiddenInput)
    
        def get_comment_model(self):
            # Use our custom comment model instead of the built-in one.
            return MPTTComment
    
        def get_comment_create_data(self):
            # Use the data of the superclass, and add in the parent field field
            data = super(MPTTCommentForm, self).get_comment_create_data()
            data['parent'] = self.cleaned_data['parent']
            return data
    

    __init__.py in your app directory

    from models import MPTTComment
    from forms import MPTTCommentForm
    
    def get_model():
        return MPTTComment
    
    def get_form():
        return MPTTCommentForm
    

    There is also another django package, this one with nested comments already built-in: django-threadedcomments. Worth checking out.