백엔드/Django

[Django] Document 따라 읽기(5) - 앱 작성(2)

안용감한호랑이 2023. 11. 10. 01:52

[전체 보기] - [Django] Document 따라 읽기(4) - 앱 작성(1)

 

[Django] Document 따라 읽기(4) - 앱 작성(1)

[백엔드/Django] - [Django] Document 따라 읽기(3) - admin 페이지 [Django] Document 따라 읽기(3) - admin 페이지 [백엔드/Django] - [Django] Documnet 따라 읽기(2) - View, Model, CRUD API(1) [Django] Documnet 따라 읽기(2) - View, Mod

dog-foot-writen.tistory.com


 

 

투표 기능 구현하기

먼저 투표 기능을 추가해보겠습니다.

 

# polls/templates/polls/detail.html

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

 

위 템플릿은 app_name을 polls라고 명명한 urls.py에서 vote를 찾습니다.

이후 qustion.id 에 해당하는 question_text를 보여주고 각 질문 선택에 대한 라디오 버튼을 표시합니다. 각 라디오 버튼의 value는 choice의 id에 해당합니다. 라디오 버튼을 클릭하고 투표하면 POST방식으로 값을 넘기게 됩니다.

 

 

이제 제출된 데이터를 처리하고 이에 대한 작업을 수행하는 View를 만들겠습니다.

# 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 : 해당 값은 항상 문자열이며 key 이름으로 전달받은 데이터에 액세스 할 수 있게 해주는 dictionary와 유사한 객체입니다. Django는 request.GET 또한 동일한 방식으로 전달받을 수 있지만 POST 호출을 통해서만 데이터가 변경되도록 권장하고 있습니다.
  • 설문을 선택하지 않는 경우가 존재하기 때문에 KeyError를 발생시켜 detail.html로 render 함수를 통해 html과 파라미터들을 전달합니다.
  • HtpResponseRedirect : 선택 횟수를 증가시킨 후 HttpResponse가 아닌 HttpResponseRedirect를 반환하여 사용자가 redirect 될 URL을 반환합니다. 
  • Django 뿐만아니라 일반적인 웹개발에서 POST 데이터 요청을 성공적으로 처리한 후 HttpResponseRedirect를 반환하는것을 추천합니다.
  • HttpResponseRedirect의 reverse 함수는 View에서 URL을 하드코딩할 필요가 없도록 도와줍니다. redirect될 URL과 변수를 제공합니다. 위의 예제에서는 results에 question.id를 전달합니다.

 

 

 

이제 results가 투표된 집계를 보여주도록 변경하겠습니다.

해당 View는 detail과 거의 동일하기 때문에 조금 아래에서 이러한 중복성을 수정 하도록 하겠습니다.

# polls/views.py

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

 

 

results가 반환할 Template을 작성하도록 하겠습니다.

# polls/templates/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/ 로 가서 투표하면 results 페이지에서 투표할 때마다 업데이트 되는 페이지가 표시됩니다.

 

 

주의

해당 코드에서는 selected_choice를 통해 데이터베이스에서 object를 가져오게 됩니다. 이후 투표값을 update해주게 되는데 완전히 동일한 시간에 수행된다면 실제 수행되어야 하는 값과 상이한 값이 계산되어 문제가 발생합니다. 이러한 현상을 race condition 이라고 합니다. 

현재 글에서는 다루지 않으며 F() 함수를 통해 race condition을 방지할 수 있습니다.

관련 Document 경로를 첨부하겠습니다.

 

 

 

 

중복되는 View

위에서 설명했듯 results와 detail은 반환하는 html만 다른 동일한 코드입니다.

이를 방지하기위해 Django는 generic view라는 것을 제공합니다.

generic view는 앱을 작성하기 위해 Python 코드를 작성할 필요도 없을 정도로 공통 패턴을 추상화합니다.

 

절차는 다음과 같습니다.

  1. URLconf 를변환합니다.
  2. 필요없는View를 삭제합니다.
  3. generic view에 맞게 재작성합니다.

 

 

 

절차 1

# polls/urls.py

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로 변경되었습니다. 이것은 generic view의 DetailView 를 사용하여 기존의 detail과 result를 대체합니다.

 

 

절차 2 ~ 절차 3

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

 

각각의 generic view는 model의 attribute(이경우에서는 DetailView와 ResultsVeiw의 model = Question 코드 입니다.) 혹은 get_queryset() method를 정의하여 제공합니다.

 

기본적으로 generic view의 DetailView는 "<app name>/<model name>_detail.html" 이라는 Template을 사용합니다. 저희는 polls/question_detail.html이라는 템플릿을 사용하게 됩니다. teamplate_name 특성은 Djagno에게 자동 생성된 기본 템플릿 이름 대신 특정 템플릿 이름을 사용하도록 지시하는데 사용됩니다. 위의 코드에서는 기존 polls/detail.html과 polls/results.html을 사용하도록 지정하고 있습니다. 결국 각각 detail(), result() View가 모두 generic.DetailView이긴 하지만 다른 결과를 나타냅니다.