2012. 5. 6. 00:15

[07] django의 form 처리

참조 : http://www.djangobook.com/en/2.0/chapter07/

HTML form은 google 검색 box의 단순함에서 부터 복잡한 data interface를 가지는 blog 댓글까지 internet web site의 근간이 된다. 본 chapter는 django로 하여금 사용자가 제출한 form data를 어떻게 접근할 것이고 유효성 체크를 한다던지 그리고 그것으로 뭔가를 하는 것들에 대해 다룰 것이다. 따라가다 보면 HttpRequest와 Form object에 대해 학습할 것이다.

Request object로 부터 data form 구하기

chapter 3에서 HttpRequest에 대해 설명했는데 자세히 다루지는 않았다. 각각의 view 함수는 HttpRequest를 첫 parameter로 받아들이고 있다.

from django.http import HttpResponse

def hello(request):
    return HttpResponse("Hello world")

여기에서 request 변수와 같은 HttpRequest object는 당신히 친숙해져야 하는 많은 속성과 method를 가지는데 무엇이 가능한지 미리 알아두면 좋기 때문이다. 이러한 속성은 현재 request(예, django로 만들어진 site에 현재 page를 로드하는 user/web 브라우져)에 관한 정보를 얻기위해 사용되며 이는 view 함수가 실행되는 시점이다.

URL에 대한 정보

HttpRequest object는 현재 요청된 URL에 대해 다음과 같은 정보로 나눠져 있다.
Attribute/method
설명
예제
request.path domain을 제외한 전체 경로. 단, /로 시작한다. "/hello/"
request.get_host() host (소위 "domain"으로 불림) "127.0.0.1:8000"
www.example.com
request.get_full_path() path에서 query 문자열을 합친것
(가능하다면)
"/hello/?print=true"
request.is_secure() HTTPS를 통하였다면 True.
그 이외에는 False
True 혹은 False
당신 virew 함수에 하드코드된것을 쓰지 말고 attribute/method를 이용하도록 하라. 다른 곳에서 쓰일때 보다 유연하게 만들어 준다. 다음은 그 예이다.

# BAD!
def current_url_view_bad(request):
    return HttpResponse("Welcome to the page at /current/")

# GOOD
def current_url_view_good(request):
    return HttpResponse("Welcome to the page at %s" % request.path)


(ps. 역자주. 이후부터 몇몇 view 함수에 대한 test를 /hello/로 연결하여 보여줍니다.)

request에 대한 다른 정보

