πŸ”« μž₯κ³  폼의 기초

μ§€λ‚œ ν¬μŠ€νŒ…μ— 이어 μ΄λ²ˆμ—λŠ” Two Scoops of Django μ±…μ˜ μž₯κ³  폼에 λŒ€ν•œ 뢀뢄을 μš”μ•½ μ •λ¦¬ν•΄λ³΄μ•˜λ‹€.

μž₯κ³  폼을 μ œλŒ€λ‘œ μ΄μš©ν•˜λ©΄ κ·Έλ™μ•ˆ 뷰에 κ±°μΆ”μž₯슀럽게 λŠ˜μ–΄λ†“μ•˜λ˜ μœ νš¨μ„± 검사에 λŒ€ν•œ 뢀뢄을 μƒλž΅ν•  수 μžˆλ‹€. 정말 κ°•λ ₯ν•œ κΈ°λŠ₯μ΄λ‹ˆ μ•Œμ°¨κ²Œ 써먹도둝 ν•˜μž!

μž₯κ³  폼을 μ΄μš©ν•˜μ—¬ λͺ¨λ“  μž…λ ₯ 데이터에 λŒ€ν•œ μœ νš¨μ„± κ²€μ‚¬ν•˜κΈ°

μž₯κ³  폼은 파이썬 λ”•μ…”λ„ˆλ¦¬μ˜ μœ νš¨μ„±μ„ κ²€μ‚¬ν•˜λŠ” 데 μ΅œμƒμ˜ 도ꡬ닀. λŒ€λΆ€λΆ„μ˜ 경우 POSTκ°€ ν¬ν•¨λœ HTTP μš”μ²­μ„ λ°›μ•„ μœ νš¨μ„±μ„ κ²€μ‚¬ν•˜λŠ” 데 μ΄μš©ν•˜μ§€λ§Œ 이런 경우 μ™Έμ—λŠ” μ ˆλŒ€λ‘œ 쓰지 λ§λΌλŠ” μ œμ•½μ€ μ—†λ‹€.

λ‹€λ₯Έ ν”„λ‘œμ νŠΈλ‘œλΆ€ν„° CSV νŒŒμΌμ„ λ°›μ•„ λͺ¨λΈμ— μ—…λ°μ΄νŠΈν•˜λŠ” μž₯κ³  앱을 가지고 μžˆλ‹€κ³  ν•˜μž.

import csv
import StringIO

from .models import Purchase


def add_csv_purchases(rows):

    rows = StringIO.StringIO(rows)
    records_added = 0

    for row in csv.DictReader(rows, delimiter=','):
        purchase.objects.create(**row)
        records_added += 1
    return records_added

이 μ½”λ“œμ—μ„œ κ°„κ³Όν•˜κ³  μžˆλŠ” 점은 Purchase λͺ¨λΈμ—μ„œ λ¬Έμžμ—΄ κ°’μœΌλ‘œ μ €μž₯λ˜μ–΄ μžˆλŠ” μ…€λŸ¬κ°€ μ‹€μ œλ‘œ μ‘΄μž¬ν•˜λŠ” μ…€λŸ¬μΈμ§€ κ·Έ μœ νš¨μ„±μ„ κ²€μ‚¬ν•˜κ³  μžˆμ§€ μ•Šλ‹€λŠ” 점이닀. λ¬Όλ‘  add_csv_purchases() ν•¨μˆ˜μ— μœ νš¨μ„± 검사 μ½”λ“œλ₯Ό μΆ”κ°€ν•  μˆ˜λ„ μžˆκ² μ§€λ§Œ 맀번 데이터가 λ°”λ€” λ•Œλ§ˆλ‹€ λ³΅μž‘ν•œ μœ νš¨μ„± 검사 μ½”λ“œλ₯Ό ν•„μš”μ— 맞좰 μœ μ§€ κ΄€λ¦¬ν•˜κΈ°λž€ 맀우 번거둜운 일이닀.

