backend/django

django tutorial: form, generic view 사용 (part4)

seul chan 2017. 3. 3. 15:08

Write a simple form


polls.detail.html에 간단한 HTML <form>문을 추가하였다.

--------------

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

--------------


- 위의 템플릿은 각각의 질문 choice에 radio button을 보여준다. 

# <input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}" />

각각의 버튼 값은 질문의 choice id 값과 연동된다. 

=> 누군가 하나의 radio 버튼을 누르고 submit을 하면 POST data choice=#id로 보내진다. 

(html 기본 form문의 방식)

- form의 action은 다음과 같이 지정하였다.

# <form action="{% url 'polls:vote' question.id %}" method="post">

{% csrf_token %}

method는 'post' 방식 => data server-side를 대체하기 위해서는 post 방식을 사용하여야 한다.

- forloop.counter는 얼마나 많은 for 티그가 loop 되는지를 나타냄

- POST form을 만들었기 때문에 Cross Site Request Forgeries를 신경써야함(??)

장고가 매우 쉬운 보호 시스템 제공

=> 모든 POST 폼들은 {% csrf_token %} 템플릿 태그로 쓰임(??)




이전 part 3에서 polls/urls.py에 vote url을 추가했었다.

url(r'^(?P<question_id>[0-9]+)/vote/$', views.vote, name='vote'),


이번에는 vote()의 실제 버전을 만들어보자

polls/views.py에서 

-------

from django.shortcuts import get_object_or_404, render

from django.http import HttpResponseRedirect, HttpResponse

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,)))

-------

vote 기능만 바꿔준다. 


튜토리얼에서 다루지 않았던 기능이 몇 개 추가되는데

- request.POST: dict과 비슷한 object- key name으로 데이터에 접근할 수 있게 해줌. 

=> request.POST['choice']는 선택된 choice의 ID를 string으로 나타냄

또한 장고는 request.GET도 같은 방식으로 제공

- request.POST['choice'] => POST 데이터가 제공되지 않았다면 KeyError 발생

위 코드는 choice가 없으면 keyerror를 체크하고 메세지를 표시해줌

- choice가 카운트 된 이후, 코드는 HttpResonse 대신에 HttpResponseRedirect를 리턴

HttpResponseRedirect는 한개의 argument를 받음: 사용저가 redirected할 URL

tip: POST 데이터를 성공적으로 다룬 뒤에는 항상 HttpResponseRedirect를 사용해줘야한다! 

- HttpResponseRedirect constructor에서 reverse() 함수를 사용

=> 하드코드 사용을 피해줌

=> view를 가르키는 이름을 주면 됨 (??)

(We are using the reverse() function in the HttpResponseRedirect constructor in this example. This function helps avoid having to hardcode a URL in the view function. It is given the name of the view that we want to pass control to and the variable portion of the URL pattern that points to that view. In this case, using the URLconf we set up in Tutorial 3, this reverse() call will return a string like)


이번 예시에서는 

reverse('polls:result', args=(question.id,))는

'polls/1/results/' 가 불러온다. (만약 3이 question.id라면)

이 재설정된 URL은 results view를 페이지에 표시해준다.


누군가 질문에 투표하면, vote() 뷰는 results page를 표시해준다. 

polls/views.py의 results 함수를 수정하자

--------

from django.shortcuts import get_object_or_404, render


def results(request, question_id):

    question = get_object_or_404(Question, pk=question_id)

    return render(request, 'polls/results.html', {'question': question})

--------

이는 튜토리얼 3에서의 detail() 뷰와 거의 동일하다. 다른건 템플릿 이름 (results.html) 뿐이다


이제 polls/results.html 템플릿을 만들자


polls/templates/polls/result.html에

----------

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

----------

를 추가해준다. 


이제 localhost:8000/polls/1/ 에 들어가서 투표를 실제로 해보자.

업데이트된 results 페이지가 나와야 한다. 

만약 아무런 선택 없이 submit을 하면 에러가 뜬다.


Note

The code for our vote() view does have a small problem. It first gets the selected_choice object from the database, then computes the new value of votes, and then saves it back to the database. If two users of your website try to vote at exactly the same time, this might go wrong: The same value, let’s say 42, will be retrieved for votes. Then, for both users the new value of 43 is computed and saved, but 44 would be the expected value.


This is called a race condition. If you are interested, you can read Avoiding race conditions using F() to learn how you can solve this issue.

(https://docs.djangoproject.com/en/1.10/ref/models/expressions/#avoiding-race-conditions-using-f)

 


Use generic views: Less code is better
    
Tutorial 3에서 만들었던 detail(), results(), index() view는 쓸모없는 코드이다. 
이 뷰들은 기본적인 web development의 흔한 케이스이다: data를 데이터베이스에서 가져와 템플릿을 로딩하여 부러오는 걳.
=> 매우 흔하기 때문에 장고는 'generic views' 시스템이라는 shortcut을 제공한다.

poll app을 generic views system을 사용해 바꾸어 보자.
1. URLconf를 변환
2. 오래되고, 쓸모없는 views 삭제
3. 장고 generic views의 기초를 소개

*Amend URLconf
polls/urls.py를 다음과 같이 변경
--------------
from django.conf.urls import url

from . import views

app_name = 'polls'
urlpatterns = [
    url(r'^$', views.IndexView.as_view(), name='index'),
    url(r'^(?P<pk>[0-9]+)/$', views.DetailView.as_view(), name='detail'),
    url(r'^(?P<pk>[0-9]+)/results/$', views.ResultsView.as_view(), name='results'),
    url(r'^(?P<question_id>[0-9]+)/vote/$', views.vote, name='vote'),
]
--------------
1, 2번째 패선은 <question_id>에서 <pk>로 변경된다

*Amend view
다음은 index, detail, results views를 지우고 장고의 generic views를 사용한다.
이를 위해서 polls/views.py를 열고
-------------
from django.shortcuts import get_object_or_404, render
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.views import generic

from .models import Choice, Question


class IndexView(generic.ListView):
    template_name = 'polls/index.html'
    context_object_name = 'latest_question_list'

    def get_queryset(self):
        """Return the last five published questions."""
        return Question.objects.order_by('-pub_date')[:5]


class DetailView(generic.DetailView):
    model = Question
    template_name = 'polls/detail.html'


class ResultsView(generic.DetailView):
    model = Question
    template_name = 'polls/results.html'


def vote(request, question_id):
    ... # same as above, no changes needed.
-----------
코드를 이렇게 수정한다. 

여기서는 두가지의 generic viw를 사용한다: ListView & DetailView
-Listview: object들의 리스트를 보여준다
-DetailView: 특정 타입의 object의 디테일 페이지를 보여주다.
-DetailView generic view는 프라이머리 키 값으로 'pk' URL을 받으므로 question_id에서 pk로 변경하였다.
-defaul 값으로 DetailView는 <app name>/<model name>_detail.html 템플릿을 사용한다. 
temlplate_name 속성은 기본 템플릿 이름이 아니라 특정 템플릿명을 사용할 때 쓴다. 
여기서는 polls/detail.html, polls/results.html을 사용하였다. 

이와 비슷하게 ListView generic view도 기본 템플릿으로 <app name>/<model name>_list.hmlt을 사용한다.
(여기서는 polls/question_list.html) 
이도 DetailView와 마찬가지로 template_name이라는 속성으로 polls/index.html 템플릿을 사용하였다.


이제 $python manage.py runserver를 통해 generic view가 detail, result 페이지에서 동작하는 것을 볼 수 있다.