8000 Fixed #24649 -- Allowed using Avg aggregate on non-numeric field types. · ddriddle/django@2d76b61 · GitHub
[go: up one dir, main page]

Skip to content

Commit 2d76b61

Browse files
committed
Fixed #24649 -- Allowed using Avg aggregate on non-numeric field types.
1 parent 26996e2 commit 2d76b61

File tree

7 files changed

+46
-15
lines changed

7 files changed

+46
-15
lines changed

django/db/backends/base/features.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,9 @@ class BaseDatabaseFeatures(object):
160160
# Support for the DISTINCT ON clause
161161
can_distinct_on_fields = False
162162

163+
# Can the backend use an Avg aggregate on DurationField?
164+
can_avg_on_durationfield = True
165+
163166
# Does the backend decide to commit before SAVEPOINT statements
164167
# when autocommit is disabled? http://bugs.python.org/issue8145#msg109965
165168
autocommits_when_autocommit_is_off = False

django/db/backends/oracle/features.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
4040
uppercases_column_names = True
4141
# select for update with limit can be achieved on Oracle, but not with the current backend.
4242
supports_select_for_update_with_limit = False
43+
can_avg_on_durationfield = False # Pending implementation (#24699).
4344

4445
def introspected_boolean_field_type(self, field=None, created_separately=False):
4546
"""

django/db/models/aggregates.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,12 +75,8 @@ class Avg(Aggregate):
7575
name = 'Avg'
7676

7777
def __init__(self, expression, **extra):
78-
super(Avg, self).__init__(expression, output_field=FloatField(), **extra)
79-
80-
def convert_value(self, value, expression, connection, context):
81-
if value is None:
< 8000 code>82-
return value
83-
return float(value)
78+
output_field = extra.pop('output_field', FloatField())
79+
super(Avg, self).__init__(expression, output_field=output_field, **extra)
8480

8581

8682
class Count(Aggregate):

docs/ref/models/querysets.txt

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2802,12 +2802,19 @@ by the aggregate.
28022802
Avg
28032803
~~~
28042804

2805-
.. class:: Avg(expression, output_field=None, **extra)
2805+
.. class:: Avg(expression, output_field=FloatField(), **extra)
28062806

2807-
Returns the mean value of the given expression, which must be numeric.
2807+
Returns the mean value of the given expression, which must be numeric
2808+
unless you specify a different ``output_field``.
28082809

28092810
* Default alias: ``<field>__avg``
2810-
* Return type: ``float``
2811+
* Return type: ``float`` (or the type of whatever ``output_field`` is
2812+
specified)
2813+
2814+
.. versionchanged:: 1.9
2815+
2816+
The ``output_field`` parameter was added to allow aggregating over
2817+
non-numeric columns, such as ``DurationField``.
28112818

28122819
Count
28132820
~~~~~

docs/releases/1.9.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,10 @@ Models
200200
(such as :lookup:`exact`, :lookup:`gt`, :lookup:`lt`, etc.). For example:
201201
``Entry.objects.filter(pub_date__month__gt=6)``.
202202

203+
* You can specify the ``output_field`` parameter of the
204+
:class:`~django.db.models.Avg` aggregate in order to aggregate over
205+
non-numeric columns, such as ``DurationField``.
206+
203207
CSRF
204208
^^^^
205209

tests/aggregation/models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ def __str__(self):
1717
class Publisher(models.Model):
1818
name = models.CharField(max_length=255)
1919
num_awards = models.IntegerField()
20+
duration = models.DurationField(blank=True, null=True)
2021

2122
def __str__(self):
2223
return self.name

tests/aggregation/tests.py

1E80
Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@
77
from django.core.exceptions import FieldError
88
from django.db import connection
99
from django.db.models import (
10-
F, Aggregate, Avg, Count, DecimalField, FloatField, Func, IntegerField,
11-
Max, Min, Sum, Value,
10+
F, Aggregate, Avg, Count, DecimalField, DurationField, FloatField, Func,
11+
IntegerField, Max, Min, Sum, Value,
1212
)
13-
from django.test import TestCase, ignore_warnings
13+
from django.test import TestCase, ignore_warnings, skipUnlessDBFeature
1414
from django.test.utils import Approximate, CaptureQueriesContext
1515
from django.utils import six, timezone
1616
from django.utils.deprecation import RemovedInDjango20Warning
@@ -40,8 +40,8 @@ def setUpTestData(cls):
4040
cls.a8.friends.add(cls.a9)
4141
cls.a9.friends.add(cls.a8)
4242

43-
cls.p1 = Publisher.objects.create(name='Apress', num_awards=3)
44-
cls.p2 = Publisher.objects.create(name='Sams', num_awards=1)
43+
cls.p1 = Publisher.objects.create(name='Apress', num_awards=3, duration=datetime.timedelta(days=1))
44+
cls.p2 = Publisher.objects.create(name='Sams', num_awards=1, duration=datetime.timedelta(days=2))
4545
cls.p3 = Publisher.objects.create(name='Prentice Hall', num_awards=7)
4646
cls.p4 = Publisher.objects.create(name='Morgan Kaufmann', num_awards=9)
4747
cls.p5 = Publisher.objects.create(name="Jonno's House of Books", num_awards=0)
@@ -441,6 +441,13 @@ def test_aggregate_annotation(self):
441441
vals = Book.objects.annotate(num_authors=Count("authors__id")).aggregate(Avg("num_authors"))
442442
self.assertEqual(vals, {"num_authors__avg": Approximate(1.66, places=1)})
443443

444+
@skipUnlessDBFeature('can_avg_on_durationfield')
445+
def test_avg_duration_field(self):
446+
self.assertEqual(
447+
Publisher.objects.aggregate(Avg('duration', output_field=DurationField())),
448+
{'duration__avg': datetime.timedelta(1, 43200)} # 1.5 days
449+
)
450+
444451
def test_sum_distinct_aggregate(self):
445452
"""
446453
Sum on a distict() QuerySet should aggregate only the distinct items.
@@ -620,7 +627,14 @@ def test_more_aggregation(self):
620627
self.assertEqual(vals, {"rating__avg": 4.25})
621628

622629
def test_even_more_aggregate(self):
623-
publishers = Publisher.objects.annotate(earliest_book=Min("book__pubdate")).exclude(earliest_book=None).order_by("earliest_book").values()
630+
publishers = Publisher.objects.annotate(
631+
earliest_book=Min("book__pubdate"),
632+
).exclude(earliest_book=None).order_by("earliest_book").values(
633+
'earliest_book',
634+
'num_awards',
635+
'id',
636+
'name',
637+
)
624638
self.assertEqual(
625639
list(publishers), [
626640
{
@@ -836,6 +850,11 @@ def test_aggregation_expressions(self):
836850
self.assertEqual(a2, {'av_age': 37})
837851
self.assertEqual(a3, {'av_age': Approximate(37.4, places=1)})
838852

853+
def test_avg_decimal_field(self):
854+
v = Book.objects.filter(rating=4).aggregate(avg_price=(Avg('price')))['avg_price']
855+
self.assertIsInstance(v, float)
856+
self.assertEqual(v, Approximate(47.39, places=2))
857+
839858
def test_order_of_precedence(self):
840859
p1 = Book.objects.filter(rating=4).aggregate(avg_price=(Avg('price') + 2) * 3)
841860
self.assertEqual(p1, {'avg_price': Approximate(148.18, places=2)})

0 commit comments

Comments
 (0)
0