Django 06편 - polls app 구현하기 3편
템플릿에서 하드 코딩된 URL을 제거하자!
1
<li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>
이 부분은 만약 polls의 url을 바꾸게 되면 템플릿에 하드 코딩된 URL도 모두 바꿔야하기 때문에 유지보수에 좋지않다.
URL 네임스페이스 (urls.py)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
""" 실제 Django 프로젝트는 앱이 몇개라도 올 수 있습니다. Django는 이 앱들의 URL을 어떻게 구별해 낼까요? 예를 들어, polls 앱은 detail이라는 뷰를 가지고 있고, 동일한 프로젝트에 블로그를 위한 앱이 있을 수도 있습니다. Django가 {% url %} 템플릿태그를 사용할 때, 어떤 앱의 뷰에서 URL을 생성할지 알 수 있을까요? 정답은 URLconf에 이름공간(namespace)을 추가하는 것입니다. polls/urls.py 파일에 app_name을 추가하여 어플리케이션의 이름공간을 설정할 수 있습니다. """ from django.urls import path from apps.polls import views as polls_views app_name = 'polls' urlpatterns = [ path('', polls_views.index, name='index'), path('<int:question_id>/', polls_views.detail, name='detail'), path('<int:question_id>/results/', polls_views.results, name='results'), path('<int:question_id>/vote/', polls_views.vote, name='vote'), ]
네임스페이스로 나눠진 상세 뷰를 가리키도록 변경 (index.html)
1 2 3 4 5
<li> <a href="{% url 'polls:detail' question.id %}" >{{ question.question_text }}</a > </li>
최소한의 form을 구성해보자!
detail.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
<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>
- csrf_token은 csrf(Cross Site Request Forgeries)를 막기 위해 django에서 지원하는 보안 시스템이다.
- forloop.counter 는 for 태그가 반복을 한 횟수를 나타낸다.
admin 페이지에서 choice를 몇 개 정도 생성
결과 화면
vote view 구현하기
views.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
from django.http import HttpResponse, HttpResponseRedirect from django.shortcuts import get_object_or_404, render from django.urls import reverse from .models import Choice, Question def vote(request, question_id): question = get_object_or_404(Question, pk=question_id) try: selected_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() # Always return an HttpResponseRedirect after successfully dealing # with POST data. This prevents data from being posted twice if a # user hits the Back button. return HttpResponseRedirect(reverse('polls:results', args=(question.id,)))
- reverse() 함수는 뷰 함수에서 url을 하드코딩하지 않도록 도와준다. 제어를 전달하기 원하는 뷰의 이름을, URL패턴의 변수부분을 조합해서 해당 뷰를 가리킨다.
- 주석에서 표현했듯이, POST 데이터를 성공적으로 처리한 후에는 항상 HttpResponseRedirect를 반환해야한다. 이것은 django에만 국한된 것이 아니라 좋은 웹 개발의 관행이다.
- 만약 POST 자료에 choice 가 없으면, request.POST[‘choice’] 는 KeyError 가 일어난다. 위의 코드는 KeyError 를 체크하고, choice가 주어지지 않은 경우에는 에러 메시지와 함께 설문조사 폼을 다시보여준다.
이 vote view 구현에는 문제점이 있다. 먼저 데이터베이스에서 selected_choice 객체를 가져온 다음, votes 의 새 값을 계산하고 나서, 데이터베이스에 다시 저장한다. 만약 두 명 이상의 사용자가 정확히 같은 시간에 투표를 시도하면 votes의 조회값이 42라고 할 경우, 두 명의 사용자에게 새로운 값인 43이 계산되어 저장된다. 결과값이 44가 되어야 정상인대도 말이다. 이를 ‘경쟁상태(race condition)’라고 한다.
result view, template 구현하기
views.py
1 2 3 4 5 6 7
from django.shortcuts import get_object_or_404, render from .models import Question def results(request, question_id): question = get_object_or_404(Question, pk=question_id) return render(request, 'polls/results.html', {'question': question})
results.html
1 2 3 4 5 6 7 8 9 10 11 12
<h1>{{ question.question_text }}</h1> <ul> {% for choice in question.choice_set.all %} <li> {{ choice.choice_text }} -- {{ choice.votes }} vote{{ choice.votes|pluralize }} </li> {% endfor %} </ul> <a href="{% url 'polls:detail' question.id %}">Vote again?</a>
결과물
views.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
import logging from .models import Question, Choice from django.shortcuts import render, get_object_or_404 from django.http import HttpResponseRedirect from django.urls import reverse logger = logging.getLogger('debug') # Create your views here. def index(request): latest_question_list = Question.objects.order_by('-pub_date')[:5] context = {'latest_question_list': latest_question_list} return render(request, 'polls/index.html', context) def detail(request, question_id): question = get_object_or_404(Question, pk=question_id) return render(request, 'polls/detail.html', {'question': question}) def results(request, question_id): question = get_object_or_404(Question, pk=question_id) return render(request, 'polls/results.html', {'question': question}) def vote(request, question_id): question = get_object_or_404(Question, pk=question_id) try: selected_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() # Always return an HttpResponseRedirect after successfully dealing # with POST data. This prevents data from being posted twice if a # user hits the Back button. return HttpResponseRedirect(reverse('polls:results', args=(question.id,)))
urls.py
1 2 3 4 5 6 7 8 9 10
from django.urls import path from apps.polls import views as polls_views app_name = 'polls' urlpatterns = [ path('', polls_views.index, name='index'), path('<int:question_id>/', polls_views.detail, name='detail'), path('<int:question_id>/results/', polls_views.results, name='results'), path('<int:question_id>/vote/', polls_views.vote, name='vote'), ]
투표 후 result 페이지
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.