Question

How do you limit what choices are shown for ForeignKey fields in Django's admin when they're displayed using the raw_id_fields option?

When rendered as a select box, it's simple to define a custom ModelForm to set that field's queryset value with the choices to want. However, this queryset appears to be completely ignored when rendered using raw_id_fields. It generates a link to that ForeignKey's model, allowing you to select any record from that model via a popup window. You can still filter these values by customizing the URL, but I can't find a way to do this via a ModelAdmin.

Was it helpful?

Solution

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.

OTHER TIPS

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

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

I created a genetic solution to solve the problem of custom parameters to pass to popup window. You just need copy this code on your project:

from django.contrib.admin import widgets

class GenericRawIdWidget(widgets.ForeignKeyRawIdWidget):
    url_params = []

    def __init__(self, rel, admin_site, attrs=None, \
        using=None, url_params=[]):
        super(GenericRawIdWidget, self).__init__(
            rel, admin_site, attrs=attrs, using=using)
        self.url_params = url_params

    def url_parameters(self):
        """
        activate one or more filters by default
        """
        res = super(GenericRawIdWidget, self).url_parameters()
        res.update(**self.url_params)

        return res

Then, you can use like this:

field.widget = GenericRawIdWidget(YOURMODEL._meta.get_field('YOUR_RELATION').rel,
            admin.site, url_params={"<YOURMODEL>__id__exact":     object_id})

I used it in this way:

class ANSRuleInline(admin.TabularInline):
    model = ANSRule 
    form = ANSRuleInlineForm
    extra = 1
    raw_id_fields = ('parent',)

    def __init__(self, *args, **kwargs):
        super (ANSRuleInline,self ).__init__(*args,**kwargs)

    def formfield_for_dbfield(self, db_field, **kwargs):
        formfield = super(ANSRuleInline, self).formfield_for_dbfield(db_field, **kwargs)
        request = kwargs.get("request", None)
        object_id = self.get_object(request)

        if db_field.name == 'parent':
            formfield.widget = GenericRawIdWidget(ANSRule._meta.get_field('parent').rel,
                admin.site, url_params={"pathology__id__exact": object_id})

        return formfield

    def get_object(self, request):
        object_id = request.META['PATH_INFO'].strip('/').split('/')[-1]
        try:
            object_id = int(object_id)
        except ValueError:
            return None
        return object_id

When I use GenericRawIdWidget, I pass a dict to url_params, that will be used on url.

@Dmitriy Sintsov's answer is great if you only need to filter on static types, but in my case I had a foriegn relationship between both models, and I wanted it to filter based on the specific ID I was working with.

To build on his answer, imagine that Project has a foreign key relationship with Blog, and when choosing a Blog to filter, you want it to only show those which are related to Project. These two changes to his answer acomplish that:

  1. Add a new variable inside the widget -- I've named it project_id
class BlogRawIdWidget(widgets.ForeignKeyRawIdWidget):

    def __init__(self, *args, **kwargs):
        self.project_id = kwargs.pop('project_id')
        super().__init__(*args, **kwargs)
  1. Modify the line where that widget is invoked:
rel=Project._meta.get_field('blog').remote_field, admin_site=site,
        project_id=self.instance.project.id)

The full code is below:

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 __init__(self, *args, **kwargs):
        self.project_id = kwargs.pop('project_id')
        super().__init__(*args, **kwargs)

    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,
        project_id=self.instance.project.id)

    class Meta:
        # Django 1.8 convenience:
        fields = '__all__'
        model = Project

class ProjectAdmin(admin.ModelAdmin):
    form = ProjectAdminForm
    raw_id_fields = ('blog',)
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top