Two Scoops of Django 3.x를 보고 정리한 글입니다.
장고를 이용한 데이터 처리 시 까다로운 상황에서 폼을 통해 극복할 수 있다.
또한 어떠한 데이터든 간에 입력 데이터라고 한다면 장고 폼을 이용해 유효성 검사를 해야한다.
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
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')
<form action="{% url 'flavor_add' %}" method="POST">
먼저 폼 예제다. 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)
뷰 예제다. 앞서 폼에서 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
form.is_valid()
는 form.full_clean()
메서드를 호출한다.form.full_clean()
은 폼 필드들과 각각의 필드 유효성을 하나하나 검사하면서 다음을 수행한다.to_python()
을 이용해 파이썬 방식으로 변환하거나 변환할 때 문제가 생기면 ValidationError를 일으킨다.clean_<field>()
메서드가 있으면 이를 실행한다.form.full_clean
이 form.clean()
메서드를 실행한다.form.post_clean()
이 다음 작업을 한다.form.is_valid()
가 True나 False로 설정되어 있는 것과 관계 없이 ModelForm의 데이터를 모델 인스턴스로 설정한다.clean()
메서드를 호출한다. 참고로 ORM을 통해 모델 인스턴스를 저장할 때는 모델의 clean()
메서드가 호출되지는 않는다. form.save()
에 의해 적용되기 전까지 ModelForm이 모델 인스턴스로 저장되지 않는다. 이러한 분리된 과정 자체를 장점으로 활용할 수 있다.# core/models.py
from django.db import models
class ModelFormFailureHistory(models.Model):
form_data = models.TextField()
model_data = models.TextField()
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)
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
# settings.py
FORM_RENDERER = 'django.forms.renderers.TemplatesSetting'
INSTALLED_APPS = [
# ...
'django.forms',
# ...
]
# 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
다음을 주의하며 위젯을 수정하자.