Commit 9b432cb6 authored by Carl Meyer's avatar Carl Meyer
Browse files

Fixed #5768 -- Added support for ManyToManyFields and reverse relations in...

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
parent 7592d685
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -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)

+23 −4
Original line number Diff line number Diff line
@@ -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)``
~~~~~~~~~~~~~~~~~~~~~~~~

+12 −0
Original line number Diff line number Diff line
@@ -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', )
+74 −9
Original line number Diff line number Diff line
@@ -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')