3. 튜토리얼 따라하기 - 설문조사 - 2

2019. 5. 27. 14:30파이썬 웹프로그래밍

3.8. 관리자 페이지 확인하기

관리자 페이지에 접근하기 위해서는 관리자 계정이 필요합니다. 다음 명령어를 이용하여 관리자 계정을 만듭니다.

python manage.py createsuperuser

관리자 계정을 생성하면 바로 서버를 실행합니다.

python manage.py runserver

서버를 동작시키고 웹 브라우저를 이용해 127.0.0.1:8000/admin/을 주소창에 입력해 관리자 페이지에 접속합시다. 로그인 화면에서는 방금 만든 관리자 계정을 이용해 로그인합니다.

로그인을 하면 관리자 메인 페이지를 볼 수 있습니다. 아직까지는 관리자 페이지에서 사용자 계정과 그룹만 관리할 수 있습니다.

관리자 페이지에서 [Question] 모델을 관리하려면 등록을 해야 합니다. admin.py 파일에 다음 코드를 입력합니다.

from django.contrib import admin
from .models import Question
admin.site.register(Question)

관리자 페이지에 모델을 등록하면 바로 확인할 수 있습니다.

새로운 투표를 등록하기 위해 [Question]를 클릭합니다. 클릭하면 투표 목록으로 이동할 수 있습니다.

투표 목록에는 아무 것도 없습니다. 아직 투표를 등록하지 않았기 때문입니다. 오른쪽 위에 있는 [ADD QUESTION] 버튼을 클릭해 등록 화면으로 이동합니다.

등록 화면에서 투표 제목을 입력하고 [Today] 버튼과 [Now] 버튼을 눌러 날짜와 시간을 입력합니다. 마지막으로 [SAVE] 버튼을 눌러 저장합니다.

투표를 추가하면 목혹 화면에 나타납니다. 이후 과정에서 이 목록도 보기 좋게 변경해 보도록 하겠습니다.

3.9. 여러 가지 뷰 추가하기

투표 앱의 메인 페이지는 작성했지만 우리는 여러 가지 뷰를 더 만들어야 합니다.

투표 목록 : 등록된 투표의 목록을 표시하고 상세 페이지로 이동하는 링크 제공
투표 상세 : 투표의 상세 항목을 보여줌
투표 기능 : 선택한 답변을 반영
투표 결과 : 선택한 답변을 반영 한 후 결과를 보여줌

위의 네가지 뷰를 만들어 보겠습니다. views.py에 다음 코드를 입력합니다.

def detail(request, question_id):
    return HttpResponse("You`re looking at question %s." % question_id)

def results(request, question_id):
    response= "You`re looking at the results of question %s."
    return HttpResponse(response % question_id)

def vote(request, question_id):
    return HttpResponse("You`re voting on question %s." % question_id)

각각의 뷰는 아직 특별한 기능은 없이 값만 출력할 것입니다. 이 뷰가 동작하도록 urls.py에 URL을 연결해 보겠습니다.

from django.urls import path
from . import views

urlpatterns=[
    # ex: /polls/
    path('', views.index, name='index'),
    # ex: /polls/5/
    path('<int:question_id>/', views.detail, name='detail'),
    # ex: /polls/5/results/
    path('<int:question_id>/results', views.results, name='results'),
    # ex: /polls/5/vote/
    path('<int:question_id>/vote', views.vote, name='vote')
]

추가한 3개의 뷰를 위한 URL을 연결했습니다. index뷰와 다르게 특이한 모양을 보여줍니다. 각 URL에 있는 <>는 변수를 의미합니다. 이 부분에 해당하는 값을 뷰에 인자로 전달합니다.

실제 동작되는 뷰를 만들기 위해 index View부터 수정하겠습니다.

from .models import Question

def index(request):
    latest_question_list=Question.objects.order_by('-pub_date')[:5]

    output=', '.join([q.question_text for q in latest_question_list])
    return HttpResponse(output)

뷰 코드를 수정하고 화면을 확인해 봅시다. 메인 화면에 투표 목록이 나타납니다. 아직은 한가지 뿐 이지만 추가하면 즉시 나타납니다.

기능이 있는 뷰를 만들었지만 MTV 패턴에 따르지 않는 형태입니다. 템플릿을 만들어 파이썬 코드와 HTML을 분리합시다.

우리는 HTML을 템플릿 파일에 입력할 것입니다. 템플릿 파일을 만들기 위해서 새로운 폴더를 만들겠습니다. polls 폴더를 마우스 오른쪽 버튼으로 클릭하고 [New -> Directory]를 선택해 폴더를 추가합니다.

폴더 이름은 [templates]라고 입력하고 [OK] 버튼을 클릭합니다. 그리고 templates폴더에 다시 [polls]라는 폴더를 추가합니다.

결과적으로 프로젝트 폴더에 polls/templates/polls 단계로 폴더가 생겼습니다. 이 폴더를 마우스 오른쪽 버튼으로 클릭하고 [New -> HTML File] 메뉴를 선택해 파일 이름이 [index]인 파일을 추가합니다.