request.META는 주어진 request에 대한 모든 가능한 HTTP header를 포함하고 있는 python dictionary이다. 이를 통해 사용자 IP 주소 그리고 user agent(일반적으로 web 브라우져의 이름과 버전)를 구할 수 있다. 가능한 header의 모든 목록은 user에서 보낸 header와 당신의 web server의 설정에 의존한다. 주요한 dictionary의 key는 다음과 같다.

  • HTTP_REFERER - 가능한 참조 URL (스펠 주의)
  • HTTP_USER_AGENT - user가 사용하는 browser의 user-agent 문자열.
    (예, "Mozilla/5.0 (X11; U; Linux i686; fr-FR; rv:1.8.1.17) Gecko/20080829 Firefox/2.0.0.17")
  • REMOTE_ADDR - client ip 주소 (예, "12.345.67.89". (만약 proxy를 통해 전달되었다면, ','로 구별되어 전달된다. (예, 12.345.67.89,23.456.78.90")))

request.META는 단지 python dictionary이기 때문에 존재하지 않는 key에 접근할 때 KeyError 예외가 발생하게 된다.(왜냐하면 HTTP header는 외부(external) data 이기 때문인데, 그것은 user browser에 의해 제출되어 발생한 것이다. 그것은 믿을 수 없으며 특정 header가 없거나 비어있는 것에 대한 안정적인 실패 처리가 필요하다.) try/except 혹은 get() method를 사용하여 정의되지 않은 key 경우를 대비할 수 있다.

# BAD!
def ua_display_bad(request):
    ua = request.META['HTTP_USER_AGENT']  # Might raise KeyError!
    return HttpResponse("Your browser is %s" % ua)

# GOOD (VERSION 1)
def ua_display_good1(request):
    try:
        ua = request.META['HTTP_USER_AGENT']
    except KeyError:
        ua = 'unknown'
    return HttpResponse("Your browser is %s" % ua)

# GOOD (VERSION 2)
def ua_display_good2(request):
    ua = request.META.get('HTTP_USER_AGENT', 'unknown')
    return HttpResponse("Your browser is %s" % ua)


(IE6.0으로 테스트. BAD! GOOD Version 1, 2 모두 동일 결과)
다음과 같이 request.META의 모든 정보를 출력하는 view를 간단히 만들 수 있다.
def display_meta(request):
    values = request.META.items()
    values.sort()
    html = []
    for k, v in values:
        html.append('<tr><td>%s</td><td>%s</td></tr>' % (k, v))
    return HttpResponse('<table>%s</table>' % '\n'.join(html))

ALLUSERSPROFILE C:\Documents and Settings\All Users
APPDATA C:\Documents and Settings\Administrator\Application Data
CLIENTNAME Console
COMMONPROGRAMFILES C:\Program Files\Common Files
COMPUTERNAME GREENFIS-FE7395
COMSPEC C:\WINDOWS\system32\cmd.exe
CONTENT_LENGTH
CONTENT_TYPE text/plain
CSRF_COOKIE 4qQPmMDWPY3LZPPHoKoNLgfEazVFkYiA
DJANGO_SETTINGS_MODULE mysite.settings
FP_NO_HOST_CHECK NO
GATEWAY_INTERFACE CGI/1.1
HOMEDRIVE C:
HOMEPATH \Documents and Settings\Administrator
HTTP_ACCEPT */*
HTTP_ACCEPT_ENCODING gzip, deflate
HTTP_ACCEPT_LANGUAGE ko
HTTP_CONNECTION Keep-Alive
HTTP_COOKIE csrftoken=4qQPmMDWPY3LZPPHoKoNLgfEazVFkYiA; sessionid=05d7f09d22d15465723588730c745091
HTTP_HOST localhost:8000
HTTP_USER_AGENT Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)
LOGONSERVER \\GREENFIS-FE7395
NUMBER_OF_PROCESSORS 1
OS Windows_NT
PATH C:\WINDOWS\system32;C:\WINDOWS;C:\WINDOWS\System32\Wbem;C:\Python25;C:\Python25\Lib\site-packages\django\bin
PATHEXT .COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH
PATH_INFO /hello/
PROCESSOR_ARCHITECTURE x86
PROCESSOR_IDENTIFIER x86 Family 6 Model 42 Stepping 7, GenuineIntel
PROCESSOR_LEVEL 6
PROCESSOR_REVISION 2a07
PROGRAMFILES C:\Program Files
PROMPT $P$G
QUERY_STRING
REMOTE_ADDR 127.0.0.1
REMOTE_HOST
REQUEST_METHOD GET
RUN_MAIN true
SCRIPT_NAME
SERVER_NAME localhost
SERVER_PORT 8000
SERVER_PROTOCOL HTTP/1.1
SERVER_SOFTWARE WSGIServer/0.1 Python/2.5.4
SESSIONNAME Console
SYSTEMDRIVE C:
SYSTEMROOT C:\WINDOWS
TEMP C:\DOCUME~1\ADMINI~1\LOCALS~1\Temp
TMP C:\DOCUME~1\ADMINI~1\LOCALS~1\Temp
USERDOMAIN GREENFIS-FE7395
USERNAME Administrator
USERPROFILE C:\Documents and Settings\Administrator
WINDIR C:\WINDOWS
wsgi.errors
wsgi.file_wrapper wsgiref.util.FileWrapper
wsgi.input
wsgi.multiprocess False
wsgi.multithread True
wsgi.run_once False
wsgi.url_scheme http
wsgi.version (1, 0)
훈련 삼아, HTML을 hard-coding하는것 대신 django의 template system을 사용하여 변환하는것을 볼 것이다. 역시 이전 section에 있었던 request.path와 다른 HttpRequest method를 추가하는 것도 해볼 것이다.

제출(submitted) data에 대한 정보

request에 대한 기본적인 metadata외에도 HttpRequest object는 user에 의해 전송된 정보를 가지는 두개의 attribute(request.GET / request.POST)를 가지고 있다. 이러한 dictionary 유사(dictionary-like) object는 GET과 POST data에 대한 접근을 가능하게 한다.
Dictionary 유사(dictionary-like) object

request.GET과 request.POST를 설명할 때 "dictionary 유사" object라 하였는데, 이는 표준 python dictionary처럼 행동하지만, 그 안을 들여다 볼때 기술적으로 dictionary는 아니기 때문이다. 예를 들어, request.GET과 request.POST는 get(), keys(), 그리고 values() method를 지원하고 심지어 for key in request.GET를 사용함으로 key들에 대해 iterate할 수 있다.

그럼 그 차이는 무었인가? 보통의 dictonary가 가지고 있지 않는 추가적인 method를 가지고 있다. 이에 대해 좀더 확인해볼 것이다.

이와 비슷하게 "file 유사(file-like)" object에 대해서도 맞닥들일 수 있는데, read()와 같은 기본적인 method를 가지고 "실(real)" file object처럼 행동하는 python object이다.
POST data는 HTML <form>으로 부터 일반적으로 제출되는 반면, GET data는 <form>으로 부터 올 수 있거나  page의 URL에 있는 query 문자열로 부터도 올 수 있다.

간단한 form 처리 예제

book, author, 그리고 publisher에서 진행했던 예제를 계속하여 title로 book database를 검색하는 view를 만들어 보도록 하자.

일반적으로 form을 개발하기 위해 두단계를 거치게 된다. 이 두단계는 HTML user interface와 뒷단에서 전송된(submitted) data를 처리하는 view code로 구성된다. 첫 단계는 간단한데, search form을 출력할 view를 구성하면 된다.

from django.shortcuts import render_to_response

def search_form(request):
    return render_to_response('search_form.html')

chater 3에서 배웠듯이 이 view는 python path어디에서도 사용이 가능하다. 일단 books/views.py에 넣도록 하자.

template를 위한 search_form.html은 다음과 같다.
<html>
<head>
    <title>Search</title>
</head>
<body>
    <form action="/search/" method="get">
        <input type="text" name="q">
        <input type="submit" value="Search">
    </form>
</body>
</html>
(ps. 위 파일은 아래와 같이 settings.py에 TEMPLATE_DIRS이 세팅된 경우, /mysite/mysite/templates/에 복사하여 넣는다.)
TEMPLATE_DIRS = (
    os.path.join(os.path.dirname(__file__), 'templates').replace('\\','/'), 
)

urls.py에 URLpattern은 다음과 같이 한다.

from mysite.books import views

urlpatterns = patterns('',
    # ...
    (r'^search-form/$', views.search_form),
    # ...
)

(views module을 "from mysite.views import search_form"과 유사한 형태 대신 바로 importing하고 있는데 덜 복잡하기 때문이다. 이에 대해서는 chapter 8에서 좀더 자세히 다루도록 한다.)

이제 http://localhost:8000/search-form/를 들어가 보면, 다음과 같은 단순한 검색 interface가 등장한다.

만약 "Search"를 눌러 form을 제출한다면, django 404 오류를 보게 된다. /search/가 구현되지 않았다고 가르켜 주는데, 다음과 같이 수정해 보자. (역자주. views.py는 mysite가 아닌 book app directory에 있는 views.py 이다)

# urls.py

urlpatterns = patterns('',
    # ...
    (r'^search-form/$', views.search_form),
    (r'^search/$', views.search),
    # ...
)

# views.py

from django.http import HttpResponse

def search(request):

    if 'q' in request.GET:
        message = 'You searched for: %r' % request.GET['q']
    else:
        message = 'You submitted an empty form.'
    return HttpResponse(message)


우선 이는 단지 사용자의 검색 단어만 표시할 뿐인데, django에 data가 잘 전달되었는지 확인하기 위함이고 system을 통해 어떻게 검색이 이뤄지는지를 알려줄 수 있다.

  1. HTML <form>은 변수 q를 정의한다. 만약 전송(submit)되었다면 /search/ URL에 GET(method="get")을 통해 q의 값이 전달된다.
  2. /search/ URL을 처리하는 django view는 request.GET의 q 값을 통해 접근 가능하다.

여기에서 중요한 것은 request.GET에 존재하는 'q'를 명시적으로 체크하는 것이다. 앞의 request.META section에서 알렸듯이 첫 위치에 전달되었다고 가정하거나 사용자에 의해 전달 되었다고 신뢰해서는 안된다. 만약 이것을 체크하지 않는다면, 비어있는 form 전달로 인해 KeyError 예외가 발생하게 된다.

# BAD!
def bad_search(request):
    # The following line will raise KeyError if 'q' hasn't
    # been submitted!
    message = 'You searched for: %r' % request.GET['q']
    return HttpResponse(message)

POST data는 request.GET 대신 request.POST를 사용하는것 빼고는 GET data와 동일하게 작동된다. 그럼 GET과 POST간의 차이점은 무었일까? data를 "구하(get)"기 위해 단지 요청하는 form인 경우에 GET을 사용하면 된다. POST는 data를 변경(change), e-mail 전송, data의 단순 표기를 위한 뭔가등에 사용하면 된다. 이곳 예제에서는 GET을 사용할 것인데, 왜냐하면 server에 어떤 data도 수정되지 않기 때문이다. (GET과 POST에 대해 자세한건 http://www.w3.org/2001/tag/doc/whenToUseGet.html를 참조바람)

request.GET이 제대로 전달되었는지를 확인하였다면, 이제 database로 부터 사용자의 search query를 실행하는 방법을 알아보도록 하자. (views.py를 다시 들여다 보자)
from django.http import HttpResponse
from django.shortcuts import render_to_response
from mysite.books.models import Book

def search(request):
    if 'q' in request.GET and request.GET['q']:
        q = request.GET['q']
        books = Book.objects.filter(title__icontains=q)
        return render_to_response('search_results.html',
            {'books': books, 'query': q})
    else:
        return HttpResponse('Please submit a search term.')
위의 내용을 정리하면 아래와 같다.

  • database query를 수행하기 전에, request.GET에 'q'가 있는지 체크한것 외에도 역시 request.GET['q']가 비어있는 값이 아닌지도 확인한다.
  • Book.objects.filter(title__icontains=q)를 사용하여 책제목에서 주어진 내용을 포함하는 모든 책을 query 한다. icontains는 lookup type(chapter 5와 부록 b에 설명됨)이고, 해당 statement는 "대소문자 구별없이 q를 포함하고 있는 title의 책을 구하라"로 대략 번역된다.

    이는 book을 검색하는 간단한 방법이다. 그러나 큰 규모의 database에서 단순히 icontains를 사용하는 것은 추천하지 않는데, 이는 느리기 때문이다. (실제로는 몇몇 정렬을 위한 검색 system을 수정하길 원할 때가 있을 것이다. open-source full-text search로 한번 web에서 검색해 보길 바란다.)
  • Book object의 list를 template에 전달한다. search_results.html template는 다음과 같다.
    <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 %}
    pluralize라는 template filter를 사용함을 주목하라. 이는 검색한 books의 개수에 따라 필요하다면 끝에 "s"를 붙여준다.

단순하게 처리했던 form 예제를 향상시키기

앞선 chapter에서 처럼, 가능한 작업에 대해 가장 간단한 방법을 확인했다. 이제 몇몇 문제점을 제시하고 그것을 해결해 보자.

우선, search() view에서 비어있는 query에 대한 처리가 빈약하다. 단지 "Please submit a search term."를 표시할 뿐이고, 뒤로가기 버튼을 선택하기를 요구한다. 이는 비전문적인 방법이고 만약 바깥으로 배포했다면 django가 가지는 특권을 없애는 일이 된다.


오류와 함께 form을 다시 표시하여 사용자가 다시 입력하도록 하는것이 훨씬 효과적일 것이다. template를 다시 그리는 가장 간단한 방법은 다음과 같다.
from django.http import HttpResponse
from django.shortcuts import render_to_response
from mysite.books.models import Book

def search_form(request):
    return render_to_response('search_form.html')

def search(request):
    if 'q' in request.GET and request.GET['q']:
        q = request.GET['q']
        books = Book.objects.filter(title__icontains=q)
        return render_to_response('search_results.html',
            {'books': books, 'query': q})
    else:
       return render_to_response('search_form.html', {'error': True})
(search_form()을 포함하였는데, 한곳에 두개를 볼 수 있도록 하기 위함이다.)

만일 query가 비어있다면, search()에서 search_form template를 다시 render할 수 있도록 하였다. 그리고, template에서 오류 메시지 출력이 필요하기 때문에, template 변수를 전달하였다. 이제 search_form.html에서 error 변수값을 체크하도록 수정한다.
<html>
<head>
    <title>Search</title>
</head>
<body>
    {% if error %}
        <p style="color: red;">Please submit a search term.</p>
    {% endif %}

    <form action="/search/" method="get">
        <input type="text" name="q">
        <input type="submit" value="Search">
    </form>
</body>
</html>

search_form()이 사용되는 view인 위 template가 처음 실행될 때는 error 값이 전달되지 않기 때문에, 오류 메시지는 표시되지 않는다.

그자리에 이 수정으로 좀더 좋은 application이 되었는데, 이 질문을 할 때가 되었다 : 특정한 작업으로 사용된 search_form() view가 과연 필요할까? 그것은 즉, /search/ (GET parameter 없는) url에 해당되는 request은 empty form으로 표시되고 에러 메시지를 남기게 된다.

누군가가 /search/에 GET parameter 없이 접근하였을 때, 오류 메시지를 숨길 수 있도록 search()를 수정하기만 한다면, 연관된 URLpattern 수정과 함께 search_form()을 제거할 수 있을 것이다.
def search(request):
    error = False
    if 'q' in request.GET:
        q = request.GET['q']
        if not q:
            error = True
        else:
            books = Book.objects.filter(title__icontains=q)
            return render_to_response('search_results.html',
                {'books': books, 'query': q})
    return render_to_response('search_form.html',
        {'error': error})

이 수정된 view로 GET parameter 없이 /search/를 방문한다면, 위와 같이 오류 메시지 없는 것을 볼 수 있다. 만일 비어있는 query로 전달하여 'q'가 비었다면 오류 메시지를 가지고 표시될 것이다. 그리고 마지막으로 'q'에 값이 있다면 검사 결과를 확인할 수 있다.

이제 마지막 수정으로 중복된 부분을 줄이도록 하자. 이제 두개의 view와 하나로 된 URL을 가지고 뒹굴었고, /search/는 search-form과 출력을 표시한다. search_form.html의 HTML <form>은 다음과 같이 hard-code된 URL이 있는데, 이제 이는 굳이 필요가 없어져 버렸다.
<form action="/search/" method="get">
은 다음과 같이 수정 가능하다.
<form action="" method="get">
action=""는 현재 page에 있는 URL에 form을 전달하라는 뜻이다. 해당 수정으로 search()를 다른 URL에서 사용할 때 함께 수정할 필요가 없게 되었다.

간단한 검증(validation)

우리의 검색 예제는 여전히 단순한데 특히 data 검증 부분에서 그러하다. 단지 비어있는 값에 대해서만 확인했을 뿐이다. 많은 HTML form은 비어있는지 확인하는것 이상의 훨씬 복잡한 수준의 검증을 포함하고 있다. 우리는 다음과 같은 오류 메시지를 본 적이 있을 것이다.

  • "정확한 e-mail 주소를 입력하세요. 'foo'는 e-mail 주소가 아닙니다."
  • "정확한 우편번호를 입력하세요. '123'은 우편번호가 아닙니다."
  • "정확한 날짜를 YYYY-MM-DD 형태로 입력하세요."
  • "최소 8자리와 한개 이상의 숫자로 구성된 비밀번호를 입력하세요,"

JavaScript 검증

이것은 이 책을 띄어넘는 부분인데, client side에서도 브라우져에서 직접 data 검증을 수행할 수 있다. 그러나, server side에서도 반드시 data 검증을 수행해야 한다. 몇몇의 사람들은 JavaScript 설정을 끈채로 실행하기도 한데, 몇몇의 나쁜 사용자는 피해를 유발할 수 있는 지를 볼 수 있도록 form 처리기에 직접 검증되지 않은 값을 전달하기도 한다.

사용자가 전달한 data를 항상 server-side에서 검증하도록 하라. JavaScript를 이용한 검증은 단지 보너스로 생각하길 바란다.
이제 search() view에서 20글자 이하로 입력되도록 수정하자. 그걸 어떻게 할 것인가? view에 직접 그 논리를 넣으면 된다.
def search(request):
    error = False
    if 'q' in request.GET:
        q = request.GET['q']
        if not q:
            error = True
        elif len(q) > 20:
            error = True
        else:
            books = Book.objects.filter(title__icontains=q)
            return render_to_response('search_results.html',
                {'books': books, 'query': q})
    return render_to_response('search_form.html',
        {'error': error})
이제 20글자 보다 긴 문장으로 검색하면 오류 메시지를 보게 될 것이다. 그러나 여전히 "Please submit a search term."라고 표시될 뿐이다.

<html>
<head>
    <title>Search</title>
</head>
<body>
    {% if error %}
        <p style="color: red;">Please submit a search term 20 characters or shorter.</p>
    {% endif %}
    <form action="/search/" method="get">
        <input type="text" name="q">
        <input type="submit" value="Search">
    </form>
</body>
</html>
여기에서는 뭔가 보기 흉한 부분이 있다. 우리의 널리 두루 사용되는 오류 메시지가 혼란스러운데 비어있는 경우도 20글자 제한에 포함되기 때문이다. 오류 메시지는 모호성과 혼란스러움이 없어야 한다.

이러한 문제가 야기된 이유는 error 값이 단순한 boolean으로 사용되었기 때문이다. 오류 메시지를 list 형태로 사용할 수 있다.
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})
    return render_to_response('search_form.html',
        {'errors': errors})
그다음 search_form.html template를 수정하여 전달된 오류 메시지 list를 출력하도록 한다.
<html>
<head>
    <title>Search</title>
</head>
<body>
    {% if errors %}
        <ul>
            {% for error in errors %}
            <li>{{ error }}</li>
            {% endfor %}
        </ul>
    {% endif %}
    <form action="/search/" method="get">
        <input type="text" name="q">
        <input type="submit" value="Search">
    </form>
</body>
</html>

연락처(contact) form 만들기

여태껏 단지 'q' field만 있는 단순한 검색 form을 알아보았다. 매우 단순했기 때문에 django의 form library를 사용하지 않았다. 연락처 form과 같이 복잡한 form일수록 복잡한 처리를 호출해야 하고 더 복잡한 개발을 해야 한다.

이는 사용자가 feedback을 제출할 수 있도록 하는 form이 될 것이며, 추가적인 e-mail 주소를 다룰 것이다. form이 제출되고 data가 검증되었다면, 자동으로 site-staff에게 e-mail을 보내도록 한다.

contact_form.html로 부터 시작하도록 하자.

<html>
<head>
    <title>Contact us</title>
</head>
<body>
    <h1>Contact us</h1>

    {% if errors %}
        <ul>
            {% for error in errors %}
            <li>{{ error }}</li>
            {% endfor %}
        </ul>
    {% endif %}

    <form action="/contact/" method="post">
        <p>Subject: <input type="text" name="subject"></p>
        <p>Your e-mail (optional): <input type="text" name="email"></p>
        <p>Message: <textarea name="message" rows="10" cols="50"></textarea></p>
        <input type="submit" value="Submit">
    </form>
</body>
</html>

우리는 subject, e-mail, 그리고 message라는 3개의 field를 정의했다. 두번째는 option이나 나머지 두개는 필수이다. method="get" 대신에 method="post"를 사용하는것을 주의하라. 왜냐하면 이 form 제출은 부작용(side-effect)이 있기 때문이다. e-mail을 전송하고 이전의 search_form.html template로 부터 오류 출력 코드를 복사한다.

만약 이전 section으로 부터 search() view에 의해 수행되었다면, contact() view는 대략 다음과 같을 것이다.

from django.core.mail import send_mail
from django.http import HttpResponseRedirect
from django.shortcuts import render_to_response

def contact(request):
    errors = []
    if request.method == 'POST':
        if not request.POST.get('subject', ''):
            errors.append('Enter a subject.')
        if not request.POST.get('message', ''):
            errors.append('Enter a message.')
        if request.POST.get('email') and '@' not in request.POST['email']:
            errors.append('Enter a valid e-mail address.')
        if not errors:
            send_mail(
                request.POST['subject'],
                request.POST['message'],
                request.POST.get('email', 'noreply@example.com'),
                ['siteowner@example.com'],
            )
            return HttpResponseRedirect('/contact/thanks/')
    return render_to_response('contact_form.html',
        {'errors': errors})

(만약 여기까지 따라왔다면 이러한 view를 books/views.py에 넣어야 할 지 궁금해할 것이다. books application에서 다루어도 큰 문제가 없으니, 다른곳에서는 가능할 까? 그것은 전적으로 당신에 달려있다. django는 이에 대해 관여하지 않고, URLconf로 부터 view를 지목할 수 있다면 가능하다. 보통 선호되는 방법으로는 별개의 directory인 contact를 생성하고, 같은 위치에 books 경로가 있도록 한다. 이런 경우 비어있는 __init__.py와 views.py가 있어야한다.)


(ps.역자주)
django 1.2 이상에서 "Submit"을 누르면 다음과 같이
"CSRF verification failed. Request aborted."
가 발생한다.


이는 CSRF(Cross site request forgery) 보안으로 인해 발생한 것으로, 다음과 같이 view를 수정하면 해결된다.
...
from django.views.decorators.csrf import csrf_exempt
...
@csrf_exempt
def contact(request):
...
여기에서 관련 사항을 확인해 보기 바란다.

e-mail을 잘못 입력하면 아래와 같이 된다.


몇가지 발생한 새로운 것들은 다음과 같다.

  • request.method가 'POST'로 되어 있다. 이는 form 전송의 경우에 해당된다. 단지 누군가가 contact form을 보기만 할 뿐이라면, 그럴 필요가 없다.(이경우, request.method는 'GET'으로 설정하는데, 일반적인 브라우징 환경에서 browser는 POST가 아닌 GET을 사용하기 때문이다.) 이는 "form 처리"로 부터 "form 출력"을 격리하는데 휼륭한 방법을 제공한다.
  • request.GET 대신 request.POST를 사용하여 form data에 접근한다. contact_form.html에 HTML <form>이 method="post"를 사용하기 때문이다. 만약 해당 view가 POST를 경유하여 접근된다면, request.GET은 비어있게 된다.
  • 이 싯점에서 subject와 message라는 두개의 필수 field를 가지게 되는데, 두개 모두 검증(validate)해야 한다. request.POST.get()을 사용하고, 빈 문자열을 기본값으로 제공한것을 주목하라. 이는 key 값이 없을 경우와 data가 없는 경우에 대해 처리를 해 주기 때문에, 꽤나 유용한 방법이다.
  • email field는 필수가 아니더라도 역시 검증을 하게 된다. 일단 우리의 검증 알고리즘은  허술한데, 단지 '@' 문자 존재 여부만 보기 때문이다. 실제로는 더 견고한 검증이 필요하다. (django에서는 그것을 제공하고 있는데, 짧막하게 설명할 것이다.)
  • e-mail을 전송하기 위해 django.core.mail.send_mail를 사용한다. 4개의 argument가 필수인데, e-mail 제목, e-mail body, "from" 주소, 그리고 수신인 주소이다. send_mail은 django의 EmailMessage class를 편리하게 wrapping한 것이다. 그 class는 보다 상세한 기능, 즉, 첨부, multipart e-mail, 그리고 e-mail header의 모든 수정등을 지원한다.

    send_mail()을 사용하여 e-mail을 보내기 위해서 server 설정이 이뤄져야 하고, django는 outbound e-mail server에 대해 통보가 이뤄져야 한다. 자세한건 http://docs.djangoproject.com/en/dev/topics/email/를 참조바란다.
  • e-mail이 전송된 다음, HttpResponseRedirect object를 return하여 "성공" page로 redirect된다. "성공" page의 구현은 당신에게 맡기도록 하겠다. 그러나 render_to_reponse() 호출 대신 redirect를 초기화했는지를 설명하고자 한다.

    그 이유 : 만약 POST로 경유되어 load된 page를 사용자가 "새로고침"한다면, 해당 request가 되풀이되게 된다. 이는 database에 다시 record가 중복해서 들어가는 경우 혹은 우리 예제에서와 같이 e-mail을 두번 전송하게 되는 경우와 같이 요구하지 않는 결과를 야기한다. 만약 POST 이후에 다른 page로 redirect된다면, 해당 request를 다시 보낼수는 없을 것이다.

    당신은 성공적인 POST reuqest에 대해 항상 redirect 실행을 준비하도록 해야 한다. 그것이야 말고 최고의 web 개발 훈련이다.

이 view가 동작하나 검증함수가 너무 복잡한 면이 있다. 수십개의 field를 가지는 form 처리를 상상해 봐라. 정말 이러한 if 구문으로 쓰고 싶은가?

다른 문제는 form 재출력(redisplay)이다. 검증 실패가 발생하는 경우에 이전에 전달하려고 했던 data를 함께 form에 전달하여 재출력하는 것이 가장 좋은 예일 것이다. 그러면 사용자가 무었이 잘못되었는지를 확인할 수 있을 것이다. 우리는 template에 POSTS data를 수동으로 전달하는데, 적당한 위치에 적당한 값을 넣기 위해 각각의 HTML field를 편집해야 한다.

# views.py

@csrf_exempt
def contact(request):

    errors = []
    if request.method == 'POST':
        if not request.POST.get('subject', ''):
            errors.append('Enter a subject.')
        if not request.POST.get('message', ''):
            errors.append('Enter a message.')
        if request.POST.get('email') and '@' not in request.POST['email']:
            errors.append('Enter a valid e-mail address.')
        if not errors:
            send_mail(
                request.POST['subject'],
                request.POST['message'],
                request.POST.get('email', 'noreply@example.com'),
                ['siteowner@example.com'],
            )
            return HttpResponseRedirect('/contact/thanks/')
    return render_to_response('contact_form.html', {
        'errors': errors,
        'subject': request.POST.get('subject', ''),
        'message': request.POST.get('message', ''),
        'email': request.POST.get('email', ''),

    })

# contact_form.html

<html>
<head>
    <title>Contact us</title>
</head>
<body>
    <h1>Contact us</h1>

    {% if errors %}
        <ul>
            {% for error in errors %}
            <li>{{ error }}</li>
            {% endfor %}
        </ul>
    {% endif %}

    <form action="/contact/" method="post">
        <p>Subject: <input type="text" name="subject" value="{{ subject }}"></p>
        <p>Your e-mail (optional): <input type="text" name="email" value="{{ email }}"></p>
        <p>Message: <textarea name="message" rows="10" cols="50">**{{ message }}**</textarea></p>
        <input type="submit" value="Submit">
    </form>
</body>
</html>

e-mail을 잘못 입력하면 다음과 같다.


이는 꽤나 불편하고 사람의 실수가 유발될 확률이 높다. form과 검증 관련 작업을 처리하는 고수준의 library를 확인해 보도록 하자.

첫 form class

django는 django.forms라는 form library가 있는데, 본 chapter에서 form을 출력하고 검증하는데 학습했던 이슈들의 많은 것들을 처리해 준다. django의 form framework으로 다시한번 contact form을 재작업 해보자.

form framework를 사용하는 주요한 방법은 당신이 다룰 각각의 HTML <form>을 위해 Form class를 정의하는 것이다. 여기에서는 단지 우리는 하나의 <form>을 가지고 있어, 하나의 Form class만 만들 것이다. 이 class는 위치에 구애받지 않는데, views.py에 직접 넣어도 되나 독립된 Forms.py에 넣도록 하자. views.py와 동일한 directory에 다음과 같이 만들자. (mysite/mysite/books/forms.py)

from django import forms

class ContactForm(forms.Form):
    subject = forms.CharField()
    email = forms.EmailField(required=False)
    message = forms.CharField()

이는 충분히 이해하기 쉽도록 되어 있고, django의 model 문법과 유사하다. form의 각 field는 Field class의 type에 의해 표현되는데, 여기에서는 CharField와 EmailField가 Form class의 속성으로 사용되었다. 각 field는 기본적으로 필수항목인데, email은 option 처리를 위해 required=False를 부여한다.

이제 python command로 들어가서 이 class로 무었을 할 수 있는지 확인해 보자. 처음으로 HTML을 출력해 보자.
C:\mydjango\mysite>python manage.py shell
Python 2.5.4 (r254:67916, Dec 23 2008, 15:10:54) [MSC v.1310 32 bit (Intel)] on
win32
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from mysite.books.forms import ContactForm
>>> f = ContactForm()
>>> print f
<tr><th><label for="id_subject">Subject:</label></th><td><input type="text" name
="subject" id="id_subject" /></td></tr>
<tr><th><label for="id_email">Email:</label></th><td><input type="text" name="em
ail" id="id_email" /></td></tr>
<tr><th><label for="id_message">Message:</label></th><td><input type="text" name
="message" id="id_message" /></td></tr>
>>>
django는 접근성을 위해 <label> tag를 각 field를 추가한다. 이러한 생각은 가능한 최적화된 기본 행동을 생성하기 위함이다.

기본 output은 HTML <table>의 format인데, 다른 것도 가능하다.
>>> print f.as_ul()
<li><label for="id_subject">Subject:</label> <input type="text" name="subject" i
d="id_subject" /></li>
<li><label for="id_email">Email:</label> <input type="text" name="email" id="id_
email" /></li>
<li><label for="id_message">Message:</label> <input type="text" name="message" i
d="id_message" /></li>
>>> print f.as_p()
<p><label for="id_subject">Subject:</label> <input type="text" name="subject" id
="id_subject" /></p>
<p><label for="id_email">Email:</label> <input type="text" name="email" id="id_e
mail" /></p>
<p><label for="id_message">Message:</label> <input type="text" name="message" id
="id_message" /></p>
>>>
<table>을 열고 닫는 부분을 주의하고, <ul>과 <form> tag는 output에 포함되지 않아서 필요시 다른 추가적인 row와 수정 사항을 넣을 수 있다.

이러한 method는 "전체 form을 출력"하는 경우에 대한 지름길(shortcut)일 뿐이다. 특정 field를 위한 HTML을 역시 출력할 수 있다.
>>> print f['subject']
<input type="text" name="subject" id="id_subject" />
>>> print f['message']
<input type="text" name="message" id="id_message" />
>>>
Form object가 할 수 있는 두번째 일은 data 검증(validation)이다. data를 검증하기 위해, 새로운 Form object를 생성하고 field 이름을 data로 매핑할 수 있는 data의 dictionary를 전달한다.
>>> f = ContactForm({'subject': 'Hello', 'email': 'adrian@example.com', 'message
': 'Nice site!'})
>>>
일단 Form instance를 견결하였다면, "bound(경계)" form을 만든 것이다.
>>> f.is_bound
True
>>>
data가 유효(valid)한지 확인하기 위해 bound된 Form에 is_valid() method를 호출한다. 각 field에 유효한 값을 전달하였기 때문에, Form은 유효하게 나온다.
>>> f.is_valid()
True
>>>
만약 email field를 전달하지 않았다면, 여전히 유효한데, 이는 required=False 덕분이다.
>>> f = ContactForm({'subject': 'Hello', 'message': 'Nice site!'})
>>> f.is_valid()
True
>>>
그러나 subject나 message field를 비워둔다면, 더이상 유효하지 않다.
>>> f = ContactForm({'subject': 'Hello'})
>>>
f.is_valid()
False
>>> f = ContactForm({'subject': 'Hello', 'message': ''})
>>> f.is_valid()
False
>>>
다음과 같이 오류 내용을 확인할 수 있다.
>>> f = ContactForm({'subject': 'Hello', 'message': ''})
>>> f['message'].errors
[u'This field is required.']
>>> f['subject'].errors
[]
>>> f['email'].errors
[]
>>>
각 bound Form instance는 errors attribute를 가지는데, 이는 오류 내용이 field 이름에 mapping된 dictionary이다.
>>> f = ContactForm({'subject': 'Hello', 'message': ''})
>>> f.errors
{'message': [u'This field is required.']}
>>>
마지막으로 유효한 Forrm instance은 cleaned_data attribute가 가능하다. 이는 전송된 data의 dictionary로, 정화(cleaned up)되어 있다. django의 form framework는 data 검증 뿐만 아니라, 적합한 python type으로 값을 변환하여 정화한다.
>>> f = ContactForm({‘subject’: ‘Hello’, ‘email’: ‘adrian@example.com’, ‘message’: ‘Nice site!’})
>>> f.is_valid()
True
>>> f.cleaned_data
{‘message’: u’Nice site!’, ‘email’:
u’adrian@example.com’, ‘subject’: u’Hello’}
>>>
우리가 사용한 form은 단지 문자열만 다루고 있는데, 이는 Unicode object로 "정화"된다. 그러나 만일 IntegerField나 DataField를 사용하고자 한다면, form framework는 cleaned_data가 주어진 field를 위해 적합한 python integers 혹은 datetime.date object를 사용하는 것을 확인시켜 준다.

view에 Form object 구속하기

Form class에 관련된 기초 지식을 알아왔는데, 우리의 contact() view에 어떻게 대체할 것인가를 알아봐야 할 것이다. 아래는 form framework 사용을 위해 contact()를 다시 작성한 것이다.

# views.py (mysite\books\views.py)

from django.shortcuts import render_to_response
from mysite.contact.forms import ContactForm

def contact(request):
    if request.method == 'POST':
        form = ContactForm(request.POST)
        if form.is_valid():
            cd = form.cleaned_data
            send_mail(
                cd['subject'],
                cd['message'],
                cd.get('email', 'noreply@example.com'),
                ['siteowner@example.com'],
            )
            return HttpResponseRedirect('/contact/thanks/')
    else:
        form = ContactForm()
    return render_to_response('contact_form.html', {'form': form})

# contact_form.html (mysite\templates\contact_form.html)

<html>
<head>
    <title>Contact us</title>
</head>
<body>
    <h1>Contact us</h1>

    {% if form.errors %}
        <p style="color: red;">
            Please correct the error{{ form.errors|pluralize }} below.
        </p>
    {% endif %}

    <form action="" method="post">
        <table>
            {{ form.as_table }}
        </table>
        <input type="submit" value="Submit">
    </form>
</body>
</html>

얼마만큼 지저분한 것이 제거되었는지를 눈으로 보기 바란다. django의 form framework은 HTML 표시와, 검증하며 data 정화 및 form을 오류와 함께 다시 표시하는것등을 처리한다.

로컬에서 실행해보고, from을 로드해 보아라. 모두 비워둔채 혹은 잘못된 email 주소, 등으로 전송해 보기 바란다.


field가 render되는 방법을 변경하기

아마도 local에서 render했을때 처음으로 확인한 것은 message field는 <input type="text">로 표시되었는데, <textare>로 되어야 할거 같다. field의 widget을 세팅함으로 가능하다.

from django import forms

class ContactForm(forms.Form):
    subject = forms.CharField()
    email = forms.EmailField(required=False)
    message = forms.CharField(widget=forms.Textarea)

form framework은 widget 집합 내의 각 field당 표현 논리를 분리해 놓았다. 각 field type은 default widget이고, 그 default를 쉽게 무시(override)할 수 있또록 했다. 또한 당신 고유의 수정(custom)된 widget 환경을 제공한다.

Field lcass를 검증 논리(validation logic)로 widget을 표현 논리(presentation logic)로 생각하라.

최대 길이 지정하기

가장 일반적으로 검증이 필요한 것중 하나는 field를 특정 길이로 제한하는 것이다. subject field에 대해 100글자로 제한하여 ContactForm을 향상시키도록 하자. 그러기 위해서 max_length를 CharField에 제공하면 된다.

from django import forms

class ContactForm(forms.Form):
    subject = forms.CharField(max_length=100)
    email = forms.EmailField(required=False)
    message = forms.CharField(widget=forms.Textarea)

옵션으로 min_length도 가능하다.

초기값 설정하기

보다 나아가, subject field에 "I love your site!"라는 초기값(initial value)를 추가해 보자. 이를 위해서 Form instance를 생성할 때 initial arugment를 사용하면 된다.
def contact(request):
    if request.method == 'POST':
        form = ContactForm(request.POST)
        if form.is_valid():
            cd = form.cleaned_data
            send_mail(
                cd['subject'],
                cd['message'],
                cd.get('email', 'noreply@example.com'),
                ['siteowner@example.com'],
            )
            return HttpResponseRedirect('/contact/thanks/')
    else:
        form = ContactForm(
            initial={'subject': 'I love your site!'}
        )
    return render_to_response('contact_form.html', {'form': form})
이제 subject field는 해당 값으로 초기화된다.


초기(initial) data를 전달하는 것과 form에 bind하는것의 차이점이 있다. 가장 큰 차이점은 만약 초기 data를 사용한다면 form은 unbound(경계값 없음)되는데, 이는 어떤 오류 메시지도 만들 수 없게 된다.

검증 규칙(validation rule) 수정(custom)하기

feedback form을 런칭했을때를 생각해봐라. 그리고 email이 줄어든다. 여기서 문제가 하나 있다 : 전송된 메시지의 일부는 하나 혹은 두단어로, 상식적으로 충분히 길지 않은 상태이다. 그럼 새로운 정책 하나를 적용하게 된다 : 4개 이상의 단어를 입력하세요(four words or more, please).

django form에 있는 검증을 수정하는 방법은 여러개 있다. 만일 다음번에도 사용될 가능성이 있다면, custom field type을 만들 수 있다. 대부분의 custom 검증은 단 한번의 작업이고 Form class에 직접 달라붙게 된다.

message field에 추가 검증을 위해 clean_message() method를 Form class에 추가한다.

from django import forms

class ContactForm(forms.Form):
    subject = forms.CharField(max_length=100)
    email = forms.EmailField(required=False)
    message = forms.CharField(widget=forms.Textarea)

    def clean_message(self):
        message = self.cleaned_data['message']
        num_words = len(message.split())
        if num_words < 4:
            raise forms.ValidationError("Not enough words!")
        return message

django의 form 시스템은 자동으로 field 이름으로 끝나고 clean_으로 시작하는 method를 찾게 된다. 만약 존재한다면 검증하는 동안 호출하게 된다.

특별하게도 clean_message() method는 주어진 field에 대한 기본(default)적인 검증 논리 수행 이후에 호출된다. 왜냐하면 field data는 이미 일부 처리가 된 상태이기 때문에 self.cleaned_data를 이후에 호출하게 된다. 마찬가지로 값이 존재하고 비어있지 않는 것을 체크할 걱정은 없다. 기본 검증에서 수행되었기 때문이다.

우리는 단어 개수를 구하기 위해 len()과 split()의 조합을 사용하였다. 만일 너무 작은 단어수로 들어왔다면 forms.ValidationError를 발생시킨다. 이 예외의 문자열은 error list의 item으로 사용자에게 표시될 것이다.

method의 끝에 정화된 값을 명시적으로 전달하는 것은 매우 중요하다. 이는 custom 검증 method에서 값을 수정 가능하게 한다. 만약 return을 깜박한다면, None이 전달되고 원래의 값은 사라지게 된다.

label 지정하기

기본적으로 django에서 자동으로 생성된 HTML form의 label은 email을 위한 field label이 "Email"인것 처럼 공백은 '_'로, 첫문자는 대문자로 치환되어 생성된다.

그러나 django의 model에서는 주어진 field를 위한 field를 수정할 수 있다.
class ContactForm(forms.Form):
    subject = forms.CharField(max_length=100)
    email = forms.EmailField(required=False, label='Your e-mail address')
    message = forms.CharField(widget=forms.Textarea)


Form design 수정하기

contact_form.html template는 form 출력을 위해 {{ form.as_table }}를 사용하는데, 그러나 출력을 더 제어하기 위해 다른 방법으로 출력할 수 있다.

form 표현을 수정하기 위한 가장 빠른 방법으로 CSS를 이용하는 것이다. error list는 좀더 보기 좋게 표시될 수 있고, 자동 생성된 error list는 <ul class="errorlist">를 CSS를 목표로 하였다면 사용할 것이다. 다음 CSS는 우리의 error를 출력해줄 것이다.

<style type="text/css">
    ul.errorlist {
        margin: 0;
        padding: 0;
    }
    .errorlist li {
        background-color: red;
        color: white;
        display: block;
        font-size: 10px;
        margin: 0 0 3px;
        padding: 4px 5px;
    }
</style>
form에서 사용할 HTML을 우리를 위해 만들어준다면 편리할 것인데, 많은 경우 default rendering을 무시(override)할 것이다. {{ form.as_table }}과 그 친구들은 application을 개발하는 과정에서 유용한 지름길(shortcut)이나 출력할 form에 대한 모든 것은 무시될 수 있다. 대부분 template 그 자체에서 가능하다.

각 field의 widget(<input type="text">, <select>,<textarea>,...)은 {{ form.fieldname }}을 접근함에 의해 개별적으로 render된다. 그리고 field와 연계된 어떠한 error는 {{ form.fieldname.errors }}에서 가능하다. 이러한 것을 바탕으로 다음과 같은 code로 수정된 template를 만들 수 있다.

<html>
<head>
    <title>Contact us</title>
</head>
<body>
    <h1>Contact us</h1>

    {% if form.errors %}
        <p style="color: red;">
            Please correct the error{{ form.errors|pluralize }} below.
        </p>
    {% endif %}

    <form action="" method="post">
        <div class="field">
            {{ form.subject.errors }}
            <label for="id_subject">Subject:</label>
            {{ form.subject }}
        </div>
        <div class="field">
            {{ form.email.errors }}
            <label for="id_email">Your e-mail address:</label>
            {{ form.email }}
        </div>
        <div class="field">
            {{ form.message.errors }}
            <label for="id_message">Message:</label>
            {{ form.message }}
        </div>
        <input type="submit" value="Submit">
    </form>
</body>
</html>


만약 error가 있는 경우{{ form.message.errors }}는 <ul class="errorlist">를 출력하고, field가 유효하다면 비어있는 문자열을 출력한다. 우리는 역시 form.message.errors를 Boolean 혹은 심지어 list를 순환하는 것으로 다룰 수 있다.

<div class="field{% if form.message.errors %} errors{% endif %}">
    {% if form.message.errors %}
        <ul>
        {% for error in form.message.errors %}
            <li><strong>{{ error }}</strong></li>
        {% endfor %}
        </ul>
    {% endif %}
    <label for="id_message">Message:</label>
    {{ form.message }}
</div>

검증 실패인 경우, "error" class에 <div>를 추가하고 순서없는 list로 출력한다.

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

[09] django template의 고급 기능  (8) 2012.09.27
[08] django Views와 URLconfs의 고급 기능  (0) 2012.05.30
[06] django admin site  (3) 2012.04.27
[05] django의 model  (8) 2012.04.12
[04] django의 template  (14) 2012.04.05