Django ModelFormでForeignKeyの選択肢をフィルタリングするにはどうすればよいですか?


227

私の中に次のものがあったとしましょう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)

つまり、複数のがありCompanies、それぞれにとの範囲がRatesありClientsます。それぞれのClientベース持っている必要がありRate、それの親から選択されるCompany's Rates、ではない別のものをCompany's Rates

を追加するためのフォームを作成するときにClientCompany選択肢を削除し(Companyページの[クライアントの追加]ボタンで既に選択されているため)、Rate選択肢もそれに限定Companyします。

Django 1.0でこれをどうやって行うのですか?

私の現在のforms.pyファイルは現時点では定型です:

from models import *
from django.forms import ModelForm

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

そして、これviews.pyも基本です:

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

Django 0.96では、テンプレートをレンダリングする前に次のようなことを行うことでこれをハッキングすることができました。

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

ForeignKey.limit_choices_to有望なようですが、渡す方法がわからないのでthe_company.id、それが管理インターフェイスの外でも機能するかどうかはわかりません。

ありがとう。(これはかなり基本的なリクエストのようですが、何かを再設計する必要がある場合は、提案を受け入れます。)


「limit_choices_to」へのヒントをありがとうございます。それはあなたの質問を解決するのではなく、鉱山:-)ドキュメント:docs.djangoproject.com/en/dev/ref/models/fields/...
guettli

回答:


243

ForeignKeyはdjango.forms.ModelChoiceFieldで表されます。これは、選択肢がモデルクエリセットであるChoiceFieldです。ModelChoiceFieldのリファレンスを参照してください

したがって、フィールドのqueryset属性にQuerySetを提供します。フォームの作成方法によって異なります。明示的なフォームを作成すると、フィールドに直接名前が付けられます。

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

デフォルトのModelFormオブジェクトを使用すると、 form.fields["rate"].queryset = ...

これは、ビューで明示的に行われます。ハッキングなし。


それは有望に聞こえます。関連するFieldオブジェクトにアクセスするにはどうすればよいですか?form.company.QuerySet = Rate.objects.filter(company_id = the_company.id)?または辞書を介して?
トム

1
わかりました。例を拡張していただきありがとうございます。「ClientForm」オブジェクトには属性「rate」がないため、form.fields ["rate"]。querysetを使用する必要があるようです。何か不足していますか?(そして、あなたの例もform.rate.querysetである必要があります。)
Tom

8
フォームの__init__メソッドで、フィールドのクエリセットを設定した方がいいのではないでしょうか。
ラクシュマンプラサード

1
@SLott最後のコメントが正しくない(または私のサイトが機能していないはずです:)。オーバーライドされたメソッドでsuper(...).__ init__呼び出しを使用して検証データを入力できます。これらのクエリセットをいくつか変更する場合は、initメソッドをオーバーライドしてパッケージ化する方がはるかに洗練されています。
マイケル

3
@Slottの乾杯、説明に600文字以上かかるので、回答を追加しました。この質問が古くても、Googleのスコアが高くなっています。
マイケル

135

S.Lottの回答に加えて、コメントでGuruになると、ModelForm.__init__関数をオーバーライドしてクエリセットフィルターを追加することができます。(これは通常のフォームに簡単に適用できます)再利用に役立ち、ビュー機能を整頓します。

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

これは、多くのモデルで必要な共通フィルターがある場合(通常、私は抽象Formクラスを宣言する)の再利用に役立ちます。例えば

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

それ以外は、Djangoのブログの資料を再掲しています。


最初のコードスニペットにタイプミスがあります。__init __()でargsとkwargsの代わりにargsを2回定義しています。
tpk

6
この答えの方が好きです。フォームの初期化ロジックを、viewメソッドではなくフォームクラスにカプセル化する方がクリーンだと思います。乾杯!
対称

44

これは簡単で、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
    ....

これはフォームクラスで指定する必要はありませんが、ModelAdminで直接指定できます。Djangoには、ModelAdminのこの組み込みメソッドがすでに含まれています(ドキュメントから)。

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)

これを行うためのさらに洗練された方法(たとえば、ユーザーがアクセスできるフロントエンド管理インターフェースを作成する場合)は、ModelAdminをサブクラス化し、以下のメソッドを変更することです。最終的な結果は、ユーザー(スーパーユーザー)がすべてを表示できるようにしながら、それらに関連するコンテンツのみを表示するユーザーインターフェイスです。

私は4つのメソッドをオーバーライドしました。最初の2つは、ユーザーが何かを削除することを不可能にし、管理サイトから削除ボタンも削除します。

3番目のオーバーライドは、(例として「user」または「porcupine」の例で)への参照を含むクエリをフィルタリングします。

最後のオーバーライドは、モデル内のすべてのforeignkeyフィールドをフィルタリングして、基本的なクエリセットと同じように使用可能な選択肢をフィルタリングします。

このようにして、ユーザーが自分のオブジェクトをいじることを可能にする、管理しやすい前面の管理サイトを提示できます。また、上記で説明した特定のModelAdminフィルターを入力することを覚える必要はありません。

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)

「削除」ボタンを削除します。

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

削除権限を禁止します

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

管理サイトで表示できるオブジェクトをフィルタリングします。

    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

管理サイトのすべてのforeignkeyフィールドの選択肢をフィルタリングします。

    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
これは、同様の参照フィールドを持つ複数のモデル管理者向けの汎用カスタムフォームとしても機能することを付け加えておきます。
nemesisfixx 2013

Django 1.4以降を使用している場合、これが最良の答えです
Rick

16

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

その中で最も重要な部分...

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

私の投稿をここ読んでください


4

フォームをまだ作成しておらず、クエリセットを変更したい場合は、次のようにします。

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

これは、一般的なビューを使用している場合に非常に便利です。


2

だから、私はこれを本当に理解しようとしましたが、Djangoはまだこれを非常に簡単にしていないようです。私はそれほど馬鹿ではありませんが、(少し)単純な解決策を見ることができません。

この種のことを行うために管理者ビューを上書きする必要があることは、一般的にかなり醜いことがわかります。

これは私が作成したモデルの非常に一般的な状況であり、これには明らかな解決策がないことは恐ろしいことです...

私はこれらのクラスを持っています:

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

これにより、会社の管理者を設定するときに問題が発生します。これは、契約と場所の両方にインラインがあり、場所の契約のm2mオプションが、現在編集している会社に従って適切にフィルタリングされないためです。

要するに、私はこのようなことをするためにいくつかの管理オプションが必要になるでしょう:

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

最終的には、フィルタリングプロセスがベースのCompanyAdminに配置されているか、ContractInlineに配置されているかは気にしません。(インラインに配置する方が理にかなっていますが、基本コントラクトを「自分」として参照することは困難です。)

このひどく必要なショートカットと同じくらい簡単なことを知っている人はいますか?私がこの種のことのためにPHP管理者を作ったとき、これは基本的な機能と考えられていました!実際、これは常に自動で行われ、本当に必要ない場合は無効にする必要がありました。


0

より一般的な方法は、Adminクラスでget_formを呼び出すことです。データベース以外のフィールドでも機能します。たとえば、ここには、フォームに「_terminal_list」というフィールドがあり、get_list(request)からいくつかの端末アイテムを選択し、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
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.