첫 번째 장고 앱 작성하기, part 4
Web-poll 애플리케이션을 제작하고 있으며, 간단한 폼 처리와 소스코드를 줄이는 데 중점을 두는 것입니다.
간단한 폼 쓰기
앞장의 투표 상세 템플릿(polls/detail.html)을 수정해 템플릿에 HTML <form> 요소를 포함 시켜보겠습니다.
<form action="{% url 'polls:vote' question.id %}" method="post">
{% csrf_token %}
<fieldset>
<legend><h1>{{ question.question_text }}</h1></legend>
{% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}
{% 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 %}
</fieldset>
<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는 그것으로부터 보호하는 유용한 시스템을 가지고 있다. 간단히 말하자면, 내부 URL을 대상으로 하는 모든 POST 양식은 {% csrf_token %} 템플릿 태그를 사용해야 한다.
이제 제출된 데이터를 처리하고 그 데이터로 무언가를 수행하는 Django 뷰를 작성하겠습니다.
저번 포스팅에서 설문조사 어플리케이션을 위해 아래에 나와있는 코드를 포함하는 URLconf 를 만들었습니다:
path("<int:question_id>/vote/", views.vote, name="vote"),
vote() 함수를 가상으로 만들었으며. 실제로 구현을 진행해보겠습니다. polls/views.py 에 다음을 추가해보겠습니다.
from django.db.models import F
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 = F("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가 주어지지 않은 경우에는 에러 메시지와 함께 설문조사 폼을 다시보여주게 됩니다.
F("votes") + 1 데이터베이스에 투표 수를 1만큼 늘리도록 지시합니다.
https://docs.djangoproject.com/ko/5.1/ref/models/expressions/#avoiding-race-conditions-using-f
설문지의 수가 증가한 이후에, 코드는 일반 HttpResponse 가 아닌 HttpResponseRedirect 를 반환하고, HttpResponseRedirect 는 하나의 인수를 받습니다
그 인수는 사용자가 재전송될 URL 입니다.
위의 Python 주석에서 지적한 바와 같이 POST 데이터를 성공적으로 처리 한 후에는 항상 HttpResponseRedirect 를 반환해야 합니다. 이 팁은 Django에만 국한된 것이 아니라 일반적으로 좋은 웹 개발 관행입니다.
이 예제에서 HttpResponseRedirect 생성자 안에서 reverse() 함수를 사용하고 있는데, 이 함수는 뷰 함수에서 URL을 하드코딩하지 않도록 도와주는데 제어를 전달하기 원하는 뷰의 이름을, URL패턴의 변수부분을 조합해서 해당 뷰를 가리키고 있습니다.
여기서 우리는 전포스팅에서 설정했던 URLconf를 사용하였으며, 이 reverse() 호출은 아래와 같은 문자열을 반환할 것입니다.
"/polls/3/results/"
여기서 3은 question.id 값입니다. 이렇게 리디렉션된 URL은 최종 페이지를 표시하기 위해 results 뷰를 호출합니다.
전 포스팅에서 언급했듯이, request 는 HttpRequest 개체로, 이 개체에 대해 더 알고싶음 request와 response 문서를 참고해야한다.
https://docs.djangoproject.com/ko/5.1/ref/request-response/
이제 다른 사람이 설문조사에 설문을 하고 난 뒤에 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})
전장의 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/ 페이지로 가서, 투표를 해보면 투표를 할 때마다 값이 반영된 결과 페이지를 볼 수 있습니다.
만약 당신이 설문지를 선택하지 않고 폼을 전송했다면, 오류 메시지를 보게 될 것입니다.
제너릭 뷰 사용하기: 적은 코드가 더 좋다.!!!
detail() (from Tutorial 3)과 results()``의 view들은 매우 간단하며, 위에서 언급한 바와 같이 중복됩니다. 여론조사 목록을 보여주는 ``index() view도 비슷합니다.
이러한 보기는 URL에 전달된 매개 변수에 따라 데이터베이스에서 데이터를 가져오고, 템플릿을 로드하고, 렌더링된 템플릿을 반환하는 기본 웹 개발의 일반적인 경우를 나타냅니다.
이것이 매우 흔하기 때문에, Django는 “generic views” 시스템이라고 불리는 단축키를 제공해준다.
일반 보기는 앱을 작성하기 위해 파이썬 코드를 작성할 필요조차 없을 정도로 공통 패턴을 추상화하는데, 예를 들어, ListView와 DetailView 일반 보기는 각각 "객체 목록 표시"와 "특정 유형의 객체에 대한 세부 페이지 표시" 개념을 추상화합니다.
설문조사 애플리케이션을 제너릭 뷰 시스템으로 변환해서 우리의 코드를 많이 삭제하도록 할건데 이러한 전환을 하려면 다음과 같은 몇 가지 단계만 거치면 됩니다:
- 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>로 변경되었음을 유의해야 합니다.
이는 DetailView 일반 보기를 사용하여 DetailView와 결과 보기를 대체할거고, URL에서 캡처한 기본 키 값이 "pk"로 불릴 것으로 예상하기 때문에 필요합니다.
views 수정
다음으로 이전의 index, detail, results뷰를 제거하고 장고의 일반적인 뷰를 대신 사용합니다.
그렇게하려면 polls/views.py 파일을 열고 다음과 같이 변경해야 합니다.
from django.db.models import F
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.
...
이제 각 일반 뷰는 해당 뷰가 어떤 모델에 작용할지 알아야 하는데 이는 모델 속성(이 예제에서는 모델 = 세부 정보 보기 및 결과 보기에 대한 질문)을 사용하거나 겟_queryset () 메서드(IndexView에 표시됨)를 정의하여 제공되고 있습니다.
기본적으로 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에게 지시하는 것이 훨씬 쉽습니다.
서버를 실행하고 제너릭 뷰를 기반으로한 새 설문조사 앱을 사용할 수 있는데
제너릭 뷰에 대한 자세한 내용은 제너릭 뷰 문서를 참조해서 공부할 수 있습니다.
https://docs.djangoproject.com/ko/5.1/topics/class-based-views/
폼 및 제너릭 뷰가 마음에 들면, 이 설문조사 앱의 테스트에 대해 다음 포스팅을 통해 배워보겠습니다.
'PYTHON-BACK' 카테고리의 다른 글
Django_tutorial_var5 (1) | 2025.01.31 |
---|---|
Django_tutorial_var3 (0) | 2025.01.21 |
Django_tutorial_var2 (1) | 2025.01.20 |
Django_tutorial_var1 (0) | 2025.01.20 |
# 24.11.28_영화추천 사이트 제작_협업 기반 필터링 (0) | 2024.11.28 |