diff --git a/django/db/models/functions/__init__.py b/django/db/models/functions/__init__.py index dd11dd1772513..0680200c7afb3 100644 --- a/django/db/models/functions/__init__.py +++ b/django/db/models/functions/__init__.py @@ -5,7 +5,9 @@ Now, Trunc, TruncDate, TruncDay, TruncHour, TruncMinute, TruncMonth, TruncQuarter, TruncSecond, TruncTime, TruncYear, ) -from .text import Concat, ConcatPair, Length, Lower, StrIndex, Substr, Upper +from .text import ( + Concat, ConcatPair, Length, Lower, Replace, StrIndex, Substr, Upper, +) from .window import ( CumeDist, DenseRank, FirstValue, Lag, LastValue, Lead, NthValue, Ntile, PercentRank, Rank, RowNumber, @@ -21,7 +23,8 @@ 'TruncMinute', 'TruncMonth', 'TruncQuarter', 'TruncSecond', 'TruncTime', 'TruncYear', # text - 'Concat', 'ConcatPair', 'Length', 'Lower', 'StrIndex', 'Substr', 'Upper', + 'Concat', 'ConcatPair', 'Length', 'Lower', 'Replace', 'StrIndex', 'Substr', + 'Upper', # window 'CumeDist', 'DenseRank', 'FirstValue', 'Lag', 'LastValue', 'Lead', 'NthValue', 'Ntile', 'PercentRank', 'Rank', 'RowNumber', diff --git a/django/db/models/functions/text.py b/django/db/models/functions/text.py index 4ec07be2dfc06..56a2b66ad5909 100644 --- a/django/db/models/functions/text.py +++ b/django/db/models/functions/text.py @@ -70,6 +70,13 @@ class Lower(Transform): lookup_name = 'lower' +class Replace(Func): + function = 'REPLACE' + + def __init__(self, expression, text, replacement=Value(''), **extra): + super().__init__(expression, text, replacement, **extra) + + class StrIndex(Func): """ Return a positive integer corresponding to the 1-indexed position of the diff --git a/docs/ref/models/database-functions.txt b/docs/ref/models/database-functions.txt index acb22249c58f2..db708a6ce0f75 100644 --- a/docs/ref/models/database-functions.txt +++ b/docs/ref/models/database-functions.txt @@ -751,6 +751,28 @@ Usage example:: >>> print(author.name_lower) margaret smith +``Replace`` +~~~~~~~~~~~ + +.. class:: Replace(expression, text, replacement=Value(''), **extra) + +.. versionadded:: 2.1 + +Replaces all occurrences of ``text`` with ``replacement`` in ``expression``. +The default replacement text is the empty string. The arguments to the function +are case-sensitive. + +Usage example:: + + >>> from django.db.models import Value + >>> from django.db.models.functions import Replace + >>> Author.objects.create(name='Margaret Johnson') + >>> Author.objects.create(name='Margaret Smith') + >>> Author.objects.update(name=Replace('name', Value('Margaret'), Value('Margareth'))) + 2 + >>> Author.objects.values('name') + + ``StrIndex`` ------------ diff --git a/docs/releases/2.1.txt b/docs/releases/2.1.txt index f2141dbb992bd..b979ba2224b04 100644 --- a/docs/releases/2.1.txt +++ b/docs/releases/2.1.txt @@ -169,6 +169,9 @@ Models * A ``BinaryField`` may now be set to ``editable=True`` if you wish to include it in model forms. +* The new :class:`~django.db.models.functions.Replace` database function + replaces strings in an expression. + Requests and Responses ~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/db_functions/test_replace.py b/tests/db_functions/test_replace.py new file mode 100644 index 0000000000000..91a1749d70d16 --- /dev/null +++ b/tests/db_functions/test_replace.py @@ -0,0 +1,55 @@ +from django.db.models import F, Value +from django.db.models.functions import Concat, Replace +from django.test import TestCase + +from .models import Author + + +class ReplaceTests(TestCase): + + @classmethod + def setUpTestData(cls): + Author.objects.create(name='George R. R. Martin') + Author.objects.create(name='J. R. R. Tolkien') + + def test_replace_with_empty_string(self): + qs = Author.objects.annotate( + without_middlename=Replace(F('name'), Value('R. R. '), Value('')), + ) + self.assertQuerysetEqual(qs, [ + ('George R. R. Martin', 'George Martin'), + ('J. R. R. Tolkien', 'J. Tolkien'), + ], transform=lambda x: (x.name, x.without_middlename), ordered=False) + + def test_case_sensitive(self): + qs = Author.objects.annotate(same_name=Replace(F('name'), Value('r. r.'), Value(''))) + self.assertQuerysetEqual(qs, [ + ('George R. R. Martin', 'George R. R. Martin'), + ('J. R. R. Tolkien', 'J. R. R. Tolkien'), + ], transform=lambda x: (x.name, x.same_name), ordered=False) + + def test_replace_expression(self): + qs = Author.objects.annotate(same_name=Replace( + Concat(Value('Author: '), F('name')), Value('Author: '), Value('')), + ) + self.assertQuerysetEqual(qs, [ + ('George R. R. Martin', 'George R. R. Martin'), + ('J. R. R. Tolkien', 'J. R. R. Tolkien'), + ], transform=lambda x: (x.name, x.same_name), ordered=False) + + def test_update(self): + Author.objects.update( + name=Replace(F('name'), Value('R. R. '), Value('')), + ) + self.assertQuerysetEqual(Author.objects.all(), [ + ('George Martin'), + ('J. Tolkien'), + ], transform=lambda x: x.name, ordered=False) + + def test_replace_with_default_arg(self): + # The default replacement is an empty string. + qs = Author.objects.annotate(same_name=Replace(F('name'), Value('R. R. '))) + self.assertQuerysetEqual(qs, [ + ('George R. R. Martin', 'George Martin'), + ('J. R. R. Tolkien', 'J. Tolkien'), + ], transform=lambda x: (x.name, x.same_name), ordered=False)