Login

Changelist filter by ForeignKey

Author:
overclocked
Posted:
November 12, 2010
Language:
Python
Version:
1.2
Score:
1 (after 1 ratings)

Want to filter by properties on FK fields? Want to display as filters only FK objects that matter to your model? See comments in the code.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
from django.contrib.admin.filterspecs import RelatedFilterSpec

class FKFilterSpec(RelatedFilterSpec):
    """
    This code allows you to add a filter on a property of a foreign
    key related model to your model's Django Admin changelist. E.g. if
    you have a model called Book, with a field "type", and you have
    another model called Author, with a FK field "book" to Book, then
    you can add a filter on the Author changelist, by book type.

    Put the code in a file and import it in urls.py, so the filter is
    registered.

    In your app's admin.py, define your model's ModelAdmin class, and
    specify the field in the related model you want to filter.

    For example, using the Book and Author models, you need to define

    class AuthorAdmin(admin.ModelAdmin):
        ....
        book_fk_filter_by = 'type'
        book_fk_filter_name = 'type'
        ...
        list_filter = (..., 'book', ...)
        ...
    
    The <fieldname>_fk_filter_by attribute in ModelAdmin specifies the
    related model property that you want to filter on. The
    <fieldname>_fk_filter_name attribute specifies what is shown on

    the filter sidebar.

    You can also follow more than one relationship. E.g.:

        book_fk_filter_by = 'type__id'
        book_fk_filter_display_field = 'type__type'
        book_fk_filter_name = 'type'

    Here I am assuming from Book model, there is a field 'type' that's
    a FK to a Type model, and the Type model has a field 'id' and a
    field 'type'.

    <fieldname>_fk_filter_display_field specifies which field contains
    values that you want to show on the filter list. For example, as
    shown above, it is a good practice to filter by Type.id in the URL
    and SQL query, but list Type.type in the UI.

    Note that you don't have to change the model to specify the filter
    using a field property. I always find it weird that for something
    that's Django Admin specific, you have to include a line in the
    model to specify the filter.

    FKFilterSpec subclasses from RelatedFilterSpec, and in fact
    replaces RelatedFilterSpec (it's registered ahead of
    RelatedFilterSpec). If you just want to have the default filter by
    a foreign key field (which filters by FK object, not by a property
    on the FK related model), FKFilterSpec still provides that
    behavior.

    When filtering by foreign key field, FKFilterSpec can also limit
    the filtering options (i.e. what is listed in the filter UI) to

    only those FK objects that are related to your model. This works
    for all FK relationships, but not generic relationships from
    Django ContentType package.

    For example, if you have an Author model with a FK to Institution
    model, you can configure Author's changelist to include a filter
    on Institution, but only allow you to select institutions that
    have authors. Institutions that do not have authors won't show up
    on the list.

    To enable this, in your model's ModelAdmin class, set

        <fieldname>_fk_filter_related_only=True
        <fieldname>_fk_filter_display_field=<display this field of the related m
odel in filter list>

    For example, in your AuthorAdmin class, you can do

        institution_fk_filter_related_only=True
        institution_fk_filter_display_field='name'

    You can also further limit the filters, based on certain
    criterias. E.g.:

        def more_filter(self,queryset):
            return queryset.filter(institution__country="US")
        institution_fk_filter_criteria_fn=more_filter

    Note that if _fk_filter_related_only is NOT set to True OR if the
    relationship is a generic relationship from Django ContentType,
    then the function filters a queryset on the model of the FK field.

    Otherwise, if _fk_filter_related_only is True, then the function
    filters a queryset on your model, not the model of the FK field.
    So how define the filter function depends on the
    _fk_filter_related_only setting.

    If you use _fk_filter_criteria_fn, you mostly likely will want to
    augment the query string used in the URLs in the filters, so that
    when you click on a filter option, the correct criteria are all
    present. By default, Django Admin only adds the _pk criteria. For
    example, using the Author and Book models, you can define the
    following in AuthorAdmin:

        book_fk_filter_by = 'type__id'
        book_fk_filter_display_field = 'type__type'
        book_fk_filter_name = 'type'
        def filter_published(self,queryset):
            return queryset.filter(book__published=1)
        book_fk_filter_criteria_fn=filter_published
        book_fk_filter_query_string={ 'book__published' : 1 }

    If you don't specify the _fk_filter_query_string option, then when
    user clicks on a book type, say "Science Fiction", they will see
    all authors of that category, including those authors from
    unpublished books, even though you can limited the types to only
    types of published books.


    """

    def __init__(self, f, request, params, model, model_admin):
        filter_by_key = f.name+'_fk_filter_by'
        filter_by_val = getattr(model_admin, filter_by_key, None)
            
        filter_related_key = f.name+'_fk_filter_related_only'
        filter_related_val = getattr(model_admin, filter_related_key, False)

        filter_nf_key = f.name+'_fk_filter_display_field'
        if filter_by_val != None:
            filter_nf_val = getattr(model_admin, filter_nf_key, filter_by_val)
        else:
            filter_nf_val = getattr(model_admin, filter_nf_key, 'pk')

        filter_crit_key = f.name+'_fk_filter_criteria_fn'
        filter_crit_fn = getattr(model_admin, filter_crit_key, None)

        filter_qs_key = f.name+'_fk_filter_query_string'
        self.filter_qs_val = getattr(model_admin, filter_qs_key, {})

        if filter_by_val != None:
            self.fk_filter_on = True
            # we call FilterSpec constructor, not RelatedFilterSpec
            # constructor; RelatedFilterSpec constructor will try to
            # get all the pk values on the related models, which we
            # won't need.
            FilterSpec.__init__(self, f, request, params, model, model_admin)

            filter_name_key = f.name+'_fk_filter_name'
            filter_name_val = getattr(model_admin, filter_name_key, None)

            if filter_name_val == None:

                self.lookup_title = f.verbose_name
            else:
                self.lookup_title = f.verbose_name+' '+filter_name_val

            self.lookup_kwarg = f.name+'__'+filter_by_val+'__exact'
            self.lookup_val = request.GET.get(self.lookup_kwarg, None)

            if filter_related_val:
                try:
                    qs = model_admin.queryset(request)
                    if filter_crit_fn != None:
                        qs = filter_crit_fn(qs)
                    qs = qs.values_list(f.name+'__'+filter_by_val,f.name+'__'+fi
lter_nf_val).order_by(f.name+'__'+filter_nf_val).distinct()
                except Exception as e:
                    # Django QuerySet can't follow generic
                    # relationships using __, so we have to use
                    # f.rel.to.objects
                    qs = f.rel.to.objects
                    if filter_crit_fn != None:
                        qs = filter_crit_fn(qs)
                    qs = qs.values_list(filter_by_val, filter_nf_val).distinct()
            else:
                qs = f.rel.to.objects
                if filter_crit_fn != None:
                    qs = filter_crit_fn(qs)
                qs = qs.values_list(filter_by_val, filter_nf_val).distinct()

            self.lookup_choices = list(qs)
            # if there was a further limiting criteria, then we want
            # to make sure we still display the filter even if there

            # is only one option
            if filter_crit_fn != None and len(self.lookup_choices) == 1:
                self.lookup_choices = self.lookup_choices+[('',''),]

        else:
            self.fk_filter_on = False
            RelatedFilterSpec.__init__(self, f, request, params, model, model_ad
min)

            if filter_related_val:
                qs = model_admin.queryset(request)
                if filter_crit_fn != None:
                    qs = filter_crit_fn(qs)
                qs = qs.values_list(f.name+'__pk',f.name+'__'+filter_nf_val).ord
er_by(f.name+'__'+filter_nf_val).distinct()
                self.lookup_choices = list(qs)

    def choices(self, cl):
        qs_all = self.filter_qs_val.keys()
        qs_all.append(self.lookup_kwarg)
        yield {'selected': self.lookup_val is None,
               'query_string': cl.get_query_string({}, qs_all),
               'display': _('All')}
        for pk_val,val in self.lookup_choices:
            qs = self.filter_qs_val
            qs[self.lookup_kwarg] = pk_val
            yield {'selected': self.lookup_val == smart_unicode(pk_val),
                   'query_string': cl.get_query_string(qs),
                   'display': val}

FilterSpec.filter_specs.insert(0, (lambda f: bool(f.rel), FKFilterSpec))

More like this

  1. Template tag - list punctuation for a list of items by shapiromatron 2 months, 2 weeks ago
  2. JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 2 months, 3 weeks ago
  3. Serializer factory with Django Rest Framework by julio 9 months, 2 weeks ago
  4. Image compression before saving the new model / work with JPG, PNG by Schleidens 10 months, 1 week ago
  5. Help text hyperlinks by sa2812 11 months ago

Comments

Please login first before commenting.