댓글 수정 기능 구현하기

Posted on 2021-12-28 by GKSRUDTN99
Django로 웹사이트 만들기 Django Comment

댓글 수정 기능 구현하기

1. 테스트 코드 작성하기

  • 댓글을 수정하고 싶을 때 누를 'Edit' 버튼이 댓글 옆에 있어야 한다.
  • 자기가 남긴 댓글만 수정할 수 있어야 하므로 자기가 남긴 댓글 옆에만 이 버튼이 나타나야 한다.
    • 로그인을 하지 않은 상태에서는 버튼이 나타나지 않는다.
  • tests.py를 열어 댓글을 수정하는 상황을 테스트하기 위한 test_comment_update()함수를 작성한다.
# /blog/tests.py
# (... 생략 ...)
def test_comment_update(self):
    # 다른 사람이 작성한 댓글이 있어야 하므로 comment_by_trump를 만든다.
    comment_by_trump = Comment.objects.create(
        post=self.post_001,
        author=self.user_trump,
        content='트럼프의 댓글입니다.'
    )
    # 로그인하지 않은 상태에서 댓글이 2개 있는 self.post_001 페이지를 연다.
    response = self.client.get(self.post_001.get_absolute_url())
    self.assertEqual(response.status_code, 200)
    soup = BeautifulSoup(response.content, 'html.parser')

    # 댓글 영역에 수정 버튼이 둘 다 보이지 않아야 한다.
    # id는 comment-{pk}-update-btn으로 한다.
    comment_area = soup.find('div', id='comment-area')
    self.assertFalse(comment_area.find('a', id='comment-1-update-btn'))
    self.assertFalse(comment_area.find('a', id='comment-2-update-btn'))

    # obama로 로그인한 상태
    self.client.login(username='obama', password='somepassword')
    response = self.client.get(self.post_001.get_absolute_url())
    self.assertEqual(response.status_code, 200)
    soup = BeautifulSoup(response.content, 'html.parser')

    # obama로 로그인했으므로 trump가 작성한 댓글에 대한 수정 버튼은 보이지 않아야 한다.
    comment_area = soup.find('div', id='comment-area')
    self.assertFalse(comment_area.find('a', id='comment-2-update-btn')
    # 반면에 obama가 작성한 self.comment_001에 대한 수정 버튼은 나타나야 한다.)
    comment_001_update_btn = comment_area.find('a', id='comment-1-update-btn'))
    # 이 수정버튼에는 'edit'이라고 써 있어야 한다.
    self.assertIn('edit', comment_001_update_btn.text)
    # 이 버튼에는 링크 경로를 담은 href 속성이 있어야 하는데, 이 경로는 'blog/update_comment/{pk}/'로 한다.
    self.assertEqual(comment_001_update_btn.attrs['href'], '/blog/update_comment/1/')
  • 작성 후 테스트를 실행하면 comment_001에 대한 버튼이 없다고 한다.
  • 'edit'버튼을 추가하고, href="/blog/update_comment/{{ comment.pk }}"로 지정한다.
  • 단, 이 버튼은 댓글 작성자 본인에게만 보여야 하므로 if문을 사용해서 로그인한 방문자가 댓글의 작성자인 경우에 한해 버튼이 보이도록 작성한다.
{% if post.comment_set.exists %}
    {% for comment in post.comment_set.iterator %}
        <!-- Single comment-->
        <div class="d-flex" id="comment-{{ comment.pk }}">
            <div class="flex-shrink-0"><img class="rounded-circle"
                                            src="https://dummyimage.com/50x50/ced4da/6c757d.jpg"
                                            alt="..."/></div>
            <div class="ms-3 flex-fill">
                <div class="fw-bold">{{ comment.author.username }} &nbsp;&nbsp; 
                    <small class="text-muted">{{ comment.created_at }}</small>
                    {% if user.is_authenticated and comment.author == user %}
                        <a role="button" class="btn btn-sm btn-info float-end" id="comment-{{ comment.pk }}-update-btn" href="/blog/update_comment/{{ comment.pk }}/">
                            edit
                        </a>
                    {% endif %}
                </div>
                {{ comment.content | linebreaks }}
            </div>
        </div>
    {% endfor %}
{% endif %}
  • 다시 테스트를 수행하면 OK가 출력된다.

