Two Scoops of Django 3.x를 보고 정리한 글입니다.
ModelForm을 사용하고 기본 Validator를 수정 없이 이용한다.
FlavorCreateView
와 FlavorUpdateView
에서 이용하도록 한다.from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import CreateView, UpdateView
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']
이와 같이 뷰에 모델을 설정하는 것만으로 ModelForm을 사용하도록 할 수 있다. 이를 통해 기본 Validator도 활용하게 된다.
간단한 커스텀 필드 Validator를 활용한다.
# core/validators.py
from django.core.exceptions import ValidationError
def validate_tasty(value):
"""단어가'Tasty'로 시작하지 않으면 ValidationError를 일으킨다."""
if not value.startswith('Tasty'):
msg = 'Must start with Tasty'
raise ValidationError(msg)
Example 12.2에서 정의한 validator를 프로젝트 전반에서 이용할 수 있도록 한다.
이를 위해 추상화 모델을 정의하고 필드에 추가한다.
# core/models.py
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
TastyTitleAbstractModel
을 부모 클래스로 지정한다.TastyTitleAbstractModel
를 상속받은 모델들은 title필드가 validate_tasty
의 유효성 검사를 통과하지 못하면 에러가 발생할 것이다.# flavors/models.py
from django.db import models
from django.urls import reverse
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()
를 폼에만 적용하고 싶거나 다른 필드에 적용하고 싶다면 다음과 같이 커스텀 폼을 작성하자.
# flavors/forms.py
from django import forms
from .models import Flavor
from core.validators import validate_tasty
class FlavorForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['title'].validators.append(validate_tasty)
self.fields['slug'].validators.append(validate_tasty)
class Meta:
model = Flavor
# flavors/views.py
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import CreateView, DetailView, UpdateView
from .models import Flavor
from .forms import FlavorForm
class FlavorActionMixin:
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().form_valid(form)
class FlavorCreateView(LoginRequiredMixin, FlavorActionMixin, CreateView):
success_msg = 'created'
# FlavorForm 클래스를 명시적으로 추가
form_class = FlavorForm
class FlavorUpdateView(LoginRequiredMixin, FlavorActionMixin, UpdateView):
success_msg = 'updated'
# FlavorForm 클래스를 명시적으로 추가
form_class = FlavorForm
class FlavorDetailView(DetailView):
model = Flavor
clean()
, clean_<field name>()
메서드를 이용하여 입력된 데이터의 유효성 검사 절차를 진행한다.clean()
메서드는 두 개 또는 그 이상의 필드들에 대해 서로 간의 유효성을 검사하는 공간이 된다.clean_<field name>()
에서 통과한 데이터를 clean()
단계에서 활용할 수 있다.clean_<field name>()
또는 clean()
함수 내에서 ORM 문법을 통해 데이터베이스에 접근clean_slug()
에서 이미 유효성 검사가 끝난 DB의 데이터가 포함된 유효성 검사를 진행하고 있다.clean()
에서 다중 필드에 대한 유효성 검사를 진행하고 있다.# flavors/forms.py
from django import forms
from flavors.models import Flavor
class IceCreamOrderForm(forms.Form):
"""일반적으로 forms.ModelForm을 이요하면 된다. 하지만
모든 종류의 폼에서 이와 같은 방식을 적용할 수 있음을 보이기 위해
form.Form을 이용했다.
"""
slug = forms.ChoiceField(label='Flavor')
toppings = forms.CharField()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
"""Flavor 필드에서 정의하지 않고 __init__함수에서 선택 항목을 동적으로 설정한다.
필드 정의에서 설정하면 서버를 다시 시작하지 않으면 상태 업데이트가 양식에 반영되지 않습니다.
"""
self.fields['slug'].choices = [
(x.slug, x.title) for x in Flavor.objects.all()
]
# NOTE: filter를 사용해서 Flavor에 scoops이 남았는지 확인할 수 있으나
# filter()가 아닌 clean_slug를 이용하는 방법을 예로 들었다.
def clean_slug(self):
slug = self.cleaned_data['slug']
if Flavor.objects.get(slug=slug).scoops_remaining <= 0:
msg = 'Sorry, we are out of that flavor.'
raise forms.ValidationError(msg)
return slug
# Example 12.8: Custom clean() Form Method
def clean(self):
cleaned_data = super().clean()
slug = cleaned_data.get('slug', '')
toppings = cleaned_data.get('toppings', '')
# "too much chocolate" 유효성 검사의 예
in_slug = 'chocolate' in slug.lower()
in_toppings = 'chocolate' in toppings.lower()
if in_slug and in_toppings:
msg = 'Your order has too much chocolate.'
raise forms.ValidationError(msg)
return cleaned_data
모델 정의는 다음과 같다. title, block_address는 입력 받아야 하고 phone, description은 입력 받지 않아도 된다. (blank=True
속성)
# stores/models.py
from django.db import models
from django.urls import reverse
class IceCreamStore(models.Model):
title = models.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('stores:store_detail', kwargs={'pk': self.pk})
# stores/forms.py
from django import forms
from .models import IceCreamStore
class IceCreamStoreUpdateForm(forms.ModelForm):
# Don't do this! Duplication of the model field!
phone = forms.CharField(required=True)
# Don't do this! Duplication of the model field!
description = forms.TextField(required=True)
class Meta:
model = IceCreamStore
__init__()
메서드에서 지정된 필드의 기존 속성을 간단히 수정할 수 있다.# stores/forms.py
# self.fields라는 유사 딕셔너리 객체에서 description, phone을 호출
from django import forms
from .models import IceCreamStore
class IceCreamStoreUpdateForm(forms.ModelForm):
class Meta:
model = IceCreamStore
def __init__(self, *args, **kwargs):
#
super().__init__(*args, **kwargs)
self.fields['phone'].required = True
self.fields['description'].required = True
# stores/forms.py
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().__init__(*args, **kwargs)
self.fields['phone'].required = True
self.fields['description'].required = True
class Meta(IceCreamStoreCreateForm.Meta):
# 모든 필드를 보여준다.
fields = ['title', 'block_address', 'phone', 'description', ]
폼 클래스를 IceCreamStore의 create와 update뷰에서 이용한다.
# stores/views
from django.views.generic import CreateView, UpdateView
from .forms import IceCreamStoreCreateForm, IceCreamStoreUpdateForm
from .models import IceCreamStore
class IceCreamCreateView(CreateView):
model = IceCreamStore
form_class = IceCreamStoreCreateForm
class IceCreamUpdateView(UpdateView):
model = IceCreamStore
form_class = IceCreamStoreUpdateForm
검색을 위한 믹스인을 정의한다.
# core/views.py
class TitleSearchMixin:
def get_queryset(self):
# 부모의 get_queryset으로 부터 queryset을 가져온다.
queryset = super().get_queryset()
# q라는 GET 파라미터 가져오기
q = self.request.GET.get('q')
if q:
# return a filtered queryset
return queryset.filter(title__icontains=q)
# No q is specified so we return queryset
return queryset
Flavor를 위한 view를 정의한다. Example 12.14에서 정의한 믹스인을 상속받아 제목에 대한 검색 기능을 구현한다.
# add to flavors/views.py
from django.views.generic import ListView
from .models import Flavor
from core.views import TitleSearchMixin
class FlavorListView(TitleSearchMixin, ListView):
model = Flavor
Store를 위한 view를 정의한다. Example 12.14에서 정의한 믹스인을 상속받아 제목에 대한 검색 기능을 구현한다.
# add to stores/views.py
from django.views.generic import ListView
from .models import Store
from core.views import TitleSearchMixin
class IceCreamStoreListView(TitleSearchMixin, ListView):
model = Store
store를 위한 검색 폼
{# form to go into stores/store_list.html template #}
<form action="" method="GET">
<input type="text" name="q" />
<button type="submit">search</button>
</form>
flavor를 위한 검색 폼
{# form to go into flavors/flavor_list.html template #}
<form action="" method="GET">
<input type="text" name="q" />
<button type="submit">search</button>
</form>