[DjangoTutorial] 2nd - Writing your first Django app (part 3-5) : views.py, Generic View, UnitTest

Alex of the year 2020 & 2021·2020년 8월 19일
0

Django

목록 보기
4/5
post-thumbnail

views.py

특정 기능과 템플릿을 제공하는 웹페이지의 한 종류
(참고: MTV 모델의 Template은 MVC 모델의 View에 해당 / MTV 모델의 View가 MVC 모델의 Controller에 해당)

In Django, web pages and other content are delivered by views. Each view is represented by a Python function (or method, in the case of class-based views). Django will choose a view by examining the URL that's requested (to be precise, the part of the URL after the domain name).

Now in your time on the web you may have come across such beauties as ME2/Sites/dirmod.htm?sid=&type=gen&mod=Core+Pages&gid=A6CD4967199A42D9B65B1B. You will be pleased to know that Django allows us much more elegant URL patterns than that.

A URL pattern is the general form of a URL - for example: /newsarchive/<year>/<month>/.

장고의 기본 동작 원리인 MTV의 흐름은 아래와 같다.

1) 사용자가 보낸 요청(Request)에 적힌 URL을 URLConf로 처리하여, 어떤 View로 요청을 보내야할지 결정
2) View에서는 해당 요청을 받아 로직을 처리한 후, 만약 DB 처리가 필요할 경우 Model을 통해 DB로 전달하여 처리 후 반환
3) View는 모든 로직 처리가 끝나면, Templates를 사용하여 사용자에게 보낼 HTML을 작성
4) View는 최종적으로 해당 HTML을 사용자에게 보내어 응답(Response)

이번 포스팅은 Django tutorial에서 shortcuts로 규정하고 있는 render()부터
중점을 두고 써보려 한다.


render()

render() 함수는

  • request 객체를 첫번째 인수로, 템플릿 이름을 두번째 인수로 받으며 (여기까지 필수)
  • context dictionary형 객체를 세번째 인수로 (선택) 받음
  • context dictionary형 객체를 받을 경우에는 인수로 지정된 context로 표현된 템플릿의 HttpResponse가 반환

render() 함수 사용 예시

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)

[404] Page Not Found Error vs. get_object_or_404()

요청된 질문의 ID가 없을 경우 Http404 예외를 발생시키는 코드는 아래와 같다.

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)
    except Question.DoesNotExist:
        raise Http404("Question does not exist")
    return render(request, 'polls/detail.html', {'question': question})

그리고 객체가 존재하지 않을 때 get()을 사용하여 Http404 예외를 발생시키는 지름길로는
아래와 같은 방법이 있다.

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

get_object_or_404도 곧 공부할 helper_function의 한 종류라고 한다.
이와 비슷한 것으로는 get_list_or_404() 함수도 있다. list이기에 get()이 아닌 filter()를 사용한다고 한다. (리스트가 비어있을 경우 404 리턴)

URL의 이름공간(name space) 정하기

프로젝트에 앱이 하나일 때는 크게 상관이 없지만(지금처럼 polls라는 앱 하나)
앱이 여러 개가 될 경우, 그 때는 URL에 대한 구분이 필요하다. 이럴 때는 URLConf에 이름 공간(name space)를 추가하는 것이 그 답이 될 수 있다.

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'),
]

reverse(), request.POST

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는 키로 전송된 자료에 접근하게 해주는 dictionary 객체로 항상 문자열을 반환
  • reverse()의 경우, 뷰 함수에서 URL을 하드코딩하지 않도록 도와준다. 뷰의 이름과 URL패턴 중 변수 부분을 조합해서 해당 뷰를 가리키고, 위 맨 마지막 줄의 reverse() 호출로 인해 /polls/3/results/를 반환하게 될 것이다. (3은 question.id 값)


Generic View - redundant한 코드를 없애고 더 적게 코드를 쓰기 위하여

장고에서 지름길 중 하나로 이야기하는 것 중 하나
generic view를 사용하기 위한 방법은 아래처럼 나와있다.

step 1) URLConf 수정
step 2) 불필요한 오래된 보기 중 일부를 삭제
step 3) Django의 제너릭 뷰를 기반으로 한 새로운 뷰를 도입

step 1) URLConf 수정

작성중인 앱의 urls.py로 넘어가서, urlpatterns들을 고치는데,
detail뷰와 results뷰에서 <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'),
]

step 2) 장고의 일반적인 방식으로의 Views 수정

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.

요 모습이 장고의 일반적인 뷰라고 한다. 튜토리얼을 여기까지 이어오면서 내가 프로젝트 때 봤던 뷰의 모습과 사뭇달라서 상당히 당황했었는데, 여기서부터 조금 이해가 되는 것도 같았다. 실제로 코드가 훨씬 더 간결하고 직관적이다.

조금 더 정확히 말하자면
IndexView에서 사용한 ListView
그 아래 DetailView와 ResultsView에서 사용한 DetailView가 두 가지 제너릭 뷰에 해당한다.

⭐️ 제너릭 뷰 사용 시의 주의점
1) 각 제너릭 뷰는 어떤 모델이 적용될 것인지 알아야 한다. model 속성을 사용하여 제공되기 때문이다.
--> 그런데 여기서 잠깐! DetailView 아래에는 model = Question 이라고 명시되어 있지만, ListView를 보면 따로 명시되어 있지 않다. 그럼 이건 뭘까?

In previous parts of the tutorial, the templates have been provided with a context that contains the question and latest_question_list context variables. For DetailView the question variable is provided automatically -- since we're using a Django model (Question), Django is able to determine an appropriate name for the context variable. However, for ListView, the automatically generated context variable is question_list. To override this we provide the context_object_name attribute, specifying that we want to use latest_question_list instead. As an alternative approach, you could change your templates to match the new default context variables -- but it's a lot easier to tell Django to use the variable you want.

