sexta-feira, 9 de novembro de 2012

Django - Como ocultar condicionalmente inlines no admin?

Esta semana precisei fazer uma coisa no admin do Django que fazia tempo que tentava e não conseguia: ocultar os inlines condicionalmente. Ou melhor, queria só exibi-los para objetos existentes e ocultá-los para um objeto sendo criado. Ou qualquer outra condição que surgisse, até então, só exceções e decepções.
Sempre soube que é possível de ocultar os campos, mas e os benditos inlines? Todas as soluções que encontrei até hoje não resolviam isso. Ajudavam bastante, ou eram tão complicadas, que eram inviáveis de serem colocadas em prática. O que me salvou foi esta dica aqui que me deu as orientações de que precisava.



A dica explicava como sumir com os inlines no admin para os usuários sem privilégios de administrador. Basta deixar a propriedade inline_instances do ModelAdmin vazia, que os inlines somem sem causar nenhum problema. Esta foi a solução que ele utilizou:
def get_readonly_fields(self, request, obj=None):
    if request.user.is_superuser:
        return ()
    else:
        self.inline_instances = []
        return ()

Depois de vários testes, funcionou parcialmente no meu caso. O comportamente ficou estranho. Meio aleatório: hora funcionava, hora não. Nisso, acabei revirando o arquivo onde esse método se encontra na declaração da classe ModelAdmin no arquivo /django/contrib/admin/options.py do Django. Lá dentro encontrei um método relacionado ao get_readonly_fields, este aqui:
    def get_fieldsets(self, request, obj=None):
        "Hook for specifying fieldsets for the add form."
        if self.declared_fieldsets:
            return self.declared_fieldsets
        form = self.get_form(request, obj)
        fields = form.base_fields.keys() + list(self.get_readonly_fields(request, obj))
        return [(None, {'fields': fields})]

E foi ele quem resolveu meu problema.
Como disse, no meu caso eu queria sumir com os inlines quando estava na página de inserção e exibi-los quando estava na página de edição/inserção de um novo elemento. Então eu fiz uma verificação, não muito bonita, mas que funcionou muito bem:
class CategoriaAdmin(admin.ModelAdmin):
    """
    Options for the admin interface
    """
    inlines = [PaginaInline, ]
    list_display = ['categoria', 'ordem_exibicao', 'permalink']
    
    def get_fieldsets(self, request, obj=None):
        "Hook for specifying fieldsets for the add form."
        if self.declared_fieldsets:
            return self.declared_fieldsets
        form = self.get_form(request, obj)
        fields = form.base_fields.keys() + list(self.get_readonly_fields(request, obj))
        try:
            int(request.get_full_path().rsplit('/', 2)[1])
        except:
            self.inline_instances = []
        return [(None, {'fields': fields})]

Como dá pra ver, eu extrai o id do objeto da url do admin. Se tudo correr bem, é porque é uma edição. Caso ocorrer uma exceção, é uma inserção e eu sumo com os inlines.


Depois, que a inserção é feita, os inlines são exibidos. Porém, cada um desses inlines tem uma caixa de seleção (ForeignKey) e eu queria filtrá-la para exibir somente os objetos referentes ao objeto pai sendo editado. Esse caso é bem mais simples:
class PaginaInline(admin.StackedInline):
    """
    Allows profile to be added when creating user
    """
    model = Pagina
    extra = 1
    #max_num = 1
    formset = RequireOneFormSet

    def formfield_for_foreignkey(self, db_field, request, **kwargs):
        if db_field.name.startswith('item'):
            try:
                id = int(request.get_full_path().rsplit('/', 2)[1])
                kwargs["queryset"] = Item.objects.filter( categoria__id=id )
            except:
                kwargs["queryset"] = Item.objects.none()
            
        return super(PaginaInline, self).formfield_for_foreignkey(db_field, request, **kwargs)

Bem, o método formfield_for_foreignkey já é bem conhecido por aí. Então basta dizer que é o método chamado para recuperar os valores a serem exibidos em um campo que seja uma chave estrangeira. Dentro dele, editei kwargs["queryset"] que é a variável que armazena a consulta que vai retornar os valores desejados.
Eu ia terminar por aqui, mas acho que você deve estar se perguntando o que é esse formset chamado RequireOneFormSet. Se trata de um snippet muito útil que achei aqui. Ele serve para obrigar o usuário a cadastrar pelo menos um item, mesmo que nenhum seja obrigatório. No caso, o meu modelo inline tinha 6 campos não obrigatórios, mas não fazia sentido criá-lo sem ter ao menos um preenchido. Se o mínimo forem dois, é só editar o código desse formset e aumentar o valor no final (ali no "if completed < 1:"). Dê uma olhada nele:
from django import forms
from django.forms.models import BaseInlineFormSet
from django.utils.translation import ugettext as _


__all__ = ('RequireOneFormSet', )

class RequireOneFormSet(BaseInlineFormSet):
    """Require at least one form in the formset to be completed."""

    def clean(self):
        """Check that at least one form has been completed."""
        super(RequireOneFormSet, self).clean()
        for error in self.errors:
            if error:
                return
        completed = 0
        for cleaned_data in self.cleaned_data:
            # form has data and we aren't deleting it.
            if cleaned_data and not cleaned_data.get('DELETE', False):
                completed += 1

        if completed < 1:
            raise forms.ValidationError( _("At least one %s is required." %
                self.model._meta.object_name.lower()) )



Bem, por hoje é só pessoal! ;)





Nenhum comentário: