Skip to content

Commit 9b432cb

Browse files
committed
Fixed #5768 -- Added support for ManyToManyFields and reverse relations in values() and values_list(). Thanks to mrmachine for the patch.
git-svn-id: http://code.djangoproject.com/svn/django/trunk@14655 bcc190cf-cafb-0310-a4f2-bffc1f526a37
1 parent 7592d68 commit 9b432cb

File tree

4 files changed

+110
-14
lines changed

4 files changed

+110
-14
lines changed

django/db/models/query.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -870,7 +870,7 @@ def _setup_query(self):
870870
self.query.select = []
871871
if self.extra_names is not None:
872872
self.query.set_extra_mask(self.extra_names)
873-
self.query.add_fields(self.field_names, False)
873+
self.query.add_fields(self.field_names, True)
874874
if self.aggregate_names is not None:
875875
self.query.set_aggregate_mask(self.aggregate_names)
876876

docs/ref/models/querysets.txt

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -398,11 +398,8 @@ Example::
398398
>>> Blog.objects.values('id', 'name')
399399
[{'id': 1, 'name': 'Beatles Blog'}]
400400

401-
A couple of subtleties that are worth mentioning:
401+
A few subtleties that are worth mentioning:
402402

403-
* The ``values()`` method does not return anything for
404-
:class:`~django.db.models.ManyToManyField` attributes and will raise an
405-
error if you try to pass in this type of field to it.
406403
* If you have a field called ``foo`` that is a
407404
:class:`~django.db.models.ForeignKey`, the default ``values()`` call
408405
will return a dictionary key called ``foo_id``, since this is the name
@@ -453,6 +450,28 @@ followed (optionally) by any output-affecting methods (such as ``values()``),
453450
but it doesn't really matter. This is your chance to really flaunt your
454451
individualism.
455452

453+
.. versionchanged:: 1.3
454+
455+
The ``values()`` method previously did not return anything for
456+
:class:`~django.db.models.ManyToManyField` attributes and would raise an error
457+
if you tried to pass this type of field to it.
458+
459+
This restriction has been lifted, and you can now also refer to fields on
460+
related models with reverse relations through ``OneToOneField``, ``ForeignKey``
461+
and ``ManyToManyField`` attributes::
462+
463+
Blog.objects.values('name', 'entry__headline')
464+
[{'name': 'My blog', 'entry__headline': 'An entry'},
465+
{'name': 'My blog', 'entry__headline': 'Another entry'}, ...]
466+
467+
.. warning::
468+
469+
Because :class:`~django.db.models.ManyToManyField` attributes and reverse
470+
relations can have multiple related rows, including these can have a
471+
multiplier effect on the size of your result set. This will be especially
472+
pronounced if you include multiple such fields in your ``values()`` query,
473+
in which case all possible combinations will be returned.
474+
456475
``values_list(*fields)``
457476
~~~~~~~~~~~~~~~~~~~~~~~~
458477

tests/modeltests/lookup/models.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,23 @@
77
from django.db import models, DEFAULT_DB_ALIAS, connection
88
from django.conf import settings
99

10+
class Author(models.Model):
11+
name = models.CharField(max_length=100)
12+
class Meta:
13+
ordering = ('name', )
14+
1015
class Article(models.Model):
1116
headline = models.CharField(max_length=100)
1217
pub_date = models.DateTimeField()
18+
author = models.ForeignKey(Author, blank=True, null=True)
1319
class Meta:
1420
ordering = ('-pub_date', 'headline')
1521

1622
def __unicode__(self):
1723
return self.headline
24+
25+
class Tag(models.Model):
26+
articles = models.ManyToManyField(Article)
27+
name = models.CharField(max_length=100)
28+
class Meta:
29+
ordering = ('name', )

tests/modeltests/lookup/tests.py

Lines changed: 74 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,43 @@
33
from django.core.exceptions import FieldError
44
from django.db import connection
55
from django.test import TestCase, skipUnlessDBFeature
6-
from models import Article
6+
from models import Author, Article, Tag
77

