첫 번째 장고 앱 작성하기, part 4¶
이 튜토리얼은 Tutorial 3 에서 이어집니다. 웹-설문조사 어플리케이션을 다루고 있고, 간단한 폼 처리와 소스코드를 줄여가는데 중점을 둘것입니다.
간단한 폼 만들기¶
Let's update our poll detail template ("polls/detail.html") from the last
tutorial, so that the template contains an HTML <form>
element:
<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>
간략하게 설명하면:
- 위의 템플릿은 각 질문 선택 항목에 대한 라디오 버튼을 표시합니다. 각 라디오 버튼의
value
는 연관된 질문 선택 항목의 ID입니다. 각 라디오 버튼의name
은"choice"
입니다. 즉, 누군가가 라디오 버튼 중 하나를 선택하여 폼을 제출하면, POST 데이터 인choice=#
을 보낼 것입니다. 여기서 #은 선택한 항목의 ID입니다. 이것은 HTML 폼의 기본 개념입니다. - 폼의
action
을{% url 'polls:vote' question.id %}
로 설정하고,method="post"
로 설정하였습니다. 이 폼을 전송하는 행위는 서버측 자료를 변경할 것이므로,method="post"
(method="get"
와 반대로) 를 사용하는 것은 매우 중요합니다. 서버 측 자료를 변경하는 폼을 작성할 때마다,method="post"
를 사용하세요. 이 팁은 Django에만 국한되지 않습니다. 이것은 웹개발시의 권장사항 입니다. forloop.counter
는for
태그가 반복을 한 횟수를 나타냅니다.- 우리는 POST 폼(자료를 수정하는 효과를 가진)을 만들고 있으므로, 사이트 간 요청 위조 (Cross Site Request Forgeries)에 대해 고민해야합니다. 고맙게도, Django는 사이트 간 요청 위조(CSRF)에 대항하기위한 사용하기 쉬운 시스템을 가지고 있기 때문에, 너무 심각하게 고민할 필요가 없습니다. 간단히 말하면, 내부 URL들을 향하는 모든 POST 폼에 템플릿 태그
{% csrf_token %}
를 사용하면됩니다.
이제 제출된 데이터를 처리하고 그 데이터로 무언가를 수행하는 Django 뷰를 작성하겠습니다. 튜토리얼 3 에서 설문조사 어플리케이션을 위해 아래에 나와있는 코드를 포함하는 URLconf 를 만들었습니다:
path('<int:question_id>/vote/', views.vote, name='vote'),
또, 우리는 vote()
함수를 가상으로 만들었습니다. 실제로 구현을 해봅시다. polls/views.py
에 다음을 추가합시다:
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,)))
위 코드는 이 튜토리얼에서 아직 다루지 않은 몇 가지를 포함하고 있습니다:
request.POST
는 키로 전송된 자료에 접근할 수 있도록 해주는 사전과 같은 객체입니다. 이 경우,request.POST['choice']
는 선택된 설문의 ID를 문자열로 반환합니다.request.POST
의 값은 항상 문자열들입니다.Django는 같은 방법으로 GET 자료에 접근하기 위해
request.GET
를 제공합니다 -- 그러나 POST 요청을 통해서만 자료가 수정되게하기 위해서, 명시적으로 코드에request.POST
를 사용하고 있습니다.만약 POST 자료에
choice
가 없으면,request.POST['choice']
는KeyError
가 일어납니다. 위의 코드는KeyError
를 체크하고, choice가 주어지지 않은 경우에는 에러 메시지와 함께 설문조사 폼을 다시보여줍니다.설문지의 수가 증가한 이후에, 코드는 일반
HttpResponse
가 아닌HttpResponseRedirect
를 반환하고,HttpResponseRedirect
는 하나의 인수를 받습니다: 그 인수는 사용자가 재전송될 URL 입니다. (이 경우에 우리가 URL을 어떻게 구성하는지 다음 항목을 보세요).위의 파이썬 주석이 지적했듯이, POST 데이터를 성공적으로 처리 한 후에는 항상
HttpResponseRedirect
를 반환해야합니다. 이 팁은 Django에만 국한되는것이 아닌 웹개발의 권장사항입니다.우리는 이 예제에서
HttpResponseRedirect
생성자 안에서reverse()
함수를 사용하고 있습니다. 이 함수는 뷰 함수에서 URL을 하드코딩하지 않도록 도와줍니다. 제어를 전달하기 원하는 뷰의 이름을, URL패턴의 변수부분을 조합해서 해당 뷰를 가리킵니다. 여기서 우리는 튜토리얼 3 에서 설정했던 URLconf를 사용하였으며, 이reverse()
호출은 아래와 같은 문자열을 반환할 것입니다'/polls/3/results/'
여기서
3
은question.id
의 값입니다. 이렇게 리디렉션된 URL은 최종 페이지를 표시하기 위해'results'
뷰를 호출합니다.
튜토리얼 3 에서 언급했듯이, request
는 HttpRequest
개체입니다. HttpRequest
개체에 대해 더 알고 싶다면 request and response documentation 을 참고하세요.
어떤 이가 설문조사에 설문을 하고난 뒤에는, vote()
뷰는 설문조사 결과 페이지로 리다이렉트합니다. 그 뷰를 작성해봅시다:
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})
Tutorial 3 의 detail()
뷰와 거의 동일합니다. 템플릿 이름만 다릅니다. 나중에 이 중복을 수정할 겁니다.
이제, polls/results.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>
이제, 웹 브라우저에서 /polls/1/
페이지로 가서, 투표를 해보세요. 당신이 투표를 할 때마다 값이 반영된 결과 페이지를 볼 수 있을 것입니다. 만약 당신이 설문지를 선택하지 않고 폼을 전송했다면, 오류 메시지를 보게 될 것입니다.
주석
우리의 vote()
뷰에는 작은 문제가 있습니다. 먼저 데이터베이스에서 selected_choice
객체를 가져온 다음, votes
의 새 값을 계산하고 나서, 데이터베이스에 다시 저장합니다. 만약 여러분의 웹사이트에 두 명의 사용자가 정확하게 같은 시간 에 투표를 할려고 시도할 경우, 잘못될 수 있습니다. votes
의 조회값이 42라고 할 경우, 두 명의 사용자에게 새로운 값인 43이 계산 되고, 저장됩니다. 그러나 44가 되야되겠죠.
이를 경쟁 상태 라 합니다. 이 문제를 해결할 수 있는 방법을 알아보려면 Avoiding race conditions using F() 를 참고하세요.
제너릭 뷰 사용하기: 적은 코드가 더 좋습니다.¶
detail()
(Tutorial 3에서 만든)과 results()
뷰는 매우 간단합니다. -- 그리고 위에서 말한것처럼 중복됩니다. polls 리스트를 보여주는 index()
뷰와도 비슷합니다.
이러한 뷰는 URL에서 전달 된 매개 변수에 따라 데이터베이스에서 데이터를 가져 오는 것과 템플릿을 로드하고 렌더링 된 템플릿을 리턴하는 기본 웹 개발의 일반적인 경우를 나타냅니다. Django는 이런 매우 일반적인 경우를 위해 "제너릭 뷰"시스템이라는 지름길을 제공합니다.
제너릭 뷰는 일반적인 패턴을 추상화하여 앱을 작성하기 위해 Python 코드를 작성하지 않아도됩니다.
우리의 설문조사 애플리케이션을 제너릭 뷰 시스템을 사용하도록 변환하여 우리의 코드를 삭제할 수 있도록 합시다. 이러한 전환을 하려면 그저 다음과 같은 몇 단계 만 거치면 됩니다:
- URLconf를 변환하십시오.
- 불필요한 오래된보기 중 일부를 삭제하십시오.
- Django의 제너릭 뷰를 기반으로 새로운 뷰를 도입하십시오.
자세한 내용은 계속 읽어 나가십시오.
왜 코드 셔플인가?
일반적으로 Django 앱을 작성할 때 일반 뷰가 문제에 적합한 지 여부를 평가할 것이며 코드를 중간에서 다시 리팩토링하지 않고 처음부터 사용하게됩니다. 그러나 이 튜토리얼은 의도적으로 현재까지 핵심 개념에 초점을 맞추기 위해 "어려운 방법"으로 뷰를 작성하는 데 중점을 두었습니다.
계산기를 사용하기 전에 기본 수학을 알아야합니다.
URLconf 수정¶
먼저, polls/urls.py
URLconf를 열어 다음과 같이 변경하십시오:
from django.urls import path
from . import views
app_name = 'polls'
urlpatterns = [
path('', views.IndexView.as_view(), name='index'),
path('<int:pk>/', views.DetailView.as_view(), name='detail'),
path('<int:pk>/results/', views.ResultsView.as_view(), name='results'),
path('<int:question_id>/vote/', views.vote, name='vote'),
]
두 번째와 세 번째 패턴의 경로 문자열에서 일치하는 패턴들의 이름이 <question_id>
에서 <pk>
로 변경되었습니다.
조회수 수정¶
다음으로 이전의 index
, detail
, results
뷰를 제거하고 장고의 일반적인 뷰를 대신 사용하겠습니다. 그렇게하려면 polls/views.py
파일을 열고 다음과 같이 변경하십시오:
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
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.
ListView
와 DetailView
의 두 가지 제너릭 뷰를 사용하고 있습니다. 이 두보기는 각각 "개체 목록 표시" 및 "특정 개체 유형에 대한 세부 정보 페이지 표시" 개념을 추상화합니다.
- 각 제너릭 뷰는 어떤 모델이 적용될 것인지를 알아야합니다. 이것은
model
속성을 사용하여 제공됩니다. DetailView
제너릭 뷰는 URL에서 캡쳐 된 기본 키 값이``"pk"\ 라고 기대하기 때문에 ``question_id
를 제너릭 뷰를 위해pk
로 변경합니다.
기본적으로 DetailView
제너릭 뷰는 <app name>/<model name>_detail.html
템플릿을 사용합니다. 우리의 경우에는 "polls/question_detail.html"
템플릿을 사용할 것입니다. template_name
속성은 Django에게 자동 생성 된 기본 템플릿 이름 대신에 특정 템플릿 이름을 사용하도록 알려주기 위해 사용됩니다. results
리스트 뷰에 대해서 template_name
을 지정합니다 - 결과 뷰와 상세 뷰가 렌더링 될 때 서로 다른 모습을 갖도록합니다. 이들이 둘다 동일한 DetailView
를 사용하고 있더라도 말이지요.
마찬가지로, ListView
제네릭 뷰는 <app name>/<model name>_list.html
템플릿을 기본으로 사용합니다; 이미 있는 "polls/index.html"
템플릿을 사용하기 위해 ListView
에 template_name
를 전달했습니다.
이 튜토리얼의 이전 부분에서 템플릿에는 question
및 latest_question_list
컨텍스트 변수가 포함 된 컨텍스트가 제공되었습니다. DetailView
의 경우, question
변수가 자동으로 제공됩니다 - Django 모델 (Question
)을 사용하기 때문에 Django는 컨텍스트 변수의 적절한 이름을 결정할 수 있습니다. 그러나 ListView의 경우 자동 생성 된 컨텍스트 변수는 question_list
입니다. 이것을 덮어 쓰려면 context_object_name
속성을 제공하고, 대신에 latest_question_list
를 사용하도록 지정하십시오. 다른 접근 방법으로, 템플릿을 새로운 기본 컨텍스트 변수와 일치하도록 변경할 수 있습니다. 그러나 원하는 변수를 사용하도록 Django에 지시하는 것이 훨씬 쉽습니다.
서버를 실행하고 제너릭 뷰를 기반으로한 새 설문조사 앱을 사용하십시오.
제너릭 뷰에 대한 자세한 내용은 제너릭 뷰 문서를 참조하십시오.
폼 및 제너릭 뷰가 마음에 들면, 이 설문조사 앱의 테스트에 대해 튜토리얼의 5장을 읽어 배워 보기 바랍니다.