πŸ”« 폼 νŒ¨ν„΄λ“€

Two Scoops of Django μ±…μ—μ„œ μ†Œκ°œν•œ 폼 νŒ¨ν„΄λ“€μ„ μš”μ•½ μ •λ¦¬ν•΄λ³΄μ•˜λ‹€.

말 κ·ΈλŒ€λ‘œ μš”μ•½ν•œ λ‚΄μš©μ΄κΈ° λ•Œλ¬Έμ— μ½”λ“œμ— λŒ€ν•œ μžμ„Έν•œ μ„€λͺ…듀은 책을 μ°Έκ³ ν•˜μ‹œλŠ” 게 μ’‹λ‹€ :)

νŒ¨ν„΄ 1: κ°„λ‹¨ν•œ λͺ¨λΈνΌκ³Ό κΈ°λ³Έ μœ νš¨μ„± 검사기

from django.views.generic import CreateView, UpdateView

from braces.views import LoginRequiredMixin

from .models import Flavor


class FlavorCreateView(LoginRequiredMixin, CreateView):
    model = Flavor
    fields = ('title', 'slug', 'scoops_remaining')


class FlavorUpdateView(LoginRequiredMixin, UpdateView):
    model = Flavor
    fields = ('title', 'slug', 'scoops_remaining')
  • Flavor λͺ¨λΈμ„ FlavorCreateView와 FlavorUpdateViewμ—μ„œ μ΄μš©ν•˜λ„λ‘ ν•œλ‹€.
  • 두 λ·°μ—μ„œ Flavor λͺ¨λΈμ— κΈ°λ°˜μ„ λ‘” ModelForm을 μžλ™ μƒμ„±ν•œλ‹€.
  • μƒμ„±λœ ModelForm이 Flavor λͺ¨λΈμ˜ κΈ°λ³Έ ν•„λ“œ μœ νš¨μ„± 검사기λ₯Ό μ΄μš©ν•˜κ²Œ λœλ‹€.

νŒ¨ν„΄ 2: λͺ¨λΈνΌμ—μ„œ μ»€μŠ€ν…€ 폼 ν•„λ“œ μœ νš¨μ„± 검사기 μ΄μš©ν•˜κΈ°

from django.core.exceptions import ValidationError


def validate_tasty(value):
    if not value.startswith("Tasty"):
        msg = "Must start with Tasty"
        raise ValidationError(msg)

validate_tasty()λ₯Ό λ‹€λ₯Έ μ’…λ₯˜μ˜ λ””μ €νŠΈ λͺ¨λΈμ— μ μš©ν•˜κΈ° μœ„ν•΄ μš°μ„  TastyTitleAbstractModelμ΄λΌλŠ” ν”„λ‘œμ νŠΈ μ „λ°˜μ—μ„œ μ΄μš©ν•  수 μžˆλŠ” 좔상화 λͺ¨λΈμ„ μΆ”κ°€ν•œλ‹€. Flavor와 Milkshake λͺ¨λΈμ΄ 각기 λ‹€λ₯Έ λͺ¨λΈμ΄λΌ κ°€μ •ν•  λ•Œ μœ νš¨μ„± 검사기λ₯Ό ν•˜λ‚˜μ˜ μ•±μ—λ§Œ μΆ”κ°€ν•˜λŠ” 것은 μ μ ˆν•˜μ§€ μ•Šμ„ 것이닀.

λ”°λΌμ„œ κ·Έ λŒ€μ‹  core/models.py λͺ¨λ“ˆμ„ λ§Œλ“€κ³  TastyTitleAbstractModel을 이곳에 μΆ”κ°€ν•˜κ² λ‹€.

from django.db import models

from .validators import validate_tasty


class TastyTitleAbstractModel(models.Model):
    title = models.CharField(max_length=255, validators=[validate_tasty])

    class Meta:
        abstract = True

