terça-feira, 22 de janeiro de 2013

Alterar campo do formulário em um Inline FormSet

Oi pessoal, tudo bem? Depois de algum tempo sem novidades, aqui esta o primeiro post do ano!

Este post esta mais para uma dica (ou um registro pessoal) de como resolver um problema aparentemente simples mas que me custou algumas horas de pesquisa. O problema esta relacionado a alterações que fazemos em campos, ou seus atributos, de formulários do Django. Por exemplo, se precisamos incluir opções ao choices de um ChoiceField ou CharField de forma dinâmica podemos sobrescrever o método inicializador do Form, mais ou menos assim:


class BookForm(forms.ModelForm):
    class Meta:
        model = Book

    def __init__(self, *args, **kwargs):
        super(BookForm, self).__init__(*args, **kwargs)
        new_choices = [('teste', 'Teste')]
        self.fields['category'].choices.extend(new_choices)

Obs: Neste caso estou inserindo uma tupla definida no código apenas para exemplificar.

Bem tranquilo certo? Mas e se este formulário fizer parte de um (Inline) FormSet?

O Django prove uma forma fácil de trabalhar com estas estruturas, são factories que constroem FormSets conforme nossa necessidade. Até aqui tudo bem, mas como realizar uma alteração como no exemplo acima em um Inline FormSet?

Neste caso a alteração no inicializador do formulário não funciona, isso acontece porque após criar o FormSet com a factory o formulário não é mais usado. A saída é utilizar um parâmetro pouco conhecido das funções que constroem FormSets, o formfield_callback. Este parâmetro deve ser uma função que fará a mudança no campo que você precisa alterar. Criei um exemplo para ajudar a entender:

def add_category(field, **kwargs):
    if field.name == 'category':
        additional_choices = [
            ('best_seller', 'Best Seller'),
            ('self_help', 'Auto Ajuda')
        ]
    for choice in additional_choices:
        if not choice in field.choices:
            field.choices.extend(additional_choices)
    
    return field.formfield(**kwargs)

Esta é a função que faz a alteração que preciso. Como o exemplo anterior, estou inserindo mais opções ao choices de um campo chamado category, estas são opções "hard coded" mas é possível fazer todo tipo de alteração que você imaginar. Se faz necessário uma pequena verificação se aquela opção já existe no choices do campo para não haver duplicações.

def author_edit_view(request, author):
    BookInlineFormSet = inlineformset_factory(
        Author, Book, extra=1,
        form=BookForm,
        formfield_callback=add_category
    )

    form = AuthorForm(request.POST or None, instance=author)
    formset = BookInlineFormSet(request.POST or None, instance=author)

    if form.is_valid() and formset.is_valid():
        form.save()
        formset.save()
        return HttpResponseRedirect('/inlines/')

    return render_to_response("manage_authors.html",
        {"formset": formset, "form": form},
        RequestContext(request))

Esta é a view de edição do modelo, ela cria o FormSet com a função inlineformset_factory usando a função add_category, instancia esse FormSet com os dados do post e uma instancia de author. Assim minha alteração é aplicada em todas as ocorrências neste FormSet:

Inline FormSet

Legal né? Gostou da dica? Então veja o código completo no meu Github:

https://github.com/rafaelnovello/Django-Examples

Bom, é isso pessoal! Espero que essa dica possa ajudar mais alguém. Se tiverem dúvidas, criticas ou sugestões escrevam ai nos comentários.

Um abraço!

Referencias

https://docs.djangoproject.com/en/1.4/topics/forms/modelforms/#inline-formsets
http://www.slideshare.net/pydanny/advanced-django-forms-usage
http://yergler.net/blog/2009/09/27/nested-formsets-with-django/
http://djangosnippets.org/snippets/1246/