ChatGPT解决这个技术问题 Extra ChatGPT

How do I filter ForeignKey choices in a Django ModelForm?

Say I have the following in my 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)

I.e. there are multiple Companies, each having a range of Rates and Clients. Each Client should have a base Rate that is chosen from it's parent Company's Rates, not another Company's Rates.

When creating a form for adding a Client, I would like to remove the Company choices (as that has already been selected via an "Add Client" button on the Company page) and limit the Rate choices to that Company as well.

How do I go about this in Django 1.0?

My current forms.py file is just boilerplate at the moment:

from models import *
from django.forms import ModelForm

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

And the views.py is also basic:

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

In Django 0.96 I was able to hack this in by doing something like the following before rendering the template:

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

ForeignKey.limit_choices_to seems promising but I don't know how to pass in the_company.id and I'm not clear if that will work outside the Admin interface anyway.

Thanks. (This seems like a pretty basic request but if I should redesign something I'm open to suggestions.)

Thank you for the hint to "limit_choices_to". It does not solve your question, but mine :-) Docs: docs.djangoproject.com/en/dev/ref/models/fields/…
These days, if you're using the generic editing views (CreateView etc) then my preferred way to filter ForeignKey choices on a ModelForm is to override get_form_class() in the view. You can then set base_fields['my_field_name'].limit_choices_to - e.g. see stackoverflow.com/questions/70399761

S
S.Lott

ForeignKey is represented by django.forms.ModelChoiceField, which is a ChoiceField whose choices are a model QuerySet. See the reference for ModelChoiceField.

So, provide a QuerySet to the field's queryset attribute. Depends on how your form is built. If you build an explicit form, you'll have fields named directly.

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

If you take the default ModelForm object, form.fields["rate"].queryset = ...

This is done explicitly in the view. No hacking around.


Ok, that sounds promising. How do I access the relevant Field object? form.company.QuerySet = Rate.objects.filter(company_id=the_company.id) ? or via a dictionary?
Ok, thanks for expanding the example, but I seem to have to use form.fields["rate"].queryset to avoid "'ClientForm' object has no attribute 'rate'", am I missing something? (and your example should be form.rate.queryset to be consistent too.)
Wouldn't it be better to set the fields' queryset, in the form's __init__ method?
@SLott the last comment is not correct (or my site shouldn't be working :). You can populate the validation data by making using the super(...).__init__ call in your overridden method. If you are making several of these queryset changes its a lot more elegant to package them by overriding the init method.
@Slott cheers, I've added an answer as it'd take more than 600 chars to explain . Even if this question is old it's getting a high google score.
T
Tom

In addition to S.Lott's answer and as becomingGuru mentioned in comments, its possible to add the queryset filters by overriding the ModelForm.__init__ function. (This could easily apply to regular forms) it can help with reuse and keeps the view function tidy.

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

This can be useful for reuse say if you have common filters needed on many models (normally I declare an abstract Form class). E.g.

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

Other than that I'm just restating Django blog material of which there are many good ones out there.


There's a typo in your first code snippet, you're defining args twice in __init__() instead of args and kwargs.
I like this answer better, I think it's cleaner to encapsulate the form initialisation logic in the form class, rather than in the view method. Cheers!
n
neil.millikin

This is simple, and works with 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
    ....

You don't need to specify this in a form class, but can do it directly in the ModelAdmin, as Django already includes this built-in method on the ModelAdmin (from the docs):

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)

An even niftier way to do this (for example in creating a front-end admin interface that users can access) is to subclass the ModelAdmin and then alter the methods below. The net result is a user interface that ONLY shows them content that is related to them, while allowing you (a super-user) to see everything.

I've overridden four methods, the first two make it impossible for a user to delete anything, and it also removes the delete buttons from the admin site.

The third override filters any query that contains a reference to (in the example 'user' or 'porcupine' (just as an illustration).

The last override filters any foreignkey field in the model to filter the choices available the same as the basic queryset.

In this way, you can present an easy to manage front-facing admin site that allows users to mess with their own objects, and you don't have to remember to type in the specific ModelAdmin filters we talked about above.

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)

remove 'delete' buttons:

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

prevents delete permission

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

filters objects that can be viewed on the admin site:

    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

filters choices for all foreignkey fields on the admin site:

    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)

And I should add that this works well as a generic custom form for multiple modeladmins with similar reference fields of interest.
This is the best answer if you're using Django 1.4+
B
BoltClock

To do this with a generic view, like 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'))

the most important part of that...

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

, read my post here


I first put the queryset assignment in the form, but I was getting a form error. By moving the queryset assignment to the view, no more error.
W
Wtower

If you haven't created the form and want to change the queryset you can do:

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

This is pretty useful when you are using generic views!


T
Tim

So, I've really tried to understand this, but it seems that Django still doesn't make this very straightforward. I'm not all that dumb, but I just can't see any (somewhat) simple solution.

I find it generally pretty ugly to have to override the Admin views for this sort of thing, and every example I find never fully applies to the Admin views.

This is such a common circumstance with the models I make that I find it appalling that there's no obvious solution to this...

I've got these classes:

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

This creates a problem when setting up the Admin for Company, because it has inlines for both Contract and Location, and Contract's m2m options for Location are not properly filtered according to the Company that you're currently editing.

In short, I would need some admin option to do something like this:

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

Ultimately I wouldn't care if the filtering process was placed on the base CompanyAdmin, or if it was placed on the ContractInline. (Placing it on the inline makes more sense, but it makes it hard to reference the base Contract as 'self'.)

Is there anyone out there who knows of something as straightforward as this badly needed shortcut? Back when I made PHP admins for this sort of thing, this was considered basic functionality! In fact, it was always automatic, and had to be disabled if you really didn't want it!


F
FloatingKiwi

A more public way is by calling get_form in Admin classes. It also works for non-database fields too. For example here i have a field called '_terminal_list' on the form that can be used in special cases for choosing several terminal items from get_list(request), then filtering based on 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

M
Martin CR

A good way to limit the choices for a ForeignKey field of a ModelForm at run time (e.g. in a CreateView), is to set limit_choices_to for base_fields['field_name'] by overriding get_form_class() in the view.

For example when creating a Client, to limit the choices of Rate to those for the Company identified in the URL:

class ClientCreateView(LoginRequired, CreateView):
    model = Client
    fields = '__all__'
    
    def get_form_class(self):
        modelform = super().get_form_class()
        modelform.base_fields['rate'].limit_choices_to = {'company': self.kwargs['company']}
        return modelform

j
jorge4larcon

According to the Django documentation, you can apply filters to your default queryset using the __init__ method of the model form.

https://docs.djangoproject.com/en/3.1/ref/contrib/admin/#django.contrib.admin.ModelAdmin.formfield_for_foreignkey

class CountryAdminForm(forms.ModelForm):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['capital'].queryset = self.instance.cities.all()

class CountryAdmin(admin.ModelAdmin):
    form = CountryAdminForm