폼으로 댓글 기능 구현하기[3]
Posted on 2021-12-23 by GKSRUDTN99
Django로 웹사이트 만들기
Django
Comment
폼으로 댓글 기능 구현하기[3]
댓글 작성 폼 구현하기
1. 댓글 작성 폼을 위한 테스트 코드 작성하기
- 로그인 하지 않은 경우에는 댓글을 남기지 못하게 할 것이다.
- 로그인하지 않은 경우, 댓글을 입력하는 폼이 보이지 않고, Login 버튼이 보이도록 한다.
- 위 내용을 만족하는 테스트 코드를 작성한다.
# /blog/tests.py
# (...생략...)
def test_comment_form(self):
# setUp() 함수에서 생성한 Comment가 1개인 것을 확인한다.
self.assertEqual(Comment.objects.count(), 1)
# 이 댓글은 self.post_001에 달린 댓글이므로 post_001의 댓글 개수도 1개인 것을 확인한다.
self.assertEqual(self.post_001.comment_set.count(), 1)
# 로그인하지 않은 상태 테스트
response = self.client.get(self.post_001.get_absolute_url())
self.assertEqual(response.status_code, 200)
soup = BeautifulSoup(response.content, 'html.parser')
# id가 'comment-area'인 div 요소를 찾아 comment-area에 저장한다.
comment_area = soup.find('div', id='comment-area')
# 로그인하지 않은 상태이므로 'Log in and leave a comment'라는 문구가 보여야 한다.
self.assertIn('Log in and leave a comment', comment_area.text)
# 로그인하지 않은 상태이므로 id가 comment-form인 요소는 존재하지 않아야 한다.
self.assertFalse(comment_area.find('form', id='comment-form'))
- 로그인이 되어 있는 경우에는 댓글 폼이 보여야 한다.
- 댓글 폼은 텍스트만 받을 수 있는 input만 있으면 된다.
- 작성자와 작성시간은 로그인한 사용자 정보를 이용해 자동으로 채울 것이다.
- 댓글 작성자가 댓글을 남기고 submit버튼을 클릭하면 댓글이 생성되고, 그 위치로 브라우저가 이동한다.
- 위 내용을 만족하는 테스트 코드를 작성한다.
# /blog/test.py
# (...생략...)
def test_comment_form(self):
# (...생략...)
# 로그인한 상태 테스트
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')
comment_area = soup.find('div', id='comment-area')
# 로그인한 상태이므로 로그인을 요구하는 문구는 보이지 않는다.
self.assertNotIn('Log in and leave a comment', comment_area.text)
# 로그인한 상태이므로 댓글 폼이 보이고, 그 안에 textare도 있다.
comment_form = soup.find('form', id='comment-form')
self.assertTrue(comment_form.find('textarea',id='id_content'))
# POST 방식으로 댓글 내용을 서버에 보내고, 결과를 response에 담는다.
# follow=True 옵션은 POST로 요청을 보내는 경우 서버에서 처리한 후 리다이렉트되는데, 이에 따라가도록 설정하는 역할을 한다.
response = self.client.post(
self.post_001.get_absolute_url() + 'new_comment/',
{
'content': "오바마의 댓글입니다.",
},
follow=True
)
self.assertEqual(response.status_code, 200)
# 댓글이 하나 추가되었으므로 전체 댓글 개수는 2개임을 확인한다.
self.assertEqual(Comment.objects.count(), 2)
# 추가된 댓글이 self.post_001의 것이므로 해당 post의 댓글 개수도 2개임을 확인한다.
self.assertEqual(self.post_001.comment_set.count(), 2)
# 마지막으로 생성된 comment를 가져온다.
new_comment = Comment.objects.last()
# POST요청을 보낼 때 follow=True로 설정했기 때문에 서버에서 처리된 뒤 comment가 달린 포스트의 상세 페이지로 리다이렉트된다.
# 그래서 웹 브라우저의 타이틀로 새로 만든 comment가 달린 포스트의 타이틀이 나타난다.
soup = BeautifulSoup(response.content, 'html.parser')
self.assertIn(new_comment.post.title, soup.title.text)
# 새로 만든 댓글의 내용과 작성자가 표시되는 것을 확인한다.
comment_area = soup.find('div', id='comment-area')
new_comment_div = comment_area.find('div', id=f'comment-{new_comment.pk}')
self.assertIn('obama', new_comment_div.text)
self.assertIn('오바마의 댓글입니다.', new_comment_div.text)
2. 로그인 상태에 따라 댓글 입력란 또는 로그인 버튼 나타내기
- post_detail.html 파일에서 댓글 폼에 해당하는 곳을 수정한다.
- if 문을 사용해서 로그인했을 때만 폼이 보이도록 수정한다.
- 로그인하지 않은 경우에는 로그인 모달을 나타낼 수 있는 버튼을 만든다.
<!-- /blog/templates/blog/post_detail.html-->
<!-- ...생략... -->
<!-- Comments section-->
<div class="mb-5" id="comment-area">
<div class="card bg-light">
<div class="card-body">
<!-- Comment form-->
{% if user.is_authenticated %}
<form class="mb-4" id="comment-form" method="POST" action="{{ post.get_absolute_url }}new_comment/">
<textarea class="form-control" rows="3" placeholder="Join the discussion and leave a comment!"></textarea>
</form>
{% else %}
<a role="button" class="btn btn-outline-dark btn btn-sm col-12" href="#" data-toggle="modal" data-target="#loginModal">
Log in and leave a comment
</a>
{% endif %}
<!-- ...생략... -->
- 이렇게 구현한 뒤 로그아웃한 상태에서 포스트 상세 페이지를 열어보면 댓글 영역이 버튼으로 대체된다.
- 터미널에서
python manage.py test
로 테스트를 수행해보면 id가 'id_content'인 textarea가 없다고 한다.
3. CommentForm 구현하기
- forms.py 만들고 필드 추가하기
- blog 앱 폴더 아래 forms.py 파일을 만들고 다음과 같이 작성한다.
- models.py에서 Comment 모델을 불러오고, django의 forms도 임포트한다.
- 폼 이름은 CommentForm이고, Comment모델을 사용한다.
- Comment 모델에는 여러 개의 필드가 있지만 여기서는 content 필드만 입력받을 것이므로
fields = ('content', )
와 같이 작성한다.
#/blog/forms.py
from .models import Comment
from django import forms
class CommentForm(forms.ModelForm):
class Meta:
model = Comment
fields = ('content', )
# fields(포함시키기) 대신 exclude(제외시키기)를 사용할 수도 있다
- views.py 수정하고 CommentForm 적용하기
- 이렇게 만든 폼을 PostDetail 클래스에 넘겨주기만 하면 된다.]
# /blog/views.py
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.core.exceptions import PermissionDenied
from django.shortcuts import render, redirect
from django.utils.text import slugify
from django.views.generic import ListView, DetailView, CreateView, UpdateView
# CommentForm을 임포트한다.
from .forms import CommentForm
from .models import Post, Category, Tag
# (... 생략 ...)
# PostDetail 클래스의 get_context_data() 함수에서 CommentForm을 comment_form이라는 이름으로 넘긴다.
class PostDetail(DetailView):
model = Post
def get_context_data(self, **kwargs):
context = super(PostDetail, self).get_context_data()
context['categories'] = Category.objects.all()
context['no_category_post_count'] = Post.objects.filter(category=None).count()
context['comment_form'] = CommentForm
return context
# (... 생략 ...)
- comment-form을 사용할 수 있도록 post_detail.html을 수정한다.
- 폼이 예쁘게 보이도록 post_form에서 적용했듯이 crispy를 적용한다.
<!-- /blog/templates/post_detail.html-->
{% extends 'blog/base.html' %}
{% load crispy_forms_tags %}
<!-- (...생략...) -->
<!-- Comment form-->
{% if user.is_authenticated %}
<form class="mb-4" id="comment-form" method="POST" action="{{ post.get_absolute_url }}new_comment/">
{{ comment_form | crispy }}
<button type="submit" class="btn btn-primary">Submit</button>
</form>
- 이제 웹 브라우저에서 로그인하면 댓글을 작성할 수 있는 창이 나타난다.
- 테스트를 수행해보면 textarea를 찾을 수 없다는 오류는 발생하지 않지만, POST의 Response Status Code가 200이 아닌 404가 되어 실패한다.
3. urls.py 수정해 new_comment 경로 추가하기
- 현재는 POST로 보낸 comment_form의 내용을 받아들일 준비가 되어있지 않아 새로운 Comment를 만들지 못하고 있다.
- blog의 urls.py에 new_comment 경로를 추가한다.
- url에 있는 pk로 포스트를 찾는다.
- 이번에는 FBV 스타일로 만들 것이므로 views 뒤에 소문자로 new_comment라고 작성한다.
# /blog/urls.py
from django.contrib import admin
from django.urls import path
from . import views
urlpatterns = [
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()),
]
- new_comment 함수를 views.py에 구현한다.
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.core.exceptions import PermissionDenied
# get_object_or_404를 임포트한다.
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
from .models import Post, Category, Tag
# (... 생략 ...)
def new_comment(request, pk):
# 로그인하지 않은 상태로 댓글을 작성하려 하면 PermissionDenied를 발생시킨다.
if request.user.is_authenticated:
# url에서 인자로 받은 pk로 댓글을 추가할 post를 가져오는데, 실패할 시 404에러를 발생시키도록 get_object_or_404를 사용한다.
post = get_object_or_404(Post, pk=pk)
# POST방식이 아닌 다른 method로 접근했을 경우 그냥 해당 포스트의 상세 페이지로 리다이렉트 되도록 한다.
if request.method == 'POST':
# POST방식으로 들어온 정보를 CommentForm의 형태로 가져온다.
comment_form = CommentForm(request.POST)
# 폼이 유효하게 작성되었다면 해당 내용으로 새로운 레코드를 만들어 DB에 저장한다.
# 이때 commit=False 옵션으로 바로 저장하는 기능을 잠시 미루고 comment_form에 담긴 정보로 Comment 인스턴스만 가져온다.
# 이때 CommentForm은 content 필드의 내용만 담고 있으므로 필요한 나머지 정보들을 모두 채운뒤 save()를 호출하여 저장한다.
if comment_form.is_valid():
comment = comment_form.save(commit=False)
comment.post = post
comment.author = request.user
comment.save()
# 처리가 끝나면 comment의 URL로 리다이렉트한다.
return redirect(comment.get_absolute_url())
else:
return redirect(post.get_absolute_url())
else:
raise PermissionDenied
- 이제 테스트를 수행해보면 OK가 나오지만, 웹 브라우저에서 직접 댓글을 작성하려 하면 403 오류가 발생한다.
- form 안에 CSRF Token이 없어 발생하는 오류로, post_detail의 댓글작성 폼 안에
{% csrf_token %}
이 있는지 확인한다. - 장고에서는 form을 사용할 때 보안을 위해 form 안에 무작위의 토큰을 input 값으로 부여하고, 나중에 서버에 POST로 들어온 값을 확인할 때 그 토큰 값이 맞는지 확인하는 과정을 거친다.
- 따라서 특별한 경우가 아니라면 form에 항상 CSRF 토큰을 추가해주자.
- form 안에 CSRF Token이 없어 발생하는 오류로, post_detail의 댓글작성 폼 안에
<!-- /blog/templates/post_detail.html-->
<!-- Comment form-->
{% if user.is_authenticated %}
<form class="mb-4" id="comment-form" method="POST" action="{{ post.get_absolute_url }}new_comment/">
{% csrf_token %}
{{ comment_form | crispy }}
- 이제 웹 브라우저에서도 정상적으로 댓글을 작성할 수 있다.