μ•žμ˜ core/models.py μ½”λ“œμ—μ„œ λ§ˆμ§€λ§‰ 두 쀄이 TastyTitleAbstractModel을 좔상화 λͺ¨λΈλ‘œ λ§Œλ“€μ–΄ μ€€λ‹€. 이제 μ›λž˜ flavors/models.py의 Flavor μ½”λ“œμ—μ„œ TastyTitleAbstractModel을 λΆ€λͺ¨ 클래슀둜 지정해 보겠닀.

from django.core.urlresolvers import reverse
from django.db import models

from core.models import TastyTitleAbstractModel


class Flavor(TastyTitleAbstractModel):
    slug = models.SlugField()
    scoops_remaining = models.IntegerField(default=0)

    def get_absolute_url(self):
        return reverse("flavors:detail", kwargs={"slug": self.slug})
  • 단지 νΌμ—λ§Œ validate_tasty()λ₯Ό μ΄μš©ν•˜κ³ μž ν•  λ•ŒλŠ” μ–΄λ–»κ²Œ ν•΄μ•Ό ν• κΉŒ?
  • 타이틀 말고 λ‹€λ₯Έ ν•„λ“œμ— 이λ₯Ό μ μš©ν•˜κ³  싢을 λ•ŒλŠ” μ–΄λ–»κ²Œ ν•  것인가?

μ΄λŸ¬ν•œ κ²½μš°λ“€μ„ μ²˜λ¦¬ν•˜κΈ° μœ„ν•΄ μ»€μŠ€ν…€ ν•„λ“œ μœ νš¨μ„± 검사기λ₯Ό μ΄μš©ν•˜λŠ” μ»€μŠ€ν…€ FlavorForm을 μž‘μ„±ν•˜κΈ°λ‘œ ν•œλ‹€.

from django import forms

from core.validators import validate_tasty
from .models import Flavor


class FlavorForm(forms.ModelForm):
    def __init__(self, *args, **kwargs):
        super(FlavorForm, self).__init__(*args, **kwargs):
        self.fields["title"].validators.append(validate_tasty)
        self.fields["slug"].validators.append(validate_tasty)

    class Meta:
        model = Flavor

μž₯고의 λͺ¨λΈ 기반 μˆ˜μ • λ·°λŠ” 뷰의 λͺ¨λΈ 속성을 기반으둜 λͺ¨λΈνΌμ„ μžλ™μœΌλ‘œ 생성해 μ€€λ‹€.

from django.contrib import messages
from django.views.generic import CreateView, UpdateView, DetailView

from braces.views import LoginRequiredMixin

from .models import Flavor
from .forms import FlavorForm


class FlavorActionMixin(object):

    model = Flavor
    fields = ('title', 'slug', 'scoops_remaining')

    @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)


class FlavorCreateView(LoginRequiredMixin, FlavorActionMixin, CreateView):

    success_msg = "created"
    form_class = FlavorForm


class FlavorUpdateView(LoginRequiredMixin, FlavorActionMixin, UpdateView):

    success_msg = "updated"
    form_class = FlavorForm


class FlavorDetailView(DetailView):

    model = Flavor

νŒ¨ν„΄ 3: μœ νš¨μ„± κ²€μ‚¬μ˜ 클린 μƒνƒœ μ˜€λ²„λΌμ΄λ”©ν•˜κΈ°

  • 닀쀑 ν•„λ“œμ— λŒ€ν•œ μœ νš¨μ„± 검사
  • 이미 μœ νš¨μ„± 검사가 λλ‚œ λ°μ΄ν„°λ² μ΄μŠ€μ˜ 데이터가 ν¬ν•¨λœ μœ νš¨μ„± 검사

