Can I make list_filter in django admin to only show referenced ForeignKeys?
Asked Answered
B

7

73

I have a django application which has two models like this:

class MyModel(models.Model):
    name = models.CharField()
    country = models.ForeignKey('Country')

class Country(models.Model):
    code2 = models.CharField(max_length=2, primary_key=True)
    name = models.CharField()

The admin class for MyModel looks like this:

class MyModelAdmin(admin.ModelAdmin):
    list_display = ('name', 'country',)
    list_filter = ('country',)
admin.site.register(models.MyModel, MyModelAdmin)

The Country table contains ~250 countries. Only a handful of countries are actually referenced by some MyModel instance.

The problem is that the list filter in the django admin lists ALL countries in the filter panel. Listing all countries (and not just those that are referenced by an instance) pretty much defeats the purpose of having the list filter in this case.

Is there some to only display the countries referenced by MyModel as choices in the list filter? (I use Django 1.3.)

Boll answered 31/8, 2012 at 12:51 Comment(0)
E
104

As of Django 1.8, there is a built in RelatedOnlyFieldListFilter, which you can use to show related countries.

class MyModelAdmin(admin.ModelAdmin):
    list_display = ('name', 'country',)
    list_filter = (
        ('country', admin.RelatedOnlyFieldListFilter),
    )

For Django 1.4-1.7, list_filter allows you to use a subclass of SimpleListFilter. It should be possible to create a simple list filter that lists the values you want.

If you can't upgrade from Django 1.3, you'd need to use the internal, and undocumented, FilterSpec api. The Stack Overflow question Custom Filter in Django Admin should point you in the right direction.

Elbaelbart answered 31/8, 2012 at 13:19 Comment(2)
Thanks for the reply. Migration to Django 1.4 is planned for the near future, so I will postpone any fixes to the problem until then.Boll
@andi thanks, I've updated the answer with the new infoElbaelbart
A
36

I know question was about Django 1.3 however you mentioned on soon upgrading to 1.4. Also to people, like me who was looking for solution for 1.4, but found this entry, i decided to show full example of using SimpleListFilter (available Django 1.4) to show only referenced (related, used) foreign key values

from django.contrib.admin import SimpleListFilter

# admin.py
class CountryFilter(SimpleListFilter):
    title = 'country' # or use _('country') for translated title
    parameter_name = 'country'

    def lookups(self, request, model_admin):
        countries = set([c.country for c in model_admin.model.objects.all()])
        return [(c.id, c.name) for c in countries]
        # You can also use hardcoded model name like "Country" instead of 
        # "model_admin.model" if this is not direct foreign key filter

    def queryset(self, request, queryset):
        if self.value():
            return queryset.filter(country__id__exact=self.value())
        else:
            return queryset

# Example setup and usage

# models.py
from django.db import models

class Country(models.Model):
    name = models.CharField(max_length=64)

class City(models.Model):
    name = models.CharField(max_length=64)
    country = models.ForeignKey(Country)

# admin.py
from django.contrib.admin import ModelAdmin

class CityAdmin(ModelAdmin):
    list_filter = (CountryFilter,)

admin.site.register(City, CityAdmin)

In example you can see two models - City and Country. City has ForeignKey to Country. If you use regular list_filter = ('country',) you will have all the countries in the chooser. This snippet however filters only related countries - the ones that have at least one relation to city.

Original idea from here. Big thanks to author. Improved class names for better clarity and use of model_admin.model instead of hardcoded model name.

Example also available in Django Snippets: http://djangosnippets.org/snippets/2885/

Athalla answered 28/1, 2013 at 19:48 Comment(5)
This example is very close to what I'm looking for, but with one exception: I'm trying to add a list_filter for User objects by a field on the UserProfile model, named user_type. So I define a class, UserTypeFilter(SimpleListFilter): but I don't know what you put in the queryset function's return queryset.filter(<user_type?>=self.value()).Rademacher
Answer to my question is here: #19187527Rademacher
If you need a generalized reusable version of this great answer, please see my answer below: https://mcmap.net/q/245914/-can-i-make-list_filter-in-django-admin-to-only-show-referenced-foreignkeysCletacleti
In your lookups method you're looping through every City, which is very inefficient. Instead of converting the queryset to a set, you should just return something like: model_admin.model.objects.order_by('country__id').values_list('country__id', 'country__name').distinct()Keishakeisling
I is very old. It is not workingZephyr
F
27

Since Django 1.8 there is: admin.RelatedOnlyFieldListFilter

The example usage is:

class BookAdmin(admin.ModelAdmin):
    list_filter = (
        ('author', admin.RelatedOnlyFieldListFilter),
    )
