Làm cách nào để lọc các lựa chọn ForeignKey trong Django ModelForm?


227

Nói rằng tôi có những điều sau đây trong 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)

Tức là có nhiều Companies, mỗi cái có một phạm vi RatesClients. Mỗi Clientnên có một cơ sở Rateđược chọn từ cha mẹ của nó Company's Rates, không phải cơ sở khác Company's Rates.

Khi tạo biểu mẫu để thêm a Client, tôi muốn xóa các Companylựa chọn (vì nó đã được chọn thông qua nút "Thêm khách hàng" trên Companytrang) và cũng giới hạn các Ratelựa chọn đó Company.

Làm thế nào để tôi đi về điều này trong Django 1.0?

forms.pyTập tin hiện tại của tôi chỉ là bản tóm tắt tại thời điểm này:

from models import *
from django.forms import ModelForm

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

views.pycũng là cơ bản:

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

Trong Django 0.96 tôi đã có thể hack nó bằng cách thực hiện một số thứ như sau trước khi kết xuất mẫu:

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

ForeignKey.limit_choices_tocó vẻ đầy hứa hẹn nhưng tôi không biết làm thế nào để vượt qua the_company.idvà tôi không rõ liệu điều đó có hoạt động bên ngoài giao diện Admin hay không.

Cảm ơn. (Đây có vẻ là một yêu cầu khá cơ bản nhưng nếu tôi nên thiết kế lại một cái gì đó thì tôi sẽ mở để đề xuất.)


Cảm ơn bạn đã gợi ý "giới hạn_choices_to". Nó không giải quyết được câu hỏi của bạn, nhưng tôi :-) Documents: docs.djangoproject.com/en/dev/ref/models/fields/...
guettli

Câu trả lời:


243

ForeignKey được đại diện bởi django.forms.ModelChoiceField, là một ChoiceField có các lựa chọn là một mô hình Queryset. Xem tài liệu tham khảo cho ModelChoiceField .

Vì vậy, hãy cung cấp một Truy vấn cho querysetthuộc tính của trường . Phụ thuộc vào cách thức hình thức của bạn được xây dựng. Nếu bạn xây dựng một biểu mẫu rõ ràng, bạn sẽ có các trường được đặt tên trực tiếp.

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

Nếu bạn lấy đối tượng ModelForm mặc định, form.fields["rate"].queryset = ...

Điều này được thực hiện rõ ràng trong quan điểm. Không có hack xung quanh.


Ok, điều đó có vẻ hứa hẹn. Làm cách nào để truy cập đối tượng Trường có liên quan? form.company.QuerySet = Rate.objects.filter (company_id = the_company.id)? hoặc thông qua một từ điển?
Tom

1
Ok, cảm ơn vì đã mở rộng ví dụ, nhưng tôi dường như phải sử dụng form.fields ["Rate"]. Truy vấn để tránh đối tượng "'ClientForm' không có thuộc tính 'Rate'", tôi có thiếu thứ gì không? (và ví dụ của bạn phải là form.rate.queryset để được nhất quán.)
Tom

8
Sẽ tốt hơn nếu đặt bộ truy vấn của các trường, theo phương thức của biểu mẫu __init__?
Lakshman Prasad

1
@SLott bình luận cuối cùng không đúng (hoặc trang web của tôi không nên hoạt động :). Bạn có thể điền dữ liệu xác thực bằng cách sử dụng cuộc gọi siêu (...) .__ init__ trong phương thức ghi đè của bạn. Nếu bạn đang thực hiện một vài trong số các bộ truy vấn này, nó sẽ thanh lịch hơn rất nhiều để đóng gói chúng bằng cách ghi đè phương thức init .
michael

3
@Slott chúc mừng, tôi đã thêm một câu trả lời vì phải mất hơn 600 ký tự để giải thích. Ngay cả khi câu hỏi này đã cũ, nó vẫn nhận được điểm google cao.
michael

135

Ngoài câu trả lời của S.Lott và khi trở thành Guru được đề cập trong các bình luận, có thể thêm các bộ lọc truy vấn bằng cách ghi đè ModelForm.__init__hàm. (Điều này có thể dễ dàng áp dụng cho các biểu mẫu thông thường) nó có thể giúp tái sử dụng và giữ cho chức năng xem gọn gàng.

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

Điều này có thể hữu ích cho việc tái sử dụng nếu bạn có các bộ lọc chung cần thiết cho nhiều mô hình (thông thường tôi khai báo một lớp Form trừu tượng). Ví dụ

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

Ngoài ra, tôi chỉ đang khôi phục tài liệu blog của Django, trong đó có nhiều tài liệu hay.


Có một lỗi đánh máy trong đoạn mã đầu tiên của bạn, bạn đang xác định args hai lần trong __init __ () thay vì args và kwargs.
tpk

6
Tôi thích câu trả lời này tốt hơn, tôi nghĩ sẽ tốt hơn khi gói gọn logic khởi tạo biểu mẫu trong lớp biểu mẫu, thay vì trong phương thức xem. Chúc mừng!
xứng

