역할
- 폼 클래스의 데이터를 렌더링하여 입력폼 HTML 생성
- 입력폼 값에 대한 유효성 검증 (Validation) 및 값 변환
- 검증을 통과한 값들을 dict 형태로 제공 ›› form.cleaned_data 으로 제공
+ CSRF (Cross Site Request Forgery)
데이터를 변경하는 HTTP 폼은 언제타 CSRF 보안 (사이트 간 위조 요청 방지) 을 이용하자. 개발 단계에서 잊어버리고 이용하지 않았을 경우 보안 위험성이 있을 수 있다는 친절한 안내 메시지를 보여주기도 한다.
django-rest-framework 같은 API 프레임워크에서는 이러한 처리를 자동으로 다 해준다.
# forms.py
...
<form action="" method="post>
{% csrf_token %}
...
</form>
보통 Model 에 맞춰서 Form을 구성하기 때문에, Model이 바뀌면 Form도 그에 따라 의존되는 코드들을 변갱해 줘야하므로 유지보수가 귀찮다. 그러나 ModelForm을 사용하면 Model에서 사용할 Field만 정의하면 그 정보를 가져와 formField 를 자동으로 구성해주기 때문에 편하다.
특징
- 장고 Form 상속 (forms.ModelForm)
- 지정된 모델로부터 필드정보를 읽어들여, 자동으로 폼 필드를 세팅
- 내부적으로 Model Instance 유지
- 유효성 검증에 통과한 값들로 지정 Model Instance로의 저장(save) 지원
- Form : 직접 필드 정의, 위젯 설정이 필요
- ModelForm : 지정된 모델로부터 필드정보를 읽어들여, 자동으로 폼 필드를 세팅
# forms.py
from django import forms
# Form
class PostForm(forms.Form):
title = forms.CharField()
content = forms.CharField(widget=form.Textarea)
# ModelForm
from .models import Post
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = '__all__' # 모든 필드를 불러온다.
🚀 (Django) The save() method
모델폼 데이터는 폼에 먼저 저장된 이후 모델 인스턴스에 저장된다.
ModelForm에서 폼 데이터는 두 가지 각기 다른 단계를 통해 저장된다.
- 첫 번째로 폼 데이터가 폼 인스턴스에 저장된다.
- 그 다음에 폼 데이터가 모델 인스턴스에 저장된다.
ModelForm 클래스에는 form.save(self, commit=True) 메서드가 구현되어 있는데, DB 저장 여부를 commit flag를 통해서 결정한다.
commit=False 를 사용하면 함수 호출을 지연시켜 DB에 바로 저장하지 않는다(임시 저장 상태).
DB에 실제 데이터를 저장하기 전에 따로 일련의 작업을 하고 싶을 때 사용하기 유용하다. form.save() 메서드에 의해 적용되기 전까지는 ModelForm이 모델 인스턴스로 저장되지 않는 분리된 특징 자체를 장점으로 이용할 수 있는 것이다.
# views.py
from django.shortcuts import render, redirect
from .models import Post
from .forms import PostForm
def post_new(request):
if request.method == 'POST':
form = PostForm(request.POST, request.FILES)
if form.is_valid():
# 필수필드인 ip필드 값을 아직 채우지 않았으므로
# DB 세이브를 지연시켜 오류를 방지한다.
post = form.save(commit=False)
post.ip = request.META['REMOTE_ADDR'] # 유저로부터 ip 필드를 입력받음.
post.save()
return redirect('/dojo/')
else:
form = PostForm()
return render(request, 'dojo/post_form.html', {'form': form,})
🚀 (Django) Accessing “clean” data
form.is_valid() 를 통해서 검증을 통화한 값들은 dict 형태로 form.cleaned_data 변수명으로 제공된다.
⭐ 폼의 입력값을 얻고 싶은 경우는form.cleaned_data 로 접근하도록 하자.
➔ Avoid code : request.POST[] 로 접근
# views.py
...
form = CommentForm(request.POST)
if form.is_valid():
# request.POST 데이터는 폼 인스턴스 초기 데이터를 가져 오므로
# `form.clean()` 메서드를 통해 변경될 가능성이 있어
# 나쁜 접근이다.
message = request.POST['message']
comment = Comment(message=message)
comment.save()
return redirect(post)
➔ Good code : from.cleaned_data[] 로 접근
# views.py
...
form = CommentForm(request.POST)
if form.is_valid():
# 폼 인스턴스 내에서 clean함수를 통해 변환되었을 수도 있는 데이터를 가져오므로
# 좋은 접근이다.
message = form.cleaned_data['message']
comment = Comment(message=message)
comment.save()
return redirect(post)
장고 폼은 파이썬 딕셔너리의 유효성을 검사하는 최상의 도구이다.
유효성 검사를 수행하는데는 2가지의 방법이 있다.
1. validator 함수를 통한 유효성 검사
- 값이 원하는 조건에 맞지 않을 때 ValidationError 예외를 발생시킨다.
- 리턴값은 사용되지 않는다.
- 모델필드 또는 폼필드 정의 시에 지정한다.
- ⚡ 사용자 정의 Validator 보다 빌트인 Validator 사용을 추천한다.
2. Form 클래스 내의 clean , clean_ 멤버함수를 통한 유효성 검사 및 값 변경
- 값이 원하는 조건에 맞지 않을 때 ValidationError 예외를 발생시킨다.
- 리턴값을 통해 값을 반환한다.
- ⚡ 원하는 포맷으로 값 변경을 원할 때 사용
+ 유효성 검사 호출 로직
유효성 검사는 form.is_valid() 가 호출되는 시점에서 수행된다.
보통 from.is_valid() 가 호출될 때 다음과 같은 절차가 진행된다.
# views.py
def post_new(request):
if request.method == 'POST':
form = PostForm(request.POST, request:FILES)
if form.is_valid(): # 유효성 검사 수행
form.save()
# SUCESS 후 처리
else:
form = PostForm()
# ...
- 폼이 데이터를 받으면 form.is_valid() 는 form.full_clean() 메서드를 호출한다.
- form.full_clean() 은 폼 필드들과 각각의 필드 유효성을 하나하나 검사하면서 다음과 같은 유효성 검사를 수행한다.
ㅤ- 필드에 들어온 데이터에 대해 파이썬 형식으로 변환하거나, 변환할 때 문제가 생기면 ValidationError를 일으킨다.
ㅤ- 커스텀 유효성 검사기(validator)를 포함한 각 필드에 특별한 유효성을 검사한다. 문제가 있을 때 ValidationError를 일으킨다.
ㅤ- 폼에 clean_() 메서드가 있으면 이를 실행한다.
- form.full_clean() 이 form.clean() 메서드를 실행한다.
- ModelForm 인스턴스의 경우 form.post_clean() 이 다음 작업을 한다.
ㅤ- form.is_valid() 가 True나 False로 설정되어 있는 것과 관계없이 ModelForm의 데이터를 모델 인스턴스로 설정한다.
ㅤ- 모델의 clean() 메서드를 호출한다.
(1) forms.py 에 적용
›› form 에서는 리턴값을 따로 처리하지 않고, forms.ValidationError 예외 발생 유무로 처리
# forms.py
import re
from django import forms
from django.forms import ValidationError
def phone_number_validator(value):
if not re.match(r'^010[1-9]\d{7}$'):
raise forms.ValidationError('3글자 이상 입력하시오.')
class ProfileForm(forms.Form):
phone_number = forms.CharField(validators=[phone_number_validator])
(2) models.py 에 적용
›› model 클래스 정의 시에 validators 옵션 적용. (권장!! 로직이 분산되지 않는다.)
›› 가급적이면 모든 validators 는 모델에 정의하고, ModelForm 을 통해 모델의 validators 정보를 가져오도록 하자.
# models.py
import re
from django import forms
from django import models
def phone_number_validator(value):
if not re.match(r'^010[1-9]\d{7}$'):
raise forms.ValidationError('3글자 이상 입력하시오.')
class Profile(forms.Model):
phone_number = forms.CharField(validators=[phone_number_validator])
# forms.py
from django import forms
from .models import Profile
class ProfileForm(forms.ModelForm):
class Meta:
model = Profile
fields = '__all__'