μž₯고의 λͺ¨λΈ 폼을 μ΄μš©ν•˜λ©΄ λ‹€μŒκ³Ό 같이 μž…λ ₯ 데이터에 λŒ€ν•΄ κ°„λ‹¨ν•˜κ²Œ μœ νš¨μ„± 검사λ₯Ό ν•  수 μžˆλ‹€.

import csv
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 = f"{seller} does not exist in purchase #{self.cleaned_data['purchase_number']}."
            raise forms.ValidationError(msg)
        return seller


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

    records_added = 0
    errors = []

    for row in csv.DictReader(rows, delimiter=','):

        from = PurchaseForm(row)
        if form.is_valid():
            form.save()
            records_added += 1
        else:
            errors.append(form.errors)

    return records_added, errors

HTML νΌμ—μ„œ POST λ©”μ„œλ“œ μ΄μš©ν•˜κΈ°

데이터λ₯Ό λ³€κ²½ν•˜λŠ” λͺ¨λ“  HTML 폼은 POST λ©”μ„œλ“œλ₯Ό μ΄μš©ν•˜μ—¬ 데이터λ₯Ό μ „μ†‘ν•˜κ²Œ λœλ‹€.

<form action="{% url 'flavor_add' %}" method="POST">

데이터λ₯Ό λ³€κ²½ν•˜λŠ” HTTP 폼은 μ–Έμ œλ‚˜ CSRF λ³΄μ•ˆμ„ μ΄μš©ν•΄μ•Ό ν•œλ‹€

μž₯κ³ μ—λŠ” CSRF(Cross-Site Request Forgery protection, μ‚¬μ΄νŠΈ κ°„ μœ„μ‘° μš”μ²­ 방지)κ°€ λ‚΄μž₯λ˜μ–΄ μžˆλ‹€.

CSRF λ³΄μ•ˆμ„ μž μ‹œ κΊΌ 두어도 λ˜λŠ” κ²½μš°λ‘œλŠ” λ¨Έμ‹ λ“€ 사이에 μ΄μš©λ˜λŠ” API μ‚¬μ΄νŠΈλ₯Ό μ œμž‘ν•  λ•Œλ‹€. django-tastypieλ‚˜ django-rest-framework 같은 API ν”„λ ˆμž„μ›Œν¬μ—μ„œλŠ” μ΄λŸ¬ν•œ 처리λ₯Ό μžλ™μœΌλ‘œ λ‹€ν•΄μ€€λ‹€. API μš”μ²­μ€ 단일 μš”μ²­μ„ 기반으둜 인증 μš”μ²­/인증 ν—ˆμš©μ„ ν•˜κΈ° λ•Œλ¬Έμ— 이런 경우 일반적으둜 HTTP μΏ ν‚€λ₯Ό 인증 μˆ˜λ‹¨μœΌλ‘œ μ΄μš©ν•˜μ§€ μ•ŠλŠ”λ‹€.

μž₯고의 CsrfViewMiddlewareλ₯Ό μ‚¬μ΄νŠΈ 전체에 λŒ€ν•œ λ³΄ν˜Έλ§‰μœΌλ‘œ μ΄μš©ν•¨μœΌλ‘œμ¨ 일일이 μ†μœΌλ‘œ csrf_protectλ₯Ό 뷰에 λ°μ½”λ ˆμ΄νŒ…ν•˜μ§€ μ•Šμ•„λ„ λœλ‹€.

AJAXλ₯Ό 톡해 데이터 μΆ”κ°€ν•˜κΈ°

AJAXλ₯Ό 톡해 데이터λ₯Ό μΆ”κ°€ν•  λ•ŒλŠ” λ°˜λ“œμ‹œ μž₯고의 CSRF λ³΄μ•ˆμ„ μ΄μš©ν•΄μ•Ό ν•œλ‹€. μ ˆλŒ€ AJAX λ·°λ₯Ό CSRF에 μ˜ˆμ™Έ μ²˜λ¦¬ν•˜μ§€ 말기 λ°”λž€λ‹€. λŒ€μ‹ μ— HTTP 헀더에 X-CSRFToken을 섀정해두도둝 ν•œλ‹€.

