2012. 9. 27. 11:14

[09] django template의 고급 기능

참고 : http://www.djangobook.com/en/2.0/chapter09/

대부분 django template language의 작업은 template 작성자의 역할임에도 불구하고, template engine을 확장하고 수정하고 싶을 때가 있다.

본 chapter는 django template system에 대해 연구한다. 그 system을 확장하고 싶다던지, 어떻게 동작하는지에 대해 다룬다. 역시 auto-escaping 기능에 대해서도 다룬다.

다른 application의 일부로 django template system을 사용하려 한다면, "독립(standalone) 모드에서의 template system 설정하기" section을 참고하기 바란다.

template lanaguage 리뷰

우선, chapter 4에서 다루었던 용어 정리부터 해보자.

  • template는 text 문서 혹은 일반적인 python 문자열로 django template language를 사용하는 marked up 언어이다.
  • template tag는 어떤것을 수행하는 template안에 있는 기호(symbol)이다. 그 정의는 의도적으로 애매하다. 예를 들어 template tag는 제어 구조(if 혹은 for loop)로 content를 생산할 수 있고, database로 부터 content를 가지거나 혹은 다른 template tag를 접근할 수 있다.

    template tag는 {$ 와 $}로 둘러쌓여있다.
    {% if is_logged_in %}
        Thanks for logging in!
    {% else %}
        Please log in.
    {% endif %}
  • 변수는 template내의 값을 출력하는 기호이다.

    변수 tag는 {{ 와 }}로 둘러쌓여있다.
    My first name is {{ first_name }}. My last name is {{ last_name }}.
  • context는 template에 전달할 name->value 매핑(python dictionary와 유사)이다.
  • template는 변수 "구멍"을 context로 부터 값을 교체하여 context를 render하고 모든 template tag를 실행한다.

보다 자세한 내용은, chapter 4(2012/04/05 - [django/the django book study] - [04] django의 template)를 참고 바란다.

RequestContext와 Context 처리기(processor)

template를 render할 때, context가 필요하다. 이는 django.template.Context의 instance이다. 그러나 django는 django.template.RequestContext라는 특별한 subclass를 제공하는데, 동작 방식이 약간 다르다. RequestContext는 다수의 변수를 디폴트로 template context에 추가할 수 있다. 이는 HttpRequest object나 현재 로그인한 user 정보와 유사하다.

연속된 template에서 같은 변수 집합을 지정할 필요가 없는 경우 RequestContext를 사용한다.
다음 예를 보아라.

from django.template import loader, Context

def view_1(request):
    # ...
    t = loader.get_template('template1.html')
    c = Context({
        'app': 'My app',
        'user': request.user,
        'ip_address': request.META['REMOTE_ADDR'],
        'message': 'I am view 1.'
    })
    return t.render(c)

def view_2(request):
    # ...
    t = loader.get_template('template2.html')
    c = Context({
        'app': 'My app',
        'user': request.user,
        'ip_address': request.META['REMOTE_ADDR'],
        'message': 'I am the second view.'
    })
    return t.render(c)

각 view는 app, user, 그리고 ip_address와 같은 동일한 3개의 변수를 전달한다. 중복된 code를 제거하는 것이 좋아 보이지 않을까?

RequestContext와 context processor는 이 문제를 해결하기 위해 만들어졌다. context processor는 각 render_to_response() 호출에 있는 변수를 지정할 필요 없이 자동으로 각 context의 집합을 구한 변수을 지정할 수있도록 해준다. Context 대신 RequestContext를 사용한다.

context processor를 사용하는 가장 저수준의 방법은 processor를 생성하고 RequestContext에 전달하는 것이다. 아래는 그 예이다.

from django.template import loader, RequestContext

def custom_proc(request):
    "A context processor that provides 'app', 'user' and 'ip_address'."
    return {
        'app': 'My app',
        'user': request.user,
        'ip_address': request.META['REMOTE_ADDR']
    }

def view_1(request):
    # ...
    t = loader.get_template('template1.html')
    c = RequestContext(request, {'message': 'I am view 1.'},
            processors=[custom_proc])
    return t.render(c)

def view_2(request):
    # ...
    t = loader.get_template('template2.html')
    c = RequestContext(request, {'message': 'I am the second view.'},
            processors=[custom_proc])
    return t.render(c)

이를 정리하면 다음과 같다.

  • custom_proc 이라는 context 처리기를 정의하였다. 그것은 HttpRequest를 받고, template context에 사용할 변수들의 dictionary를 리턴한다.
  • Context 대신 RequestContext를 사용하기 위해 두개의 view 함수를 수정하였다. 두개의 차이점은 context가 어떻게 만들어지냐이다. 하나, RequestContext는 HttpRequest object를 필요로 하고 그것은 일등으로 view function에 전달된다. 둘, RequestContext는 processors라는 옵션 argument를 받는데, 사용할 context 처리기의 tuple 이나 list를 의미한다. 여기서는 custom_proc을 전달한다.
  • 각 view는 더이상 context 생성시 app, user 혹은 ip_address가 필요 없는데, 이들은 custom_proc에서 제공되기 때문이다.
  • 각 view는 여전히 필요한 custom template 변수를 소개하는데 유연성이 있다. 예를 들어, message template 변수는 각 view에서 다르게 세팅된다.

