[Two Scoops of Django] 11장. 장고 폼의 기초

guava·2021년 10월 27일
1

Two Scoops of Django

목록 보기
11/12
post-thumbnail

Two Scoops of Django 3.x를 보고 정리한 글입니다.

Daniel’s made-up statistics

  • 장고 프로젝트의 100%는 Forms를 사용해야 한다.
  • 장고 프로젝트의 95%는 ModelForms를 사용해야 한다.
  • 모든 장고 프로젝트의 91%가 ModelForms를 사용합니다.
  • ModelForms의 80%는 사소한 논리를 필요로 한다.
  • ModelForms의 20%는 복잡한 로직을 필요로 한다.

장고를 이용한 데이터 처리 시 까다로운 상황에서 폼을 통해 극복할 수 있다.
또한 어떠한 데이터든 간에 입력 데이터라고 한다면 장고 폼을 이용해 유효성 검사를 해야한다.

11.1 장고 폼을 이용하여 모든 입력데이터에 대한 유효성 검사하기

  • 장고 폼은 유효성 검사를 위한 최적이 도구다.
  • POST가 포함된 HTTP 요청을 받아서 유효성 검사를 하는데 이용되지만 이것 외에도 사용이 가능하다.
  • 다음은 CSV 파일을 받아 모델에 업데이트하는 예제이다.

Example 11.1: How Not to Import CSV (나쁜 예제)

  • 유효성 검사를 진행하지 않고 모델에 전달한다.
  • add_csv_purchases에 유효성 검사 코드를 추가할 수 있지만 매번 데이터가 바뀔 때마다 유지 관리되기가 쉽지 않다.
import csv

from django.utils.six import StringIO

from .models import Purchase

def add_csv_purchases(rows):
    rows = StringIO.StringIO(rows)
    records_added = 0
    
    # 첫번째 csv행이 키인 행당 dict를 생성
    for row in csv.DictReader(rows, delimiter=','):
        # 절대 따라하지 말 것: 유효성 검사 없이 데이터를 모델에 전달
        Purchase.objects.create(**row)
        records_added += 1
    return records_added

Example 11.2: How to Safely Import CSV

  • 폼을 통해 유효성 검사를 진행 후 모델에 전달하거나 에러 메시지를 추가한다.
  • 장고에서 제공하는 데이터 테스트 프레임워크(장고 폼)를 이용하였다.
import csv

from django.utils.six import StringIO

from django import forms

from .models import Purchase, Seller


class PurchaseForm(forms.ModelForm):

    class Meta:
        model = Purchase

    def clean_seller(self):
        seller = self.cleaned_data['seller']
        try:
            Seller.objects.get(name=seller)
        except Seller.DoesNotExist:
            msg = '{0} does not exist in purchase #{1}.'.format(
                seller,
                self.cleaned_data['purchase_number']
            )
            raise forms.ValidationError(msg)
        return seller

    def add_csv_purchases(rows):
        rows = StringIO.StringIO(rows)

        records_added = 0
        errors = []
        # 첫번째 csv행이 키인 행당 dict를 생성
        for row in csv.DictReader(rows, delimiter=','):
            # PurchaseForm에 로우 데이터 추가
            form = PurchaseForm(row)
            # 로우 데이터가 유효한지 검사
            if form.is_valid():
                # 로우 데이터가 유효하므로 해당 레코드 저장
                form.save()
                records_added += 1
            else:
                errors.append(form.errors)
                
        return records_added, errors

추가로 다음과 같이 에러에 code 파라미터를 이용하면 에러 원인을 명확하게 추적할 때 용이할 것이다.
forms.ValidationError(_('Invalid value'), code='invalid')

11.2 HTML 폼에서 POST 메서드 이용하기

Example 11.3: HTML 폼을 이용한 POST 전송

<form action="{% url 'flavor_add' %}" method="POST">
  • 데이터를 변경하는 대부분의 HTML 폼은 POST 메서드를 이용하여 데이터를 전송한다.
  • 검색폼은 유일하게 GET을 사용한다. 데이터를 변경하지 않기 때문이다.

11.3 데이터를 변경하는 HTTP 폼은 언제나 CSRF 보안을 이용해야 한다

  • CSRF 보안을 사용하지 않으면 치명적인 보안 문제를 일으킬 수 있다.
  • 장고의 CsrfViewMiddleware는 사이트 전체에 걸쳐 적용되며 csrf_protect를 뷰에 데코레이팅 하지 않아도 된다.
  • dj-rest-auth(with drf)와 같은 입증된 라이브러리에 의한 머신 사이에 이용되는 API 제작 시에는 csrf를 제외할 수 있다.
  • API 요청은 요청별로 서명/인증되어야 하므로 HTTP 쿠키 의존은 현실적이지 않기 때문이다.

