Limite las opciones de clave externa en select en una forma en línea en admin

La lógica es del modelo es:

  • Un Building tiene muchas Rooms
  • Una Room puede estar dentro de otra Room (un armario, por ejemplo, ForeignKey en “sí mismo”)
  • Una Room solo puede estar dentro de otra Room en el mismo edificio (esta es la parte difícil)

Aquí está el código que tengo:

 #spaces/models.py from django.db import models class Building(models.Model): name=models.CharField(max_length=32) def __unicode__(self): return self.name class Room(models.Model): number=models.CharField(max_length=8) building=models.ForeignKey(Building) inside_room=models.ForeignKey('self',blank=True,null=True) def __unicode__(self): return self.number 

y:

 #spaces/admin.py from ex.spaces.models import Building, Room from django.contrib import admin class RoomAdmin(admin.ModelAdmin): pass class RoomInline(admin.TabularInline): model = Room extra = 2 class BuildingAdmin(admin.ModelAdmin): inlines=[RoomInline] admin.site.register(Building, BuildingAdmin) admin.site.register(Room) 

El en línea mostrará solo las habitaciones en el edificio actual (que es lo que quiero). El problema, sin embargo, es que para el inside_room desplegable inside_room , muestra todas las salas en la tabla de Salas (incluidas las de otros edificios).

En la línea de las rooms , debo limitar las opciones de inside_room a solo las rooms que están en el building actual (el registro de construcción actualmente está siendo modificado por el formulario principal de BuildingAdmin ).

No puedo encontrar una manera de hacerlo con un limit_choices_to en el modelo, ni puedo encontrar la manera de sobrescribir correctamente el formset en línea del administrador (creo que de alguna manera debería crear una forma en línea personalizada, pasar el building_id del formulario principal a la línea personalizada, luego limite el conjunto de preguntas para las elecciones del campo en función de eso, pero no puedo entender cómo hacerlo).

Tal vez esto es demasiado complejo para el sitio de administración, pero parece ser algo que en general sería útil …

Instancia de solicitud utilizada como contenedor temporal para obj. Método en línea invalidado formfield_for_foreignkey para modificar queryset. Esto funciona al menos en django 1.2.3.

 class RoomInline(admin.TabularInline): model = Room def formfield_for_foreignkey(self, db_field, request=None, **kwargs): field = super(RoomInline, self).formfield_for_foreignkey(db_field, request, **kwargs) if db_field.name == 'inside_room': if request._obj_ is not None: field.queryset = field.queryset.filter(building__exact = request._obj_) else: field.queryset = field.queryset.none() return field class BuildingAdmin(admin.ModelAdmin): inlines = (RoomInline,) def get_form(self, request, obj=None, **kwargs): # just save obj reference for future processing in Inline request._obj_ = obj return super(BuildingAdmin, self).get_form(request, obj, **kwargs) 

Después de leer esta publicación y experimentar mucho, creo que encontré una respuesta bastante definitiva a esta pregunta. Como este es un patrón de diseño que se utiliza con frecuencia, he escrito un Mixin para que el administrador de Django lo use.

(Dinámicamente) limitar el queryset para campos ForeignKey ahora es tan simple como LimitedAdminMixin subclases de LimitedAdminMixin y definir un get_filters(obj) para devolver los filtros relevantes. Alternativamente, se puede establecer una propiedad de filters en el administrador si no se requiere el filtrado dynamic.

Ejemplo de uso:

 class MyInline(LimitedAdminInlineMixin, admin.TabularInline): def get_filters(self, obj): return (('', dict()),) 

Aquí, es el nombre del campo FK que debe filtrarse y es una lista de parámetros, como normalmente los especificaría en el método filter() de querysets.

Existe la opción limit_choices_to ForeignKey que permite limitar las opciones de administración disponibles para el objeto

Puede crear un par de clases personalizadas que luego pasarán una referencia a la instancia principal al formulario.

 from django.forms.models import BaseInlineFormSet from django.forms import ModelForm class ParentInstInlineFormSet(BaseInlineFormSet): def _construct_forms(self): # instantiate all the forms and put them in self.forms self.forms = [] for i in xrange(self.total_form_count()): self.forms.append(self._construct_form(i, parent_instance=self.instance)) def _get_empty_form(self, **kwargs): return super(ParentInstInlineFormSet, self)._get_empty_form(parent_instance=self.instance) empty_form = property(_get_empty_form) class ParentInlineModelForm(ModelForm): def __init__(self, *args, **kwargs): self.parent_instance = kwargs.pop('parent_instance', None) super(ParentInlineModelForm, self).__init__(*args, **kwargs) 

en la clase RoomInline acaba de agregar:

 class RoomInline(admin.TabularInline): formset = ParentInstInlineFormset form = RoomInlineForm #(or something) 

En su forma, ahora tiene acceso en el método init a self.parent_instance! parent_instance ahora se puede usar para filtrar elecciones y otras cosas

algo como:

 class RoomInlineForm(ParentInlineModelForm): def __init__(self, *args, **kwargs): super(RoomInlineForm, self).__init__(*args, **kwargs) building = self.parent_instance #Filtering and stuff 

Esta pregunta y respuesta es muy similar, y funciona para un formulario de administrador regular

Dentro de una línea – y ahí es donde se cae a pedazos … simplemente no puedo obtener los datos de la forma principal para obtener el valor de clave foránea que necesito en mi límite (o en uno de los registros en línea para obtener el valor) .