chapter 4에서 render_to_response()를 소개했는데, loader.get_template()를 호출하는 것을 절약할 수 있고 그다음 template에 render() method를 호출한다. context 처리기의 저수준 작업을 보여주기 위해 render_to_response()는 호출하지 않는다. 단 그러나 render_to_response()에 context 처리기를 사용하는 것이 가능하다. 이는 다음ㄱ과 같이 context_instance argument와 쓰인다.

from django.shortcuts import render_to_response
from django.template import RequestContext

def custom_proc(request):
    "A context processor that provides 'app', 'user' and 'ip_address'."
    return {
        'app': 'My app',
        'user': request.user,
        'ip_address': request.META['REMOTE_ADDR']
    }

def view_1(request):
    # ...
    return render_to_response('template1.html',
        {'message': 'I am view 1.'},
        context_instance=RequestContext(request, processors=[custom_proc]))

def view_2(request):
    # ...
    return render_to_response('template2.html',
        {'message': 'I am the second view.'},
        context_instance=RequestContext(request, processors=[custom_proc]))

본 코드를 평가함에 있어, 다른 것들에 비해 과도한 면이 있음을 인정해야 한다. 중복된 부분을 제거하였는데, context processor를 사용하는 것이 processors를 타이핑 하는 것과 비교하여 크게 절약시켜 주진 않는다.

이러한 이유로 django는 global context processor를 지원한다. TEMPLATE_CONTEXT_PROCESSORS 세팅은 RequestContext에 항상 적용될 context processor를 지정하게 한다.

디폴트로 TEMPLATE_CONTEXT_PROCESSORS는 다음과 같다.
TEMPLATE_CONTEXT_PROCESSORS = (
    'django.core.context_processors.auth',
    'django.core.context_processors.debug',
    'django.core.context_processors.i18n',
    'django.core.context_processors.media',
)
이 설정은 위에 있는 custom_proc 함수 즉 context에 합쳐질 itemp의 dictionary를 전달하고 argument로 request object 취하는 함수로, 동일한 interface를 사용하는 호출하능한 tuple이다. TEMPLATE_CONTEXT_PROCESSORS는 문자열로 지정된다.

각 pocessor는 순서대로 적용된다.

django는 다수의 context processor를 지원하고 기본으로 몇개가 활성화되어 있다.

django.core.context_processors.path
만약 TEMPLATE_CONTEXT_PROCESSORS에 이 processor가 포함되었다면, 모든 RequestContext는 다음 변수를 가진다.

  • user : 현재 로그인한 user를 표현하는 django.contrib.auth.models.User instance이다. (로그인하지 않았따면, AnonymousUser instance)
  • message : 로그인한 user에 대한 문자열 message 리스트. 이 변수는 모든 request에 대해 request.user.get_and_delete_message()를 호출한다. 이 method는 user의 message를 수집하고 database로 부터 삭제한다.
  • perms : django.core.context_processors.PermWrapper의 instance이다. 이는 현재 로그인한 user의 퍼미션을 나타낸다.

user, message, 그리고 perms는 이후 chapter 14에서 다루기로 한다.

django.core.context_processors.debug

이 processor는 template 계층에 디버깅 정보를 전달한다. 만약 TEMPLATE_CONTEXT_PROCESSORS에 포함되었다면, 모든 RequestContext는 다음 변수를 가진다.

  • debug : DEBUG 설정 값 (True 혹은 False). template에서 debug mode 여부를 알려준다.
  • sql_queries : {'sql':..., 'time':... } 형태의 dictionary list로, request동안 발생한 모든 SQL query와 수행 시간을 표시한다.

debugging 정보는 민감하기 때문에 아래와 같은 조건일 때 동작한다.

  • DEBUG 설정이 True
  • INTERNAL_IPS 설정에 포함된 IP로 부터 접근

django.core.context_processors.i18n

이 processor가 활성화되면, 다음 변수 사용이 가능하다.

  • LANGUAGE : LANGUAGE 설정 값
  • LANGUAGE_CODE : request.LANGUAGE_CODE 값(존재한다면). 그렇지 않다면 LANGUAGE_CODE 설정 값

django.core.context_processors.request

만약 이 processor가 활성화되면, 모든 RequestContext는 request 변수를 포함하는데, 현재의 HttpRequest object를 가르킨다. 이 설정은 기본으로 포함되어 있지 않으며, 직접 활성화해야 한다.

{{ request.REMOTE_ADDR }}와 같은 형태로 사용 가능하다.

역자주) 위 의 TEMPLATE_CONTEXT_PROCESSORS를 기존의 예제(search.html)에 덧붙여 테스트하면 다음과 같다.

