본문으로 바로가기

Forms API 와 validation

category Django, Flask/🔫 Django 2020. 7. 3. 20:18
 

The Forms API | Django documentation | Django

The Django Software Foundation deeply values the diversity of our developers, users, and community. We are distraught by the suffering, oppression, and systemic racism the Black community faces every day. We can no longer remain silent. In silence, we are

docs.djangoproject.com

 

장고 템플릿에 form을 수작업으로 작성한다고 해보자.

 

겨우 하나의 input 필드를 받는데도 이렇게 코드가 길다. input을 10개 정도 받으면 경우에 따라 코드가 100줄 정도가 될 수도 있다.

<form method="get" action="{% url "rooms:search"%}">
    <div>
        <h3>Amenities</h3>
        <ul>
            {% for amenity in amenities %}
                <li>
                    <label for="a_{{amenity.pk}}">{{amenity.name}}</label>
                    <input type="checkbox" name="amenities" id="a_{{amenity.pk}}" value="{{amenity.pk}}" 
                        {% if amenity.pk|slugify in s_amenities %} checked {% endif %} />
                </li>
            {% endfor %}
        </ul>
    </div>
    <input type="submit" value="Search"></input>
</form>

 

 

이러한 작업을 도와주는 FormAPI를 이용해보자

 

 

form API 를 활용한 form 구성

 

모델을 작성하는 것과 비슷합니다. 해당 필드명과 속성은 공식 문서의 form field를 참고합시다