리스트뷰에 한하여는 context_object_name이라는 속성이 따로 있고, 이 속성으로 인하여 question_list를 오버라이드하여 latest_question_list를 사용할 수 있다고 한다.



UnitTest - 자동화된 테스트

Tests are routines that check the operation of your code.

자동화된 테스트를 해야 하는 이유는 다음과 같다.

  • 시간 절약
  • 문제 식별 및 예방
  • 코드 매력 증가
  • 협업 효율성 증가

was_published_recently() : T or F

이번 튜토리얼에서 작업중인 polls앱에서 Question 모델이 가진 속성에는 pub_date, 즉 발행 시기가 있다. models.py에 해당 질문의 발행시기가 최근인지 아닌지를 알아볼 수 있는 메소드를 함께 정의했다. 클래스 내 두번째 def 부분이다.

import datetime
from django.db    import models
from django.utils import timezone

class Question(models.Model):
    question_text = models.CharField(max_length=200)
    pub_date = models.DateTimeField('date published')

    def __str__(self):
        return self.question_text

    def was_published_recently(self):
        return self.pub_date >= timezone.now() - datetime.timedelta(days=1)

과연 이 메소드가 잘작동하는지는 다음과 같이 shell로 알아보았다.
(shell 실행 후)

>>> import datetime
>>> from django.utils import timezone
>>> from polls.models import Question
>>> future_question = Question(pub_date=timezone.now() + datetime.timedelta(days=30))
>>> future_question.was_published_recently()
True

발행일이 미래인 질문은 '최근에' 작성되었다고 말할 수 없다. ('최근'이라는 말이 과거성을 내포하고 있기 때문에 불가능하다.) 이런 메소드의 잘못된 동작을 잡아내기 위해 사용하는 것이 tests.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):
        time = timezone.now() + datetime.timedelta(days=30)
        future_question = Question(pub_date=time)
        self.assertIs(future_question.was_published_recently(), False)

클래스명에 QuestionModelTests라고 써 넣어 models.py에 대한 검증임을, 그리고 TestCase를 임포트하여 이것이 검증용 코드임을 확실히 했다. def명 역시 test_로 시작하여 테스트코드임을 확실히한다.
코드를 읽어보면 30일 이후에 작성된 질문은 'recently'하게 작성된 질문이 아니므로 False값을 반환해야 한다.

이 테스트 코드를 작동하는 flow는 아래와 같다.

1) manage.py 단에서 : python manage.py test polls(앱 이름) 명령
2) django.test.TestCase (tests.py에 미리 임포트 해두었음)클래스의 서브 클래스를 찾음
3) 이름이 test로 시작하는 파일을 찾음
4) 해당 파일로 들어가 위에 작성한대로 pub_date 필드가 30일 미래인 Question 인스턴스를 생성
5) assertIs() 메소드를 사용하여 False가 나와야하는데 True를 반환 시킴
--> 코드에 문제가 있다는 의미. 이 테스트를 실행할 경우 어떤 테스트가 실패했으며, 어디서 실패가 발생했는지를 알려줌.

문제가 있다면, 코드를 수정하자

이미 테스트 코드 이름에서 보았듯, models.py에 관련한 테스트코드였다. 그럼 당연히 수정도 models.py에서 이루어져야 한다.

def was_published_recently(self):
    now = timezone.now()
    return now - datetime.timedelta(days=1) <= self.pub_date <= now

model 내의 def return 값을 수정한다.
self.pub_date가 '지금 이순간부터 24시간 전 ~ 지금'에 해당할 때에만 True를 반환하도록 식을 바꾼 것이다.

수정 이후 다시 test를 돌리면,
우리가 가장 좋아하는 알파벳 조합인
OK를 확인할 수 있다.


tests.py의 비대함에 대하여: When testing, more is better

장고 튜토리얼에는 이 이외에도 더욱더 포괄적인 테스트 방법(ex. models.py: 미래에 발행된 질문, 이전에 발행된 질문, 최근에 발행된 질문에 대해 최근 질문인지 묻기), 오늘 나는 실제로 모두 실습을 하였다. 하지만 가장 기본이 되는 위의 테스트가 개념에 충실한 것 같아 정리는 기본 테스트만 정리하였다.

장고는 튜토리얼에서 이렇게 밝히고 있다.

It might seem that our tests are growing out of control. At this rate there will soon be more code in our tests than in our application, and the repetition is unaesthetic, compared to the elegant conciseness of the rest of our code.

It doesn’t matter. Let them grow. For the most part, you can write a test once and then forget about it. It will continue performing its useful function as you continue to develop your program.

Sometimes tests will need to be updated. Suppose that we amend our views so that only Questions with Choices are published. In that case, many of our existing tests will fail - telling us exactly which tests need to be amended to bring them up to date, so to that extent tests help look after themselves.

At worst, as you continue developing, you might find that you have some tests that are now redundant. Even that’s not a problem; in testing redundancy is a good thing.

코드 자체에 있어서는 the less the better이겠으나
tests.py 만큼은 the more the better인듯하다.

(실제로 위코드 멘토님도 테스트 코드 경우의 수 3가지로 짜는 것을 어려워하는 내게 앞으로 실제로 현업가면 테스트 코드만 엄~청 짤거라고 말씀해주셨음..ㅎ)

profile
Backend 개발 학습 아카이빙 블로그입니다. (현재는 작성하지 않습니다.)

0개의 댓글