본문 바로가기
Research/Django

[Django] 점프 투 장고 튜토리얼 - 3-09 수정과 삭제

by RIEM 2021. 11. 25.

Django 점프 투 장고 정리

작성일 : 2021-11-24

문서버전 : 1.0 

개요

이 문서는 점프 투 장고 사이트의 장고 튜토리얼 학습 내용을 정리한 내용입니다.

레퍼런스

점프 투 장고 https://wikidocs.net/72242

3-09 수정과 삭제

작성한 질문과 답변을 수정하거나 삭제할 수 있는 기능을 구현해보자. 비슷한 기능을 반복 구현하기 때문에 장고 패턴에 익숙해질 수 있는 절호의 기회다!

 

수정 일시

질문이나 답변의 수정 날짜 정보 확인을 위해 Question과 Answer 모델에 수정 일시를 의미하는 modify_date 속성 추가하자.

>../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는 어떤 조건으로도 값이 없어도 괜찮다는 의미다. 그리고 수정일시(modify_date)는 수정 할 경우에만 생성되는 데이터다.


> makemigrations, migrate

모델 변경을 했으니 makemigrations, migrate 명령을 수행해보자

(mysite) c:\projects\mysite>python manage.py makemigrations
Migrations for 'pybo':
  pybo\migrations\0004_auto_20200331_0945.py
    - Add field modify_date to answer
    - Add field modify_date to question

(mysite) c:\projects\mysite>python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, pybo, sessions
Running migrations:
  Applying pybo.0004_auto_20200331_0945(... 생략 ...) OK

(mysite) c:\projects\mysite> 

 

질문 수정

작성한 질문을 수정을 위한 질문 상세 화면에 ‘수정’버튼이 필요하다.

질문 수정 버튼

질문 상세 화면에 다음과 같은 질문 수정 버튼 추가하기.

 
(... 생략 ...)
<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

위 템플릿에 {% url ‘pybo:question_modify’ question.id %} URL이 추가되었다. 따라서 pybo/urls.py에 아래와 같이 매핑해주어야 한다.

> ../mysite/pybo/urls.py

 
(... 생략 ...)

urlpatterns = [
    (... 생략 ...)
    path('question/modify/<int:question_id>/', views.question_modify, name='question_modify'),
]

 

Views.py

위 urls.py에서 정의한 views.question_modify 함수를 아래와 같이 작성해주자.

> ../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는 장고가 제공하는 모듈이며 넌필드 오류(non-field error) 발생시킬 시 사용된다.

 

질문 상세 화면에서 ‘수정’버튼을 클릭하면 localhost:8000/pybo/question/modify/2/ 페이지가 GET 방식으로 호출되어 질문수정 화면이 보여진다. 나타난 질문 수정 화면에서 ‘저장하기’ 버튼을 클릭해 localhost:8000/pybo/question/modify/2/ 페이지가 POST방식으로 호출되어 데이터가 수정된다.

 

Form 태그에 action 속성이 없는 경우 현재 페이지가 디폴트 action이 된다. 질문 수정 화면에서 사용한 템플릿은 질문 등록시 사용한 pybo/question_form.html 템플릿과 동일하다.

 

GET 요청인 경우 질문수정 화면에 조회된 질문의 제목과 내용이 반영되도록 아래와 같이 폼을 생성해야 한다.

Form = QuestionForm(instance=question)

 

폼 생성시 instance 값을 지정해주면 폼 속성 값을 instance값으로 채울 수 있다. 이를 활용하면 질문 수정하는 화면에 제목과 내용을 채워서 보여줄 수 있다. POST 요청인 경우 수정된 내용을 반영해야 하는 케이스이므로 다음처럼 폼을 생성해야 한다.

Form = QuestionForm(request.POST, instance=question)

위 코드는 intance 기준으로 QuestionForm을 생성하되 request.POST의 값으로 다시 덮어쓰라는 의미다. 따라서 질문 수정화면에서 제목 또는 내용을 변경하여 POST 요청하면 변경된 내용이 다시 QuestionForm에 저장될 것이다. 

 

질문 수정 일시는 현재일시로 저장한다.

Question.modify_date = timezone.now()

 

오류 표시

