[Django Tutorial] 2

jinsik·2022년 11월 11일
0
post-custom-banner

django 웹 프로그래밍 강좌 (#3 view)(django view 사용, html 불러오기 및 연결)

🏆 View와 Template에 대해 배우고 URL-View-Template 구조를 복습한다.

개요

  • 4가지 view를 만들 것이다.
    • 질문 “색인” 페이지 – 최근의 질문들을 표시
    • 질문 “세부” 페이지 – 질문 내용과, 투표할 수 있는 서식을 표시
    • 질문 “결과” 페이지 – 특정 질문에 대한 결과를 표시
    • 투표 기능 – 특정 질문에 대해 특정 선택을 할 수 있는 투표 기능을 제공

View 추가하기

#in poll/views.py
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)
  • 클라이언트로부터 요청을 받을 때 request 객체 안에 정보를 받아오게 되고, 알맞은 처리 후에 HttpResponse 객체를 리턴하는 것이 View의 기본 역할이다.
  • view는 HttpResponse를 반환하거나 Http404같은 예외를 발생하여야 한다.
#in poll/urls.py
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'),
]
  • path() 함수를 통해 view와 URL을 연결한다.
  • 인자로 <int:question_id>이라는 URL pattern을 넣게 되면 question_id라는 정수형 변수와 그 값을 받을 수 있다.
  • 이 변수의 이름은 두번째 인자로 받은 view의 함수의 매개변수 이름과 같아야 한다.
  • 즉, path(URL pattern, view, name=template

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

#in poll/views.py
from .models import Question

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)
  • “Hello World”라는 메시지만 반환하던 index라는 view를 위와 같이 변경해보자.
  • latest_question_list: Question object들 중에서 출판일자를 기준으로 정렬하여 데이터 5개를 담는다.
  • 그리고 콤마(,)로 나열해 한 줄의 문자열로 만들어 반환한다.
  • 저장하고 http://127.0.0.1:8000/polls/ 에 들어가면 index라는 view가 처리되어 Question의 객체를 5개까지 보여준다.
    • 지금은 “What’s new??”밖에 없기 때문에 1개만 보여줄 것이다.
  • 아무런 디자인도 없이 출력만 하기 때문에 편한 디자인을 위해서는 template이 필요하다.
  • polls 디렉토리 안에 templates 라는 디렉토리를 만들고, 다시 그 안에 polls라는 디렉토리를 만든다.
    • 만약 templates/polls를 만들지 않는다면 django는 제일 먼저나온 이름의 템플릿을 선택하므로, 다른 앱의 템플릿과 구별할 수 없다.
  • 그런 다음 아래와 같이 polls/templates/polls/index.html과 polls/views.py를 편집한다.
#in polls/templates/polls/index.html
{% if latest_question_list %}
    <ul>
    {% for question in latest_question_list %}
        <li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>
    {% endfor %}
    </ul>
{% else %}
    <p>No polls are available.</p>
{% endif %}

#in polls/views.py
from django.template import loader

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))
  • context를 통해서 view에서 template로 latest_question_list를 전달해주게 된다.

shortcut: render() 사용하기

  • django.shortcuts에 있는 render()를 사용하게 되면 view에서 HttpResponse를 반환할 필요 없이 render 함수를 반환하면 쉽게, 짧게 템플릿을 뿌려주는 것이 가능하다.
#in polls/view.py
from django.shortcuts import render

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)
  • 만약 모든 뷰를 render 함수를 사용해 템플릿을 뿌려주게 되면 loader와 HttpResponse는 더이상 import하지 않아도 된다.

404 error 일으키기

#in polls/view.py
from django.http import Http404

def detail(request, question_id):
    try:
        question = Question.objects.get(pk=question_id)
    except Question.DoesNotExist:
        raise Http404("Question does not exist")
    return render(request, 'polls/detail.html', {'question': question})

#create polls/templates/polls/detail.html
{{ question }}
  • http://127.0.0.1:8000/polls/1/ 에 들어가면 “What’s new??”가 보인다.
  • http://127.0.0.1:8000/polls/2/ 에 들어가면 어떨까?
  • 위의 코드를 작성 후 들어가면 id가 2인 Question 객체가 없기 때문에 위 코드에서 처리한대로 Http404 에러를 일으키며 “Question does not exist”라는 메시지가 뜬다.

shortcut: get_object_or_404() 사용하기

#in polls/views.py
from django.shortcuts import get_object_or_404, render