2. 테스트 코드 수정하기

  • 이제 edit 버튼을 클릭했을 때 실제로 수정이 가능하도록 한다.
    • 먼저, 이에 맞게 테스트 코드를 추가한다.
def test_comment_update(self):
    # (... 생략 ...)
    # edit 버튼을 클릭하면 댓글을 수정하는 폼이 있는 페이지로 이동한다.
    self.assertIn('edit', comment_001_update_btn.text)
    self.assertEqual(comment_001_update_btn.attrs['href'], '/blog/update_comment/1/')

    response = self.client.get('/blog/update_comment/1/')
    self.assertEqual(response.status_code, 200)
    soup = BeautifulSoup(response.content, 'html.parser')

    # 이 페이지의 타이틀은 'Edit Comment - Blog'이다.
    self.assertEqual('Edit Comment - Blog', soup.title.text)
    update_comment_form = soup.find('form', id='comment-form')
    # 폼 안에는 id가 'id_content'인 textarea가 있어야 하고, 그 안에는 수정하기 전의 comment 내용이 담겨있어야 한다.
    content_textarea = update_comment_form.find('textarea', id='id_content')
    self.assertIn(self.comment_001.content, content_textarea.text)

    # 이 폼의 content 내용을 수정하고 submit 버튼을 클릭하면 해당 댓글이 수정된다. 이 부분을 self.client.post로 구현한다.
    # POST가 정상적으로 수행된 뒤에 redirect를 따라갈 수 있도록 follow=True 옵션을 사용한다.
    response = self.client.post(
        f'/blog/update_comment/{self.comment_001.pk}/',
        {
            'content': "오바마의 댓글을 수정합니다.",
        },
        follow=True
    )

    self.assertEqual(response.status_code, 200)
    soup = BeautifulSoup(response.content, 'html.parser')
    comment_001_div = soup.find('div', id=f'comment-{self.comment_001.pk}')
    # 수정된 댓글은 수정된 내용으로 바뀌어 있어야하고, 수정되었을 때는 'Updated: '라는 문구가 나타나야한다.
    self.assertIn('오바마의 댓글을 수정합니다.', comment_001_div.text)
    self.assertIn('Updated: ', comment_001_div.text)
  • 아직 테스트코드만 작성한 상황이므로, 테스트를 수행해보면 /blog/update_comment/1/에 대한 요청의 status_code로 404가 반환된다.

3. 페이지 경로 추가하고 CommentUpdate 클래스 만들기

  • /blog/urls.py에 댓글 수정 페이지의 경로를 추가한다.
    • 이번에는 CBV로 뷰를 만들 예정이므로, 그에 맞게 작성한다.
# /blog/urls.py
from django.contrib import admin
from django.urls import path
from . import views

urlpatterns = [
    # 아래의 경로를 추가한다.
    path('update_comment/<int:pk>/', views.CommentUpdate.as_view()),
    path('update_post/<int:pk>/', views.PostUpdate.as_view()),
    path('create_post/', views.PostCreate.as_view()),
    path('tag/<str:slug>/', views.tag_page),
    path('category/<str:slug>/', views.category_page),
    path('<int:pk>/new_comment/', views.new_comment),
    path('<int:pk>/', views.PostDetail.as_view()),
    path('', views.PostList.as_view()),
]
  • 추가한 URL에 대응하는 CommentUpdate 클래스를 views.py에 만든다.
    • 로그인되어 있지 않은 상태로 CommentUpdate에 POST 방식으로 정보를 보내는 상황은 막기위해 LoginRequireMixin을 포함시킨다.
    • Comment 모델을 사용하겠다고 선언하고, form_class는 이전의 CommentForm을 활용한다.
    • dispatch() 메서드는 이 요청이 GET인지, POST인지 확인하는 동작을 하는 메서드이다.
      • 부모 클래스의 dispatch() 메서드를 실행하기 전에, 사용자의 로그인 여부와 작성자와 동일인인지 확인하는 작업을 진행한다.