‘수정권한 없습니다’라는 오류는 messages 모듈에 의해 잘 발생할 수 있도록 질문 상세 화면 상단에 오류 영역을 추가하자.

 
{% 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>
    (... 생략 ...)
(... 생략 ...)

 

로그인한 사용자와 글 작성자가 동일한 경우에만 작동되기 때문에 오류가 표시될 일은 없다. 하지만 비정상적 방법으로 질문 수정할 경우를 대비해 해당 코드를 추가해두었다. 

 

자 확인해보자.



질문 삭제

작성 질문을 삭제하기 위해 질문 상세 화면에 ‘삭제’버튼을 추가해주자.

질문 삭제 버튼

작성한 글 삭제할 수 있는 버튼을 아래와 같이 추가해주자.

> ../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’항목이 추가되었다.

 

Data-uri 속성은 제이쿼리에서 $(this).data(‘uri’)와 같이 사용하여 그 값을 얻는다.

 

Href에 삭제 URL을 직접 사용하지 않고 위 방식을 사용하는 이유는 삭제 버튼 클릭 시 ‘정말로 삭제하겠습니까?’라는 확인창을 띄우기 위함이다.

 

jQuery

삭제 버튼 클릭 시 확인창 호출하기 위해선 다음과 같은 자바스크립트 코드가 필요하다.

 
<script type='text/javascript'>
$(document).ready(function(){
    $(".delete").on('click', function() {
        if(confirm("정말로 삭제하시겠습니까?")) {
            location.href = $(this).data('uri');
        }
    });
});
</script>

 

위 JS코드는 delete 클래스 포함한 콤포넌트 클릭 시 ‘정말로 삭제하시겠습니까?’라는 질문을 띄우고 ‘확인’을 선택할 경우 해당 컴포넌트의 data-uri값으로 URL 호출을 하라는 의미다. ‘확인’대신 ‘취소’를 선택하면 물론 아무일도 발생하지 않는다.

‘$(document).ready(function()는 화면 로드된 후 자동으로 호출시키는 jQuery 함수다.

 

결국 ‘삭제’버튼 누르고 ‘확인’을 선택하면 data-uri 속성에 해당하는 {% url ‘pybo:question_delete’ question.id %}이 호출된다.

스크립트 블록

이 JS는 jQuery로 작성되었기 때문에 jQuery 라이브러리가 먼저 로드되는 것이 순서다. 일전에 부트스트랩의 기능 사용하기 위해 jQuery 라이브러리는 base.html에 포함되어 있는 상태다. 템플릿에서 jQuery를 안전히 사용하기 위해선 아래와 같이 수정을 해주자.

> ../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>

 

<script src=”{% static ‘jquery-3.4.1.min.js’ %}’은 jQuery 라이브러리를 로드하는 코드다. 이후 {% block script %}, {% endblock %} 블록을 추가해주었다. 라이브러리를 먼저 로드한 뒤 JS 작성할 수 있는 블록을 선언해준 것이다.

 

이제 base.html 상속하는 템플릿은 jQuery 라이브러리를 신경쓰지 않고 블록안에 jQuery 자바스크립트만 잘 작성해주면 된다.


> ../templates/pybo/question_detail.html

Question_detail.html 하단에 {% block script %}{% endblock %} 블록을 추가해주자.

 
(... 생략 ...)
{% endblock %}
{% block script %}
<script type='text/javascript'>
$(document).ready(function(){
    $(".delete").on('click', function() {
        if(confirm("정말로 삭제하시겠습니까?")) {
            location.href = $(this).data('uri');
        }
    });
});
</script>
{% endblock %}

질문 삭제할 수 있는 JS 작성 완료했다.

 

Urls.py

Data-uri에 {% url ‘pybo:question_delete’ question.id %} URL이 추가되었으므로 pybo/urls.py에 다음처럼 URL 매핑 추가하자.

> ../mysite/pybo/urls.py

 
(... 생략 ...)

urlpatterns = [
    (... 생략 ...)
    path('question/delete/<int:question_id>/', views.question_delete, name='question_delete'),
]

 

Views.py

Views.question_delete 함수를 다음과 같이 작성하자.

> ../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')

@login_requred 애너테이션이 적용되었는데 이는 question_delete 함수 역시 로그인이 필요하기 때문이다. 따라서 로그인한 사용자와 글쓴이가 동일한 경우에만 삭제 가능하다.

 

질문 삭제 확인

질문 작성 사용자와 로그인한 사용자가 동일한 경우 다음과 같이 상세조회 화면에 ‘삭제’버튼이 노출된다.

 

삭제 버튼을 누르면 삭제하겠냐는 문구가 뜨고 확인을 누르면 글은 삭제되고 이전 화면으로 돌아간다.

 

답변 수정

질문 수정 기능을 만들어 주었으니 답변 수정 기능도 추가해주자. 질문 수정 기능 구현과 유사하다. 답변 수정은 답변 등록 템플릿이 따로 없기 때문에 답변 수정에 사용할 템플릿을 추가로 만들어야 한다. 

답변 등록은 질문 상세 화면 하단에 텍스트 입력창을 추가하여 구현된 상황이다. 따라서 답변 수정을 위해 질문 상세 템플릿은 부적합하다. 

답변 수정 버튼

> ../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

위에서 {% url ‘pybo:answer_modify’ answer.id %}로 추가한 url을 pybo/urls.py 파일에 URL 매핑 추가해주자.

> ../pybo/urls.py

 
(... 생략 ...)

urlpatterns = [
    (... 생략 ...)
    path('answer/modify/<int:answer_id>/', views.answer_modify, name='answer_modify'),
]

 

Views.py

위에서 정의한 url 내 매핑된 views.answer_modify 함수를 정의해주자.

> ../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 템플릿을 새로 만들어주자.

> ../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 %}

 

잘 작동되는지 확인해보자.

 

 

답변 수정 기능이 잘 작동된다.



답변 삭제

답변 삭제하는 기능을 추가해보자. 답변 삭제도 질문 삭제와 동일하게 구현하면 된다.

답변 삭제 버튼

질문 상세 화면에서 답변 삭제할 수 있는 버튼 다음과 같이 추가한다.

> ../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>
            <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

위에서 {% url ‘pybo:answer_delete’ answer.id %} 추가했으니 해당 url을 pybo/urls.py에 URL 매핑하여 반영해주자.

> ../mysite/pybo/urls.py

 
(... 생략 ...)

urlpatterns = [
    (... 생략 ...)
    path('answer/delete/<int:answer_id>/', views.answer_delete, name='answer_delete'),
]

 

Views.py

위에서 정의한 views.answer_delete 함수를 다음과 같이 작성하자.

> ../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)

 

 

답변 삭제 기능이 잘 작동하는 것을 확인했다.

 

수정일시 표시하기

수정일시를 질문 상세 화면에서 확인할 수 있도록 표시해주자. 질문과 답변에는 작성 일시를 표시하고 있으니, 작성일시 바로 왼쪽 편에 수정일시를 표시해주자.

 

> ../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 %}

(... 생략 ...)

 

 

 

댓글