88

99
class LookupTests(TestCase):
1010

1111
#def setUp(self):
1212
def setUp(self):
13+
# Create a few Authors.
14+
self.au1 = Author(name='Author 1')
15+
self.au1.save()
16+
self.au2 = Author(name='Author 2')
17+
self.au2.save()
1318
# Create a couple of Articles.
14-
self.a1 = Article(headline='Article 1', pub_date=datetime(2005, 7, 26))
19+
self.a1 = Article(headline='Article 1', pub_date=datetime(2005, 7, 26), author=self.au1)
1520
self.a1.save()
16-
self.a2 = Article(headline='Article 2', pub_date=datetime(2005, 7, 27))
21+
self.a2 = Article(headline='Article 2', pub_date=datetime(2005, 7, 27), author=self.au1)
1722
self.a2.save()
18-
self.a3 = Article(headline='Article 3', pub_date=datetime(2005, 7, 27))
23+
self.a3 = Article(headline='Article 3', pub_date=datetime(2005, 7, 27), author=self.au1)
1924
self.a3.save()
20-
self.a4 = Article(headline='Article 4', pub_date=datetime(2005, 7, 28))
25+
self.a4 = Article(headline='Article 4', pub_date=datetime(2005, 7, 28), author=self.au1)
2126
self.a4.save()
22-
self.a5 = Article(headline='Article 5', pub_date=datetime(2005, 8, 1, 9, 0))
27+
self.a5 = Article(headline='Article 5', pub_date=datetime(2005, 8, 1, 9, 0), author=self.au2)
2328
self.a5.save()
24-
self.a6 = Article(headline='Article 6', pub_date=datetime(2005, 8, 1, 8, 0))
29+
self.a6 = Article(headline='Article 6', pub_date=datetime(2005, 8, 1, 8, 0), author=self.au2)
2530
self.a6.save()
26-
self.a7 = Article(headline='Article 7', pub_date=datetime(2005, 7, 27))
31+
self.a7 = Article(headline='Article 7', pub_date=datetime(2005, 7, 27), author=self.au2)
2732
self.a7.save()
33+
# Create a few Tags.
34+
self.t1 = Tag(name='Tag 1')
35+
self.t1.save()
36+
self.t1.articles.add(self.a1, self.a2, self.a3)
37+
self.t2 = Tag(name='Tag 2')
38+
self.t2.save()
39+
self.t2.articles.add(self.a3, self.a4, self.a5)
40+
self.t3 = Tag(name='Tag 3')
41+
self.t3.save()
42+
self.t3.articles.add(self.a5, self.a6, self.a7)
2843