μž₯고의 폼 μΈμŠ€ν„΄μŠ€ 속성을 μΆ”κ°€ν•˜λŠ” 방법 μ΄ν•΄ν•˜κΈ°

λ•Œλ•Œλ‘œ μž₯κ³  폼의 clean(), clean_FOO(), save() λ©”μ„œλ“œμ— μΆ”κ°€λ‘œ 폼 μΈμŠ€ν„΄μŠ€ 속성이 ν•„μš”ν•  λ•Œκ°€ μžˆλ‹€. 이럴 κ²½μš°μ—λŠ” request.user 객체λ₯Ό μ΄μš©ν•˜λ©΄ λœλ‹€.

from django import forms

from .models import Taster


class TasterForm(forms.ModelForm):

    class Meta:
        model = Taster

    def __init__(self, *args, **kwargs):
        self.user = kwargs.pop('user')
        super(TasterForm, self).__init__(*args, **kwargs)
from django.views.generic import UpdateView

from braces.views import LoginRequiredMixin

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):
            kwargs = super(TasterUpdateView, self).get_form_kwargs()
            kwargs['user'] = self.request.user
            return kwargs

폼이 μœ νš¨μ„±μ„ κ²€μ‚¬ν•˜λŠ” 방법 μ•Œμ•„λ‘κΈ°

form.is_valid()κ°€ 호좜될 λ•Œ μ—¬λŸ¬ 가지 일이 λ‹€μŒ μˆœμ„œλ‘œ μ§„ν–‰λœλ‹€.

  1. 폼이 데이터λ₯Ό λ°›μœΌλ©΄ form.is_valid()λŠ” form.full_clean() λ©”μ„œλ“œλ₯Ό ν˜ΈμΆœν•œλ‹€.
  2. form._full_clean()은 폼 ν•„λ“œλ“€κ³Ό 각각의 ν•„λ“œ μœ νš¨μ„±μ„ ν•˜λ‚˜ν•˜λ‚˜ κ²€μ‚¬ν•˜λ©΄μ„œ λ‹€μŒκ³Ό 같은 과정을 μˆ˜ν–‰ν•œλ‹€.

    1. ν•„λ“œμ— λ“€μ–΄μ˜¨ 데이터에 λŒ€ν•΄ to_python()을 μ΄μš©ν•˜μ—¬ 파이썬 ν˜•μ‹μœΌλ‘œ λ³€ν™˜ν•˜κ±°λ‚˜ λ³€ν™˜ν•  λ•Œ λ¬Έμ œκ°€ 생기면 ValidationErrorλ₯Ό μΌμœΌν‚¨λ‹€.
    2. μ»€μŠ€ν…€ μœ νš¨μ„± 검사기λ₯Ό ν¬ν•¨ν•œ 각 ν•„λ“œμ— νŠΉλ³„ν•œ μœ νš¨μ„±μ„ κ²€μ‚¬ν•œλ‹€. λ¬Έμ œκ°€ μžˆμ„ λ•Œ ValidationErrorλ₯Ό μΌμœΌν‚¨λ‹€.
    3. 폼에 clean_<field>() λ©”μ„œλ“œκ°€ 있으면 이λ₯Ό μ‹€ν–‰ν•œλ‹€.
  3. form.full_clean()이 form.clean() λ©”μ„œλ“œλ₯Ό μ‹€ν–‰ν•œλ‹€.
  4. ModelForm μΈμŠ€ν„΄μŠ€μ˜ 경우 form.post_clean()이 λ‹€μŒ μž‘μ—…μ„ ν•œλ‹€.

    1. form.is_valid()κ°€ Trueλ‚˜ False둜 μ„€μ •λ˜μ–΄ μžˆλŠ” 것과 관계없이 ModelForm 데이터λ₯Ό λͺ¨λΈ μΈμŠ€ν„΄μŠ€λ‘œ μ„€μ •ν•œλ‹€.
    2. λͺ¨λΈμ˜ clean() λ©”μ„œλ“œλ₯Ό ν˜ΈμΆœν•œλ‹€. 참고둜 ORM을 톡해 λͺ¨λΈ μΈμŠ€ν„΄μŠ€λ₯Ό μ €μž₯ν•  λ•ŒλŠ” λͺ¨λΈμ˜ clean() λ©”μ„œλ“œκ°€ ν˜ΈμΆœλ˜μ§€λŠ” μ•ŠλŠ”λ‹€.

