Como filtrar as opções ForeignKey em um Django ModelForm?

227

Digamos que tenho o seguinte no meu models.py:

class Company(models.Model):
   name = ...

class Rate(models.Model):
   company = models.ForeignKey(Company)
   name = ...

class Client(models.Model):
   name = ...
   company = models.ForeignKey(Company)
   base_rate = models.ForeignKey(Rate)

Ou seja, existem vários Companies, cada um com um intervalo de Ratese Clients. Cada um Clientdeve ter uma base Rateescolhida de seu pai Company's Rates, não outra Company's Rates.

Ao criar um formulário para adicionar um Client, eu gostaria de remover as Companyopções (como isso já foi selecionado por meio de um botão "Adicionar cliente" na Companypágina) e limitar as Rateopções a isso Companytambém.

Como eu faço isso no Django 1.0?

Meu forms.pyarquivo atual é apenas clichê no momento:

from models import *
from django.forms import ModelForm

class ClientForm(ModelForm):
    class Meta:
        model = Client

E o views.pytambém é básico:

from django.shortcuts import render_to_response, get_object_or_404
from models import *
from forms import *

def addclient(request, company_id):
    the_company = get_object_or_404(Company, id=company_id)

    if request.POST:
        form = ClientForm(request.POST)
        if form.is_valid():
            form.save()
            return HttpResponseRedirect(the_company.get_clients_url())
    else:
        form = ClientForm()

    return render_to_response('addclient.html', {'form': form, 'the_company':the_company})

No Django 0.96, eu pude invadir isso fazendo algo como o seguinte antes de renderizar o modelo:

manipulator.fields[0].choices = [(r.id,r.name) for r in Rate.objects.filter(company_id=the_company.id)]

ForeignKey.limit_choices_toparece promissor, mas não sei como passar the_company.ide não sei se isso funcionará fora da interface do administrador.

Obrigado. (Parece uma solicitação bastante básica, mas se eu deveria redesenhar algo, estou aberto a sugestões.)

Tom
fonte
Obrigado pela dica para "limit_choices_to". Não resolve a sua pergunta, mas o meu :-) Docs: docs.djangoproject.com/en/dev/ref/models/fields/...
guettli

Respostas:

243

ForeignKey é representado por django.forms.ModelChoiceField, que é um ChoiceField cujas opções são um modelo QuerySet. Veja a referência para ModelChoiceField .

Portanto, forneça um QuerySet ao querysetatributo do campo . Depende de como seu formulário é criado. Se você criar um formulário explícito, terá campos nomeados diretamente.

form.rate.queryset = Rate.objects.filter(company_id=the_company.id)

Se você pegar o objeto ModelForm padrão, form.fields["rate"].queryset = ...

Isso é feito explicitamente na exibição. Não hackear.

S.Lott
fonte
Ok, isso parece promissor. Como faço para acessar o objeto de campo relevante? form.company.QuerySet = Rate.objects.filter (company_id = the_company.id)? ou através de um dicionário?
Tom
1
Ok, obrigado por expandir o exemplo, mas parece que tenho que usar form.fields ["rate"]. Queryset para evitar "'ClientForm' objeto não tem nenhum atributo 'rate'", estou perdendo alguma coisa? (e seu exemplo deve ser form.rate.queryset ser consistente também.)
Tom
8
Não seria melhor definir o conjunto de consultas dos campos, no __init__método do formulário ?
Lakshman Prasad /
1
@Lott o último comentário não está correto (ou o meu site não deve estar funcionando :). Você pode preencher os dados de validação usando a chamada super (...) .__ init__ no seu método substituído. Se você estiver fazendo várias dessas alterações no conjunto de consultas, é muito mais elegante empacotá-las, substituindo o método init .
7289 Michael
3
Felicidades @Slott, eu adicionei uma resposta, pois levaria mais de 600 caracteres para explicar. Mesmo que essa pergunta seja antiga, está obtendo uma pontuação alta no Google.
227 Michael
135

Além da resposta de S.Lott e como se tornar o Guru mencionado nos comentários, é possível adicionar os filtros do conjunto de consultas substituindo a ModelForm.__init__função. (Isso pode se aplicar facilmente a formulários regulares), pode ajudar na reutilização e manter a função de visualização organizada.

class ClientForm(forms.ModelForm):
    def __init__(self,company,*args,**kwargs):
        super (ClientForm,self ).__init__(*args,**kwargs) # populates the post
        self.fields['rate'].queryset = Rate.objects.filter(company=company)
        self.fields['client'].queryset = Client.objects.filter(company=company)

    class Meta:
        model = Client

def addclient(request, company_id):
        the_company = get_object_or_404(Company, id=company_id)

        if request.POST:
            form = ClientForm(the_company,request.POST)  #<-- Note the extra arg
            if form.is_valid():
                form.save()
                return HttpResponseRedirect(the_company.get_clients_url())
        else:
            form = ClientForm(the_company)

        return render_to_response('addclient.html', 
                                  {'form': form, 'the_company':the_company})

