2012. 5. 30. 10:31

[08] django Views와 URLconfs의 고급 기능

참고 : http://www.djangobook.com/en/2.0/chapter08/
(chapter 7까지 django의 core 내용이여서 full 번역하였지만, chapter 8 부터는 고급 내용이라 축약 번역합니다.)

chapter 3(2012/04/04 - [django/the django book study] - [03] django 1.4의 view와 urlconfs
)에서 django view 함수와 URLconfs의 기초에 대해 알아보았다. 본 chapter는 해당 기능에 대해 자세히 알아보자.

URLconf 요령

function import 간소화

chapter 3에서 만들었던 URLconf를 고려해보자.

from django.conf.urls.defaults import *
from mysite.views import hello, current_datetime, hours_ahead

urlpatterns = patterns('',
    (r'^hello/$', hello),
    (r'^time/$', current_datetime),
    (r'^time/plus/(\d{1,2})/$', hours_ahead),
)

chapter 3에서 설명하였듯이, URLconf의 각 항목은 연계할 view function을 포함하는데, function object로 바로 전달한다. 이것은 module의 윗부분에 해당 view 함수를 import 해야함을 의미한다.

그러나 django application이 복잡하게 성장하게 되면, URLconf 역시 커져간다. import 구문이 길어지게 되는데, 이를 줄일 수 있는 방법이 있다. 다음은 앞선 URLconf와 동일하다.

from django.conf.urls.defaults import *
from mysite import views

urlpatterns = patterns('',
    (r'^hello/$', views.hello),
    (r'^time/$', views.current_datetime),
    (r'^time/plus/(d{1,2})/$', views.hours_ahead),
)

django는 URLconf에서 특정 패턴을 위한 view 함수를 지정하는 다른 방법을 제공한다 : function object 그자체를 쓰기 보다 module 이름과 함수 이름을 포함한 문자열을 사용한다. 다음은 그 예이다.

from django.conf.urls.defaults import *

urlpatterns = patterns('',
    (r'^hello/$', 'mysite.views.hello'),
    (r'^time/$', 'mysite.views.current_datetime'),
    (r'^time/plus/(d{1,2})/$', 'mysite.views.hours_ahead'),
)

(인용부호를 사용함을 유의하라.)

더 나아가 공통의 "view prefix"를 제거하는 방법도 있다.  앞선 예에서 중복된 'mysite.views'로 모두 시작하고 있다. 이를 제거하는 것으로, 아래는 그 예이다.

from django.conf.urls.defaults import *

urlpatterns = patterns('mysite.views',
    (r'^hello/$', 'hello'),
    (r'^time/$', 'current_datetime'),
    (r'^time/plus/(d{1,2})/$', 'hours_ahead'),
)

prefix에 "."으로 끝나지 않음을 주의하라.

이러한 두가지 접근법을 염두해둔채, 어떤것이 더 좋을지 생각해 보자. 이는 단지 당신의 코딩 스타일에 의존한다.

문자열 접근법은 다음의 장점이 있다.
  • view function을 import하지 않기 때문에 간편하다.
  • 만약 view function이 여러개의 다른 python module로 나눠져 있다면 보다 읽고 관리하기 쉽다.

function object를 사용하는 장점은 다음과 같다.

  • view fuction을 "wrapping"하기 쉽다. 다음의 "view function을 wrapping하기"를 참고 바람
  • 좀더 python 스럽다. 이는 python의 전통을 따른다는 뜻이다.

두 접근법은 유효하고 동일한 URLconf에서 적절히 섞어 쓸 수 있다. 선택은 당신의 몪이다.

여러개의 view prefix 사용하기

만약 문자열 접근법을 사용한다면, 공통의 prefix를 만들어 내기가 힘들 수 있다. 그런 경우 patterns() object를 add하는 방법이 있다.

수정전:

from django.conf.urls.defaults import *

urlpatterns = patterns('',
    (r'^hello/$', 'mysite.views.hello'),
    (r'^time/$', 'mysite.views.current_datetime'),
    (r'^time/plus/(\d{1,2})/$', 'mysite.views.hours_ahead'),
    (r'^tag/(\w+)/$', 'weblog.views.tag'),
)

수정후:

from django.conf.urls.defaults import *