μœ„ 두 가지 경우 μ „λΆ€ μ»€μŠ€ν…€ 둜직으둜 clean() λ˜λŠ” clean_<field_name>() λ©”μ„œλ“œλ₯Ό μ˜€λ²„λΌμ΄λ”© ν•  수 μžˆλŠ” 졜적의 κ²½μš°λ‹€. κΈ°λ³Έ λ˜λŠ” μ»€μŠ€ν…€ ν•„λ“œ μœ νš¨μ„± 검사기가 μ‹€ν–‰λœ ν›„, μž₯κ³ λŠ” λ‹€μŒ κ³Όμ •μœΌλ‘œ clean() λ©”μ„œλ“œλ‚˜ clean_<field_name>() λ©”μ„œλ“œλ₯Ό μ΄μš©ν•˜μ—¬ μž…λ ₯된 λ°μ΄ν„°μ˜ μœ νš¨μ„±μ„ κ²€μ‚¬ν•˜λŠ” 절차λ₯Ό μ§„ν–‰ν•œλ‹€.

  1. clean() λ©”μ„œλ“œλŠ” μ–΄λ–€ νŠΉλ³„ν•œ ν•„λ“œμ— λŒ€ν•œ μ •μ˜λ„ 가지고 μžˆμ§€ μ•ŠκΈ° λ•Œλ¬Έμ— 두 개 λ˜λŠ” κ·Έ μ΄μƒμ˜ ν•„λ“œλ“€μ— λŒ€ν•΄ μ„œλ‘œ κ°„μ˜ μœ νš¨μ„±μ„ κ²€μ‚¬ν•˜λŠ” 곡간이 λœλ‹€.
  2. 클린(clean) μœ νš¨μ„± 검사 μƒνƒœλŠ” μ˜μ† 데이터에 λŒ€ν•΄ μœ νš¨μ„±μ„ κ²€μ‚¬ν•˜κΈ°μ— 쒋은 μž₯μ†Œλ‹€. 이미 μœ νš¨μ„± 검사λ₯Ό 일뢀 마친 데이터에 λŒ€ν•΄ λΆˆν•„μš”ν•œ λ°μ΄ν„°λ² μ΄μŠ€ 연동을 쀄일 수 μžˆλ‹€.
from django import forms
from flavors.models import Flavor


class IceCreamOrderForm(forms.Form):

    slug = forms.ChoiceField("Flavor")
    toppings = forms.CharField()

    def __init__(self, *args, **kwargs):
        super(IceCreamOrderForm, self).__init__(*args, **kwargs)
        self.fields["slug"].choices = [(x.slug, x.title) for x in Flavor.objects.all()]

    def clean_slug(self):
        slug = self.cleaned_data["slug"]
        if Flavor.objects.get(slug=slug).scoops_remaining <= 0:
                raise forms.ValidationError(msg)
        return msg

def clean(self):
    cleaned_data = super(IceCreamOrderForm, self).clean()
    slug = cleanec_data.get("slug", "")
    toppings = cleaned_data.get("toppings", "")

    if "chocolate" in slug.lower() and "chocolate" in toppings.lower():
        msg = "Your order has too much chocolate."
        raise forms.ValidationError(msg)
    return cleaned_data

νŒ¨ν„΄ 4: 폼 ν•„λ“œ ν•΄ν‚Ήν•˜κΈ°(두 개의 CBV, 두 개의 폼, ν•œ 개의 λͺ¨λΈ)

from django.core.urlresolvers import reverse
from django.db import models


class IceCreamStore(models.Model):
    title = modes.CharField(max_length=100)
    block_address = models.TextField()
    phone = models.CharField(max_length=20, blank=True)
    description = models.TextField(blank=True)

    def get_absolute_url(self):
        return reverse("store_detail", kwargs={"pk": self.pk})

μ‚¬μš©μžκ°€ titleκ³Ό block_addressλŠ” μž…λ ₯ν•΄μ•Ό ν•˜μ§€λ§Œ phoneκ³Ό description ν•„λ“œλŠ” μž…λ ₯ν•˜μ§€ μ•Šμ•„λ„ 되게 κ΅¬μ„±λ˜μ–΄ μžˆλ‹€. 후에 μ‚¬μš©μžκ°€ phoneκ³Ό description ν•„λ“œλ₯Ό μΆ”κ°€μ μœΌλ‘œ μ—…λ°μ΄νŠΈν•˜λŠ” 것이 κ°€λŠ₯ν•˜λ„λ‘ κ΅¬μ„±ν•˜κ³  μ‹Άλ‹€λ©΄ μ–΄λ–»κ²Œ ν•΄μ•Όν• κΉŒ?

