Programming

; develop a program

Framework/Django

[Django] 장고(Django) 서비스 개발 - 모델 변경

Clloud_ 2022. 12. 18. 23:12
반응형

게시판의 질문, 답변에는 누가 글을 작성했는지 알려주는 "글쓴이" 항목이 필요하다.

따라서 Question과 Answer 모델에 "글쓴이"에 해당되는 author 속성을 추가한다.

 

이번 포스팅에서는 장고를 사용하여 게시판 서비스 개발에 필요한 모델 변경에 대하여 공부를 해보고자 한다.

 


Question 속성 추가

먼저 Question 모델에 author 속성을 추가한다.

 

[파일명: projects\mysite\pybo\models.py]

from django.db import models
from django.contrib.auth.models import User


class Question(models.Model):
    author = models.ForeignKey(User, on_delete=models.CASCADE)
    (... 생략 ...)

 

author 필드는 User 모델을 ForeignKey로 적용하여 선언한다.

User 모델은 django.contrib.auth 앱이 제공하는 사용자 모델로 회원 가입 시 데이터 저장에 사용했던 모델이다.

on_delete=models.CASCADE는 계정이 삭제되면 이 계정이 작성한 질문을 모두 삭제하라는 의미이다.

 

모델을 변경한 후에는 반드시 makemigrations와 migrate를 통해 데이터베이스를 변경해 주어야 한다.

다음과 같이 makemigrations을 실행한다.

(mysite) c:\projects\mysite>python manage.py makemigrations
You are trying to add a non-nullable field 'author' to question without a default; we can't do that (the database needs something to populate existing rows).
Please select a fix:
1) Provide a one-off default now (will be set on all existing rows with a null value for this column)
2) Quit, and let me add a default in models.py Select an option:

 

하지만 makemigrations을 실행하면 option을 선택하라는 위와 같은 메시지가 나타난다. 

 

이 메시지가 나타난 이유는 Question 모델에 author를 추가하면 이미 등록되어 있던 게시물에 author에 해당되는 값이 저장되어야 하는데, 장고는 author에 어떤 값을 넣어야 하는지 모르기 때문이다.

그래서 장고가 여러분에게 기존에 저장된 Question 모델 데이터의 author에 어떤 값을 저장해야 하는지 묻는 것이다.

 

이 문제를 해결하는 방법에는 2가지가 있다.

첫 번째 방법은 author 속성을 null로 설정하는 방법이고, 두 번째 방법은 기존 게시물에 추가될 author에 강제로 임의 계정 정보를 추가하는 방법이다.

질문, 답변에는 author 값이 무조건 있어야 하므로 두 번째 방법을 사용할 것이다. 

 

위 메시지를 유지한 상태에서 '1'을 입력한다.

Select an option: 1
Please enter the default value now, as valid Python
The datetime and django.utils.timezone modules are available, so you can do e.g. timezone.now
Type 'exit' to exit this prompt
>>>

 

그러면 파이썬 셸이 기동 되고>>> 프롬프트가 나타난다.

여기서 다시 '1'을 다음과 같이 입력한다.

>>> 1
Migrations for 'pybo':
    pybo\migrations\0002_question_author.py
        - Add field author to question

(mysite) c:\projects\mysite>

 

여기서 입력한 '1'은 admin 계정의 id 값이다.

따라서 기존 게시물의 author에는 admin 계정이 등록된다.

계정 생성 시마다 id가 1부터 순차적으로 증가한다.
따라서 createsuperuser로 최초 생성했던 계정인 admin의 id 값은 1이다.

 

migrate 명령으로 변경된 내용을 데이터베이스에 적용한다.

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

 


Answer 속성 추가

Question 모델과 같은 방법으로 Answer 모델에 author 속성을 추가한다.

 

[파일명: projects/mysite/pybo/models.py]

(... 생략 ...)

class Answer(models.Model):
    author = models.ForeignKey(User, on_delete=models.CASCADE)
    (... 생략 ...)

 

이어서 makemigrations 명령을 실행하면 다음처럼 Question 모델과 동일한 선택 메시지가 나타난다.

역시 동일한 방법으로 1)을 선택하고 admin의 id 값인 1을 입력한다.