urlpatterns = patterns('mysite.views',
    (r'^hello/$', 'hello'),
    (r'^time/$', 'current_datetime'),
    (r'^time/plus/(\d{1,2})/$', 'hours_ahead'),
)

urlpatterns += patterns('weblog.views',
    (r'^tag/(\w+)/$', 'tag'),
)

모든 framework은 module-level의 변수인 urlpatterns를 다룬다. 이 변수는 위 예와 같이 동적으로 생성될 수 있다. 우리는 또한 이를 add 시켰다.

Debug mode에서의 URLs에 대한 특별한 case

urlpatterns를 동적으로 생성한다고 했는데, django의 debug mode동안 URLconf의 행위를 변경할 수 있는 기법이 있다. 이를 위해 runtime에서 DEBUG setting을 다음과 같이 확인하라.

from django.conf import settings
from django.conf.urls.defaults import *
from mysite import views

urlpatterns = patterns('',
    (r'^$', views.homepage),
    (r'^(\d{4})/([a-z]{3})/$', views.archive_month),
)

if settings.DEBUG:
    urlpatterns += patterns('',
        (r'^debuginfo/$', views.debug),
    )

이 예에서 /debuginfo/ URL은 DEBUG setting이 True인 경우 가능하다.

명명된(named) group 사용하기

여태껏 예제는 간단한 명명되지 않은(non-named) regular expression group을 사용하였다. 이는 URL에서 원하는 부분을 가져오기(capture) 위해 괄호를 사용했다는 것이고, django는 가져온 text를 positional argument로서 view function에 전달한다. 보다 고급 사용을 위해 명명된(named) regular expression group을 사용하여 URL 부분을 가져오고 view에 keyword argument로 전달한다.

keyword arguments vs. positional arguments

python function은 keyword argument와 positional argument를 사용할 수 있다. 어떤 경우에는 동시에 사용할 수 있다. keyword argument 호출은 전달할 값을 가지고 argument의 이름을 지정해야 한다. positional argument 호출은 값을 매치할 argument의 명시적인 지정없이 단순히 argument를 전달하면 된다. argument의 순서가 암묵적으로 적용된다.

예를 들어, 다음의 간단한 함수를 고려해 봐라.
def sell(item, price, quantity):
    print "Selling %s unit(s) of %s at %s" % (quantity, item, price)
positional argument로 호출한다면, 함수 정의에 있는 argument의 순서에 따라 전달한다.
sell('Socks', '$2.50', 6)
keyword argument로 호출하기 위해 값과 함께 argument의 이름을 지정해야 한다. 다음은 모두 동일하다.
sell(item='Socks', price='$2.50', quantity=6)
sell(item='Socks', quantity=6, price='$2.50')
sell(price='$2.50', item='Socks', quantity=6)
sell(price='$2.50', quantity=6, item='Socks')
sell(quantity=6, item='Socks', price='$2.50')
sell(quantity=6, price='$2.50', item='Socks')
마지막으로 섞어서 사용할 수 있는데, positional argument는 keyword argument에 앞서 전달되어야 한다. 아래는 위 예와 동일하다.
sell('Socks', '$2.50', quantity=6)
sell('Socks', price='$2.50', quantity=6)
sell('Socks', quantity=6, price='$2.50')
python의 regular expression에서 명명된 regular expression group의 문법은 (?P<name>pattern)으로 name은 group의 이름이고 pattern은 매치할 패턴이다.

아래는 명명되지 않은 group으로 된 URLconf의 예이다.

from django.conf.urls.defaults import *
from mysite import views

urlpatterns = patterns('',
    (r'^articles/(\d{4})/$', views.year_archive),
    (r'^articles/(\d{4})/(\d{2})/$', views.month_archive),
)

다음과 같이 이것을 명명된 group으로 만들 수 있다.

from django.conf.urls.defaults import *
from mysite import views

urlpatterns = patterns('',
    (r'^articles/(?P<year>\d{4})/$', views.year_archive),
    (r'^articles/(?P<year>\d{4})/(?P<month>\d{2})/$', views.month_archive),
)

이는 이전 예와 완전히 동일하게 동작하는데, 미묘한 한가지만 다르다 : 가져온(capture) 값은 positional argument 대신 keyword argument로 view function에 전달된다.

