2008-11-15 14 views
188

decir que tengo el siguiente en mi models.py:¿Cómo puedo filtrar las opciones de ForeignKey en un Django ModelForm?

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 Rates y Clients. Cada Client debe tener una base Rate que se elija de su matriz Company's Rates, no otra Company's Rates.

Al crear un formulario para añadir un Client, me gustaría quitar los Company opciones (como que ya ha sido seleccionado a través de un botón "Añadir cliente" en la página Company) y limitar los Rate opciones para que Company así .

¿Cómo hago esto en Django 1.0?

Mi actual archivo forms.py es repetitivo en la actualidad

from models import * 
from django.forms import ModelForm 

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

Y el views.py tambié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 cortar esto en haciendo algo como lo siguiente antes de representar la plantilla:

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

ForeignKey.limit_choices_to parece prometedor, pero no sé cómo pasar the_company.id y no estoy seguro si eso funcionará fuera de la interfaz de administración de todos modos.

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

Respuesta

200

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

Proporcione un QuerySet al atributo queryset del campo. Depende de cómo se construye tu formulario. Si construye un formulario explícito, tendrá campos nombrados directamente.

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

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

Esto se hace de forma explícita en la vista. Sin hackear.

+0

Ok, que los sonidos prometedor. ¿Cómo accedo al objeto Field 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 evitarlo "El objeto 'ClientForm' no tiene atributo 'rate'", ¿me falta algo? (y su ejemplo debe ser form.rate.queryset para ser coherente también.) – Tom

+0

Excelente, gracias por aclarar. Para referencia futura, puede valer la pena tener en cuenta cuando editas tu respuesta a través de un comentario porque las modificaciones no se muestran en la pestaña de respuestas de mi página de usuario. – Tom

115

Además de la respuesta de S.Lott y como convertirse en Gurú mencionado en los comentarios, es posible agregar los filtros de conjunto de preguntas anulando la función ModelForm.__init__. (Esto podría aplicarse fácilmente a los formularios regulares) puede ayudar con la reutilización y mantiene ordenada la función de vista.

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 su reutilización decir si tiene filtros comunes que se necesitan en muchos modelos (normalmente Declaro una clase de forma de resumen). 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 material del blog Django sólo estoy repitiendo de los cuales hay muchos buenos por ahí.

+0

Hay un error ortográfico en su primer fragmento de código, está definiendo args dos veces en __init __() en lugar de args y kwargs. – tpk

+0

ovaciones, eso se actualizó – michael

+5

Me gusta más esta respuesta, creo que es más limpio para encapsular la lógica de inicialización de formularios en la clase de formulario, en lugar de en el método de vista. ¡Aclamaciones! – Symmetric

2

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

Encuentro generalmente bastante feo tener que anular las vistas de administrador para este tipo de cosas, y cada ejemplo que encuentro nunca se aplica completamente a las vistas de administrador.

Ésta es una circunstancia común con los modelos que hago que me parece terrible que no hay una solución obvia a este ...

que 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 entrantes para el contrato y la ubicación, y las opciones m2m del contrato para la ubicación no se filtran correctamente de acuerdo con la empresa que está editando actualmente.

En resumen, necesitaría alguna opción de administración 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 les importaría si el proceso de filtrado se colocó en el CompanyAdmin base, o si fue colocado en el ContractInline. (Colocarlo en línea tiene más sentido, pero hace que sea difícil hacer referencia al Contrato base como 'uno mismo').

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

3

Si no ha creado la forma y desee cambiar el conjunto de consultas que puede hacer:

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

Esto es muy útil cuando se está utilizando vistas genéricas!

15

Para hacer esto con una visión 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 ese ...

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

, read my post here

37

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 es necesario especificar esto en una clase de formulario, pero puede hacerlo directamente en el ModelAdmin, como Django ya incluye este método integrado en el 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) 

Un Incluso una forma más sencilla de hacer esto (por ejemplo, al crear una interfaz de administrador front-end a la que los usuarios pueden acceder) es crear una subclase de ModelAdmin y luego modificar los métodos a continuación. El resultado neto es una interfaz de usuario que SÓLO muestra el contenido que está relacionado con ellos, mientras le permite a usted (un súper usuario) ver todo.

He reemplazado cuatro métodos, los dos primeros hacen que sea imposible para un usuario eliminar algo, y también quita los botones de eliminar del sitio de administración.

La tercera override filtros Cualquier consulta que contiene una referencia a (en el ejemplo 'usuario' o 'puercoespín' (sólo como una ilustración).

El último override filtra cualquier campo ForeignKey en el modelo para filtrar el opciones disponibles igual que el conjunto de consulta básico.

De esta manera, puede presentar un sitio de administración frontal fácil de administrar que permite a los usuarios jugar con sus propios objetos, y no tiene que recordar escribir los filtros específicos de ModelAdmin de los que hablamos 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) 

remove 'eliminar' botones:

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

impide eliminar el permiso

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

filtros 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 

filtros opciones para todos los campos ForeignKey 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) 
+1

Y debo agregar que esto funciona bien como un formulario genérico personalizado para múltiples modeladmins con campos de referencia de interés similares. – nemesisfixx

+0

Esta es la mejor respuesta si usa Django 1.4+ –

0

Una forma más pública es llamando a get_form en las clases de administración. 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 utilizar en casos especiales para la elección de varios elementos terminales de get_list (petición), entonces el filtrado basado en 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 
Cuestiones relacionadas