당신은 멋쟁이, 우리는 장고쟁이~

0%

Writing your first Django app, part4 - 1편

최소한의 폼 작성하기



튜토리얼 part3 에 이어서, 계속 진행 합니다. Web-poll 어플리케이션을 이어서 진행하고.


part4 에서는 form 프로세싱과 코드양을 줄이는데 집중합니다.


polls/detail.html 수정하기


지난 포스팅까지 작성한

디테일 템플릿 polls/detail.html 파일에

html <form> 요소를 추가하여 업데이트 해줍니다.


1
2
3
4
5
6
7
8
9
10
11
12
<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>


빠른 설명:


  • 위 템플릿은 각각의 Question 의 Choice 를 라디오 버튼으로 표시해 줍니다. 각 라디오 버튼의 Value 값은 질문에 붙어 있는 Choice 의 ID 가 됩니다. 각 라디오 버튼의 이름은 choice 가 됩니다. 이것이 의미하는 바는, 누군가가 하나의 라디오 버튼을 누르고 폼을 제출하면, 폼은 POST데이터 choice=# 을 전송합니다. #은 선택된 choice 의 ID 값입니다.

  • form 에 action 은 {% url 'polls:vote' question.id %} 를 설정해줍니다. 
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

    form의 method 는 POST 로 설정해 줍니다. Form 의 method 를 POST 로 설정해 주는것은 중요합니다. 왜냐하면, 이 form 을 제출한다는것은 서버쪽의 데이터를 변경할것이기 때문입니다. 언제든 서버쪽 데이터를 바꾸고 싶으면, method="POST" 를 사용하면 됩니다. Django 에만 국한된 이야기가 아니라, 전반적으로 좋은 웹개발을 위한 연습이 됩니다.

    - forloop.counter 는 for 태그가 반복문의 몇번을 수행했는지 표시합니다

    - POST 폼을 생성하고 있기 때문에, 우리는 `Cross Site Request Forgeries`. 즉, 사이트간 요청 위조에 대해서 걱정해야 합니다. 감사하게도, 이것에 대해서 너무 걱정할 필요가 없습니다. Django 는 이것에 대한 아주 요용한 시스템을 갖추고 있습니다.

    ```django
    내부 URL 을 타겟으로 하는 모든 POST 폼들은 {% csrf_token %} 템플릿 테그를 사용해 줍니다.


CSRF (사이트간 요청 위조)


1
내부 URL 을 타겟으로 하는 모든 POST 폼들은 {% csrf_token %} 템플릿 테그를 사용해 줍니다.

Cross-site Request Forgery (CSRF, XSRF) 는,

웹사이트 취약점 공격의 하나로, 사용자가 자신의 의지와는 무관하게

공격자가 의도한 행위 (수정, 삭제, 등록) 을 웹사이트에 요청하게 하는 공격을 의미합니다.

출처 : 위키백과


URL 설정



이제 form 이 수행하는 Django view 를 수정해줘야 할 차례입니다. 전송된 데이터를 가지고 무언가를 수행할 django view 를 작성해 줍시다. part3 에서, vote 를 처리하는 url 설정을 해주었었습니다.



1
path('<int:question_id>/vote/', views.vote, name='vote'),


polls/views.py 수정하기


위 url 은 vote 함수를 불러옵니다. part3 에서, 이것을 처리하는 예시 view 를 작성했었는데,

이 polls/views.py 파일에서 vote() 함수를 아래와 같이 업데이트 해줍니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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):
# 투표 폼을 다시 출력
return render(request, 'polls/detail.html',
{ 'question': question,
'error_message': "You didn't select a choice",
})
else:
selected_choice.votes += 1
selected_choice.save()
# 항상 POST 데이터를 성공적으로 다루었을때는
# HttpResponseRedirect 를 반환해 줍니다
# 이것으로 사용자가 데이터를 뒤로가기 버튼을 눌러서 데이터를 두번 전송하는것을 방지합니다.
return HttpResponseRedirect(reverse('polls:results', args=(question.id,)))


위의 예시에는, 아직 우리가 공부하지 않은 몇가지가 있습니다.


  • request.POST는 사전 형태의 객체로 전송된 데이터를 key 이름으로 접근할수 있게 해줍니다. 해당 예시에서는, request.POST['choie']는 선택된 choice 의 ID 를 문자열 형태로 반환합니다. request.POST 값은 언제나 문자열로 되어 있습니다.

  • Django 는 request.GET도 지원합니다. GET 데이터를 접근할때 사용합니다. 하지만, 튜토리얼에서는, 명시적으로 request.POST를 사용합니다. 데이터를 바꿀때에는 POST 콜을 통해서만 작업합니다.

  • 만약 choice 가 POST 데이터에서 제공되지 않았을때, request.POST['choice']KeyError를 발생시킵니다. 위의 예시 코드는 choice 가 주어지지 않았을때, KeyError 를 확인하고 question 폼을 다시 표시합니다.

  • choice 카운트가 증가하였을때, 코드는 HttpResponse 대신에, HttpResponseRedirect를 반환합니다. HttpResponseRedirect는 하나의 인자, 즉 redirect 할 URL 을 받습니다. 위의 코드에 주석으로 나와 있듯이, POST 데이터를 성공적으로 다루었을때에는, HttpResponseRedirect를 반환하여, 데이터가 2번 전송되는것을 방지 합니다.

  • reverse() 함수를 HttpResponseRedirect에 사용하는데. reverse() 함수는 view에서 하드코드된 URL 을 가지게 되는것을 방지합니다. reverse('polls:results', args=(question.id)) 에서 보이듯, view 의 이름과 전달할 변수명을 받습니다. 해당 예시에서는, reverse() 함수가 아래와 같은 문자열을 반환 합니다.

  • reverse('polls:results', args=(quesiton.id,)) 
    
    '/polls/3/results/' 
    
    # 3 은 question.id 이고 
    # Redirect 된 URL 은 'results' 뷰를 호출하여 마지막 페이지를 출력합니다 
    
    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



    <br>

    part3 에서 다루었지만, request 는 HttpRequest 객체입니다.

    <br>HttpRequest 객체에 대해서 더 알고 싶으면, request and response 문서를 확인 하면 됩니다.



    <br>

    누군가가 질문에 대한 투표를 마치면, vote() 뷰는 질문에 대한 results 페이지로 리다이렉트 시킵니다. 따라서, result view 도 바꿔줘야 합니다.

    <br>

    ## polls/views.py (Results view) 수정

    <br>

    polls/views.py 에 results() 함수를 아래와 같이 업데이트 해줍니다.

    <br>

    ```django
    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})


