¿Cómo filtro las opciones de ForeignKey en un Django ModelForm?

227

Digamos que tengo lo siguiente en mi 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)

Es decir, hay múltiples Companies, cada uno con un rango de Ratesy Clients. Cada uno Clientdebe tener una base Rateelegida de su padre Company's Rates, no otro Company's Rates.

Al crear un formulario para agregar un Client, me gustaría eliminar las Companyopciones (ya que eso ya se ha seleccionado a través de un botón "Agregar cliente" en la Companypágina) y limitar las Rateopciones a eso Companytambién.

¿Cómo hago esto en Django 1.0?

Mi forms.pyarchivo actual es solo repetitivo en este momento:

from models import *
from django.forms import ModelForm

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

Y el views.pytambién es 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})

En Django 0.96 pude hackear esto haciendo algo como lo siguiente antes de renderizar la plantilla:

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

ForeignKey.limit_choices_toparece prometedor, pero no sé cómo pasar the_company.idy no estoy claro si eso funcionará fuera de la interfaz de administrador de todos modos.

Gracias. (Esto parece una solicitud bastante básica, pero si debo rediseñar algo estoy abierto a sugerencias)

Tom
fuente
Gracias por la sugerencia de "limit_choices_to". No resuelve tu pregunta, pero la mía :-) Documentos: docs.djangoproject.com/en/dev/ref/models/fields/…
guettli

Respuestas:

243

ForeignKey está representada por django.forms.ModelChoiceField, que es un ChoiceField cuyas opciones son un modelo QuerySet. Consulte la referencia de ModelChoiceField .

Por lo tanto, proporcione un QuerySet al querysetatributo del campo . Depende de cómo se construya su formulario. Si crea un formulario explícito, tendrá campos nombrados directamente.

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

Si toma el objeto ModelForm predeterminado, form.fields["rate"].queryset = ...

Esto se hace explícitamente en la vista. No hackear.

S.Lott
fuente
Ok, eso suena prometedor. ¿Cómo accedo al objeto de campo relevante? form.company.QuerySet = Rate.objects.filter (company_id = the_company.id)? o a través de un diccionario?
Tom
1
Ok, gracias por ampliar el ejemplo, pero parece que tengo que usar form.fields ["rate"]. Queryset para evitar que el objeto 'ClientForm' no tenga el atributo 'rate' ", ¿me falta algo? (y su ejemplo debería ser form.rate.queryset para ser coherente también.)
Tom
8
¿No sería mejor establecer el conjunto de consultas de los campos, en el __init__método del formulario ?
Lakshman Prasad
1
@SLott el último comentario no es correcto (o mi sitio no debería funcionar :). Puede completar los datos de validación mediante el uso de la llamada super (...) .__ init__ en su método reemplazado. Si está realizando varios de estos cambios en el conjunto de consultas, es mucho más elegante empaquetarlos anulando el método init .
michael
3
@Slott aplausos, he agregado una respuesta, ya que tomaría más de 600 caracteres para explicar. Incluso si esta pregunta es antigua, está obteniendo una alta puntuación en Google.
michael
135

Además de la respuesta de S.Lott y como se está convirtiendo en Guru mencionado en los comentarios, es posible agregar los filtros del conjunto de consultas anulando la ModelForm.__init__función. (Esto podría aplicarse fácilmente a los formularios normales) puede ayudar con la reutilización y mantiene ordenada la función de visualización.

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})

Esto puede ser útil para reutilizar, por ejemplo, si necesita filtros comunes en muchos modelos (normalmente declaro una clase de formulario abstracto). P.ej

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')

Aparte de eso, solo estoy reafirmando el material del blog de Django del cual hay muchos buenos por ahí.

Miguel
fuente
Hay un error tipográfico en su primer fragmento de código, está definiendo args dos veces en __init __ () en lugar de args y kwargs.
tpk
66
Me gusta más esta respuesta, creo que es más limpio encapsular la lógica de inicialización del formulario en la clase de formulario, en lugar de en el método de vista. ¡Salud!
Simétrico
44

