장고 마스터하기 - 6장
장고를 사용자가 제출한 양식 데이터에 액세스하고, 유효성을 검사하고, 뭔가를 수행하는 방법을 설명한다.
요청 객체로부터 데이터 얻기
from django.http import HttpResponse
def hello(request):
return HttpResponse("Hello World")
간단한 뷰 함수 hello()이다.
뷰 함수는 hello() 같이 파라미터로 HttpRequest 객체를 사용한다.
이 HttpRequest 객체로 여러가지 속성과 메소드를 사용할 수 있다.
URL에 대한 정보
HttpRequest 객체를 현재 요청된 URL에 대한 정보를 담고 있다.
속성/메소드 | 설명 | 예제 |
---|---|---|
request.path | 전체 경로, 도메인을 포함하지 않고 선행 슬래시를 포함 | “/hello” |
request.get_host() | 호스트 도메인 | “www.example.com” |
request.get_full_path() | 경로와 쿼리 문자열(사용할 수 있는 경우) | “/hello/?print=true” |
request.is_secure() | 요청이 HTTPS를 통해 이뤄진 경우 True, 아니면 False | True or False |
뷰에서 힘들게 코딩하는 것보다 이 속성과 메소드를 사용하면 쉽게 URL을 가져올 수 있다.
요청에 대한 정보
request.META는 사용자의 IP 주소와 사용자 Agent(일반적으로 웹 브라우저의 이름과 버전)를 포함해 지정된 요청에 대해 사용할 수 있는 모든 HTTP 헤더가 들어있는 파이썬 딕셔너리다.
request.MEAT 속성
- HTTP_REFERER : 참조 URL
-
HTTP_USER_AGENT : 사용자 웹 브라우저의 사용자 에이전트 문자열.
ex)
“Mozilla/5.0 (X11; U; Linux`` i686; fr-FR; rv:1.8.1.17) Gecko/20080829 Firefox/2.0.0.17”)
- REMOTE_ADDR – 클라이언트의 IP 주소.
설계 주의점
- request.META는 기본 파이썬 딕셔너리이므로 존재하지 않는 키에 접근하면 KeyError 예외 발생.
- 헤더는 외부 데이터이므로 신뢰할 수 없고, 특정 헤더가 비어있거나 존재하지 않으면 정상적으로 동작하지 않도록 해야한다.
- 정의되지 않는 키의 대소 문자를 처리하려면 try/except 나 get() 메소드 사용
예시
- 안좋은 예시
def ua_display_bad(request):
ua = request.META['HTTP_USER_AGENT'] # Might raise KeyError!
return HttpResponse("Your browser is %s" % ua)
- 좋은 예시 1
def ua_display_good1(request):
try:
ua = request.META['HTTP_USER_AGENT']
except KeyError:
ua = 'unknown'
return HttpResponse("Your browser is %s" % ua)
- 좋은 예시 2
def ua_display_good2(request):
ua = request.META.get('HTTP_USER_AGENT', 'unknown')
return HttpResponse("Your browser is %s" % ua)
요청 META 데이터 모두 보기 예시
META 데이터를 통해 요청의 모든 것을 보여주는 함수를 작성할 수 있다.
def display_meta(request):
values = request.META
html = []
for k in sorted(values):
html.append('<tr><td>%s</td><td>%s</td></tr>' % (k, values[k]))
return HttpResponse('<table>%s</table>' % '\n'.join(html))
다른 방법은 시스템을 중단할 때 장고 오류 웹페이지를 자세히 보면 모든 HTTP 헤더 및 기타 요청 객체 등 유용한 정보들이 있다.
제출된 데이터에 대한 정보
HttpRequest 객체엔 사용자가 보낸 데이터를 request.GET이나 request.POST에 저장된다.
간단한 폼 처리 예제
검색창 예제
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>
- 검색창 화면
- 오류 변수가 넘어오면 에러 메세지 표시
- HTML은 변수
q
를 정의하고, 제출할 때q
의 값을 GET(method=”get”)을 통해 URL/search/
로 전송된다.
books/view.py
from django.shortcuts import render
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(request, 'books/search_results.html', {'books': books, 'query': q})
return render(request, 'books/search_form.html', {'error': error})
- URL
/search/(search())
를 처리하는 장고 뷰는 request.GET의 q값으로 접근할 수 있다. q
가 request.GET 에 있는지 확인해야한다. 사용자가 모두 제출했다고 가정해선 안된다.q
존재하는 검사 외에 데이터베이스 쿼리 전에 비어있는 값인지 확인.- Book.objects.filter(title_icontains = q)를 사용해 모든 book에서 주어진 입력값을 가지고 있는지 조회한다. 이 명령은 대소문자를 구분하지 않는다.
- 이 방법은 실행이 느릴 수 있으므로 대용량 데이터베이스에선 icontains를 사용하지 않는 것이 좋다.
- books를 search_results.html 템플릿에 전달.
- 검색어가 비어있는 경우 search_form.html을 다시 렌더링하고 오류 변수 전달.
search_result.html
<html>
<head>
<title>Book Search</title>
</head>
<body>
<p>You searched for: <strong></strong></p>
<p>No books matched your search criteria.</p>
</body>
</html>
books/urls.py
from django.conf.urls import url
from books import views
urlpatterns = [
url(r'^search/$', views.search),
]
mysite/urls.py
from django.conf.urls import include, url
urlpatterns = [
url(r'^', include('books.urls')),
]
form 검증(Validation)
검색창 예제에서 간단한 form 검증 기능을 추가해보자.
기능
- 검색 단어가 20자 이상일 때 에러 발생
- 아무것도 입력을 안했을 때 에러 발생
구현
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(request, 'search_results.html', {'books': books, 'query': q})
return render(request, 'search_form.html', {'errors': errors})
- 7~10 줄로 인해 이제 20자 이상은 검색할 수 없다.
- 다른 오류에 대한 각각의 메세지 출력을 위해 errors 변수를 객체로 선언
- 각 에러 발생 시 에러 추가
<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>
- error들을 모두 표시
연락처 양식 만들기
검색창 정도의 양식 필드는 괜찮을 수 있지만 양식이 많아지면 코드가 복잡해지고 반복적이다. 장고 개발자들은 이 문제점을 해결하기 위해 형식 및 유효성 검사 관련 라이브러리를 만들었다.
라이브러리를 사용해 연락처 양식을 만들어보자.
첫 번째 form 클래스
장고의 django.forms라는 form 라이브러리는 양식 표시부터 유효성 검사까지 많은 기능을 제공한다.
mysite_project\contact\forms.py
from django import forms
class ContactForm(forms.Form):
subject = forms.CharField()
email = forms.EmailField(required=False)
message = forms.CharField()
- views.py 파일에 직접 포함할 수 있지만, 커뮤니티 규칙에 따르면 forms.py라는 별도 파일에 정의하는 것을 권장.
- 각 필드는 Field 클래스 유형으로 나타냄.
- 선택 사항은 required=False 추가
확인
이 클래스가 무엇을 만들어 주는지 인터프리터를 통해 볼 수 있다.
$ python manage.py shell
>>> from mysite.forms import ContactForm
>>> f = ContactForm()
>>> print(f)
<tr><th><label for="id_subject">Subject:</label></th><td><input type="text" name="subject" required id="id_subject" /></td></tr>
<tr><th><label for="id_email">Email:</label></th><td><input type="email" name="email" id="id_email" /></td></tr>
<tr><th><label for="id_message">Message:</label></th><td><input type="text" name="message" required id="id_message" /></td></tr>
장고는 접근성을 위해 <label>
태그와 함께 만들어준다. 기본 출력은 <table>
형식이지만, 다른 방식으로 출력할 수 있다.
>>> print(f.as_ul())
<li><label for="id_subject">Subject:</label> <input type="text" name="subject" required id="id_subject" /></li>
<li><label for="id_email">Email:</label> <input type="email" name="email" id="id_email" /></li>
<li><label for="id_message">Message:</label> <input type="text" name="message" required id="id_message" /></li>
>>> print(f.as_p())
<p><label for="id_subject">Subject:</label> <input type="text" name="subject" required id="id_subject" /></p>
<p><label for="id_email">Email:</label> <input type="email" name="email" id="id_email" /></p>
<p><label for="id_message">Message:</label> <input type="text" name="message" required id="id_message" /></p>
전체가 아닌 하나의 특정 필드만 보일 수도 있다.
>>> print(f['subject'])
<input type="text" name="subject" required id="id_subject" />
>>> print f['message']
<input type="text" name="message" required id="id_message" />
유효성 검사
>>> f = ContactForm({'subject': 'Hello', 'email': 'nige@example.com', 'message': 'Nice site!'})
form 클래스에 데이터를 넣으면, Form 클래스의 인스턴스들과 연결되어 bound form(연결된 양식)이 만들어진다.
>>> f.is_bound
True
연결된 form에서 is_valid()
메소드를 호출해 유효성 여부를 확인할 수 있다.
>>> f.is_valid()
True
필수 데이터를 넘겨주지 않으면 유효하지 않게 된다. errors 속성은 에러 내용을 보여준다.
>>> f = ContactForm({'subject': 'Hello', 'message': ''})
>>> f.is_valid()
False
>>> f.errors
{'message': ['This field is required.']}
파이썬 유형으로 정리
데이터가 유효한 것으로 판명된 form은 cleaned_data 속성을 사용할 수 있다. 데이터를 파이썬 문법에 맞게 변환해 표현해준다.
>>> f = ContactForm({'subject': 'Hello', 'email': 'nige@example.com', 'message': 'Nice site!'})
>>> f.is_valid()
True
>>> f.cleaned_data
{'subject': 'Hello', 'email': 'nige@example.com', 'message': 'Nice site!'}
form 객체를 뷰로 묶기
만든 form 클래스를 사용해 연락처를 표시할 수 있다.
# views.py
from django.http import Http404, HttpResponse, HttpResponseRedirect
from django.shortcuts import render
import datetime
from mysite.forms import ContactForm
from django.core.mail import send_mail, get_connection
def contact(request):
if request.method == 'POST':
form = ContactForm(request.POST)
if form.is_valid():
cd = form.cleaned_data
con = get_connection('django.core.mail.backends.console.EmailBackend')
send_mail(
cd['subject'],
cd['message'],
cd.get('email', 'noreply@example.com'),
['siteowner@example.com'],
connection=con
)
return HttpResponseRedirect('/contact/thanks/')
else:
form = ContactForm()
return render(request, 'contact_form.html', {'form': form})
# 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" novalidate>
<table>
{{ form.as_table }}
</table>
{% csrf_token %}
<input type="submit" value="Submit">
</form>
</body>
</html>
- POST form은 Cross Site Request Forgeies 대비를 위해
{% csrf_token %}
를 사용했다.
# urls.py
from mysite.views import hello, current_datetime, hours_ahead, contact
urlpatterns = [
url(r'^contact/$', contact),
]
필드 위젯 변경
메시지 필드는 text보단 <textarea>가 되어야 한다.
# forms.py
from django import forms
class ContactForm(forms.Form):
subject = forms.CharField()
email = forms.EmailField(required=False)
message = forms.CharField(widget=forms.Textarea)
최대 길이 설정
max_length 속성을 이용하면 최대 길이를 설정할 수 있다.
# forms.py
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)
초깃값 설정
제목 필드에 초깃값을 추가해보자.
# views.py
def contact(request):
# ...
else:
form = ContactForm(
initial={'subject': 'I love your site!'}
)
return render(request, 'contact_form.html', {'form':form})
사용자 지정 유효성 검사
사용자 지정 유효성 검사를 추가하기 위해 여러 방법이 있지만, 규칙을 여러번 사용할 것이면 필드 유형을 직접 만들 수 있다.
장고의 폼 시스템은 이름이 clean_
으로 시작하고 필드의 이름으로 끝나는 메소드를 자동으로 찾는다. 그런 메소드가 있으면 유효성 검증 중에 호출된다. 지정된 필드에 대한 기본 유효성 검사 다음에 호출된다.
메세지 필드를 4단어 이상 하도록 새 유효성을 추가해보자
# forms.py
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
- message 필드에 추가하므로 clean_message 함수를 추가했다.
- 값이 존재하고 비어 있지 않은 검사는 기본 검사기에서 수행한다.
레이블 지정
기본적으로 장고는 양식 레이블을 만들 때, 밑줄을 공백으로 바꾸고 첫 번째 문자를 대문자로 만들어 생성한다.
하지만 특정 필드의 레이블을 사용자 정의할 수 있다.
# forms.py
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)
양식 디자인 사용자 정의 뀨
css로 정의한 양식 디자인도 적용할 수 있다.
에러 목록을 정의해 오류가 더 잘 보이도록 해보자.
{{form.as_table}}
이 생성해주는 에러는 <ul class="errorlist">
이므로 css로 정확히 지정해 정의한다.
<style type="text/css">
ul.errorlist {
margin: 0;
padding: 0;
}
.errorlist li {
background-color: red;
color: white;
display: block;
font-size: 1.2em;
margin: 0 0 3px;
padding: 4px 5px;
}
</style>
이 내용을 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">
<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>
{% csrf_token %}
<input type="submit" value="Submit">
</form>
</body>
</html>
여러 에러를 제어할때 {{form.message.errors}}
를 사용하면 에러가 있을 때 장고가 <ul class="errorlist">
를 만들어낸다. 그럼 {{form.message.errors}}
를 boolean이나 반복해 리스트로 만들 수 있다.
<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>