Skip to content

Commit b7c8574

Browse files
committed
Added support for filtering on related model fields. Thanks to Yuri Baburov
1 parent 88ca142 commit b7c8574

File tree

6 files changed

+63
-12
lines changed

6 files changed

+63
-12
lines changed

django_filters/filterset.py

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
from django import forms
44
from django.db import models
5+
from django.db.models.fields import FieldDoesNotExist
6+
from django.db.models.related import RelatedObject
7+
from django.db.models.sql.constants import LOOKUP_SEP
58
from django.utils.datastructures import SortedDict
69
from django.utils.text import capfirst
710

@@ -32,21 +35,43 @@ def get_declared_filters(bases, attrs, with_base_filters=True):
3235

3336
return SortedDict(filters)
3437

38+
def get_model_field(model, f):
39+
parts = f.split(LOOKUP_SEP)
40+
opts = model._meta
41+
for name in parts[:-1]:
42+
try:
43+
rel = opts.get_field_by_name(name)[0]
44+
except FieldDoesNotExist:
45+
return None
46+
if isinstance(rel, RelatedObject):
47+
model = rel.model
48+
opts = rel.opts
49+
else:
50+
model = rel.rel.to
51+
opts = model._meta
52+
try:
53+
rel, model, direct, m2m = opts.get_field_by_name(parts[-1])
54+
except FieldDoesNotExist:
55+
return None
56+
if not direct:
57+
return rel.field.rel.to_field
58+
return rel
59+
3560
def filters_for_model(model, fields=None, exclude=None, filter_for_field=None):
36-
field_list = []
61+
field_dict = SortedDict()
3762
opts = model._meta
38-
for f in sorted(opts.fields + opts.many_to_many):
39-
if fields is not None and f.name not in fields:
63+
if fields is None:
64+
fields = [f.name for f in sorted(opts.fields + opts.many_to_many)]
65+
for f in fields:
66+
if exclude is not None and f in exclude:
4067
continue
41-
if exclude is not None and f.name in exclude:
68+
field = get_model_field(model, f)
69+
if field is None:
70+
field_dict[f] = None
4271
continue
43-
filter_ = filter_for_field(f, f.name)
72+
filter_ = filter_for_field(field, f)
4473
if filter_:
45-
field_list.append((f.name, filter_))
46-
field_dict = SortedDict(field_list)
47-
if fields:
48-
field_dict = SortedDict([(f, field_dict.get(f)) for f in fields if
49-
(not exclude) or (exclude and f not in exclude)])
74+
field_dict[f] = filter_
5075
return field_dict
5176

5277
class FilterSetOptions(object):

django_filters/tests/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from tests import (GenericViewTests, InheritanceTest, ModelInheritanceTest,
22
DateRangeFilterTest, FilterSetForm, AllValuesFilterTest, InitialValueTest,
3-
filter_tests)
3+
RelatedObjectTest, filter_tests)
44