μž₯κ³  폼을 μ‚¬μš©ν•  땐 λ°˜λ“œμ‹œ λ‹€μŒ 사항을 κΈ°μ–΅ν•˜μž.

μ‹€μ²΄ν™”λœ 폼 κ°μ²΄λŠ” μœ μ‚¬ λ”•μ…”λ„ˆλ¦¬ 객체인 fields 속성 μ•ˆμ— κ·Έ ν•„λ“œλ“€μ„ μ €μž₯ν•œλ‹€. λ”°λΌμ„œ 폼으둜 ν•„λ“œμ˜ μ •μ˜λ₯Ό 볡사, 뢙이기 ν•˜λŠ” λŒ€μ‹ μ— κ°„λ‹¨ν•˜κ²Œ ModelForm의 __init__() λ©”μ„œλ“œμ—μ„œ μƒˆλ‘œμš΄ 속성을 μ μš©ν•˜λ©΄ λœλ‹€.

from .models import IceCreamStore


class IceCreamStoreUpdateForm(forms.ModelForm):

    class Meta:
        model = IceCreamStore

    def __init__(self, *args, **kwargs):
        super(IceCreamStoreUpdateForm, self).__init__(*args, **kwargs)
        self.fields["phone"].required = True
        self.fields["description"].required = True

κ²°κ΅­ κΈ°μ–΅ν•΄μ•Ό ν•  μ€‘μš”ν•œ 점은 μž₯고의 폼도 κ²°κ΅­ 파이썬 ν΄λž˜μŠ€λΌλŠ” 사싀이닀. μž₯고의 폼 λ˜ν•œ 객체둜 μ‹€μ²΄ν™”λ˜κ³  μŠˆνΌν΄λž˜μŠ€κ°€ λ˜μ–΄ λ‹€λ₯Έ 클래슀λ₯Ό μƒμ†ν•˜κΈ°λ„ ν•œλ‹€.

from django import forms

from .models import IceCreamStore


class IceCreamStoreCreateForm(forms.ModelForm):

    class Meta:
        model = IceCreamStore
        fields = ("title", "block_address", )


class IceCreamStoreUpdateForm(IceCreamStoreCreateForm):

    def __init__(self, *args, **kwargs):
        super(IceCreamStoreUpdateForm, self).__init__(*args, **kwargs)
        self.fields["phone"].requird = True
        self.fields["description"].required = True

    class Meta(IceCreamStoreCreateForm.Meta):
        fields = ("title", "block_address", "phone", "description", )
from django.views.generic import CreateView, UpdateView

from .forms import IceCreamStoreCreateForm
from .forms import IceCreamStoreUpdateForm
from .models import IceCreamStore


class IceCreamCreateView(CreateView):

    model = IceCreamStore
    form_class = IceCreamStoreCreateForm


class IceCreamUpdateView(UpdateView):

    model = IceCreamStore
    form_class = IceCreamStoreUpdateForm

νŒ¨ν„΄ 5: μž¬μ‚¬μš© κ°€λŠ₯ν•œ 검색 믹슀인 λ·°

class TitleSearchMixin(object):

    def get_queryset(self):
        queryset = super(TitleSearchMixin, self).get_queryset()

        q = self.request.GET.get("q")
        if q:
                return queryset.filter(title__icontains=q)
        return queryset
from django.views.generic import ListView

from core.views import TitleSearchMixin
from .models import Flavor


class FlavorListView(TitleSearchMixin, ListView):

    model = Flavor
from django.views import ListView

from core.views import TitleSearchMixin
from .models import Store


class IceCreamStoreListView(TitleSearchMixin, ListView):

    model = Store
<form action="" method="GET">
    <input type="text" name="q" />
    <button type="submit">search</button>
</form>

References

  • Two Scoops of Django

Written by@ugaemi
Record things I want to remember

🐱 GitHubπŸ“š Reading Space