(mysite) c:\projects\mysite>python manage.py makemigrations
You are trying to add a non-nullable field 'author' to answer without a default; we can't do that (the database needs something to populate existing rows).
Please select a fix:
  1) Provide a one-off default now (will be set on all existing rows with a null value for this column)
  2) Quit, and let me add a default in models.py
Select an option: 1
Please enter the default value now, as valid Python The datetime and django.utils.timezone modules are available, so you can do e.g. timezone.now
Type 'exit' to exit this prompt
>>> 1
Migrations for 'pybo':
    pybo\migrations\0003_answer_author.py
        - Add field author to answer

 

이어서 migrate 명령을 실행한다.

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

(mysite) c:\projects\mysite>

 


author 속성에 null 허용하기

추가한 author 속성에 null을 허용하려면 다음처럼 null=True 속성을 추가하면 된다.

author = models.ForeignKey(User, on_delete=models.CASCADE, null=True)
null=True 속성을 부여하면 migrate시 데이터베이스에 null 허용 컬럼으로 생성된다.

 


author 저장

이제 Question, Answer 모델에 author 속성이 추가되었으므로 질문과 답변 저장 시에 author도 함께 저장해야 한다.

질문, 답변에 글쓴이를 추가한다는 느낌으로 작업을 진행한다.

 

먼저 answer_create 함수를 수정한다.

 

[파일명: projects\mysite\pybo\views.py]

def answer_create(request, question_id):
    (... 생략 ...)
        if form.is_valid():
            answer = form.save(commit=False)
            answer.author = request.user  # author 속성에 로그인 계정 저장
            (... 생략 ...)
    (... 생략 ...)

 

답변의 글쓴이는 현재 로그인한 계정이므로 answer.author = request.user로 처리한다. 

request.user는 현재 로그인한 계정의 User 모델 객체이다.

 

question_create 함수도 마찬가지 방법으로 수정한다.

 

[파일명: projects\mysite\pybo\views.py]

def question_create(request):
    (... 생략 ...)
        if form.is_valid():
            question = form.save(commit=False)
            question.author = request.user  # author 속성에 로그인 계정 저장
    (... 생략 ...)

 

질문의 글쓴이도 현재 로그인한 계정이므로 question.author = request.user로 처리한다.

 

다시 로컬 서버를 시작하고 로그인한 다음 질문 · 답변 등록을 테스트해보면 잘 될 것이다.

 


로그인이 필요한 함수

하지만 로그아웃 상태에서 질문 또는 답변을 등록하면 다음과 같은 ValueError가 발생한다.

 

 

이 오류는 request.user가 User 객체가 아닌 AnonymousUser 객체라서 발생한 것이다.

조금 더 자세히 설명하자면 request.user에는 로그아웃 상태이면 AnonymousUser 객체가, 로그인 상태이면 User 객체가 들어있는데, 앞에서 author 속성을 정의할 때 User를 이용하도록 했다.

그래서 answer.author = request.user에서 User 대신 AnonymousUser가 대입되어 오류가 발생한 것이다.

 

이 문제를 해결하려면 request.user를 사용하는 함수에 @login_required 애너테이션을 사용해야 한다. 

@login_required 애너테이션이 붙은 함수는 로그인이 필요한 함수를 의미한다.

 

[파일명: projects\mysite\pybo\views.py]

from django.shortcuts import render, get_object_or_404, redirect
from django.utils import timezone
from .models import Question
from .forms import QuestionForm, AnswerForm
from django.core.paginator import Paginator
from django.contrib.auth.decorators import login_required

(... 생략 ...)

@login_required(login_url='common:login')
def answer_create(request, question_id):
    (... 생략 ...)

@login_required(login_url='common:login')
def question_create(request):
    (... 생략 ...)

 

answer_create 함수와 question_create 함수는 함수 내에서 request.user를 사용하므로 로그인이 필요한 함수이다.

따라서 위와 같이 @login_required 어노테이션을 사용해야 한다.

 

로그아웃 상태에서 @login_required 어노테이션이 적용된 함수가 호출되면 자동으로 로그인 화면으로 이동하게 된다. @login_required 어노테이션은 login_url='common:login' 처럼 로그인 URL을 지정할 수 있다.

 

이렇게 수정한 후 로그아웃 상태에서 질문을 등록하거나 답변을 등록하면 자동으로 로그인 화면으로 이동되는 것을 확인할 수 있다.

 