예를 들어, 명명되지 않은 group으로, /article/2006/03/ 은 아래와 같이 함수가 호출된다.
month_archive(request, '2006', '03')
명명된 group에서는, 아래와 같이 호출된다.
month_archive(request, year='2006', month='03')
명명된 group의 사용은 URRLconfs를 좀더 명시적으로 만들어주며, argument 순서에 따른 버그를 줄여준다. 그리고 view 함수 정의의 argument 순서를 재정할 수 있다.

물론, 명명된 group의 장점은 간결성에 있다. 몇몇 개발자는 명명된 group 문법이 못생기고 장황하다고 한다. 그럼에도 불구하고 regular expression 혹은 django application에 익숙하지 않은 사람들에게도 가독성의 장점이 있다.

matching/grouping 알고리즘 이해하기

URLconf에서 명명된 group을 사용하는 통고는 단일 URLconf pattern이 명명된거나 명명되지 않은 group 모두 포함할 수 없다는 것이다. 만일 그러하다면 django는 오류를 발생하지는 않으나 예상과 다르게 URL이 match되지 않는것을 발견할 것이다. 특별히 URLconf parse가 따르는 알고리즘을 명명된 group vs 명명되지 않은 group을 가지고 설명한다.
  • 명명된 argument가 있다면, 사용될 것이며, 명명되지 않은 argument는 무시된다.
  • 그렇지 않은 경우, positional argument 처럼 모든 명명되지 않은 argument에 전달한다.
  • 위 두 경우에 대해, keyword argument로 추가 option을 전달한다. 다음 section을 참고하라.

view function에 추가(extra) option 전달하기

한번쯤 당신은 조금만 다르고 비슷해 보이는 view 함수를 자성하고 있는 자신을 발견할 것이다. 예를 들어, 사용할 template를 제외하고 동일한 내용을 가지는 2개의 view를 가지고 있다고 가정하자.

# urls.py

from django.conf.urls.defaults import *
from mysite import views

urlpatterns = patterns('',
    (r'^foo/$', views.foo_view),
    (r'^bar/$', views.bar_view),
)

# views.py

from django.shortcuts import render_to_response
from mysite.models import MyModel

def foo_view(request):
    m_list = MyModel.objects.filter(is_new=True)
    return render_to_response('template1.html', {'m_list': m_list})

def bar_view(request):
    m_list = MyModel.objects.filter(is_new=True)
    return render_to_response('template2.html', {'m_list': m_list})

좋은 해결 방법으로 URLconf의 option parameter를 이용하는 것이다. URLconf의 각 pattern은 세번째 item을 가질 수 있는데, 그것은 keyword argument의 dictionarry를 view function에 전달한다.

이를 가지고 위 code를 다음과 같이 다시 작성할 수 있다.

# urls.py

from django.conf.urls.defaults import *
from mysite import views

urlpatterns = patterns('',
    (r'^foo/$', views.foobar_view, {'template_name': 'template1.html'}),
    (r'^bar/$', views.foobar_view, {'template_name': 'template2.html'}),
)

# views.py

from django.shortcuts import render_to_response
from mysite.models import MyModel

def foobar_view(request, template_name):
    m_list = MyModel.objects.filter(is_new=True)
    return render_to_response(template_name, {'m_list': m_list})

이와 같이 URLconf에 template_name을 지정하고 있다. view function은 다른 parameter로 그것을 다룬다.

이러한 추가 URLconf option 테크닉은 view function에게 추가 정보를 전달하는 아주 좋은 방법이다. 대부분의 generic view와 같은 경우 사용된다.(chapterr 11 참고)

URLconf에서 가져온(captured) 값을 속이기(faking)

pattern에 맞지 않는 URL이 있으나 동일한 view logic이 실행되어야 할 경우가 있다. 이 경우, 가져온 값을 "속일(fake)"수 있다.

예를 들어, 특정 일에 어떤 data를 출력해야 하는 application이 있다면, URLs는 다음과 같을 것이다.
/mydata/jan/01/
/mydata/jan/02/
/mydata/jan/03/
# ...
/mydata/dec/30/
/mydata/dec/31/
이것은 충분히 다루기 간단하다. 아래와 같이 URLconf할 수 있다. (명명된 group 사용)
urlpatterns = patterns('',
    (r'^mydata/(?P<month>\w{3})/(?P<day>\d\d)/$', views.my_view),
)
그럼 view function은 다음과 같다.
def my_view(request, month, day):
    # ....
