summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIan Foote <python@ian.feete.org>2016-08-15 11:35:12 +1000
committerTim Graham <timograham@gmail.com>2016-08-18 16:05:15 -0400
commit39f35d4b9de223b72c67bb1d12e65669b4e1355b (patch)
tree945d6d64af708824d2b9f0fc552127b0c8d4f866
parentd4eefc7e2af0d93283ed1c03e0af0a482982b6f0 (diff)
Fixed #25871 -- Added expressions support to QuerySet.values().
-rw-r--r--django/db/models/query.py21
-rw-r--r--docs/ref/models/querysets.txt34
-rw-r--r--docs/releases/1.11.txt3
-rw-r--r--tests/expressions/test_queryset_values.py64
4 files changed, 115 insertions, 7 deletions
diff --git a/django/db/models/query.py b/django/db/models/query.py
index 1fe2f45870..7a2ef08ba2 100644
--- a/django/db/models/query.py
+++ b/django/db/models/query.py
@@ -678,14 +678,17 @@ class QuerySet(object):
using = self.db
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()
+ if expressions:
+ clone = clone.annotate(**expressions)
clone._fields = fields
clone.query.set_values(fields)
return clone
- def values(self, *fields):
- clone = self._values(*fields)
+ def values(self, *fields, **expressions):
+ fields += tuple(expressions)
+ clone = self._values(*fields, **expressions)
clone._iterable_class = ValuesIterable
return clone
@@ -697,7 +700,17 @@ class QuerySet(object):
if flat and len(fields) > 1:
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
return clone
diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt
index 8d2e3ebb3b..f11a09fb96 100644
--- a/docs/ref/models/querysets.txt
+++ b/docs/ref/models/querysets.txt
@@ -506,7 +506,7 @@ Examples (those after the first will only work on PostgreSQL)::
``values()``
~~~~~~~~~~~~
-.. method:: values(*fields)
+.. method:: values(*fields, **expressions)
Returns a ``QuerySet`` that returns dictionaries, rather than model instances,
when used as an iterable.
@@ -538,6 +538,23 @@ Example::
>>> Blog.objects.values('id', 'name')
<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:
* 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,
in which case all possible combinations will be returned.
+.. versionchanged:: 1.11
+
+ Support for ``**expressions`` was added.
+
``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,
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
-the first field, etc. For example::
+respective field or expression passed into the ``values_list()`` call — so the
+first item is the first field, etc. For example::
>>> Entry.objects.values_list('id', 'headline')
[(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``
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')
[('Noam Chomsky',), ('George Orwell',), (None,)]
+.. versionchanged:: 1.11
+
+ Support for expressions in ``*fields`` was added.
+
``dates()``
~~~~~~~~~~~
diff --git a/docs/releases/1.11.txt b/docs/releases/1.11.txt
index a09e38a3ae..c8fc03c2b6 100644
--- a/docs/releases/1.11.txt
+++ b/docs/releases/1.11.txt
@@ -227,6 +227,9 @@ Models
to truncate :class:`~django.db.models.DateTimeField` to its time component
and exposed it through the :lookup:`time` lookup.
+* Added support for expressions in :meth:`.QuerySet.values` and
+ :meth:`~.QuerySet.values_list`.
+
Requests and Responses
~~~~~~~~~~~~~~~~~~~~~~
diff --git a/tests/expressions/test_queryset_values.py b/tests/expressions/test_queryset_values.py
new file mode 100644
index 0000000000..f9d35b4385
--- /dev/null
+++ b/tests/expressions/test_queryset_values.py
@@ -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))