
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
다음을 주의하며 위젯을 수정하자.