Fixed #25871 -- Added expressions support to QuerySet.values().

This commit is contained in:
Ian Foote 2016-08-15 11:35:12 +10:00 committed by Tim Graham
parent d4eefc7e2a
commit 39f35d4b9d
4 changed files with 115 additions and 7 deletions

View File

@ -678,14 +678,17 @@ class QuerySet(object):
using = self.db using = self.db
return RawQuerySet(raw_query, model=self.model, params=params, translations=translations, using=using) return RawQuerySet(raw_query, model=self.model, params=params, translations=translations, using=using)
def _values(self, *fields): def _values(self, *fields, **expressions):
clone = self._clone() clone = self._clone()
if expressions:
clone = clone.annotate(**expressions)
clone._fields = fields clone._fields = fields
clone.query.set_values(fields) clone.query.set_values(fields)
return clone return clone
def values(self, *fields): def values(self, *fields, **expressions):
clone = self._values(*fields) fields += tuple(expressions)
clone = self._values(*fields, **expressions)
clone._iterable_class = ValuesIterable clone._iterable_class = ValuesIterable
return clone return clone
@ -697,7 +700,17 @@ class QuerySet(object):
if flat and len(fields) > 1: if flat and len(fields) > 1:
raise TypeError("'flat' is not valid when values_list is called with more than one field.") raise TypeError("'flat' is not valid when values_list is called with more than one field.")
clone = self._values(*fields) _fields = []
expressions = {}
for field in fields:
if hasattr(field, 'resolve_expression'):
field_id = str(id(field))
expressions[field_id] = field
_fields.append(field_id)
else:
_fields.append(field)
clone = self._values(*_fields, **expressions)
clone._iterable_class = FlatValuesListIterable if flat else ValuesListIterable clone._iterable_class = FlatValuesListIterable if flat else ValuesListIterable
return clone return clone

View File

@ -506,7 +506,7 @@ Examples (those after the first will only work on PostgreSQL)::
``values()`` ``values()``
~~~~~~~~~~~~ ~~~~~~~~~~~~
.. method:: values(*fields) .. method:: values(*fields, **expressions)
Returns a ``QuerySet`` that returns dictionaries, rather than model instances, Returns a ``QuerySet`` that returns dictionaries, rather than model instances,
when used as an iterable. when used as an iterable.
@ -538,6 +538,23 @@ Example::
>>> Blog.objects.values('id', 'name') >>> Blog.objects.values('id', 'name')
<QuerySet [{'id': 1, 'name': 'Beatles Blog'}]> <QuerySet [{'id': 1, 'name': 'Beatles Blog'}]>
The ``values()`` method also takes optional keyword arguments,
``**expressions``, which are passed through to :meth:`annotate`::
>>> from django.db.models.functions import Lower
>>> Blog.objects.values(lower_name=Lower('name'))
<QuerySet [{'lower_name': 'beatles blog'}]>
An aggregate within a ``values()`` clause is applied before other arguments
within the same ``values()`` clause. If you need to group by another value,
add it to an earlier ``values()`` clause instead. For example::
>>> from django.db.models import Count
>>> Blog.objects.values('author', entries=Count('entry'))
<QuerySet [{'author': 1, 'entries': 20}, {'author': 1, 'entries': 13}]>
>>> Blog.objects.values('author').annotate(entries=Count('entry'))
<QuerySet [{'author': 1, 'entries': 33}]>
A few subtleties that are worth mentioning: A few subtleties that are worth mentioning:
* If you have a field called ``foo`` that is a * If you have a field called ``foo`` that is a
@ -603,6 +620,10 @@ You can also refer to fields on related models with reverse relations through
pronounced if you include multiple such fields in your ``values()`` query, pronounced if you include multiple such fields in your ``values()`` query,
in which case all possible combinations will be returned. in which case all possible combinations will be returned.
.. versionchanged:: 1.11
Support for ``**expressions`` was added.
``values_list()`` ``values_list()``
~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~
@ -610,11 +631,14 @@ You can also refer to fields on related models with reverse relations through
This is similar to ``values()`` except that instead of returning dictionaries, This is similar to ``values()`` except that instead of returning dictionaries,
it returns tuples when iterated over. Each tuple contains the value from the it returns tuples when iterated over. Each tuple contains the value from the
respective field passed into the ``values_list()`` call — so the first item is respective field or expression passed into the ``values_list()`` call — so the
the first field, etc. For example:: first item is the first field, etc. For example::
>>> Entry.objects.values_list('id', 'headline') >>> Entry.objects.values_list('id', 'headline')
[(1, 'First entry'), ...] [(1, 'First entry'), ...]
>>> from django.db.models.functions import Lower
>>> Entry.objects.values_list('id', Lower('headline'))
[(1, 'first entry'), ...]
If you only pass in a single field, you can also pass in the ``flat`` If you only pass in a single field, you can also pass in the ``flat``
parameter. If ``True``, this will mean the returned results are single values, parameter. If ``True``, this will mean the returned results are single values,
@ -661,6 +685,10 @@ not having any author::
>>> Entry.objects.values_list('authors') >>> Entry.objects.values_list('authors')
[('Noam Chomsky',), ('George Orwell',), (None,)] [('Noam Chomsky',), ('George Orwell',), (None,)]
.. versionchanged:: 1.11
Support for expressions in ``*fields`` was added.
``dates()`` ``dates()``
~~~~~~~~~~~ ~~~~~~~~~~~

