[Django] 튜토리얼 -2

김가람휘·2022년 2월 9일
0

Django

목록 보기
3/13

Part 3

뷰 추가하기

# polls/views.py
from django.http import HttpResponse


def index(request):
    return HttpResponse("Hello, world. You're at the polls index.")

# 뷰 추가
def detail(request, question_id):
    return HttpResponse("You're looking at question %s." % question_id)

def results(request, question_id):
    response = "You're looking at the results of question %s."
    return HttpResponse(response % question_id)

def vote(request, question_id):
    return HttpResponse("You're voting on question %s." % question_id)
# polls/urls.py
from django.urls import path

from . import views

urlpatterns = [
    # ex: /polls/
    path('', views.index, name='index'),
    # ex: /polls/5/
    path('<int:question_id>/', views.detail, name='detail'),
    # ex: /polls/5/results/
    path('<int:question_id>/results/', views.results, name='results'),
    # ex: /polls/5/vote/
    path('<int:question_id>/vote/', views.vote, name='vote'),
]
  • 사용자가 웹사이트의 페이지를 요청할 때, 예로 /polls/34/를 요청했다면 django는 mysite.urls 파이썬 모듈을 불러옵니다.
  • ROOT_URLCONF 설정에 의해 해당 모듈을 바라보도록 지정되어 있기 때문에 mysite.urls에서 urlpatterns라는 변수를 찾고 순서대로 패턴을 따라갑니다.
  • polls/를 찾은 후엔 polls/를 버리고 남은 34/를 <polls.urls> URLconf로 전달하여 남은 처리를 진행합니다.
  • <int:question_id>/와 일치하여 결과적으로 detail() 뷰 함수가 호출됩니다.
    -> detail(request=<HttpRequest object>, question_id=34)



뷰가 실제로 뭔가를 하도록 만들기

  • 각 뷰는 두 가지 중 하나를 하도록 되어 있습니다.
    • 요청된 페이지의 내용이 담긴 HttpResponse 객체를 반환한다.
    • Http404 같은 예외를 발생하게 해야한다.
  • 뷰는 데이터베이스의 레코드를 읽을 수 있고 django나 python에서 서드파티로 제공되는 템플릿 시스템을 사용할 수도 있다.
  • 뷰는 PDF를 생성하거나, XML을 출력하거나, 실시간으로 ZIP 파일을 만들 수 있습니다.
# polls/views.py
from django.http import HttpResponse

from .models import Question

# 새로운 index() 뷰 하나를 호출했을 때, 시스템에 저장된 
# 최소한 5개의 투표 질문이 콤마로 분리되어, 발행일에 따라 출력됩니다.
def index(request):
    latest_question_list = Question.objects.order_by('-pub_date')[:5]
    output = ', '.join([q.question_text for q in latest_question_list])
    return HttpResponse(output)

# Leave the rest of the views (detail, results, vote) unchanged
  • 뷰에서 페이지의 디자인이 하드코딩 되어있다면 페이지가 보여지는 방식을 바꾸고 싶다면 python 코드를 편집해야만 합니다.
    -> 뷰에서 사용할 수 있는 템플릿을 작성하여, python 코드로부터 디자인을 분리하도록 Django의 템플릿 시스템을 사용해 봅시다.
  • DjangoTemplates은 각 INSTALLED_APPS 디렉토리의 "templates" 하위 디렉토리를 탐색합니다.
  • 따라서 DjangoTemplates가 찾을 수 있게 polls 디렉토리 아래에 templates라는 디렉토리를 만들고 그 아래에 polls라는 디렉토리를 하나 더 만들어주고 그곳에 index.html 파일을 생성할 것입니다.
  • polls/templates/polls/index.html
    -> 장고가 polls/templates를 찾은 후, 이 템플릿을 polls/index.html이라는 이름으로 참조합니다.
<!-- polls/templates/polls/index.html -->
{% if latest_question_list %}
    <ul>
    {% for question in latest_question_list %}
      <!-- question객체가 있다면 각각의 id링크 생성 -->
        <li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>
    {% endfor %}
    </ul>
{% else %}
<!-- question객체가 없을 때 -->
    <p>No polls are available.</p>
{% endif %}
# polls/views.py
from django.http import HttpResponse
from django.template import loader # template import

from .models import Question


def index(request):
    latest_question_list = Question.objects.order_by('-pub_date')[:5]
    template = loader.get_template('polls/index.html')
    context = {
        'latest_question_list': latest_question_list,
    }
    return HttpResponse(template.render(context, request))
  • 지름길 -> render() : request와 template_name을 필수로 받고 넘겨준 것들을 조합해서 HTTPRespense를 리턴해 주는 함수입니다.
  • 모든 뷰에 render()를 적용한다면 더이상 loaderHttpResponse를 import하지 않아도 됩니다.
