From 4b6dfe16226a81fea464ac5f77942f4d6ba266e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?E=CC=81tienne=20Beaule=CC=81?= Date: Tue, 4 Jun 2019 04:01:16 -0300 Subject: [PATCH] Fixed #30542 -- Fixed crash of numerical aggregations with filter. Filters in annotations crashed when used with numerical-type aggregations (i.e. Avg, StdDev, and Variance). This was caused as the source expressions no not necessarily have an output_field (such as the filter field), which lead to an AttributeError: 'WhereNode' object has no attribute output_field. Thanks to Chuan-Zheng Lee for the report. Regression in c690afb873cac8035a3cb3be7c597a5ff0e4b261 and two following commits. --- django/db/models/functions/mixins.py | 8 ++++---- docs/releases/2.2.3.txt | 4 +++- tests/aggregation/test_filter_argument.py | 15 ++++++++++++++- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/django/db/models/functions/mixins.py b/django/db/models/functions/mixins.py index 8486ddb005..636340f015 100644 --- a/django/db/models/functions/mixins.py +++ b/django/db/models/functions/mixins.py @@ -42,9 +42,9 @@ class FixDurationInputMixin: class NumericOutputFieldMixin: def _resolve_output_field(self): - source_expressions = self.get_source_expressions() - if any(isinstance(s.output_field, DecimalField) for s in source_expressions): + source_fields = self.get_source_fields() + if any(isinstance(s, DecimalField) for s in source_fields): return DecimalField() - if any(isinstance(s.output_field, IntegerField) for s in source_expressions): + if any(isinstance(s, IntegerField) for s in source_fields): return FloatField() - return super()._resolve_output_field() if source_expressions else FloatField() + return super()._resolve_output_field() if source_fields else FloatField() diff --git a/docs/releases/2.2.3.txt b/docs/releases/2.2.3.txt index 73dba08a83..a42e57c779 100644 --- a/docs/releases/2.2.3.txt +++ b/docs/releases/2.2.3.txt @@ -9,4 +9,6 @@ Django 2.2.3 fixes several bugs in 2.2.2. Bugfixes ======== -* ... +* Fixed a regression in Django 2.2 where :class:`~django.db.models.Avg`, + :class:`~django.db.models.StdDev`, and :class:`~django.db.models.Variance` + crash with ``filter`` argument (:ticket:`30542`). diff --git a/tests/aggregation/test_filter_argument.py b/tests/aggregation/test_filter_argument.py index 0339afcb51..0c8829efdf 100644 --- a/tests/aggregation/test_filter_argument.py +++ b/tests/aggregation/test_filter_argument.py @@ -1,8 +1,11 @@ import datetime from decimal import Decimal -from django.db.models import Case, Count, F, OuterRef, Q, Subquery, Sum, When +from django.db.models import ( + Avg, Case, Count, F, OuterRef, Q, StdDev, Subquery, Sum, Variance, When, +) from django.test import TestCase +from django.test.utils import Approximate from .models import Author, Book, Publisher @@ -40,6 +43,16 @@ class FilteredAggregateTests(TestCase): agg = Sum('age', filter=Q(name__startswith='test')) self.assertEqual(Author.objects.aggregate(age=agg)['age'], 200) + def test_filtered_numerical_aggregates(self): + for aggregate, expected_result in ( + (Avg, Approximate(66.7, 1)), + (StdDev, Approximate(24.9, 1)), + (Variance, Approximate(622.2, 1)), + ): + with self.subTest(aggregate=aggregate.__name__): + agg = aggregate('age', filter=Q(name__startswith='test')) + self.assertEqual(Author.objects.aggregate(age=agg)['age'], expected_result) + def test_double_filtered_aggregates(self): agg = Sum('age', filter=Q(Q(name='test2') & ~Q(name='test'))) self.assertEqual(Author.objects.aggregate(age=agg)['age'], 60)