댓글 삭제 기능 추가하기

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

1. 댓글 삭제 기능을 위한 테스트 코드 작성하기

delete 버튼 모양 구상하기

  • 댓글의 delete 버튼도 자신이 작성한 댓글에만 나타나야 한다.
  • delete 버튼을 클릭했을 때는, 바로 삭제되는 것 보다 정말로 삭제하고 싶은지 물어보는 과정이 필요하다.
  • 위 상황에 맞게 테스트코드를 작성한다.
def test_delete_comment(self):
    # Trump로 로그인하여 댓글을 작성한다.
    comment_by_trump = Comment.objects.create(
        post=self.post_001,
        author=self.user_trump,
        content='트럼프의 댓글입니다.'
    )

    # setUp()에서 obama 계정으로 댓글을 하나 만들었으니, 총 댓글은 2개이며 post_001의 댓글 수도 2개이다.
    self.assertEqual(Comment.objects.count(), 2)
    self.assertEqual(self.post_001.comment_set.count(), 2)

    # 로그인하지 않은 상태
    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-delete-btn'으로 한다.
    comment_area = soup.find('div', id='comment-area')
    self.assertFalse(comment_area.find('a', id='comment-1-delete-btn'))
    self.assertFalse(comment_area.find('a', id='comment-2-delete-btn'))

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

    # trump로 로그인한 상태이므로 obama가 작성한 댓글에 대한 삭제 버튼은 보이지 않아야한다.
    comment_area = soup.find('div', id='comment-area')
    self.assertFalse(comment_area.find('a', id='comment-1-delete-btn'))
    # 반면 pk=2에 대한 삭제 버튼은 있어야 한다.
    comment_002_delete_modal_btn = comment_area.find(
        'a', id='comment-2-delete-modal-btn'
    )
    self.assertIn('delete', comment_002_delete_modal_btn)
    self.assertEqual(
        comment_002_delete_modal_btn.attrs['data-target'],
        '#deleteCommentModal-2'
    )

    # 삭제를 할지 물어보는 모달에는 'Are You Sure?'이라는 문구와 함께 delete 버튼이 있어야 한다.
    # 이 버튼의 링크는 '/blog/delete_comment/삭제할 comment의 pk'로 되어 있어야 한다.
    delete_comment_modal_002 = soup.find('div', id='deleteCommentModal-2')
    self.assertIn('Are You Sure?', delete_comment_modal_002.text)
    really_delete_btn_002 = delete_comment_modal_002.find('a')
    self.assertIn('Delete', really_delete_btn_002.text)
    self.assertEqual(
        really_delete_btn_002.attrs['href'],
        '/blog/delete_comment/2/'
    )

    # delete 버튼을 클릭하면 댓글이 삭제되고 post_001의 페이지로 리다이렉트된다.
    # 해당 페이지에는 더이상 트럼프의 댓글이 존재하지 않고, 댓글의 개수도 1로 줄어든다.
    response = self.client.get('/blog/delete_comment/2/', follow=True)
    self.assertEqual(response.status_code, 200)
    soup = BeautifulSoup(response.content, 'html.parser')
    self.assertIn(self.post_001.title, soup.title.text)
    comment_area = soup.find('div', id='comment-area')
    self.assertNotIn('트럼프의 댓글입니다.', comment_area.text)

    self.assertEqual(Comment.objects.count(), 1)
    self.assertEqual(self.post_001.comment_set.count(), 1)
  • 현재 상태로 테스트를 진행해보면 'comment를 삭제할지 물어보는 모달을 나타내기 위한 버튼이 없다'는 오류가 발생한다.

2. 댓글 삭제 버튼과 모달 만들기

post_detail.html 수정하기

  • 앞의 오류를 해결하기 위해 post_detail.html을 다음과 같이 수정한다.
<!-- /blog/templates/post_detail.html -->

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

{% if user.is_authenticated and comment.author == user %}
    <div class="float-end">
        <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>
        <a role="button" class="btn btn-sm btn-danger" id="comment-{{ comment.pk }}-delete-modal-btn" href="#" data-toggle="modal" data-target="#deleteCommentModal-{{ comment.pk }}">
            delete
        </a>
    </div>
    <!-- Modal -->
    <div class="modal fade" id="deleteCommentModal-{{ comment.pk }}" tabindex="-1" role="dialog" aria-labelledby="deleteCommentModalLabel" aria-hidden="true">
        <div class="modal-dialog" role="document">
            <div class="modal-content">
                <div class="modal-header">
                    <h5 class="modal-title" id="deleteModalLabel">Are You Sure?</h5>
                    <button type="button" class="btn-close" data-dismiss="modal" aria-label="Close">
                        <span aria-hidden="true">&times;</span>
                    </button>
                </div>
                <div class="modal-body">
                    <del>{{ comment | linebreaks }}</del>
                </div>
                <div class="modal-footer">
                    <button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
                    <a role="button" class="btn btn-danger" href="/blog/delete_comment/{{ comment.pk }}/">Delete</a>
                </div>
            </div>
        </div>
    </div>
{% endif %}
  • 모달을 만드는데 사용된 속성들

    • data-toggle="modal": modal 계층에 data-target을 통해 지정된 부분을 보이게 만들 것이다.
    • data-target="#deleteCommentModal": #(id Selector)+deleteCommentModal, 즉 id가 deleteCommentModal인 태그를 지정한다.
    • data-dismiss="modal": modal 계층에 보이고 있는 부분들을 없앤다.
    • tabindex="-1": 탭을 눌러서 태그들을 순회할 때 포함되지 않도록 한다.
    • aria-: 장애가 있는 사용자들이 사용하는 스크린 리더등을 사용할 때 접근성을 높이는 속성들이다.
    • <del>: 취소선을 추가하는 태그이다.
  • 다시 테스트를 수행해보면 '페이지를 찾을 수 없음'을 의미하는 404오류가 발생한다.

    • 아직 /blog/delete_comment/{{ comment.pk }}에 해당하는 url처리를 추가하지 않았기 때문이다.

urls.py와 views.py 수정하기

  • urls.py에 URL을 추가하고, views.py에 FBV 스타일로 뷰를 추가한다.
#/blog/urls.py
from django.contrib import admin
from django.urls import path
from . import views

urlpatterns = [
    path('delete_comment/<int:pk>/', views.delete_comment),
    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()),
]
# /blog/views.py

# (...생략...)

def delete_comment(request, pk):
    comment = get_object_or_404(Comment, pk=pk)
    post = comment.post
    if request.user.is_authenticated and request.user == comment.author:
        comment.delete()
        return redirect(post.get_absolute_url())
    else:
        raise PermissionDenied
  • 이제 테스트를 실행해보면 OK가 나온다.
  • 사실, 장고에서 기본적으로 제공하는 DeleteView가 있긴 하지만, 해당 View를 사용하면 '정말 사용자가 삭제할 것인지' 확인하는 페이지로 이동한 뒤 삭제가 이루어지는 방식이다.
    • 사용자 입장에서는 모달을 통한 삭제가 더 편할 것이라 생각하여 이렇게 구현한 것이다.