15- Django User Comment System with Replies

First, I want to start with displaying the user comments on the watch movie page for the corresponding movie. So I will change watchmovie view like this.

I fetch the relevant comments fromdatabase by using the query below and pass it to the template

views.py 

def watchmovie(request, id):
    movieobj = Movie.objects.get(pk=id)
    movieobj.numberOfViews += 1
    movieobj.save()

    #This part is for which user watched which movie
    if request.user.is_authenticated:
            Watched.objects.create(user= request.user, movie=movieobj)

    #Get user comments from UserComment model where movie field on UserComment model matches the passed id in the request
    usercomments = UserComment.objects.filter(movie = id)
    return render(request, 'watchmovie.html', {'movieobj': movieobj, 'usercomments': usercomments})

 

Then add a for loop to my template to display comments for this movie

{% extends 'base.html' %}
{% block title %}
    <title>Watch Movie</title>
{% endblock title %}

{% block content %}
    <div class="container">
        <table class="table table-bordered">
            <thead class="thead-dark">
                <tr>
                    <th scope="col">{{movieobj.title}}</th>
                </tr>
            </thead>
            <tbody>
                
                <tr>
                <td>
                    <video controls> 
                    <source src="/{{ MEDIA_URL }}{{movieobj.moviefile}}" type="video/mp4"> 
                    </video>
                </td>
                </tr>

                <tr>
                <td>Viewed: {{movieobj.numberOfViews}} | 
                Duration: {{movieobj.duration}} min. | 
                Published: {{movieobj.publishDate}} | 
                Date Added: {{movieobj.date_added}} | 
                MA Score: {{movieobj.movieapp_score}}
                </td>
                </tr>

                <tr>
                    <td>
                        <h4>Comments:</h4>
                        {% for uc in usercomments%}
                            <tr><td>
                            {{uc.user}}:
                            {{uc.comment}}
                            </td></tr>
                        {%endfor%}
                    </td>
                </tr>

                
            </tbody>
        </table>
    </div>

{% endblock content%}

 

 

 

Now I need to create a comment form for authenticated user where they can enter their comments.

Import UserComment model into forms.py 

forms.py

from .models import MyCustomUser, Movie, UserComment

class UserCommentForm(forms.ModelForm):
    comment = forms.CharField(label="", widget=forms.Textarea(attrs={'rows':4, 'cols':110}))

    class Meta:
        model = UserComment
#excluded fields (user and movie data) will be added to form implicitly in views.py 
        exclude =['user', 'movie']
        fields = ('comment',)

 

 views.py

Import  UserCommentForm and HttpResponseRedirect

from django.http import HttpResponse, HttpResponseRedirect
from moviesapp.forms import MyCustomUserChangeForm, MyCustomUserCreationForm, MyCustomUserPasswordChangeForm, AddMovieForm, UserCommentForm


def watchmovie(request, id):
    if request.method == 'POST':
        initialform = UserCommentForm(request.POST) #form has only the comment data at this point
        form = initialform.save(commit=False) #I will add additional fields(movie and user data), therefore I skip saving the form
        form.user = request.user #get the logged on user and add him to the form as the user
        form.movie = Movie.objects.get(pk=id) #get this movie that is passed with the request and add it to the form
        form.save() #Now save it
        messages.success(request, "Comment is Added", extra_tags='green')
        #I want to return to the same page after a POST request. But the path in urls.py expects url + id
        # Therefore, The easiest way to return to the same page is using HttpResponseRedirect subclass
        return HttpResponseRedirect(request.path_info)

    else:
        movieobj = Movie.objects.get(pk=id)
        movieobj.numberOfViews += 1
        movieobj.save()
        initialform = UserCommentForm()
        #Get user comments from UserComment model where movie field on UserComment model matches the passed id in the request
        usercomments = UserComment.objects.filter(movie = id)
        # Here I pass 3 objects to my template.
    return render(request, 'watchmovie.html', {'movieobj': movieobj, 'usercomments': usercomments , 'form': initialform})

 

 Finally I modified my template accordingly. I added crispy_form_tags beforethe title tags. Then added a form in an if else staement to make sure only logged on user would see this comment form.

watchmovie.html

{% extends 'base.html' %}
{% load crispy_forms_tags %}

{% block title %}
    <title>Watch Movie</title>
{% endblock title %}

{% block content %}
    <div class="container">
        <table class="table table-bordered">
            <thead class="thead-dark">
                <tr>
                    <th scope="col">{{movieobj.title}}</th>
                </tr>
            </thead>
            <tbody>
                
                <tr>
                <td>
                    <video controls> 
                    <source src="/{{ MEDIA_URL }}{{movieobj.moviefile}}" type="video/mp4"> 
                    </video>
                </td>
                </tr>

                <tr>
                <td>Viewed: {{movieobj.numberOfViews}} | 
                Duration: {{movieobj.duration}} min. | 
                Published: {{movieobj.publishDate}} | 
                Date Added: {{movieobj.date_added}} | 
                MA Score: {{movieobj.movieapp_score}}
                </td>
                </tr>

                <tr>
                    <td>

                        <h4>Comments:</h4>
                        {% if user.is_authenticated %}
                        <form method="POST">
                        {% csrf_token %}
                        {{ form.comment | as_crispy_field }}
                        <input type="submit" value="Save" class="btn btn-secondary mb-2">
                        </form>
                        {%endif%}


                        {% for uc in usercomments%}
                            <tr><td>
                            {{uc.user}}:
                            {{uc.comment}}
                            </td></tr>
                        {%endfor%}
                    </td>
                </tr>

                
            </tbody>
        </table>
    </div>

