How to limit choices of ForeignKey choices for Django raw_id_field
The method below works for me but it is a queryset that affects every admin that needs to use the Customer model. But if you have another Admin, e.g. Invoice that requires a different queryset, you might want to experiment a bit with model proxy.
Model
class Customer(models.Model):
name = models.CharField(max_length=100)
is_active = models.BooleanField()
class Order(models.Model):
cust = models.ForeignKey(Customer)
Admin
class CustomerAdmin(admin.ModelAdmin):
def queryset(self, request):
qs = super(CustomerAdmin, self).queryset(request)
return qs.filter(is_active=1)
class OrderAdmin():
raw_id_fields = ('cust', )
I find the given solution (customizing the ModelAdmin
queryset) a bit too restrictive, for realistic projects.
What I do, is usually the following:
- create a custom filter in my
ModelAdmin
(e.g. subclassingadmin.SimpleListFilter
, see the doc) create my subclass of widget
ForeignKeyRawIdWidget
as follows:class CustomRawIdWidget(ForeignKeyRawIdWidget): def url_parameters(self): """ activate one or more filters by default """ res = super(CustomRawIdWidget, self).url_parameters() res["<filter_name>__exact"] = "<filter_value>" return res
note that what the only thing the custom widget does is to "preselect" the filter that, in turn, is responsible for "restricting" the queryset
use the custom widget:
class MyForm(forms.ModelForm): myfield = forms.ModelChoiceField(queryset=MyModel.objects.all(), ... widget=CustomRawIdWidget( MyRelationModel._meta.get_field('myfield').rel, admin.site))
One weak point of this approach is that the filter selected by the widget does not prevent from selecting some other instance from that model. If this is desired, I override the method ModelAdmin.save_model(...)
(see the doc) to check that the related instances are only the allowed ones.
I find this approach a bit more complex, but much more flexible than limiting the queryset for the entire ModelAdmin
.
I use similar to FSp approach in my Django 1.8 / Python 3.4 project:
from django.contrib import admin
from django.contrib.admin import widgets
from django.contrib.admin.sites import site
from django import forms
class BlogRawIdWidget(widgets.ForeignKeyRawIdWidget):
def url_parameters(self):
res = super().url_parameters()
res['type__exact'] = 'PROJ'
return res
class ProjectAdminForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['blog'].queryset = Blog.objects.filter(type='PROJ')
self.fields['blog'].widget = BlogRawIdWidget(rel=Project._meta.get_field('blog').remote_field, admin_site=site)
class Meta:
# Django 1.8 convenience:
fields = '__all__'
model = Project
class ProjectAdmin(admin.ModelAdmin):
form = ProjectAdminForm
raw_id_fields = ('blog',)
to select only blog.type == 'PROJ'
as foreign key Project.blog
in django.admin
. Because end-users may and will to select anything, unfortunately.
If you need to filter your raw_id list_view popup based on model instance you can use example below:
1. Write custom widget
class RawIdWidget(widgets.ForeignKeyRawIdWidget):
def url_parameters(self):
res = super(RawIdWidget, self).url_parameters()
object = self.attrs.get('object', None)
if object:
# Filter variants by product_id
res['product_id'] = object.variant.product_id
return res
2. Pass instance on form init
class ModelForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(ModelForm, self).__init__(*args, **kwargs)
obj = kwargs.get('instance', None)
if obj and obj.pk is not None:
self.fields['variant'].widget = RawIdWidget(
rel=obj._meta.get_field('variant').rel,
admin_site=admin.site,
# Pass the object to attrs
attrs={'object': obj}
)