django comment 댓글 구현

2019-04-21

django comment를 만들어보자 물론 엄청나게 사용하기 쉬운 disqus라는 외부 프로그램이 있지만 서비스를 이용하는 사용자에게는 별로라고 생각한다.
특히 한국에서는 더더욱 그렇기에 어차피 글을 쓸때 로그인이 필요하다면 댓글도 자체서비스로 만드는게 좋다고 생각했다. 개인적으로 정말 어려웠기때문에 조금이라도 사람들에게 이 글이 도움이 됐으면 좋겠다.

우선 댓글을 작성하려면 모델이 있어야한다.

models.py

class Comment(models.Model):
    post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name="comments")
    comment_date = models.DateTimeField(auto_now=True)
    comment_text = models.CharField(max_length=200)
    comment_writer = models.ForeignKey(User, on_delete=models.CASCADE, null=True)
    
    def __str__(self):
        return self.comment_text # admin화면에서 comment_text가 우선으로 출력됨

    class Meta:
        ordering = ["comment_date"] # 날짜 순으로 정렬

post에는 사용하고 있는 Post 모델을 외래키로 받아 넣어준다.

related_name은 모델을 사용할때 지정해놓은 값으로 호출 가능케 해준다. 예를 들어 아래와 같은 Map모델이 있다.
여기서는 ‘maps’라는 related_name이 지정되어 있다.

class Map(db.Model):
    members = models.ManyToManyField(User, related_name='maps',
                                     verbose_name=_('members'))

만약 related_name을 지정하지 않으면 Django는 자동적으로 _set을 만들어 모델을 사용할 때 User.map_set.all()과 같이 변한다. 만약 related_name=maps을 선언 하면 User.map_set도 잘 작동하지만 User.maps로 사용할 수 있다. 좀 더 명확하게 사용할 수 있게 되는 것이다. 예를 들어 user object로 current_user를 가지고 있다면 current_user.maps.all()을 사용할 수 있을 것이다. 그러면 current_user와 관련된 Map모델의 모든 instance를 가져올 수 있게 된다.

원문: https://stackoverflow.com/questions/2642613/what-is-related-name-used-for-in-django

필자는 post.comments.all이런 형태로 사용했는데 뒤에 template에서 다시 확인할 수 있다.

comment_writer에는 User 모델을 그대로 외래키로 받아 사용했다.

admin.py

댓글을 관리하기 위해 추가해준다.

from django.contrib import admin
from . import models
admin.site.register(models.Comment)

forms.py

댓글을 위한 form을 만들어 준다.

class CommentForm(forms.ModelForm):
    class Meta:
        model = models.Comment
        fields = ('comment_text',) # 작성자는 자동으로 넣어줄것이므로 댓글 본문만 값으로 받는다.

views.py

함수형 뷰를 만든다.

from django.contrib.auth.decorators import login_required
...

@login_required
def comment_write(request, pk):
    post = get_object_or_404(Post, pk=pk)
    if request.method == 'POST':
        form = CommentForm(request.POST)
        if form.is_valid():
            comment = form.save(commit=False)
            comment.comment_writer = request.user
            comment.post = post
            comment.save()
            return redirect('posts:all')
    else:
        form = CommentForm()
    return render(request, 'posts/comment_form.html', {'form':form})

get_object_or_404는 try, except 구문을 간단히 구현하게 해주는 django의 기능이다. 첫 parameter는 모델이나 Manager 혹은 Queryset이 들어간다. 여기서는 Post 모델을 넣어준다. **kwargs에는 값을 가져오기 위해 get(), filter()가 받아들일 수 있는 값을 넣어준다. post에 맞춰 comment는 달라야 하니까 구별을 위해 pk를 넣어준다. (나름 이해한 방식)

공식문서: https://docs.djangoproject.com/en/2.2/topics/http/shortcuts/#get-object-or-404

form에는 만들어둔 CommentForm을 넣는다. 원래 참고했던 코드에는 comment.comment_writer = request.user 이게 없다. 그래서 작성자를 표시하고 싶어도 계속 None이라고만 출력이 됐고 혹은 내가 직접 선택해야했다. 하지만 위와 같이 현재 유저인 request.user를 본인이 만들어 놓은 모델에 알맞게 넣어주고 save()를 하면 저장이 돼서 작성자를 표시할 수 있게 된다.

urls.py

기능을 사용할 수 있게 url도 추가한다. path('<int:pk>/comment/', views.comment_write, name='comment_write'),

Template

django와 관련한 코드는 전부 구현했으니 이제 출력을 해보자

# post_detail.html
<h3 class="display-5">댓글( {{post.comments.count}} )</h3>
    {% if user.is_authenticated %} #user 로그인 상태일때만
    <form action="{% url 'posts:comment_write' pk=post.pk %}" method="POST">
        {% csrf_token %}
        <textarea type="text" name="comment_text" rows="3"></textarea>
        <input type="submit" class="btn btn-outline-primary" value="작성">
    </form>
{% endif %}

간단한 형식이다.

여기까지 하면 댓글 작성이 가능해진다.

댓글 출력

{% for comment in post.comments.all %}
    <div class="container">
        <div class="card shadow-sm p-3 mb-5 bg-white rounded">
            <div class="card-body">
                    작성자 {{comment.comment_writer}}
                    <p class="font-weight-light">{{comment.comment_date}}</p>
                    <p class="text-monospace">{{comment.comment_text}}</p>
                    {% if user.is_authenticated and comment.comment_writer == user %}
                    <a class="btn btn-primary btn-sm" href="{% url 'posts:comment_remove' pk=comment.pk %}"><i class="fas fa-trash-alt"></i></a>
                    {% endif %}
            </div>
        </div>
    </div>
{% empty %}
    <div class="bg-light" style="width:17rem";>글이 좋으셨다면 댓글을 남겨주세요 :)</div>
{% endfor %}

삭제버튼은 댓글을 작성한 본인만 삭제할 수 있게 했다. <i class="fas fa-trash-alt"></i> 이건 fontawesome의 아이콘 코드이므로 없애도 무관하다.

댓글 삭제

@login_required
def comment_remove(request, pk):
    comment = get_object_or_404(Comment, pk=pk)
    post_pk = comment.post.pk
    comment.delete()
    return redirect('posts:all')