- 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
- Template tag - list punctuation for a list of items by shapiromatron 9 months ago
- JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 9 months, 1 week ago
- Serializer factory with Django Rest Framework by julio 1 year, 3 months ago
- Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 4 months ago
- Help text hyperlinks by sa2812 1 year, 5 months ago
Comments
Please login first before commenting.