web-dev-qa-db-ja.com

adminのインライン形式のselectで外部キーの選択を制限する

モデルのロジックは次のとおりです。

  • Buildingには多くのRoomsがあります
  • Roomは、別のRoom(たとえば、クローゼット-ForeignKey on 'self')の中にある場合があります
  • Roomは、同じ建物内の別のRoom内にのみ配置できます(これは注意が必要な部分です)

ここに私が持っているコードがあります:

#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

そして:

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

インラインには、現在の建物内の部屋のみが表示されます(これが必要です)。ただし、問題はinside_roomドロップダウンの場合、Roomsテーブルのすべての部屋(他の建物の部屋を含む)が表示されることです。

roomsのインラインで、inside_roomの選択肢を、現在のrooms(現在変更されているビルディングレコードにあるbuildingのみに制限する必要があります。メインBuildingAdminフォーム)。

モデル内のlimit_choices_toでそれを行う方法がわからず、管理者のインラインフォームセットを適切にオーバーライドする方法を正確に把握することもできません(何らかの形でカスタムインラインフォームを作成する必要があるように感じます、メインフォームのbuilding_idをカスタムインラインに渡し、それに基づいてフィールドの選択肢のクエリセットを制限します。

これは管理サイトにとっては複雑すぎるかもしれませんが、一般的に有用なもののようです...

68
mightyhal

Objの一時コンテナとしてリクエストインスタンスを使用しました。クエリセットを変更するためのインラインメソッドformfield_for_foreignkeyをオーバーライドしました。これは、少なくとも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)
96
nogus

この投稿を読んで多くのことを試した後、この質問に対するかなり決定的な答えを見つけたと思います。これはよく使用されるデザインパターンなので、 Django admin の混合を使用して)を作成しました。

(動的)ForeignKeyフィールドのクエリセットの制限は、LimitedAdminMixinをサブクラス化し、get_filters(obj)メソッドを定義して関連するフィルターを返すのと同じくらい簡単になりました。または、動的フィルタリングが不要な場合は、filtersプロパティを管理者に設定できます。

使用例:

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

ここで、_<field_name>_はフィルタリングされるFKフィールドの名前であり、_<filters>_はクエリセットのfilter()メソッドで通常指定するパラメーターのリストです。

17
Mathijs

limit_choices_to ForeignKeyオプションがあり、オブジェクトの利用可能な管理者の選択肢を制限できます。

14
user1022684

いくつかのカスタムクラスを作成して、親インスタンスへの参照をフォームに渡すことができます。

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)

クラスRoomInlineに次を追加します。

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

フォームでは、initメソッドでself.parent_instanceにアクセスできます! parent_instanceを使用して、選択項目などをフィルタリングできるようになりました

何かのようなもの:

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

この質問と回答は非常に似ており、通常の管理フォームで機能します

インラインの内部-そして、それはバラバラになります...私は、メインフォームのデータにアクセスして、制限内で(または値を取得するためにインラインのレコードの1つに)必要な外部キー値を取得できません。 。

これが私のadmin.pyです。私は????を置き換える魔法を探していると思いますwith--ハードコードされた値(たとえば、1)をプラグインすると、正常に機能し、インラインで使用可能な選択肢が適切に制限されます...

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

かなりエレガントなソリューション が見つかりました。これはインラインフォームに適しています。

同じ建物内にある部屋のみを返すようにinside_roomフィールドをフィルタリングするモデルに適用されます。

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

基本的に、「インスタンス」キーワードがフォームに渡されると、インラインに表示される既存のレコードであるため、インスタンスから建物を取得するだけです。インスタンスではない場合、それはインラインの空の「余分な」行の1つであるため、暗黙的なリレーションをメインページに保存するインラインの非表示フォームフィールドを通過し、そこからid値を取得します。次に、そのbuilding_idに基づいて建物オブジェクトを取得します。最後に、建物ができたので、関連するアイテムのみを表示するようにドロップダウンのクエリセットを設定できます。

クラッシュしてインラインとして焼かれた元のソリューションよりもエレガントです(ただし、個々のフォームでドロップダウンを埋めるために途中でフォームを保存してもかまいません)。

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

非インラインの場合、部屋がすでに存在していれば機能していました。そうでない場合、エラー(DoesNotExist)がスローされるので、キャッチしてフィールドを非表示にします(管理者からは、部屋のレコード全体が新しいため、正しい建物に制限する方法がなかったため、建物はまだ設定されていません!)...保存を押すと、建物が保存され、リロードすると選択肢が制限される可能性があります...

新しいレコードの1つのフィールドから別のフィールドに外部キーフィルターをカスケードする方法を見つける必要があります(つまり、新しいレコード、建物を選択すると、レコードが取得される前に、inside_room選択ボックスの選択肢が自動的に制限されます)保存しました。しかし、それは別の日です...

4
mightyhal

@nogus回答の問題は、ポップアップ/?_to_field=id&_popup=1

ユーザーがポップアップで間違ったアイテムを選択できるようにする

最終的に機能させるには、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)
4
Daniil Mashkin

あなたの質問を編集した後、ダニエルが答えていない場合-私は私が多くの助けになるとは思わない... :-)

Django admin独自のビュー、フォーム、テンプレートのグループとして実装したほうがよいロジックを管理するように強制することをお勧めします。

そのようなフィルタリングをInlineModelAdminに適用することは不可能だと思います。

2
cethegeek

In Django 1.6:

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

私はあなたがやろうとしていることを正確には守らなかったと認めざるを得ませんが、あなたのサイトを管理者から切り離さないことを検討したいほど複雑だと思います。

一度シンプルな管理インターフェースから始めたサイトを構築しましたが、最終的には非常にカスタマイズされたため、管理者の制約内で作業することは非常に困難になりました。ゼロから始めたばかりであれば、最初はより多くの作業が必要でしたが、柔軟性が増し、最後に苦痛は減りました。私の経験則は、あなたがやろうとしていることが文書化されていない場合(つまり、管理メソッドのオーバーライド、管理ソースコードの覗き込みなど)、管理者を使用しない方が良いでしょう。ちょうど私は2セント。 :)

1
user27478