댓글 수정 기능 구현하기
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 }}
<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 }} <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>