Esto es simple y funciona con 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
    ....

No necesita especificar esto en una clase de formulario, pero puede hacerlo directamente en ModelAdmin, ya que Django ya incluye este método incorporado en ModelAdmin (de los 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)

Una forma aún más ágil de hacer esto (por ejemplo, al crear una interfaz de administración front-end a la que los usuarios pueden acceder) es subclasificar ModelAdmin y luego alterar los métodos a continuación. El resultado neto es una interfaz de usuario que SOLAMENTE les muestra contenido relacionado con ellos, mientras le permite a usted (un superusuario) ver todo.

He anulado cuatro métodos, los dos primeros hacen imposible que un usuario elimine algo, y también elimina los botones de eliminación del sitio de administración.

La tercera anulación filtra cualquier consulta que contenga una referencia a (en el ejemplo 'usuario' o 'puercoespín' (solo como ilustración).

La última anulación filtra cualquier campo de clave externa en el modelo para filtrar las opciones disponibles al igual que el conjunto de consultas básico.

De esta manera, puede presentar un sitio de administración frontal fácil de administrar que permita a los usuarios meterse con sus propios objetos, y no tiene que acordarse de escribir los filtros específicos de ModelAdmin que mencionamos anteriormente.

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)

eliminar botones 'borrar':

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

evita el permiso de eliminación

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

filtra los objetos que se pueden ver en el sitio de administración:

    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 las opciones para todos los campos de clave externa en el sitio de administración:

    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
fuente
1
Y debo agregar que esto funciona bien como un formulario personalizado genérico para múltiples modeladmins con campos de interés de referencia similares.
nemesisfixx
Esta es la mejor respuesta si está utilizando Django 1.4+
Rick Westera
16

Para hacer esto con una vista 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'))

la parte más importante de eso ...

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

, lee mi post aquí

Teewuane
fuente
4

Si no ha creado el formulario y desea cambiar el conjunto de consultas, puede hacer lo siguiente:

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

¡Esto es bastante útil cuando usa vistas genéricas!

Hassek
fuente
2

Entonces, realmente he tratado de entender esto, pero parece que Django todavía no lo hace muy sencillo. No soy tan tonto, pero no puedo ver ninguna solución (algo) simple.

En general, me parece bastante feo tener que anular las vistas de administrador para este tipo de cosas, y cada ejemplo que encuentro nunca se aplica por completo a las vistas de administrador.

Esta es una circunstancia tan común con los modelos que hago que me parece terrible que no haya una solución obvia para esto ...

Tengo estas clases:

# 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)

Esto crea un problema al configurar el administrador para la empresa, ya que tiene líneas en línea tanto para el contrato como para la ubicación, y las opciones de m2m del contrato para la ubicación no se filtran adecuadamente según la empresa que está editando actualmente.

En resumen, necesitaría alguna opción de administrador para hacer algo como esto:

# 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')

En última instancia, no me importaría si el proceso de filtrado se colocó en la base CompanyAdmin, o si se colocó en ContractInline. (Colocarlo en línea tiene más sentido, pero hace que sea difícil hacer referencia al Contrato base como 'auto').

¿Hay alguien por ahí que sepa algo tan sencillo como este atajo tan necesario? Cuando creé administradores de PHP para este tipo de cosas, ¡esto se consideraba una funcionalidad básica! De hecho, siempre fue automático, ¡y tenía que ser desactivado si realmente no lo deseaba!

Tim
fuente
0

Una forma más pública es llamando a get_form en las clases de administrador. También funciona para campos que no son de base de datos. Por ejemplo, aquí tengo un campo llamado '_terminal_list' en el formulario que se puede usar en casos especiales para elegir varios elementos de terminal de get_list (request), y luego filtrar según 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
fuente