Bagaimana cara memfilter pilihan ForeignKey di Django ModelForm?

227

Katakanlah saya memiliki yang berikut ini di 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)

Yaitu ada beberapa Companies, masing-masing memiliki kisaran Ratesdan Clients. Masing Client- masing harus memiliki basis Rateyang dipilih dari orang tuanya Company's Rates, bukan yang lain Company's Rates.

Saat membuat formulir untuk menambahkan Client, saya ingin menghapus Companypilihan (seperti yang telah dipilih melalui tombol "Tambah Klien" pada Companyhalaman) dan batasi Ratepilihan itu Companyjuga.

Bagaimana saya melakukannya tentang Django 1.0?

forms.pyFile saya saat ini hanyalah boilerplate saat ini:

from models import *
from django.forms import ModelForm

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

Dan views.pyitu juga dasar:

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

Di Django 0.96 saya bisa meretas ini dengan melakukan sesuatu seperti berikut sebelum merender template:

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

ForeignKey.limit_choices_totampaknya menjanjikan tetapi saya tidak tahu bagaimana cara masuk the_company.iddan saya tidak jelas apakah itu akan bekerja di luar antarmuka Admin.

Terima kasih. (Ini sepertinya permintaan yang cukup mendasar tetapi jika saya harus mendesain ulang sesuatu, saya terbuka untuk saran.)

Tom
sumber
Terima kasih atas petunjuk untuk "limit_choices_to". Itu tidak menyelesaikan pertanyaan Anda, tetapi pertanyaan saya :-) Docs: docs.djangoproject.com/en/dev/ref/models/fields/…
guettli

Jawaban:

243

ForeignKey diwakili oleh django.forms.ModelChoiceField, yang merupakan ChoiceField yang pilihannya adalah model QuerySet. Lihat referensi untuk ModelChoiceField .

Jadi, berikan QuerySet ke querysetatribut bidang . Tergantung pada bagaimana formulir Anda dibangun. Jika Anda membuat formulir eksplisit, Anda akan memiliki bidang yang dinamai secara langsung.

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

Jika Anda mengambil objek ModelForm default, form.fields["rate"].queryset = ...

Ini dilakukan secara eksplisit dalam tampilan. Tidak perlu meretas.

S.Lott
sumber
Ok, kedengarannya menjanjikan. Bagaimana cara mengakses objek Bidang yang relevan? form.company.QuerySet = Rate.objects.filter (company_id = the_company.id)? atau melalui kamus?
Tom
1
Oke, terima kasih telah memperluas contoh, tetapi saya tampaknya harus menggunakan form.fields ["rate"]. Queryset untuk menghindari "objek 'ClientForm' tidak memiliki atribut 'rate'", apakah saya melewatkan sesuatu? (dan contoh Anda harus form.rate.queryset juga konsisten.)
Tom
8
Bukankah lebih baik untuk mengatur queryset bidang, dalam metode formulir __init__?
Lakshman Prasad
1
@ SLott komentar terakhir tidak benar (atau situs saya seharusnya tidak berfungsi :). Anda dapat mengisi data validasi dengan membuat menggunakan panggilan super (...) .__ init__ dalam metode yang diganti. Jika Anda membuat beberapa perubahan queryset ini jauh lebih elegan untuk mengemasnya dengan mengganti metode init .
michael
3
@ Slott cheers, saya sudah menambahkan jawaban karena butuh lebih dari 600 karakter untuk menjelaskan. Sekalipun pertanyaan ini sudah lama, ia mendapatkan skor google tinggi.
michael
135

Selain jawaban S.Lott dan sebagaimana menjadiGuru disebutkan dalam komentar, dimungkinkan untuk menambahkan filter queryset dengan mengabaikan ModelForm.__init__fungsi. (Ini bisa dengan mudah diterapkan pada formulir biasa) ini dapat membantu menggunakan kembali dan menjaga fungsi tampilan tetap rapi.

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

Ini dapat berguna untuk digunakan kembali katakan jika Anda memiliki filter umum yang diperlukan pada banyak model (biasanya saya mendeklarasikan kelas Formulir abstrak). Misalnya

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

Selain itu saya hanya ulangan materi blog Django yang ada banyak yang bagus di luar sana.

michael
sumber
Ada kesalahan ketik dalam cuplikan kode pertama Anda, Anda mendefinisikan args dua kali dalam __init __ () alih-alih args dan kwargs.
tpk
6
Saya suka jawaban ini lebih baik, saya pikir ini lebih bersih untuk merangkum logika inisialisasi bentuk di kelas bentuk, daripada dalam metode tampilan. Bersulang!
Simetris
44