# settings.py
...
INTERNAL_IPS = (
 '127.0.0.1',
 'localhost',
)

TEMPLATE_CONTEXT_PROCESSORS = (
    'django.contrib.auth.context_processors.auth',
    'django.core.context_processors.debug',
    'django.core.context_processors.i18n',
    'django.core.context_processors.media',
    'django.core.context_processors.request',

)
...

# views.py (mysite/books)
...
def search(request):
    errors = []
    if 'q' in request.GET:
        q = request.GET['q']
        if not q:
            errors.append('Enter a search term.')
        elif len(q) > 20:
            errors.append('Please enter at most 20 characters.')
        else:
            books = Book.objects.filter(title__icontains=q)
            return render_to_response('search_results.html',
                {'books': books, 'query': q},
        context_instance=RequestContext(request))

        return render_to_response('search_form.html',
            {'errors': errors})
...

# search_results.html (mysite/templates)
<p>You searched for: <strong>{{ query }}</strong></p>

{% if books %}
    <p>Found {{ books|length }} book{{ books|pluralize }}.</p>
    <ul>
        {% for book in books %}
        <li>{{ book.title }}</li>
        {% endfor %}
    </ul>
{% else %}
    <p>No books matched your search criteria.</p>
{% endif %}
<hr><b>Context Processor Test</b><hr>
<p>user :<br/>{{ user }}</p>
<p>message :<br/>{{ message }}</p>
<p>perms :<br/>{{ perms }}</p>
<p>debug :<br/>{{ debug }}</p>
<p>sql_queries :<br/>{{ sql_queries }}</p>
<p>LANGUAGE :<br/>{{ LANGUAGE }}</p>
<p>LANGUAGE_CODE :<br/>{{ LANGUAGE_CODE }}</p>
<p>request :<br/>{{ request }}</p>

자기 자신의 Context Processor 작성 가이드 라인

  • 가능한 작은 기능 집합으로 구성하라.
  • TEMPLATE_CONTEXT_PROCESSORS에 있는 어떠한 context processor도 setting file에 의해 활성화되면 모두 사용가능하다. 그래서 변수명 선정에 유의하라.
  • context_processors.py를 만들어 관리하면 TEMPLATE_CONTEXT_PROCESSORS setting이 편리해진다.

자동화된(automated) HTML escaping

template로 부터 HTML이 만들어질때, 다음 예제와 과 같은 리스크가 존재한다.

Hello, {{ name }}
사용자 이름을 출력하는 단순한 내용이지만, 사용자 이름에 따라, 다음과 같은 현상이 발생할 수 있다.
Hello, <script>alert('hello')</script>
즉, Javascript alert box가 발생할 수 있다.
그리고, 만일 name에 <가 포함된 경우,
Hello, <b>username
와 같을 수 있으며, 이후의 모든 pagee의 text가 bold체가 될 수 있다.

이와 같이 악의적 사용에 대비하여 사용자 전달 data에 대해 신뢰를 해서는 안된다.
(XSS등 자세한건 chapter 20을 참고 바람)

이 문제를 해결하기 위해 다음 두개의 option을 고려해야 한다.

  • escape filter를 사용한다. 이는 유해한 HTML 문자를 그렇지 않도록 변환시켜 준다. django의 초창기 기본 해결 방법으로 응용되었는데, 당신에게 책임을 전가하는 문제가 있다. 개발자와 template 작성자 모두 모든것에 대해 escape 처리를 고려해야 한다. 그럼에도 escape 처리를 깜빡하는 경우가 쉽게 발생한다.
  • 자동 HTML escaping을 사용한다.

django에서는 디폴트로 모든 template는 자동으로 escape를 처리한다. 특별히 다음 다섯개의 문자가 escape된다.

  • < 는 &lt;
  • > 는 &gt;
  • ' 는 &#39;
  • " 는 &quot;
  • & 는 &amp;

이와 같이 django template system은 자동으로 보호해준다.

어떻게 off하는가?

만일 raw HTML를 render하도록 의도하였다면, 자동화된 escape가 불필요할 수 있다. 즉, 많은 신뢰된 HTML을 미리 저장해 놓고 불러오는 경우가 포함된다.

개별 변수에 대해

아래와 같이 safe filter를 사용한다.

This will be escaped: {{ data }}
This will not be escaped: {{ data|safe }}
만약 data에 'b'가 포함되어 있다면, 다음과 같다.
This will be escaped: &lt;b&gt;
This will not be escaped: <b>

Template block에 대해

autoescape tag를 사용한다.
{% autoescape off %}
    Hello {{ name }}
{% endautoescape %}
위와 같이 강제로 off 시킬 수 있다.

Auto-escaping is on by default. Hello {{ name }}

{% autoescape off %}
    This will not be auto-escaped: {{ data }}.

    Nor this: {{ other_data }}
    {% autoescape on %}
        Auto-escaping applies again: {{ name }}
    {% endautoescape %}
{% endautoescape %}