Fiord answered 8/1, 2015 at 9:41 Comment(3)
1.8 has arrived and this feature is now availableMandrake
@Rrrrrrrrrk updated my answe making clear the current state ;)Fiord
for some reason, it seems this is not in geodjango admin, and pulling it over from regular admin does not seem to workPsychomotor
P
5

I’d change lookups in darklow's code like this:

def lookups(self, request, model_admin):
    users = User.objects.filter(id__in = model_admin.model.objects.all().values_list('user_id', flat = True).distinct())
    return [(user.id, unicode(user)) for user in users]

This is much better for database ;)

Pearlene answered 28/3, 2014 at 9:10 Comment(0)
C
2

A generalized reusable version of the great @darklow's answer:

def make_RelatedOnlyFieldListFilter(attr_name, filter_title):

    class RelatedOnlyFieldListFilter(admin.SimpleListFilter):
        """Filter that shows only referenced options, i.e. options having at least a single object."""
        title = filter_title
        parameter_name = attr_name

        def lookups(self, request, model_admin):
            related_objects = set([getattr(obj, attr_name) for obj in model_admin.model.objects.all()])
            return [(related_obj.id, unicode(related_obj)) for related_obj in related_objects]

        def queryset(self, request, queryset):
            if self.value():
                return queryset.filter(**{'%s__id__exact' % attr_name: self.value()})
            else:
                return queryset

    return RelatedOnlyFieldListFilter

Usage:

class CityAdmin(ModelAdmin):
    list_filter = (
        make_RelatedOnlyFieldListFilter("country", "Country with cities"),
    )
Cletacleti answered 7/4, 2015 at 20:55 Comment(0)
P
2

This is my take on a general and reusable implementation for Django 1.4, if you happen to be stuck at that version. It is inspired by the built-in version that is now part of Django 1.8 and upwards. Also, it should be quite a small task to adapt it to 1.5–1.7, mainly the queryset methods have changed name in those. I've put the filter itself in a core application that I have but you can obviously put it anywhere.

Implementation:

# myproject/core/admin/filters.py:

from django.contrib.admin.filters import RelatedFieldListFilter


class RelatedOnlyFieldListFilter(RelatedFieldListFilter):
    def __init__(self, field, request, params, model, model_admin, field_path):
        self.request = request
        self.model_admin = model_admin
        super(RelatedOnlyFieldListFilter, self).__init__(field, request, params, model, model_admin, field_path)

    def choices(self, cl):
        limit_choices_to = set(self.model_admin.queryset(self.request).values_list(self.field.name, flat=True))
        self.lookup_choices = [(pk_val, val) for pk_val, val in self.lookup_choices if pk_val in limit_choices_to]
        return super(RelatedOnlyFieldListFilter, self).choices(cl)

Usage:

# myapp/admin.py:

from django.contrib import admin
from myproject.core.admin.filters import RelatedOnlyFieldListFilter
from myproject.myapp.models import MyClass


class MyClassAdmin(admin.ModelAdmin):
    list_filter = (
        ('myfield', RelatedOnlyFieldListFilter),
    )

admin.site.register(MyClass, MyClassAdmin)

If you later update to Django 1.8 you should be able to just change this import:

from myproject.core.admin.filters import RelatedOnlyFieldListFilter

To this:

from django.contrib.admin.filters import RelatedOnlyFieldListFilter
Pennyroyal answered 7/5, 2015 at 7:41 Comment(0)
A
1

@andi, thanks for letting know about the fact that Django 1.8 will have this feature.

I took a look on how it was implemented and based on that created version that works for Django 1.7. This is a better implementation than my previous answer, because now you can reuse this filter with any Foreign Key fields. Tested only in Django 1.7, not sure if it works in earlier versions.

Here is my final solution:

from django.contrib.admin import RelatedFieldListFilter

class RelatedOnlyFieldListFilter(RelatedFieldListFilter):
    def __init__(self, field, request, params, model, model_admin, field_path):
        super(RelatedOnlyFieldListFilter, self).__init__(
            field, request, params, model, model_admin, field_path)
        qs = field.related_field.model.objects.filter(
            id__in=model_admin.get_queryset(request).values_list(
                field.name, flat=True).distinct())
        self.lookup_choices = [(each.id, unicode(each)) for each in qs]

Usage:

class MyAdmin(admin.ModelAdmin):
    list_filter = (
        ('user', RelatedOnlyFieldListFilter),
        ('category', RelatedOnlyFieldListFilter),
        # ...
    )
Athalla answered 24/3, 2015 at 13:57 Comment(2)
Unfortunately, does not work in Django 1.4 :( 'ForeignKey' object has no attribute 'related_field'Cletacleti
Worked fine on Django 1.6 :DDey

© 2022 - 2024 — McMap. All rights reserved.