Ini sederhana, dan bekerja dengan 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
    ....

Anda tidak perlu menentukan ini dalam kelas formulir, tetapi bisa melakukannya langsung di ModelAdmin, karena Django sudah menyertakan metode bawaan ini pada ModelAdmin (dari dokumen):

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)

Cara yang bahkan lebih mudah untuk melakukan ini (misalnya dalam membuat antarmuka admin front-end yang dapat diakses pengguna) adalah dengan subkelas ModelAdmin dan kemudian ubah metode di bawah ini. Hasil bersihnya adalah antarmuka pengguna yang HANYA menunjukkan kepada mereka konten yang terkait dengannya, sambil memungkinkan Anda (pengguna super) melihat semuanya.

Saya telah mengganti empat metode, dua yang pertama tidak memungkinkan bagi pengguna untuk menghapus apa pun, dan itu juga menghapus tombol hapus dari situs admin.

Timpa ketiga memfilter kueri yang berisi referensi ke (dalam contoh 'pengguna' atau 'landak' (seperti ilustrasi).

Timpa terakhir memfilter bidang asing apa pun dalam model untuk memfilter pilihan yang tersedia sama dengan kueryset dasar.

Dengan cara ini, Anda dapat menyajikan situs admin yang menghadap ke depan yang mudah dikelola yang memungkinkan pengguna untuk mengacaukan objek mereka sendiri, dan Anda tidak harus ingat untuk mengetikkan filter ModelAdmin spesifik yang telah kita bicarakan di atas.

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)

hapus tombol 'hapus':

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

mencegah izin hapus

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

memfilter objek yang dapat dilihat di situs admin:

    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

memfilter pilihan untuk semua bidang asing di situs admin:

    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)
neil.millikin
sumber
1
Dan saya harus menambahkan bahwa ini berfungsi dengan baik sebagai bentuk kustom umum untuk beberapa modeladmin dengan bidang referensi serupa yang menarik.
nemesisfixx
Ini adalah jawaban terbaik jika Anda menggunakan Django 1.4+
Rick Westera
16

Untuk melakukan ini dengan tampilan umum, seperti 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'))

bagian terpenting dari itu ...

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

, baca posting saya di sini

Teewuane
sumber
4

Jika Anda belum membuat formulir dan ingin mengubah kueryset yang dapat Anda lakukan:

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

Ini sangat berguna saat Anda menggunakan tampilan umum!

Hassek
sumber
2

Jadi, saya sudah benar-benar mencoba memahami ini, tetapi tampaknya Django masih tidak membuat ini dengan mudah. Aku tidak sebodoh itu, tapi aku tidak bisa melihat (agak) solusi sederhana.

Saya merasa secara umum sangat buruk untuk menimpa tampilan Admin untuk hal semacam ini, dan setiap contoh yang saya temukan tidak pernah sepenuhnya berlaku untuk tampilan Admin.

Ini adalah keadaan umum dengan model yang saya buat sehingga saya merasa mengerikan bahwa tidak ada solusi yang jelas untuk ini ...

Saya memiliki kelas-kelas ini:

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

Ini menimbulkan masalah ketika menyiapkan Admin untuk Perusahaan, karena memiliki inline untuk Kontrak dan Lokasi, dan opsi m2m Kontrak untuk Lokasi tidak difilter dengan benar menurut Perusahaan yang sedang Anda edit.

Singkatnya, saya perlu beberapa opsi admin untuk melakukan sesuatu seperti ini:

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

Pada akhirnya saya tidak akan peduli jika proses penyaringan ditempatkan pada basis CompanyAdmin, atau jika ditempatkan pada ContractInline. (Menempatkannya di garis lebih masuk akal, tetapi membuatnya sulit untuk merujuk Kontrak dasar sebagai 'diri'.)

Adakah orang di luar sana yang mengetahui sesuatu yang langsung seperti jalan pintas yang sangat dibutuhkan ini? Kembali ketika saya membuat admin PHP untuk hal semacam ini, ini dianggap fungsi dasar! Bahkan, itu selalu otomatis, dan harus dinonaktifkan jika Anda benar-benar tidak menginginkannya!

Tim
sumber
0

Cara yang lebih umum adalah dengan memanggil get_form di kelas Admin. Ini juga berfungsi untuk bidang non-database juga. Sebagai contoh di sini saya memiliki bidang yang disebut '_terminal_list' pada formulir yang dapat digunakan dalam kasus-kasus khusus untuk memilih beberapa item terminal dari get_list (permintaan), kemudian penyaringan berdasarkan pada 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
F.Tamy
sumber