이것까지는 복잡하지는 않다. month와 혹은 day가 포함되지 않는 URL을 my_view에서 처리할 수 있는 속임수를 사용해 보자.

예를 들어 /mydata/birthday/와 같은 URL을 추가하고자 한다면, /mydata/jan/06/과 일치하게 된다. 이를 해결하기 위해 URLconf의 추가 option을 사용할 수 있다.
urlpatterns = patterns('',
    (r'^mydata/birthday/$', views.my_view, {'month': 'jan', 'day': '06'}),
    (r'^mydata/(?P<month>\w{3})/(?P<day>\d\d)/$', views.my_view),
)
이를 통해 view function 자체의 수정없이 해결되었다. view function은 단지 month와 day parameter를 구하는 데에만 상관할 뿐이다.

View Generic 만들기

공통된 부분을 제거하는 것은 좋은 programming 훈련이다. 예를 들어, 다음 두개의 python function을 보아라.

def say_hello(person_name):
    print 'Hello, %s' % person_name

def say_goodbye(person_name):
    print 'Goodbye, %s' % person_name

이는 다음과 같이 하나의 parameter를 만들어 제거할 수 있다.
def greet(person_name, greeting):
    print '%s, %s' % (greeting, person_name)
이것을 추가 URLconf parameter를 사용하여 적용할 수 있다.

이를 가지고 view에 대해 높은 수준의 추상화를 만들 수 있을 것이다.
아래 code를 확인해 보아라.

# urls.py

from django.conf.urls.defaults import *
from mysite import views

urlpatterns = patterns('',
    (r'^events/$', views.event_list),
    (r'^blog/entries/$', views.entry_list),
)

# views.py

from django.shortcuts import render_to_response
from mysite.models import Event, BlogEntry

def event_list(request):
    obj_list = Event.objects.all()
    return render_to_response('mysite/event_list.html', {'event_list': obj_list})

def entry_list(request):
    obj_list = BlogEntry.objects.all()
    return render_to_response('mysite/blogentry_list.html', {'entry_list': obj_list})

두개의 view는 object list를 출력하는 것으로 동일하다. 이것을 제거해 보자.

# urls.py

from django.conf.urls.defaults import *
from mysite import models, views

urlpatterns = patterns('',
    (r'^events/$', views.object_list, {'model': models.Event}),
    (r'^blog/entries/$', views.object_list, {'model': models.BlogEntry}),
)

# views.py

from django.shortcuts import render_to_response

def object_list(request, model):
    obj_list = model.objects.all()
    template_name = 'mysite/%s_list.html' % model.__name__.lower()
    return render_to_response(template_name, {'object_list': obj_list})

이러한 작은 변화로, 재사용 가능한 view를 구하게 되었다. 이제 우리가 한 것들을 정리해 보자.

  • model class를 model parameter로서 직접 전달한다. 추가 URLconf option의 dictionary는 문자열 뿐만 아니라 어떤 종류의 python object도 가능하다.
  • model.object.all()은 duck typing의 예이다 : "오리처럼 걷고 오리처럼 말한다면, 그것을 오리로 다룬다". object model이 어떤 type인지 우리는 모르고 있다. 단지 요구되는 것은 model은 objects라는 attribute가 있어야 하고 all() method가 있어야 한다는 것이다.
  • model.__name__.lowe()를 사용하여 template 이름을 결정한다. 모든 python class는 class 이름을 전달하는 __name__ attribute를 가진다. 이 기능은 실행시(runtime)까지 class의 type을 모르는 경우에 유용하다. 예를 들어, BlogEntry class의 __name__은 'BlogEntry'라는 문자열이다.
  • 본 예제와 이전 예제의 차이점은, template에 generic 변수 이름인 object_list를 전달했다는 것이다. 우리는 blogentry_list 혹은 event_list가 되기 위해 이 변수 이름을 쉽게 변경할 수 있지만, 이는 남겨두었다.

database-driven web site는 몇몇 공통된 pattern을 가지고 있기 때문에, django는 시간을 절약해 줄 "generic view"라는 것을 가져왔다. 이는 chapter 11에서 다룰 예정이다.

view 설정 option 주기