파일을 추가하면 자동으로 html 확장자가 붙습니다. [body] 태그 안에 템플릿 코드를 입력합니다.

{% if latest_question_list %}
    <ul>
    {% for question in latest_question_list %} 
        <li><a href="/polls/{{question.id}}/">{{question.question_text}}</a></li>
    {% endfor %}
    </ul>
{% else %}
    <p>No polls are availavle.</p>
{% endif %}

코드를 추가하면 아래 그림과 같은 상태입니다.

만든 템플릿을 이용하도록 뷰를 변경하겠습니다. views.py에 템플릿을 불러오기 위해 [loader]를 임포트 하고 index뷰의 내용을 다음 코드를 참조해 변경합니다.

from djang.http import HttpResponse
from django.template import loader

drom .models import Question

def index(request):
    latest_question_list=Question.objects.order_by('-pub_date')[:5]
    template=loader.get_template('polls/index.html')
    context={
        'latest_question_list':latest_question_list,
    }
    return HttpResponse(template.render(context, request))

loader를 이용해 index.html를 불러오고 여기에 미리 만들어 둔 투표 목록을 context라는 변수를 이용해 전달합니다. 하지만 이런 절차가 약간 불편합니다. 그래서 장고에는 render라는 단축 함수가 존재합니다. 이 함수를 이용하도록 코드를 변경하겠습니다.

render는 views.py 파일 위쪽에 이미 임포트 되어 있습니다.

def index(request):
    latest_question_list=Question.objects.order_by('-pub_date')[:5]
    template=loader.get_template('polls/index.html')
    context={
        'latest_question_list':latest_question_list,
    }
    return render(request, 'polls/index.html', context)

render 메서드는 request와 템플릿 이름 그리고 사전형 객체를 인자로 받습니다. 여기서 사전형 객체는 템플릿에서 사용할 변수들입니다. 투표 메인 페이지에 다시 접근하면 아래와 같이 목록의 모양이 바뀐 것을 볼 수 있습니다.

이제 다른 뷰들에도 변경 사항을 추가해보겠습니다.

3.10. 404오류 일으키기

404는 웹 서비스에서 자주 볼 수 있는 오류입니다. 파일이 존재하지 않을 때 발생하는 오류인데 게시판 등 정보를 불러 오는 페이지의 경우 해당 데이터가 존재하지 않는다는 의미로 사용합니다. views.py에 detail 뷰를 수정하겠습니다. 다음 코드를 참조하여 detail 뷰를 변경합니다.

from django.http import Http404

def detail(request, question_id):
    try:
        question=Question.objects.get(pk=question_id)
    except Question.DoesNotExist:
        raise Http404("Question does not exist")
    return render(request, 'polls/detail.html', {'question':question})

Http404를 이용하면 상세 정보를 불러올 수 있는 토표 항목이 없을 경우 404 오류를 발생시킬 수 있습니다.

이전의 index 뷰와 마찬가지로 detail 뷰에서 detail.html이라는 템플릿을 사용하는 것을 볼 수 있습니다. detail.html 파일을 만들고 body 태크 안에 다음 코드를 입력합니다.

{{question}}

Http404를 처리할 때는 loader-render 관계처럼 단축 함수가 존재 합니다. 바로 get_object_or_404입니다. 이 함수를 사용해 detail 뷰를 수정합니다.

from django.shortcuts import render, get_object_or_404

def detail(request, question_id):
    question=get_object_or_404(Question, pk=question_id)
    return render(request, 'polls/detail.html', {'question':question})

render 옆에 get_objecct_of_404를 추가해 임포트 합니다. 그리고 detail 뷰에서는 try-except 구문을 없애고 get_object_or_404를 이용해 코드를 간소화했습니다.

마지막으로 detail.html 템플릿에 내용을 추가하겠습니다. body 태그 안의 내용을 다음 코드를 참조해 수정합니다.

<h1<{{question.question_text}}</h1>
<ul>
{% for choice in question.choice_set.all %}
<li>{{choice.choice_text}}</li>
</ul>

3.11. 하드 코딩된 URL 없애기

index.html 파일을 살펴보면 상세 페이지로 이동하기 위한 링크의 주소가 하드 코딩되어 있는 것을 알 수 있습니다.

<li><a href="/polls/{{question.id}}/">{{question.question_text}}</a></li>

위의 코드처럼 href 속성의 값을 직접 써주는 방식으로 해둘 경우 나중에 주소를 polls가 아닌 다른 형태로 바꾸려하면 html을 직접 다 열어서 변경해야 하는 불편함이 있습니다. 그래서 url 템플릿 태그를 사용해 하드 코딩된 URL을 없애 보겠습니다.

<li><a href="% url 'detail' question.id %"> question.question_text</a></li>

위의 코드 처럼 index.html을 일부 수정했습니다. url 템플릿 태그를 사용해 주소를 만들어 출력하는 방식입니다. url 템플릿 태그는 URL의 이름을 필수 인자로 전달 받습니다. detail이라는 이름을 가진 URL 형식을 찾아서 URL을 만들어 출력하는 것입니다. 해당 이름을 가진 URL은 urls.py 전체를 검색해 찾습니다.