View File

@ -227,6 +227,9 @@ Models
to truncate :class:`~django.db.models.DateTimeField` to its time component to truncate :class:`~django.db.models.DateTimeField` to its time component
and exposed it through the :lookup:`time` lookup. and exposed it through the :lookup:`time` lookup.
* Added support for expressions in :meth:`.QuerySet.values` and
:meth:`~.QuerySet.values_list`.
Requests and Responses Requests and Responses
~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~

View File

@ -0,0 +1,64 @@
from __future__ import unicode_literals
from django.db.models.aggregates import Sum
from django.db.models.expressions import F
from django.test import TestCase
from .models import Company, Employee
class ValuesExpressionsTests(TestCase):
@classmethod
def setUpTestData(cls):
Company.objects.create(
name='Example Inc.', num_employees=2300, num_chairs=5,
ceo=Employee.objects.create(firstname='Joe', lastname='Smith', salary=10)
)
Company.objects.create(
name='Foobar Ltd.', num_employees=3, num_chairs=4,
ceo=Employee.objects.create(firstname='Frank', lastname='Meyer', salary=20)
)
Company.objects.create(
name='Test GmbH', num_employees=32, num_chairs=1,
ceo=Employee.objects.create(firstname='Max', lastname='Mustermann', salary=30)
)
def test_values_expression(self):
self.assertSequenceEqual(
Company.objects.values(salary=F('ceo__salary')),
[{'salary': 10}, {'salary': 20}, {'salary': 30}],
)
def test_values_expression_group_by(self):
# values() applies annotate() first, so values selected are grouped by
# id, not firstname.
Employee.objects.create(firstname='Joe', lastname='Jones', salary=2)
joes = Employee.objects.filter(firstname='Joe')
self.assertSequenceEqual(
joes.values('firstname', sum_salary=Sum('salary')).order_by('sum_salary'),
[{'firstname': 'Joe', 'sum_salary': 2}, {'firstname': 'Joe', 'sum_salary': 10}],
)
self.assertSequenceEqual(
joes.values('firstname').annotate(sum_salary=Sum('salary')),
[{'firstname': 'Joe', 'sum_salary': 12}]
)
def test_chained_values_with_expression(self):
Employee.objects.create(firstname='Joe', lastname='Jones', salary=2)
joes = Employee.objects.filter(firstname='Joe').values('firstname')
self.assertSequenceEqual(
joes.values('firstname', sum_salary=Sum('salary')),
[{'firstname': 'Joe', 'sum_salary': 12}]
)
self.assertSequenceEqual(
joes.values(sum_salary=Sum('salary')),
[{'sum_salary': 12}]
)
def test_values_list_expression(self):
companies = Company.objects.values_list('name', F('ceo__salary'))
self.assertSequenceEqual(companies, [('Example Inc.', 10), ('Foobar Ltd.', 20), ('Test GmbH', 30)])
def test_values_list_expression_flat(self):
companies = Company.objects.values_list(F('ceo__salary'), flat=True)
self.assertSequenceEqual(companies, (10, 20, 30))