이는 include tag에서도 적용된다.

# base.html

{% autoescape off %}
<h1>{% block title %}{% endblock %}</h1>
{% block content %}
{% endblock %}
{% endautoescape %}

# child.html

{% extends "base.html" %}
{% block title %}This & that{% endblock %}
{% block content %}{{ greeting }}{% endblock %}

만약 greeting 변수에 <b>Hello!</b>가 포함되어 있다면, 다음과 같이 render 된다.
<h1>This & that</h1>
<b>Hello!</b>

주의

일반적으로 template 작성자는 auto-escaping에 대해 그리 걱정하지 않는다. python 개발자(view와 custom filter 작성자)는 어떤 data가 escape되면 안되는지를 생각해야 하고, 필요시 template를 체크하고 수정해야 한다.

auto-escaping이 on인지 아닌지 확신이 서지 않는 경우, escape filter를 이용하여 escape를 수행할 수 있다. 만일 auto-escaping이 on이라면 이중(double)으로 escaping이 일어날 위험은 없다.

filter argument 문자열에 대한 자동화된 escaping

다음과 같이 filter argument가 가능하다.
{{ data|default:"This is a string literal." }}
모든 arugment 문자열은 template에 자동으로 escaping되지 않은채 추가된다. 즉, safe filter를 통해 들어온 것처럼 동작된다. 왜냐하면, template 작성자는 escape된 text를 넣을 것으로 확신하기 때문이다.
즉, 이는 다음을 의미한다.
{{ data|default:"3 &lt; 2" }}
를 사용해야 한다. 다음을 사용해서는 안된다.
{{ data|default:"3 < 2" }}  <-- Bad! Don't do this.

Template loading 깊숙히

일반적으로 filesystem에 file로 template을 저장한다.  다른 source로 부터 template를 로드하기 위해 template loader를 수정할 수 있다.

django는 다음 방법으로 template를 로드한다.

  • django.temlpate.loader.get_template(template_name) :
    get_template는 주어진 이름을 가지는 template에 대해 compile된 template(Template object)를 리턴한다. 만약 없는 경우, TemplateDoesNotExist 예외가 발생한다.
  • django.template.loader.select_template(temlpate_name_list) :
    select_template는 get_template와 유사한데, template 이름의 list를 사용하는것만 다르다. list에서 처음으로 존재하는 template를 리턴한다. 없는 경우 TemplateDoesNotExist 예외가 발생한다.

chapter 4에서 봤듯이 이러한 각 함수는 디폴트로 TEMPLATE_DIRS 설정을 사용하여 template를 로드한다. 내부적으로 그러나 이러한 함수는 부하가 거린다.

몇몇 loader는 디폴트로 비활성화 되어 잇는데, TEMPLATE_LOADERS 설정으로 활성화 할 수 있다. TEPMLATE_LOADERS는 문자열 tuple이어야 하고 template loader를 표시해야 한다.

  • django.template.loaders.filesystem.Loader
    TEMPLATE_DIRS로 부터 template를 file system으로 부터 로드한다. 디폴트값이다.
  • django.template.loaders.app_directories.Loader
    file system으로 부터 로드한다. INSTALLED_APPS의 있는 각 application에 대해 loader는 'templates'라는 hard code된 subdirectory를 찾는다. 만일 존재한다면, 그곳으로 부터 template를 찾는다.
    이는 개별 application에 대해 template를 저장할 수 있다는 것이며 쉽게 배포할 수 있다. 예를 들어, INSTALLED_APPS가 ('myproject.polls', 'myproject.music')과 같다면 get_template('foo.html')는 다음과 같은 순서로 찾게 된다.
    /path/to/myproject/polls/templates/foo.html
    /path/to/myproject/music/templates/foo.html
    디폴트로 설정되어 있다.
  • django.template.loaders.eggs.Loader
    app_direrctories와 유사하되 file system 대신 python eggs로 부터 로드한다. 디폴트로 비활성화 되어 있다.

Template system 확장 하기

custom code로 system을 확장하는 방법을 알아보자.

대부분의 template customization은 template tag나 filter를 수정하는 형태이다. 비록 django에서 많은 built-in tag와 filter가 있지만 당신 고유의 tag나 filter library를 만들고 싶을 것이다.

template library 생성하기

tag나 filter를 custom하려면 우선 template library를 생성해야 한다.

이는 다음과 같은 두단계로 이뤄져 있다.

  • 우선, 어떤 django application에서 template library를 보관할지를 결정햐야 한다. 만일 manage.py startapp을 통해 app을 생성하였다면 그곳에 둘 수 있다. 혹은 template library를 위해 단독으로 또다른 app을 생성할 수 있다. 우리는 후자를 추천한다.

    어떤 방법이든 INSTALLED_APPS setting에 추가해야 함을 잊지 마라. 그 이유는 이후에 설명한다.
  • 다음으로 적당한 django application의 package에 templatetags directory를 생성한다. 이는 models.py, views.py와 같은 레벨에 위치해야 한다. 다음은 그 예이다.
    books/    __init__.py    models.py    templatetags/    views.py
    templatetags directory에 비어있는 __init__.py와 custom tag/filter가 정의된 파일을 생성한다. 정의된 파일 이름은 사용할 tag 이름이 된다. 예를 들어, 만일 custom tag와 filter가 poll_extras.py로 되었다면, 다음과 같이 template을 작성할 수 있다.
    {% load poll_extras %}
    {% load %} tag는 INSTALLED_APPS setting을 살펴보고 설치된 django application내에서 template library들을 load하는 것만 단지 허락한다. 이것은 보안을 위함이다.

