ฉันจะกรองตัวเลือก ForeignKey ใน Django ModelForm ได้อย่างไร


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 RatesCompany's Rates

เมื่อสร้างแบบฟอร์มสำหรับการเพิ่มClientฉันต้องการลบCompanyตัวเลือก (เนื่องจากได้ถูกเลือกผ่านปุ่ม "เพิ่มลูกค้า" ใน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 ซึ่งตัวเลือกเป็น QuerySet รุ่น ดูอ้างอิงสำหรับModelChoiceField

ดังนั้นให้ QuerySet กับquerysetแอตทริบิวต์ของเขตข้อมูล ขึ้นอยู่กับการสร้างแบบฟอร์มของคุณ หากคุณสร้างแบบฟอร์มที่ชัดเจนคุณจะมีชื่อเขตข้อมูลโดยตรง

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

ถ้าคุณใช้วัตถุ ModelForm เริ่มต้น form.fields["rate"].queryset = ...

สิ่งนี้ทำอย่างชัดเจนในมุมมอง ไม่มีการแฮ็ค


ตกลงนั่นฟังดูมีแนวโน้ม ฉันจะเข้าถึงวัตถุฟิลด์ที่เกี่ยวข้องได้อย่างไร form.company.QuerySet = Rate.objects.filter (company_id = the_company.id)? หรือผ่านพจนานุกรม?
ทอม

1
ตกลงขอบคุณที่ขยายตัวอย่าง แต่ฉันต้องใช้ form.fields ["rate"]. queryset เพื่อหลีกเลี่ยงวัตถุ "'ClientForm' ไม่มีแอตทริบิวต์ 'rate'" ฉันไม่มีอะไรเลยหรือ (และตัวอย่างของคุณควรจะ form.rate.queryset เพื่อให้สอดคล้องเกินไป.)
ทอม

8
จะเป็นการดีกว่าหรือไม่ที่จะตั้งค่าชุดข้อมูลคิวรีของเขตข้อมูลใน__init__วิธีการของฟอร์ม
Lakshman Prasad

1
@SLott ความคิดเห็นล่าสุดไม่ถูกต้อง (หรือเว็บไซต์ของฉันไม่ควรทำงาน :) คุณสามารถเติมข้อมูลการตรวจสอบความถูกต้องโดยใช้การโทร super (... ) .__ init__ ในวิธีการแทนที่ของคุณ หากคุณกำลังทำให้ชุดข้อความค้นหาเหล่านี้หลายชุดมีการเปลี่ยนแปลงที่สวยงามกว่าแพ็คเกจเหล่านี้โดยการแทนที่เมธอดinit
ไมเคิล

3
@Slott ไชโยฉันได้เพิ่มคำตอบเพราะมันต้องใช้เวลามากกว่า 600 ตัวเพื่ออธิบาย แม้ว่าคำถามนี้จะเก่า แต่ก็มีคะแนน google สูง
ไมเคิล

135

นอกเหนือจากคำตอบของ S.Lott และในฐานะที่เป็นกูรูที่กล่าวถึงในความคิดเห็นเป็นไปได้ที่จะเพิ่มตัวกรองชุดข้อมูลโดยการแทนที่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})

สิ่งนี้มีประโยชน์สำหรับการใช้ซ้ำถ้าคุณมีตัวกรองทั่วไปที่ต้องการในหลายรุ่น (ปกติฉันจะประกาศคลาสแบบนามธรรม) เช่น

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 ซึ่งมีสิ่งดีๆมากมายอยู่ในนั้น


มีการพิมพ์ผิดในข้อมูลโค้ดแรกของคุณคุณกำหนด args สองครั้งใน __init __ () แทนที่จะเป็น args และ kwargs
tpk

6
ฉันชอบคำตอบนี้ดีกว่าฉันคิดว่ามันสะอาดกว่าที่จะแค็ปซูลตรรกะการเริ่มต้นแบบฟอร์มในคลาสฟอร์มแทนที่จะเป็นวิธีการดู ไชโย!
สมมาตร

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 แล้วเปลี่ยนวิธีการด้านล่าง ผลลัพธ์สุทธิคืออินเทอร์เฟซผู้ใช้ที่แสดงเฉพาะเนื้อหาที่เกี่ยวข้องกับพวกเขาเท่านั้นในขณะที่อนุญาตให้คุณ (ผู้ใช้ขั้นสูง) เห็นทุกสิ่ง

ฉันได้ลบล้างวิธีการสี่วิธีแล้วทั้งสองวิธีแรกทำให้ผู้ใช้ไม่สามารถลบสิ่งใด ๆ ได้และยังลบปุ่มลบออกจากไซต์ผู้ดูแลระบบ

การแทนที่ที่สามกรองการสืบค้นใด ๆ ที่มีการอ้างอิงถึง (ในตัวอย่าง 'ผู้ใช้' หรือ 'เม่น' (เช่นเดียวกับภาพประกอบ)

การแทนที่ครั้งล่าสุดจะกรองฟิลด์ 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

นี่เป็นคำตอบที่ดีที่สุดถ้าคุณใช้ Django 1.4+
Rick Westera

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)

สิ่งนี้จะสร้างปัญหาเมื่อตั้งค่าผู้ดูแลระบบสำหรับ บริษัท เนื่องจากมี inline สำหรับทั้งสัญญาและที่ตั้งและตัวเลือก 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 (การวางไว้บน inline เหมาะสมกว่า แต่มันยากที่จะอ้างอิงสัญญาพื้นฐานว่า 'ตนเอง')

มีใครบ้างไหมที่รู้สิ่งที่ตรงไปตรงมาเช่นนี้ต้องการทางลัดที่ไม่ดีหรือไม่? ย้อนกลับไปเมื่อฉันสร้างผู้ดูแลระบบ PHP สำหรับสิ่งนี้ถือว่าเป็นฟังก์ชันพื้นฐาน! ในความเป็นจริงมันเป็นไปโดยอัตโนมัติเสมอและจะต้องปิดการใช้งานถ้าคุณไม่ต้องการมัน!


0

วิธีที่เปิดเผยมากขึ้นคือการเรียกใช้ get_form ในคลาสของผู้ดูแลระบบ นอกจากนี้ยังใช้งานได้กับเขตข้อมูลที่ไม่ใช่ฐานข้อมูลด้วย ตัวอย่างเช่นที่นี่ฉันมีฟิลด์ชื่อ '_terminal_list' ในแบบฟอร์มที่สามารถใช้ในกรณีพิเศษสำหรับการเลือกรายการเทอร์มินัลจำนวนมากจาก get_list (คำขอ) จากนั้นกรองตาม 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.