next

그런데 로그아웃 상태에서 '질문 등록하기'를 눌러 로그인 화면으로 전환된 상태에서 웹 브라우저 주소창의 URL을 보면 next 파라미터가 있는 것을 확인할 수 있다.

 

 

 

이는 로그인 성공 후 next 파라미터에 있는 URL로 페이지를 이동하겠다는 의미이다.

그런데 지금은 그렇게 되고 있지 않다.

로그인 후 next 파라미터에 있는 URL로 페이지를 이동하려면 로그인 템플릿에 다음과 같이 hidden 타입의 next 항목을 추가해야 한다.

 

[파일명: projects\mysite\templates\common\login.html]

(... 생략 ...)
<form method="post" action="{% url 'common:login' %}">
    {% csrf_token %}
    <input type="hidden" name="next" value="{{ next }}">  <!-- 로그인 성공후 이동되는 URL -->
    {% include "form_errors.html" %}
(... 생략 ...)

 

그러면 로그인 후 next 항목의 URL로 이동하는 것을 확인할 수 있다.

 


disabled

현재 질문 등록은 로그아웃 상태에서는 아예 글을 작성할 수 없다.

하지만 답변 등록은 로그아웃 상태에서도 글을 작성할 수 있다.

물론 답변 작성 후 <저장하기>를 누르면 자동으로 로그인 화면으로 이동되므로 큰 문제는 아니지만 작성한 답변이 사라지는 문제가 있다.

 

작성한 글이 사라지는 문제를 해결하려면 로그아웃 상태에서는 아예 답변 작성을 못하게 막는 것이 좋을 것이다.

 

pybo/question_detail.html 파일을 다음과 같이 수정한다.

 

[파일명: projects\mysite\templates\pybo\question_detail.html]

(... 생략 ...)
<div class="mb-3">
    <label for="content" class="form-label">답변내용</label>
    <textarea {% if not user.is_authenticated %}disabled{% endif %}
              name="content" id="content" class="form-control" rows="10"></textarea>
</div>
<input type="submit" value="답변등록" class="btn btn-primary">
(... 생략 ...)

 

로그인 상태가 아닌 경우 textarea 태그에 disabled 속성을 적용하여 입력을 못하게 만들었다. 

{% if not user.is_authenticated %} 태그는 현재 사용자가 로그아웃 상태인지를 체크하는 태그이다.

 

다음은 로그아웃 상태에서 disabled가 적용된 화면이다.

 

 

disabled가 적용되어 답변을 등록할 수는 없지만 이 상태에서 "답변 등록" 버튼을 누르면 로그인 화면으로 이동하게 되고, 로그인을 수행하면 아무것도 나타나지 않는 상황이 발생한다.

Chrome 에서는 405 오류가 발생한다.

 

 

 

왜냐하면 로그인 시에 전달된 next 파라미터 때문에 로그인 후에 답변 등록 URL인 /answer/createGET 방식으로 호출되기 때문이다.

하지만 알다시피 답변 등록 시에는 POST가 아닌 경우 HttpResponseNotAllowed 오류를 발생하도록 코딩했다.

그래서 405 오류가 발생하는 것이다.

 

따라서 405 오류를 발생시키는 코드를 다음과 같이 수정해야 한다.

 

[파일명: projects\mysite\pybo\views.py]

(... 생략 ...)
# from django.http import HttpResponseNotAllowed
(... 생략 ...)

@login_required(login_url='common:login')
def answer_create(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    if request.method == 'POST':
        form = AnswerForm(request.POST)
        if form.is_valid():
            answer = form.save(commit=False)
            answer.author = request.user  # author 속성에 로그인 계정 저장
            answer.create_date = timezone.now()
            answer.question = question
            answer.save()
            return redirect('pybo:detail', question_id=question.id)
    else:
        form = AnswerForm()
    context = {'question': question, 'form': form}
    return render(request, 'pybo/question_detail.html', context)

(... 생략 ...)

 

이제 로그아웃 상태에서 "답변 등록" 버튼을 누르더라도 로그인 수행 후 405 오류가 발생하지 않고 상세화면으로 잘 돌아가는 것을 확인할 수 있다.

 


반응형