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는 각각의 범위를 가지는, RatesClients. 각각 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로 표시되며, 이는 선택 사항이 모델 QuerySet 인 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)? 또는 사전을 통해?
Tom

1
좋습니다. 예제를 확장 해 주셔서 감사합니다.하지만 ''ClientForm '개체에'rate ''속성이 없습니다. (그리고 예제도 form.rate.queryset도 일관성이 있어야합니다.)
Tom

8
양식의 __init__메소드 에서 필드의 쿼리 세트를 설정하는 것이 더 좋지 않습니까?
Lakshman Prasad

1
@SLott 마지막 의견이 올바르지 않습니다 (또는 내 사이트가 작동하지 않아야합니다 :). 재정의 된 메서드에서 super (...) .__ init__ 호출을 사용하여 유효성 검사 데이터를 채울 수 있습니다. 이러한 쿼리 세트를 여러 개 만드는 경우 init 메소드 를 재정 의하여 패키지화하기 위해 쿼리 세트를 훨씬 더 우아하게 변경하십시오 .
마이클

3
@Slott는 환호합니다. 설명하는 데 600 자 이상이 걸리므로 답변을 추가했습니다. 이 질문이 오래되었지만 Google 점수가 높습니다.
michael

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

많은 모델에 필요한 공통 필터가있는 경우 재사용에 유용 할 수 있습니다 (일반적으로 추상 폼 클래스를 선언합니다). 예 :

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

그 외에는 장고 블로그 자료를 잘 정리하고 있습니다.


첫 번째 코드 스 니펫에는 오타가 있습니다. args 및 kwargs 대신 __init __ ()에서 args를 두 번 정의하고 있습니다.
tpk

6
이 답변이 더 좋았습니다.보기 메소드가 아닌 양식 클래스에서 양식 초기화 논리를 캡슐화하는 것이 더 깨끗하다고 ​​생각합니다. 건배!
Symmetric

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
    ....

Django는 이미 ModelAdmin에서이 내장 메소드를 문서에서 제공하므로 폼 클래스에서이를 지정할 필요는 없지만 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을 서브 클래 싱 한 다음 아래 방법을 변경하면됩니다. 최종 결과는 사용자와 관련된 콘텐츠 만 표시하고 수퍼 유저는 모든 것을 볼 수있는 사용자 인터페이스입니다.

네 가지 방법을 재정의했으며 처음 두 가지 방법으로 사용자가 아무것도 삭제할 수 없으며 관리 사이트에서 삭제 버튼도 제거합니다.

세 번째 재정의는 참조 (예 : 'user'또는 'porcupine'(예시))가 포함 된 모든 쿼리를 필터링합니다.

마지막 재정의는 모델의 외래 키 필드를 필터링하여 기본 쿼리 집합과 동일한 선택 항목을 필터링합니다.

이러한 방식으로, 사용자가 자신의 객체를 망칠 수있는 관리하기 쉬운 전면 관리 사이트를 제시 할 수 있으며 위에서 언급 한 특정 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

관리 사이트의 모든 외래 키 필드에 대한 선택을 필터링합니다.

    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

이것은 당신이 1.4 장고를 사용하는 경우 가장 좋은 답변입니다
릭 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

그래서, 나는 이것을 정말로 이해하려고 노력했지만 장고는 여전히 이것을 매우 간단하게하지 않는 것 같습니다. 나는 그 바보가 아니지만 간단한 해결책을 볼 수는 없습니다.

나는 이런 종류의 일에 대해 관리자보기를 재정의 해야하는 것이 일반적으로 꽤 추악하다는 것을 알았습니다. 내가 찾은 모든 예제는 관리자보기에 완전히 적용되지 않습니다.

이것은 내가 만든 모델과 같은 일반적인 상황으로, 이것에 대한 명확한 해결책이 없다는 것을 알게됩니다 ...

나는이 수업을 가지고 있습니다 :

# 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

보다 공용적인 방법은 관리 클래스에서 get_form을 호출하는 것입니다. 데이터베이스 이외의 필드에서도 작동합니다. 예를 들어 여기에는 get_list (request)에서 여러 터미널 항목을 선택한 다음 request.user를 기준으로 필터링하는 특별한 경우에 사용할 수있는 양식에 '_terminal_list'라는 필드가 있습니다.

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.