44

Điều này rất đơn giản và hoạt động với 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
    ....

Bạn không cần chỉ định điều này trong một lớp biểu mẫu, nhưng có thể thực hiện trực tiếp trong ModelAdmin, vì Django đã bao gồm phương thức tích hợp sẵn này trên ModelAdmin (từ các tài liệu):

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)

Một cách thậm chí còn nhanh hơn để làm điều này (ví dụ như trong việc tạo giao diện quản trị viên phía trước mà người dùng có thể truy cập) là phân lớp ModelAdmin và sau đó thay đổi các phương thức bên dưới. Kết quả cuối cùng là giao diện người dùng CHỈ hiển thị cho họ nội dung có liên quan đến họ, đồng thời cho phép bạn (một siêu người dùng) nhìn thấy mọi thứ.

Tôi đã ghi đè bốn phương thức, hai phương thức đầu tiên khiến người dùng không thể xóa bất cứ thứ gì và nó cũng xóa các nút xóa khỏi trang quản trị.

Ghi đè thứ ba lọc bất kỳ truy vấn nào có chứa tham chiếu đến (trong ví dụ 'người dùng' hoặc 'porcupine' (giống như một minh họa).

Ghi đè cuối cùng lọc bất kỳ trường khóa ngoại nào trong mô hình để lọc các lựa chọn có sẵn giống như bộ truy vấn cơ bản.

Theo cách này, bạn có thể trình bày một trang quản trị trực diện dễ quản lý, cho phép người dùng gây rối với các đối tượng của riêng họ và bạn không phải nhớ nhập các bộ lọc ModelAdmin cụ thể mà chúng ta đã nói ở trên.

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)

xóa các nút 'xóa':

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

ngăn chặn xóa quyền

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

lọc các đối tượng có thể được xem trên trang quản trị:

    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

lọc các lựa chọn cho tất cả các trường khóa ngoài trên trang quản trị:

    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
Và tôi nên thêm rằng điều này hoạt động tốt như một hình thức tùy chỉnh chung cho nhiều mô hình với các trường tham chiếu tương tự.
nemeisfixx

Đây là câu trả lời tốt nhất nếu bạn đang sử dụng Django 1.4+
Rick Westera

16

Để thực hiện việc này với chế độ xem chung, như Tạo ...

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

phần quan trọng nhất của điều đó ...

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

, đọc bài viết của tôi ở đây


4

Nếu bạn chưa tạo biểu mẫu và muốn thay đổi bộ truy vấn, bạn có thể thực hiện:

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

Điều này khá hữu ích khi bạn đang sử dụng các quan điểm chung!


2

Vì vậy, tôi đã thực sự cố gắng để hiểu điều này, nhưng có vẻ như Django vẫn không thực hiện điều này rất đơn giản. Tôi không ngu ngốc đến thế, nhưng tôi không thể thấy bất kỳ giải pháp đơn giản nào (phần nào).

Tôi thấy khá là xấu khi phải ghi đè lên các lượt xem của Quản trị viên cho loại điều này và mọi ví dụ tôi thấy không bao giờ áp dụng hoàn toàn cho các lượt xem của Quản trị viên.

Đây là một tình huống phổ biến như vậy với các mô hình mà tôi thực hiện mà tôi thấy nó kinh khủng rằng không có giải pháp rõ ràng nào cho việc này ...

Tôi đã có những lớp học này:

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

Điều này tạo ra sự cố khi thiết lập Quản trị viên cho Công ty, vì nó có nội tuyến cho cả Hợp đồng và Vị trí và các tùy chọn m2m của Hợp đồng cho Vị trí không được lọc đúng theo Công ty mà bạn hiện đang chỉnh sửa.

Nói tóm lại, tôi sẽ cần một số tùy chọn quản trị để làm một cái gì đó như thế này:

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

Cuối cùng, tôi sẽ không quan tâm nếu quy trình lọc được đặt trên BaseAdmin cơ sở hay nếu nó được đặt trên ContractInline. (Đặt nó trên dòng có ý nghĩa hơn, nhưng nó khiến cho việc tham chiếu Hợp đồng cơ sở là 'bản thân' trở nên khó khăn.)

Có ai ngoài đó biết về một cái gì đó đơn giản như lối tắt rất cần thiết này? Quay lại khi tôi tạo quản trị viên PHP cho loại điều này, đây được coi là chức năng cơ bản! Trên thực tế, nó luôn tự động và phải bị vô hiệu hóa nếu bạn thực sự không muốn nó!


0

Một cách công khai hơn là bằng cách gọi get_form trong các lớp Quản trị viên. Nó cũng hoạt động cho các lĩnh vực không phải là cơ sở dữ liệu quá. Ví dụ ở đây tôi có một trường có tên '_terminal_list' trên biểu mẫu có thể được sử dụng trong các trường hợp đặc biệt để chọn một số mục thiết bị đầu cuối từ get_list (request), sau đó lọc dựa trên 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
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.