Beschränken Sie die Auswahl von Fremdschlüsseln in select in einem Inline-Formular in admin

75

Die Logik des Modells ist:

  • A Buildinghat vieleRooms
  • A Roomkann sich in einem anderen befinden Room(zum Beispiel in einem Schrank - ForeignKey on 'self')
  • A Roomkann nur in einem anderen Roomim selben Gebäude sein (dies ist der schwierige Teil)

Hier ist der Code, den ich habe:

#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

und:

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

In der Inline werden nur Räume im aktuellen Gebäude angezeigt (was ich möchte). Das Problem ist jedoch, dass für das inside_roomDropdown-Menü alle Räume in der Zimmertabelle angezeigt werden (einschließlich der Räume in anderen Gebäuden).

In der Zeile von roomsmuss ich die inside_roomAuswahl auf nur roomsdie beschränken, die im aktuellen Formular enthalten sind building(der Gebäudedatensatz wird derzeit durch das Hauptformular geändert BuildingAdmin).

Ich kann weder mit a limit_choices_toim Modell einen Weg finden , noch kann ich herausfinden, wie genau das Inline-Formset des Administrators richtig überschrieben werden soll (ich denke, ich sollte irgendwie ein benutzerdefiniertes Inline-Formular erstellen und die building_id von übergeben das Hauptformular auf die benutzerdefinierte Inline, dann beschränken Sie das Abfrageset für die Auswahl des Feldes basierend darauf - aber ich kann mich einfach nicht darum kümmern, wie es geht).

Vielleicht ist dies zu komplex für die Admin-Site, aber es scheint etwas zu sein, das allgemein nützlich wäre ...

mächtig
quelle

Antworten:

101

Verwendete Anforderungsinstanz als temporärer Container für obj. Überschriebene Inline-Methode formfield_for_foreignkey zum Ändern des Abfragesatzes. Dies funktioniert zumindest auf 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)
Nogus
quelle
1
Das hier hat mir viel Ärger erspart. Ich musste die Auswahl filtern, aber nach einer Sitzungsvariablen. Diese Antwort lässt mich mit 5 Codezeilen machen. Vielen Dank.
Peter G
3
Tausend Dank! Eine Alternative besteht darin, kwargs ['queryset'] zuzuweisen, bevor super gemäß docs aufgerufen wird
powlo
Dieser Code hat mir auch Tonnen von Zeit gespart. Vielen Dank für die Veröffentlichung dieses
fangsterr
DIES! Ich habe nach so etwas für mein Problem gesucht. Ich habe Tage gebraucht, um das zu finden.
Miguel Ike
1
Der Benutzer kann jedoch Roomim Popup immer noch eine falsche Auswahl treffen. Siehe stackoverflow.com/a/50298577/2207154 für die Lösung
Daniil Mashkin
17

Nachdem ich diesen Beitrag gelesen und viel experimentiert habe, denke ich, dass ich eine ziemlich endgültige Antwort auf diese Frage gefunden habe. Da dies ein Designmuster ist, das oft verwendet wird, habe ich ein Mixin für den Django-Administrator geschrieben , um es zu verwenden.

Das (dynamische) Einschränken des Abfragesatzes für ForeignKey-Felder ist jetzt so einfach wie das Unterklassifizieren LimitedAdminMixinund Definieren einer get_filters(obj)Methode zum Zurückgeben der relevanten Filter. Alternativ kann eine filtersEigenschaft für den Administrator festgelegt werden, wenn keine dynamische Filterung erforderlich ist.

Anwendungsbeispiel:

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

Hier <field_name>ist der Name des zu filternden FK-Feldes und <filters>eine Liste von Parametern, wie Sie sie normalerweise in der filter()Methode von Abfragesätzen angeben würden.

Mathijs
quelle
1
Danke, funktioniert super! Viel sauberer. (Und übrigens, Sie haben einige Protokollierungsanweisungen in Ihrem Code hinterlassen, die nirgendwo hingehen)
Dave
17

Es gibt die Option limit_choices_to ForeignKey, mit der die verfügbaren Administratoroptionen für das Objekt eingeschränkt werden können

user1022684
quelle
2
Dies hilft nicht, da die Abfrage, die in limit_choices_to ausgeführt wird, keinen Verweis auf die "übergeordnete Klasse" hat. Das heißt, wenn ein Modell A einen Fremdschlüssel für B und auch für C hat und C einen Fremdschlüssel für B hat und wir sicherstellen möchten, dass sich A nur auf ein C bezieht, das sich auf dasselbe B wie A bezieht muss die Abfrage über A-> B Bescheid wissen, was nicht der Fall ist.
Chris Cogdon
1
Es kann mit Top-Antwort-Kombination nützlich sein, siehe stackoverflow.com/a/50298577/2207154
Daniil Mashkin
8

Sie können einige benutzerdefinierte Klassen erstellen, die dann einen Verweis auf die übergeordnete Instanz an das Formular weiterleiten.

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)

in der Klasse RoomInline einfach hinzufügen:

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

In Ihrem Formular haben Sie jetzt in der init-Methode Zugriff auf self.parent_instance! parent_instance kann jetzt verwendet werden, um Auswahlmöglichkeiten und so weiter zu filtern

etwas wie:

class RoomInlineForm(ParentInlineModelForm):
    def __init__(self, *args, **kwargs):
        super(RoomInlineForm, self).__init__(*args, **kwargs)
        building = self.parent_instance
        #Filtering and stuff
alav
quelle
Danke dafür! Es ist die erste Version, die für meine Anwendung funktioniert hat und es ist auch schön und klar.
Justin
8

