Django 06편 - polls app 구현하기 3편
포스트
취소

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 라이센스를 따릅니다.