django application을 배포하고 있다면, 설정의 정도를 원하는 사용자가 있을 것이다. 원하는 변경사항을 view에 추가하는것이 좋은 생각일 것이다. 이 목적으로 URLconf parameter를 사용할 수 있다.

설정을 만들기 위한 application의 일부는 다음과 같이 template 이름으로 가정하자.

def my_view(request, template_name):
    var = do_something()
    return render_to_response(template_name, {'var': var})

가져온(captured) 값 vs. 추가(extra) option의 우선권 이해하기

충돌이 있는 경우, 추가 URLconf parameter가 가져온 parameter보다 우선해서 전달한다. 다시 말해 만약 URLconf가 명명된 group 변수로 가져오고 추가 URLconf parameter로도 동일한 이름으로 가져왔을 경우, 추가 URLconf parameter 값이 사용된다.

아래는 그 예이다.

from django.conf.urls.defaults import *
from mysite import views

urlpatterns = patterns('',
    (r'^mydata/(?P<id>\d+)/$', views.my_view, {'id': 3}),
)

여기에서 regular express과 추가 dictionary에 id를 포함하고 있다. hard-code된 id가 우선권을 가지는데, 이는 /mydata/2/ 혹은 /mydata/432432/에서도 id에 3이 전달된다.

default view argument 사용하기

다른 편리한 속임수로 view의 argument에 default parameter를 지정하는 것이다. 이는 지정되지 않는 경우 default로 사용할 값을 view에 알려주는 것이다.

아래는 그 예이다.

# urls.py

from django.conf.urls.defaults import *
from mysite import views

urlpatterns = patterns('',
    (r'^blog/$', views.page),
    (r'^blog/page(?P<num>\d+)/$', views.page),
)

# views.py

def page(request, num='1'):
    # Output the appropriate page of blog entries, according to num.
    # ...

여기서, URL pattern 모두 동일한 view를 사용하고 있다는 점인데, 첫번째는 URL에 어떤것도 받아들이지 않는다. 만약 첫번째 pattern에 match된다면 page() function은 default parameter인 num을 '1'로 받아들이다. 만약 두번째 pattern이 match된다면 page()는 regular expression에 가져온 값을 전달한다.

(주의할 것은, '1'이 정수가 아니라 문자열임을 명심하라. 일관성 유지를 위해 가져온 값이 항상 문자열이기 때문에 그렇게 하였다.)

설정 option으로도 응용되고도 한다. 이 예는 이전 section에서의 예보다 좀더 발전되었다.
def my_view(request, template_name='mysite/my_view.html'):
    var = do_something()
    return render_to_response(template_name, {'var': var})

특별한 용도의(special-casing) view

큰 집합의 URL을 처리하는 하나의 pattern이 있는 경우, 가끔은 특별한 예외 처리를 하고 싶을 때가 있다. 이런 경우, URLconf가 순서대로 실행되는 방식의 장점을 살려, 그 예외를 선두에 놓도록 한다.

예를 들어, django admin site에 있는 "add an object" page를 생각해보자.
urlpatterns = patterns('',
    # ...
    ('^([^/]+)/([^/]+)/add/$', views.add_stage),
    # ...
)
이는 /myblog/entries/add와 /auth/groups/add/와 같은 것을 match한다. 그러나 /auth/user/add와 같은 page는 특별한 경우이다. 이를 특별한 용도로 처리할 수 있다.
def add_stage(request, app_label, model_name):
    if app_label == 'auth' and model_name == 'user':
        # do special-case code
    else:
        # do normal code
그러나 이는 view에 URL 논리가 들어가는 좋지 않는 경우이다. 보다 우아한 방법으로 URLconfs가 위에서 부터 아래의 순서로 실행되는 사실의 장점을 살리면 된다.
urlpatterns = patterns('',
    # ...
    ('^auth/user/add/$', views.user_add_stage),
    ('^([^/]+)/([^/]+)/add/$', views.add_stage),
    # ...
)

URL의 text 가져오기(capture)

각각의 가져온 argument는 regular express가 만든 match의 종류와 관계없이 python unicode 문자열로 전달된다. 다음과 같은 URLconf를 고려해 봐라.
(r'^articles/(?P<year>\d{4})/$', views.year_archive),
views.year_archive() view에 전달될 year arugment는 integer가 아닌 문자열이며, 심지어 \d{4}도 문자열이다.