3.12. URL 네임스페이스(Namespace) 설정하기

네임스페이스는 프로그래밍 용어 중 하나입니다. 분리된 경로는 만드는 개념인데 예를 들어 detail이라는 주소 이름을 가진 뷰가 polls에도 있고 다른 앱에도 있을 경우 장고는 어느 뷰의 URL을 만들지 알 수가 없습니다. 그래서 이런 경우 네임스페이스를 설정해 각각의 뷰가 어느 앱에 속한 것인지 구분하도록 할 수 있습니다.  필수로 설정할 필요는 없지만 프로젝트가 복잡 해질수록 네임스페이스가 있는 것이 편리합니다.

네임스페이스는 urls.py에 설정합니다.

app_name='polls'

urls.py에 [app_name]이라는 변수를 추가하고 이름을 설정하면 네임스페이스 설정은 끝입니다. 이 네임스페이스를 사용하기 위해서 템플릿에도 수정을 해야 합니다.

<li><a href="{% url 'polls:detail' question.id %}">{{question.question_text}}</a></li>

URL 이름은 detail 앞에 [polls:]라고 네임스페이즈를 추가하면 사용할 수 있습니다. 다음은 투표기능이 동작하도록 뷰를 변경해보겠습니다.

3.13. 간단한 폼 만들기

투표 기능이 동작하려면 detail.html을 수정하고 vote 뷰에도 기능을 추가해야 합니다. detail.html을 다음 코드를 참조하여 수정합니다.

<h1>{{question.question_text}}</h1>
{% if error_message %}<p><strong>{{error_message}}</strong></p>{% endif %}

<form action="{% url 'polls:vote' question.id %}" method="post">
{% csrf_token %}
{% for choice in question.choice_set.all %}
    <input type="radio" name="choice" id="choice{{forloop.counter}}" value={{choice.id}}">
    <label for="choice{{forloop.counter}}">{{choice.choice_text}}</label><br>
{% endfor %}
<input type="submit" value="vote">
</form>

변경 점을 확인해보겠습니다. [form] 태그를 사용해서 사용자가 답변 항목을 선택하고 전달할 수 있도록 만들었습니다. 사용자가 선택한 항목의 번호를 vote 뷰를 전달하도록 action 속성에 vote 뷰를 전달하도록 action 속성에 vote UTL이 출력되게 url 템플릿 태그를 사용했습니다. method 속성에 써있는 post는 HTTP메서드 중에 하나이며 서버로 정보를 전달할 때 사용하는 일반적인 방법입니다. forloop.counter는 템플릿 문법에서 제공하는 기능 중 하나로 반복문의 반복 획수를 출력해주는 기능을 합니다. 여기서 선택한 답변의 번호를 vote뷰에 [choice=번호] 형태로 전달합니다. csrf_token은 CSRF 공격을 막기 위한 수단 중 하나입니다. 간단히 말해서 방금 서버로 들어온 요청이 사이트 내부에서 온 것이 맞는지 확인하는 용도로 csrf_token의 값을 사용합니다. detail.html에서 만들어진 정보를 받을 vote 뷰를 수정하겠습니다. 다음 코드를 참조하여 views.py에 vote 뷰를 수정합니다.

from django.http import HttpResponse, HttpResponseRedirect
from django.urls import reverse
from .models import Question, Choice

def vote(request, question_id):
    question=get_object_or_404(Question, pk=question_id)
    try:
        seleted_choice=question.choice_set.get(pk=request.POST['choice'])
    except (KeyError, Choice.DoesNotExist):
        # Redisplay the question voting form.
        return render(request, 'polls/detail.html', {
            'question':question,
            error_message':"You didn`t select a choice.",
        })
    else:
        selected_choice.votes+=1
        selected_choice.save()
        return HttpResponseRedirect(reverse('polls:results',args=(question.id,)))

전체 과정은 간단합니다. request.POST[변수이름]을 통해 전달받은 변수의 값들을 확인할 수있습니다. 이 때 전달되는 값은 항상 문자열이기 때문에 문자열 이라는 사실을 기억하고 다뤄야 합니다. 전달받은 답변이 해당 투표 항목에 있는지 확인하고 없다면 다시 상세 페이지로 이동합니다. 이 대 답변을 선택하지 않았다는 오류 메시지고 같이 전달합니다. 반대로 제대로된 답변을 선택한 것이라면 해당 답변을 답변수를 1증가 시키고 결과 화면으로 이동합니다.

결과를 출력하는 views.py에 result 뷰도 변경해 봅시다.

def results(request, qustion_id):
    question=get_object_or_404(Question, pk=question_id)
    return render(request, 'polls/results.html', {'question':question})

그리고 results.html도 만들어 줍니다.

<h1>{{question.question_text}}</h1>
    <ul>
{% for choice in question.choice_set.all %}
    <li>{{ choice.choice_text}}--{{choice.vote}} vote{{choice.votes|pluralize}}</li>
{% endfor %}
</ul>
<a href="{% url 'polls:detail' question.id %}">Vote again?</a>

results.html은 각 답변 항목과 투표 수를 한꺼번에 보여 줍니다.