11.3.1 AJAX를 통해 데이터 추가하기

11.4 장고의 폼 인스턴스 속성을 추가하는 방법 이해하기

  • 장고 폼에서 clean(), clean_FOO(), save() 메서드에 추가로 폼 인스턴스 속성이 필요할 때가 있다.
  • 샘플 사례로 request.user를 사용할수 있는 예시를 제시하겠다.

Example 11.4: Taster Form

먼저 폼 예제다. keyword arguments에서 user를 꺼내서 인스턴스 변수로 정의하고있다.

from django import forms

from .models import Taster

class TasterForm(forms.ModelForm):
    
    class Meta:
        model = Taster

    def __init__(self, *args, **kwargs):
        # set the user as an attribute of the form 
        self.user = kwargs.pop('user')
        super().__init__(*args, **kwargs)

Example 11.5: Taster Update View

뷰 예제다. 앞서 폼에서 keyword arguments에서 user를 꺼낼 수 있도록 keyword arguments에 유저를 업데이트 하고있다.

from django.contrib.auth.mixins import LoginRequiredMixin 
from django.views.generic import UpdateView

from .forms import TasterForm 
from .models import Taster

class TasterUpdateView(LoginRequiredMixin, UpdateView): 
    model = Taster
    form_class = TasterForm
    success_url = '/someplace/'
    
    def get_form_kwargs(self):
        """폼에 keyword arguments를 주입한다"""
        # grab the current set of form #kwargs
        kwargs = super().get_form_kwargs()
        # Update the kwargs with the user_id
        kwargs['user'] = self.request.user 
        return kwargs

11.5 폼이 유효성 검사하는 방법 알아두기

  • 폼의 유효성 검사를 이해함으로써 코드의 품질을 높일 수 있다.
  • form.is_valid()가 호출될 때 그 이면에서는 여러 가지 일이 다음 순서로 진행 된다.
  1. 폼이 데이터를 받으면 form.is_valid()form.full_clean() 메서드를 호출한다.
  2. form.full_clean()은 폼 필드들과 각각의 필드 유효성을 하나하나 검사하면서 다음을 수행한다.
    • 필드에 들어온 데이터에 대해 to_python()을 이용해 파이썬 방식으로 변환하거나 변환할 때 문제가 생기면 ValidationError를 일으킨다.
    • 커스텀 유효성 검사기(validator)를 포함한 각 필드에 특별한 유효성을 검사한다. 문제가 있을 때 ValidationError를 일으킨다.
    • 폼에 clean_<field>() 메서드가 있으면 이를 실행한다.
  3. form.full_cleanform.clean() 메서드를 실행한다.
  4. ModelForm 인스턴스의 경우 form.post_clean()이 다음 작업을 한다.
    • form.is_valid()TrueFalse로 설정되어 있는 것과 관계 없이 ModelForm의 데이터를 모델 인스턴스로 설정한다.
    • 모델의 clean()메서드를 호출한다. 참고로 ORM을 통해 모델 인스턴스를 저장할 때는 모델의 clean()메서드가 호출되지는 않는다.

11.5.1 모델폼 데이터는 폼에 먼저 저장된 이후 모델 인스턴스에 저장된다

ModelForm에서 폼 데이터가 저장되는 단계

  1. 첫 번째로 폼 데이터가 폼 인스턴스에 저장된다.
  2. 그 다음에 폼 데이터가 모델 인스턴스에 저장된다.
  • form.save()에 의해 적용되기 전까지 ModelForm이 모델 인스턴스로 저장되지 않는다. 이러한 분리된 과정 자체를 장점으로 활용할 수 있다.
  • 예를들면 폼 입력 시도 실패에 대해 좀 더 자세한 사항이 필요할 때 사용자가 입력한 폼의 데이터와 모델 인스턴스의 변화를 둘 다 저장할 수 있다.

Example 11.6: Form Failure History Model

# core/models.py
from django.db import models

class ModelFormFailureHistory(models.Model):
    form_data = models.TextField()
    model_data = models.TextField()

Example 11.7: FlavorActionMixin

  • form_invalid()는 유효성 검사에 실패한 후에 호출되는 함수다.
  • 잘못된 폼과 모델의 데이터를 저장하기위해 form_invalid()를 사용하였다.