λͺ¨λΈνΌ λ°μ΄ν„°λŠ” 폼에 λ¨Όμ € μ €μž₯된 이후 λͺ¨λΈ μΈμŠ€ν„΄μŠ€μ— μ €μž₯λœλ‹€

ModelFormμ—μ„œ 폼 λ°μ΄ν„°λŠ” 두 가지 각기 λ‹€λ₯Έ 단계λ₯Ό 톡해 μ €μž₯λœλ‹€.

  1. 첫 번째둜 폼 데이터가 폼 μΈμŠ€ν„΄μŠ€μ— μ €μž₯λœλ‹€.
  2. κ·Έ λ‹€μŒμ— 폼 데이터가 λͺ¨λΈ μΈμŠ€ν„΄μŠ€μ— μ €μž₯λœλ‹€.

form.save() λ©”μ„œλ“œμ— μ˜ν•΄ 적용되기 μ „κΉŒμ§€λŠ” ModelForm이 λͺ¨λΈ μΈμŠ€ν„΄μŠ€λ‘œ μ €μž₯λ˜μ§€ μ•ŠκΈ° λ•Œλ¬Έμ— μ΄λ ‡κ²Œ λΆ„λ¦¬λœ κ³Όμ • 자체λ₯Ό μž₯점으둜 μ΄μš©ν•  수 μžˆλ‹€.

예λ₯Ό λ“€λ©΄ 폼 μž…λ ₯ μ‹œλ„ μ‹€νŒ¨μ— λŒ€ν•΄ μ’€ 더 μžμ„Έν•œ 사항이 ν•„μš”ν•  λ•Œ, μ‚¬μš©μžκ°€ μž…λ ₯ν•œ 폼의 데이터와 λͺ¨λΈ μΈμŠ€ν„΄μŠ€μ˜ λ³€ν™”λ₯Ό λ‘˜ λ‹€ μ €μž₯ν•  수 μžˆλ‹€.

from django.db import models


class ModelFormFailuserHistory(models.Model):
    form_data = models.TextField()
    model_data = models.TextField()
import json


from django.contrib import messages
from django.cors import serializers
from core.models import ModelFormFailuerHistory


class FlavorActionMixin(self):

    @property
    def success_msg(self):
        return NotImplemented

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

    def form_invalid(self, form):
        form_data = json.dumps(form.cleaned_data)
        model_data = serializers.seralize("json", [form.instance])[1:-1]
        ModelFormFailuserHistory.objects.create(
                form_data=form_data,
                model_data=model_data
        )
        return super(FlavorActionMixin, self).form_invalid(form)

Form.add_error()λ₯Ό μ΄μš©ν•˜μ—¬ 폼에 μ—λŸ¬ μΆ”κ°€ν•˜κΈ°

from django import forms


class IceCreamReviewForm(forms.Form):
    # tester 폼의 λ‚˜λ¨Έμ§€ λΆ€λΆ„

    def clean(self):
        cleaned_data = super(TasterForm, self).clean()
        flavor = cleaned_data.get("flavor")
        age = cleaned_data.get("age")

        if flavor == 'coffee' and age < 3:
            msg = 'Coffee Ice Cream is not for Babies.'
            self.add_error('flavor', msg)
            self.add_error('age', msg)

        return cleaned_data

References

  • Two Scoops of Django

Written by@ugaemi
Record things I want to remember

🐱 GitHubπŸ“š Reading Space