from django.shortcuts import render

from .models import Question


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)
  • index 뷰를 보기위해 /polls/로 접속해 봅시다.
  • 클릭시 /polls/{{ question.id }}/ 로 이동
    -> detail 뷰로 이동

404 에러 일으키기

  • detail 뷰에 에러를 일으켜보겠습니다.
from django.http import Http404
from django.shortcuts import render

from .models import Question
# ...
def detail(request, question_id):
    try:
        question = Question.objects.get(pk=question_id)
    # 요청된 question의 id가 없을 경우 Http404 예외 발생
    except Question.DoesNotExist:
        raise Http404("Question does not exist")
    return render(request, 'polls/detail.html', {'question': question})
<!-- polls/templates/polls/detail.html -->
{{ question }}
  • 위의 예제 동작을 위해 임시의 detail.html을 생성했습니다.
  • mysite/settings.py 에서 DEBUG = False 설정 시

  • question.id = 1은 존재하지 않아 404에러가 발생하였고 question.id = 2는 존재해서 question의 내용을 출력했습니다.
  • 지름길 -> get_object_or_404() : 만약 객체가 존재하지 않을 때 get()을 사용하여 Http404 예외를 발생시키는 것
  • get_object_or_404() 함수는 django 모델을 첫번째 인자로 받고, 몇 개의 키워드 인수를 모델 관리자의 get()함수에 넘깁니다. 만약 객체가 존재하지 않을 경우, Http404 예외가 발생합니다.
  • get_list_or_404() : get() 대신 filter()를 씁니다. 리스트가 비어있을 경우, Http404 예외를 발생시킵니다.
# polls/views.py
from django.shortcuts import get_object_or_404, render

from .models import Question
# ...
def detail(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    return render(request, 'polls/detail.html', {'question': question})

템플릿 시스템 사용하기

<!-- polls/templates/polls/detail.html -->
<h1>{{ question.question_text }}</h1>
<ul>
{% for choice in question.choice_set.all %}
    <li>{{ choice.choice_text }}</li>
{% endfor %}
</ul>

  • 템플릿 시스템은 변수의 속성에 접근하기 위해 점-탐색(dot-lookup) 문법을 사용합니다.
  • django는 먼저 객체에 대해 사전형으로 탐색하고 탐색에 실패하면 속성값으로 탐색합니다. 만약 속성 탐색에도 실패한다면 리스트의 인덱스 탐색을 시도합니다. {{ question.question_text }}question

템플릿에서 하드코딩된 URL 제거하기

<li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>
  • 이렇게 하드코딩된 접근방식의 문제는 수 많은 템플릿을 가진 프로젝트들의 URL을 바꾸는 게 어려운 일이 된다는 점입니다.
  • polls.urls 모듈의 path() 함수에서 인수의 이름을 정의했으므로 template 태그를 사용하여 url 설정에 정의된 특정한 URL 경로들의 의존성을 제거할 수 있습니다.
<li><a href="{% url 'detail' question.id %}">{{ question.question_text }}</a></li>
  • 만약 detail 뷰의 URL을 polls/specifics/12/로 바꾸고 싶다면 템플릿에서 바꾸는 것이 아니라 polls/urls.py에서 바꿔야 합니다.
# polls/urls.py
...
# added the word 'specifics'
path('specifics/<int:question_id>/', views.detail, name='detail'),
...

URL의 이름공간 정하기

  • 튜토리얼에서는 polls이라는 앱 하나만 가지고 진행했지만, 실제 django 프로젝트는 앱이 몇개라도 올 수 있습니다.
  • polls앱은 detail이라는 뷰를 가지고 있고, 동일한 프로젝트에 블로그를 위한 앱이 있을 수 있습니다.
    -> django는 이 앱들의 URL을 어떻게 구별할까요?
  • URLconf에 이름공간(namespace)을 추가하면 됩니다.
  • polls/urls.py 파일에 app_name을 추가하여 앱의 이름공간을 설정할 수 있습니다.
# polls/urls.py
from django.urls import path

from . import views

app_name = 'polls' # 앱의 이름공간 추가
urlpatterns = [
    path('', views.index, name='index'),
    path('<int:question_id>/', views.detail, name='detail'),
    path('<int:question_id>/results/', views.results, name='results'),
    path('<int:question_id>/vote/', views.vote, name='vote'),
]
<!-- polls/templates/polls/index.html -->
<li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li>

Part 4

Write a minimal form

  • 템플릿에 HTML <form> 요소를 포함시켜 봅시다.
<!-- polls/templates/polls/detail.html -->
<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는 question과 연관된 choice의 ID입니다. 각 라디오 버튼의 이름은 "choice"입니다.
  • 누군가가 라디오 버튼 중 하나를 선택하여 폼을 제출하면, POST 데이터인 choice=#(선택한 항목의 ID)을 보낼 것입니다. -> HTML폼의 기본 개념입니다.
# 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_id라는 primary key를 기준으로 일치하는 Question을 가져옵니다.
    question = get_object_or_404(Question, pk=question_id)
    try:
    	# post request에서 준 choice라는 value와 일치하는 primary key를 가진 Question을 가져옵니다.
        selected_choice = question.choice_set.get(pk=request.POST['choice'])
    except (KeyError, Choice.DoesNotExist):
        # KeyError 발생시 question 투표 양식을 다시 표시합니다.
        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,)))
  • 가상으로 만든 vote() 함수를 실제로 구현했습니다.
