장고 마스터하기 - 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>

김땡땡's blog

김땡땡's blog

김땡땡