def detail(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    return render(request, 'polls/detail.html', {'question': question})
  • try-except 구문 없이 저 한 줄로 구현이 가능하다.
  • get_object_or_404 함수는 모델을 첫 번째 인자로 받고, 몇 개의 키워드 인자를 모델 관리자의 get() 함수에 넘긴다.
  • 만약 객체가 존재하지 않으면, Http404 예외가 발생한다.

템플릿 시스템 사용하기

#in 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>
  • 먼저, detail.html을 위와 같이 수정해주자.
  • question에 대해 외래키를 갖는 choice를 모두 가져와 choice_text를 리스트로 나열하게 된다.
  • 그 전에, 관리자 페이지에 Choice 모델을 등록하고 question 객체에 choice를 추가해보자.
#in polls/admin.py
from django.contrib import admin

from .models import Question, Choice

admin.site.register(Question)
admin.site.register(Choice);
  • 관리자 페이지에서, Choice에서 Add Choice버튼을 눌러 choice_text가 Ubuntu, MS, RedHat 3개를 추가해보자.
  • 그런 다음, http://127.0.0.1:8000/polls/1/ 에 들어가면 What’s new?? 아래에 3개의 choice가 나열된 것을 볼 수 있다.

하드코딩 되어있는 index.html 수정하기

#in polls/templates/polls/detail.html
#...
<li><a href="{% url 'detail' question.id %}">{{ question.question_text }}</a></li>
#...
  • 이제 다른 앱에서도 detail을 사용할 수 있다.
  • 하지만 여러 다른 앱에 detail이 존재한다고 하면 어떻게 polls의 detail 이라는 것을 알 수 있을까?

튜토리얼의 프로젝트는 polls라는 앱 하나만 가지고 진행했습니다. 실제 Django 프로젝트는 앱이 몇개라도 올 수 있습니다. Django는 이 앱들의 URL을 어떻게 구별해 낼까요? 예를 들어, polls 앱은 detail이라는 뷰를 가지고 있고, 동일한 프로젝트에 블로그를 위한 앱이 있을 수도 있습니다. Django가 {% url %} 템플릿태그를 사용할 때, 어떤 앱의 뷰에서 URL을 생성할지 알 수 있을까요? 정답은 URLconf에 이름공간(namespace)을 추가하는 것입니다. polls/urls.py 파일에 app_name을 추가하여 어플리케이션의 이름공간을 설정할 수 있습니다.

#in polls/urls.py
app_name = 'polls'
urlpatterns = [
		#...
]

#in polls/templates/polls/detail.html
#...
<li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li>
#...
  • polls/urls.py에 앱 이름을 명시해주어야 한다.
  • 또한, 이제 polls라는 namespace를 명시하여 detail 뷰를 가리키도록 변경할 수 있다.

🏆 폼을 이용하여 클라이언트로부터 데이터를 받아오는 법을 학습한다.

간단한 폼 쓰기

  • 이전의 detail.html을 수정하여, form 요소를 포함시킨다.
#in polls/templates/polls/detail.html
<h1> {{ question.question_text }} </h1>

{% if error_message %} <p> {{ error_message }}</p> {% endif %}
<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>
  • {% csrf_token %}
    • 위조 공격을 막기 위한 보안성 문구
    • 따로 공부할 필요성이 있다.
  • 태그에 있는 value 값이 submit을 누르면 polls의 vote, 즉 polls/vote 넘어가게 된다.
  • 또한, views.py에서 vote()도 아래와 같이 변경한다.
#in polls/views.py
from django.http import HttpResponse, HttpResponseRedirect
from django.urls import reverse
#...
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, )))
  • question_id를 받아온다.
  • try/except에서 question의 form에서 넘겨받은 값의 외래키를 받아오고 (request.POST[name])
  • 값이 없다면 에러가 발생한다.
    • 에러 처리로는 polls/detail에 question과 에러메시지를 넘겨준다.
  • 값이 있는 경우에는 votes를 1 올려주고 저장하게 된다.
  • POST 데이터를 받아온 경우에는 HttpResponseRedirect를 사용해주어야 한다.
  • reverse(’appname:url’, …): 위를 보면 polls/results로 리다이렉트 하게 된다.
  • 현재 results view를 작성을 하지 않았기 때문에 아래와 같이 작성한다.
#in polls/views.py
def results(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    return render(request, 'polls/results.html', {'question': question})
  • 또한, 그에 맞게 results.html도 새로 작성한다.
#in 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>
  • choice.votes|pluralize
    • choice.votes가 복수일 경우 복수형으로 나타내고, 단수일 경우 단수형으로 나타낸다.

Generic View 사용하기

  • generic한 view를 작성해보자.
  • 제네릭 뷰는 일반적인 패턴을 추상화하여 앱을 작성하기 위해 Python 코드를 작성하지 않아도 된다.

URLconf 수정

#in polls/urls.py
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'),
]
  • <int:question_id> 에서 <int:pk>로 변경되었다.

views 수정