Das Problem in @nogus Antwort gibt es immer noch falsche URL im Popup /?_to_field=id&_popup=1

die es dem Benutzer ermöglichen, ein falsches Element im Popup auszuwählen

Damit es endlich funktioniert, musste ich das field.widget.rel.limit_choices_toDiktat ändern

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)
Daniil Mashkin
quelle
Dies funktionierte für mich in Django 2.2, ohne es verwenden zu müssenfield.widget.rel.limit_choices_to = {'building_id': building.id}
Twitch
4

Diese Frage und Antwort ist sehr ähnlich und funktioniert für ein reguläres Administrationsformular

Innerhalb einer Inline - und dort fällt es auseinander ... Ich kann einfach nicht auf die Daten des Hauptformulars zugreifen, um den Fremdschlüsselwert zu erhalten, den ich in meinem Limit benötige (oder auf einen der Inline-Datensätze, um den Wert zu ermitteln). .

Hier ist meine admin.py. Ich schätze, ich suche die Magie, um die ???? mit - wenn ich einen fest codierten Wert (z. B. 1) einstecke, funktioniert dies einwandfrei und schränkt die verfügbaren Auswahlmöglichkeiten in der Inline ordnungsgemäß ein ...

#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)
mächtig
quelle
4

Ich habe eine ziemlich elegante Lösung gefunden , die sich gut für Inline-Formulare eignet.

Auf mein Modell angewendet, bei dem ich das Feld inside_room filtere, um nur Räume zurückzugeben, die sich im selben Gebäude befinden:

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

Wenn ein 'Instanz'-Schlüsselwort an das Formular übergeben wird, handelt es sich im Grunde genommen um einen vorhandenen Datensatz, der in der Inline angezeigt wird, sodass ich das Gebäude einfach aus der Instanz abrufen kann. Wenn es sich nicht um eine Instanz handelt, handelt es sich um eine der leeren "zusätzlichen" Zeilen in der Inline. Daher werden die ausgeblendeten Formularfelder der Inline durchlaufen, in denen die implizite Beziehung zur Hauptseite gespeichert ist, und der ID-Wert wird daraus abgerufen. Dann wird das Gebäudeobjekt basierend auf dieser building_id erfasst. Nachdem wir nun das Gebäude haben, können wir den Abfragesatz der Dropdowns so einstellen, dass nur die relevanten Elemente angezeigt werden.

Eleganter als meine ursprüngliche Lösung, die als Inline abgestürzt und gebrannt hat (aber funktioniert hat - nun, wenn es Ihnen nichts ausmacht, das Formular teilweise zu speichern, damit die Dropdowns ausgefüllt werden - für die einzelnen Formulare):

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

Bei Nicht-Inlines funktionierte es, wenn der Raum bereits vorhanden war. Wenn nicht, würde es einen Fehler auslösen (DoesNotExist), also würde ich ihn abfangen und dann das Feld ausblenden (da es vom Administrator keine Möglichkeit gab, es auf das richtige Gebäude zu beschränken, da der gesamte Raumdatensatz neu war). und es wurde noch kein Gebäude festgelegt!) ... Sobald Sie auf Speichern klicken, wird das Gebäude gespeichert und beim erneuten Laden können die Auswahlmöglichkeiten eingeschränkt werden ...

Ich muss nur einen Weg finden, um die Fremdschlüsselfilter in einem neuen Datensatz von einem Feld in ein anderes zu kaskadieren - dh einen neuen Datensatz, ein Gebäude auswählen und die Auswahlmöglichkeiten im Auswahlfeld inside_room automatisch einschränken -, bevor der Datensatz abgerufen wird Gerettet. Aber das ist für einen anderen Tag ...

mächtig
quelle
2

Wenn Daniel nach der Bearbeitung Ihrer Frage nicht geantwortet hat - ich glaube nicht, dass ich viel helfen werde ... :-)

Ich werde vorschlagen, dass Sie versuchen, eine Anpassung in den Django-Administrator zu erzwingen, die besser als Ihre eigene Gruppe von Ansichten, Formularen und Vorlagen implementiert werden kann.

Ich denke nicht, dass es möglich ist, diese Art der Filterung auf InlineModelAdmin anzuwenden.

Cethegeek
quelle
2

In Django 1.6:

 form = SpettacoloForm( instance = spettacolo )
 form.fields['teatro'].queryset = Teatro.objects.filter( utente = request.user ).order_by( "nome" ).all()
max4ever
quelle
1
Könnten Sie bitte die Lösung an die in der Frage vorhandenen Modelle anpassen?
Raratiru
1

Ich muss zugeben, ich habe nicht genau verfolgt, was Sie versuchen, aber ich denke, es ist komplex genug, dass Sie in Betracht ziehen sollten, Ihre Website nicht auf den Administrator zu stützen.

Ich habe einmal eine Site erstellt, die mit der einfachen Administrationsoberfläche begann, aber schließlich so angepasst wurde, dass es sehr schwierig wurde, mit den Einschränkungen des Administrators zu arbeiten. Ich wäre besser dran gewesen, wenn ich gerade von vorne angefangen hätte - mehr Arbeit am Anfang, aber viel mehr Flexibilität und weniger Schmerzen am Ende. Meine Faustregel wäre, wenn das, was Sie versuchen, nicht dokumentiert ist (dh das Überschreiben von Admin-Methoden, das Durchsuchen des Admin-Quellcodes usw.), ist es wahrscheinlich besser, den Admin nicht zu verwenden. Nur ich zwei Cent. :) :)

gerdemb
quelle