part3 에서 봤던 detail() 뷰와 거의 흡사합니다만, template 이름만 다른것을 확인 할수 있습니다.


results 뷰에서,

‘polls/results.html’에 render 를 해주는데.

polls/results.html 파일이 없습니다.

polls/results.html 파일을 생성하고, 아래와 같이 내용을 채워줍니다.


1
2
3
4
5
6
7
8
9
<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/ 로 접속합니다.



what’s up? 질문을 클릭합니다.



Not Much 를 선택하고 Vote 를 눌러봅니다



아무것도 선택 안하고,

vote 버튼을 누르면, 에러가 표시될겁니다.



Race conditions


vote() 뷰는 조그마한 문제점이 하나 있습니다. Votes 뷰는 첫번째로, selected_choice 객체를 데이터베이스에서 가져옵니다. 그리고, 새 votes 의 값을 계산하고 데이터베이스에 다시 저장합니다.

하지만, 만약 두명의 사용자가 동시에 vote 를 할때에는, 이것이 잘못 될수도 있습니다.

이것은 race condition 이라고 불리우는데, F() 함수를 사용해서 이점을 피할수도 있습니다.

마치며..


reverse() 함수를 자주 사용하던데, reverse 함수 사용법을 다시 숙지 해야 합니다.

그리고 F() 함수도 나중에 리뷰해야 할것 같습니다.