2944
def test_exists(self):
3045
# We can use .exists() to check that there are some
@@ -182,6 +197,42 @@ def test_values(self):
182197
'id_plus_seven': self.a1.id + 7,
183198
'id_plus_eight': self.a1.id + 8,
184199
}], transform=identity)
200+
# You can specify fields from forward and reverse relations, just like filter().
201+
self.assertQuerysetEqual(
202+
Article.objects.values('headline', 'author__name'),
203+
[
204+
{'headline': self.a5.headline, 'author__name': self.au2.name},
205+
{'headline': self.a6.headline, 'author__name': self.au2.name},
206+
{'headline': self.a4.headline, 'author__name': self.au1.name},
207+
{'headline': self.a2.headline, 'author__name': self.au1.name},
208+
{'headline': self.a3.headline, 'author__name': self.au1.name},
209+
{'headline': self.a7.headline, 'author__name': self.au2.name},
210+
{'headline': self.a1.headline, 'author__name': self.au1.name},
211+
], transform=identity)
212+
self.assertQuerysetEqual(
213+
Author.objects.values('name', 'article__headline').order_by('name', 'article__headline'),
214+
[
215+
{'name': self.au1.name, 'article__headline': self.a1.headline},
216+
{'name': self.au1.name, 'article__headline': self.a2.headline},
217+
{'name': self.au1.name, 'article__headline': self.a3.headline},
218+
{'name': self.au1.name, 'article__headline': self.a4.headline},
219+
{'name': self.au2.name, 'article__headline': self.a5.headline},
220+
{'name': self.au2.name, 'article__headline': self.a6.headline},
221+
{'name': self.au2.name, 'article__headline': self.a7.headline},
222+
], transform=identity)
223+
self.assertQuerysetEqual(
224+
Author.objects.values('name', 'article__headline', 'article__tag__name').order_by('name', 'article__headline', 'article__tag__name'),
225+
[
226+
{'name': self.au1.name, 'article__headline': self.a1.headline, 'article__tag__name': self.t1.name},
227+
{'name': self.au1.name, 'article__headline': self.a2.headline, 'article__tag__name': self.t1.name},
228+
{'name': self.au1.name, 'article__headline': self.a3.headline, 'article__tag__name': self.t1.name},
229+
{'name': self.au1.name, 'article__headline': self.a3.headline, 'article__tag__name': self.t2.name},
230+
{'name': self.au1.name, 'article__headline': self.a4.headline, 'article__tag__name': self.t2.name},
231+
{'name': self.au2.name, 'article__headline': self.a5.headline, 'article__tag__name': self.t2.name},
232+
{'name': self.au2.name, 'article__headline': self.a5.headline, 'article__tag__name': self.t3.name},
233+
{'name': self.au2.name, 'article__headline': self.a6.headline, 'article__tag__name': self.t3.name},
234+
{'name': self.au2.name, 'article__headline': self.a7.headline, 'article__tag__name': self.t3.name},
235+
], transform=identity)
185236
# However, an exception FieldDoesNotExist will be thrown if you specify
186237
# a non-existent field name in values() (a field that is neither in the
187238
# model nor in extra(select)).
@@ -192,6 +243,7 @@ def test_values(self):
192243
self.assertQuerysetEqual(Article.objects.filter(id=self.a5.id).values(),
193244
[{
194245
'id': self.a5.id,
246+
'author_id': self.au2.id,
195247
'headline': 'Article 5',
196248
'pub_date': datetime(2005, 8, 1, 9, 0)
197249
}], transform=identity)
@@ -250,6 +302,19 @@ def test_values_list(self):
250302
(self.a7.id, self.a7.id+1)
251303
],
252304
transform=identity)
305+
self.assertQuerysetEqual(
306+
Author.objects.values_list('name', 'article__headline', 'article__tag__name').order_by('name', 'article__headline', 'article__tag__name'),
307+
[
308+
(self.au1.name, self.a1.headline, self.t1.name),
309+
(self.au1.name, self.a2.headline, self.t1.name),
310+
(self.au1.name, self.a3.headline, self.t1.name),
311+
(self.au1.name, self.a3.headline, self.t2.name),
312+
(self.au1.name, self.a4.headline, self.t2.name),
313+
(self.au2.name, self.a5.headline, self.t2.name),
314+
(self.au2.name, self.a5.headline, self.t3.name),
315+
(self.au2.name, self.a6.headline, self.t3.name),
316+
(self.au2.name, self.a7.headline, self.t3.name),
317+
], transform=identity)
253318
self.assertRaises(TypeError, Article.objects.values_list, 'id', 'headline', flat=True)
254319

255320
def test_get_next_previous_by(self):
@@ -402,7 +467,7 @@ def test_error_messages(self):
402467
self.fail('FieldError not raised')
403468
except FieldError, ex:
404469
self.assertEqual(str(ex), "Cannot resolve keyword 'pub_date_year' "
405-
"into field. Choices are: headline, id, pub_date")
470+
"into field. Choices are: author, headline, id, pub_date, tag")
406471
try:
407472
Article.objects.filter(headline__starts='Article')
408473
self.fail('FieldError not raised')

0 commit comments

Comments
 (0)