이는 view code 작성시 염두해 두는것이 중요하다. 종종 datetime.date object에 문자열을 전달하면 오류가 발생한다.
>>> import datetime
>>> datetime.date('1993', '7', '9')
Traceback (most recent call last):
    ...
TypeError: an integer is required
>>> datetime.date(1993, 7, 9)
datetime.date(1993, 7, 9)
>>>
URLconf와 view로 번역하면 다음과 같고, 오류가 발생한다.

# urls.py

from django.conf.urls.defaults import *
from mysite import views

urlpatterns = patterns('',
    (r'^articles/(\d{4})/(\d{2})/(\d{2})/$', views.day_archive),
)

# views.py

import datetime

def day_archive(request, year, month, day):
    # The following statement raises a TypeError!
    date = datetime.date(year, month, day)

day_archive는 다음과 같아야 한다.
def day_archive(request, year, month, day):
    date = datetime.date(int(year), int(month), int(day))
int()에 만일 숫자가 아닌 다른 문자열이 입력되면 ValueError 예외가 발생함을 주의하라.

URLconf가 무었을 검색하는지 결정하기

request가 도착하면 django는 request URL에 대해 URLconf pattern을 python 문자열로 match한다. 이는 GET 혹은 POST parameter 그리고 domain 이름도 포함되지 않는다. 역시 맨 앞의 /는 포함되지 않는다.

예를 들어, http://www.example.com/myapp/ 요청이 있었다면 django는 myapp/를 match할 것이다. http://www.example.com/myapp/?page=3 이었다면 django는 myapp/을 match한다.

request method(POST 혹은 GET)은 URLconf에서 사용되지 않는다. 다시말해 모든 request method는 동일 URL에 라우트된다.

높은 추상화 수준의 view function

다음의 URLconf/view를 고려해보자.

# urls.py

from django.conf.urls.defaults import *
from mysite import views

urlpatterns = patterns('',
    # ...
    (r'^somepage/$', views.some_page),
    # ...
)

# views.py

from django.http import Http404, HttpResponseRedirect
from django.shortcuts import render_to_response

def some_page(request):
    if request.method == 'POST':
        do_something_for_post()
        return HttpResponseRedirect('/someurl/')
    elif request.method == 'GET':
        do_something_for_get()
        return render_to_response('page.html')
    else:
        raise Http404()

이 예에서, some_page() view의 POST/GET 처리는 꽤나 달라 보인다. 단지 공통된 부분은 /somepage/라는 공통된 URL 뿐이다. 이런 경우 view 함수를 POST 처리, GET 처리 하는 것으로 분리하는 것이 좋아 보인다.

다음은 그 예이다.

# views.py

from django.http import Http404, HttpResponseRedirect
from django.shortcuts import render_to_response

def method_splitter(request, GET=None, POST=None):
    if request.method == 'GET' and GET is not None:
        return GET(request)
    elif request.method == 'POST' and POST is not None:
        return POST(request)
    raise Http404

def some_page_get(request):
    assert request.method == 'GET'
    do_something_for_get()
    return render_to_response('page.html')

def some_page_post(request):
    assert request.method == 'POST'
    do_something_for_post()
    return HttpResponseRedirect('/someurl/')

# urls.py

from django.conf.urls.defaults import *
from mysite import views

urlpatterns = patterns('',
    # ...
    (r'^somepage/$', views.method_splitter, {'GET': views.some_page_get, 'POST': views.some_page_post}),
    # ...
)

설명하자면 다음과 같다.

  • method_splitter()라는 새로운 view 함수를 작성하였다. 이는 request.method 기반으로 다른 view를 선별한다. view function이여야 하는 GET과 POST라는 두개의 keyword argument를 검색한다. 만약 request.method가 'GET'이라면 GET view를 호출한다. 만약 request.method가 'POST'이라면 POST view를 호출한다. 만약 request.method가 어느것도 아니라면(예, HEAD), Http404를 발생한다.
  • URLconf에서 method_splitter()의 /somepage/를 주목하고 추가 argument로 전달한다.
  • 마지막으로 some_page() view는 두개의 view function으로 나뉜다. 이는 단일 view로 구성한 것보다는 좋아 보인다.

    이러한 view function들은 기술적으로 request.method 체크를 할 필요가 없는데, method_splitter()가 했기 때문이다. 그러나 보다 안전하게 확인하고 있다.