#in polls/views.py
from django.views import generic

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'
  • Index, Detail, Results view를 삭제하고 class로 위와 같이 작성한다.
  • 사용할 템플릿 이름과 템플릿에서 사용할 모델 이름을 명시해주기만 하면 된다.
  • context에 넘겨주는 이름이 model 이름과 다르다면 object 이름을 다시 정해주고 필요한 데이터를 get_queryset 함수를 통해서 다시 작성해주면 된다.

참고

  • request.method로 어떤 http method 방식을 사용했는지 알 수 있다.

django 웹 프로그래밍 강좌 (#5 test)(django test 사용, test case 작성, unit test)

🏆 Django test case 작성

test code를 작성하는 이유

  • 테스트를 통해 시간을 절약할 수 있다.
  • 테스트는 문제를 그저 식별하는 것이 아니라 예방한다.
  • 테스트가 코드를 더 매력적으로 만든다.
  • 테스트는 팀이 함께 일하는것을 돕는다.

기초 테스팅 전략 (테스트 주도 개발)

  • 실제 코드를 작성하기 전에 테스트를 작성

첫번째 테스트 작성하기

  • 우리가 만든 polls 앱에서는 버그가 존재했다.
  • Question.was_published_recently() 메소드는 Question의 pub-date 필드가 미래로 설정되어 있을 경우에도 True를 반환한다. (틀린 동작)
  • 쉘에서 이를 확인할 수 있지만, 코드로도 확인할 수 있다.
  • 아래와 같이 tests.py를 작성해보자.
#in polls/test.py
import datetime

from django.test import TestCase
from django.utils import timezone

from .models import Question

class QuestionModelTests(TestCase):

    def test_was_published_recently_with_future_question(self):
        """
        was_published_recently() returns False for questions whose pub_date
        is in the future.
        """
        time = timezone.now() + datetime.timedelta(days=30)
        future_question = Question(pub_date=time)
        self.assertIs(future_question.was_published_recently(), False)
  • test class는 TestCase 클래스를 상속하여야 한다.
  • 마지막 줄은 question.was_published_recently()의 값이 False이 나오는지 확인할 수 있다.
  • 다음과 같은 명령어로 테스트를 진행할 수 있다.
$ python manage.py test polls
  • 이 때 다른 오류, 특히 NameError가 발생한다면, 그것은 아마도 내가 겪었던 문제이다.
    • polls/models.py에 import datetime과 frome django.utils import timezone을 적어주어야 한다.
  • 정상적이라면, 위의 명령어를 실행했을 때 AssertionError: True is not False 라는 오류가 나와야 한다.

버그 수정

#in polls/models.py
def was_published_recently(self):
    now = timezone.now()
    return now - datetime.timedelta(days=1) <= self.pub_date <= now
  • 위와 같이 polls/models.py를 수정하자.
  • 수정한 후 테스트를 다시 해보면 OK 메시지를 받을 수 있다.

뷰 테스트

  • 이제까지 함수에 대한 테스트를 진행했다면 뷰에 대한 테스트를 진행해보자.
  • 뷰의 핵심은 request를 받고, response를 하는 것이다.

장고 테스트 클라이언트

  • Django는 view level에서 사용자를 시뮬레이트를 하기 위한 테스트 클라이언트 클래스 Client를 제공한다.
  • 이 Client 인스턴스를 이용하여 서버에 request를 보내보거나, 또한 response를 받아볼 수 있다.
  • 먼저, views.py를 다음과 같이 수정해주자.
#in polls/views.py
from django.utils import timezone

def get_queryset(self):
    """
    Return the last five published questions (not including those set to be
    published in the future).
    """
    return Question.objects.filter(
        pub_date__lte=timezone.now()
    ).order_by('-pub_date')[:5]
#in polls/tests.py

from django.urls import reverse

def create_question(question_text, days):
    """
    Create a question with the given `question_text` and published the
    given number of `days` offset to now (negative for questions published
    in the past, positive for questions that have yet to be published).
    """
    time = timezone.now() + datetime.timedelta(days=days)
    return Question.objects.create(question_text=question_text, pub_date=time)

class QuestionIndexViewTests(TestCase):
    def test_no_questions(self):
        """
        If no questions exist, an appropriate message is displayed.
        """
        response = self.client.get(reverse('polls:index'))
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, "No polls are available.")
        self.assertQuerysetEqual(response.context['latest_question_list'], [])

    def test_past_question(self):
        """
        Questions with a pub_date in the past are displayed on the
        index page.
        """
        question = create_question(question_text="Past question.", days=-30)
        response = self.client.get(reverse('polls:index'))
        self.assertQuerysetEqual(
            response.context['latest_question_list'],
            [question],
        )

    def test_future_question(self):
        """
        Questions with a pub_date in the future aren't displayed on
        the index page.
        """
        create_question(question_text="Future question.", days=30)
        response = self.client.get(reverse('polls:index'))
        self.assertContains(response, "No polls are available.")
        self.assertQuerysetEqual(response.context['latest_question_list'], [])

    def test_future_question_and_past_question(self):
        """
        Even if both past and future questions exist, only past questions
        are displayed.
        """
        question = create_question(question_text="Past question.", days=-30)
        create_question(question_text="Future question.", days=30)
        response = self.client.get(reverse('polls:index'))
        self.assertQuerysetEqual(
            response.context['latest_question_list'],
            [question],
        )

    def test_two_past_questions(self):
        """
        The questions index page may display multiple questions.
        """
        question1 = create_question(question_text="Past question 1.", days=-30)
        question2 = create_question(question_text="Past question 2.", days=-5)
        response = self.client.get(reverse('polls:index'))
        self.assertQuerysetEqual(
            response.context['latest_question_list'],
            [question2, question1],
        )