Isso pode ser útil para reutilizar, digamos, se você possui filtros comuns necessários em muitos modelos (normalmente eu declaro uma classe Form abstrata). Por exemplo

class UberClientForm(ClientForm):
    class Meta:
        model = UberClient

def view(request):
    ...
    form = UberClientForm(company)
    ...

#or even extend the existing custom init
class PITAClient(ClientForm):
    def __init__(company, *args, **args):
        super (PITAClient,self ).__init__(company,*args,**kwargs)
        self.fields['support_staff'].queryset = User.objects.exclude(user='michael')

Fora isso, estou apenas reafirmando o material do blog Django, do qual existem muitos bons por aí.

Michael
fonte
Há um erro de digitação no seu primeiro trecho de código, você está definindo args duas vezes em __init __ () em vez de args e kwargs.
TPK
6
Gosto mais dessa resposta, acho mais fácil encapsular a lógica de inicialização do formulário na classe form, em vez do método view. Felicidades!
Simétrico
44

Isso é simples e funciona com o Django 1.4:

class ClientAdminForm(forms.ModelForm):
    def __init__(self, *args, **kwargs):
        super(ClientAdminForm, self).__init__(*args, **kwargs)
        # access object through self.instance...
        self.fields['base_rate'].queryset = Rate.objects.filter(company=self.instance.company)

class ClientAdmin(admin.ModelAdmin):
    form = ClientAdminForm
    ....

Você não precisa especificar isso em uma classe de formulário, mas pode fazê-lo diretamente no ModelAdmin, pois o Django já inclui este método interno no ModelAdmin (nos documentos):