(https://docs.djangoproject.com/en/3.0/ref/forms/fields/)

from django import forms
from . import models
from django_countries.fields import CountryField


class SearchForm(forms.Form):

    city = forms.CharField(initial="Anywhere")
    country = CountryField(default="KR").formfield()
    
    # FK 관계인 경우 ModelChoiceField를 사용합니다.
    room_type = forms.ModelChoiceField(
        required=False, empty_label="Any kind", queryset=models.RoomType.objects.all()
    )
    
    price = forms.IntegerField(required=False)
    guests = forms.IntegerField(required=False)
    bedrooms = forms.IntegerField(required=False)
    beds = forms.IntegerField(required=False)
    baths = forms.IntegerField(required=False)
    
    instant_book = forms.BooleanField(required=False)
    host = forms.BooleanField(required=False)
    
    # 다대다 관계인 경우 ModelMultipleChoiceField를 사용합니다.
    amenities = forms.ModelMultipleChoiceField(
        queryset=models.Amenity.objects.all(), widget=forms.CheckboxSelectMultiple
    )
    facilities = forms.ModelMultipleChoiceField(
        queryset=models.Facility.objects.all(), widget=forms.CheckboxSelectMultiple
    )

 

views.py에서 작성한 form을 렌더하고자 하는 템플릿에 context로 넘겨주기

from . import models, forms

def search(request):

    form = forms.SearchForm()

    return render(request, "rooms/search.html", {"form": form})

 

내부에서 form 사용하기 (form 태그 내부에 존재해야 합니다)

(https://docs.djangoproject.com/en/3.0/ref/forms/api/#outputting-forms-as-html)

기본적으로 form은 테이블 형태로 나타나지만 as_p (paragraph), as_ul 를 사용해 그 형태를 바꿀 수도 있습니다.

    <form method="get" action="{% url "rooms:search"%}">
        {{form.as_p}}
        <input type="submit" value="Search"></input>
    </form>

 

 

 

여기서 알아둬야할 것은 form field는 html 위젯을 렌더한다는 것이다. 결국 python 으로 작성해도 보여지는 화면은 html이지 않겠는가.

 

예를 들어 CharField의 경우 Text 타입의 input를 렌더한다. 이를 변경하고 싶다면 widget을 이용해 이렇게 바꿀 수 있다.

city = forms.CharField(initial="Anywhere", widget=forms.Textarea)

 

 

근본적으로 form을 직접 작성하지 않고 form api를 사용해야 하는 이유가 무엇인가?

 

편리하기 때문이다. 필드를 지정함으로써 자동으로 타입을 체킹할 수 있고, 제출된 폼에 여러 validation을 손 쉽게 체크할 수도 있게 된다. 물론 form을 직접 작성해도 이러한 기능을 작성할 수는 있으나 코드가 길어져 verbose해진다.

 

is_valid나 cleaned_data와 같은 훌륭한 메서드들을 이용할 수도 있다!

def search(request):

    country = request.GET.get("country")

    if country:

        # 작성한 form의 내용이 제출 후에도 value로 남아있게 합니다.
        form = forms.SearchForm(request.GET)

        # form이 에러없이 작성되었다면
        if form.is_valid():

            city = form.cleaned_data.get("city")
            room_type = form.cleaned_data.get("room_type")
            host = form.cleaned_data.get("host")
            amenities = form.cleaned_data.get("amenities")

            filter_args = {}

            if city != "Anywhere":
                filter_args["city__startswith"] = city

            if host is True:
                filter_args["host__superhost"] = True

            for amenity in amenities:
                filter_args["amenities"] = amenity


            rooms = models.Room.objects.filter(**filter_args)

    else:
        # country 없이 처음으로 렌더될 때
        form = forms.SearchForm()

    return render(request, "rooms/search.html", {"form": form, "rooms": rooms})

 

 

is_valid() 이용하여 validation하기

 

로컬 로그인을 구현하기 위한 form을 가져와보았습니다.

clean_[필드명] 메서드임에 주의합시다.

 

from django import forms
from . import models


class LoginForm(forms.Form):

    # passwordField는 없습니다
    email = forms.EmailField()
    password = forms.CharField(widget=forms.PasswordInput)

    # 각 필드의 validation을 위한 clean 함수로, 함수의 앞이 clean_[필드명]이어야 한다
    def clean_email(self):
        # self.cleaned_data에 해당 필드의 값이 들어있음
        email = self.cleaned_data.get("email")

        try:
            # 기존 User 모델에서 가입했는지 체크(유저네임을 이메일로 씀)
            models.User.objects.get(username=email)
            return email
        except models.User.DoesNotExist:
            raise forms.ValidationError("User does not exist")

    def clean_password(self):
        password = self.cleaned_data.get("password")
        try:
            email = self.cleaned_data.get("email")
            user = models.User.objects.get(username=email)
            if user.check_password(password):
                return password
            else:
                raise forms.ValidationError("password is wrong")

        except models.User.DoesNotExist:
            pass

 

verbose하다고 생각하면 clean() 메서드로 통합해도 됩니다.

 

clean_[필드명]과 다른 점은 clean_[필드명]은 에러는 그냥 raise하는 방식으로 해도 특정 필드는 검증하는 것이기 때문에 괜찮지만 clean()는 복합적이니 에러를 제기할 때 add_error 메서드를 통해 어느 필드에서 오류가 났는지 명시해야 한다는 것입니다.

 

여기서 사용한 User 모델의 인스턴스가 가진 메서드는 아래 공식문서를 참고합시다.

docs.djangoproject.com/en/3.1/ref/contrib/auth/#methods

 

from django import forms
from . import models


class LoginForm(forms.Form):

    # passwordField는 없습니다
    email = forms.EmailField()
    password = forms.CharField(widget=forms.PasswordInput)

    # 필드 간의 관계 등 다른 validation을 위해서는 clean함수를 사용하자.
    # clean_[필드명] 꼴이 verbose하다고 생각하면 clena으로 한 방에 통합해도 된다.
    def clean(self):
        email = self.cleaned_data.get("email")
        password = self.cleaned_data.get("password")

        try:
            user = models.User.objects.get(email=email)
            if user.check_password(password):
                return self.cleaned_data
            else:
                # clean 메서드를 사용할 시 어느 필드에서 오류가 났는지 add_error에서 명시해줄 것
                self.add_error("password", forms.ValidationError("password is wrong"))
        except models.User.DoesNotExist:
            self.add_error("email", forms.ValidationError("User does not exist"))

darren, dev blog
블로그 이미지 DarrenKwonDev 님의 블로그
VISITOR 오늘 / 전체