# a라는 같은 키값으로, a=1과 a=2, c=3을 같이 보낸다
>>> QueryDict('a=1&a=2&c=3')

# <QueryDict: {'a':['1','2'], 'c':['3']}>
  • QueryDict objects
    • HttpRequest object인 GET, POST 속성은 django.http.QueryDict의 인스턴스입니다.
    • QueryDict는 딕셔너리 같은 것입니다.
  • request.POST는 키로 전송된 자료에 접근할 수 있도록 해주는 딕셔너리 같은 객체입니다.
  • choice=id로 들어온 POST 요청을 key는 choice, value는 id인 딕셔너리로 변환시켜서 request.POST['choice']로 id 값을 구합니다.
    -> 선택된 choice의 id를 문자열로 반환합니다.
  • request.POST의 값은 항상 문자열들입니다.
  • HttpResponseRedirect(reverse('polls:results', args=(question.id,)))
    • Exception이 발생하지 않으면 choice의 투표 수가 증가한 이후에, 코드는 일반 HttpResponse가 아닌 HttpResponseRedirect를 반환하고 하나의 인수를 받습니다. 그 인수는 재전송될 URL입니다.
      -> 선택을 끝낸 사용자에게 보여줄 결과화면의 URL
    • reverse() : 뷰 함수에서 URL을 하드코딩하지 않도록 하기 위해서 사용합니다. 제어를 전달하기 원하는 뷰의 이름과 URL패턴의 변수부분을 조합해서 해당 뷰를 가리킵니다.
    • reverse('polls:results', args=(question.id,)) -> /polls/3/results/ 반환 (3은 question.id 값입니다.)
  • 설문조사에 설문을 하고난 후 vote() 뷰는 설문조사 결과 페이지로 리다이렉트합니다.
# 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})
<!-- 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>


  • 우리의 vote() 뷰에서는 경쟁 상태를 해결할 수 없습니다.
    -> 두 명의 사용자가 정확하게 같은 시간에 투표하려고 시도할 경우, 잘못될 수 있습니다.

제너릭 뷰 사용하기 : 적은 코드가 더 좋습니다.

  • 제너릭 뷰(Generic view) : 일반적인 패턴을 추상화하여 앱을 작성하기 위해 Python코드를 작성하지 않아도 됩니다.
    • URLconf를 변환하세요.
    • 불필요한 오래된 보기 중 일부를 삭제하세요.
    • Django의 제너릭 뷰를 기반으로 새로운 뷰를 도입하세요.
# polls/urls.py
# <question_id> -> <pk>로 변경
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'),
]
# 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 # 제너릭 뷰 import

from .models import Choice, Question


class IndexView(generic.ListView):
    template_name = 'polls/index.html'
    context_object_name = 'latest_question_list'

    def get_queryset(self):
        # 최소 5개의 투표 question이 발행일에 따라 리턴
        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.
  • 각 제너릭 뷰는 어떤 모델이 적용될 것인지를 알아야합니다. 이것은 model 속성을 사용하여 제공됩니다.
  • DetailView
    • URL에서 캡처된 기본 키 값이 "pk"라고 기대하기 때문에 question_id를 제너릭 뷰를 위해 pk로 변경합니다.
    • 기본적으로 <app name>/<model name>_detail.html 템플릿을 사용합니다.
      -> polls/question_detail.html
    • detail 뷰와 result 뷰가 동일한 DetailView를 사용하고 있지만 template_name 속성을 통해 렌더링 될 때 서로 다른 모습을 갖도록 합니다.
  • ListView
    • 기본적으로 <app name>/<model name>_list.html 템플릿을 사용합니다.
    • polls/index.html 템플릿을 사용하기 위해 template_name으로 전달했습니다.

0개의 댓글