diff --git a/django/contrib/postgres/aggregates/general.py b/django/contrib/postgres/aggregates/general.py index 1dda69c449..6ff6727bd4 100644 --- a/django/contrib/postgres/aggregates/general.py +++ b/django/contrib/postgres/aggregates/general.py @@ -32,10 +32,11 @@ class BoolOr(Aggregate): class StringAgg(Aggregate): function = 'STRING_AGG' - template = "%(function)s(%(expressions)s, '%(delimiter)s')" + template = "%(function)s(%(distinct)s%(expressions)s, '%(delimiter)s')" - def __init__(self, expression, delimiter, **extra): - super(StringAgg, self).__init__(expression, delimiter=delimiter, **extra) + def __init__(self, expression, delimiter, distinct=False, **extra): + distinct = 'DISTINCT ' if distinct else '' + super(StringAgg, self).__init__(expression, delimiter=delimiter, distinct=distinct, **extra) def convert_value(self, value, expression, connection, context): if not value: diff --git a/docs/ref/contrib/postgres/aggregates.txt b/docs/ref/contrib/postgres/aggregates.txt index 943b0b9373..f9b7be0fd3 100644 --- a/docs/ref/contrib/postgres/aggregates.txt +++ b/docs/ref/contrib/postgres/aggregates.txt @@ -61,7 +61,7 @@ General-purpose aggregation functions ``StringAgg`` ------------- -.. class:: StringAgg(expression, delimiter) +.. class:: StringAgg(expression, delimiter, distinct=False) Returns the input values concatenated into a string, separated by the ``delimiter`` string. @@ -70,6 +70,13 @@ General-purpose aggregation functions Required argument. Needs to be a string. + .. attribute:: distinct + + .. versionadded:: 1.11 + + An optional boolean argument that determines if concatenated values + will be distinct. Defaults to ``False``. + Aggregate functions for statistics ================================== diff --git a/docs/releases/1.11.txt b/docs/releases/1.11.txt index a2a427e7a4..9c162b41c4 100644 --- a/docs/releases/1.11.txt +++ b/docs/releases/1.11.txt @@ -81,7 +81,9 @@ Minor features :mod:`django.contrib.postgres` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -* ... +* The new ``distinct`` argument for + :class:`~django.contrib.postgres.aggregates.StringAgg` determines if + concatenated values will be distinct. :mod:`django.contrib.redirects` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/postgres_tests/test_aggregates.py b/tests/postgres_tests/test_aggregates.py index 07e4ea0101..f6f48fdd61 100644 --- a/tests/postgres_tests/test_aggregates.py +++ b/tests/postgres_tests/test_aggregates.py @@ -111,6 +111,24 @@ class TestGeneralAggregate(PostgreSQLTestCase): self.assertEqual(values, {'stringagg': ''}) +class TestStringAggregateDistinct(PostgreSQLTestCase): + @classmethod + def setUpTestData(cls): + AggregateTestModel.objects.create(char_field='Foo') + AggregateTestModel.objects.create(char_field='Foo') + AggregateTestModel.objects.create(char_field='Bar') + + def test_string_agg_distinct_false(self): + values = AggregateTestModel.objects.aggregate(stringagg=StringAgg('char_field', delimiter=' ', distinct=False)) + self.assertEqual(values['stringagg'].count('Foo'), 2) + self.assertEqual(values['stringagg'].count('Bar'), 1) + + def test_string_agg_distinct_true(self): + values = AggregateTestModel.objects.aggregate(stringagg=StringAgg('char_field', delimiter=' ', distinct=True)) + self.assertEqual(values['stringagg'].count('Foo'), 1) + self.assertEqual(values['stringagg'].count('Bar'), 1) + + class TestStatisticsAggregate(PostgreSQLTestCase): @classmethod def setUpTestData(cls):