method_splitter()를 좀더 향상시킬 수 있는데, GET과 POST에 대해 가정하고 만들어졌다. 만일 method_splitter()에 URL로 부터 가져온 값이던지 혹은 optional keyword argument를 받을수 없을까?

이를 위해 *(asterisks)가 포함된 변수를 사용할 것이다. 다음은 그 예이다.

def method_splitter(request, *args, **kwargs):
    get_view = kwargs.pop('GET', None)
    post_view = kwargs.pop('POST', None)
    if request.method == 'GET' and get_view is not None:
        return get_view(request, *args, **kwargs)
    elif request.method == 'POST' and post_view is not None:
        return post_view(request, *args, **kwargs)
    raise Http404
여기에서 method_splitter()는 GET과 POST keyword argument가 제거되고 *args와 *kwargs가 들어가게 되었다. 이는 실행시(runtime)까지 argument의 이름의 개수를 알수 없을 때, 동적으로 받아들이도록 하는 것이다. 단일 *를 사용한 것은 모든 positional argument를 단일 tuple로 받아들임을 의미한다. **는 모든 keyword argument를 dictionary로 받아들임을 의미한다.

예를 들어 다음 함수를 보아라.
def foo(*args, **kwargs):
    print "Positional arguments are:"
    print args
    print "Keyword arguments are:"
    print kwargs
그 동작은 다음과 같다.
>>> foo(1, 2, 3)
Positional arguments are:
(1, 2, 3)
Keyword arguments are:
{}
>>> foo(1, 2, name='Adrian', framework='Django')
Positional arguments are:
(1, 2)
Keyword arguments are:
{'framework': 'Django', 'name': 'Adrian'}
>>>
method_splitter()로 돌아와서 *args와 **kwargs는 함수에 모든 argument를 받아들이고 적합한 view에 전달한다. 그러기 이전에 kwargs.pop()은 GET과 POST argument를 구하기 위해 호출하였다. (pop()에서 기본값 None을 사용하여 정의되지 않는 경우 발생할 KeyError를 예방한다.)

view 함수를 wrapping하기

마지막 view 속임수로 python 테크닉을 이용하는 것이다. 다음과 같이 중복 code를 확인해 보아라.

def my_view1(request):
    if not request.user.is_authenticated():
        return HttpResponseRedirect('/accounts/login/')
    # ...
    return render_to_response('template1.html')

def my_view2(request):
    if not request.user.is_authenticated():
        return HttpResponseRedirect('/accounts/login/')
    # ...
    return render_to_response('template2.html')

def my_view3(request):
    if not request.user.is_authenticated():
        return HttpResponseRedirect('/accounts/login/')
    # ...
    return render_to_response('template3.html')

여기서 각 view는 request.user가 인증(authenticated)되었는지 체크함으로 시작한다(request.user는 chapter 14에서 진행됨).

만약 위와 같은 조그마한 중복된 부분을 제거하기 위해, 다음과 같이 view wrapper를 작성하면 된다.
def requires_login(view):
    def new_view(request, *args, **kwargs):
        if not request.user.is_authenticated():
            return HttpResponseRedirect('/accounts/login/')
        return view(request, *args, **kwargs)
    return new_view
이 function은 view function을 받고 새로운 view function을 리턴한다. 새로운 view function은 requres_login 내에 정의된 new_view이고 그것은 request.user.is_authenticated()를 체크하는 logic 처리하고 원래의 view를 위임한다.

이제 wrap된 view를 적용하면 다음과 같다.

from django.conf.urls.defaults import *
from mysite.views import requires_login, my_view1, my_view2, my_view3

urlpatterns = patterns('',
    (r'^view1/$', requires_login(my_view1)),
    (r'^view2/$', requires_login(my_view2)),
    (r'^view3/$', requires_login(my_view3)),
)


다른 URLconf 포함(include)하기

만약 여러개의 django 기반 site에서 사용할 code를 의도한다면, URLconf를 포함하는 것을 고민해야 한다.

URLconf는 다른 URLconf module을 "포함"할 수 있다. 다음은 그 예이다.

from django.conf.urls.defaults import *