만약 어떤 특정한 model/view를 접근하지 않는 template library를 작성하려고 한다면, templatetags package만 포함된 django application인 것은 유효하며 정상이다. templatetags package에 추가할 module 개수는 제한이 없다. {% load %} statement가 tag/filter를 load하는 것을 염두해 두길 바란다.

유효한 tag library를 위해 module은 template.Library instace에 있는 register라는 이름의 module-level 변수가 있어야 한다. 당신 모듈의 상단에 아래가 있어야 한다.

from django import template

register = template.Library()

위와 같이 register 변수가 생성되었다면 template filter와 tag를 생성할 수 있다.

Template Filter Custom하기

filter를 custom하는 것은 한 두개의 argument를 가지는 python 함수라 생각하면 된다.

  • 변수 값 (input)
  • argument의 값은 기본값을 가질 수 있다.

예를 들어 {{ var|foo:"bar" }} filter 내에서 foo 라는 filter는 var 변수값을 전달 받고 argument로 "bar"를 받는다.

filter 함수는 반드시 무엇을 return해야 한다. 그렇지 않으면 exception이 발생하게 된다.

아래는 filter 정의의 예이다.

def cut(value, arg):
    "Removes all values of arg from the given string"
    return value.replace(arg, '')
그리고 filter가 변수값으로 부터 공백을 제거하는 방법은 아래와 같다.
{{ somevariable|cut:" " }}
대부분의 filter는 argument를 받지 않는다. 다음이 가능하다.
def lower(value): # Only one argument.
    "Converts a string into all lowercase"
    return value.lower()
filter를 일단 정의하였다면 Library instance에 등록해야 한다. 그러면 django template language에서 사용이 가능해 진다.
register.filter('cut', cut)
register.filter('lower', lower)
Library.filter() method는 다음과 같이 두개의 argument를 받는다.

  • filter의 이름 (string)
  • filter function

만일 python 2.4 그 이상을 사용하고 있다면 다음과 같이 register.filter()를 decorate할 수 있다.

@register.filter(name='cut')
def cut(value, arg):
    return value.replace(arg, '')

@register.filter
def lower(value):
    return value.lower()
만일 name argument를 비워둔다면 django에서는 function 이름을 filter의 이름으로 간주할 것이다.

아래는 template library에 대한 종합 예제이다.
from django import template

register = template.Library()

@register.filter(name='cut')
def cut(value, arg):
    return value.replace(arg, '')

Template tag custom하기

tag는 filter보다 복잡하다.

chapter 4에서 template system이 어떻게 2단계 process로 동작하는지가 설명되었다(compiling/rendering). template tag를 custom하기 위해 django에게 위 두 단계를 처리하는 방법을 알려줘야 한다.

django가 template를 compile할 때, template text를 node라는 것으로 분리한다. 각 node는 django.template.Node의 instance로 render() method를 가지고 있다. 즉, compile된 template는 단순히 Node object의 list일 뿐이다. 아래는 그 예이다.
Hello, {{ person.name }}.

{% ifequal name.birthday today %}
    Happy birthday!
{% else %}
    Be sure to come back on your birthday
    for a splendid surprise message.
{% endifequal %}
compile된 template form에서 이 template는 node의 list로 표현되어 진다.

  • Text node : "Hello, "
  • Variable node : person.name
  • Text node : ".\n\n"
  • IfEqual node : name.birthday / today

만일 render()를 호출하였다면 template는 node list에 있는 각각의 node에 대해 render()를 주어진 context로 호출한다. 그 결과는 template의 output을 구성하기 위해 결합되어 진다. 즉 custom template tag를 정의하기 위해서 당신은 원본 template tag가 어떻게 Node(compile된)로 변환되어야 하는지와 node의 render()가 어떻게 동작해야 하는지를 기술해야 한다.

다음의 section에서 tag를 custom 단계를 설명한다.

Compile된 Function 작성하기

parser가 각 template tag를 만날때 해당 tag와 parser object를 가지고 python function을 호출한다. 그 function은 tag의 content를 기반으로 Node instance를 리턴해야 할 책임이 있다.

예를 들어, {% current_time %}이라는 template tag를 작성한다고 하자. 다음과 같이 tag에 syntax를 전달하면 좋을 것이다.

<p>The time is {% current_time "%Y-%m-%d %I:%M %p" %}.</p>
이 function에 대한 parser는 parameter를 붙잡고 Node object를 생성한다.

