[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 |
URL에 대한 정보
HttpRequest object는 현재 요청된 URL에 대해 다음과 같은 정보로 나눠져 있다.
|
|
|
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 |
# BAD! # GOOD |
(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! # GOOD (VERSION 1) # GOOD (VERSION 2) |
(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) |
제출(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이다. |
간단한 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): |
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> |
TEMPLATE_DIRS = ( os.path.join(os.path.dirname(__file__), 'templates').replace('\\','/'), ) |
urls.py에 URLpattern은 다음과 같이 한다.
from mysite.books import views urlpatterns = patterns('', |
이제 http://localhost:8000/search-form/를 들어가 보면, 다음과 같은 단순한 검색 interface가 등장한다.
만약 "Search"를 눌러 form을 제출한다면, django 404 오류를 보게 된다. /search/가 구현되지 않았다고 가르켜 주는데, 다음과 같이 수정해 보자. (역자주. views.py는 mysite가 아닌 book app directory에 있는 views.py 이다)
# urls.py urlpatterns = patterns('', # views.py from django.http import HttpResponse |
우선 이는 단지 사용자의 검색 단어만 표시할 뿐인데, django에 data가 잘 전달되었는지 확인하기 위함이고 system을 통해 어떻게 검색이 이뤄지는지를 알려줄 수 있다.
- HTML <form>은 변수 q를 정의한다. 만약 전송(submit)되었다면 /search/ URL에 GET(method="get")을 통해 q의 값이 전달된다.
- /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 %}
단순하게 처리했던 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}) |
만일 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"> |
간단한 검증(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를 이용한 검증은 단지 보너스로 생각하길 바란다. |
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}) |
<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> |
이러한 문제가 야기된 이유는 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}) |
<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> {% if errors %} <form action="/contact/" method="post"> |
만약 이전 section으로 부터 search() view에 의해 수행되었다면, contact() view는 대략 다음과 같을 것이다.
from django.core.mail import send_mail def contact(request): |
(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 # contact_form.html <html> {% if errors %} <form action="/contact/" method="post"> |
이는 꽤나 불편하고 사람의 실수가 유발될 확률이 높다. 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): |
이제 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> >>> |
기본 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> >>> |
이러한 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" /> >>> |
>>> f = ContactForm({'subject': 'Hello', 'email': 'adrian@example.com', 'message ': 'Nice site!'}) >>> |
>>> f.is_bound True >>> |
>>> f.is_valid() True >>> |
>>> f = ContactForm({'subject': 'Hello', 'message': 'Nice site!'}) >>> f.is_valid() True >>> |
>>> 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 [] >>> |
>>> f = ContactForm({'subject': 'Hello', 'message': ''}) >>> f.errors {'message': [u'This field is required.']} >>> |
>>> 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’} >>> |
view에 Form object 구속하기
Form class에 관련된 기초 지식을 알아왔는데, 우리의 contact() view에 어떻게 대체할 것인가를 알아봐야 할 것이다. 아래는 form framework 사용을 위해 contact()를 다시 작성한 것이다.
# views.py (mysite\books\views.py) from django.shortcuts import render_to_response def contact(request): # contact_form.html (mysite\templates\contact_form.html) <html> {% if form.errors %} <form action="" method="post"> |
로컬에서 실행해보고, from을 로드해 보아라. 모두 비워둔채 혹은 잘못된 email 주소, 등으로 전송해 보기 바란다.
field가 render되는 방법을 변경하기
아마도 local에서 render했을때 처음으로 확인한 것은 message field는 <input type="text">로 표시되었는데, <textare>로 되어야 할거 같다. field의 widget을 세팅함으로 가능하다.
from django import forms class ContactForm(forms.Form): |
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 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}) |
초기(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): def clean_message(self): |
특별하게도 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> |
각 field의 widget(<input type="text">, <select>,<textarea>,...)은 {{ form.fieldname }}을 접근함에 의해 개별적으로 render된다. 그리고 field와 연계된 어떠한 error는 {{ form.fieldname.errors }}에서 가능하다. 이러한 것을 바탕으로 다음과 같은 code로 수정된 template를 만들 수 있다.
<html> {% if form.errors %} <form action="" method="post"> |
만약 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 |