Django newforms tip: 폼요소에 같은 이름 여러번 사용하기 :: 2008/03/19 00:05

외부키로 연결된 많은 항목들을 저장하는 폼을 만들 경우. 가령, 개인정보를 입력하는 폼에 복수개의 취미를 입력받아야 한다고 할 때, 보통 다음의 방법을 생각할 수 있다.

접근방법
 1. "+/-" 버튼을 만들고, "+"를 누를 때마다 폼요소(text input)를 하나씩 만든다.
 2. 만들어지는 폼요소의 이름을 hobby1, hobby2 이런식으로 자동증가 하도록 한다.
 3. submit 후, 서버측에서 hobby+숫자의 조합으로 값이 있을 때까지 찾은 뒤 처리한다.

위방법을 구현하자니, 조금 짜증나는 면이 있다. 총 몇개의 취미가 입력될지도 알 수 없고, 그때그때 자바스크립트로 폼요소 추가코드를 만든다 하더라도, 서버측에서 일일이 값을 취하는 것이 매끄럽지 못하다.

그러던 중, 몰랐던 사실 가운데 하나. 폼요소에 같은 이름을 여러번 사용했을 경우, 그 값들을 모두 받아 올 수 있었던 것. 이는 php에서 폼요소를 <input type="text" name="hobbys[]"> 처럼 쓰는 것에서 힌트를 얻었다. php 가 아닌 경우, []는 안써도 무방하다.

즉,
<form method="post">
    <input type="text" name="name" />
    <input type="text" name="hobbys" />
    <input type="text" name="hobbys" />
    <input type="text" name="hobbys" />
</form>
로 되어있는 폼에서 hobbys 에 입력된 3곳의 값을 모두 가져올 수 있다는 얘기다. (따로 HTTP 스펙에 있는지는 확인해보지 않았음. 어쨌건 몰랐던 사실)

Django
에서는 아래의 방법으로 hobbys의 값들을 가져올 수 있다.
request.POST.getlist('hobbys')
물론 일반적인 아래의 방법으로는 마지막 값만 가져온다.
request.POST.get('hobbys')

해서, newforms 에서는 다음의 방법으로 hobbys를 가져다 쓸 수 있다.
from django import newforms as forms
class MyForm(forms.Form):
    hobbys = forms.CharField()
    def clean_hobbys(self):
        return self.data.getlist('hobbys')
       
하지만, 위의 방법은 데이터 입력은 잘 동작하지만, form.is_valid()를 통과하지 못하고 폼에러가 랜더링 될 때, 마지막 입력값만을 보여주게 된다. 만일 사용자가 hobbys를 다수 입력하고서, 다른 항목의 입력오류를 냈을 때, 고스란히 다시 입력해야 한다는 것.

폼 필드를 랜더링하는 과정에서 그 마지막 값만 사용되는 것이 분명하다. 그렇다면 방법이 없는 것일까? 혹시나 하고 비슷한 케이스가 없나 생각해보니, MultipleSelect 는 복수개의 값을 하나의 필드에 제대로 보여주고 있더라는 것. 아하 그럼 나만의 위젯을 따로 만들어서, 랜더링함수를 재정의하면 되겠군.
from django import newforms as forms
from django.newforms.widgets import TextInput

class MultipleTextInput(TextInput):
    def render(self, name, value, attrs=None):
        if value is None: value = []
        final_attrs = self.build_attrs(attrs, type=self.input_type, name=name)
        if value != []:
            str_values = set([force_unicode(v) for v in value]) # Normalize to strings.
            final_attrs['value'] = force_unicode('|'.join(str_value))
        return mark_safe(u'<input%s />' % flatatt(final_attrs))

class MyForm(forms.Form):
    hobbys = forms.CharField(widget=MultipleTextInput)
    def clean_hobbys(self):
        return self.data.getlist('hobbys')

"|" 구분자로 모든 항목들을 다 보여주려는 속셈. 어짜피 나중에 JavaScript로 따로 폼요소들을 만들어주어도 된다. 그러나 어쩐일인지, render의 세번째 인수인 value는 여전히 리스트가 아니라, 단일 문자열이다. 대체 어떤 원리로 저 value 가 MultipleSelect 에는 리스트가 되며, TextInput 에는 단일 문자열이 되는가? 역시나 Django 문서화는 매우 아쉬운 부분. 이런 내용은 문서에 없다. Django newforms 소스를 여기저기 찾아봤더니, 오호, value_from_datadict 라는 메쏘드가 있었군. 아래 메쏘드를 MultipleTextInput 에 추가해주면 된다.
    def value_from_datadict(self, data, files, name):
        if isinstance(data, MultiValueDict):
            return data.getlist(name)
        return data.get(name, None)

위 방법으로 복수개의 값들을 처리하는 폼 위젯을 만들 수 있었다. 어찌보면 꽤나 삽질 비슷하게 여기까지 오긴 했으나 (아 문서화...), newforms의 설계에 대해 좀 더 이해할 수 있었다. 문서에 없는 것은 직접 소스를 읽어보시오 라고 말하는 듯. (장점일라나? ㅡ.ㅡ;)

혹시나 위 방법이 일반적인 접근방법이 아닐 수도 있겠다. 그렇다하더라도, 이러한 방법으로 newforms를 커스터마이징하며 쓰는 것이 꽤나 그럴듯하게 웹개발을 OOP화 해 놓은 듯 보인다. Django는 쓰면 쓸수록 파이썬스러운 웹 OOP라는 생각.
Trackback Address :: http://yong27.biohackers.net/trackback/303
Name
Password
Homepage
Secret