from django import template

register = template.Library()

def do_current_time(parser, token):
    try:
        # split_contents() knows not to split quoted strings.
        tag_name, format_string = token.split_contents()
    except ValueError:
        msg = '%r tag requires a single argument' % token.split_contents()[0]
        raise template.TemplateSyntaxError(msg)
    return CurrentTimeNode(format_string[1:-1])
위 예에 대한 설명은 다음과 같다.

  • 각 template tag compilation function은 parser와 token argument를 받는다. parser는 template parser object이다. 여기서는 그것을 사용하지는 않고 있다. token은 parser에 의해 현재 막 parse된 token이다.
  • token.contents는 tag의 원본 content에 해당되는 string이다. 위 예에서는 'current_time "%Y-%m-%d %I:%M %p"'가 된다.
  • token.split_contents() method는 공백으로 argument를 구별한다. 물론 "가 있는 경우에는 함께 묶는다. token.contents.split() 사용을 피하라. 동작이 그다지 견고하지 못하다.
  • 이 함수는 django.template.TemplateSyntaxError 예외 발생에 대해 책임이 있다.
  • 에러 message에 tag의 이름을 hard-code하지 마라. 왜냐하면 tag 이름이 당신 function에 결합되기 때문이다. token.split_contents()[0]은 항상 당신의 tag 이름이 들어 있다.
  • CurrentTtimeNode를 리턴한다. 해당 case에서는 "%Y-%m-%d %I:%M %p"를 전달한다. 시작과 끝의 "는 format_string[1:-1]에 의해 제거된다.
  • Node subclass를 반드시 리턴해야 하며 그렇지 않은 경우 오류가 발생한다.

Template node 작성하기

custom tag를 작성하기 위한 두번째 단계로 render() method를 가지는 Node subclass를 정의하는 일이다. 위 예에서 CurrentTimeNode를 정의해야 한다.

import datetime

class CurrentTimeNode(template.Node):
    def __init__(self, format_string):
        self.format_string = str(format_string)

    def render(self, context):
        now = datetime.datetime.now()
        return now.strftime(self.format_string)
__init__()과 render()와 같은 두 function은 template를 처리하는 두 단계(compilation/rendering)를 직접 매핑한다. 따라서 초기화 function은 단지 이후 사용될 format string을 저장하며 render() function은 실제 자업을 하게된다.

tag 등록하기

마지막으로 모듈의 library instance를 가지고 tag를 등록하는 일이 남았다. custom tag의 등록은 filter 등록과 매우 유사하다. 단지 template.Library instance를 가지고 tag() method를 호출하면 된다. 다음은 그 예이다.
register.tag('current_time', do_current_time)
tag() method는 두개의 argument를 받는다.

  • template tag의 이름
  • compilation function

filter와 유사하게 reigster.tag를 사용할 수 있다.

@register.tag(name="current_time")
def do_current_time(parser, token):
    # ...

@register.tag
def shout(parser, token):
    # ...
만일 name argument를 비워둔다면 django에서는 function 이름을 tag 이름으로 사용한다.

context에 변수 설정하기

앞선 예에서는 단순히 값을 리턴하는 형태였다. 종종 값을 리턴하는 것 보다는 template 변수를 설정하는 것이 유용할 때가 많다. 그 말은 template 작성자는 template tag가 설정한 변수를 사용할 수 있다는 것이다.

context에 변수를 설정하기 위해 dictionary 할당을 render() method에 있는 context object에 사용할 수 있다. 아래는 앞선 예에서, 리턴하는 것대신 template 변수를 사용한 것으로 수정한 것이다.
class CurrentTimeNode2(template.Node):
    def __init__(self, format_string):
        self.format_string = str(format_string)

    def render(self, context):
        now = datetime.datetime.now()
        context['current_time'] = now.strftime(self.format_string)
        return ''
render() 함수는 ''를 리턴함을 주의하라. render()는 항상 string을 리턴해야 하기 때문이다.

아래는 위 예제가 적용되었을 때의 사용법이다.
{% current_time2 "%Y-%M-%d %I:%M %p" %}
<P>The time is {{ current_time }}.</P>
그러나 CurrentTimeNode2에 다음과 같은 문제가 있다 : current_time 이름의 변수는 핟드코드되어 있다. 이는 당신의 template에서는 {{ current_time }} 이라는 template를 사용할 수 없다는 것을 의미하는데, 이는 {% current_time2 %}에서 해당 변수를 overwrite하기 때문이다.

이를 위해 다음음과 같이 수정한다.
{% get_current_time "%Y-%M-%d %I:%M %p" as my_current_time %}

The current time is {{ my_current_time }}.

관련된 function은 다음과 같다.
import re

class CurrentTimeNode3(template.Node):
    def __init__(self, format_string, var_name):
        self.format_string = str(format_string)
        self.var_name = var_name

    def render(self, context):
        now = datetime.datetime.now()
        context[self.var_name] = now.strftime(self.format_string)
        return ''