Aquí está mi admin.py Supongo que estoy buscando la magia para reemplazar el ???? con – si enchufo un valor codificado (digamos, 1), funciona bien y limita adecuadamente las opciones disponibles en la línea …

 #spaces/admin.py from demo.spaces.models import Building, Room from django.contrib import admin from django.forms import ModelForm class RoomInlineForm(ModelForm): def __init__(self, *args, **kwargs): super(RoomInlineForm, self).__init__(*args, **kwargs) self.fields['inside_room'].queryset = Room.objects.filter( building__exact=????) # <------ class RoomInline(admin.TabularInline): form = RoomInlineForm model=Room class BuildingAdmin(admin.ModelAdmin): inlines=[RoomInline] admin.site.register(Building, BuildingAdmin) admin.site.register(Room) 

Encontré una solución bastante elegante que funciona bien para formularios en línea.

Aplicado a mi modelo, donde estoy filtrando el campo inside_room para solo devolver las habitaciones que están en el mismo edificio:

 #spaces/admin.py class RoomInlineForm(ModelForm): def __init__(self, *args, **kwargs): super(RoomInlineForm, self).__init__(*args, **kwargs) #On init... if 'instance' in kwargs: building = kwargs['instance'].building else: building_id = tuple(i[0] for i in self.fields['building'].widget.choices)[1] building = Building.objects.get(id=building_id) self.fields['inside_room'].queryset = Room.objects.filter(building__exact=building) 

Básicamente, si se pasa una palabra clave ‘instancia’ al formulario, se trata de un registro existente que se muestra en línea, por lo que puedo tomar el edificio de la instancia. Si no es una instancia, es una de las filas “extra” en blanco en la línea, y por lo tanto pasa por los campos de formulario ocultos de la línea que almacena la relación implícita de vuelta a la página principal, y toma el valor de identificación de eso. Luego, toma el objeto de construcción basado en ese building_id. Finalmente, ahora que tenemos el edificio, podemos establecer el conjunto de consulta de los menús desplegables para que solo muestren los elementos relevantes.

Más elegante que mi solución original, que se colgó y se quemó como en línea (pero funcionó, bueno, si no te importa guardar el formulario parcialmente para completar los menús desplegables para los formularios individuales):

 class RoomForm(forms.ModelForm): # For the individual rooms class Meta: mode = Room def __init__(self, *args, **kwargs): # Limits inside_room choices to same building only super(RoomForm, self).__init__(*args, **kwargs) #On init... try: self.fields['inside_room'].queryset = Room.objects.filter( building__exact=self.instance.building) # rooms with the same building as this room except: #and hide this field (why can't I exclude?) self.fields['inside_room']=forms.CharField( #Add room throws DoesNotExist error widget=forms.HiddenInput, required=False, label='Inside Room (save room first)') 

Para no en línea, funcionó si la sala ya existía. Si no, lanzaría un error (DoesNotExist), así que lo atraparía y luego escondería el campo (ya que no había forma, desde el Administrador, de limitarlo al edificio correcto, ya que todo el registro de la sala era nuevo, ¡y aún no se había establecido ningún edificio!) … una vez que tocas guardar, guarda el edificio y al volverlo a cargar podría limitar las opciones …

Solo necesito encontrar una forma de conectar en cascada los filtros de clave externa de un campo a otro en un nuevo registro, es decir, un nuevo registro, seleccionar un edificio y automáticamente se limitan las opciones en el cuadro de selección inside_room, antes de que se obtenga el registro salvado. Pero eso es por otro día …

Si Daniel, después de editar tu pregunta, no ha respondido, no creo que sea de mucha ayuda … 🙂

Voy a sugerir que intentes encajar en el administrador de django una lógica que sería mejor implementar como tu propio grupo de vistas, formularios y plantillas.

No creo que sea posible aplicar ese tipo de filtrado a InlineModelAdmin.

En django 1.6:

  form = SpettacoloForm( instance = spettacolo ) form.fields['teatro'].queryset = Teatro.objects.filter( utente = request.user ).order_by( "nome" ).all() 

Debo admitir que no seguí exactamente lo que intenta hacer, pero creo que es lo suficientemente complejo como para considerar no basar su sitio en el administrador.

Construí un sitio una vez que comenzó con la interfaz de administración simple, pero finalmente llegó a ser tan personalizado que se volvió muy difícil trabajar dentro de las limitaciones del administrador. Hubiera sido mejor si hubiera empezado de cero: más trabajo al principio, pero mucha más flexibilidad y menos dolor al final. Mi regla empírica sería si lo que estás tratando de hacer no está documentado (es decir, implica anular los métodos de administración, mirar el código fuente del administrador, etc.) entonces es mejor que no uses el administrador. Solo yo dos centavos. 🙂

El problema en la respuesta de @nogus todavía hay una URL incorrecta en popup /?_to_field=id&_popup=1

que permiten al usuario seleccionar el elemento incorrecto en emergente

Para finalmente hacerlo funcionar tuve que cambiar field.widget.rel.limit_choices_to dict

 class RoomInline(admin.TabularInline): model = Room def formfield_for_foreignkey(self, db_field, request=None, **kwargs): field = super(RoomInline, self).formfield_for_foreignkey( db_field, request, **kwargs) if db_field.name == 'inside_room': building = request._obj_ if building is not None: field.queryset = field.queryset.filter( building__exact=building) # widget changed to filter by building field.widget.rel.limit_choices_to = {'building_id': building.id} else: field.queryset = field.queryset.none() return field class BuildingAdmin(admin.ModelAdmin): inlines = (RoomInline,) def get_form(self, request, obj=None, **kwargs): # just save obj reference for future processing in Inline request._obj_ = obj return super(BuildingAdmin, self).get_form(request, obj, **kwargs) 
    Intereting Posts