Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Fixed #5768 -- Added support for ManyToManyFields and reverse relatio…

…ns 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
  • Loading branch information...
commit 9b432cb67bfc0648862cafb49868e670583050df 1 parent 7592d68
@carljm carljm authored
View
2  django/db/models/query.py
@@ -870,7 +870,7 @@ def _setup_query(self):
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)
View
27 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)``
~~~~~~~~~~~~~~~~~~~~~~~~
View
12 tests/modeltests/lookup/models.py
@@ -7,11 +7,23 @@
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', )
View
83 tests/modeltests/lookup/tests.py
@@ -3,28 +3,43 @@
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 @@ def test_values(self):
'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 @@ def test_values(self):
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 @@ def test_values_list(self):
(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 @@ def test_error_messages(self):
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')

0 comments on commit 9b432cb

Please sign in to comment.
Something went wrong with that request. Please try again.