urlpatterns = patterns('',
    (r'^weblog/', include('mysite.blog.urls')),
    (r'^photos/', include('mysite.photos.urls')),
    (r'^about/$', 'mysite.views.about'),
)

(chapter 6에서 소개된바 있다)

여기서 중요한점이 하나 있다 : 본 예에서 include를 사용하는 regular expression에서는 $가 없다. 그러나 끝에 /를 붙이고 있다. django는 include()를 만나게 되면 match된 URL의 부분을 자르고 남아있는 문자열을 이후의 작업을 위해 include한 URLconf에 전달한다.

이 예에서 계속하여, mysite.blog.urls의 URLconf는 다음과 같다.

from django.conf.urls.defaults import *

urlpatterns = patterns('',
    (r'^(\d\d\d\d)/$', 'mysite.blog.views.year_detail'),
    (r'^(\d\d\d\d)/(\d\d)/$', 'mysite.blog.views.month_detail'),
)

이러한 두개의 URLconfs를 가지고 다음을 참고하라.

  • /weblog/2007/ : 첫 URLconf인 r'^weblog/'가 match된다. 왜냐하면, include가 사용되었기 때문이다. django는 match된 text인 'weblog/'를 제거한다. 남은 부분의 URL인 2007/이 mysite.blog.urls URLconf의 첫부분에 의해 match된다.
  • /weblog//2007/ : 첫 URLconf인 r'^weblog/'가 match된다. 왜냐하면, include가 사용되었기 때문이다. django는 match된 text인 'weblog/'를 제거한다. 남은 부분인 /2007/은 mysite.blog.urls URLconf에서 어떤것도 match되지 못한다.
  • /about/ : URLconf의 첫부분인 mysite.views.about view에 match된다. include를 사용한 것과 사용하지 않은 것을 섞어 쓸 수 있다.

include를 사용했을때, parameter를 어떻게 가져오는가?

include를 사용한 URLconf는 부모(parent) URLconf로 부터 parameter를 가져온다. 다음은 그 예이다.

# root urls.py

from django.conf.urls.defaults import *

urlpatterns = patterns('',
    (r'^(?P<username>\w+)/blog/', include('foo.urls.blog')),
)

# foo/urls/blog.py

from django.conf.urls.defaults import *

urlpatterns = patterns('',
    (r'^$', 'foo.views.blog_index'),
    (r'^archive/$', 'foo.views.blog_archive'),
)

이 예에서 가져온 username 변수는 include된 URLconf와 URLconf내의 모든 view function에 전달된다.

가져온 parameter는 include된 URLconf에 받아들일 수 있는지 여부와 관계없이 항상 있는 모든 line에 전달된다. 이러한 이유로 이 테크닉은 당신이 URLconf에 있는 모든 view가 당신이 전달한 parameter를 받아들이는 것을 확신할 때 사용할 수 있다.

include를 사용했을떄, 추가 URLconf option는 어떻게 동작하는가?

유사하게도 추가 URLconf option을 include에 전달할 수 있다. 이를 위해 include된 URLconf에 각각의 line은 추가 option을 전달받게 된다.

예를 들어, 다음 두개의 URLconf 집합은 기능적으로 동일하다.

# urls.py

from django.conf.urls.defaults import *

urlpatterns = patterns('',
    (r'^blog/', include('inner'), {'blogid': 3}),
)

# inner.py

from django.conf.urls.defaults import *

urlpatterns = patterns('',
    (r'^archive/$', 'mysite.views.archive'),
    (r'^about/$', 'mysite.views.about'),
    (r'^rss/$', 'mysite.views.rss'),
)

# urls.py

from django.conf.urls.defaults import *

urlpatterns = patterns('',
    (r'^blog/', include('inner')),
)

# inner.py

from django.conf.urls.defaults import *

urlpatterns = patterns('',
    (r'^archive/$', 'mysite.views.archive', {'blogid': 3}),
    (r'^about/$', 'mysite.views.about', {'blogid': 3}),
    (r'^rss/$', 'mysite.views.rss', {'blogid': 3}),
)

'django > the django book study' 카테고리의 다른 글

[09] django template의 고급 기능  (8) 2012.09.27
[07] django의 form 처리  (0) 2012.05.06
[06] django admin site  (3) 2012.04.27
[05] django의 model  (8) 2012.04.12
[04] django의 template  (14) 2012.04.05