55
__test__ = {
66
'filter_tests': filter_tests,
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
[{"pk": 1, "model": "tests.user", "fields": {"username": "alex", "status": 1, "first_name": "", "last_name": "", "favorite_books": [1, 2], "is_active": false}}, {"pk": 2, "model": "tests.user", "fields": {"username": "aaron", "status": 0, "first_name": "", "last_name": "", "favorite_books": [1, 3], "is_active": false}}, {"pk": 3, "model": "tests.user", "fields": {"username": "jacob", "status": 0, "first_name": "", "last_name": "", "favorite_books": [], "is_active": true}}, {"pk": 1, "model": "tests.comment", "fields": {"date": "2009-01-30", "text": "super awesome!", "time": "03:04:05", "author": 1}}, {"pk": 2, "model": "tests.comment", "fields": {"date": "2009-01-27", "text": "psycadelic!", "time": "05:04:03", "author": 2}}, {"pk": 3, "model": "tests.comment", "fields": {"date": "2008-12-31", "text": "funky fresh!", "time": "12:55:00", "author": 3}}, {"pk": 1, "model": "tests.book", "fields": {"average_rating": 4.7999999999999998, "price": "10", "title": "Ender's Game"}}, {"pk": 2, "model": "tests.book", "fields": {"average_rating": 4.5999999999999996, "price": "15", "title": "Rainbox Six"}}, {"pk": 3, "model": "tests.book", "fields": {"average_rating": 4.2999999999999998, "price": "20", "title": "Snowcrash"}}]
1+
[{"pk": 1, "model": "tests.user", "fields": {"username": "alex", "status": 1, "first_name": "", "last_name": "", "favorite_books": [1, 2], "is_active": false}}, {"pk": 2, "model": "tests.user", "fields": {"username": "aaron", "status": 0, "first_name": "", "last_name": "", "favorite_books": [1, 3], "is_active": false}}, {"pk": 3, "model": "tests.user", "fields": {"username": "jacob", "status": 0, "first_name": "", "last_name": "", "favorite_books": [], "is_active": true}}, {"pk": 1, "model": "tests.comment", "fields": {"date": "2009-01-30", "text": "super awesome!", "time": "03:04:05", "author": 1}}, {"pk": 2, "model": "tests.comment", "fields": {"date": "2009-01-27", "text": "psycadelic!", "time": "05:04:03", "author": 2}}, {"pk": 3, "model": "tests.comment", "fields": {"date": "2008-12-31", "text": "funky fresh!", "time": "12:55:00", "author": 3}}, {"pk": 1, "model": "tests.article", "fields": {"author": 1, "published": "2009-07-08 00:00:00"}}, {"pk": 2, "model": "tests.article", "fields": {"author": 1, "published": "2009-08-08 00:00:00"}}, {"pk": 3, "model": "tests.article", "fields": {"author": 3, "published": "2009-09-03 00:00:00"}}, {"pk": 1, "model": "tests.book", "fields": {"average_rating": 4.7999999999999998, "price": "10", "title": "Ender's Game"}}, {"pk": 2, "model": "tests.book", "fields": {"average_rating": 4.5999999999999996, "price": "15", "title": "Rainbox Six"}}, {"pk": 3, "model": "tests.book", "fields": {"average_rating": 4.2999999999999998, "price": "20", "title": "Snowcrash"}}]

django_filters/tests/models.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from django.db import models
44

5+
56
STATUS_CHOICES = (
67
(0, 'Regular'),
78
(1, 'Admin'),
@@ -31,8 +32,11 @@ class Comment(models.Model):
3132
def __unicode__(self):
3233
return "%s said %s" % (self.author, self.text[:25])
3334

35+
3436
class Article(models.Model):
3537
published = models.DateTimeField()
38+
author = models.ForeignKey(User, null=True)
39+
3640

3741
class Book(models.Model):
3842
title = models.CharField(max_length=100)

django_filters/tests/tests.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,24 @@ class Meta:
103103
self.assertEqual(list(F().qs), [User.objects.get(username='alex')])
104104
self.assertEqual(list(F({'status': 0})), list(User.objects.filter(status=0)))
105105

106+
107+
class RelatedObjectTest(TestCase):
108+
fixtures = ['test_data']
109+
110+
def test_foreignkey(self):
111+
class F(django_filters.FilterSet):
112+
class Meta:
113+
model = Article
114+
fields = ['author__username']
115+
self.assertEqual(F.base_filters.keys(), ['author__username'])
116+
form_html = ('<tr><th><label for="id_author__username">Username:</label>'
117+
'</th><td><input type="text" name="author__username" '
118+
'id="id_author__username" /></td></tr>')
119+
self.assertEqual(str(F().form), form_html)
120+
self.assertEqual(F({'author__username': 'alex'}).qs.count(), 2)
121+
self.assertEqual(F({'author__username': 'jacob'}).qs.count(), 1)
122+
123+
106124
filter_tests = """
107125
>>> from datetime import datetime
108126
>>> from django import forms

docs/usage.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,10 @@ The inner ``Meta`` class also takes an optional ``form`` argument. This is a
9595
form class from which ``FilterSet.form`` will subclass. This works similar to
9696
the ``form`` option on a ``ModelAdmin.``
9797

98+
Items in the ``fields`` sequence in the ``Meta`` class may include
99+
"relationship paths" using Django's ``__`` syntax to filter on fields on a
100+
related model.
101+
98102
If you want to use a custom widget, or in any other way overide the ordering
99103
field you can overide the ``get_ordering_field()`` method on a ``FilterSet``.
100104
This method just needs to return a Form Field.

0 commit comments

Comments
 (0)