django 웹 프로그래밍 강좌 (#6 css, static file)(django css 연결, static 파일)

🏆 Django와 CSS를 연결한다.

static file

  • html, css, javascript
  • django는 이 static file들과 분리할 수 있는 기능을 제공해준다.
  • static file은 app 내부에 static 디렉토리를 만들어 관리한다.
  • 이 때, templates 처럼 앱의 namespace를 명시하기 위해 polls/static/polls/filename처럼 만든다.
#in polls/static/polls/style.css
li a {
    color: green;
}

#in polls/templates/polls/index.html
{% load static %}

<link rel="stylesheet" href="{% static 'polls/style.css' %}">
  • 위와 같이 html에서 css 파일을 불러올 수 있다.

django 웹 프로그래밍 강좌 (#7 customize admin)(django customize the admin)

🏆 admin 기능을 customize하는 방법을 학습한다.

관리자 폼 커스터마이징

  • admin을 커스터마이징하기 위해서 admin을 상속받는 클래스를 하나 선언해야 한다.
  • 클래스 내에 우리가 필요한 값을 직접 부여하여 커스터마이징 할 수 있다.
#in polls/admin.py
from django.contrib import admin

from .models import Question, Choice

class QuestionAdmin(admin.ModelAdmin):
    fields = ['pub_date', 'question_text']

admin.site.register(Question, QuestionAdmin)
admin.site.register(Choice)
  • 위와 같이 하면 Queston 모델의 필드 순서를 text, date 순이 아닌 date, text 순으로 나타내 보여줄 수 있다.
  • Choice에서 Question을 외래키로 받고 있기 때문에, Question을 추가할 때 choice 속성도 같이 받기를 원할 것이다. (지금은 없다)
  • 그러므로 다음과 같이 추가해줄 수 있다.
#in polls/admin.py
from django.contrib import admin

from .models import Choice, Question

class ChoiceInline(admin.StackedInline):
    model = Choice
    extra = 3

class QuestionAdmin(admin.ModelAdmin):
    fieldsets = [
        (None,               {'fields': ['question_text']}),
        ('Date information', {'fields': ['pub_date'], 'classes': ['collapse']}),
    ]
    inlines = [ChoiceInline]

admin.site.register(Question, QuestionAdmin)
  • Choice를 Question 어드민 페이지에서 관리할 수 있게 된다.
  • ChoiceInline 클래스에서 StackedInline 대신 TabularInline을 상속받으면 테이블 형태로 관리할 수 있다.

리스트 커스터마이징

  • class QuestionAdmin에 다음을 추가해준다.
list_display = ('question_text', 'pub_date', 'was_published_recently')
  • Question 리스트에서 이제 question_text 뿐만 아니라 발행일을 확인할 수 있다.
  • models.py에서도 다음과 같이 변경하여 리스트 테이블의 필드이름을 바꿔줄 수 있다.
#in polls/models.py
from django.contrib import admin

class Question(models.Model):
    # ...
    @admin.display(
        boolean=True,
        ordering='pub_date',
        description='Published recently?',
    )
		def was_published_recently(self):
			#...
  • @admin.display 데코레이터는 def was_published_recently(self) 위에 작성해야 한다.

필터 추가하기

  • list_filter = ['pub_date'] 를 QuestionAdmin에 추가하면 필터를 추가할 수 있다.
  • search_fields = ['question_text'] 를 추가하면 그 속성에 대한 값으로 검색을 할 수 있다.

배운 내용 정리

  1. 새롭게 배운 것
    • view와 template의 기능에 대해서 알게 되었다.
    • form태그로 POST 메시지를 보내 값을 받고 알맞은 처리를 해보았다.
    • 테스트의 중요성과 테스트 코드를 작성해 테스트 케이스를 테스트해보았다.
    • 어드민 페이지를 커스터마이징 해보았다.
  2. 배운 것 중에 모르겠는 것들
    • reverse() 함수
  3. 모르는 것을 알기 위해 찾아본 것들
profile
공부
post-custom-banner

0개의 댓글