# /blog/views.py
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.core.exceptions import PermissionDenied
from django.shortcuts import render, redirect, get_object_or_404
from django.utils.text import slugify
from django.views.generic import ListView, DetailView, CreateView, UpdateView

from .forms import CommentForm
# Comment 모델 임포트
from .models import Post, Category, Tag, Comment

# (... 생략 ...)

class CommentUpdate(LoginRequiredMixin, UpdateView):
    model = Comment
    form_class = CommentForm

    def dispatch(self, request, *args, **kwargs):
        if request.user .is_authenticated and request.user == self.get_object().author:
            return super(CommentUpdate, self).dispatch(request, *args, **kwargs)
        else:
            raise PermissionDenied
  • 이제 테스트를 수행해보면 이번에는 comment_form.html이 존재하지 않아 테스트가 실패한다.

4. 템플릿 만들기

  • 앞에서 만들었던 post_form.html을 참고하여 다음과 같이 comment_form.html을 만든다.
  • 브라우저의 타이틀은 'Edit Comment - Blog'로 하고, Crispy를 적용한다.
{% extends 'blog/base_full_width.html' %}
{% load crispy_forms_tags %}
{% block head_title %}Edit Comment - Blog{% endblock %}

{% block main_area %}
    <h1>Edit Comment</h1>
    <hr/>
    <form method="post" id="comment-form">
        {% csrf_token %}
        {{ form | crispy }}
        <br/>
        <button type="submit" class="btn btn-primary float-end">Submit</button>
    </form>
{% endblock %}
  • 이제 테스트를 수행해보면, 리다이렉트 된 페이지에서 'Updated: '라는 문구를 찾을 수 없다고 한다.
  • 해결을 위해 post_detail.html을 수정한다.
    • 처음 댓글이 생성되면 created_at과 updated_at이 동일하지만 한 번 수정하면 updated_at만 수정되므로 둘의 값이 달라지게 된다.
    • 이를 이용해 댓글이 수정된 경우에만 Updated: 뒤에 update_at을 출력하는 기능을 추가한다.
    • 수정 시각은 오른쪽 아래에 약간 흐릿하게 나오도록 <p>태그의 클래스에 'text_muted'와 'float-end'를 추가하고, 댓글 내용보다 작게 표시되도록 <small>태그도 추가한다.
    • 수정하지 않은 댓글이라도 미세한 차이가 있는 경우가 있어 "Y-m-d H:i"로의 형태로 변환한 뒤에 비교해야 한다.
<!-- /blog/templates/post_detail.html-->

<!-- ... 생략 ... -->

<!-- Single comment-->
<div class="d-flex" id="comment-{{ comment.pk }}">
    <div class="flex-shrink-0"><img class="rounded-circle"
                                    src="https://dummyimage.com/50x50/ced4da/6c757d.jpg"
                                    alt="..."/></div>
    <div class="ms-3 flex-fill">
        <div class="fw-bold">{{ comment.author.username }} &nbsp;&nbsp; <small
                class="text-muted">{{ comment.created_at }}</small>
            {% if user.is_authenticated and comment.author == user %}
                <a role="button" class="btn btn-sm btn-info float-end" id="comment-{{ comment.pk }}-update-btn" href="/blog/update_comment/{{ comment.pk }}/">
                edit
                </a>
            {% endif %}
        </div>
        <p>{{ comment.content | linebreaks }}</p>
        {% if comment.created_at|date:'Y-m-d H:i' != comment.updated_at|date:'Y-m-d H:i' %}
            <p class="text-muted float-end">
                <small>Updated: {{ comment.updated_at }}</small>
            </p>
        {% endif %}
    </div>
</div>