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

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 varias Companies , cada una con un rango de Rates y Clients . Cada Client debe tener una Rate base que se elija entre Company's Rates matriz, no Company's Rates otra Company's Rates .

Al crear un formulario para agregar un Client , me gustaría eliminar las opciones de la Company (ya que eso ya se seleccionó a través del botón “Agregar Cliente” en la página de la Company ) y limitar las opciones de Rate a esa Company también.

¿Cómo hago para esto en Django 1.0?

Mi archivo de forms.py actual es solo boilerplate en este momento:

 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 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_to parece prometedor pero no sé cómo pasar en the_company.id y no estoy claro si funcionará fuera de la interfaz de administración de todos modos.

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

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

Por lo tanto, proporcione un QuerySet al atributo queryset del campo. Depende de cómo se construye tu formulario. Si creas un formulario explícito, tendrás 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 alrededor.

Además de la respuesta de S.Lott y como se convirtió en gurú mencionado en los comentarios, es posible agregar los filtros queryset 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 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 su reutilización, por ejemplo, si tiene filtros comunes necesarios 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, simplemente estoy replanteando el material del blog de Django del que hay muchos buenos por ahí.

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 que especifique esto en una clase de formulario, pero puede hacerlo directamente en ModelAdmin, ya que Django ya incluye este método incorporado en ModelAdmin (de la documentación):

 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 inteligente de hacerlo (por ejemplo, al crear una interfaz de administración de 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 SOLAMENTE les muestra contenido relacionado con ellos, mientras que le permite a usted (un superusuario) verlo 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 una ilustración).

La última anulación filtra cualquier campo de clave foránea en el modelo para filtrar las 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 hemos hablado 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 los 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 

impide 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 llave extranjera 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) 

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 aqui

Si no ha creado el formulario y desea cambiar el queryset, puede hacerlo:

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

¡Esto es bastante útil cuando estás usando vistas genéricas!

Entonces, realmente 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.

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 completamente 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 compañía, porque tiene líneas tanto para el Contrato como para la Ubicación, y las opciones de m2m del Contrato para la ubicación no se filtran correctamente de acuerdo con la Compañía 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 el CompanyAdmin base, o si se colocó en el ContractInline. (Colocarlo en la línea tiene más sentido, pero hace que sea difícil hacer referencia al Contrato base como ‘yo’).

¿Hay alguien por ahí que sepa algo tan sencillo como este atajo tan necesario? Cuando hice 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 deshabilitado si realmente no lo quería.

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 también. Por ejemplo, aquí tengo un campo llamado ‘_terminal_list’ en el formulario que se puede usar en casos especiales para elegir varios elementos terminales de get_list (solicitud), y luego filtrar según la solicitud.usuario:

 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