From 9b432cb67bfc0648862cafb49868e670583050df Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Sun, 21 Nov 2010 02:28:25 +0000 Subject: [PATCH] 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 --- django/db/models/query.py | 2 +- docs/ref/models/querysets.txt | 27 ++++++++-- tests/modeltests/lookup/models.py | 12 +++++ tests/modeltests/lookup/tests.py | 83 +++++++++++++++++++++++++++---- 4 files changed, 110 insertions(+), 14 deletions(-) diff --git a/django/db/models/query.py b/django/db/models/query.py index 25e37cc701..9b19ec1caf 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -870,7 +870,7 @@ class ValuesQuerySet(QuerySet): self.query.select = [] if self.extra_names is not None: self.query.set_extra_mask(self.extra_names) - self.query.add_fields(self.field_names, False) + self.query.add_fields(self.field_names, True) if self.aggregate_names is not None: self.query.set_aggregate_mask(self.aggregate_names) diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index 5659579955..ccea5884ed 100644 --- a/docs/ref/models/querysets.txt +++ b/docs/ref/models/querysets.txt @@ -398,11 +398,8 @@ Example:: >>> Blog.objects.values('id', 'name') [{'id': 1, 'name': 'Beatles Blog'}] -A couple of subtleties that are worth mentioning: +A few subtleties that are worth mentioning: - * The ``values()`` method does not return anything for - :class:`~django.db.models.ManyToManyField` attributes and will raise an - error if you try to pass in this type of field to it. * If you have a field called ``foo`` that is a :class:`~django.db.models.ForeignKey`, the default ``values()`` call 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()``), but it doesn't really matter. This is your chance to really flaunt your individualism. +.. versionchanged:: 1.3 + +The ``values()`` method previously did not return anything for +:class:`~django.db.models.ManyToManyField` attributes and would raise an error +if you tried to pass this type of field to it. + +This restriction has been lifted, and you can now also refer to fields on +related models with reverse relations through ``OneToOneField``, ``ForeignKey`` +and ``ManyToManyField`` attributes:: + + Blog.objects.values('name', 'entry__headline') + [{'name': 'My blog', 'entry__headline': 'An entry'}, + {'name': 'My blog', 'entry__headline': 'Another entry'}, ...] + +.. warning:: + + Because :class:`~django.db.models.ManyToManyField` attributes and reverse + relations can have multiple related rows, including these can have a + multiplier effect on the size of your result set. This will be especially + pronounced if you include multiple such fields in your ``values()`` query, + in which case all possible combinations will be returned. + ``values_list(*fields)`` ~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/modeltests/lookup/models.py b/tests/modeltests/lookup/models.py index ac63210656..d807531850 100644 --- a/tests/modeltests/lookup/models.py +++ b/tests/modeltests/lookup/models.py @@ -7,11 +7,23 @@ This demonstrates features of the database API. from django.db import models, DEFAULT_DB_ALIAS, connection from django.conf import settings +class Author(models.Model): + name = models.CharField(max_length=100) + class Meta: + ordering = ('name', ) + class Article(models.Model): headline = models.CharField(max_length=100) pub_date = models.DateTimeField() + author = models.ForeignKey(Author, blank=True, null=True) class Meta: ordering = ('-pub_date', 'headline') def __unicode__(self): return self.headline + +class Tag(models.Model): + articles = models.ManyToManyField(Article) + name = models.CharField(max_length=100) + class Meta: + ordering = ('name', ) diff --git a/tests/modeltests/lookup/tests.py b/tests/modeltests/lookup/tests.py index 3273349faa..cf18a83945 100644 --- a/tests/modeltests/lookup/tests.py +++ b/tests/modeltests/lookup/tests.py @@ -3,28 +3,43 @@ from operator import attrgetter from django.core.exceptions import FieldError from django.db import connection from django.test import TestCase, skipUnlessDBFeature -from models import Article +from models import Author, Article, Tag class LookupTests(TestCase): #def setUp(self): def setUp(self): + # Create a few Authors. + self.au1 = Author(name='Author 1') + self.au1.save() + self.au2 = Author(name='Author 2') + self.au2.save() # Create a couple of Articles. - self.a1 = Article(headline='Article 1', pub_date=datetime(2005, 7, 26)) + self.a1 = Article(headline='Article 1', pub_date=datetime(2005, 7, 26), author=self.au1) self.a1.save() - self.a2 = Article(headline='Article 2', pub_date=datetime(2005, 7, 27)) + self.a2 = Article(headline='Article 2', pub_date=datetime(2005, 7, 27), author=self.au1) self.a2.save() - self.a3 = Article(headline='Article 3', pub_date=datetime(2005, 7, 27)) + self.a3 = Article(headline='Article 3', pub_date=datetime(2005, 7, 27), author=self.au1) self.a3.save() - self.a4 = Article(headline='Article 4', pub_date=datetime(2005, 7, 28)) + self.a4 = Article(headline='Article 4', pub_date=datetime(2005, 7, 28), author=self.au1) self.a4.save() - self.a5 = Article(headline='Article 5', pub_date=datetime(2005, 8, 1, 9, 0)) + self.a5 = Article(headline='Article 5', pub_date=datetime(2005, 8, 1, 9, 0), author=self.au2) self.a5.save() - self.a6 = Article(headline='Article 6', pub_date=datetime(2005, 8, 1, 8, 0)) + self.a6 = Article(headline='Article 6', pub_date=datetime(2005, 8, 1, 8, 0), author=self.au2) self.a6.save() - self.a7 = Article(headline='Article 7', pub_date=datetime(2005, 7, 27)) + self.a7 = Article(headline='Article 7', pub_date=datetime(2005, 7, 27), author=self.au2) self.a7.save() + # Create a few Tags. + self.t1 = Tag(name='Tag 1') + self.t1.save() + self.t1.articles.add(self.a1, self.a2, self.a3) + self.t2 = Tag(name='Tag 2') + self.t2.save() + self.t2.articles.add(self.a3, self.a4, self.a5) + self.t3 = Tag(name='Tag 3') + self.t3.save() + self.t3.articles.add(self.a5, self.a6, self.a7) def test_exists(self): # We can use .exists() to check that there are some @@ -182,6 +197,42 @@ class LookupTests(TestCase): 'id_plus_seven': self.a1.id + 7, 'id_plus_eight': self.a1.id + 8, }], transform=identity) + # You can specify fields from forward and reverse relations, just like filter(). + self.assertQuerysetEqual( + Article.objects.values('headline', 'author__name'), + [ + {'headline': self.a5.headline, 'author__name': self.au2.name}, + {'headline': self.a6.headline, 'author__name': self.au2.name}, + {'headline': self.a4.headline, 'author__name': self.au1.name}, + {'headline': self.a2.headline, 'author__name': self.au1.name}, + {'headline': self.a3.headline, 'author__name': self.au1.name}, + {'headline': self.a7.headline, 'author__name': self.au2.name}, + {'headline': self.a1.headline, 'author__name': self.au1.name}, + ], transform=identity) + self.assertQuerysetEqual( + Author.objects.values('name', 'article__headline').order_by('name', 'article__headline'), + [ + {'name': self.au1.name, 'article__headline': self.a1.headline}, + {'name': self.au1.name, 'article__headline': self.a2.headline}, + {'name': self.au1.name, 'article__headline': self.a3.headline}, + {'name': self.au1.name, 'article__headline': self.a4.headline}, + {'name': self.au2.name, 'article__headline': self.a5.headline}, + {'name': self.au2.name, 'article__headline': self.a6.headline}, + {'name': self.au2.name, 'article__headline': self.a7.headline}, + ], transform=identity) + self.assertQuerysetEqual( + Author.objects.values('name', 'article__headline', 'article__tag__name').order_by('name', 'article__headline', 'article__tag__name'), + [ + {'name': self.au1.name, 'article__headline': self.a1.headline, 'article__tag__name': self.t1.name}, + {'name': self.au1.name, 'article__headline': self.a2.headline, 'article__tag__name': self.t1.name}, + {'name': self.au1.name, 'article__headline': self.a3.headline, 'article__tag__name': self.t1.name}, + {'name': self.au1.name, 'article__headline': self.a3.headline, 'article__tag__name': self.t2.name}, + {'name': self.au1.name, 'article__headline': self.a4.headline, 'article__tag__name': self.t2.name}, + {'name': self.au2.name, 'article__headline': self.a5.headline, 'article__tag__name': self.t2.name}, + {'name': self.au2.name, 'article__headline': self.a5.headline, 'article__tag__name': self.t3.name}, + {'name': self.au2.name, 'article__headline': self.a6.headline, 'article__tag__name': self.t3.name}, + {'name': self.au2.name, 'article__headline': self.a7.headline, 'article__tag__name': self.t3.name}, + ], transform=identity) # However, an exception FieldDoesNotExist will be thrown if you specify # a non-existent field name in values() (a field that is neither in the # model nor in extra(select)). @@ -192,6 +243,7 @@ class LookupTests(TestCase): self.assertQuerysetEqual(Article.objects.filter(id=self.a5.id).values(), [{ 'id': self.a5.id, + 'author_id': self.au2.id, 'headline': 'Article 5', 'pub_date': datetime(2005, 8, 1, 9, 0) }], transform=identity) @@ -250,6 +302,19 @@ class LookupTests(TestCase): (self.a7.id, self.a7.id+1) ], transform=identity) + self.assertQuerysetEqual( + Author.objects.values_list('name', 'article__headline', 'article__tag__name').order_by('name', 'article__headline', 'article__tag__name'), + [ + (self.au1.name, self.a1.headline, self.t1.name), + (self.au1.name, self.a2.headline, self.t1.name), + (self.au1.name, self.a3.headline, self.t1.name), + (self.au1.name, self.a3.headline, self.t2.name), + (self.au1.name, self.a4.headline, self.t2.name), + (self.au2.name, self.a5.headline, self.t2.name), + (self.au2.name, self.a5.headline, self.t3.name), + (self.au2.name, self.a6.headline, self.t3.name), + (self.au2.name, self.a7.headline, self.t3.name), + ], transform=identity) self.assertRaises(TypeError, Article.objects.values_list, 'id', 'headline', flat=True) def test_get_next_previous_by(self): @@ -402,7 +467,7 @@ class LookupTests(TestCase): self.fail('FieldError not raised') except FieldError, ex: self.assertEqual(str(ex), "Cannot resolve keyword 'pub_date_year' " - "into field. Choices are: headline, id, pub_date") + "into field. Choices are: author, headline, id, pub_date, tag") try: Article.objects.filter(headline__starts='Article') self.fail('FieldError not raised')