# flavors/views.py
import json

from django.contrib import messages
from django.core import serializers

from core.models import ModelFormFailureHistory

class FlavorActionMixin:
    @property
    def success_msg(self):
        return NotImplemented

    def form_valid(self, form):
        messages.info(self.request, self.success_msg)
        return super().form_valid(form)

    def form_invalid(self, form):
        """나중에 참조할 수 있도록 잘못된 폼과 모델의 데이터를 저장합니다."""
        form_data = json.dumps(form.cleaned_data)
        # Serialize the form.instance
        model_data = serializers.serialize('json', [form.instance])
        model_data = model_data[1:-1]
        ModelFormFailureHistory.objects.create(
               form_data=form_data,
               model_data=model_data
        )
        return super().form_invalid(form)

11.6 Form.add_error()를 이용하여 폼에 에러 추가하기

Example 11.8: Using Form.add_error

  • Form.add_error() 메서드를 활용한 Form.clean()을 간소화
from django import forms

class IceCreamReviewForm(forms.Form):
    # Rest of tester form goes here
    # ...

    def clean(self):
        cleaned_data = super().clean()
        flavor = cleaned_data.get('flavor')
        age = cleaned_data.get('age')
        if flavor == 'coffee' and age < 3:
            # Record errors that will be displayed later.
            msg = 'Coffee Ice Cream is not for Babies.'
            self.add_error('flavor', msg)
            self.add_error('age', msg)

        # Always return the full collection of cleaned data.
        return cleaned_data

11.6.1 유용한 폼 메서드들

11.7 기존에 만들어진 위젯이 없는 필드

  • django.contrib.postgres필드의 ArrayField, HStoreField는 장고의 HTML필드들과 제대로 작동하지 않는다.
  • 그럼에도 불구하고 이러한 필드와 함께 폼을 계속 사용해야 한다. (11.1 참고)

11.8 위젯 커스터마이징

  • 장고는 장고 위젯의 HTML을 재정의하거나 사용자 정의 위젯을 만드는 것이 간단하다.
    위젯을 사용 시 어드바이스
  1. 심플하게 정의하자. 프레젠테이션의 역할에만 집중해야 한다.
  2. 위젯이 데이터를 변경해서는 안된다.
  3. 장고 패턴을 따르고 모든 사용자 정의 위젯을 widget.py모듈에 넣는다.

11.8.1 내장 위젯의 HTML 재정의

  • Bootstrap, Zurb 및 기타 반응형 프론트엔드 도구를 통합하기에 유용하다.
  • 기본 템플릿을 재정의 하면 모든 폼이 이 변경을 사용하기에 주의해야 한다.
  • 기본 템플릿을 재정의하려면 다음과 같이 settings.py를 다음과 같이 변경해야 한다.

Example 11.9: Overriding Django Form Widget HTML

# settings.py
FORM_RENDERER = 'django.forms.renderers.TemplatesSetting'
INSTALLED_APPS = [
    # ...
    'django.forms',
    # ...
]
  • 완료 시 템플릿 디렉터리 안에 디렉터리를 만들고 템플릿 재정의를 한다.

추가 정보

11.8.2 새로운 커스텀 위젯 생성하기

  • 특정 데이터 타입에 대한 변경을 제한하는 등 위젯을 세밀하게 제어하기 위해 커스텀 위젯을 만들 수 있다.
  1. 여기로 이동해 원하는 것과 가장 가까운 위젯을 선택한다.
  2. 원하는 대로 작동하도록 위젯을 변경한다. 변경은 최소화 한다.

Example 11.10: Creating an Ice Cream Widget

# flavors/widgets.py
from django.forms.widgets import TextInput

class IceCreamFlavorInput(TextInput):
    """Ice cream flavors는 Ice Cream으로 끝나도록 하기 위한 예시"""
    def get_context(self, name, value, attrs):
        context = super().get_context(name, value, attrs)
        value = context['widget']['value']
        if not value.strip().lower().endswith('ice cream'):
            context['widget']['value'] = '{} IceCream'.format(value)
        return context

다음을 주의하며 위젯을 수정하자.

  • 위젯이 하는 일은 오직 프레젠테이션이다.
  • 위젯은 브라우저에서 돌아오는 데이터를 확인, 수정하지 않는다. 이건 모델이나 폼의 역할이다.
  • 최소한으로만 기능을 확장해야 한다.

0개의 댓글