이전포스트
[Django] 1. 개발 환경 설정 https://wroni.tistory.com/4
[Django] 2 - 1. URL과 View https://wroni.tistory.com/5
[Django] 2 - 2. 모델 https://wroni.tistory.com/6
[Django] 2 - 3. 장고 관리자 https://wroni.tistory.com/7
[Django] 2 - 4. 조회와 템플릿 https://wroni.tistory.com/8
[Django] 2 - 5. URL과 네임스페이스, 2 - 6. 데이터 저장 https://wroni.tistory.com/9
[Django] 2 - 7. 스태틱 https://wroni.tistory.com/10
[Django] 2 - 8. 부트스트랩 https://wroni.tistory.com/11
[Django] 2 - 9. 템플릿 상속 https://wroni.tistory.com/12
[Django] 2 - 10. 폼 https://wroni.tistory.com/13
[Django] 3 - 1. 네비게이션바 https://wroni.tistory.com/14
[Django] 3 - 2. 페이징 https://wroni.tistory.com/15
[Django] 3 - 3. 템플릿 필터, 3 - 4. 답변 개수 표시 https://wroni.tistory.com/16
[Django] 3 - 5. 로그인과 로그아웃 https://wroni.tistory.com/18
[Django] 3 - 6. 계정생성 https://wroni.tistory.com/19
[Django] 3 - 7. 모델 변경 https://wroni.tistory.com/20
[Django] 3 - 8. 글쓴이 표시 https://wroni.tistory.com/21
3 - 9. 수정과 삭제
이번에는 질문과 답변을 수정하고 삭제할 수 있는 기능을 구현해볼 것이다.
1. 수정 일시
질문이나 답변이 언제 수정되었는지 알 수 있도록 Question과 Answer 모델에 수정 일시를 의미하는 modify_date 속성을 추가한다.
파일 : C:\projects\mysite\pybo\models.py
class Question(models.Model):
(...생략...)
modify_date = models.DateTimeField(null=True, blank=True)
(...생략...)
class Answer(models.Model):
(...생략...)
modify_date = models.DateTimeField(null=True, blank=True)
(...생략...)
null=True는 데이터베이스에서 modify_date 칼럼에 null을 허용한다는 의미이다.
blank=True는 form.is_valid()를 통한 입력 데이터 검사 시 값이 없어도 된다는 의미이다.
즉, null=True, blank=True는 어떤 조건으로든 값을 비워둘 수 있다.
수정일시는 수정한 경우에만 생성되는 데이터이므로 null=True, blank=True를 지정하였다.
이제 모델이 변경되었으므로 까먹지 말고 makemigrations, migrate 명령을 수행하자.
엥 modify_data가 아니라 modify_date가 되어야 하는데... 방금 models.py에서 modify_data로 잘못 적었나보다.
얼른 modify_date로 수정하고 다시 makemigrations, migrate를 수행하였다.
빨리 발견해서 다행이다...
rename 하였다고 뜨며, modify_date로 수정되었다. 휴 다행이다.
2. 질문 수정
질문 상세 화면에서 "수정" 버튼을 클릭하면 수정 화면으로 진입하도록 해야 한다.
질문 수정 버튼
질문 상세 화면에서 이와 같이 질문 수정 버튼을 추가한다.
파일 : C:\projects\mysite\templates\pybo\question_detail.html
(... 생략 ...)
<div class="card my-3">
<div class="card-body">
<div class="card-text" style="white-space: pre-line;">{{ question.content }}</div>
<div class="d-flex justify-content-end">
<div class="badge badge-light p-2 text-left">
<div class="mb-2">{{ question.author.username }}</div>
<div>{{ question.create_date }}</div>
</div>
</div>
{% if request.user == question.author %}
<div class="my-3">
<a href="{% url 'pybo:question_modify' question.id %}"
class="btn btn-sm btn-outline-secondary">수정</a>
</div>
{% endif %}
</div>
</div>
(... 생략 ...)
수정 버튼은 로그인한 사용자와 글쓴이가 동일한 경우에만 노출될 수 있도록 {% if request.user == question.author %}을 적용한다.
만약 로그인힌 사용자와 글쓴이가 다르다면 수정 버튼은 노출되지 않는다.
urls.py
그리고 question_detail에서 {% url 'pybo:question_modify' question.id %} URL이 추가되었으니 pybo/urls.py에 해당되는 URL 매핑을 추가하자.
파일 : C:\projects\mysite\pybo\urls.py
... 생략 ...
urlpatterns = [
(... 생략 ...)
path('question/modify/<int:question_id>/', views.question_modify, name='question_modify'),
]
views.py
이제 urls.py에서 정의한 views.question_modify 함수를 정의하자.
파일 : C:\projects\mysite\pybo\views.py
... 생략 ...
from django.contrib import messages
... 생략 ...
@login_required(login_url='common:login')
def question_modify(request, question_id):
"""
pybo 질문수정
"""
question = get_object_or_404(Question, pk=question_id)
if request.user != question.author:
messages.error(request, '수정권한이 없습니다')
return redirect('pybo:detail', question_id=question.id)
if request.method == "POST":
form = QuestionForm(request.POST, instance=question)
if form.is_valid():
question = form.save(commit=False)
question.modify_date = timezone.now() # 수정일시 저장
question.save()
return redirect('pybo:detail', question_id=question.id)
else:
form = QuestionForm(instance=question)
context = {'form': form}
return render(request, 'pybo/question_form.html', context)
question_modify 함수는 로그인한 사용자(request.user)와 수정하려는 질문의 글쓴이(question.author)가 다른 경우에는 "수정권한이 없습니다."라는 오류를 발생시키도록 하였다. 해당 오류를 발생시키기 위해서는 messages 모듈을 이용하였다. messages는 장고가 제공하는 모듈로 넌필드 오류를 발생시킬 경우에 사용한다.
질문 상세 화면에서 "수정" 버튼을 클릭하면
http://localhost:8000/pybo/question/modify/2/ 페이지가 GET 방식으로 호출된다. 이후 질문 수정 화면이 보여진다.
질문 수정화면에서 "저장하기" 버튼을 클릭하면
http://localhost:8000/pybo/question/modify/2/ 페이지가 POST 방식으로 호출되어 데이터가 수정된다.
GET 요청인 경우에 질문수정 화면에 조회된 질문의 제목과 내용이 반영될 수 있도록 다음과 같이 폼을 생성해야 한다.
form = QuestionForm(instance=question)
폼 생성시 이처럼 instance 값을 지정하면 폼의 속성 값이 instance의 값으로 채워진다. 따라서 질문을 수정하는 화면에서 제목과 내용이 채워진 채로 보일 것이다.
POST 요청인 경우에 수정된 내용을 반영하는 케이스이므로 이와같이 폼을 생성해야 한다.
form = QuestionForm(request.POST, instance=question)
해당 코드의 의미는 instance를 기준으로 QuestionForm을 생성하지만 request.POST의 값으로 덮어쓰라는 것이다.
따라서 질문 수정화면에서 제목 또는 내용을 변경하여 POST 요청하면 변경된 내용이 QuestionForm에 저장될 것이다.
질문의 수정일시는 다음처럼 현재일시로 저장되도록 했다.
question.modify_date = timezone.now()
오류표시
그리고 messages 모듈에 의해 발생되는 '수정권한이 없습니다'라는 오류가 표시될 수 있도록 질문 상세 화면 위쪽에 오류 영역을 추가하자.
파일 : C:\projects\mysite\templates\pybo\question_detail.html
{% extends 'pybo/base.html' %}
{% block content %}
<div class="container my-3">
<!-- 사용자오류 표시 -->
{% if messages %}
<div class="alert alert-danger my-3" role="alert">
{% for message in messages %}
<strong>{{ message.tags }}</strong>
<ul><li>{{ message.message }}</li></ul>
{% endfor %}
</div>
{% endif %}
<h2 class="border-bottom py-2">{{ question.subject }}</h2>
... 생략 ...
... 생략 ...
수정은 로그인 한 사용자와 글 작성자가 동일한 경우에만 가능하기 때문에 해당 오류가 표시될 일은 없을 것이다.
하지만 비정상적인 방법으로 질문을 수정할 경우 오류를 보여주어야 하므로 필요하다.
3. 질문 삭제
질문 삭제 버튼
작성한 글을 삭제할 수 있는 버튼을 추가하자.
파일 : C:\projects\mysite\templates\pybo\question_detail.html
... 생략 ...
{% if request.user == question.author %}
<div class="my-3">
<a href="{% url 'pybo:question_modify' question.id %}"
class="btn btn-sm btn-outline-secondary">수정</a>
<a href="#" class="delete btn btn-sm btn-outline-secondary"
data-uri="{% url 'pybo:question_delete' question.id %}">삭제</a>
</div>
{% endif %}
... 생략 ...
삭제 버튼은 수정 버튼과는 달리 href 속성값을 "#"로 설정하였다. 그리고 삭제를 실행할 URL을 얻기 위해 data-uri 속성을 추가하고, 삭제 버튼이 눌리는 이벤트를 확인할 수 있도록 class 속성에 "delete" 항목을 추가해 주었다.
href에 삭제 URL을 직접 사용하지 않고 이러한 방식을 사용하는 이유는 삭제 버튼을 클릭했을 때, "정말로 삭제하시겠습니까?"와 같은 확인창이 필요하기 때문이다.
jQuery
삭제 버튼을 클릭했을 때, 확인창 호출 위해서는 다음과 같이 자바스크립트 코드가 필요하다.
<script type='text/javascript'>
$(document).ready(function(){
$(".delete").on('click', function() {
if(confirm("정말로 삭제하시겠습니까?")) {
location.href = $(this).data('uri');
}
});
});
</script>
해당 자바스크립트는 delete라는 클래스를 포함하는 컴포넌트를 클릭하면 "정말로 삭제하시겠습니까?"라는 질문을 하고 확인을 선택했을 때 해당 컴포넌트의 data-uri 값으로 URL 호출을 하라는 의미이다. 확인 대신 취소를 선택하면 아무 일도 발생하지 않는다.
따라서 삭제 버튼을 클릭하고 확인을 선택하면 data-uri 속성에 해당하는 {% url 'pybo:question_delete' question.id %}이 호출될 것이다.
$(document).ready(function()는 화면이 로드된 후 자동으로 호출되는 jQuery 함수이다.
스크립트 블록
이 자바스크립트는 jQuery로 작성되었기 때문에 jQuery 라비르러리가 먼저 로드된 후 호출되어야 한다. jQuery 라이브러리는 부트스트랩의 기능을 사용하기 위해 base.html에 포함되어 있다.
템플릿에서 jQuery를 안전하게 사용하려면 다음처럼 base.html을 수정해주어야 한다.
파일 : C:\projects\mysite\templates\base.html
... 생략 ...
<!-- jQuery JS -->
<script src="{% static 'jquery-3.4.1.min.js' %}"></script>
<!-- Bootstrap JS -->
<script src="{% static 'bootstrap.min.js' %}"></script>
<!-- 자바스크립트 Start -->
{% block script %}
{% endblock %}
<!-- 자바스크립트 End -->
</body>
</html>
jQuery라이브러리르 로드하는 문장인 <script src="{% static 'jquery-3.4.1.min.js' %}</script> 이후에 {% block script %}{% endblock %} 블록을 추가하였다. 왜냐하면 위에서 언급한 것과 같이 jQuery 자바스크립트를 작성하기 위해서는 jQuery 라이브러리를 먼저 로드해야 하기 때문이다.
이제 base.html을 상속하는 템플릿은 jQuery 라이브러리를 신경 쓸 필요없이 {% block script %}{% endblock %} 블록에 jQuery 자바스크립트를 작성하면 된다.
question_detail.html 하단에 {% block script %}{% endblock %} 블록을 다음과 같이 추가하자.
파일 : C:\projects\mysite\templates\pybo\question_detail.html
... 생략 ...
{% endblock %}
{% block script %}
<script type='text/javascript'>
$(document).ready(function(){
$(".delete").on('click', function() {
if(confirm("정말로 삭제하시겠습니까?")) {
location.href = $(this).data('uri');
}
});
});
</script>
{% endblock %}
{% block script %} 과 {% endblock %} 사이에 질문을 삭제할 수 있는 자바스크립트를 작성하였다.
urls.py
그리고 data-uri에 {% url 'pybo:question_delete' question.id %} URL이 추가되었으므로 pybo/urls.py에 다음처럼 URL 매핑을 추가하자.
파일 : C:\projects\mysite\pybo\urls.py
... 생략 ...
urlpatterns = [
... 생략 ...
path('question/delete/<int:question_id>/', views.question_delete, name='question_delete'),
]
views.py
위에서 정의한 views.question_delete 함수를 다음처럼 작성하자.
파일 : C:\projects\mysite\pybo\views.py
... 생략 ...
@login_required(login_url='common:login')
def question_delete(request, question_id):
"""
pybo 질문삭제
"""
question = get_object_or_404(Question, pk=question_id)
if request.user != question.author:
messages.error(request, '삭제권한이 없습니다')
return redirect('pybo:detail', question_id=question.id)
question.delete()
return redirect('pybo:index')
question_delete 함수 역시 로그인이 필요하므로 @login_required 에너테이션을 적용하고 로그인한 사용자와 글쓴이가 동일한 경우에만 삭제할 수 있도록 하였다.
질문 삭제 확인
질문을 작성한 사용자와 로그인한 사용자가 동일할 경우 다음처럼 상세조회하면 삭제 버튼이 표시될 것이다.
4. 답변 수정
이제 답변 수정 기능을 구현하자. 질문 수정과 비슷한 방법으로 하면 된다. 하지만 답변 수정을 답변 등록 템플릿이 따로 없기 때문에 답변 수정에 사용할 템플릿이 추가로 필요하다.
답변 수정 버튼
답변 목록이 출력되는 부분에 답변 수정 버튼을 추가한다.
파일 : C:\projects\mysite\templates\pybo\question_detail.html
... 생략 ...
{% for answer in question.answer_set.all %}
<div class="card my-3">
<div class="card-body">
... 생략 ...
{% if request.user == answer.author %}
<div class="my-3">
<a href="{% url 'pybo:answer_modify' answer.id %}"
class="btn btn-sm btn-outline-secondary">수정</a>
</div>
{% endif %}
</div>
</div>
{% endfor %}
... 생략 ...
urls.py
qustion_detail.htlm 에서 {% url 'pybo:answer_modify' answer.id %}가 추가되었으니 pybo/urls.py 에 URL 매핑을 추가하자.
파일 : C:\projects\mysite\pybo\urls.py
...생략...
urlpatterns = [
...생략...
path('answer/modify/<int:answer_id>', views.answer_modify, name='answer_modify'),
]
views.py
이제 urls.py에서 정의한 views.answer_modify 함수를 작성하자.
파일 : C:\projects\mysite\pybo\views.py
... 생략 ...
from .models import Question, Answer
(... 생략 ...)
@login_required(login_url='common:login')
def answer_modify(request, answer_id):
"""
pybo 답변수정
"""
answer = get_object_or_404(Answer, pk=answer_id)
if request.user != answer.author:
messages.error(request, '수정권한이 없습니다')
return redirect('pybo:detail', question_id=answer.question.id)
if request.method == "POST":
form = AnswerForm(request.POST, instance=answer)
if form.is_valid():
answer = form.save(commit=False)
answer.modify_date = timezone.now()
answer.save()
return redirect('pybo:detail', question_id=answer.question.id)
else:
form = AnswerForm(instance=answer)
context = {'answer': answer, 'form': form}
return render(request, 'pybo/answer_form.html', context)
답변 수정 폼
답변 수정을 위해 템플릿 answer_form.html 파일을 새로 생성한다.
파일 : C:\projects\mysite\templates\pybo\answer_form.html
{% extends 'base.html' %}
{% block content %}
<div class="container my-3">
<form method="post" class="post-form">
{% csrf_token %}
{% include "form_errors.html" %}
<div class="form-group">
<label for="content">답변내용</label>
<textarea class="form-control" name="content" id="content"
rows="10">{{ form.content.value|default_if_none:'' }}</textarea>
</div>
<button type="submit" class="btn btn-primary">저장하기</button>
</form>
</div>
{% endblock %}
답변 내용을 확인하고 수정할 수 있도록 한 템플릿이다.
5. 답변 삭제
답변 삭제 버튼
질문 상세화면에서 답변을 삭제할 수 있는 버튼을 추가하자.
파일 : C:\projects\mysite\templates\pybo\qustion_detail.html
... 생략 ...
{% for answer in question.answer_set.all %}
<div class="card my-3">
<div class="card-body">
(... 생략 ...)
{% if request.user == answer.author %}
<div class="my-3">
<a href="{% url 'pybo:answer_modify' answer.id %}"
class="btn btn-sm btn-outline-secondary">수정</a>
<a href="#" class="delete btn btn-sm btn-outline-secondary "
data-uri="{% url 'pybo:answer_delete' answer.id %}">삭제</a>
</div>
{% endif %}
</div>
</div>
{% endfor %}
... 생략 ...
수정 버튼 옆에 삭제 버튼을 추가하였다. 질문의 삭제 버튼과 마찬가지로 삭제 버튼에 delete 클래스를 적용하였으므로 삭제 버튼을 누르면 data-uri 속성에 설정한 url이 실행된다.
urls.py
question_detail.html에서 {% url 'pybo:answer_delete' answer.id %}가 추가되었으므로 pybo/urls.py 파일에 URL 매핑을 추가한다.
파일 : C:\projects\mysite\pybo\urls.py
... 생략 ...
urlpatterns = [
... 생략 ...
path('answer/delete/<int:answer_id>/', views.answer_delete, name='answer_delete'),
]
views.py
그리고 방금 urls.py 에서 정의한 views.answer_delete 함수를 작성하자.
파일 : C:\projects\mysite\pybo\views.py
... 생략 ...
@login_required(login_url='common:login')
def answer_delete(request, answer_id):
"""
pybo 답변삭제
"""
answer = get_object_or_404(Answer, pk=answer_id)
if request.user != answer.author:
messages.error(request, '삭제권한이 없습니다')
else:
answer.delete()
return redirect('pybo:detail', question_id=answer.question.id)
답변 삭제 확인
질문 상세 화면에서 답변을 작성한 사용자와 로그인한 사용자가 같다면 삭제 버튼이 나타날 것이다. 그리고 수정 버튼도 함께 나타날 것이다.
6. 수정일시 표시하기
마지막으로 질문 상세 화면에서 수정일시를 확인할 수 있도록 템플릿을 수정해보자.
질문과 답변에는 이미 작성일시를 표시하고 있다. 작성일시 바로 왼쪽에 수정일시를 추가하자.
파일 : C:\projects\mysite\templates\pybo\question_detail.html
... 생략 ...
<div class="card-body">
<div class="card-text" style="white-space: pre-line;">{{ question.content }}</div>
<div class="d-flex justify-content-end">
{% if question.modify_date %}
<div class="badge badge-light p-2 text-left mx-3">
<div class="mb-2">modified at</div>
<div>{{ question.modify_date }}</div>
</div>
{% endif %}
<div class="badge badge-light p-2 text-left">
<div class="mb-2">{{ question.author.username }}</div>
<div>{{ question.create_date }}</div>
</div>
</div>
... 생략 ...
</div>
... 생략 ...
{% for answer in question.answer_set.all %}
<div class="card my-3">
<div class="card-body">
<div class="card-text" style="white-space: pre-line;">{{ answer.content }}</div>
<div class="d-flex justify-content-end">
{% if answer.modify_date %}
<div class="badge badge-light p-2 text-left mx-3">
<div class="mb-2">modified at</div>
<div>{{ answer.modify_date }}</div>
</div>
{% endif %}
<div class="badge badge-light p-2 text-left">
<div class="mb-2">{{ answer.author.username }}</div>
<div>{{ answer.create_date }}</div>
</div>
</div>
... 생략 ...
</div>
</div>
{% endfor %}
... 생략 ...
이제 질문이나 답변을 수정하면 수정일시도 함께 표시된다.
이번 챕터는 내용이 많았는데 질문 삭제, 질문 수정, 답변 삭제, 답변 수정, 등 비슷한 패턴을 가지고 있어서 실습해보면 감을 잡을 수 잇을 것이다.
※ 본 내용은 django 공부 기록이며, 점프 투 장고를 참고하였습니다.
'Django > Django 기초' 카테고리의 다른 글
[Django] 3 - 11. views.py 파일 분리 (0) | 2021.12.21 |
---|---|
[Django] 3 - 10. 댓글 (0) | 2021.12.15 |
[Django] 3 - 8. 글쓴이 표시 (0) | 2021.12.14 |
[Django] 3 - 7. 모델 변경 (0) | 2021.12.09 |
[Django] 3 - 6. 계정생성 (0) | 2021.12.07 |