diff --git a/django/db/models/query.py b/django/db/models/query.py index 1fe2f458700..7a2ef08ba27 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 8d2e3ebb3b0..f11a09fb96e 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') +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')) + + +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')) + + >>> Blog.objects.values('author').annotate(entries=Count('entry')) + + 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 a09e38a3ae8..c8fc03c2b6a 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 00000000000..f9d35b4385e --- /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))