ModelAdmin.formfield_for_foreignkey(self, db_field, request, **kwargs
'''The formfield_for_foreignkey method on a ModelAdmin allows you to 
   override the default formfield for a foreign keys field. For example, 
   to return a subset of objects for this foreign key field based on the
   user:'''

class MyModelAdmin(admin.ModelAdmin):
    def formfield_for_foreignkey(self, db_field, request, **kwargs):
        if db_field.name == "car":
            kwargs["queryset"] = Car.objects.filter(owner=request.user)
        return super(MyModelAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)

Uma maneira ainda mais interessante de fazer isso (por exemplo, na criação de uma interface administrativa de front-end que os usuários possam acessar) é subclassificar o ModelAdmin e alterar os métodos abaixo. O resultado líquido é uma interface do usuário que APENAS mostra o conteúdo relacionado a eles, permitindo que você (um superusuário) veja tudo.

Eu substituí quatro métodos, os dois primeiros tornam impossível para um usuário excluir qualquer coisa e também remove os botões de exclusão do site de administração.

A terceira substituição filtra qualquer consulta que contenha uma referência a (no exemplo 'usuário' ou 'porco-espinho' (apenas como ilustração).

A última substituição filtra qualquer campo de chave estrangeira no modelo para filtrar as opções disponíveis da mesma forma que o conjunto de consultas básico.

Dessa forma, você pode apresentar um site de administração fácil de gerenciar, que permite que os usuários mexam com seus próprios objetos, e você não precisa se lembrar de digitar os filtros ModelAdmin específicos sobre os quais falamos acima.

class FrontEndAdmin(models.ModelAdmin):
    def __init__(self, model, admin_site):
        self.model = model
        self.opts = model._meta
        self.admin_site = admin_site
        super(FrontEndAdmin, self).__init__(model, admin_site)

remova os botões "excluir":

    def get_actions(self, request):
        actions = super(FrontEndAdmin, self).get_actions(request)
        if 'delete_selected' in actions:
            del actions['delete_selected']
        return actions

impede permissão de exclusão

    def has_delete_permission(self, request, obj=None):
        return False

filtra objetos que podem ser visualizados no site de administração:

    def get_queryset(self, request):
        if request.user.is_superuser:
            try:
                qs = self.model.objects.all()
            except AttributeError:
                qs = self.model._default_manager.get_queryset()
            return qs

        else:
            try:
                qs = self.model.objects.all()
            except AttributeError:
                qs = self.model._default_manager.get_queryset()

            if hasattr(self.model, user’):
                return qs.filter(user=request.user)
            if hasattr(self.model, porcupine’):
                return qs.filter(porcupine=request.user.porcupine)
            else:
                return qs

filtra as opções de todos os campos de chave estrangeira no site de administração:

    def formfield_for_foreignkey(self, db_field, request, **kwargs):
        if request.employee.is_superuser:
            return super(FrontEndAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)

        else:
            if hasattr(db_field.rel.to, 'user'):
                kwargs["queryset"] = db_field.rel.to.objects.filter(user=request.user)
            if hasattr(db_field.rel.to, 'porcupine'):
                kwargs["queryset"] = db_field.rel.to.objects.filter(porcupine=request.user.porcupine)
            return super(ModelAdminFront, self).formfield_for_foreignkey(db_field, request, **kwargs)
neil.millikin
fonte
1
E devo acrescentar que isso funciona bem como um formulário personalizado genérico para vários administradores de modelos com campos de referência semelhantes de interesse.
Nemesisfixx
Esta é a melhor resposta se você estiver usando Django 1.4+
Rick Westera
16

Para fazer isso com uma exibição genérica, como CreateView ...

class AddPhotoToProject(CreateView):
    """
    a view where a user can associate a photo with a project
    """
    model = Connection
    form_class = CreateConnectionForm


    def get_context_data(self, **kwargs):
        context = super(AddPhotoToProject, self).get_context_data(**kwargs)
        context['photo'] = self.kwargs['pk']
        context['form'].fields['project'].queryset = Project.objects.for_user(self.request.user)
        return context
    def form_valid(self, form):
        pobj = Photo.objects.get(pk=self.kwargs['pk'])
        obj = form.save(commit=False)
        obj.photo = pobj
        obj.save()

        return_json = {'success': True}

        if self.request.is_ajax():

            final_response = json.dumps(return_json)
            return HttpResponse(final_response)

        else:

            messages.success(self.request, 'photo was added to project!')
            return HttpResponseRedirect(reverse('MyPhotos'))

a parte mais importante disso ...

    context['form'].fields['project'].queryset = Project.objects.for_user(self.request.user)

, leia meu post aqui

teewuane
fonte
4

Se você não criou o formulário e deseja alterar o conjunto de consultas, pode fazer:

formmodel.base_fields['myfield'].queryset = MyModel.objects.filter(...)

Isso é bastante útil quando você está usando visualizações genéricas!

Hassek
fonte
2

Então, eu realmente tentei entender isso, mas parece que o Django ainda não torna isso muito simples. Não sou tão burra assim, mas simplesmente não consigo ver nenhuma solução (um tanto) simples.

Acho geralmente muito feio ter que substituir as visualizações de administrador por esse tipo de coisa, e todos os exemplos que eu acho nunca se aplicam totalmente às visualizações de administrador.

Essa é uma circunstância tão comum nos modelos que eu acho que é terrível que não haja uma solução óbvia para isso ...

Eu tenho essas classes:

# models.py
class Company(models.Model):
    # ...
class Contract(models.Model):
    company = models.ForeignKey(Company)
    locations = models.ManyToManyField('Location')
class Location(models.Model):
    company = models.ForeignKey(Company)

Isso cria um problema ao configurar o Admin for Company, porque ele possui linhas para Contrato e Local, e as opções m2m do Contrato para Local não são filtradas corretamente de acordo com a Empresa que você está editando no momento.

Em resumo, eu precisaria de alguma opção de administrador para fazer algo assim:

# admin.py
class LocationInline(admin.TabularInline):
    model = Location
class ContractInline(admin.TabularInline):
    model = Contract
class CompanyAdmin(admin.ModelAdmin):
    inlines = (ContractInline, LocationInline)
    inline_filter = dict(Location__company='self')

Por fim, eu não me importaria se o processo de filtragem fosse colocado no CompanyAdmin base ou se fosse no ContractInline. (Colocá-lo na linha faz mais sentido, mas dificulta a referência ao contrato base como 'próprio'.)

Existe alguém por aí que sabe de algo tão simples quanto esse atalho tão necessário? Quando eu criei administradores PHP para esse tipo de coisa, isso era considerado funcionalidade básica! Na verdade, era sempre automático e precisava ser desativado se você realmente não o quisesse!

Tim
fonte
0

Uma maneira mais pública é chamar get_form nas classes Admin. Também funciona para campos que não são do banco de dados. Por exemplo, aqui eu tenho um campo chamado '_terminal_list' no formulário que pode ser usado em casos especiais para escolher vários itens de terminal de get_list (request) e filtrar com base em request.user:

class ChangeKeyValueForm(forms.ModelForm):  
    _terminal_list = forms.ModelMultipleChoiceField( 
queryset=Terminal.objects.all() )

    class Meta:
        model = ChangeKeyValue
        fields = ['_terminal_list', 'param_path', 'param_value', 'scheduled_time',  ] 

class ChangeKeyValueAdmin(admin.ModelAdmin):
    form = ChangeKeyValueForm
    list_display = ('terminal','task_list', 'plugin','last_update_time')
    list_per_page =16

    def get_form(self, request, obj = None, **kwargs):
        form = super(ChangeKeyValueAdmin, self).get_form(request, **kwargs)
        qs, filterargs = Terminal.get_list(request)
        form.base_fields['_terminal_list'].queryset = qs
        return form
F.Tamy
fonte