폼을 통해 댓글 기능을 구현해보며 폼을 만들고 처리하는 방법에 대해 더 알아보는 시간을 가져보자.
먼저 댓글 기능을 추가하기 위해서 Comment
모델을 만들자.
댓글에 필요한 요소는 어떤 게 있을까?
어떤 포스트에 대한 댓글인지를 저장하는 post
필드
작성자를 저장할 author
필드
댓글 내용을 담을 content
필드
작성일시와 수정일시를 담을 created_at
, modified_at
필드
이렇게 총 5개의 필드를 포함한 Comment
클래스를 models.py
에 다음과 같이 추가한다.
(생략)
class Comment(models.Model):
post = models.ForeignKey(Post, on_delete=models.CASCADE)
author = models.ForeignKey(User, on_delete=models.CASCADE)
content = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
modified_at = models.DateTimeField(auto_now=True)
def __str__(self):
return f'{self.author}::{self.content}'
우선 post
필드와 author
필드는 댓글과 다대일(Many-To-One) 관계다. 포스트 하나에 여러개의 댓글이 달릴 것 이고, 한명의 작성자가 여러개의 댓글을 달 수 있을것이다. 이처럼 포스트와 작성자는 One이고, 댓글은 Many 쪽 이다. 이러한 관계에서는 ForeignKey
를 사용할 수 있고, ForeignKey
는 Many쪽에 써줘야 하기 때문에 Comment
클래스 안에 사용하여 post
와 author
필드를 정의해준다.
댓글 내용을 담을 content
필드는 TextField
를 사용했고, created_at
과 modified_at
필드는 DateTimeField
로 만들었다. 이때 처음 생성될 때 시간을 저장하도록 created_at
필드에는 auto_now_add=True
로 설정하고, modified_at
은 저장될 때의 시간을 저장하도록 auto_now=True
로 설정했다. 마지막으로 관리자 페이지에 작성자명과 content
내용을 출력하는 __str__()
함수를 정의했다.
이후 마이그레이션을 진행한다.
$ python manage.py makemigrations
$ python manage.py migrate
blog/admins.py
안에 admin.site.register(Comment)
를 작성하여 위에서 만든 Comment
클래스를 등록해주자.
그리고 관리자 페이지에 들어가 보면 다음과 같이 BLOG 메뉴에 Comments가 추가되어 있고, Add 버튼을 눌러 포스트와 작성자를 고르고 댓글을 작성할 수 있다.
부트스트랩으로 가져온 디자인의 댓글 구조를 보면 다음과같다.
<div class="comment-area">
안에 댓글과 관련된 부분이 구현되어 있고, 댓글을 작성하기 위한 comment form
과 <div>
태그로 구분된 댓글들이 있다. 댓글에 id
는 부여되어있지 않지만, 댓글을 구분하기 위해 comment-{{ 댓글의 pk }}
로 id
를 부여할 생각이다.
post_detail.html
을 다음과 같이 수정하자.
(..생략..)
<!-- Comments -->
<div id="comment-area">
<!-- Comments Form -->
<div class="card my-4">
<h5 class="card-header">Leave a Comment:</h5>
<div class="card-body">
<form>
<div class="form-group">
<textarea class="form-control" rows=3"></textarea>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</div>
</div>
<!-- 해당 포스트에 댓글이 있다면 -->
{% if post.comment_set.exists %}
{% for comment in post.comment_set.iterator %}
<!-- Single Comment -->
<div class="media mb-4" id="comment-{{ comment.pk }}">
<img class="d-flex mr-3 rounded-circle" src="http://placehold.it/50x50" alt="">
<div class="media-body">
<!--"작성자 이름 날짜" 및 내용 출력-->
<h5 class="mt-0">{{ comment.author.username }} <small class="text-muted">{{ comment.created_at }}</small>
</h5>
<p>{{ comment.content | linebreaks }}</p>
</div>
</div>
{% endfor %}
{% endif %}
</div>
<hr/>
Comments Form은 차후에 변경할 것이다.
우선 if
문으로 comment
가 있는지 먼저 확인한다. comment
가 있다면 모든 comment
를 불러와 for
문으로 반복해서 HTML 코드를 만든다. 이때 각 <div>
태그의 id
는 comment-{{ comment의 pk }}
가 붙는 형태로 만든다.
또한 원래 Comment Name
이라고 되어 있던 부분은 {{ comment.author.username }}
으로 바꾸어 댓글을 단 사람의 사용자명이 나오도록 하고, 그 뒤에는 바로 작성일이 나오도록 했다.
comment
의 내용은 <p>
태그 안에서 나타나도록 했다.
이제 관리자 페이지에서 댓글을 작성해보자. 그런데, 작성한 댓글을 빠르게 확인할 수 있도록 해당 댓글 위치로 바로 이동하는 VIEW ON SITE 버튼을 만들고싶다.
이 문제는 Comment
모델에 get_absolute_url()
함수를 정의하여 해결할 수 있다.
# blog/models.py
class Comment(models.Model):
post = models.ForeignKey(Post, on_delete=models.CASCADE)
author = models.ForeignKey(User, on_delete=models.CASCADE)
content = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
modified_at = models.DateTimeField(auto_now=True)
def __str__(self):
return f'{self.author}::{self.content}'
def get_absolute_url(self):
return f'{self.post.get_absolute_url()}#comment-{self.pk}'
이때 #comment-{self.pk}
를 추가한 이유는 #
이 HTML 요소의 id
를 의미하기 때문이다.
이렇게하면 웹 브라우저가 해당 포스터를 열고, comment-{self.pk}
에 해당하는 위치로 이동하게 된다.
또한 이렇게 get_absolute_url()
함수를 정의함으로써, 관리자페이지에서 댓글을 작성하고 나면, VIEW ON SITE 버튼도 나타나게 된다.
두 개의 댓글을 관리자페이지에서 작성해봤다. 다음과 같으 포스트 상세 페이지에서도 잘 나타나는 것을 볼 수 있다.
이제 댓글을 포스트 상세 페이지에서 직접 작성할 수 있게, 댓글 작성 폼을 구현해보자!!
새로운 파일인 blog/forms.py
를 만들고, 사용자가 댓글을 제출할 수 있는 form을 직접 구현하자.
from .models import Comment
from django import forms
class CommentForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(CommentForm, self).__init__(*args, **kwargs)
self.fields['content'].label = False # "content *" 표시 없애기.
class Meta:
model = Comment
fields = ('content',)
Comment 모델에는 여러 가지 필드가 있지만, "댓글 작성 폼"에 필요한 것은 content 뿐이므로 content필드만 입력.
앞서 만든 폼을 PostDetail
클래스에 넘겨주자. CommentForm
을 임포트하고, PostDetail
클래스의 get_context_data()
함수에서 CommentForm
을 comment_form
이라는 이름으로 넘긴다. 이렇게 해야 우리가 만든 CommentForm
이 PostDetail
에 담겨서 post_detail.html
에 심어져 방문자에게 폼을 제공할 수 있다.
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
post_detail.html
을 수정하여, 로그인 한 상태라면 댓글 입력 폼이 나타나게 하고, 로그인 하지 않은 상태라면 로그인 모달을 나타나게 하는 버튼을 추가 해보자.
로그인하지 않았을 때 나타나는 버튼의 문구는 "Log in and leave a comment"로 했다.
로그인 모달은 내비게이션 바에서 이미 정의했었다. data-toggle
과 data-target
을 내비게이션 바의 로그인 버튼과 동일하게 설정하여, 사용자가 이 버튼을 클릭하면 로그인 모달이 나타나도록 하였다.
로그인했을 때 보이는 부분의 <form>
태그에는 id="comment-form" method="POST" action="{{ post.get_absolute_url }}new_comment/
를 추가했다. 즉, 방문자가 폼에 내용을 입력하면, 그 내용을 POST 방식으로 action에 적힌 URL(blog/{post의 pk}/new_comment
)에 보내겠다는 뜻이다.
또한 앞서 정의한 comment_form
을 사용하고, 폼이 예쁘게 보이도록 crispy
를 적용할 수 있게 맨 위에 {% load crispy_forms_tags %}
를 입력하고, <div class="form-group">
안에 {{comment_form | crispy}}
를 넣어줬다.
<!-- post_detail.html -->
{% extends 'blog/base.html' %}
{% load crispy_forms_tags %}
(..생략..)
<!-- Comments -->
<div id="comment-area">
<!-- Comments Form -->
<div class="card my-4">
<h5 class="card-header">Leave a Comment:</h5>
<div class="card-body">
{% if user.is_authenticated %}
<form id="comment-form" method="POST" action="{{ post.get_absolute_url }}new_comment/">{% csrf_token %}
<div class="form-group">
{{ comment_form | crispy }}
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
{% else %}
<a role="button" class="btn btn-outline-dark btn-block btn-sm" href="#" data-toggle="modal" data-target="#loginModal">Log in and leave a comment</a>
{% endif %}
</div>
(..생략..)
템플릿에서 <form>
태그를 통해 작성된 내용이 POST 방식을 통해서 blog/{post의 pk}/new_comment
로 넘어오게끔 했다. 즉, URL에 있는 pk
로 포스트를 찾고, 그 포스트에 댓글을 달기 위해 다음과 같이 설정하자.
blog.urls.py
의 urlpatterns
에 다음과 같이 추가하자.
path('<int:pk>/new_comment/', views.new_comment),
urls.py
에서 blog/{post의 pk}/new_comment
에 접근하면 views.new_comment
에서 처리하게끔 작성했으므로, views.py
에 FBV 방식으로 new_comment
를 다음과 같이 추가한다.
# blog/views.py
(..생략..)
def new_comment(request, pk):
if request.user.is_authenticated:
post = get_object_or_404(Post, pk=pk)
if request.method == "POST":
comment_form = CommentForm(request.POST)
if comment_form.is_valid():
comment = comment_form.save(commit=False)
comment.post = post
comment.author = request.user
comment.save()
return redirect(comment.get_absolute_url())
else:
return redirect(post.get_absolute_url())
else:
raise PermissionError
if request.user.is_authenticated else: raise PermissionError
-> 로그인하지 않은 상태에서는 댓글 폼이 포스트 상세 페이지에 보이지 않게 템플릿에서 설정했지만, 비정상적인방법으로 new_comment
에 접근하려는 시도가 있을 수 있다. 이러한 경우에 대비해 로그인 하지 않은 경우에는 PermissionDenied
를 발생시킨다.
post = get_object_or_404(Post, pk=pk)
-> 인자로 받은 pk
로 댓글을 달 포스트를 쿼리를 날려 가져온다. 이때 pk
에 해당하는 포스트가 없는 경우에는 404 에러를 자동으로 발생.
if request.method == "POST" else: return redirect(post.get_absolute_url())
-> 폼을 작서한 후 submit 버튼을 클릭하면 POST 방식으로 전달된다. 그런데 어떤 사람이 브라우저에서 127.0.0.1:8000/5/new_comment/
로 입력하는 경우도 생각할 수 있다. 이 경우는 POST 방식이 아니라 GET 방식으로 서버에 요청하게 되므로, 그냥 pk=5
인 포스트의 페이지로 리다이렉트되도록 했다.
comment_form = CommentForm(request.POST)
-> 정상적으로 폼을 작성하고 POST 방식으로 서버에 요청이 들어왔다면 POST 방식으로 들어온 정보를 CommentForm
의 형태로 가져온다.
if comment_form.is_valid(): 이후 ~~
-> 이 폼이 유효하게 작성되었다면, 해당 내용으로 새로운 레코드를 만들어 DB에 저장한다. 이때 comment = comment_form.save(commit=False)
로 바로 저장하는 기능을 잠시 미루고 comment_form
에 담긴 정보로 Comment
인스턴스만 가져온다. CommentForm
은 content
필드의 내용만 담고 있으므로 post
필드는 pk
로 가져온 포스트로 채우고, author
필드는 로그인한 사용자 정보로 채운다.
return redirect(comment.get_absolute_url())
-> 마지막으로 comment
의 URL로 리다이렉트를 한다. 해당 포스트의 상세 페이지에서 이 댓글이 작성되어 있는 위치로 브라우저가 이동하게 된다.
이제 포스트 상세 페이지에서 댓글을 작성할 수 있을 것이다.
다음 그림 처럼 자기가 남긴 댓글 옆에만 edit 버튼이 나오도록 하고, 해당 버튼을 누르면 댓글 수정 페이지로 넘어가게끔 해보자.
post_detail.html
파일을 수정하자.
댓글이 들어가는 위치 위에 다음과 같이 edit 버튼을 추가하고 float-right
를 추가하여 오른쪽으로 정렬되게끔 한다. 그리고 id="comment-{{comment.pk}}update-btn
으로 지정하여 "해당 버튼이 몇 번째 댓글의 edit 버튼인지" 표시했다.
또한 href="/blog/update_comment/{{commen.pk}}"
로 지정하여, 해당 버튼을 누르면 댓글 수정 페이지의 URL로 가게끔 하자. 해당 URL은 이후에 구현 및 매핑할것이다.
단, 이 버튼은 댓글 작성자 본인에겜나 보여야 하므로 if
문을 사용해 '로그인한 방문자가 댓글의 작성자인 경우'에 한해서 edit 버튼이 보이게끔 한다.
<!-- post_detail.html -->
(..생략..)
{% if post.comment_set.exists %}
{% for comment in post.comment_set.iterator %}
<!-- Single Comment -->
<div class="media mb-4" id="comment-{{ comment.pk }}">
<img class="d-flex mr-3 rounded-circle" src="http://placehold.it/50x50" alt="">
<div class="media-body">
<!--댓글 edit 버튼이 로그인 되어 있고, 작성자 본인 일때만 보이게-->
{% if user.is_authenticated and comment.author == user %}
<div class="float-right">
<a role="button"
class="btn btn-sm btn-info"
id="comment-{{ comment.pk }}-update-btn"
href="/blog/update_comment/{{ comment.pk }}/">
edit
</a>
{% endif %}
<!--"작성자 이름 날짜" 및 내용 출력-->
<h5 class="mt-0">{{ comment.author.username }} <small class="text-muted">{{ comment.created_at }}</small></h5>
<p>{{ comment.content | linebreaks }}</p>
(..생략..)
앞서 작성자 본인이 작성한 댓글의 오른쪽에 edit 버튼을 만들었다. 하지만 해당 버튼을 누르면은 에러가 발생한다.
왜냐하면? 아직 해당 경로를 urls.py에 지정하지 않았고, 어떠한 view도 만들지 않았고, 댓글 수정 페이지도 만들지 않았기 때문이다.
blog/urls.py
에 댓글 수정 페이지의 경로를 추가하자. 포스트 수정 페이지를 만들 때 PostUpdate
를 만든 것 처럼 CBV 스타일로 View를 만들것이다.
path('update_comment/<int:pk>/', views.CommentUpdate.as_view())
추가한 URL에 대응하는 CommentUpdate
클래스를 views.py
에 만들어보자.
먼저 로그인되어 있지 않은 상태로 CommentUpdate
에 POST 방식으로 정보를 보내는 상황을 막기 위해 LoginRequiredMixin
을 포함시킨다.
Comment
모델을 사용하겠다고 선언하고, form_class
는 앞서 만들어두었던 CommentForm
을 불러와서 활용했다.
그리고 조금 더 보안을 신경쓰기 위해 dispatch()
를 추가했다. 15장에서 설명했듯이 해당 메서드는 웹 사이트 방문자의 요청이 GET인지, POST인지 판단하는 역할을 한다.
방문자가 edit 버튼을 클릭해 "댓글 수정 페이지"로 접근했다면 GET 방식이므로 pk=1
인 comment
의 내용이 폼에 채워진 상태의 페이지가 나타난다. 이 페이지에서 submit 버튼을 클릭하면 /blog/update_comment/1/
경로로 POST 방식을 사용해 폼의 내용을 전달할것이다.
문제는 a라는 사용자가 로그인한 상테에서 /blog/update_comment/1/
을 주소 창에 입력하면 b가 작성한 pk=1
인 comment
를 수정할 수 있다는 것이다!!
이런 상황을 방지하기 위해 dispatch()
메서드에서 GET 방식인지 POST 방식인지를 판단하기에 앞서, 댓글 작성자와 로그인한 사용자가 다른 경우에는 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 django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from .models import Post, Category, Tag, Comment
from .forms import CommentForm
from django.core.exceptions import PermissionDenied
(..생략..)
class CommentUpdate(LoginRequiredMixin, UpdateView):
"""
로그인 되어 있지 않은 상태로 접근하는 것을 막기 위해 LoginRequiredMixin 포함
"""
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
UpdateView
를 사용했으므로, 템플릿 이름을 comment_form.html
로 새롭게 하나 생성하여 다음과 같이 작성했다.
<!-- blog/comment_form.html -->
{% 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-right">Submit</button>
</form>
{% endblock %}
이제 그러면 다음과 같이 edit 버튼이 생기는 것을 볼 수 있고, 이 edit 버튼을 눌러보자.
그러면 댓글 수정 페이지에 갈 수 있다. 또한 수정할 댓글의 내용이 그대로 나오는 것을 볼 수 있다.
👀UpdateView 더 알아보기
UpdateView
를 사용하면 데이터베이스에서 기존 object를 검색하고 가져와서 form 필드를 채우는 작업을 처리한다.
UpdateView
클래스는 내부적으로get_object()
메서드를 사용하여 편집 중인 object를 가져온다. 기본적으로pk_url_kwarg
속성(기본값은pk
)에 지정된 URL 매개변수를 기반으로 object를 검색한다. 그런 다음 이 object를 초기 데이터로 form에 전달하여 form 필드를 채우는 것이다.이러한 점은 사용자가 내용을 수정하기 전에 현재 내용을 볼 수 있어, 원활한 편집 환경을 제공한다.
이 동작을 재정의하거나 양식을 채우는 초기 데이터를 수정하려면
UpdateView
를 상속받고get_object()
또는get_initial()
과 같은 메서드를 재정의하면된다.def get_initial(self): initial = super(CommentUpdate, self).get_initial() initial["content"] = "초기 데이터 변경" return initial
위 내용을
CommentUpdate
클래스에 추가하면 다음과 같이 댓글 수정 페이지에 들어갔을때 초기 내용이 바뀌게된다.
댓글이 수정되었다면 "수정이 됐는지, 언제 됐는지" 표시가 되었음 좋겠다.
다음과 같이 post_detail.html
에 추가하자.
<!-- post_detail.html -->
(..생략..)
{% if post.comment_set.exists %}
{% for comment in post.comment_set.iterator %}
<!-- Single Comment -->
<div class="media mb-4" id="comment-{{ comment.pk }}">
<img class="d-flex mr-3 rounded-circle" src="http://placehold.it/50x50" alt="">
<div class="media-body">
<!--댓글 edit 버튼이 로그인 되어 있고, 작성자 본인 일때만 보이게-->
{% if user.is_authenticated and comment.author == user %}
<div class="float-right">
<a role="button"
class="btn btn-sm btn-info"
id="comment-{{ comment.pk }}-update-btn"
href="/blog/update_comment/{{ comment.pk }}/">
edit
</a>
{% endif %}
<!--"작성자 이름 날짜" 및 내용 출력-->
<h5 class="mt-0">{{ comment.author.username }} <small class="text-muted">{{ comment.created_at }}</small></h5>
<p>{{ comment.content | linebreaks }}</p>
<!-- 댓글 수정 시 "Updated: "문구 추가 -->
{% if comment.created_at != comment.modified_at %}
<p class="text-muted float-right"><small>Updated: {{ comment.modified_at }}</small></p>
{% endif %}
(..생략..)
처음 댓글이 생성되면 create_at과 modified_at이 동일하지만, 한 번 수정하면 modified_at만 수정한 시각으로 변경되는 점을 이용했다.
그래서 만약 수정이 됐다면, 오른쪽 아래에 Updated: {수정된 시간}
이 표시되도록 했고, 약간 흐릿하게 나오도록 <p>
태그의 클래스에 text-muted
와 float-right
를 추가했고, 댓글 내용보다 작게 표시되도록 <p>
태그 안에 <small>
태그도 추가했다.
웹 브라우저에서 보면 다음과 같다.
댓글 작성자가 자신의 댓글을 삭제할 수 있는 기능을 구현해보자.
댓글 삭제 버튼을 누르면, 재확인하는 모달이 나오고, 해당 모달에 있는 cancel과 delete 버튼 중 delete 버튼을 눌렀을 때 댓글이 삭제되도록 할것이다.
모달에 관련해서는 앞서 내비게이션 바를 구현했을 때 코드를 활용할것이다.
앞서 만들었던 edit 버튼 아래에 다음과 같이 내용을 추가하자.
<!-- post_detial.html -->
(..생략..)
<!--댓글 edit 버튼이 로그인 되어 있고, 작성자 본인 일때만 보이게-->
{% if user.is_authenticated and comment.author == user %}
<div class="float-right">
<a role="button"
class="btn btn-sm btn-info"
id="comment-{{ comment.pk }}-update-btn"
href="/blog/update_comment/{{ comment.pk }}/">
edit
</a>
<!-- 댓글의 오른쪽에 삭제 버튼 구현 -->
<!-- 삭제 버튼을 클릭하면 실제로 삭제를 할지 물어보는 모달이 나타나게 함 -->
<!-- 모달을 나타내기 위한 버튼이므로 data-target 정의. 또한 data-target과 똑같은 id를 가진 div가 있어야함 -->
<!-- # 기호는 대상 요소가 id 속성으로 식별됨을 나타내기 위해 data-target 속성에서 사용됩니다. # 기호 다음에는 대상 요소의 id 값이 옵니다. -->
<a role="button"
class="btn btn-sm btn-danger" id="comment-{{ comment.pk }}-delete-btn"
data-toggle="modal" data-target="#deleteCommentModal-{{ comment.pk }}"
href="#">
delete
</a>
</div>
(..생략..)
delete 버튼을 만들고 이 버튼을 클릭하면 실제로 삭제를 할지 물어보는 모달을 나타내도록 해야하기 때문에, 버튼의 data-target
으로 #deleteCommentModal-{{ comment.pk }}
을 지정했다. 이 버튼이 작동하기 위해서는 data-target
에서 지정한 값과 똑같은 id
를 가진 div
를 구현해주면 된다.
👀
#
기호는 뭘까?
#
기호는 대상 요소가id
속성으로 식별됨을 나타내기 위해data-target
속성에서 사용됩니다.#
기호 다음에는 대상 요소의id
값이 옵니다.
ex)data-target="#{{ 대상 요소의 id }}"
즉,
data-toggle
속성은 수행할 작업을 지정하고data-target
속성은 조치를 적용해야 하는 대상 요소를 지정합니다.
그러므로data-target
속성의#deleteCommentModal-{{ comment.pk }}
값은 id 속성이 "deleteCommentModal-{{ comment.pk }}"인 요소를 참조하게 되는 것 입니다.
post_detail.html
을 이어서 수정하여 delete 버튼을 누르면 모달이 나오도록 해보자.
앞서 16장에서 로그인과 회원가입 기능을 구현할 때 Log In 모달을 수정한 적이 있다. Log In 모달 내용을 복사해서 붙인 후 다음과 같이 수정하자.
<div class="modal fade" id="deleteCommentModal-{{ comment.pk }}" tabindex="-1" role="dialog" aria-labelledby="deleteCommentModalLabel" aria-hidden="true">
class="modal fade"
: "modal" 클래스와 "fade" 클래스를 적용하여 모달이 나타나고 사라지는 페이드 효과를 추가합니다.
id="deleteCommentModal-{{ comment.pk }}"
: 모달 요소의 고유한 ID를 지정합니다.
tabindex="-1"
: 모달이 키보드 탭 키를 이용한 포커스 이동에 응답하지 않도록 설정합니다.
role="dialog"
: 요소의 역할을 "dialog"로 정의합니다.
aria-labelledby="deleteCommentModalLabel"
: 모달의 레이블을 나타내기 위해 사용되는 요소의 ID를 지정합니다. deleteCommentModalLabel은 레이블 요소의 ID입니다.
aria-hidden="true"
: 모달이 숨겨져 있는 상태임을 나타냅니다.
<div class="modal-dialog" role="document">
role="document"
로 설정하여 문서나 컨텐츠를 담는 역할을 수행합니다.<div class="modal-content">
<div>
안에 구현하면 됩니다.modal-title
modal-body
<del>
태그로 감싸 취소선이 나타나도록 합니다.modal-footer
href
로 경로를 지정해줬습니다.<!-- post_detial.html -->
(..생략..)
<a role="button"
class="btn btn-sm btn-danger" id="comment-{{ comment.pk }}-delete-btn"
data-toggle="modal" data-target="#deleteCommentModal-{{ comment.pk }}"
href="#">
delete
</a>
</div>
<!-- 댓글 삭제 Modal {{ comment.pk }} -->
<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>
<!-- x버튼 -->
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<!-- 모달의 내용(바디), 어떤 댓글을 삭제하는지 알게 댓글 내용을 보여주기 -->
<div class="modal-body">
<del>{{ comment | linebreaks }}</del> <!-- <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 %}
(..생략..)
결과
하지만 아직 경로에 대한 설정을 해주지 않아서 delete 버튼을 누르면 에러가 발생한다!.
모달의 푸터에 있는 delete 버튼에 경로로 "/blog/delete_comment/{{ comment.pk }}/"
를 지정해줬다. 이제 이 경로에 해당하는 URL을 처리할 수 있게 하자.
blog/urls.py
에 해당 경로를 추가하고, blog/views.py
에 이 URL에 매칭되는 함수를 추가하자. FBV로 구현할것이다.
path('delete_comment/<int:pk>/', views.delete_comment) 를 urlpatterns에 추가!
blog/views.py
를 열고 delete_comment()
함수를 FBV 스타일로 구현한다.
get_object_or_404()
함수를 사용해 delete_comment()
함수에서 인자로 받은 pk
값과 같은 pk
값을 가진 댓글을 쿼리셋으로 받아 comment
변수에 저장. 만약 인자로 받아온 pk
에 해당하는 댓글이 존재하지 않는다면 404 에러 발생시키기.
해당하는 댓글을 받아왔다면 그 댓글이 달린 포스트를 post
변수에 저장한다. 왜냐하면 댓글이 삭제된 이후에 그 댓글이 ㅇ달려 있던 포스트 상세페이지로 리다이렉트 해야 하기 때문.
delete_comment()
함수까지 접근한 방문자가 로그인한 사용자인지, 로그인 했다면 이 댓글의 작성자인지 확인한다. 조건을 만족하지 않는다면 권한이 없는데 접근한 것이므로 PermissionDenied 오류를 발생시킨다.
조건을 만족한다면 해당 댓글을 삭제하고 이 댓글이 달려 있던 포스트의 상세 페이지로 리다이렉트한다.
(..생략..)
def delete_comment(request, pk):
comment = get_object_or_404(Comment, pk=pk) # 인자로 받은 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
이제 웹 브라우저로 가서 테스트를 해 보면 댓글 삭제에 성공한 것을 볼 수 있다.
form에 대해서 좀 더 정리하고 학습하기
모달에 대한 HTML 문법 좀 알아보기
UpdateView 처럼 View들이 어떻게 구현되어 있는지 탐색해보기