def do_current_time(parser, token):
    # This version uses a regular expression to parse tag contents.
    try:
        # Splitting by None == splitting by spaces.
        tag_name, arg = token.contents.split(None, 1)
    except ValueError:
        msg = '%r tag requires arguments' % token.contents[0]
        raise template.TemplateSyntaxError(msg)

    m = re.search(r'(.*?) as (\w+)', arg)
    if m:
        fmt, var_name = m.groups()
    else:
        msg = '%r tag had invalid arguments' % tag_name
        raise template.TemplateSyntaxError(msg)

    if not (fmt[0] == fmt[-1] and fmt[0] in ('"', "'")):
        msg = "%r tag's argument should be in quotes" % tag_name
        raise template.TemplateSyntaxError(msg)

    return CurrentTimeNode3(fmt[1:-1], var_name)
다른 template tag 까지 parsing하기

Template tags는 다른 tag를 포함하는 block처럼 동작한다(예, {% if %}, {% for %}. ...). 이러한 template tag를 생성하기 위해 parser.parse()를 당신의 function에 추가한다.

아래는 표준 {% comment %} tag를 구현한 것이다.
def do_comment(parser, token):
    nodelist = parser.parse(('endcomment',))
    parser.delete_first_token()
    return CommentNode()

class CommentNode(template.Node):
    def render(self, context):
        return ''
parser.parse()는 parse가 끝날 template tag에 해당되는 이름의 tuple을 받아들이다. 그리고 그것은 django.template.NodeList의 instance를 리턴하는데, tuple에 있는 어떤 이름을 parser 만나기 직전에 해당되는 모든 Node object의 리스트이다.

앞선 예에서 nodelist는 {% comment %}와 {% endcomment %}사이의 모든 node의 list이지, {% comment %}와 {% endcomment %} 그 자체는 아니다.

parser.parse()가 호출되었다면 parser는 {% endpoint %} tag를 "섭취"하지는 않았다. 그래서 parrser.delete_first_token()을 명시적으로 호출해야 한다.

CommentNode.render()는 단순히 비어있는 문자열을 리턴한다. {% comment %}와 {% endcomment %} 사이는 무시된다.

다른 template tag까지 parsing하고 content 저장하기

이전 예제의 do_comment()는 {% comment %}와 {% endcomment %} 사이의 모든 것을 버리는 것으로 되어 있는데 뭔가 다른 일도 할 수 있다.

예를 들어, custom template tag로 {% upper %}와 {% endupper %}사이의 문자를 대문자로 변경하는 것을 생각해 보자.
{% upper %}
    This will appear in uppercase, {{ user_name }}.
{% endupper %}
이전 예와 같이, parser.parse()를 사용할 것이다. 이번에는 nostlist를 Node에 전달할 것이다.
def do_upper(parser, token):
    nodelist = parser.parse(('endupper',))
    parser.delete_first_token()
    return UpperNode(nodelist)

class UpperNode(template.Node):
    def __init__(self, nodelist):
        self.nodelist = nodelist

    def render(self, context):
        output = self.nodelist.render(context)
        return output.upper()
새로운 개념은 단지 self.nodelist.render(context) 뿐이다. 이것은 단순히 node list의 각 Node에 render()를 호출한 것이다.

{% if %}, {% for %}, {% ifequal %} 그리고 {% ifchanged %}와 같은 예를 보고자 한다면 django/template/defaulttags.py를 참고하기 바란다.

tag 사용 단순화 하기

많은 template tag는 string 혹은 template 변수와 같은 단일 argument를 받는다. 그리고 input argument에 대해 처리된 string을 리턴한다. 예를 들어 앞서 작성했던 current_time tag는 다양하게 나타난다. format string을 전달하고 string으로된 time을 전달 받는다.

이러한 종류의 tag의 생성을 쉽게하기 위해 django는 simple_tag라는 helper function을 제공한다. django.template.Library에 있는 이 함수는 하나의 argument를 받아들이는데, render function에서 그것을 감싸(wrap)고 template system에 등록한다.

이전의 current_time function은 다음과 같이 작성할 수 있다.
def current_time(format_string):
    try:
        return datetime.datetime.now().strftime(str(format_string))
    except UnicodeEncodeError:
        return ''

register.simple_tag(current_time)
python 2.4에서는 다음이 가능하다.
@register.simple_tag
def current_time(token):
    # ...
simple_tag helper function 관련된 내용은 다음과 같다.

  • function에 단 하나의 argument만 전달된다.
  • argument의 수를 체크하는 것은 이미 이뤄진 것이기 때문에 그것을 수행할 필요는 없다.
  • "문자는 제거된 plain unicode string을 전달받는다.

inclusion tag