{% endblock content%}

 

 If the user is not logged in he can not see the form. If he is authenticated, this is how it looks.

 

 

 

Adding Reply Feature:

Our users can add new comments for movies by using the form on watchmovies page but they can not add replies to those comments. That's what we are going to add now. The first question we might ask would be what database schema we should have. I read this article while planning the database schema. https://focusustech.com/blog/create-a-comment-and-reply-system-in-django

Our "UserComment" model needs an extra ForeignKey field which points ot UserComment model (itself). This field will store the parent comments id. I also add another DateField field (this is optional) to store the date of the comment. I will use this especially for ordering the comments. This is the modified  UserComment model.

models.py

class UserComment(models.Model):
    user = models.ForeignKey('MyCustomUser', blank=True, null=True, related_name='Comment_User', on_delete=models.CASCADE)
    movie = models.ForeignKey('Movie',blank=True, null=True, related_name='Comment_Movie', on_delete=models.CASCADE)
    comment = models.CharField(max_length=255, blank=True, null=True)
    datePosted = models.DateField(auto_now=True)
    #Many-to-One relationship with itself. because a comment can have many replies, but a reply can have only one parent.
    parent = models.ForeignKey('self', null=True, blank=True, on_delete=models.CASCADE, related_name='replies') 

    
    def __str__(self):
        return str(self.comment)
    class Meta:
        ordering = ('datePosted',)
    
    #by using @property we create a function that queries the table and use the returned queryset as a variable named "children". 
    #we will use this directly as it was a variable in our template
    @property 
    def children(self):
        return UserComment.objects.filter(parent=self).reverse()

    #Check if comment is parent or reply.
    #This also will be used in our template as a variable.
    @property
    def is_parent(self):
        if self.parent is None:
            return True
        return False

 

 

watchmovie.html

This is where my UserCommentForm and user comments are displayed to the user.  Note that we use is_parent and children properties that we defined in oour model here on this template. There is a nested for loop.The first loop displays user and his/her comment if this comment is a parent object. Then second for loop displays the children of this object. Both for loops has Reply link which points to reply comment url and passing comment id and movie id along withthe url.

                <tr>
                    <td>
                        <h4>Comments:</h4>
                        {% if user.is_authenticated %}
                        <form method="POST">
                        {% csrf_token %}
                        {{ form.comment | as_crispy_field }}
                        <input type="submit" value="Save" class="btn btn-secondary mb-2">
                        </form>
                        {%endif%}
                        {% for uc in usercomments%}
                            <tr><td>
                            {% if uc.is_parent %}
                                <b>{{uc.user}}:</b> {{uc.comment}}
                                <br>
                                <a href="/{% url 'replycomment' uc.id movieobj.id %}">Reply</a>
                                <hr>
                            {% endif %}

                            {% for child in uc.children %}
                            &nbsp; &nbsp; &nbsp; <b> {{child.user}}: </b> {{child.comment}}
                            <br>
                            <a href="/{% url 'replycomment' uc.id movieobj.id %}">Reply</a>
                            <hr>
                            {%endfor%}
                            
                            </td></tr>
                        {%endfor%}
                    </td>
                </tr>

 

urls.py

In this url, I am passing comment id and movie id as I previously mentioned

path('replycomment/<int:id>/<int:movieid>/', views.replycomment, name='replycomment')

 

forms.py

I have 2 forms for comments. The first one is the existing form that I use on the watchmovie page. The second one will be used on replycomment.html template while replying the comments.

class UserCommentForm(forms.ModelForm):
    comment = forms.CharField(label="", widget=forms.Textarea(attrs={'rows':4, 'cols':110}))

    class Meta:
        model = UserComment
        exclude =['user', 'movie']
        fields = ('comment',)

class ReplyCommentForm(forms.ModelForm):
    comment = forms.CharField(label="", widget=forms.Textarea(attrs={'rows':4, 'cols':110}))
    class Meta:
        model = UserComment
        exclude =['user', 'movie', 'parent']
        fields = ('comment',)

 

replycomment.html

This is just a simple form that has one textarea for the field "comment"

{% extends 'base.html' %}
{% load crispy_forms_tags %}
{% block title%}
    <title>Edit User Profile</title>
{% endblock title%}

{% block content %}
    <h4>Comments:</h4>
    {% if user.is_authenticated %}
    <form method="POST">
    {% csrf_token %}
    {{ initialform.comment | as_crispy_field }}
    <input type="submit" value="Save" class="btn btn-secondary mb-2">
    </form>
    {%endif%}
    </div>
{% endblock content %}

 

 

views.py

I created another view named replycomment. ReplyCommentForm's posted data is stored in intialform but we do not save it. We add user, movie and parent comment to form and then save it.

def replycomment(request, id, movieid):
    if request.method == 'POST':
        initialform = ReplyCommentForm(request.POST)
        if initialform.is_valid():
            form = initialform.save(commit=False)
            form.user = request.user
            form.movie = Movie.objects.get(pk=movieid)
            form.parent = UserComment.objects.get(pk=id)
            form.save()
            return redirect('watchmovie', movieid)
    else:
        initialform = ReplyCommentForm()
        return render(request, 'replycomment.html', {'initialform': initialform,})

 

 

This is how it looks. Note that child comments are intented.

 

 

Thanks for reading.