HTML파일의 form을 사용해 사용자는 데이터를 서버에 보낸다. 따라서 사용자로부터 데이터를 받으려면 form이 필요하다. django는 form의 객체를 사용할 수 있게 기능을 제공해준다.
form의 가장 중요한 역활은 사용자로부터 데이터를 받아와 적절한 데이터인지 검증하는 것이다. BaseForm 내부에서 어떤함수가 작동하는지 확인해보자.
모든 값의 유무 검증
사용자가 제출한 데이터가 모두 존재하는지 검사
def is_valid(self):
"""Return True if the form has no errors, or False otherwise."""
return self.is_bound and not self.errors
cleaned_data 초기화
우선 아래 함수는 is_valid()를 통해 binding 여부와 값이 모두 있는지 확인후 실행된다. 값이 모두 있다면 full_clean()함수를 실행하고 최종적으로 error 여부에 따라 is_vaild()의 리턴값이 True or False 로 결정된다.
# django/forms/forms.py
def full_clean(self):
"""
Clean all of self.data and populate self._errors and self.cleaned_data.
"""
self._errors = ErrorDict()
if not self.is_bound: # Stop further processing.
return
self.cleaned_data = {}
# If the form is permitted to be empty, and none of the form data has
# changed from the initial data, short circuit any validation.
if self.empty_permitted and not self.has_changed():
return
self._clean_fields()
self._clean_form()
# 모델폼을 사용한 경우
self._post_clean()
필드의 제약사항 검증
아래 함수는 form의 필드를 순회하면서 필드의 clean()함수를 사용해 제약사항을 확인한다. 필드의 제약사항에 부합하는 값이면 초기화된 cleaned_data에 값을 저장하고 필드에 적절한 값이 아니면 오류를 추가한다.
# django/forms/forms.py
def _clean_fields(self):
for name, field in self.fields.items():
# value_from_datadict() gets the data from the data dictionaries.
# Each widget type knows how to retrieve its own data, because some
# widgets split data over several HTML fields.
if field.disabled:
value = self.get_initial_for_field(field, name)
else:
value = field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name))
try:
if isinstance(field, FileField):
initial = self.get_initial_for_field(field, name)
value = field.clean(value, initial)
else:
value = field.clean(value)
self.cleaned_data[name] = value
if hasattr(self, 'clean_%s' % name):
value = getattr(self, 'clean_%s' % name)()
self.cleaned_data[name] = value
except ValidationError as e:
self.add_error(name, e)
# django/forms/fields.py
def clean(self, value):
"""
Validate the given value and return its "cleaned" value as an
appropriate Python object. Raise ValidationError for any errors.
"""
value = self.to_python(value)
self.validate(value)
self.run_validators(value)
return value
추가적인 유효성 검사
clean()함수의 오버라이딩(hook)을 통해서 추가적인 검증 작업을 할 수 있다.
def _clean_form(self):
try:
cleaned_data = self.clean()
except ValidationError as e:
self.add_error(None, e)
else:
if cleaned_data is not None:
self.cleaned_data = cleaned_data
def clean(self):
"""
Hook for doing any extra form-wide cleaning after Field.clean() has been
called on every field. Any ValidationError raised by this method will
not be associated with a particular field; it will have a special-case
association with the field named '__all__'.
"""
return self.cleaned_data
clean()함수 오버라이딩
# vi accounts/forms.py (생성)
from django import forms
from django.contrib.auth.hashers import check_password
from account.models import User
class LoginForm(forms.Form):
username = forms.CharField(max_length=64, label='아이디', error_messages={'required':'아이디를 입력하세요'})
password = forms.CharField(widget=forms.PasswordInput, label='비밀번호', error_messages={'required':'비밀번호를 입력하세요'})
def clean(self):
cleaned_data = super().clean()
username = cleaned_data.get('username')
password = cleaned_data.get('password')
if username and password:
try:
user = User.objects.get(username=username)
except User.DoesNotExist:
self.add_error('username', '아이디가 없습니다.')
return
if not check_password(password, user.password):
self.add_error('password','비밀번호가 틀렸습니다.')
else:
self.user_id = user.id
form 객체를 사용하면 코드의 양도 줄어들고 view에서 하는 로직이 더욱 명확하게 보인다. view에서 form의 유효성 로직을 분리해낼 수 있음을 확인했다.
form 객체를 사용하지 않은 함수
def login(request):
if request.method == 'GET':
return render(request, 'login.html')
elif request.method == 'POST':
username = request.POST.get('username',None)
password = request.POST.get('password',None)
res_data = {}
if not (username and password):
res_data['error'] = '모든 값을 입력해야 합니다.'
else:
user = User.objects.get(username=username)
if check_password(password, user.password):
request.session['user'] = user.id
return redirect('/')
else:
res_data['error'] = '비밀번호가 일치하지 않습니다'
return render(request, 'login.html', res_data)
form 객체를 사용한 함수
def login(request):
if request.method == 'POST'
form = LoginForm(request.POST)
if form.is_valid():
request.session['user'] = form.user_id
return redirect('/')
else:
form = LoginForm()
return render(request, 'login.html', {'form':form})
login() 함수를 통해 form을 넘겨받아 HTML파일에 작성할 수 있다.
form은 한 개 이상의 field로 구성되어 있어 반복문을 사용해 form을 작성한다.
{% extends 'base.html' %}
{% block content %}
<div class="row">
<div class="col-12 text-center" >
<h1>로그인</h1>
</div>
</div>
<div class="row">
<div class="col-12">
<form method="post" action=".">
{% csrf_token %}
{% for field in form %}
<div class="form-group">
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
<input type="{{ field.field.widget.input_type }}"
class="form-control"
id="{{ field.id_for_label }}"
placeholder="{{field.label}}"
name="{{field.name}}">
</div>
{% if field.errors %}
<span style="color:red">{{ field.errors }}</span>
{% endif %}
{% endfor %}
<button type="submit" class="btn btn-primary mt-3">로그인</button>
</form>
</div>
</div>
{% endblock %}
글 잘 읽었습니다. 깔끔하게 정리하셨네요!