다른 통상적인 template tag는 다른 tamplate에 의해 render된 몇몇 data를 출력하기도 한다. 예를 들어 django admin interface는 "add/change" 버튼을 출력할 때 custom template tag를 사용한다. 이 버튼들은 동일하게 보여지나 편집된 object에 따라 link의 target은 다르게 된다. 그들은 현재 object로 부터의 상세 정보로 채워진 작은 template를 사용하는 용도에 적합하다.

이러하튼 종류의 tag를 inclusion tag라 한다. 이전 chapter에서 진행되었던 books에 대한 Author object를 예로 들어보자.

{% books_for_author author %}
그 결과는 다음과 같다.
<ul>
    <li>The Cat In The Hat</li>
    <li>Hop On Pop</li>
    <li>Green Eggs And Ham</li>
</ul>
우선 augument를 받아들일 function을 정의하고 결과를 위한 data의 dictionary를 만들어야 한다. dictionary만 리턴되는것을 주의하라.
def books_for_author(author):
    books = Book.objects.filter(authors__id=author.id)
    return {'books': books}
다음엔 tag의 output을 출력하는데 사용될 template를 만든다.
<ul>
{% for book in books %}
    <li>{{ book.title }}</li>
{% endfor %}
</ul>
마지막으로 Library object에 있는 inclusion_tag() method를 호출하여 inclusion tag를 생성하고 등록한다.

만약 book_snippet.html이라는 파일에 해당 template이 있었다면 다음과 같이 tag를 등록해야 한다.
register.inclusion_tag('book_snippet.html')(books_for_author)
python 2.4 decorator 문법이라면 다음이 가능하다.
@register.inclusion_tag('book_snippet.html')
def books_for_author(author):
    # ...
가끔 당신의 inclusion tag는 부모 template의 context를 접근할 필요가 있다. 이를 위해 django는 takes_context option을 사용할 수 있다. 만약 takes_context를 inclusion tag 생성시 명시하였다면 tag는 argument가 요구되지 않고 그 밑의 python function은 tag가 호출된 template ccontext안 단 하나의 argument를 가지게 된다.

예를 들어 main page의 back을 가르키는 home_link와 home_title 변수를 포함하는 context를 항상 사용해야 하는 inclusion tag를 작성한다고 할 때, 다음과 같다.
@register.inclusion_tag('link.html', takes_context=True)
def jump_link(context):
    return {
        'link': context['home_link'],
        'title': context['home_title'],
    }
(fuunction에 전달되는 첫 parameter는 반드시 호출된 context여야 한다.)

link.html template는 다음을 포함할 것이다.
Jump directly to <a href="{{ link }}">{{ title }}.
그 다음, 위 custom tag를 사용하려면 library를 load하고 argument 없이 다음과 같이 호출한다.
{% jump_link %}

custom template loader 작성하기

django의 built-in template loader는 template를 load하는 요구를 모두 충족하고 있지만 특별한 loading 논리를 만들고 싶을 때가 있다. 예를 들어 database 혹은 Subversion의 python binding을 이용한 Subversion repository 혹은 압축된 zip으로 부터 직접 template를 load하는 경우이다.

template loader(다시 말해 TEMPLATE_LOADERS에 있는 각 entry)는 다음 interface로 호출 가능한 object로 예상된다.
load_template_source(template_name, template_dirs=None)
template_name argument는 load할 template의 이름이고 template_dirs는 TEMPLATE_DIRS 대신 search할 directory의 list이다.

만약 loader가 template load를 성공하였다면 tuple을 리턴해야 한다 : (template_source, tmeplate_path). 여기서 template_source는 template engine에 의해 compile된 template string이며 template_path는 load된 template의 path가 된다. 해당 path는 debug 목적을 위해 사용자에게 보여지기 위함이다.

만약 loader가 template을 load할 수 없다면 django.template.TemplateDoesNotExist 예외가 발생한다.

각 loader fuction은 is_usable fuction attribute를 가지고 있다. 이것은 이 loader가 현재 설치된 python에서 가능한지를 여부를 template engine에게 알려주는 boolean이다. 예를 들어, eggs loader는 pkg_resource module이 설치되지 않았다면 is_usable을 False로 설정한다.

아래는 ZIP file로 부터 template를 load하는 template loader function이다.
from django.conf import settings
from django.template import TemplateDoesNotExist
import zipfile

def load_template_source(template_name, template_dirs=None):
    "Template loader that loads templates from a ZIP file."

    template_zipfiles = getattr(settings, "TEMPLATE_ZIP_FILES", [])

    # Try each ZIP file in TEMPLATE_ZIP_FILES.
    for fname in template_zipfiles:
        try:
            z = zipfile.ZipFile(fname)
            source = z.read(template_name)
        except (IOError, KeyError):
            continue
        z.close()
        # We found a template, so return the source.
        template_path = "%s:%s" % (fname, template_name)
        return (source, template_path)

    # If we reach here, the template couldn't be loaded
    raise TemplateDoesNotExist(template_name)

# This loader is always usable (since zipfile is included with Python)
load_template_source.is_usable = True

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

[08] django Views와 URLconfs의 고급 기능  (0) 2012.05.30
[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